8934 Commits
Author SHA1 Message Date
Neil b5b727bddb feat(composer): restore compact branch picker UX (#21741)
* feat(composer): restore compact branch picker UX

* fix(composer): address picker review feedback
2026-09-19 19:05:14 -07:00
84d827a6ab fix(daemon): pause producers when stream backlogs grow (#20947)
* fix(daemon): pause producers when stream backlogs grow

* fix(daemon): reset stream backpressure on socket replacement

* docs(daemon): point retention audit at current reproducer

* test(daemon): validate stream retention audit outcomes

* fix(daemon): bound the stream producer stall and leave a visible gap

Stream backpressure pauses a session's PTY with no deadline: the only
un-pause comes from the consumer draining, so a half-open peer that stops
reading without closing freezes the shell for the rest of the session.

Arm a 60s watchdog on the false->true stream-pause transition (not on the
re-assertions refresh() makes for neighbouring sessions). On fire, mark the
session stall-released: it becomes keep-tail droppable, its backlog is
thinned behind a dataGap, and the producer runs again. The existing dataGap
path makes the renderer restore that pane from the daemon's snapshot, so the
user sees the terminal jump to current rather than sit frozen. The mark
clears once the session's last byte leaves the daemon, restoring ordinary
pausing. Nothing here reports a process exit - loss of contact with a
consumer is not evidence about the child.

Also enable TCP keepalive on the stream socket so a genuinely dead peer
closes and onStreamDisconnected clears the pause.

* test(daemon): put each casting SAFETY: directive on one line

`oxlint-disable-next-line` covers only the line directly after it, so a
rationale wrapped onto a second comment line suppressed nothing and the
casts failed the changed-code quality gate. Drop the remaining JSON.parse
cast for an annotated binding.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:51:59 -07:00
921882619e fix: retire closed editor models from the app shell (#21178)
* fix: retire closed editor models from the app shell

* test(editor): use checked Monaco attachment calls

* Preserve bounded editor view caches when retiring closed models

* docs(editor): describe batched model retirement

* fix(editor): preserve cleanup work across registry replacement

* fix(editor): build editor model URIs with the file scheme

Monaco keys its model registry by `uri.toString()`, and both
`@monaco-editor/react` (via the `path` prop) and the closed-tab disposal
path built that key with `Uri.parse`. On Windows a raw path such as
`C:\repo\a.ts` parses as scheme `c`, which fails the scheme gate in
`modelService._schemaShouldMaintainUndoRedoElements`, so closed-file undo
history was dropped for every file at any size — not only the large files
the tradeoff note covers.

Add `toEditorModelUri`, the one filesystem-path -> model-key function,
built on `Uri.file` so the result always carries the `file:` scheme and
re-parses to itself. Route model creation, disposal lookup and the
still-open ownership comparison through it so all three agree; a
divergence there would dispose a model an open editor is still editing.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:51:55 -07:00
NeilandXiro The Dev ee354a35d7 feat(agents): add OpenCode 2 beta support (#21418)
* feat(agents): add OpenCode 2 beta support

Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>

* fix(opencode2): support current plugin lifecycle and session storage

* fix(opencode2): preserve lifecycle ordering and full session capture

* test(opencode2): cover setup event bridge

* test(opencode2): cover setup event bridge

* test(browser): satisfy anti-slop naming check

* test(opencode2): cover live form lifecycle

* fix(relay): preserve OMP config directory selection

* test(opencode2): avoid assertions in bridge fixture

* fix(rebase): retain OMP resume and fresh launch behavior

* test: align upstream OMP resume expectations

* test(opencode2): verify rejected form closes waiting state

---------

Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>
2026-09-19 17:49:03 -07:00
403c0881e1 Bound AI Vault transcript record assembly before allocation (#20963)
* fix(ai-vault): bound incremental transcript record assembly

* fix(ai-vault): skip one oversized record instead of dropping the session

An agent transcript record over the 10 MiB budget threw out of the JSONL
fold, so the whole session vanished from Agent Session History and from
search. A 10 MiB base64 image or a runaway tool result is ordinary.

The reader now discards the offending record up to its newline and keeps
folding. The in-progress record always starts at `consumedThrough`, which
is what makes both its running size and the resume offset past a discarded
span exact; an unterminated oversized tail leaves the cursor at the
record's start so a still-growing record is re-read rather than guessed at.
Skips accumulate on the resume point keyed by start offset, and the scanner
reports them as a per-session `notice` so nothing is silently lost.

The budget itself is unchanged.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:47:56 -07:00
f87359cda6 fix(runtime): persist acknowledged terminal tab retirement (#21020)
* fix(runtime): persist acknowledged terminal tab retirement

* test(runtime): drain tab retirement fixture writes before teardown

* fix(runtime): explain a refused workspace terminal close

The Sleep-workspace path threw the raw refusal enum ("stale-terminal") as an
Error message, which reaches a CLI user verbatim and a Sleep toast via
describeSleepFailure. Map each refusal reason to a sentence instead.

Also pins two behaviours that had no coverage: the user-visible outcome of a
republished stale-terminal refusal on the web client (the caller cannot tell it
from a real close), and the one-call-per-close invariant that keeps a successor
terminal alive.

The bounded close retry was NOT implemented: notifier.closeTerminalTab carries
only a tab id, so a second call destroys whatever successor took that id.

* test(runtime): build refusal fixtures without type assertions

The changed-code quality gate rejects new `as` casts. Replace the
branded-outcome cast with refusedMobileSessionTabClose, and model the
wire-skew reason as a decoded host answer instead of `as never`.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:38:05 -07:00
db7b57b846 fix(claude): enforce history window quota while reading (#21021)
* fix(claude): enforce history window quota while reading

* test: repair history quota audit dependency and CI import

* fix(native-chat): record why restart reconciliation leaves work unconfirmed

Two silent paths hid the cause of an unconfirmed submission. The reconciler's
bare `continue` on an `unknown` outcome dropped the reason it already carried,
and the transcript read swallowed its error, collapsing an oversize file and a
genuine read failure into the same verdict.

Log both. No control flow changes.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:25:36 -07:00
a445abadd4 fix(browser): bound CDP output for stalled clients (#20949)
* fix(browser): bound CDP output for stalled clients

* fix(browser): log CDP outbound overflow before terminating the client

The outbound queue terminated the automation client silently on overflow, so
the client saw a socket close indistinguishable from a crash. Surface the cap
that tripped and the backlog held when it did.

The queue dropped its backlog before invoking onOverflow, so the counters were
already zero at the callback. Snapshot them first and pass them through.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:24:33 -07:00
4d82149fe5 fix(runtime): reject stale inventory after PTY lifecycle changes (#21014)
* fix(runtime): reject provider inventory across PTY lifecycle changes

* fix(runtime): canonicalize SSH inventory generation keys

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:24:29 -07:00
Neil b766f512ec fix(editor): extract diff first-change auto-scroll to a hook to unblock main (#21738) 2026-09-19 17:06:58 -07:00
OrcaWinandm4air b8f67a6266 Close workspace board when selecting sidebar worktree (#21737)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-19 16:54:56 -07:00
Neil 85a3ba6d42 fix(terminal): align CJK IME preedit spacing (#19367)
* fix(terminal): align IME preedit to terminal cell grid

* fix(terminal): preserve native shaping and reuse IME preedit on repaint

* fix(terminal): preserve native shaping with bounded IME spacing runs

* test(terminal): account for inline preedit subpixel rounding

* test(terminal): keep the IME grid fixture wide at every DPI

* chore: regenerate xterm patch after rebase

* test(terminal): remove IME assertion lint findings

* test(terminal): avoid reflective IME fixture access

* test(e2e): run IME renderer matrix with WebGL available

* fix(ci): restore editor line budget
2026-09-19 16:44:38 -07:00
Neil e4c7632db2 perf(terminal): skip kitty scans for plain PTY output (#21643)
* perf(terminal): skip kitty scans for plain output

* fix(terminal): keep the kitty scan fast path total for absent chunks

The new escape-byte fast path dereferences the chunk before the string
concatenation that used to coerce a nullish value, so an unchecked
caller now throws instead of no-opping. Normalize once at the top.

Also type the AgentTerminalPreview connect mock against the real preload
signature, which turns the stale bare-string replay fixture that tripped
this into a compile error.
2026-09-19 16:26:32 -07:00
Neil abd310e5a3 perf(terminal): skip background SGR scans without ESC (#21646)
* perf(terminal): skip background SGR scan without escapes

* perf(terminal): avoid duplicate renderer risk scans

* test(terminal): pin the carried renderer risk scan tail

The foreground renderer-risk scan splices the carried tail onto the
incoming chunk before classifying it, and nothing covered that ordering:
a pre-gate moved back above the concatenation would silently drop the
refresh for a background SGR split across ConPTY chunks.

Also pins the escape-free ASCII path and the shared global SGR pattern's
statelessness across calls, since the background hit returns mid-loop.
2026-09-19 16:25:58 -07:00
3e7da29767 feat(editor): add opt-in collapsed unchanged regions for file diffs (#11955)
* feat(editor): add opt-in collapsed unchanged regions for file diffs

The combined "View All Changes" diff already collapses unchanged lines into
expandable bands (DiffSectionBody sets Monaco's hideUnchangedRegions), but a
single-file diff opened from Source Control renders the whole file. Reviewing
one changed line in a long file means scrolling past everything else.

Adds a General > Editor setting, default off, that applies the same Monaco
option to the single-file diff viewer. Off keeps today's full-file rendering.

The option is always emitted rather than omitted when off: Monaco retains the
last applied value across an options update, so dropping the key would strand
an open diff in collapsed mode after the setting is turned back off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(settings): register collapse unchanged search entry

* fix(editor): keep diff viewer under line limit

* fix(editor): satisfy diff viewer line budget

---------

Co-authored-by: Dan Cieslak <dcieslak19973@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 15:53:28 -07:00
Neil 844e9df98f fix(editor): keep long file paths distinguishable (#21631)
* fix(editor): keep filename visible in long paths

* fix(review): keep diff filenames visible

* fix(review): prevent path metadata overlap
2026-09-19 15:43:45 -07:00
Jinjing 93d245e358 Sort dev builds by timestamp instead of semver (#21720)
Dev build base versions can move backwards when a branch is cut before
the latest main build. Their embedded timestamp is the authoritative
"newest" signal for the picker. For dedicated release repos, compare
publishedAt timestamps before falling back to semver comparison.
2026-09-19 15:15:23 -07:00
OrcaWinandm4air 9309350864 fix(chat): enforce legacy import byte budget during reading (#20976)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-19 14:53:31 -07:00
89acf1e1fa fix(plugins): release diagnostic logs after successful uninstall (#21185)
* fix(plugins): retire log owners after successful uninstall

* fix: address memory PR review regressions and withdraw false positives

* fix(plugins): fence stale activation after uninstall

* chore: allow durable plugin uninstall audit evidence

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 14:53:28 -07:00
2741bdad38 fix(session): remember closed and initialized runtime tabs (#21630)
* fix(session): persist defaultTerminalTabsAppliedByWorktreeId

Host persist snapshots wrote tabs but omitted this write-once map, and
full session replaces / hydration treated omission as "never applied".
Union the marker across persist and hydrate so default terminals are not
re-spawned on every launch or re-attach.

Fixes #18117

* fix(runtime): stop re-seeding a runtime-owned workspace the user emptied

Focusing a workspace owned by a remote runtime created a terminal every
time, and sometimes two.

The mirror could never record the closed-last-terminal state. A host
snapshot with no terminals produced `nextTerminalTabs === null`, which
`withWorktreeEntry` turns into a deleted key -- and a missing row is
exactly how every seeder spells "never initialized"
(initial-terminal.ts). Keep an explicit empty row instead, so the remote
path reads the same tombstone the local one already honours. A worktree
that never had a terminal still gets no row, because `sameTerminalTabs`
treats a missing row and an empty one as equal; removal frames and
synthesized unpublished frames keep deleting, since neither is evidence
the user emptied anything.

The duplicate had a second cause. `requestedInitialTerminal` was a `let`
inside the session-tabs subscription closure, so "one focus creates at
most one terminal" held only for as long as that closure lived. Its
effect re-runs whenever the environment, connection generation, pairing
revision, or session-ready flag settles -- all of which move during a
workspace switch -- so a second closure re-armed the flag while the first
create was still in flight. That is the asymmetry in the report: one
terminal when arriving from the landing screen, two when arriving from
another workspace. Latch the bootstrap per worktree in a module-scoped
set instead, modelled on web-runtime-wake-terminal-respawn.ts, released
when the create settles. The closure flag stays alongside it so a failed
create still does not retry on every later frame of the same
subscription.

Fixes STA-6173.

* fix(runtime): harden the runtime-owned initial-terminal bootstrap latch

Follow-up on the STA-6173 fix, addressing restore-time safety gaps found in
review.

- Decline the bootstrap on a synthesized unpublished frame
  (`UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH` at version 0). That frame is the
  runtime saying "ask me later", not a host with zero terminals; seeding on it
  can duplicate a pane the host is about to republish after a restart. This is
  the same "ask me later" frame the tombstone write already refuses to treat as
  the user emptying the workspace.
- Release the module-scoped bootstrap latch on worktree tracking teardown and
  environment teardown, mirroring web-runtime-wake-terminal-respawn. A create
  RPC that never settles during a disconnect would otherwise leave the
  per-worktree key set and suppress the next bootstrap after reconnect.

Tests:
- New per-worktree and per-environment latch-release cases.
- New "unpublished frame declines" bootstrap case.
- New hook-level regression pinning the second defect end to end: a forced
  active-subscription re-run while the first create is in flight seeds exactly
  one terminal (two on the pre-fix tree).

* fix(runtime): key the initial-terminal bootstrap latch per environment

Addresses review on the STA-6173 hardening.

- Key the bootstrap latch by (environment, worktree), not worktree alone. A
  worktree id is `repoId::path` with no host component, so the same id can be
  live on two paired runtimes at once. The latch was cleared wholesale on any
  environment teardown, so tearing down environment A released environment B's
  in-flight key and a fresh B subscription could seed a duplicate — the STA-6173
  defect through another door. Environment teardown now clears only its own
  keys; worktree teardown clears only that (environment, worktree).
- Hold the latch after a successful create until a mirrored `tabsByWorktree` row
  exists. The snapshot refresh the create awaits can resolve on an empty,
  unconfirmed frame that leaves no row; releasing then let a later effect re-run
  seed a second terminal. A failed create still releases for retry. The latch
  claim/create/release now lives in web-runtime-initial-terminal-bootstrap-dispatch.ts,
  keeping active-session-subscription within its line budget.

Tests (each mutation-tested against its own regression):
- Cross-environment: a create for env B in flight, env A torn down, a fresh B
  closure must still decline. Fails when env teardown sweeps all environments.
- Hold-until-row: a create that resolved without mirroring a row must not let an
  effect re-run seed again. Fails on unconditional release.

* fix(runtime): release the bootstrap latch when the create returns failed

createWebRuntimeSessionTerminal never throws: the operation catches RPC and
network failures and returns `{ status: 'failed' }`. The dispatch helper
released the latch only from `catch` (dead for that path) or once a
tabsByWorktree row existed (false after a failure), so a failed create left
the latch held and suppressed every later auto-seed for that environment's
worktree until teardown -- the opposite of its own doc comment.

Capture the outcome and release on a returned failure as well, so the next
focus can retry.

Regression: a create that resolves `{ status: 'failed' }` followed by an
effect re-run must create again. Fails on the previous release condition.

* fix(runtime): release a parked bootstrap on the mirror's next frame

The previous commit released the latch on a returned failure, but a create
that *succeeded* with no mirrored row yet had no release at all: the
row-conditional check was the only exit for the success path, so a host that
accepted the tab while the mirror never got a frame held the latch until
environment teardown and suppressed every later auto-seed for the worktree.

Give the latch two phases. `creating` blocks other closures while the RPC is
in flight. A success with no row is parked as `awaiting-mirror` instead of
held, and the next frame the mirror accepts for that worktree releases it --
that frame is the mirror's answer either way (a row now exists and the
predicate declines on its own, or the host genuinely has no terminal and a
retry is right). A create still in flight keeps its claim: releasing it on a
frame would reopen the re-armed-closure race the latch exists to close.

Also correct the closure-flag comment: `requestedInitialTerminal` is set only
after the dispatch resolves, so a thrown create never sets it and a later
frame may retry. The flag records that this subscription already owned a
create; it never described a failed one.

Regressions, each mutation-tested against its own term:
- success with no row, then the mirror's empty answer, then a fresh closure
  -> must create again (fails when success-with-no-row is held instead of
  parked, and when the subscription does not call the frame release)
- a mirror frame releases an awaiting-mirror claim but never a pending
  create (fails when the release ignores the phase)

* fix(runtime): let a failed create retry inside its own subscription

The returned-failure release freed the module latch but the caller still
latched its closure-local requestedInitialTerminal whenever the dispatch
reported it owned the create, including a create that returned
{ status: 'failed' }. A thrown failure never set it and retried on the
next frame; since every RPC and network failure is reported as a return,
the live path was the one that suppressed the whole subscription. The
dispatch now reports false for a failure whichever way it arrives.

* fix(runtime): close the second re-seed door on an emptied workspace

ensureWebRuntimeWorktreeTerminalAfterWake read tabsByWorktree through
`?? []`, and the only guard that could have seen presence was &&-gated on
the value that erased it, so it could never fire for a tombstone. It runs
on every activateAndRevealWorktree, not only after a wake, and a
tombstoned workspace routes exclusively here: the stream-frame path
returns at localTerminalCount === 0 before reaching its own guard. So the
workspace was re-seeded on every focus regardless of the mirror fix.

Two states shared one line and now do not. With no rows the workspace is
being seeded for the first time and the decision goes to
shouldAutoCreateInitialTerminal with presence read by Object.hasOwn; with
rows present the question is whether a woke workspace's chrome outlived
its PTYs, which the tombstone says nothing about. Both arms are pinned,
because reverting only the respawn arm broke no existing test.

* fix(runtime): stop the worktree-id re-key inventing a tombstone

canonicalizeTerminalSessionWorktreeId read the source row through `?? []`
and always wrote the target one, so a workspace with NO tabsByWorktree row
came out the other side with an explicit empty one. That is the
closed-last-terminal tombstone, and this PR's new activation-path reader
honours it, so the re-keyed workspace never gets its initial terminal.

Guarded on the source row's presence, which is the guard the sibling
keyed maps six lines below already use. Mutating the guard to test
emptiness instead of presence breaks the assertion that a real tombstone
still survives the re-key, which is the distinction that matters.

* fix(runtime): reconcile session tab bootstrap integration

* fix(runtime): preserve terminal wake launch options

* test(session): annotate cross-project persistence fixture

* test(session): remove obsolete typecheck suppression

* test(session): keep persistence regression in renderer project

* refactor(runtime): remove inert snapshot recovery wrapper

* fix(runtime): keep removal-frame import merge-safe

* fix(runtime): deduplicate merged removal-frame import

---------

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-19 14:30:25 -07:00
Neil f016d38e9c test(e2e): prove Pi EOF removes sidebar agent row (#21722) 2026-09-19 13:00:46 -07:00
Jinjing 3cd674fdff Make jump-palette selection state higher contrast (#21718)
- Increase light-mode contrast: foreground mix 12%→13%, ring 18%→19%
- Extend selectors for .jump-palette-command scope and add class
2026-09-19 12:13:42 -07:00
Neil b6b974cb82 fix(terminal): clear stale agent identity after shell exit (#21714)
* fix(terminal): clear stale agent identity after shell exit

* test(identity): update resolver decision table
2026-09-19 12:10:10 -07:00
Neil c4c9486470 fix(pi): do not install extensions for disabled agents (#21711)
* fix(pi): honor disabled agent extensions in PTY setup

* fix(pi): preserve fresh OMP config when disabled
2026-09-19 11:22:56 -07:00
Neil 97763edac0 fix(pi): ignore idle utility dialogs in completion status (#21707) 2026-09-19 10:47:06 -07:00
Neil b0cbb919ba fix(pi): use Pi configured provider for Source Control AI defaults (#21693)
* fix(pi): let Source Control AI use Pi configured default

When Orca runs Pi for automatic branch names or commit messages without an explicit model override, omit --model so Pi resolves its configured provider. Preserve explicit discovered model selection and add regression coverage.

* fix(pi): preserve discovered fallback for non-Pi agents

Keep the configured-default sentinel behavior limited to agents whose default is the explicit CLI sentinel. Other dynamic agents still fall back to the first discovered model when their static default is unavailable.

* test(pi): pin configured-default dry-run arguments

Prove Source Control AI does not render the Pi configured-default sentinel as a literal model argument, and assert explicit model flag pairing positionally.
2026-09-19 10:14:51 -07:00
Neil e8a7be4ce2 fix(omp): recover retired pane status with validated restart authority
Merged after fresh run 35448889017 passed all required checks, including static analysis, typecheck, package jobs, all test shards, changed E2E, Docker SSH E2E, and verify.
2026-09-19 08:05:33 -07:00
Neil e7da72c3d7 fix(omp): attach desktop and mobile images through file mentions
Merge fully verified: desktop/mobile focused suites, node and mobile typechecks, changed-code quality, hosted RPC recording pin, package checks, all test shards, and verify pass. This fixes #20389 across composer, drop, picker, and mobile clipboard-accessory paths.
2026-09-19 07:05:20 -07:00
Neil 3ba7cb4de9 feat(diagnostics): trace terminal startup delivery phases
Merge fully verified: all required CI checks pass. This lands bounded startup timing instrumentation for the open Windows OMP first-paint investigation in #19333; it does not claim the latency fix itself.
2026-09-19 06:58:31 -07:00
Brennan Benson 061a756b84 test(agent-status): pin that omp's approval_mode cannot hide a real prompt (#21499)
omp forwards its `approval_mode` on every `tool_approval_requested`, and the
shared normalizer deliberately ignores it. Nothing recorded why, so the field
reads like a dropped qualifier that a future change should start honouring.

It must not be honoured. Measured against omp 17.0.5: the CLI emits this event
only after its own policy engine already resolved the call to "prompt", and then
parks on a human Approve/Deny select. Auto-approved calls emit nothing at all.
`approval_mode` carries the ambient mode (always-ask | write | yolo), not the
verdict, so a per-tool `tools.approval.<tool>: prompt` produces a genuinely
blocked human carrying `yolo` -- the one value that looks auto-approving.

No behaviour change. Records the reason at the decision site, replaces two
fixtures that asserted an `approval_mode` of 'prompt' (not a member of omp's
enum) with captured values, and adds guards pinning that every real mode,
plus a missing or unrecognised one, stays blocked, and that pi is unaffected.
2026-09-19 06:12:04 -07:00
Neil b0ec11f5b0 fix(omp): redact credential references before status transport (#21673) 2026-09-19 05:58:09 -07:00
NeilandBrennan Benson 5cc89b6406 fix(sidebar): preserve OMP status across sparse split pane ids (#21671)
Use stable PTY-to-leaf bindings when in-session pane closes leave sparse runtime pane ids. Keep the existing parked and dense slot resolution paths, and cover the completed OMP pane/sibling-running case from #15557.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-09-19 05:57:48 -07:00
Neil bf250b1200 test(omp): prevent duplicate tab on worktree reselect (#21670) 2026-09-19 05:48:35 -07:00
Neil 2e278426ef fix(omp): sweep local agent PTYs during app quit (#21668) 2026-09-19 05:48:32 -07:00
Neil 5cf65d79fc fix(omp): ignore nested task session ownership (#21663) 2026-09-19 05:25:16 -07:00
Neil 6abd1ce53b fix(agents): refresh remote detection on new launch surfaces (#21659) 2026-09-19 05:12:42 -07:00
NeilandSudoAI-DEV ae9c06c941 feat(omp): discover and switch native-chat models on desktop and mobile (#20612)
* fix(omp): discover and switch native-chat models

Report the running OMP provider/model and discover available choices on
the execution host for desktop and mobile. Register an extension command
to switch through the OMP API because its TUI does not accept /model args.
Advertise that command in status so older hosts remain read-only.

Addresses the OMP portion of #17603; Pi chat enablement remains separate.
Model reporting begins on lifecycle activity; no startup status is invented.

Co-authored-by: SudoAI-DEV <220139811+SudoAI-DEV@users.noreply.github.com>

* refactor(omp): check generated model metadata types

* test(omp): verify model picker command and reported selection

* test(omp): add repeatable real model-switch runtime proof

* test(omp): require model capability delivery in runtime smoke

* fix(mobile): decode OMP model discovery through RPC operations

* fix(omp): preserve exact reported model selectors

* fix(omp): preserve generated extension syntax after rebase

* fix(omp): merge generated harness UI context types

* test(omp): model switching keeps one session manager

* test(omp): include transcript path in model status proof

* test(omp): avoid renderer error-type union

* test(omp): keep renderer test type explicit

---------

Co-authored-by: SudoAI-DEV <220139811+SudoAI-DEV@users.noreply.github.com>
2026-09-19 05:11:48 -07:00
Jinwoo Hong c22c442fdb feat(mobile): answer native verbs on the shell, clipboard first (OTA phase C, C2.4) (#21623)
* feat(mobile): declare the native verb table and advertise it (OTA phase C, C2.4)

The contract half of the shell-answered request seam: the `native.` prefix, a
typed table with params and result schemas per verb, and the two clipboard
verbs.

`MOBILE_WEB_SHELL_GRANTS` spreads the table's own name tuple rather than
restating it, so a verb cannot be advertised without a row and a row cannot
exist unadvertised — the table is `Record<BridgeNativeVerb, …>`, so a missing
row does not compile, and the suite holds the other direction. Verb names go
in the flat grant list on purpose: a route may declare one, and a shell that
lacks it keeps that route native rather than walling it.

The mime shape admits `image` because a later build will serve one; this one
refuses it, and the reason will say out of scope rather than unsupported,
since `expo-clipboard` implements the image calls.

No frame kind is added and no protocol version moves.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): answer native verbs on the shell and fence them from the desktop (OTA phase C, C2.4)

The host half of the seam. `forward()` is the one place a request reaches the
client, so the `native.` check sits there and nothing about ids, caps,
settlement or cancel moves: a native request takes a pending slot and settles
on the same frames as a forwarded one.

`readBridgeNativeVerbCall` is the whole decision, separate from the host so
the `ungranted` arm can be exercised at all — every page is offered every
verb this build implements, so through a real host that arm is unreachable
today and is the point of the check once a grant is per-route.

Refusals carry `native_verb_refused`, which the desktop's vocabulary does not
contain: an unlisted method comes back from `MOBILE_RPC_METHOD_ALLOWLIST` as
`forbidden`, so reusing that would make a leaked fence read as an ordinary
scope refusal. Every case in the host suite reads `client.requests` for the
same reason.

`_meta` is omitted from host-authored replies per the ruling, which required
making it optional on `RpcSuccess`/`RpcFailure`: the type required a field the
wire never has. `isRpcResponse` does not read it, `runtime-rpc-envelope`
already makes it optional on a failure, and nothing in this app reads it —
every occurrence is a fixture writing one. Zero other type errors resulted.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): restore the harness verb-type import and drop an unused one

Two leftovers from threading the native reply type through and then removing
it: the host harness lost its `BridgeNativeVerb` import, and the request
module kept a type import nothing uses. `tsc` and oxlint both failed on the
previous commit; this is the follow-up rather than an amend.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): typecheck the native verb suites and drop the dead reply type

Three leftovers the ratchet caught, none visible to `tsc -p tsconfig.json`,
which excludes test files:

- the fence suite read `frame.payload` off the whole `reply` union, and a
  chunked reply has no `payload`; it narrows on the field now
- the bridge hook's own suite builds its caller options inline and had no
  `serveNativeVerb`
- `BridgeHostAuthoredReply` became unused once `_meta` was optional, and an
  exported type nothing reads is the pattern round 2 of C2.3 flagged; the
  statement it carried already lives in the verb table's header

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): give the page a typed surface for the native verbs (OTA phase C, C2.4)

`useNativeVerbs` is the page's side, typed from the same table the host
serves, so a verb cannot be called with params the shell will refuse.

Each call goes out as an ordinary `request` and settles on the ordinary
frames; the method name is the whole difference. A verb the shell did not
grant is refused before a frame is sent, because a rejection after a round
trip and one that never left look identical to an `await` and only the first
costs an in-flight slot — `granted` is exposed so a caller can pick its own
fallback instead.

Results are parsed rather than trusted: the shell is a different build than
the page, and a result shape that moved should fail at the seam rather than
halfway through a screen reading a field that is not there.

No call site uses it yet; the two `Clipboard.setStringAsync` sites are the
consumer PR's.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): send native verbs from the module that owns the request port (OTA phase C, C2.4)

`use-native-verbs.ts` called `client.sendRequest` directly, which the
unvalidated-request-port boundary refuses: new code must send through an
`RpcOperation`, and nothing may be added to the inventory.

An `RpcOperation` is not available to this seam. Its `method` is typed
`RpcMethodName`, which is `keyof typeof RPC_PARAMS_BY_METHOD` from the
desktop's generated params catalog. Putting `native.clipboard.read` there
would declare that the desktop serves a method the whole fence exists to keep
off it.

So the send moves into `bridge-rpc-client.ts`, already listed as an owner of
the port — a module that implements the port rather than a call site picking
its own method and acceptance. `callNativeVerb` rides the same frame, id
space and in-flight cap as any request, and the page surface stays a thin
typed wrapper that reaches no raw port.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): fence native methods on subscribe, not only on request (OTA phase C, C2.4)

The fence sat in `forward()`, which is the one place a *request* reaches the
client. A `subscribe` reaches the same client by another door: a frame naming
`native.clipboard.read` opened a real stream on the desktop, and because
`client.requests` stayed empty the whole suite read as green over it.

Refused in `handleSubscribe` before the id is claimed, under the same
`native_verb_refused` code, so nothing about the frame reaches the desktop or
occupies a slot. Cancel needs no arm of its own: it can only settle an id
that was admitted, and none is.

The oracle is widened with it. Every case now reads the client's streams as
well as its requests, because the old one could not see this at all — an
absence that only ever looked at half the boundary.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): hold a native verb's answer to the result it declares (OTA phase C, C2.4)

The table names a result schema per verb and the host never applied it, so a
handler could answer `{ nonsense: 1 }` and the page's own parse would be the
first to notice — halfway through a screen, not at the seam.

Validated on the host and refused by name on a mismatch, which is what makes
the table's claim true on the side that serves it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep the native verb member from being a raw port (OTA phase C, C2.4)

`callNativeVerb(verb: string, params: unknown)` took any method, so
`callNativeVerb('worktree.list', …)` reached the desktop through the real
pair — a raw request port in the one module allowed to hold one, and invisible
to the inventory, whose scan counts `.sendRequest` shapes and not a bare
call inside the owner.

The parameter is typed `BridgeNativeVerb` now, which is the fence for every
caller the compiler can see, and the prefix is checked at runtime for one
that reached the member through a widened type. The compile-time half is
pinned by a `@ts-expect-error` the tests-typecheck ratchet holds: widening
the parameter back makes that directive unused and fails there.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): give every native verb refusal one typed error at the page (OTA phase C, C2.4)

Only the seam's own refusal carried `native_verb_refused`. A handler that
declined and a reply too large to send arrived as other categories with no
code at all, and the hook rethrew a bare `Error(message)` — so a caller
telling an out-of-scope mime from an unsendable clipboard had to read message
text, and those want different handling.

Three changes, one shape. The host re-raises a handler's failure under the
seam's code, keeping the handler's message because that is what says why.
`BridgeReplyUndeliverableError` carries its frame refusal as a code, so
`reply-too-large` survives to the page. The hook throws `NativeVerbError`
with a `reason` read off the code `reconstructBridgeError` already copies
onto the rejection, plus `ungranted` for the arm this side decides.

Removes the unreachable `ok: false` branch from the hook with it. The
narrowing it was doing moves into the client member, which now promises a
success or a rejection and nothing else.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): test the in-flight cap and cancel, not the duplicate-id branch (OTA phase C, C2.4)

The case named for the cap sent the same id twice, so what it exercised was
the already-in-flight check. It never held a second slot and would have
passed against a seam that took none.

It now fills the cap with distinct ids against a handler that never settles,
and asserts the one over it is refused with the cap's own message. A cancel
case goes with it: a native request cancelled before its handler settles
posts nothing afterwards, the way a forwarded one does not answer an
exchange the page has moved on from.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the native verb member's type with a directive, not a cast

The case proving a desktop method cannot go through `callNativeVerb` reached
the runtime guard with `as never`, which the casting gate refuses — and a
cast is the wrong tool anyway: it asserts past the very type the case exists
to pin.

`@ts-expect-error` instead, which the tests-typecheck ratchet holds: widening
the parameter back to `string` makes the directive unused and fails there.
The call still runs, so the runtime guard is exercised too.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): parse the shell's error code instead of reaching for it (OTA phase C, C2.4)

The anti-slop audit refuses `Reflect.get`: dynamic input is parsed into a
named shape before it is read. `code` is not a property of `Error` — it is
whatever `reconstructBridgeError` copied onto the rejection from the capture
— so a schema is the honest reader here, and it says what this takes without
asserting the rest away.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): name the id collision before the native fence on subscribe (OTA phase C, C2.4)

The fence ran before the already-in-flight check, so a `subscribe` naming a
`native.` method under a live request's id settled that request with the
fence's message. The page lost the request either way — the collision class
predates this PR — but it was told the wrong cause, which is the difference
between a bug it can see and one it cannot.

Collision first. The fence still runs before any slot is taken, so nothing
about the frame reaches the desktop.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): parse a native verb result once, inside the catch (OTA phase C, C2.4)

The result was parsed twice: by the send path against the table's schema, and
again at each caller against the concrete one. The second parse was dead, and
it sat outside `call`'s catch, so a shell answering a shape the page did not
expect would have escaped as a bare `ZodError` — the one shape this surface
promises not to throw.

`call` takes the verb's result schema and parses once, inside the catch, so
every failure leaves as a `NativeVerbError`. The params parse at the callers
goes with it; the host validates params and the page builds them typed.

Also moves the comment block documenting `onExternalLink` back above it,
which `serveNativeVerb` had landed in front of.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): give every native verb refusal its own code, and keep handler words on the device (OTA phase C, C2.4)

Two findings that had to land together. Six faults all arrived as
`native_verb_refused` and differed only in message text, which the hook's own
comment said nobody may switch on. And a handler's message crossed verbatim:
a clipboard read that failed after reading is free to put what it read in its
error, and the error frame is the only path out of this seam that is not a
declared result.

So each fault gets a code — unknown verb, ungranted, bad params, wrong
result, out of scope, handler failure, native-on-subscribe — and the three
paths that reached the page uncoded get one too: the in-flight cap, a
non-native method through a widened member, and host disposal. `reason` is
now drawn from a declared list with no `unknown` arm, asserted at the hook.

A handler's code crosses and its message does not; the shell logs the real
one. The out-of-scope mime stays distinguishable because the code carries it,
not the text.

`bridge-host.ts` crossed the line cap with this, so the serving half moves to
`bridge-host-native-verbs.ts` — read the call, serve it, hold the answer to
what the verb declares — leaving the host the frames around it. No cap was
disabled or raised.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): refuse unknown verb params, and floor an unknown code (OTA phase C, C2.4)

Two the bots caught, both about a shape one side does not know.

`z.object` strips unknown keys, so `{ mime, value, unexpected }` dispatched
as if the extra key had not been sent — and the page and the shell are
separate builds, so a param the shell silently ignores is the shape of a verb
that changed underneath a page. `z.strictObject` on the verb params and
results.

And the page passed any code through as `reason`, while its own doc and
`NATIVE_VERB_REASONS` promised a closed list; a shell newer than the page
would have fallen off the end of a caller's switch. Unrecognised codes floor
to `unreported`, `reason` is typed to the list, and the doc says what the list
actually is rather than the single code the per-arm ones replaced.

The flooring is tested by delivering the frame such a shell would send: this
build's host normalises an unknown code before it leaves, so the pair cannot
produce one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): serve no request before the page has asked for a session (OTA phase C, C2.4)

`serving` starts true so a page's first frames are not refused for arriving in
the same native batch as its `ready`, but nothing checked whether an `init`
had ever been sent. So a request from a document this host had told no caps,
no grants and no route was forwarded to the desktop, or served as a native
verb, while the notify path had refused exactly that since C0.

Gated on `initSent`, under the protocol's own `before-ready` name. Streams are
left alone: the finding names requests, and gating `subscribe` too is a wider
change than it asked for — worth its own decision, since the same hole is
there.

Fourteen host cases and five hook cases were relying on this: they open a
request without ever asking for a session, which no real page does. They take
a `ready` now, through a harness option, and the counts that read what the
host posted account for the `init` a session opens with.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): grant a page session what its route declared, not what the app can do (OTA phase C, C2.4)

`init.grants.native` handed every session the shell's whole capability set,
so a route declaring only `navigate` and `storage` was granted
`native.clipboard.read` as well. That was harmless while every grant was a
navigation or a write the page could make anyway. It stopped being harmless
the moment a verb reads something back, which is this PR.

The session is now granted the intersection of what this shell implements and
what the mounted route declared in `MOBILE_WEB_PAGE_ROUTES`, plus the
protocol's own `fault`. One list: `init` issues it and every grant check —
notify and native verb — reads the same one, so what a page is told it may do
and what it will be served cannot drift.

`MOBILE_WEB_SHELL_GRANTS` and `implementsGrant` are unchanged; the shell's
capability set is still the ceiling a route's list is drawn from.

User-mediated authorization is not attempted here and goes to C2.7 as an open
question.

`use-mobile-web-shell-session.ts` crossed the line cap with the extra field,
so the three effect workers that touch the network and the disk move to
`mobile-web-shell-session-effects.ts`, leaving the hook its reducer and
callbacks. No cap was disabled or raised.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): key the pre-handshake refusal on the session, and apply it to streams (OTA phase C, C2.4)

Two rulings, one mechanism.

The gate keyed on the host instance, and a host is rebuilt whenever the client
under it changes. The page does not know: the session id is the same, so it
neither re-handshakes nor hears that the shell was replaced. So a live page's
next request was refused, and would have been until reload — a regression, not
a safety gain, and not covered by the in-flight settling as delivery-unknown.
The host now inherits whether its session already handshook, which the hook
records when the page first asks.

And the rule is about the session rather than the frame kind, so `subscribe`
is gated with `request`: a stream opened before the handshake was the same
hole.

Fourteen stream cases were exercising a state the protocol forbids — they
subscribe without ever asking for a session, which no page does. Every one is
about caps, backpressure windows, acks, cancel, idempotency or arity; none
was testing anything through the hole itself. They complete the handshake
now, and the counts that read what the host posted account for the `init`.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): judge a cached fallback by its own routes and grants (OTA phase C, C2.4)

The newer manifest is read before the download is attempted, so its
`pageRoutes` and `routeGrants` are already on the session when the download
fails. Opening the cached generation then mounted an older page under a newer
bundle's grants: a cached route that never declared the clipboard would have
been granted it by a manifest it is not running.

The fallback now derives both from `cached.routes`, and applies that
generation's own render eligibility before mounting it — a route only the
newer bundle claims is not a route the cached page can serve.

This is the Phase D "grants across generations" item arriving early. Only the
grant side is fixed here; persisting a generation's grants with the
generation itself stays Phase D's.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): remount the shell on a route change, so its bridge cannot outlive it (OTA phase C, C2.4)

A host captures the grants its session was opened with, and the agent-history
route renders `MobileWebShellScreen` with a pathname derived from
`worktreeId` and no key. So changing worktree updated the screen in place:
the old bridge stayed mounted and kept authorising frames under the grants of
the route the page had already left.

Keyed on the route now, which makes the change a remount — the old bridge is
disposed in the commit, before it can read another frame, and the new session
starts with no grants until its own `init`. The worktree-list and embedded-
browser routes are keyed on the host id for the same reason; the hazard is
the same whenever a dynamic segment moves under a mounted shell.

The probe that catches this uses an empty dependency array on purpose: keyed
on the pathname it re-fires on a prop update and reads exactly like a
remount, which is the one thing it exists to tell apart.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(shared): let a manifest declare a native verb as a route grant (OTA phase C, C2.4)

`GRANT_NAME_PATTERN` was dotless, and the contract's own pin asserted a
dotted grant is refused. So no manifest the desktop can produce could declare
`native.clipboard.write` — and once grants are scoped per route, a verb no
route can name is a verb no route is ever granted. Every native verb was
unreachable for every route.

The grammar now admits the verb shape the table names: `native.` followed by
at least two lowercase dotted segments, which is `native.<domain>.<action>`.
A plain name wearing a dot is still refused, `native.navigate` included, so
the pin keeps its meaning.

Wire compatibility, checked rather than assumed: widening what a manifest
field may contain is a new optional value reaching readers that shipped
before it, and the phone's reader already tolerates one. Its route schema
bounds a grant's length and nothing else, deliberately — an unknown name is
not a parse failure that would refuse the whole bundle, it is a grant this
build does not implement, so `implementsGrant` drops it and the route stays
native. Both halves are now tested: an unknown verb leaves its route native
and grants nothing, and a known one reaches `init.grants.native`.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): split the session reducer suite by concern before main pushes it over the cap

Merged with main the reducer suite reaches 805 lines against a cap of 800 —
neither side alone crosses it, which is the case the lane rules warn about.

Split at a concern boundary rather than raised: the grant-facing cases (the
cached fallback's own routes, and a manifest verb reaching the session
grants) move to `mobile-web-shell-session-grants.test.ts`, and the fixtures
both suites drive the reducer with move to a shared module beside them, the
way the bridge host suites already share a harness.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-19 07:07:02 -04:00
latteandNeil bb2afe1792 fix(ai-vault): discover and parse Devin sessions on Windows (#21337)
* fix(ai-vault): discover and parse Devin sessions on Windows, restore workspace mapping

Devin sessions never appeared in the AI Vault on Windows, and parsed
nearly empty elsewhere:

- The transcripts root hardcoded the XDG layout
  (~/.local/share/devin/cli/transcripts), but Devin CLI writes under
  %APPDATA%/devin/cli/transcripts on Windows. The root is now
  platform-aware (APPDATA on win32, XDG_DATA_HOME elsewhere) for both
  local scans and win32 remote hosts, and APPDATA joins the scanner
  child's env allowlist so relocated AppData resolves.
- The parser read metadata.is_user_input / created_at / metrics, which
  real ATIF-v1.7 transcripts don't carry. It now also accepts the ATIF
  step shape (source, timestamp, step-level metrics/model_name,
  plain-string message) while keeping the legacy shape.
- ATIF transcripts carry no working_directory, so sessions couldn't
  group under a workspace. The sibling sessions.db index is now merged
  through the existing sidecar seam: it fills cwd/title/model/
  timestamps, honors the db's hidden flag, and re-merges on db-only
  changes without re-reading transcripts.

* fix(ai-vault): inline Devin transcripts root, harden parser/db edge cases

- Resolve the platform-aware Devin cli dir in agent-sources instead of
  importing the shared devin-cli-data-dir module, which is not part of
  this change (broke typecheck).
- Exclude source:'system' steps unconditionally, even when legacy
  metadata fields would classify them as user/assistant messages.
- Guard unix-seconds conversion against out-of-range values so a single
  bad sessions.db row cannot mark the whole index unreadable.

* fix(ai-vault): watch sessions.db-wal so live Devin metadata cannot go stale

In WAL mode, committed rows sit in sessions.db-wal while sessions.db
keeps its stat until checkpoint, so keying the dependency on the db
alone could serve a stale index. The dependency now observes the wal
when one exists; the reader still opens sessions.db itself.

* fix(ai-vault): probe sessions.db-wal through the WSL-gated stat

existsSync bypasses wslGatedStat and can hang a scan on a stalled 9P
mount; the fs-import guard forbids it in session-scanner modules. The
dependency path resolution is now async and probes through the gate.

* fix(ai-vault): honor zero metrics and array messages in Devin steps

- firstDevinMetricValue skipped explicit numeric zeros, letting a
  lower-priority positive metric win and overstating token totals.
- ATIF allows step.message as an array of content parts; route it
  through extractContentText so those steps still feed title/preview.

* test(ai-vault): cover array-valued ATIF message extraction

The extractDevinStepText fallback that routes an array-valued
step.message through extractContentText shipped without a fixture that
produces that shape, so a future refactor could silently drop the
branch. Pin that an array of text parts feeds the step's title and
preview.

* fix(ai-vault): invalidate old Devin caches and bound database retries

* Discover current Devin ATIF exports alongside legacy transcripts

* Recognize drawn geometry in the browser markup contract test

* Deduplicate Devin exports across transcript roots

* Account for the workspace sleep-state reader in scan budget

* Align OMP integration tests with recorded-path resume

* fix: update scan benchmarks and await relay environment test

---------

Co-authored-by: Neil <neil@stably.ai>
2026-09-19 03:32:12 -07:00
Neil ff934256ae fix(omp): retain recorded transcript paths when resuming (#20634)
Based on the resume-locator proposal in stablyai/orca#16276 by @CodeHourra. Retains UUID-based ownership and existing reattach behavior.
2026-09-19 02:02:50 -07:00
Neilandunknown 4dec64d533 feat(source-control-ai): support OMP generation (#20624)
* feat(source-control-ai): support OMP text generation

Read prompts on stdin, retain OMP configured model by default, and reuse JSON model discovery.

Co-authored-by: unknown <1784931579@qq.com>

* test(source-control-ai): cover OMP large input and model overrides

* fix(omp): keep configured model default out of discovered catalog

* fix(omp): hide config default from model discovery catalog

* fix(omp): separate terminal discovery from generation defaults

* test(omp): keep model probe import compatible with CLI typecheck

* test: align Source Control AI registry contracts with OMP

---------

Co-authored-by: unknown <1784931579@qq.com>
2026-09-19 01:54:50 -07:00
Neil ef03188956 fix(sidebar): retain remote OMP rows before tab hydration (#20616)
* fix(sidebar): retain remote OMP rows before tab hydration

* fix(sidebar): sweep host-retracted rows without hydrated tabs

* perf(sidebar): scope mirrored retractions to indexed host tabs

* fix(sidebar): scope status retractions to host identity

* fix(sidebar): retain host identity on mirrored statuses

* chore(ci): refresh validation against fixed main baseline

* fix: scope mirrored tab retractions to owning host panes

* fix(sidebar): preserve foreign pane metadata during host retraction

* fix(sidebar): retain remote workspace attribution after tab hydration
2026-09-19 01:49:50 -07:00
Neil 605a4ef868 fix(omp): start new tasks without auto-resuming old sessions (#20622)
* wip(omp): prove fresh settings overlay without redirecting storage

* fix(omp): guard fresh launches with execution-host settings

* fix(omp): preserve unmodelled shell launch commands

* test(omp): consolidate shell fixture path import

* preserve fresh OMP launch status

* test: cover preserved OMP launch status

* fix: recognize wrapped fresh OMP launches

* chore(ci): refresh validation against fixed main baseline

* fix(omp): recognize generated fresh launch guards across shells

* fix(omp): preserve draft status and clear prefill across Unix shells

* fix(omp): run cmd draft cleanup after either guard branch

* fix(omp): launch drafts safely with nounset enabled

* fix(omp): select draft shell without parser diagnostics

* test(omp): await relay environment augmentation
2026-09-19 01:42:14 -07:00
Neil 14cddfeda7 fix(codex): disable plugins in short-lived probes (#21617) 2026-09-19 01:31:08 -07:00
Neilandstevelliu ea02d90704 fix(omp): answer startup Kitty queries before renderer handoff (#20620)
* fix(omp): answer startup Kitty queries before renderer handoff

Forward actual renderer capability through local and remote spawn. Preserve source ranges and following keyboard mode pushes, and retain independent ConPTY color authority.

Refs #17081. Secondary review: #17082.

Co-authored-by: stevelliu <stevelliu@tencent.com>

* test(omp): cover fragmented keyboard modes and ConPTY handoff

* fix: preserve keyboard startup intent without terminal colors

* fix: negotiate keyboard support for host-authoritative agent launches

* fix: keep terminal creation within line budget

* fix(omp): negotiate keyboard support for paired web launches

* test: remove obsolete message type import after main integration

* fix: validate paired launch results and retry incomplete SSH test snapshots

* fix(omp): negotiate keyboard support for background paired launches

---------

Co-authored-by: stevelliu <stevelliu@tencent.com>
2026-09-19 01:29:16 -07:00
OrcaWin 54a19c2ba3 fix(pdf): render CJK text with pdf.js resources
Merged after PR-specific checks passed. CI failures are unrelated baseline findings in ClientHostedBrowserPagePane.markup.test.tsx and pane-title-update-global-scan-budget.test.tsx.
2026-09-19 01:28:09 -07:00
Neil db478bd7d8 fix(omp): keep task storage aligned with the login shell
Verified across macOS, Windows, Linux, local shell/config roots, SSH E2E, changed E2E, package, typecheck, static analysis, and Pullfrog.
2026-09-19 01:17:57 -07:00
Kien LeandKien Le 3af09f824b fix(sidebar): keep group headers visible when measured rows grow (#20883)
Co-authored-by: Kien Le <122910950+kien-ship-it@users.noreply.github.com>
2026-09-19 01:12:47 -07:00
Jinjing a37282bb1d feat(diff-comments): add multi-line range selection with drag and keyboard (#20959)
* feat(diff-comments): add multi-line range selection with drag and keyboa

Enable users to select a range of lines for diff comments by dragging in the gutter, with auto-scroll and visual feedback. Add keyboard shortcut (Mod+Shift+A) to open a comment on the editor selection. Extract line-range logic into testable utilities and add comprehensive test coverage including E2E scenarios.

* feat(diff-comments): handle upward drags and gutter-only presses

- Preserve anchor/focus directionality in selections to handle upward drags correctly
- Restrict gutter presses to line numbers only, leaving fold chevrons to Monaco
- Prevent add-note chord from opening composer while a gutter drag is in progress

* fix diff note range annotation test

* fix(diff-comments): capture release coordinate missed by pointermove

Pointerup can arrive at a position no pointermove event reported, causing fast drags to lose their final lines if the release landed before the animation frame fired. Capture the release coordinate and resolve it before committing the range.

Also: extract resolveFocusLine for clarity, improve test harness to track pointer position, add polling to gutter detection for rendering delays.
2026-09-19 01:04:21 -07:00
Yoann TERUEL 0e1a5f39d0 fix(browser): enable screenshot markup on client-hosted pages
Verified with focused Vitest, web typecheck, changed-file quality/Oxlint, and paired Electron headless E2E.
2026-09-19 01:01:56 -07:00