Commit Graph
1784 Commits
Author SHA1 Message Date
Aldin Saracevic 634478c620 fix(jira): shape user-typed create fields and seed reporter with viewer
- Jira rejects a bare string for reporter/user-picker fields on issue
  create, so shape customFields values into {accountId}/{name} objects
  for keys the caller flags via userFieldKeys.
- Seed required user fields with the authenticated viewer by default
  and add a searchable user picker (jira.searchUsers) so users aren't
  forced into free text for reporter/custom user fields.
2026-08-30 18:55:40 -07:00
a0d36f5290 fix(agents): keep OMP identity and forward ask/approval events (#15713)
* fix(agents): keep OMP identity and forward ask/approval events (STA-4130)

A live OMP pane was re-owned as Pi because the generic pi-compatible
fallback always won, ask events blocked without a question payload, and
OMP suppressed tool_approval_* unless an extension registered handlers.

Mark Pi as the title-group fallback so a specific OMP identity is not
downgraded, publish OMP ask input as the existing questions envelope, and
forward tool_approval_requested/resolved onto blocked/working.

STA-4130
Related to #14278

Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>

* fix(agents): keep launch Pi ownership over OMP wrapper frames (STA-4130)

The pi-compatible fallback treated every generic Pi owner as inferred, so an
explicit launch-Pi pane (and a launchless Pi pane with an OMP-shaped title)
was re-owned as OMP. Launch provenance now stays authoritative; only an
inferred status-frame owner yields to a specific sibling, and same-group
titles no longer count as reuse.

STA-4130

* fix(agents): drop Pi wrapper idle titles while OMP hook is active (STA-4130)

Title-completion suppression compared pick-a-winner ownership, so a Pi ready
frame looked like a different agent than a live OMP hook and fired a spurious
task-complete notification. Reuse checks now use the title-identity group.

STA-4130

* fix(agents): restore OMP approval forwarding after merge

* fix(agents): restore title-owner API after merge

* test(agents): update identity inventory ratchet

---------

Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>
Co-authored-by: Merge Sim <sim@local>
2026-08-30 18:51:24 -07:00
Brennan BensonandMerge Sim e54cfc1901 fix(omp): read Pi/OMP static state-title markers and retire stale spinners (#14602)
* fix(omp): read Pi/OMP static state-title markers and retire stale spinners

OMP 17.2.12 replaced its animated braille title frames with static markers
on WSL/ConPTY (`π : working`, `π > idle`, `π ! needs input`). Orca read all
three as idle, so a working OMP pane lost its status, and a synthetic title
spinner started by an earlier hook kept rotating after its status row was
gone.

Classify the markers from one shared table so a later upstream punctuation
change is a row, not a reparse, and stop the spinner when the hook row it
stands in for is cleared or dismissed.

Fixes #13890

* test(omp): preserve static state titles during normalization

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-30 18:50:56 -07:00
OrcaWinandm4air b1f5d2dd2a fix(agents): preserve manual mode for newly added defaults (#17515)
* fix(agents): preserve manual mode for newly added defaults

* test: handle optional migrated settings fields

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-30 18:38:25 -07:00
Neil 1f20a53d22 Fix duplicate Codex startup command echo
Deliver local POSIX Codex startup commands through the shell wrapper at shell initialization, preventing duplicate PTY echo.
2026-08-30 18:13:36 -07:00
Brennan Benson cc6b600e21 Fix orchestration CLI recovery, settled-Dispatch mail, and guide defects (#16919)
* Fix orchestration CLI recovery, settled-Dispatch mail, and guide defects

Five reported orchestration CLI defects, verified individually before fixing.
Two were real code defects, one was a docs error, one was correct as-is, and
one was correct on both ends except for its recovery wording.

- Mail addressed to a settled `dispatch:<id>` was accepted and silently dropped.
  Local sends bypassed the settlement check the federated branch already had, so
  the caller was told success for a delivery no worker would ever read. Reject
  with `dispatch_inactive` and name the Run mailbox to use instead.

- A lost mutation response offered no read-only way to ask whether it took
  effect. `--retry-request` does dedupe correctly, but the recovery guidance
  emitted a query command only when the payload carried a dispatch id, which is
  exactly what a lost response lacks. Add read-only
  `orca orchestration request-show --request <id>` over the durable receipt
  ledger, and always emit a read-only step before the keyed retry.

- The bundled `orca-cli` guide documented `check --unread --inject`, a flag the
  parser rejects. Correct it to `--format` and add a ratchet that runs every
  orchestration invocation in the bundled guides through the real CLI parser.

- `check --json` is one stdout document and its keepalives are stderr-only; the
  reported `Extra data: line 2` came from merging the streams. Document the
  contract rather than changing the wire.

- A rejected lifecycle message is loud on both ends already, but the rejection
  never named the flag that supplies the missing capability. Name it.

* Harden orchestration mutation recovery guidance
2026-08-30 18:12:58 -07:00
Neil 0569c3d633 perf(terminal): fast-path chunks without agent status markers (#17514) 2026-08-30 17:59:06 -07:00
Brennan BensonandMerge Sim f2e9ba453c fix(agent-hooks): route reminted pane keys to canonical identity (STA-3993) (#15714)
* fix(agent-hooks): route reminted pane keys to canonical identity (STA-3993)

Spawn was stripping $$<base32>:L$$ ORCA_PANE_KEY values (and the launch
token) instead of rewriting them to the metadata-proven tab:leaf key, so
OMP hooks never entered last-status.json and sleeping rows stayed working.

Alias that exact remint form onto the canonical pane so later posts still
route, and keep unmatched tokens from stamping another pane.

* fix(agent-hooks): keep reminted pane-key aliases first-pane-wins

Remint tokens have no embedded tab identity, so a later spawn that reused
the same $$ token with a different tab/leaf was overwriting the alias and
routing leftover hook posts onto the new pane. Refuse destination changes
for that form while still allowing same-pane pty id updates.

* fix(agent-hooks): keep pane alias limit import valid after refactor

* fix(agent-hooks): bound pane alias destination keys

* fix(ssh): keep pane identity env stripped when hooks disabled

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-30 17:54:13 -07:00
Neil 879fdfdac6 fix(cli): resolve WSL mounted-drive worktree paths 2026-08-30 16:36:20 -07:00
Brennan Benson f23d0b166f fix(relay): mint PTY ids that carry the relay incarnation instead of a restarting counter (#16901)
* fix(relay): scope PTY ids to mint epochs

* test(relay): treat minted PTY ids as opaque

* test(relay): pin mint-epoch id shape and restore spawn-sequence assertions

The epoch escaping had no test: dropping encodeURIComponent left the whole
relay suite green. Pin the three-field id shape against an epoch that carries
both separators, and cover a colon-bearing relay id through the unchanged
app-side SSH id wrapper.

subprocess.test.ts had traded `pty-1`/`pty-2` for `expect.any(String)`, which
discarded the invariant those two cases exist to prove: an early node-pty load
failure burns no sequence, a late spawn failure burns one.

* test(relay): mirror production epoch escaping in testPtyId

The harness built the expected id without the encodeURIComponent production
applies at the mint site. A test epoch carrying a reserved character would
diverge silently across ~40 assertions in 11 files.
2026-08-30 14:49:18 -07:00
Neil 5bd66bac8b fix(cli): resolve a WSL worktree by the Linux path its own shell prints (#16628) (#17440)
On a Windows host the runtime stores a WSL worktree as the UNC path Windows
sees, but a user inside the distro types the Linux spelling, so every `path:`
selector missed: `worktree show`, `terminal list --worktree` and
`worktree rm --worktree` all reported selector_not_found for a directory Orca
manages.

Translate once in the CLI, which is the only side that can prove which distro
the typed path belongs to — from its own UNC cwd, never from WSL_DISTRO_NAME,
which a Linux-native CLI also sets. The runtime's `path:` branch stays
exact-spelling-only for the same reason: this resolver feeds delete, so a
tail-only match would remove another distro's copy.
2026-08-30 14:42:20 -07:00
Neil 1268fb56f1 fix(worktree): complete a create Git can confirm but cannot list (#17388)
* fix(worktree): complete a create Git can confirm but cannot list

`worktree.create` verified against `listWorktrees`, which softens every git
failure to `[]`. Any listing failure therefore failed a create whose worktree
and branch `git worktree add` had already written, orphaning both, and reported
only 'Worktree created but not found in listing' — the real cause reached the
main-process console and never the user.

Verify against the error-propagating listing instead, and when that fails or
omits the row, rebuild the row by asking Git about the worktree itself. The
direct read returns nothing unless Git resolves the path into this repo's
object store with the expected branch checked out, so an unrelated or half-made
checkout still fails the create.

Fixes #16520

* fix(worktree): authorize a recovered create and reject an unreadable HEAD

Review follow-ups on the create-verification fallback:
- register the recovered worktree's own root, additively, so the create the
  user just made is not rejected by filesystem/git-status IPC
- treat an unreadable HEAD as no recovery instead of a blank OID
- keep the direct read's failure when the listing merely omitted the row
- skip the symlink cases on Windows and reset the new harness mock

* fix(worktree): bound the create-recovery disk read and keep WSL paths case-sensitive

Readiness-scan follow-ups:
- deadline the filesystem common-dir read; a .git on a hung mount left the whole
  create IPC pending where it used to fail after the Git deadline
- offer no disk candidate for a bare repo instead of a fabricated <repo>/.git
- compare POSIX common dirs case-sensitively, so two WSL repos differing only in
  case are not accepted as one object store on a Windows desktop
- move toGitOutputSpace to shared/wsl-paths as toWslExecutionSpace, next to the
  parseWslUncPath callers that already open-code it

* fix(worktree): share one budget for create verification and keep recovered roots

Three follow-ups from review of the create-recovery path:

- The recovery no longer starts a fresh 30s deadline after the listing already
  burned one, so worst-case create verification stays at ~30s instead of ~60s.
  A 5s floor keeps the direct read a chance to answer when the listing spent
  the whole budget.
- rebuildAuthorizedRootsCache now carries a repo's previously registered roots
  forward when its listing throws. A rebuild running while Git is still broken
  could otherwise un-authorize the worktree a create just recovered.
- Corrected the scan-cache doc comment: it claimed strict and lenient listings
  coalesce, but the cache key includes the runner name precisely to keep them
  apart, so a strict joiner can never inherit a lenient scan's softened [].

Each change has a negative control: reverting the hunk fails exactly its own
test and nothing else.

* fix(worktree): keep a recovered worktree authorized across roots-cache rebuilds

The previous approach registered a recovered create into the same per-repo set
the rebuild recomputes from `git worktree list`. That set is derived from the
very listing that failed, so a rebuild would re-deny the worktree — either by
overlapping the registration, or by simply listing again and omitting the row.
Carrying old roots forward on a thrown listing did not cover either case.

Recovered roots now live in their own additive layer that rebuilds union in
rather than replace. The layer is retired on evidence, not on a timer:

- the listing can see the worktree again (Git recovered), or
- the listing succeeded and the directory is gone (worktree removed).

A repo whose listing threw is left untouched, because a dead mount fails both
the listing and the stat, and treating that as "removed" would revoke the
worktree in exactly the outage this layer exists for. The layer is capped so it
cannot grow unbounded, and survives cache invalidation deliberately: repo
mutations are frequent and would otherwise re-deny a recovered worktree.

Three tests cover the healthy-rebuild-omits-the-row case, the in-flight rebuild
race, and retirement once the listing sees it again. Removing the union fails
exactly the two keep-tests and nothing else.

* perf(worktree): only read the repo's .git from disk when Git's own answer disagrees

The disk read is a second opinion on Git's reading of the common dir, but it ran
unconditionally as part of the same Promise.all. A deadline bounds the IPC, not
the syscall: Promise.race cannot cancel an in-flight fs operation, and a `.git`
on a hung mount (dead NFS/SSHFS, stalled WSL 9p) pins a libuv threadpool thread
that no timeout can reclaim. AbortSignal would not help either — fsPromises.stat
takes no signal, and a blocked syscall is not interruptible from userland.

So stop paying it on the happy path: read from disk only when Git's own reading
did not already confirm the common dir. Same accept/reject outcome, but the
threadpool exposure now requires both a failed listing and Git disagreeing about
the repo, instead of every recovered create.

* fix(worktree): compare the disk common-dir witness in Git's execution space

Exercising the fix on a real Windows host against WSL Ubuntu-24.04 found the
filesystem second opinion is inert there. Node reads `.git` in the caller's
space and answers `\\wsl.localhost\<Distro>\home\...\.git`, while Git-in-the-
distro answers `/home/...`. isSameCommonDirPath refuses to compare a POSIX path
against a Windows one, and canonicalizeLocalPath cannot bridge them because
realpath on a Linux path from a Windows process is ENOENT.

So the candidate could never match, and the one case that depends on this
witness alone — a symlinked repo root on the Git 2.25 fallback — declined a
worktree Git had already confirmed. Run the disk result through
toWslExecutionSpace, the same translation readRepoLocation already uses.

This is a false reject, not a false accept: it made recovery give up, never
adopt the wrong repo. Verified on awin; the modern --path-format=absolute
branch was unaffected because Git answers both sides itself there.

* fix(worktree): retire a recovered root only on proof, never on a stalled probe

The prune ran an unbounded stat and read every failure as removal. Two consequences, both in
the outage the recovered layer exists for: a hung mount stalled the rebuild that gates
filesystem auth, and a transient EACCES/EIO revoked a live worktree. The listingFailed guard
did not cover either, because listWorktrees softens Git failures to [] and never throws.

Prune now retires on definitive ENOENT only, probes in parallel under a deadline, and treats a
stall as inconclusive. The capacity bound refuses a new root instead of evicting an authorized
one, so an over-cap create is merely unauthorized rather than a live worktree being revoked.
2026-08-30 14:42:07 -07:00
Brennan BensonandMerge Sim b5a85890ac perf(git): bound git subprocess execution with an atomic admission scheduler (#16874)
* perf(git): bound git subprocess execution with an atomic admission scheduler

Field traces (#16038, #11363) show Windows freeze storms driven by unbounded
concurrent git children (12+ at once, 50-65s status convoys for 25+ minutes).
Admit every main-process git child against atomic per-budget base+headroom
counters (general / network / per-route), with reserved interactive capacity,
ordering-only aging, close-bound permit release, a 120s fail-safe read timeout
that feeds scheduler backoff, tier plumbing through every option carrier, and
coalesced+jittered visibility pollers. Killswitch: ORCA_GIT_ADMISSION_DISABLED=1.

Storm harness A/B: max concurrent children 65 -> 6, interactive p95 791ms -> 88ms;
output-parity battery byte-identical with admission on vs off.

* test(git): run the admission output-parity battery on every platform

Parity needs real git, not the storm harness's PATH stub, so it must not share
that file's POSIX gate - Windows is the platform where parity evidence matters.

* fix(git): preserve interactive admission invariants

* perf(git): keep admission queue drains linear

* fix(git): close final admission gaps

* perf(git): bound eligible route selection

* fix(merge): remove unrelated stale snapshot changes

* fix(git): preserve refresh lifecycle authority

* test(git): align admission lifetime contracts

* fix(git): harden admission across runtime paths

* fix(git): restore freshness for bulk status reads

* test(git): repoint delete-dialog source pins after admission plumbing

The hydration effect now orders its targets through
orderDeleteWorktreeStatusHydrationTargets and passes includeLineStats
alongside the abort signal, so both literal anchors stopped matching.
The invariants are unchanged and still pinned: dropping the signal, the
main-worktree/folder filter, or getState-instead-of-subscribe each
still reddens this test.

* Fix git admission tier propagation and lock ordering

Decode optional Git status tiers permissively and default runtime RPC status reads to the status lane while preserving renderer caller intent.

Acquire the FETCH_HEAD mutex before atomic admission so same-repository fetch waiters hold no global or route permits.

Preserve automatic pull-request refresh reasons, keep explicit hosted-review refreshes interactive, remove the dead candidate tier, and keep relay scheduling unchanged.

Use tier-aware status lease keys because a shared lease cannot be safely promoted after its admission request is queued or granted.

* test: align expectations with admission plumbing

* refactor(child-process): move the process contract types to process-spec

run-process.ts crossed its line cap after gaining the termination observer;
the public types and defaults move out with re-exports so no caller changes.

* chore: restore pnpm-lock.yaml to main (unintended local drift)

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-30 14:19:05 -07:00
Brennan BensonandMerge Sim c3aceacc7b Fix PR unlink for auto-detected reviews (#16898)
* fix: make PR unlink hide auto-detected reviews

* Type the empty-content test double against the real model

The literal narrowed suppressedGitHubPR to number and typed the callback
as Mock, so neither direction was comparable and tsconfig.tc.web.json
failed on TS2352. Keeping the 'as' cast preserves checking of the fields
the double does supply.

* Add localization keys for the unlinked checks-panel state

The unlinked title, relink action, and the remote-runtime upgrade notice
introduced untranslated keys that static analysis requires in en.json.

* Advertise PR suppression capability in the transport test

The client capability list is pinned by websocket-transport.test.ts, and
adding WORKTREE_GITHUB_PR_SUPPRESSION left the expected list stale.

* Fix stale PR suppression in Checks

* fix: harden PR unlink suppression state

* refactor: extract PR unlink state handling

* fix: show PR relink recovery in source control

* fix: add unlinked PR localization

* Clarify workspace-scoped PR unlinking

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-30 12:24:51 -07:00
Neil 3ab9766e38 perf(worktree): prepare checkouts while the composer is open
Squashed merge of PR #17290.
2026-08-30 12:12:04 -07:00
Neil 70df6f0224 fix(terminal): mask the agent composer's dim placeholder during a preedit (#17377)
Split out of #17170, which now carries only the xterm composition-overlay work.

Codex and Claude draw an all-dim, full-row ghost placeholder. The opaque preedit
overlay reproduces the committed row tail it covers, so without this the ghost is
repeated to the right of the composing syllable instead of staying masked. The
binding keys off the `.xterm-composition-remainder` class that #17170 adds and
hides it through CSS while a composition owns a structurally verified placeholder
row — bold prompt glyph plus a dimmed model footer below a blank gap for Codex, a
frame line above the prompt for Claude. Arbitrary dim output, shell lookalikes,
and any row carrying typed text keep their tail visible.

readTerminalCursorLineContext moves from src/main/daemon to src/shared because the
renderer now needs the same reader the daemon uses; the move is import-only.

Depends on #17170.
2026-08-30 03:12:22 -07:00
Jinwoo Hong ae0f3675a1 fix(remote): focus host-delegated split panes (#16886)
* fix(remote): focus host-delegated split panes

Return the authoritative leaf identity from terminal.split, record viewer-local focus intent behind the captured pairing revision, and replay the mirrored layout before focusing the exact pane. Preserve old-host fallback and prevent delayed split responses from stealing focus after the viewer moves away.

Add deterministic runtime, renderer, concurrency, compatibility, and headed paired-Electron coverage for Cmd+D, header splits, and immediate PTY input routing.

Fixes #16510

* fix(remote): preserve split focus across tab groups

Resolve the initiating source tab and leaf from the remote PTY, while keeping the viewer's current focus as a separate anti-steal baseline. This lets context-menu/header splits from non-focused group tabs focus their result without allowing delayed responses to override a later navigation.

* test(remote): drive split focus with key events

* test(remote): use the platform split shortcut

* fix(remote): fence concurrent split focus intent

* fix(remote): harden split focus ordering

* fix(remote): preserve split focus after runtime refactor

* fix(remote): fence stale split focus gestures

* test(remote): keep split focus regression within line budget
2026-08-30 03:07:20 -04:00
Jinwoo Hong f572ba34bc feat(browser): address-bar convergence — previews and browser tabs convert in place (STA-5681) (#16998) 2026-08-29 22:38:59 -07:00
Neil 23009d9345 Split telemetry event schema responsibilities (#17235)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Split mobile host screen

* Retarget mobile view settings source test

* Split runtime file client layers

* Split ports panel layers

* Split runtime environments pane layers

* Split local PTY provider responsibilities

* Split CDP bridge responsibilities

* Split relay Git handler responsibilities

* Track moved relay Git fetch audit

* Split Linear item drawer responsibilities

* Split telemetry event schema responsibilities

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:19:52 -07:00
Neil 5650ec8c6b Split local PTY provider responsibilities (#17206)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Split mobile host screen

* Retarget mobile view settings source test

* Split runtime file client layers

* Split ports panel layers

* Split runtime environments pane layers

* Split local PTY provider responsibilities

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:18:28 -07:00
Neil 953114c1c1 Split daemon initialization lifecycle (#17161)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:14:12 -07:00
Neil 7ae916cebd perf(worktrees): batch-prune stale local metadata (#17278) 2026-08-29 16:06:15 -07:00
Neil b826b3fa5b feat(editor): raise rich Markdown size limit to 600 KB (#17288) 2026-08-29 15:03:36 -07:00
JahyunBaekandClaude Opus 5 aa95bdb11a test(shared): stop two suites asserting POSIX separators on Windows (#16511)
Both files describe paths with POSIX literals while their subjects compose
paths through `node:path`, so the assertions only hold where the separator
happens to be `/`.

`node-markdown-document-discovery` keys its fake tree at `/repo/docs` and
`/repo/one`, but `discoverMarkdownRelativePaths` descends with
`join(absoluteDirectoryPath, entry.name)` — `\repo\docs` on win32. The child
lookup misses, `readDirectory` yields nothing, and the walk stops at the root:
`docs/guide.mdx` disappears and the depth-limit case never reaches its limit,
so it resolves `[]` instead of rejecting. Keying the children with `join` walks
the tree the subject actually walks.

`git-fetch-head-lock` expects `cwd: '/tmp/repo'` from a subject that returns
`path.resolve(cwd, 'repo')`, which is `C:\tmp\repo` on win32. Asserting through
`path.resolve` pins the behaviour — that `-C` and `--git-dir` are resolved
against the cwd — rather than the separator of whichever machine runs the suite.

Verified on Windows 11: the two files go from 3 failed / 12 passed to
14 passed / 1 skipped, and the wider `src/shared` run shows no regression.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 14:23:22 -07:00
Neil 2dfaa676d8 chore: update oxlint and oxfmt (#17150) 2026-08-29 14:13:35 -07:00
Neil 8d3e32a2ff Fix setup-provisioned skills missing at agent startup (#17124)
* fix(setup): let repos gate agent startup

* test(setup): update runner call expectations
2026-08-29 01:53:31 -07:00
Neil 8dc26e152f perf(editor): stop re-rendering every code block on each keystroke (#17008)
* perf(editor): stop re-rendering every code block on each keystroke

Profiling a 305 KB document in a packaged build showed typing was dominated
by two things that had nothing to do with the text being typed.

Tiptap re-renders a React node view whenever its document *position* changes,
even when the node and its decorations are untouched (@tiptap/react 3.22.5,
ReactNodeView.update). Typing shifts the position of every node after the
caret, so one keystroke in a document with 533 code blocks cost 533 React
renders. That re-render only exists so a component can observe a fresh
getPos(); RichMarkdownCodeBlock never reads it, so it now opts out via an
explicit `update`. getPos() stays correct for later callers — Tiptap updates
its position bookkeeping before calling `update`, and passes getPos as a live
function rather than a captured value.

The language <select> also mounted ~25 <option> elements per code block, for
a dropdown almost nobody opens: 13,858 option elements in that document, more
than a quarter of its DOM. The list now mounts on first interaction
(mousedown/focus, flushed synchronously so the native popup never paints a
stale list); until then a single option renders the same visible label. The
labels themselves were getters that re-translated on every property read, so
one render cost thousands of i18next lookups; they are now resolved once per
locale.

Median keystroke latency, packaged build, M-series:

  305 KB   84 ms -> 59 ms
  600 KB  265 ms -> 201 ms

Verified in the running app that the dropdown still expands to the full list
by mouse and by keyboard, that an unknown fence keeps its verbatim label and
fallback option, that changing the language still applies, and that typing
inside a code block still updates with syntax highlighting intact.

This does not move the size limit: the blocking mount (1.7 s at 300 KB) is
what pins that, and it is unchanged. The constant now records the measured
numbers, including that node-view count drives cost far more than byte size.

* fix(editor): refresh cached code language labels
2026-08-29 01:35:28 -07:00
Brennan Benson fd9125ea8c feat(native-chat): Codex structured native chat restructure (#16729)
* feat(native-chat): port structured Codex sessions from restructure-recovery

Rebuilds the desktop structured native-chat implementation from
brennanb2025/native-chat-restructure-recovery (tip 4e31c08db3) on top of
current main as a single commit, scoped to the local Codex path.

Ported:
- Structured agent-session core: durable record store + single-writer lease,
  canonical journal, agent-session wire host/attach/eviction/subscribers,
  `agentSession.*` RPC surface (registered via ALL_RPC_METHODS; host-side
  mobile allowlist included for wire compat), pty write gate, transcript
  additions, and the Codex app-server adapter/launch resolution.
- Renderer: NativeChatStructuredSession view/composer stack, structured
  launch path with the single-flight guard, local structured session tabs
  sync, activation gate + structured inventory (read-only
  `agentSession.handoffStatus` probe), agent-session tabs in the tab strip,
  AI-vault structured session activation, and the settings pane with the
  parent Experimental Chat UI toggle plus the nested "Use updated structured
  native chat" toggle. New sessions require both flags, agent codex, no
  prompt, and a local non-WSL, non-Windows-host execution host
  (structured-native-chat-availability).
- Fixes 72c013cea6 (verified Codex launch recovery), 8ddbaf5e3d (defer
  native terminal view switching affordances), and 4e31c08db3 (release the
  launch gate after a visibility retry) with their regression tests,
  including the third-launch-after-retry guard case.
- Cross-version agent-session wire test + CI lane, packaging entries
  (proper-lockfile, agent-tooling asar excludes), and the wire-compat doc
  section.

Deliberately not ported: mobile/ changes, the Claude structured runtime
(only the claude-transcript-branch-proof and claude-structured-owner-identity
leaf modules remain, backing the kept TUI-recovery arms), the terminal↔chat
adoption/handoff flow (`agentSession.adoptTerminal`/`requestHandoff`, the
handoff request engine, TUI adoption machinery, orca-runtime adoption
methods), renderer switching affordances and their dead leftovers, the
hook/subagent-status refactor cluster, and unrelated branch changes. The
crash-during-acquisition recovery path (restart handoff adjudication,
restore/reverse re-acquire, lease schema handoff keys) is kept because every
plain direct launch depends on it; a trimmed handoff coordinator exposes
only status/restore/close.

Branch edits that targeted files main has since split (ipc/pty.ts,
worktrees.ts, rpc/methods/terminal.ts, useIpcEvents, pty-connection,
store/slices/terminals.ts, runtime-types, web preload) were re-applied to
the split modules, preserving main's newer logic (Windows CIM fallback,
browser tab close rework, cold-restore resume flow, dispatcher threading).

Known seam: the mobile clipboard image-provenance CONSUMER gate ships
(agentSession.send refuses unproven mobile image refs with
agent_session_image_untrusted) but the producer hunk in
rpc/methods/clipboard.ts stays with the unported mobile cluster, so mobile
image sends into structured chat fail closed until that side ports.

* fix(native-chat): trust only authenticated local image uploads

* fix(build): preserve Windows process-tree patch application

* test(windows): include process creation time in addon fixture

* fix(build): run windows-process-tree node-gyp from the physical package dir

gyp expands the node-addon-api dependency by probing node, whose cwd
resolves to the package's physical directory in the store, so the emitted
target is a store-relative ../../../../node-addon-api@... hop. gyp then
resolves that hop against the rebuild cwd; from the node_modules
symlink/junction it escapes the store and configure fails with
"node_addon_api.gyp not found" (run 32999886072).

Rebuild from realpath(package dir) so both bases agree, matching how the
package manager itself runs native install scripts. The regression test
replays gyp's expansion+resolution against the planned cwd and fails
without the fix.

* fix(native-chat): keep chat tabs visible through terminal closes and empty-worktree launches

Two proven blockers in the native Codex tab contract:

closeTerminalTab pre-empted the canonical unified close. With one terminal
left it deactivated the worktree on a terminal/editor/browser-only check,
blanking a workspace that still held a renderable agent-session tab; with
two or more it pre-picked a successor from terminal entities only,
re-stamping the group active before closeUnifiedTab's MRU/neighbor repair
could land on the chat tab. Successor choice now defers to the unified
contract whenever the terminal has a unified row, and deactivation is
gated on the unified renderable count (matching leaveWorktreeIfEmpty),
with the legacy pre-pick kept only for terminals without a unified row.

A structured session created on an empty worktree was published into the
host's headless group while preserveLocalLayout froze the local layout,
leaving the tab in store but permanently off screen. A preserveLocalLayout
owner now always takes client-owned placement — repairing a rendered
leaf whose group record is missing, or materializing a rendered group on a
truly empty worktree — and applies the client-derived layout repair while
still rejecting host-authored layout.

Regression tests drive the real store through closeTerminalTab (git
worktree and folder workspace) and the real snapshot applier for the
empty-worktree adoption states; all fail without the fixes.

* fix(native-chat): close stale turns and retry rejected sends

* fix(native-chat): retire hosted rows on structured tab activation

* fix(native-chat): preserve rpc defaults across main merge

* chore: format remote wire compatibility guide

* test(native-chat): cover retry after unconfirmed send

* fix(native-chat): reload outbox on session switch

* docs(settings): disclose structured chat platform limits

* fix(native-chat): await Codex launch-home preparation

* fix(codex): align child-process allowlist with async trust bridge

* test(identity): update inventory for tab surface refactor

* fix(windows): preserve process-tree CRLF patch sources

* fix(native-chat): anchor an unmatched chat echo where it was sent (#16117)

* fix(native-chat): anchor an unmatched chat echo where it was sent

The reported symptom was old user messages replaying below every new turn, so the
conversation read as scrambled. The cause was not that the echo failed to match a
transcript row. Claude consumes a mid-turn send through a `queued_command`
attachment and writes no `type:"user"` record for it, so some echoes can never
match, and no amount of matching will change that. The cause was WHERE an
unmatched echo rendered: buildMobileNativeChatTransientData appended every pending
item after the entire transcript, so it re-read below each turn that landed
afterwards.

Render each echo directly after the transcript row it was sent against, using the
baseline the send already captures. An unmatched echo is then at worst a duplicate
in the right position rather than a scrambled one, and it stays visible. Echoes
sharing an anchor keep send order; a send with no baseline, or one whose anchor
folding dropped, still falls back to the tail.

Deliberately NOT fixed by deleting the echo. Inferring from send ordering that an
echo can never match, then removing it, loses the user's own text for a message
the agent did receive, and it cannot fire in the common case anyway - measured
drain groups are 1,017 of size 1 against 55 larger. It also escalates an existing
gap: the count pass has no baseline-tail guard, unlike the glue pass, while
`messages` is a 40-row window that head-trims, resets on reconnect and grows at
the front on loadEarlier, so a false landing there would license deleting a
DIFFERENT outstanding message.

That count-pass gap is real and left for a separate change; anchoring makes its
worst case a duplicate in place rather than a scrambled conversation.

* fix(native-chat): preserve folded echo anchors

* fix(native-chat): preserve forward-folded echo anchors

* fix(native-chat): keep leading folded echoes in place

* fix(workspace-cleanup): show git status for every row (#16690)

* fix(native-chat): refuse structured chat on every Windows execution path

canUseStructuredNativeChat only refused win32 when a project runtime
resolved, so folder-workspace keys (and other keys with no project
runtime) failed open into structured chat on Windows. Fail closed on
win32 unconditionally after the host check, matching the settings copy:
local macOS/Linux only; Windows/WSL/SSH stay on terminal chat.

* fix(native-chat): restore runtime refusals behind the win32 gate

506d375de3 replaced the project-runtime checks with a bare platform test,
so a WSL or repair-required runtime resolution would no longer refuse
structured chat off-win32. Keep the unconditional win32 refusal and
re-run the runtime resolution after it, so the gate does not depend on
the resolver's own platform guard. Tests inject WSL and repair-required
resolutions on darwin/linux and fail against the regressed gate.

* fix structured session journal durability

* fix structured tab active pointer after restart

* fix(native-chat): await optional lease renewal callbacks

* refactor(skills): extract install error messages

* fix(agent-session): harden recovery ownership

* fix(native-chat): retain panes across tab activation

* fix(native-chat): address round-one review findings

* test(native-chat): align integration coverage after main merge

* fix(native-chat): harden round-two reliability

* fix(native-chat): harden round-three reliability

* fix(native-chat): close round-four recovery gaps

* fix(native-chat): separate bounded journal key forms

* fix(native-chat): reset outbox error in render on session switch

The switch effect adjusted error state after the sessionId prop changed,
tripping react-doctor's no-adjust-state-on-prop-change on the changed-code
gate and flashing the old session's banner for a frame. Reset it with the
render-time previous-value guard instead.

* fix(native-chat): invalidate stale outbox settlements

* test(native-chat): restore settled-error session-switch regression

a6e2379bd1 replaced this test with the in-flight settlement race test,
leaving the render-time error reset unpinned: deleting the reset block
still passed the whole native-chat suite. Keep both scenarios pinned;
they are distinct (settled error clears on switch vs stale settlement
invalidated in the commit-to-passive window).

* test(wire): make release checkouts race safe

* test(wire): pin cross-process checkout single-flight and importer specifier contract

* test(wire): harden release checkout lifecycle

* fix(build): drop CR-byte residue from windows-process-tree patch

The two trailing CR bytes on the patch's deletion lines are a proven
no-op: pnpm hashes patches CRLF-normalized (both forms hash to the
lockfile's 946ffb2b) and materializes this package without applying the
patch in either form, so the load-bearing build edits come solely from
applyWindowsProcessTreeBuildFixes() (#16947), which handles both source
EOL forms. Restore byte-identity with main and repin the contract test
to the post-#16947 reality: LF-only patch bytes plus lockfile hash sync.

* fix(native-chat): skip empty startup recovery
2026-08-28 16:45:58 -07:00
Brennan Benson 4fa3022c47 fix(remote): stop painting a disconnected host as connected (#17050)
* fix(remote): stop painting a disconnected host as connected

A remote host row read "Connected" with a green dot in two states where it
was not connected: a cleanly closed control channel (server restart, host
sleep, network blip leaves lastError null, and the mapping required an error
string before it would say disconnected), and a half-open handshake still in
awaiting_ready/awaiting_authenticated.

lastError/lastClose were also never cleared on a successful reconnect, so a
recovered host kept showing "Connected" beside a stale failure indefinitely.
The SSH lane already clears on success; the shared-control lane did not, which
is why only Remote Server rows showed stale text.

* test(remote): cover stale diagnostics after reconnect
2026-08-28 14:39:20 -07:00
Brennan Benson 41ce4fadd9 Fix GitLab MR management menu in Checks sidebar (#16906)
* fix: add GitLab MR management menu

* fix: restore GitLab menu typecheck

* fix stale GitLab review relink updates

* fix review relink guard lifecycle

* test local owner scope for GitLab relinks

* fix(gitlab): honor linked MR during review lookup

* fix: reuse hosted review cache after relink

* fix: avoid duplicate GitLab detail refresh
2026-08-28 14:29:23 -07:00
Brennan Benson df95f03101 Show ready and close actions for draft reviews (#16889)
* Fix draft review sidebar actions

* Drop unused React import in draft actions test

The automatic JSX runtime makes the default React import dead, and
tsconfig.tc.web.json failed the branch on TS6133.

* Add localization keys for draft review actions

The new Ready for review controls introduced five untranslated keys and
the static analysis job requires them present in en.json.

* Name the draft action for what it does

The button read 'Ready for review', which states a status rather than an
action, directly under a header already showing the PR state. The i18n
key (markReady), the in-flight label ('Marking ready...') and the success
toast ('marked ready for review') all already used the verb.
2026-08-28 12:31:55 -07:00
Neil 7bbb8adc61 fix(ssh): replay an undelivered remote PTY stop on the next handshake (#12447 item 1) (#17011)
* fix(ssh): replay an undelivered remote PTY stop on the next handshake

A pty.shutdown that dies on the transport left the remote shell running
forever: kill.ts marked liveness unverifiable and nothing retried.

Record the undelivered stop on the existing durable SshRemotePtyLease and
replay it against the authoritative host on the next handshake to that same
target, fenced by the host-minted PTY incarnation so a replay cannot kill a
later PTY that reused a recycled pty-N id. Retire the record on confirmed
delivery, on the host reporting the PTY absent, and on a bounded TTL.

No wire change: the fence reads incarnationId, already published on
pty.listProcesses. A host that does not publish it degrades to no replay.

* fix(ssh): do not leave a replayable kill order behind a reversible stop

Worktree sleep stops through stopAndWait and marks those stops reversible;
when one does not land the pane stays live and the user keeps using it. An
order recorded there would come back on a later handshake and kill that
terminal. Only killPtyFromRuntimeController — where the client gives the PTY
up for good — records one, and it skips any PTY a reversible stop owns.

* fix(ssh): cover the renderer kill route and harden the replay's evidence

pty:kill is a separate implementation from killPtyFromRuntimeController and
is the one an ordinary tab close reaches, so the record was never written on
the path #12447 describes. Extracted it out of inspect.ts (which was over the
line budget and was not what the file is named for) and wired both branches.

Also:
- finishPtyShutdown no longer retires the order. It runs on paths that asked
  the host and on paths that never did, so retiring there was a contract every
  caller had to know, and the one that forgot silently dropped a kill order.
  Retirement is the replay's, on inventory evidence only.
- A recycled relay id now expires its lease. Declining to kill was only half:
  reattach fences on paneKey/tabId, never incarnation, so an untouched lease
  bound the user's old pane to whatever now holds the id.
- Dropped isPtyAlreadyGoneError from the tombstone path. It matches message
  text a transport failure could wear; every tombstone now traces to a listing.
- TTL is owned by a durable prune that actually deletes, not by a branch that
  was unreachable behind the read filter and only looked tested.
- The replay re-reads the inventory per wave and re-checks the fence next to
  each shutdown, and can never reject into the connect path.
2026-08-28 04:08:15 -07:00
Neil 65dd06a870 feat(editor): add "Open anyway" for oversized rich Markdown files (#16964) (#16971) 2026-08-28 02:38:51 -07:00
Jinwoo Hong fc8c981103 fix(browser-preview): enforce canonical runtime grants (#16975) 2026-08-27 23:27:39 -07:00
Neil b1fe9075db fix(ssh): never claim authority over host tabs this client could not place (#16956)
* fix(ssh): a host tab row this client cannot place is unverifiable, not absent

A degraded listLineage leaves worktreesByRepo empty, so exactTargetWorktreeIds
returns nothing, every host path fails to resolve, and importRemoteWorkspaceSession
silently dropped every tab row. The apply then marked the target hydrated and
'synced', freezing that emptiness in permanently — nothing re-pulls a hydrated
target (STA-3593).

The importer now reports unplaceable rows, the apply claims authority only when
every row landed, and a bounded chain re-pulls the missing input (catalog +
lineage). On exhaustion it settles back to the pre-fix behaviour so a genuinely
unplaceable path is never left worse off than today.

* fix(ssh): keep the re-pull chain bounded, unwedgeable, and announced once

Five defects in the first cut of the chain, all found before merge:

1. the caller owned the attempt counter, so an unsolicited host push re-armed
   it at 0 and the chain never exhausted - an unbounded workspace.get loop;
2. exhaustion never cleared the counter, so after one bad connection every
   later reconnect re-exhausted instantly and the retry was silently dead;
3. the exhaustion check preceded the armed-timer guard, letting a concurrent
   report cancel the still-pending final retry;
4. a rejected host read left no timer armed and nothing rescheduled, stranding
   the target on 'pulling' and un-hydrated forever - and an un-hydrated target
   never uploads again, the exact permanent degradation this design avoids;
5. exhaustion re-announced on every later report, re-marking hydrated and
   rewriting status on each host push.

The module now owns the counter, resetTarget gives each connection a fresh
chain, the armed guard precedes exhaustion, the timer body always reschedules
so any failure walks to exhaustion, and exhaustion is announced once.

* fix(ssh): never authorise uploads from a picture known to be incomplete

Reversal of this branch's own exhaustion fallback, on evidence.

Hydration authorises uploads (use-app-session-persistence.ts), and an upload is
a workspace.patch of kind 'replace-session' (remote-workspace-relay-sync.ts:66)
which wholesale replaces the host snapshot (relay/workspace-session-handler.ts).
So marking a target hydrated on a picture we know is missing rows does not
'settle back to the old behaviour' - it uploads an empty projection that DELETES
the host tabs we failed to adopt. Suppressed uploads are recoverable; a wiped
host snapshot is not. That data loss is reachable on main today, because today
the apply marks hydrated immediately.

Exhaustion therefore reports 'error' and leaves the target un-hydrated, so
terminal authority stays 'unverifiable' and no upload can be built from it.

Also closes two chain-lifecycle gaps found in review:
- the callback dropped its timer guard before awaiting the host, leaving a gap
  in which a concurrent report armed a second overlapping chain and could trip
  exhaustion before the pending apply resolved; an in-flight guard now spans it;
- resetTarget could not cancel a callback already past its await, so a stale one
  rescheduled on top of the new connection's chain; chains are now generation
  stamped and a stale callback exits.

* fix(ssh): scope re-pull in-flight ownership to a generation

Two races found in review of the previous commit:

- schedule(target,'placed') cleared timer/count/exhaustion but did not
  invalidate an apply already in flight. When that apply later resolved
  'unplaced' its generation still matched, so it started a fresh chain from
  attempt 0; a host repeating placed pushes during each in-flight retry could
  reset the budget indefinitely. Retirement now bumps the generation too.

- the in-flight marker was a bare Set, so a superseded callback's finally
  deleted whichever marker was present - including one a newer generation had
  since taken. A later report could then arm an overlapping timer while that
  newer apply was still running. Ownership is now a target -> generation map and
  a callback releases only the marker it still owns.

resetTarget deliberately no longer drops the marker: its owner is the only party
that may release it, and clearing it there would let a new chain arm while the
superseded apply is still running.

* fix(ssh): replay an unplaced report that was blocked by a superseded apply

Regression from the previous commit. Keeping the stale in-flight marker across
resetTarget stops overlap, but it also swallows the new connection's result: the
new apply reports 'unplaced', hits the guard because the superseded apply still
owns the marker, and the superseded callback then exits on its stale generation
without scheduling. Nothing replayed the dropped report, so no chain started -
the retry silently never ran for that connection.

A blocked unplaced report is now recorded, and the marker's owner replays it on
release. Only the stale path reaches the finally still owning the marker, so the
normal path - which released early and scheduled its own outcome - cannot replay
twice.

* refactor(ssh): drop the re-pull retry chain, keep the fix

The chain produced ten defects across review - unbounded retry, dead retry,
cancelled final attempt, wedged chain, repeated exhaustion, overlapping chains,
stale-callback cleanup, a lost report - every one in code that passed the full
suite at the time. It bought only faster recovery *within* one connection:
syncAfterConnect and applyUnsolicitedSnapshot already re-pull on the next
connect or host push, so dropping it costs a retry, never the data.

What remains is the part that was correct from the first commit: the importer
reports rows it could not place, and an apply that could not place them neither
marks the target hydrated nor sets 'synced'. Because hydration is what
authorises uploads, and an upload wholesale replaces the host snapshot, that
single rule is what stops a client from deleting the host tabs it failed to
adopt.

Status is now 'error' rather than 'pulling': with no chain pending, 'pulling'
claimed a request that was not in flight.

* test(ssh): name the upload-suppression case for the chainless design

* fix(ssh): revoke stale hydration and keep authority unverifiable when unplaced

Two holes in the previous commit, both found in review.

The hydrated set is add-only (ssh.ts), so withholding hydration only protects a
target that never synced. A target that synced cleanly and then reconnected with
a degraded lineage kept its flag, and hydration is what authorises uploads - so
it would still send a replace-session patch built from the incomplete picture
and delete the host tabs it had just failed to place. Hydration is now revoked,
not merely withheld.

The status phase was 'error'. workspace-terminal-host-authority.ts treats
'offline'/'error' on an un-hydrated target as its bounded floor and resolves
them to 'none' - which authorises seeding AND sleeping-agent resume, the exact
double-resume this gate exists to prevent. 'conflict' is the phase that actually
describes the situation, is excluded from uploads by use-app-session-persistence,
and is deliberately outside that floor set, so authority stays 'unverifiable'.

Both invariants are pinned by tests verified to fail when either fix is reverted
individually; the pre-existing tests passed with both reverted.

* fix(ssh): drop the mismatched message on the unplaced conflict status

The phase drives the user-visible label ('Workspace sync conflict'); carrying an
'unavailable' message alongside it only risked contradicting that wherever the
message is surfaced.

* docs+refactor(ssh): correct the authority floor's premise, drop a dead wrapper

Two findings from the post-merge correctness sweep.

The bounded floor in workspace-terminal-host-authority.ts justified itself on
'remoteWorkspaceHydratedTargetIds is add-only, clearRemoteWorkspaceHydrated has
no production caller'. This branch adds that caller, so the premise is now false
and a future reader would have been misled by it. The comment records the real
consequence: a target that later lands on offline/error reaches the floor having
demonstrably answered, so seeding is authorised over live host terminals. Not a
regression - before revocation existed the same target was marked hydrated and
synced, reaching 'none' sooner - but the floor should learn to tell a revoked
target from one that never answered. Flagged for the SSH-v3 consolidation, where
one authoritative liveness source replaces this pair.

applyUnsolicitedSnapshot had become a pass-through to applyPreparedSnapshot,
carrying a docstring about a re-pull chain that no longer exists. The two
collapse back into one function.

* refactor(ssh): delete the DirectSshSnapshotPlacement union

Consolidation pass finding. The union was exported and threaded through two
modules, but no production consumer ever read it: remote-workspace-ipc-bridge.ts
discards the promise's value and syncAfterConnect ignored it. 'not-applied' was
not a placement at all, only 'this apply did not happen'.

That is a parallel verdict concept with no consumer - precisely what the SSH-v3
consolidation would have had to unpick. It collapses to a local
hasUnplacedTerminalTabs boolean and a void return.

The one test that asserted the return value now asserts adoption instead, which
is the observable outcome rather than a proxy for it. All five unplaced oracles
still fail when the placement decision is forced, verified individually.

* docs(ssh): compress the tombstone rationale to its load-bearing WHY

Elegance pass. Kept the two non-obvious claims - absence cannot distinguish
'never told' from 'user closed', and uuid tab ids make a tombstoned id safe to
drop - and cut the incident narrative around them. The twice-reverted history in
remote-workspace-session-merge.ts is deliberately left alone: that one is
institutional memory about regressions, not restatement of the code.

* test(ssh): pin the fixed behaviour instead of the defect it replaced

The spec was a characterization test whose own title said 'because hydration is
marked even when adoption wrote nothing', and whose comment described exactly
the defect this branch fixes: markRemoteWorkspaceHydrated ran unconditionally
after the hydrate calls, so in the same tick adoption yielded zero, authority
flipped unverifiable -> none, and Terminal.tsx seeded a phantom tab. It polled
for hydrated === true, so the fix turned it red.

It now asserts hydrated === false, phase === 'conflict', and zero tabs - the
count measured before asserting rather than assumed, confirming the phantom seed
is gone. The phase is re-read after the tabs settle and asserted a second time,
because a conflict verdict a later apply flipped back would silently re-authorise
seeding and a single poll would miss it.

The fixme stays a fixme: this branch stops the client claiming false authority
and overwriting the host, but adoption is still the open gap. Declining to seed
is a safe wait, not the destination.

Three-legged A/B against fork point c72afda498, spec byte-identical across the
first two legs:
  branch   + updated spec  -> PASS
  baseline + updated spec  -> FAIL 'never reported the unplaced snapshot as a conflict'
  baseline + original spec -> PASS (baseline actively exhibits the old behaviour)
2026-08-27 22:14:35 -07:00
Jinwoo Hong cb848647e5 fix(browser-preview): require explicit preview capabilities (STA-5758) (#16921)
* fix(browser-preview): require explicit preview capabilities (STA-5758)

Scope document reads to approved directories, confirm external links before opening them, revoke grants with tab lifecycle, and keep document-preview session state rollback-safe across mixed client/runtime versions.

* Harden document preview lifecycle and permissions

* Document preview DNS prefetch residual

* Make preview E2E guest focus explicit

* fix(browser-preview): entry-file-only authority for root-level docs, contained chip layout, re-issued gate paths (STA-5758)

A grant whose document directory is its own request base — a doc at the
workspace root, or outside any workspace — now reads nothing but the entry
file until the reader approves a directory, at both the lexical and the
canonical containment pass. The DNS-prefetch residual can only beacon what
the page can read, and a root-level document could previously read the
whole worktree silently.

The identity chip's host badge overflowed the chip's layout box under
squeeze (Linux CI): every row member can now shrink and truncate, verified
by a width sweep in isolated Chromium down to ~120px chips.

The Allow banner says what it grants: 'Allow folder', reading files in the
named directory, for the life of the preview.

The reliability-gate manifest command, testFiles entry, assertion refs and
dated evidence naming the deleted doc-preview-external-link-bridge.test.ts
are re-issued at doc-preview-external-link-confirmation.test.ts with a
fresh 189/189 run; the focus-gate assertion text follows the shipped gate.

* fix(browser-preview): hide the chip identity row below 24rem instead of clipping it, ellipsize the host badge, catalog the new i18n keys (STA-5758)

CI's preview pane leaves the chip ~40px: no truncation shows anything
there, so the Workspace-file label and host badge now hide whole below a
24rem container threshold sized so that visible implies contained. The
badge text gains an inner text box — text directly inside the flex pill
clipped both ends with no ellipsis. The e2e geometry oracle asserts
containment when the row shows and the threshold when it does not.

verify:localization-catalog: the hardening's new preview keys (and the
renamed allowDirectory) join en.json via sync:localization-catalog.

* feat(browser-preview): batch blocked folders into one access decision (STA-5758)

Sequential per-folder banners trained the allow reflex without adding
judgment — a reader cannot weigh assets/ against data/. The banner now
accumulates every folder a load surfaces, names them (three, then a
count, full list in the title), and grants exactly that set with one
Allow-N-folders click and one reload. Dismiss fences the whole named
set. The map lives behind a ref with a version tick so a dismissal
fences an offer landing in the same event batch.
2026-08-28 00:42:07 -04:00
Jinwoo Hong b19a397d3e feat(browser-preview): reland remote HTML document previews (STA-5758) (#16920)
Reapply the reverted remote HTML document preview implementation so remote workspace files render locally over the orca-preview scheme.
2026-08-28 00:27:01 -04:00
Jinjing 52ade074a9 Display host on automation details and dialog (#16823)
* Show automation host in details and support moving between hosts

- Rename AutomationCreateDestinationField to AutomationDestinationField to
  reflect dual use in create and edit modes
- Add host display to automation detail view, showing storage authority
- Allow editing automations to move them to different hosts within same
  authority; project list filters to available projects on chosen host
- Update copy from create-only terminology to mode-agnostic wording

* Display automation host and support cross-authority moves

Users can now move automations to different storage authorities. The
destination picker shows all available hosts, and selecting a new one
displays a warning about the move. The save creates the automation on
the destination and deletes it from the source; if deletion fails,
both copies remain and the user is notified.

* Remove cross-authority move support for automations

An automation's authority (the Orca instance that stores and schedules it)
cannot change; edits now only offer hosts within the same authority and
move logic is removed entirely. This simplifies the destination picker and
removes move-specific UI messaging.

* Fix undefined selectedRowKey in automation host recovery

Replace references to the undefined selectedRowKey variable with
selectedRow?.key to properly access the row's key when recovering
automation runs across hosts.

* Support moving automations across execution authorities

Allows users to move automations between different authorities (desktop ↔ runtime environments) during editing. A save to a different authority creates the automation on the destination and deletes the original with its run history. Includes clear messaging about the move operation, proper handling of workspace id resets, and graceful error handling when deletion fails. Supports destination-aware project and worktree fetching.

* Reuse creationKey across move retries when schedule changes

When retrying a failed automation move, dtstart is minted fresh each
attempt, changing the payload. Previously, operationKey included the
full payload, so retries would mint new creationKeys and risk duplicate
automations on the destination if the initial create failed in transport.
Now key only by the move (source + destination) to ensure stable
creationKey across retries.

Also fix workspace auto-selection to use authority-scoped worktrees
instead of the merged cache, preventing unwanted restoration of
source-host workspaces after switching authorities.

* Rename `note` to `moveWarning` for automation host moves

Clarifies that the field specifically warns when an automation would move to another host, replacing the plain storage line.
2026-08-27 20:10:17 -07:00
Neil 2b391652b1 fix(terminal): a close the host never heard must survive the reconnect (#16752)
An enterprise user: "Every day I open orca and it opens more tabs daily at a
linear scale." Three reports over a week, told on 08-19 that a PR had fixed it,
reported twice more after. STA-4658 (P0), GH #12447, #15136, #10342, #9585. One
install held 39 zombie tab records. The revived tab's sleeping-agent record still
holds the pre-close session id, so it boots `claude --resume <old id>` -- two
agents on one transcript.

## The chain, measured

Reproduced deterministically in `ssh-lost-kill-tab-resurrection.spec.ts`: close
an SSH tab, kill the relay daemon in the container so `pty.kill` rejects with a
transport-class error, reconnect.

    drop 2 resurrected the closed tab <id>:
      baseline=1  drop1=1  drop2=2 (closed tab returned)  drop3=1

The trigger is narrow and had to be measured rather than assumed: killed relay
daemon reproduces **6 of 6 runs**; an orderly `ssh.disconnect` **passes**. Only
an ungraceful loss -- network partition, host reboot, relay crash, a laptop
sleeping mid-session -- strands the close with the RPC rejecting on a
transport-class error. Both variants live in the spec behind one
`runResurrectionCycles` parameterized solely by the disruption, so the difference
is attributable to that single variable.

What actually carries the tab back, from the pull path
(`workspace.get` -> `getRemoteSnapshot`, `remote-workspace-relay-sync.ts:29`):

    pullSnapshot rev=3 tabs={repo:["16c4a3e1","06aba6b6"]}
    pullSnapshot rev=4 tabs={repo:["16c4a3e1","ff72768e"]}   <- ff72768e IS the resurrected tab
    pullSnapshot rev=5 tabs={repo:["16c4a3e1","ff72768e","da21b76c"]}

The client uploaded the session containing the tab; the user closed it; the kill
RPC rejected so the close never reached the host; the host's snapshot still lists
it; the client pulls it back and the merge restores it -- **correctly, by its own
rule that the host is authoritative for what it knows.** A pane then mounts,
respawns, and takes the recycled pty id.

Client-side correlation from the same run, two controls and one positive in one
run differing in exactly one variable:

| Tab | Close events observed | Resurrected? |
|---|---|---|
| `6305cc07` | `user` + `pty-exit` | No |
| `ed56f66c` | `user` + `pty-exit` | No |
| `2036e760` | `user` only | **YES** |

## The fix

`src/shared/closed-terminal-tab-tombstones.ts` (99 lines). A client-recorded
close is first-party intent and must survive until the host acknowledges it. Per
`docs/reference/ssh-execution-boundary.md` the remote verdict is `unverifiable`
-- which may not authorise declaring the process dead, but equally must not
authorise resurrecting the tab. This is SSH-v3 principle P2, "durable tombstones
with a monotonic per-scope revision", reusing the existing
`RemoteWorkspaceSnapshot.revision` rather than adding a twelfth per-tab identity
field (the codebase carries eleven, 784 refs, that SSH-v3 Phase 3 deletes).

- **Recorded** only on `closeReason === 'user'` (`terminal-tab-close.ts:69`).
- **Suppresses** a host-sourced tab only when
  `tabId in tombstones && !currentTabsById.has(tabId)` -- a live local tab always
  wins, because deleting a live pane is the one outcome the merge exists to
  avoid.
- **Retires** on positive acknowledgement:
  `!hostKnownTabIds.has(tabId) && hostRevision > observed`. Strictly newer, so a
  pull already in flight at close time cannot ack a close it predates.
- Three never-retire guards: no revision retires nothing; a worktree the snapshot
  has no row for retires nothing; the first omitting snapshot only stamps the
  watermark.
- TTL (30d) + cap (500) are **backstops** for a target the user never returns to,
  not the mechanism.
- **Client-local only** -- never crosses the wire, so there is no mixed-version
  exposure.
- Suppression is scoped to `replaceWorktreeIds`, which is what makes the
  live-tab check meaningful. A final whole-map sweep over the assembled
  `tabsByWorktree` would break that (a live tab is absent from `currentTabsById`
  outside the scope and would look suppressible); it is deliberately not there,
  and the comment at the top of the function says so.

## Evidence

The load-bearing evidence is an A/B control on one tree, not the oracle's
assertion. Flipping `isSuppressedByClose` to `false` -- one character --
reproduces the resurrection on demand:

    --repeat-each=2:
      1) drop 2 resurrected the closed tab ab0e305d-…: baseline=1 drop1=1 drop2=2
      2) drop 2 resurrected the closed tab 51533e34-…: baseline=1 drop1=1 drop2=2
      2 failed

With suppression on: **0 occurrences of "resurrected the closed tab" across five
runs plus one independent run by a second agent.** Provenance verified
positively, not by mtime: `closedTerminalTabTombstonesByTabId` appears 13x across
3 renderer chunks including `store-Do3KBvRE.js`; for every red control run
`mayCreate` appeared 0 times in `out/main/index.js`.

At the unit layer, disabling the same predicate: 3 failed | 39 passed. Restored:
42 passed; 287 across the workspace-session, terminal-store, remote-workspace,
shared-tombstone and profile suites; 24 in the four tombstone suites.

## The oracle spec: GREEN in the full lane

`ssh-lost-kill-tab-resurrection.spec.ts` passes both tests at this commit. Full
Docker-SSH lane, clean tree:

    BUILD_SHA=49bb96e0b4c   DIRTY=0
    PROVENANCE  tombstone=13  hasLocalTabsRow=2  hostAuthority=4  mayCreate=3
    14 specs / 20 tests -> 17 passed, 2 failed, 1 skipped (10.7m)

    [12/20] :178 does not resurrect tabs whose kill was lost to a killed relay
            daemon                                                      PASSED
    [13/20] :190 does not resurrect tabs closed while the host is
            disconnected                                                PASSED

    grep -c "resurrected the closed tab"  (whole lane)  -> 0

It passes WITHOUT PR 7 in the build (`mayCreate` present,
`SshPtyAbsentFromRelayError` absent), so the bug-2 fix below is not required for
it.

Test 1 fails intermittently in ISOLATED single-spec runs, where a third defect
blocks its cycle-2 setup. The resurrection assertion itself has never failed with
this fix in place -- the intermittent failure is always a setup failure, never a
resurrected tab. A reviewer running the spec alone may see it red; that is not
this fix regressing.

Three defects sit under STA-3374 and should not be conflated:

- Bug 1 -- the closed tab resurrects. Fixed here.
- Bug 2 -- `ssh-pty-session-reattach.ts:227-231` rewrites the relay's
  `PTY "pty-1" not found` into a bare `SSH_SESSION_EXPIRED`, so
  `isPtyAlreadyGoneError`'s `/PTY ".+" not found/` cannot match and
  `attachStablePaneOwner:242`'s already-correct fallback never runs. Owned by
  PR 7 (`nwparker/ssh-07-absent-from-relay`). Not required for the oracle above.
- Bug 3 -- after the daemon is killed and the client launches a replacement, the
  client's OWN SSH transport drops and does not reconnect within 60s: no
  "delay step 2/9", no handshake failure, nothing. `ssh-connection.ts:1533` only
  logs on an SSH-level close. Unfixed, its own ticket. This is what makes test 1
  intermittent in isolation.

Discriminator for bug 3, measured in the isolated runs (the lane above ran
without `ORCA_E2E_FORWARD_APP_LOGS=1`, so it was not re-confirmed there):
`[ssh-relay] Socket probe result:` reads "DEAD" on every cycle of test 1 (daemon
killed, a NEW relay must be launched) and "ALIVE" on every cycle of test 2
(daemon survived). Whenever a new daemon must be launched, the SSH transport
drops afterwards and does not recover.

An earlier reading blamed `kill.ts:82-84` for skipping `finishPtyShutdown` on a
non-already-gone error. That was eliminated by direct test: the implied fix,
`markSshRemotePtyLease(…, 'expired')` in that branch, was implemented, changed
nothing, and was reverted rather than shipped unproven. Recorded so the path is
not re-walked. The `SSH_SESSION_EXPIRED` rejection is real but fires during cycle
1 for the baseline pane, after which cycle 1 completes; the 60s silence begins
only after `Relay channel lost ..., triggering reconnect`.

The spec is claimed by the Docker-SSH lane, and that lane does not gate merges
today.

## Persistence: the tombstone must survive a relaunch

`closedTerminalTabTombstonesByTabId` is declared on `WorkspaceSessionState` but was missing from
`workspaceSessionStateSchema` (`src/shared/workspace-session-schema.ts`), which is the load boundary
for BOTH partitions -- `normalize-loaded-state-collections.ts` for `local` and
`workspace-session-partitions.ts` for `ssh:<target>`. Zod strips unknown keys and the write side does
not validate, so the map reached disk and was discarded on the next launch. Measured with the repo's
own parser:

    input : closedTerminalTabTombstonesByTabId: { 'tab-1': {...} }
    ok    = true
    tombstones after parse = undefined

That made the fix ineffective in the exact reported scenario: close an SSH tab with the transport
down, QUIT, relaunch, reconnect -- the merge runs with an empty map, the host still lists the tab,
and it resurrects. "Every day I open orca and it opens more tabs" is a claim about restarts.

Neither the green oracle nor the A/B control could see it: both run entirely inside one app process.
It also made the 30-day TTL and the 500 cap unreachable.

Fixed by adding the field with a `salvagingRecord` matching its sibling
`terminalSurfaceTombstonesByPaneKey`, so one malformed entry drops that entry rather than the map.

`workspace-session-schema.ts` was one line under its 300-line max-lines limit, so adding the field
required room rather than a suppression (the project forbids max-lines disables and per-file bumps).
Two value schemas were extracted to modules named after what they contain:
`terminal-tab-id-schema.ts` and `terminal-surface-tombstone-schema.ts`. The closed-tab tombstone's
own schema is colocated with its type in `closed-terminal-tab-tombstones.ts`, which is where it
belongs -- omitting it from the session schema is exactly the drift that caused this bug.

`workspace-session-schema-field-coverage.test.ts` is the ratchet. Two sibling tables already pin
themselves with `satisfies Record<keyof WorkspaceSessionState, ...>`; this schema had no such guard
and is the one that fell behind. The new file adds both halves -- a `satisfies` list that makes a
forgotten field a compile error, and a runtime assertion that names it -- plus a
`parseWorkspaceSession` round-trip. Without the schema entry: 3 failed. With it: 3 passed.

## A host tab the user never closed could be deleted

`tabId in closedTerminalTabTombstonesByTabId` answers true for every `Object.prototype` key even on
an EMPTY map, because the map is a plain object from `Object.fromEntries`. A host tab whose id is
`toString` was filtered from the reconciled list, blocked from the host-unknown branch, and stripped
of its layout and session id. Tab ids are validated only as non-empty and colon-free, and `createTab`
honours caller-supplied id hints, so the id is reachable rather than theoretical. This was the only
path in either direction that could delete a tab the user never closed.

Now `Object.hasOwn`, as the same file already uses elsewhere.

Suppression is also scoped structurally: `isSuppressedByClose` compares the tombstone's stored
`worktreeId`, which it already carried, so it cannot reach another workspace's tab. The two sweeps
that have no worktree in scope (`terminalLayoutsByTabId`, `remoteSessionIdsByTabId`) now consult the
set of ids this merge actually suppressed rather than re-deriving a verdict without that scope.

The scope comment at the top of the function was also wrong and is corrected. It claimed every use of
suppression sits inside `replaceWorktreeIds`; it does not -- the tabs pass walks all of
`orderedWorktreeIds` and the two sweeps cover the whole remote maps. What actually makes it safe is
that `closeTab` strips the id from every worktree row before recording the tombstone, plus the
worktree match above, plus `closeReason === 'user'` being the only writer. Real guarantee, different
from the documented one.

## Divergences from open PR #16571

#16571 implements the same concept. Three deliberate changes:

1. It never retires on acknowledgement -- TTL+cap only, so it never converges.
   Ack retirement added.
2. It crosses the wire and lets a HOST-sourced tombstone delete a LOCAL tab in a
   final whole-map sweep. After #14361 that is the wrong risk; dropped. This also
   removes the mixed-version regression its own body flags.
3. Its hydration unions rather than replaces the map -- a union resurrects every
   tombstone the merge just retired, so it never converges.

Its `activeTabId` nulling is also dropped as redundant:
`workspace-terminal-hydration.ts:99-105,126-138` already revalidates both
pointers against the tab rows it just built, and nulling twice would add a second
rule that has to stay in step with the first.

## Can a tab the user did NOT close disappear?

No, but the guarantee needs stating precisely. The only writer is
`recordClosedTerminalTabTombstone` (`terminal-tab-close.ts:69`), reachable only
on `closeReason === 'user'`; suppression additionally requires the tab not be live
locally. Reopen (`recently-closed-tabs.ts:122-166`) calls `createTab` and restores
cwd/shell/title/color/position, never the old id.

**Caveat, stated because the slogan is not literally true:** `createTab` honours a
caller-supplied id hint (`terminal-tab-creation.ts:53-65`, used by `useIpcEvents`
for host-admitted tabs), so "tab ids are uuids that never recur" does not hold in
this codebase. The guarantee rests on the `closeReason === 'user'` writer plus the
live-local-tab check, not on id uniqueness.

## Risk

Renderer-side, client-local, no wire change. The blast radius is
`mergeDirectSshRemoteWorkspaceSession` and the persisted session field. Worst case
if the ack logic were wrong in the retiring direction: a tombstone outlives its
usefulness and suppresses a host tab whose id the host re-issues -- bounded by the
live-local-tab check, the 30d TTL and the 500 cap. Worst case in the other
direction is today's behaviour. `profile-project-session-field-disposition.ts`
records the new field as `notRepoScoped` / `notTransferred` residue, bounded by
the same TTL and cap.

## Verify

    pnpm test src/shared/closed-terminal-tab-tombstones.test.ts \
      src/renderer/src/lib/workspace-session-closed-tab-tombstones.test.ts \
      src/renderer/src/store/terminals/terminal-tab-close-tombstone.test.ts \
      src/renderer/src/hooks/remote-workspace-session-merge-close-tombstones.test.ts

To reproduce the bug this fixes, set `isSuppressedByClose` to `() => false` in
`remote-workspace-session-merge.ts` and run
`pnpm test:e2e:ssh-docker -- tests/e2e/ssh-lost-kill-tab-resurrection.spec.ts --repeat-each=2`.
2026-08-27 19:47:15 -07:00
Jinjing 2ac23c29be Add parent worktree selection when creating workspace (#15420)
* feat: select parent worktree for nesting when creating workspace

Enable users to pick a parent workspace in the composer's Advanced drawer,
organizing newly created worktrees hierarchically in the sidebar. The backend
validates lineage relationships and gracefully retries without the parent if
it becomes unavailable during creation.

* feat: select parent worktree for nesting when creating workspace

- Record app-picked parents as manual actions, not CLI-flag equivalents,
  ensuring the same user action carries consistent cleanup semantics across hosts
- Gracefully retry without parent if the selection goes stale, warn the
  user instead of failing
- Refactor create into modules: parent resolution, payload building,
  state merge

* Allow selecting parent worktree when creating workspace

- New parent worktree picker filtered by execution host and project
- Preserve concurrent local writes to lineage by comparing per-record state instead of key-set membership

* Allow selecting parent worktree when creating workspace

- Rename "Parent workspace" label to "Parent worktree"
- Filter candidates by execution host and project to prevent nesting across hosts
- Wire parentWorktreeId through composer state and creation request pipeline
- Update translations and add new copy for nesting-related messages
2026-08-27 19:38:01 -07:00
Neil 3ed7796624 fix(fork-sync): address runtime repos by main worktree id, not repo id (#16876)
Repo-level fork sync (Safe Auto and the Sync Now button) passed `repo.id`
as the runtime worktree selector, so runtime-hosted repos always failed
with `worktree_id_requires_full_path`. Compose the repo's main worktree
id (`<repoId>::<repo.path>`) via a new shared `getRepoMainWorktreeId`.

Fixes #16447
2026-08-27 18:16:06 -07:00
Jinjing 551fbb9ac7 Revert "feat(browser-preview): render remote HTML docs locally over an orca-preview scheme (STA-5557) (#16679)"
This reverts commit 249d93bc5d.
2026-08-27 16:32:35 -07:00
Neil 7ee8b5e1a6 Refactor lower max-lines modules (#16760) 2026-08-27 16:10:51 -07:00
Brennan Benson 81ae98e10d fix(mobile): honor host worktree create retention (#16342)
* fix(mobile): honor host worktree create retention

* fix(mobile): cover malformed worktree retention policy

* fix(mobile): fail closed on malformed retention policy

* fix(mobile): fail closed on missing dedupe ttl
2026-08-27 16:02:38 -07:00
Brennan Benson 6ba6d58cd2 fix(orchestration): route @agent messages by resolved identity, not terminal title (#16237)
* feat(agent-status): add the pane agent identity resolver

Four ladders answer "which agent is in this pane" independently — the tab icon, the
open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they
disagree. Two consult the terminal title before the launch record, so a string Orca parsed
outranks a fact Orca owns.

resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering:

1. Evidence is ranked by how directly it observes the process; a display title is last.
2. Each observation carries the runId of the agent run it describes. Evidence from a superseded
   run is INELIGIBLE, not merely outranked.

Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming
B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) —
identical signals, opposite correct answers. Run ids make them different facts: in the bug both
belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as
a test asserting the two produce opposite answers from the same evidence.

Missing run ids are treated as eligible. Absence means "this peer does not publish them", not
"this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so
pane-scoped consumers cannot inherit another pane's agent.

No consumer imports this yet; each migrates separately with its own evidence.

Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the
run filter fails 3.

* fix(agent-status): close three resolver contract holes found in review

**Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the
first match, so two live hooks naming different agents were settled by input position — the exact
property this resolver exists to remove. The original order-independence test only used DISTINCT
sources, so it never exercised it. Conflicting same-class evidence now returns null with
`ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two
hooks disagree is worse than saying nothing.

**A bare numeric runId collided across authority restarts.** `incarnation` is a total order only
within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per
authority instance, so a restarted host counting from its own floor would report `1` and match an
unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT
authority is treated as incomparable — kept, like an absent key — rather than as stale.

**Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse
unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker
evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed
string even by reordering its inputs. Display surfaces omit it and are unaffected.

Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was
lost when this branch was rebased.

Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping
the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used
`incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses
differing incarnations.

The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here:
it needs an observation timestamp the evidence type does not yet carry. Recorded rather than
silently dropped.

* fix(orchestration): route @agent messages by resolved identity, not terminal title

`@claude` picked its recipients with `buildAgentNameRe('claude').test(title)`, so any pane whose
TITLE contained the word received Claude's messages. Terminal titles carry task text, and people
describe agent work in them, so this is the ordinary case rather than a contrived one: the
recorded title "Switch Claude and Codex off the load balancer… - grok" is a Grok pane that
received both @claude and @codex. Misdelivered instructions, not a cosmetic slip.

The cause is that `RuntimeTerminalSummary` carried no identity at all — `title` was the only
identity-ish field on it, so routing by title was the only option available. Fix the input:

- `RuntimeTerminalSummary.agentIdentity?: TuiAgent` — optional, host-resolved from launch and
  foreground-process evidence the host owns, with the title ranked last and contributing only
  when the evidence parser finds an unambiguous name. A title that merely mentions an agent
  yields no evidence, which is the whole point.
- `resolvePublishedPaneAgentIdentity` in `src/shared` rather than inside the runtime class, so
  the decision is testable without a runtime and so routing, delivery and the UI cannot drift.
- Groups match `agentIdentity`; the title matcher and its bespoke Cursor predicate are deleted.

Unknown fails closed. `agentIdentity` is absent when the host predates the field or had no
evidence beyond the title, and delivery is an action: not delivering is visible and recoverable
(the sender sees no recipients), while delivering to the wrong agent is neither. The optional
field is additive, so an old client simply ignores it (wire rule 1).

This is also the first real caller of the evidence parser and the identity resolver.

Tests: 27 in groups, 8 for the publisher, 3 RPC fan-out cases updated to the new contract. The
`@cursor`-must-not-match-"text cursor blink" hazard is now excluded structurally instead of by a
per-agent predicate.

Verified non-vacuous by mutation: swapping the process/title ranks fails 2 publisher assertions,
and reverting groups to title matching fails 15 of 27. One earlier mutation silently failed to
apply after formatting reflowed the block — the file was checked before trusting the result.

* perf(runtime): reuse terminal title during summary build

* fix(orchestration): refuse title evidence when publishing identity for routing

Rebuilt on current main so this carries the hardened parser from #16148 and the corrected
resolver from #16157 (authority-scoped run keys, no order-dependent duplicate resolution).

Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes
an action — routing decides which real agent pane receives a message — so ranking title last is
not enough; the floor removes it from consideration entirely, and no amount of reordering by a
caller can bring it back.

The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did
not launch, and whose foreground process the host cannot read, is no longer addressable by @agent.
Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an
undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry
launch/foreground evidence is the open question, and is what live validation must answer.

* fix(pty): preserve agent identity on daemon reattach

* fix(runtime): retire stale pane agent identity

* chore: normalize runtime types formatting

* fix(agent-status): identify a pane from its own hook, not from how it was started

Two defects, one cause: identity was inferred from the outside instead of read from the agent.

**Hook evidence was never plumbed in.** The publisher considered `process`, `launch` and `title`
and contained zero hook references — while the resolver ranks `live-hook` first. The top rung of
the ladder was never connected.

That made identity depend on Orca having launched the agent. Most agents are started by typing
`claude` or `codex` at a shell, which leaves no launch record. On macOS the foreground process
still names them, so the gap was invisible. On WSL the Windows host reads the foreground process
as `wsl.exe` — the distro wrapper, not the agent inside it — so those panes had no signal at all
and became unaddressable by `@agent`.

A hook is the agent reporting itself, so it survives both: no launch record needed, and no
dependency on reading a process across the WSL boundary.

**`launch` outranked `completed-hook`.** Ranking is now by TENSE rather than by how authoritative
a source sounds:

    present: live-hook > process
    past:    completed-hook > launch > sleeping-session > sibling > title

A launch record is an event, not a state — it stays true after the agent exits, which is why a
pane reused after closing its agent kept reading as the old one. A completed hook at least proves
the agent actually ran in that pane; a launch record only proves Orca tried to start one.

Neither rank was covered: all 392 existing tests passed unchanged after reordering. Mutation now
fails 2 on the old order and 4 with hook evidence removed.

Known remaining gap, deliberately not papered over: a hand-started WSL agent with no managed hooks
has no identity signal at all. Restoring a title guess there would reinstate the misdelivery this
PR exists to prevent.

* fix(orchestration): restore title as the last resort, not a forbidden source

An earlier revision passed `minimumSource: 'launch'` so routing could not see a title at any rank,
reasoning that a display string must never authorize a write. That conflated the evidence parser
with the raw substring match it replaced.

`buildAgentNameRe('claude').test(title)` was the misdelivery. `collectAgentTitleEvidence` returns
null on exactly those shapes: "Review the Claude session-history fix" on a Codex pane yields
nothing, and "Switch Claude and Codex off the load balancer… - grok" yields grok from its owner
suffix. Ranking title last is therefore sufficient; refusing it is not necessary.

Refusing it had a real cost. An agent a user starts by hand inside an Orca WSL terminal has no
launch record, no readable foreground process (the Windows host sees `wsl.exe`, not the agent in
the distro), and — until managed Codex hooks install there — no hook either. An unambiguous title
was the only thing left, and dropping it made that pane unaddressable by @agent where the previous
code could reach it. That is a regression, and most agents are started that way.

End-to-end coverage added at the routing layer with title allowed: @claude still does not reach a
Codex pane whose task text names Claude, @codex still does not reach a Grok pane whose task text
names Codex, and a pane identified only by an unambiguous title is reachable again.

* revert(agent-status): keep launch above completed-hook until run keys exist

Reverts the tense-based reorder from this branch. The reasoning behind it was sound as far as it
went — a launch record is a past event, not an observation, which is why a reused pane kept reading
as its previous agent — but it fixed one staleness by opening a worse one.

A completed hook is past tense too, and without an agent-run key it never expires at all. Ranking
it above `launch` lets a stale hook from a previous agent outrank the launch record Orca stamped
for the process running NOW. pane-agent-owner.ts already says this in its own comment: "Ranking
launch/live-hook above the completed/sleeping records keeps a genuine pane on its real agent and
stops a stale record from hijacking it."

The reorder belongs with authority-scoped run generation, which is what makes any past-tense
evidence expire. It is staged in the migration plan rather than shipped here.

What this branch keeps: hook evidence feeding pane identity (so an agent a user starts by hand is
identified from its own report rather than needing a launch record), and title restored as a
genuine last resort behind the evidence parser.

* fix(runtime): guard the pane key so terminal.list survives a non-UUID leaf

`makePaneKey` throws on a leaf id that is not a UUID. The hook-evidence lookup called it unguarded
inside `buildTerminalSummary`, so a single such leaf took down `terminal.list` for the whole list
rather than degrading that one pane — 136 tests across 5 files, and the native code-quality gate
tripped separately on a duplicate test title.

Both were mine, and both were caught by CI rather than by me: I ran the focused suites before
pushing instead of the affected directories.

* fix(runtime): declare published terminal agent identity

* fix(runtime): demote completed hook identity evidence
2026-08-27 15:53:05 -07:00
Brennan Benson 0665c758d1 feat(settings): name the agents the Chat UI supports (#16830)
The Chat UI setting described itself in terms of "supported agent terminal panes" without naming them, and an unsupported agent falls back to the terminal silently — no toast, no toggle, no explanation. A user on OpenCode reported this as a bug in Discord.

Adds a "Supported agents:" icon row under the toggle, matching the existing StatusBarUsageEmptyCta legend pattern, driven by the same list the availability predicate uses so it cannot drift from actual support. Icons carry role="img" plus a tooltip for identification. Also adds the missing openclaude/omp settings-search keywords.
2026-08-27 15:49:52 -07:00
Neil 6cdae26e1c fix(source-control): allow an empty AI-generated PR description in Create PR (#16873) 2026-08-27 15:36:15 -07:00
Brennan Benson efb4050f4d fix(ai-vault): index Cline sessions (#16814)
* fix(ai-vault): index Cline sessions

* fix(ai-vault): constrain Cline session discovery
2026-08-27 15:33:26 -07:00
Brennan Benson 419e3b4496 Fix terminal reads that flatten composer drafts into output (#16711)
* fix(terminal): separate composer drafts from read output

Rendered screen reads treated cursor-line suggestion overlays as PTY output. Detect composer-owned text from cell attributes and cursor context, remove it from tail, and expose it as structured draft metadata.

* fix(terminal): handle wrapped composer overlays

* fix(terminal): preserve draft wrapping and tail alignment

* fix(terminal): preserve composer wrap boundaries

* fix(terminal): preserve draft continuations with middle dots

* fix(terminal): recognize configurable Codex status lines
2026-08-27 15:28:09 -07:00
Brennan Benson bc3911a3a6 fix(native-chat): separate image attachment paths from following prompt text (STA-4993) (#15820)
* fix(native-chat): separate image paths from following prompt text (STA-4993)

Native Chat image send wrote a framed path and then the prompt with no
separator, so after the TUI unwrapped the paste the two glued together
(`…pngdescribe`). Put a trailing space after the frame when text follows,
share that rule with clipboard image paste and terminal drops, and split
the image send path out so the runtime send file stays under the line cap.

* refactor(native-chat): keep image separator fix focused

* fix(native-chat): keep consecutive image frames bare

* test(runtime): update export parity for terminal degradation

* test(native-chat): pin image frame separator contract
2026-08-27 15:26:39 -07:00