mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
5a33e2acb05286d4aa03382e8bcfeeb023078f59
8101
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5a33e2acb0 | perf(editor): reuse Markdown source blocks while positioning review notes (#18895) | ||
|
|
926f3ff585 | perf(browser): assemble fragmented tunnel frames once (#18893) | ||
|
|
bf87b1290f |
perf(repos): avoid quadratic icon source scans (#18892)
* perf(repos): avoid quadratic icon source scans * perf: avoid repeated malformed HTML icon scans * bench: balance icon parser timing samples |
||
|
|
1c41d59203 |
perf(relay): drain fragmented frame buffers in linear time (#18891)
* perf(relay): drain fragmented frame buffers in linear time * style: follow block-body lint in relay buffer checks |
||
|
|
681119dc05 |
test: isolate window mocks from inherited launch flags (#18989)
* test: isolate mocked window activation from inherited launch flags * Preserve background window regressions added on main |
||
|
|
84432d3aa1 |
fix(native-chat): repair a structured chat tab permanently fenced by an inherited publication epoch (#18906)
* fix(native-chat): repair a structured tab fenced out by a returning publisher A publication epoch is retired whenever another publisher takes over a worktree, and a retired epoch is then rejected forever. But a live publisher can return after transient interlopers - a `removed:` retraction, then a headless rebuild whose version restarts at 1 - and the structured tab publish inherits the worktree's existing epoch rather than minting one, so it arrives under the blacklisted epoch and is dropped. The chat tab never reaches the tab bar. The fence is right to reject the frame: it cannot tell a returning publisher apart from a delayed frame queued by a dead generation, whose version can outrank the live cursor. So the drop is no longer final - it schedules one bounded, debounced authoritative `session.tabs.listAll`, and only that census may revive an epoch, and only the one it names current. Subscription frames stay fenced exactly as before. * fix(native-chat): decay the structured tab repair cap and prune its state The attempt cap latched: three transient RPC failures left `exhausted` set for the renderer's lifetime, permanently hiding a chat tab behind a single console warning. It now decays, so a worktree that has been quiet for a minute gets its full budget back. The repair map was also missing from the sweep that drops publisher cursors for vanished worktrees, leaking an entry per deleted worktree. Pruning it there required inverting the repair lane's dependency on the inventory refresh, which is now injected. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
6031c19e9f |
ci: reduce dependency, checkout, and test deadline overhead (#18968)
* ci: reduce dependency, checkout, and test deadline overhead * ci: avoid generic E2E jobs for native-only IME changes |
||
|
|
d7722a698c | test: drain project menu focus restoration before teardown (#18971) | ||
|
|
b6ca8dad99 |
fix(hooks): register the Claude hook script directly on Windows (#18875) (#18905)
* fix(hooks): register the Claude hook script directly on Windows (#18875) The Windows Claude Code lifecycle hook was registered as `powershell.exe -NoProfile -EncodedCommand <...>` whose entire decoded payload was a `Test-Path` and a call to `~/.orca/agent-hooks/claude-hook.cmd`. Every hook event paid a full PowerShell start-up to reach a script that exits at its first `ORCA_PANE_KEY` guard, so sessions outside Orca paid it to do nothing. Register the script path itself instead, with `|| echo {}` for the neutral-JSON-when-missing contract (#14818). Measured on Windows 11, invoked as Claude Code invokes it (`printf payload | bash -c -l "<command>"`): idle (n=12) baseline 177ms | before 471ms | after 213ms 10-way conc (n=40) -- | before 656ms | after 296ms p95 under load -- | before 696ms | after 337ms It also drops an interpreter from the chain the hook's timeout kill must tear down. Killing the hook does not kill its PowerShell grandchild, which still holds the stdout handle the agent reads to EOF -- measured, EOF arrived 352ms AFTER the kill, when the orphan exited by itself. msys2 creates children suspended and resumes them after, so a kill landing in that window strands one that never exits and EOF never comes; that is the reported frozen session. The encoded launcher stays as the fallback for profile paths the shells cannot carry bare (space, `%`, `^`, `&`, non-ASCII) and for hosts where Git Bash is not resolvable, because PowerShell 5.1 rejects `||`. Every other agent's hook is untouched, as is the remote/SSH path. Not adopted from the report: `cmd.exe /d /c <path>` (MSYS rewrites the `/c` under Git Bash -- measured, the invocation fails), and raising the 10s timeout (the orphan survives the kill regardless; the fast path puts the hook 30x under the budget so the kill effectively stops firing). * fix(build): list the new hook launcher modules in the CLI tsconfig project config/tsconfig.cli.json enumerates its files explicitly, so the two new imports reached by src/main/claude/hook-settings.ts failed tc:cli with TS6307. src/main/git-bash.ts pulls in only node:fs, node:path and a shared constant, so it adds nothing heavy to the CLI project. * fix(hooks): address review of the direct Windows Claude hook launcher - Make the Windows hook suites host-independent. A box with a cmd.exe AutoRun (HKCU\...\Command Processor\AutoRun) failed them at HEAD too: the tests redirect USERPROFILE, the AutoRun target vanishes, and MSYS spawns a .cmd without /d so AutoRun runs and lands on the hook's stderr. Seed an empty target, including under the deliberately-absent profile. - Note in managed-hook-stdin-lifecycle why the "missing managed script" case no longer exercises the fallback for the direct shape (it carries an absolute path, so a redirected profile changes nothing); that path is covered live in windows-direct-cmd-hook-command.test.ts. - Keep the direct shape off UNC profiles: WINDOWS_CMD_SAFE_PATH admits them, but //server/share/... is not a command cmd.exe reliably starts. - Correct the comments: `|| echo {}` also fires when cmd.exe itself exits non-zero (failing AutoRun), printing {} twice. The encoded launcher exited 1 on that same box, so neither shape is clean there. - Test the contract that replaced runtime %USERPROFILE% resolution (STA-3348): a stale absolute path reports not_installed and is rewritten on install. - Record the standing unmeasured assumption in windows-edr-posture.md: `||` does not parse in Windows PowerShell 5.1, so a compat consumer that hosts hook strings there would fail closed. Measure before widening to another agent. - Trim the launcher comments per AGENTS.md; the numbers live in the doc. * test(win32): register the new Windows-gated hook test in the CI lane win32-test-lane-registration guards against exactly this: a Windows-gated file that self-skips on ubuntu and reports success, so it runs on no machine. The new windows-direct-cmd-hook-command.test.ts needs both entries — WINDOWS_PACKAGE_TESTS decides whether package_windows runs for a diff, and the workflow argv decides whether the file runs once that job started. * test(win32): remove the hook temp tree through the retrying helper windows-lane-tree-removal-boundary scans exactly the specs in the Windows CI lane, so registering windows-direct-cmd-hook-command.test.ts subjected it to the rule: cmd.exe and bash have just exited in that tree, and a raw recursive rm throws EPERM on Windows while their handles drain, turning a green spec into a lane failure. Use removeTreeSync, which carries the repo's maxRetries policy. --------- Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
9f0054d89c |
ci: skip idle Mac allocations and redundant native compiler setup (#18954)
* ci: avoid idle Mac allocations and cached native toolchain installs * test: anchor artifact fixtures before their fixed expiry |
||
|
|
54a8afc91d |
fix(orchestration): typed error codes for dispatch and worker-start refusals (#18902)
* fix(orchestration): typed error codes for dispatch and worker-start refusals orchestration dispatch (and worker-start, which composes it) surfaced task not found, task not ready, and inject rejected as the same bare runtime_error, so an agent reading the receipt could not choose between creating the task, waiting on dependencies, or picking another terminal. Add task_not_found (data.taskId), task_not_ready (data.status, data.unmetDependencies), and inject_rejected (data.terminal, data.reason), each carrying data.nextSteps so every shipped CLI already prints the recovery. worker-start's not-ready refusal moves from task_not_startable to task_not_ready with the same detail. runtime_error stays for genuinely unexpected failures. Proven red-first from RpcDispatcher through the CLI's own failure formatting, plus an SSH bridge test that the host CLI's typed refusal relays unchanged. * test(orchestration): load CLI formatter at runtime in the dispatch-code test The composite node typecheck (config/tsconfig.node.json without --composite false, as CI runs it) rejects a static import of src/cli from a main test with TS6307. Load the formatter and error class dynamically behind narrow structural types, as the CLI/runtime boundary test does. * fix(orchestration): keep task_not_startable and split the CLI-format proof Review on #18902: - Drop task_not_ready. worker-start already published task_not_startable for a not-ready Task, so renaming it would change an existing receipt value under old clients. dispatch now emits task_not_startable too (it was a bare runtime_error before, so this is purely additive), with the new data.status / data.unmetDependencies / data.nextSteps. - Move the refusal receipts (code, message, data) into src/shared/orchestration-dispatch-refusal-contract.ts so the runtime emits them and the CLI test formats the identical envelope. The RPC test under src/main asserts toEqual against the contract; the new src/cli/orchestration-dispatch-refusal-format.test.ts feeds those same receipts to formatCliError / reportCliError. Neither tsconfig widens and the composite typecheck CI runs is clean. * fix(orchestration): keep published refusal messages and type the DB claim guards Codex review of #18902: - Every call site keeps the exact message it published on main ("Task not found: <id>", "only a ready Task can start.", "cannot retry from Dispatch"); the shared contract now takes the message per site and only owns the code and data. Baseline strings are pinned as literals. - createDispatchContext's own missing/non-ready guards, including the atomic-claim loser, now emit the same typed receipt instead of a bare Error, so a dispatch that races a status change no longer flattens to runtime_error. Covered by a dispatcher-level race test. - Invalid --retry-of keeps task_not_startable but now carries status, unmetDependencies, retryOf, and a retry-specific next step. - Dependency recovery text distinguishes waiting on running deps from retrying/unblocking failed ones. - CLI test adds an unknown-code case so the old-client claim rests on an assertion, not a comment; SSH test asserts exact stdout. - Guide table narrowed to the covered preflight cases; occupancy stays runtime_error and is named as such. |
||
|
|
71f2c5d3f9 | test: keep artifact share fixtures unexpired across calendar dates (#18955) | ||
|
|
abdee9ebd3 |
feat(automations): restore column sorting on the list (#18885)
The flat-table redesign in #16532 dropped the sort UI, orphaning AutomationListSortHeader, nextAutomationListSort and the whole AutomationListViewItem layer. Wire them back to the rendered list. Name and Last run become interactive header cells again; the other six columns stay plain text. Sorting now spans local and external rows as one list, so the panel renders per-row components from a single sorted collection instead of two independent sections. Two model fixes fall out of that: - View items key on the host-qualified row key, not the bare automation ID. The old builder predated automation-list-row-identity, so under All hosts two authorities returning the same ID collapsed in the sort tie-break. - sortAutomationListViewItems takes the locale as a parameter instead of reading getIntlLocale(). A hidden global read is invisible to a dependency array, and the list result is memoized. Keyboard traversal and focus recovery now read the sorted order, so arrow navigation matches what is on screen. The dead unified filter is removed in favor of the live row/entry filters the page already used. |
||
|
|
a730becd7a | fix(automation): keep explicit background launches off screen (#18898) | ||
|
|
6a5c1f9535 | refactor(agent-session): consolidate wire type imports below lint limit (#18930) | ||
|
|
c58d7a0ecd |
fix(agent-session): never open a sibling terminal on an unproven create (#18735)
An `agentSession.create` the host could not confirm — it committed the session but could not publish its tab, and answered `agent_session_operation_unknown` — was rejected with a bare `Error` carrying a `code`. Nothing in the type said "unknown", so the verdict lived only in the code string, and the shared transport matcher was still free to re-read that error's *message*: an unknown refusal whose text ends in a definitive token (`Owner check failed: method_not_found`) classified as definitive, which is exactly the answer that permits a legacy sibling terminal. Make the class the verdict. `StructuredAgentSessionCreateUnknownOutcomeError` is a sibling of `StructuredAgentSessionCreateRefusalError`, not a subclass, so the nine existing `instanceof` consumers keep reading "refusal" as "you may fall back" with zero edits, and an unknown outcome flows down the lost-reply path instead — replaying the same envelope, re-publishing the tab the host failed to publish, and parking as visibility-unknown rather than creating anything. Classification now short-circuits on our own classes, so a message we wrote can never invert the verdict we already reached. Adds an end-to-end guard that drives the real classifier through `startStructuredAgentLaunch`: an unknown outcome opens zero legacy terminals, a definitive refusal opens exactly one. Ablating the branch turns that green suite red with `['legacy-terminal']` — the duplicate session the guard exists to prevent. Co-authored-by: Merge Sim <sim@local> |
||
|
|
8ab8c950be |
fix(native-chat): tell a pre-SQLite chat how to carry on (#18808)
A chat whose journal is still the pre-SQLite `log.jsonl` opened empty and indistinguishable from one created seconds ago. It now carries one status row naming the transcript still on disk and saying to send a message to continue, and read restore no longer drops such sessions — an unpublished chat had its tab pruned from persisted state, leaving nowhere for the message to appear. The notice survives a crash between the epoch commit and its append (re-offered while the epoch holds nothing) and stays out of a journal the same open just repaired, where it would have retired the unreconcilable_prefix marker and permanently ended provider-history recovery. No importer: the history is explained, not replayed. Nothing reads the remnant beyond its existence, and nothing moves or deletes it. |
||
|
|
f8780a2c86 |
feat(native-chat): stop monitored tasks individually (#18807)
* feat(native-chat): stop monitored tasks individually * test: expect Claude task stop capability --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
2513e21390 |
fix(native-chat): publish structured session status from the host so the sidebar never goes stale (#18776)
* fix(native-chat): publish structured session status from the host The sidebar learned whether a structured chat was mid-turn by replaying the session journal in the renderer, through a reader whose lifetime was tied to the chat pane. Hiding the pane stopped the reader before the turn's settlement arrived, so the row stayed on "working" until the chat was reopened. The same coupling meant a tab never opened this session showed no status at all, and a reloaded renderer lost every settled row. The host owns the journal, so it now projects each session's status once per journal publication and fans the changes out on one stream per client (`agentSession.subscribeStatus`). The projection survives eviction of an idle session's provider child and is republished when readable sessions are restored. The renderer bridge subscribes to that feed per runtime target and never opens a transcript reader; the observation hook is gone. Additive wire surface behind the existing structured capability; old hosts reject the method and the renderer retries, showing no status. * fix(native-chat): negotiate the status feed and stop losing a change on subscribe The status stream is additive to a surface that already shipped, so a host advertising agent-session.structured.v1 can still answer subscribeStatus with method_not_found. Every renderer error path reconnected, so a remote host one release behind got a relay round-trip every 5s and no sidebar status at all. Give the method its own capability and probe it before subscribing; a failed probe still retries, an absent capability does not. Re-projecting on subscribe also wrote straight into the shared cache, so a second client could pin the first to a stale summary. Route those diffs through publish() before the arriving subscriber is registered. * fix(native-chat): bound the status prompt, merge snapshots, and prove the unread path One status frame carries every retained session and a send admits 256 KB per prompt, so ~16 large-prompt sessions could push the snapshot past the 4 MB outbound guard and into the retry loop. Bound latestPrompt to the same 200-char single-line preview every other agent-status row already carries. A snapshot also replaced the cached map wholesale, so the empty first frame from a restarting host retracted every row before restore republished them. Merge instead; the tab map, not this feed, decides which sessions are listed. Tests: the hidden-pane claim now sits at the host, where a journal with no transcript subscriber is driven from running to idle; the RPC test reads a real projection instead of its own stub. * fix(native-chat): merge the duplicated status-event type import * test(native-chat): pin the restart status publication, and log the unsupported host Startup restore indexes a readable session and publishes its status, which is what puts a never-reopened tab back in the sidebar. Only an Electron screenshot covered that wiring; a sitting status subscriber now pins it directly. The terminal "host too old" branch was silent, so a mixed-version report showed an empty sidebar with nothing in the log to explain it. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
471a5f4aa7 |
feat(native-chat): model Codex MCP and web-search items instead of leaking opcodes (#18763)
* feat(native-chat): model Codex MCP and web-search items instead of leaking opcodes
Codex's app-server sends 19 thread-item types; the structured translator handled
six. The rest fell through to a generic gray `codex · item:<type>` row, even
though the disposition table's own comment says it exists so a new item type
cannot leak like that — the table had one entry.
Give `mcpToolCall` and `webSearch` real tool-call bodies, and chrome `sleep`,
which carries only a duration and renders as nothing in Codex's own TUI.
`subAgentActivity` and `collabAgentToolCall` deliberately keep their generic
rows. They arrive in real sessions today and are currently the only visible
sign a subagent is running; hiding them before the subagent UI lands would
render minutes of work as an idle turn. Tests pin that they stay visible.
MCP tool names pass through verbatim when they contain `:`, `.`, `/` or `__`,
so `mcp__server__tool` survives instead of being title-cased into nonsense.
* fix(native-chat): keep Codex MCP tool identity and web-search results on the row
Four fixes to the Codex MCP / web-search item bodies:
- Drop the title-casing display name. `get_forecast` became `Get Forecast`,
which no longer matches the raw snake_case identifiers that the diff
renderer, question parsers, and tool-input previews dispatch on, and does not
match how the Claude lane or the sibling `shell`/`apply_patch`/`web_search`
bodies name a tool. The row name is now `server/tool` verbatim, the bare
`tool` when no server is given, and `mcp` when the item names no tool at all.
Server-qualifying also stops an MCP tool that happens to be called
`apply_patch` from hijacking the diff renderer.
- Pass the MCP call's own `arguments` as the tool input instead of wrapping it
in `{server, tool, arguments}`. Row-label derivation only reads top-level
keys, so the wrapper degraded every MCP row to a truncated raw JSON blob.
A non-object `arguments` stays addressable under a key rather than being
dropped; an absent one becomes null, which labels as empty rather than `{}`.
- Carry a web search's `results` as the call output, bounded like every other
inline payload and omitted when there are none. They were being dropped
entirely, which showed less than the generic fallback row it replaced.
- No streaming branches were added for these two item types: the Codex delta
stream is a closed set of six methods that neither can reach, so such
branches would be unreachable.
* fix(native-chat): label Codex web searches and argument-less MCP calls
A row label is derived from top-level `input` keys only, so a webSearch
whose detail lives inside `action` — an opened page, an in-page find, or
a bare `other` — fell through to the raw JSON of the whole input, as did
the empty `query` Codex leaves on a completed search. Hoist the action's
`url`, `pattern` and `type` beside the query, keep the full `action`
object so the expanded detail loses nothing, and emit no input at all for
the start frame.
An MCP tool that takes no arguments sends `arguments: {}`, which passed
straight through and labelled the row a literal `{}`; treat it as absent
so the row reads as a bare `server/tool`.
Split the durable-identity half of the item translator into
`codex-thread-item-identity.ts`, re-exported so every existing import is
unchanged, to keep both files under the max-lines cap.
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
d7767fb196 |
perf(worktree): remove redundant creation and terminal startup work (#18793)
* perf(worktree): remove redundant creation and terminal startup work * test(worktree): cover optimized creation call signatures Preserve explicit branch adoption, WSL callback routing and sparse cleanup expectations. * perf: preserve user Git checkout worker settings * perf(git): skip malformed remote base probes * perf(cli): avoid loading other agent hooks for Codex preflight * fix(build): retain Codex preflight entry for packaged CLI * test(ssh): wait for replacement PTY before lease recovery input * test(ssh): verify recovered shell execution and lease ownership * test(electron): reap isolated macOS crash reporters on teardown * test: allow either observed self-exit snapshot ordering * test: capture frozen-host input recovery evidence |
||
|
|
51eed5a1bc |
feat(cli): report SSH host platforms (#18896)
* feat(cli): report SSH host platforms * feat(cli): include SSH connection status * fix(cli): preserve unknown SSH connection state |
||
|
|
cd70048092 |
Fix favicon retention across same-origin navigations (#18879)
* fix: retain favicons across same-origin navigations Move favicon clearing from did-start-loading to did-start-navigation and only clear when origin changes. Chromium re-announces favicons only when the icon URL list changes, so clearing on every load orphans same-origin navigations. Extract favicon URL validation into a shared module. * fix: drop favicon on cross-origin redirects When a same-origin navigation redirects to a different origin, the favicon should be cleared to prevent stale icons from displaying the wrong site's identity. |
||
|
|
ddc5b75ac7 |
feat(native-chat): label Codex tool rows by what the command actually did (#18760)
* feat(native-chat): label Codex tool rows by what the command actually did
Codex's app-server `commandExecution` item carries `commandActions`, which
already classifies each command as a read, a search, or a directory listing
with the target path, name, or query extracted. Orca ignored the field, so
every shell call rendered as an undifferentiated row of raw argv.
Read it and name the row by its class, keeping the raw command and cwd for the
expanded view. Unclassified commands are untouched: absent, null, or malformed
`commandActions` produces byte-identical output to before.
Rank the search term above the command in the shared label keys so a classified
search row reads by what it looked for rather than the shell text that ran it.
No first-party tool input carries both keys today, so this only reaches the new
rows; an MCP tool supplying both would prefer its search term.
Note `commandActions` is the app-server spelling. `parsedCmd` is the rollout-file
shape and never arrives on this lane; a test pins that it stays ignored.
* feat(native-chat): give tool rows a category glyph beside their word
A row named only by a word makes the reader parse text to tell a read from
a search. Pair the word with an icon: icon for category, word for action,
argument for target.
Name the full eight-category vocabulary in `src/shared/native-chat-tool-icon.ts`
now — read/search/listFiles/unknown/fileChange/webSearch/mcpToolCall/
subAgentActivity — even though only the classified shell categories reach a row
today, so the MCP and web-search rows landing separately inherit these names
rather than coining their own. Glyph ids are the lucide spelling shared by
`lucide-react` and `lucide-react-native`, so mobile can resolve one name to its
own component when it adopts this; mobile rows stay text-only for now.
The glyph is decorative and `aria-hidden`: the word is the accessible name, and
never renders without it. One glyph per category, fixed across running,
completed, and failed — a row that swapped icons on completion would read as
changing identity — so the run header's active row also takes its category glyph
instead of the generic wrench it fell back to once these rows stopped being
called `shell`. A word outside the vocabulary gets the terminal glyph rather
than a blank slot, so rows stay left-aligned.
Also stand `.` in for a `listFiles` action whose `path` is null, which is what a
bare `ls` sends. The row named the action and then showed the raw argv as its
target; now it names the directory it listed.
* fix(native-chat): hold the tool run header's glyph fixed and size its slot to 16/14
The header swapped its leading glyph on settle: the active tool's icon while
running, a check once done. That is the identity swap a fixed per-category glyph
exists to prevent — the row appeared to become a different thing when it
finished. Name the header by the run's latest tool in both states and move the
completion check to the trailing edge, where the rest of the state signal already
lives.
Size both header slots to the mock's 16px slot with a 14px glyph, matching the
tool rows beneath them and the subagent summary row landing separately. They were
24/16, so the icon columns sat 8px apart and broke the left alignment the icon
treatment depends on.
The fixity test walks running, completed, and failed and pins the leading glyph
of every row by lucide's own class name, so a swap shows up as a different name
rather than a still-present icon.
* fix(codex): stop a classified shell row from asserting facts the command doesn't support
Three claims the `commandActions` row model was making on its own:
- `listFiles` with a null path was given `path: '.'`. Codex sends null for a
recursive walk and for the repo root, and the invented path flows into
`createToolInputDisplay().filePath`, which mobile turns into a tappable
"open file" link onto a directory — an affordance that can only fail. The row
now keeps the raw command, which is what the label logic already falls back to.
- A command whose actions classify as two different things (`cat a.txt && ls src`)
was named after the first one, silently dropping the rest. Recognized actions
must now agree on one class; a repeat of one class keeps the class and only a
target every entry names.
- `read` lifted `name` into the journal payload, where no label ever reads it —
`path` always wins — so it was bounded weight carrying nothing.
* fix(native-chat): give an unmodelled tool row a generic glyph, not a terminal
The row-word vocabulary named seven words, and everything else fell through to
the terminal glyph — which reads as "a shell ran here" for rows where nothing
says one did. Codex's own `apply_patch` row, `Grep`/`Glob`/`Task`/`WebFetch`/
`TodoWrite`, and every `mcp__*` tool all rendered a terminal, leaving the
declared `mcpToolCall` and `subAgentActivity` categories unreachable.
- Split the vocabulary: `unknown` stays the shell command Codex could not
classify and keeps the terminal, while a new `other` carries the generic
wrench that unmodelled words now fall back to.
- Read the edit family from `EDIT_TOOL_NAMES` and the command tools from
`isCommandToolName` rather than restating either. Command tools resolve first:
`isEditToolName` counts `shell`/`exec` as possible patch carriers, and a shell
row is not an edit.
- Result rows get no category glyph. Their word is `translate(…, 'Result')`, so
keying a category off it resolved a different glyph per locale; an empty slot
keeps the rows aligned.
- The header and the row now resolve through `NativeChatToolIcon`, so one `Grep`
run can no longer show a wrench in the header and a terminal on its line. The
glyph map and the unused `category` prop go with the duplication.
* fix(native-chat): give the projected Diff row the file-change glyph
Every Codex fileChange item projects to a tool call named `Diff`, which the
edit set does not name — it names the tools that carry the edit in their own
input. So a run whose body renders an edited-file card was headed by the
generic wrench.
* fix(codex): stop a classified shell row offering a folder as a file to open
A listFiles action's path is a directory, and a search action's path is the
root it scanned. Lifted under `path`, both became the row's file target, which
mobile renders as a tappable open-file link that can only fail — the same dead
link the removed `{ path: '.' }` stand-in would have produced. They lift to
`directory` instead, which still labels the row but is never a file target.
* fix(mobile): keep the terminal glyph on a classified Codex shell row
Mobile's run header picks between a terminal and a generic glyph by tool
name. Now that the host publishes `read`/`search`/`list` for the same
commands it used to publish as `shell`, that name check answers false and
a command that really ran heads its run with a wrench.
Ask the shared category vocabulary instead. Mobile keeps its two icons —
porting the full glyph set is a separate lane.
* fix(native-chat): say what the run header's glyph actually guarantees
The comment claimed the header names the same tool in both states, so its
glyph cannot change on settle. It can: the live header names the running
call while the settled one names the run's last tool call, and with
out-of-order completion those differ. The glyph is fixed for whichever
tool the header names — say that, and drop the never-taken running branch
from the settled header's call.
Also pin the other half of the file-target rule: `read` keeps `path`, so
its row stays tappable, where `list`/`search` lift a folder to
`directory` and offer no target at all.
* fix(native-chat): give a rollout-transcript shell row the terminal glyph
`exec` and `local_shell` are what the Codex rollout transcript names a
shell call — `native-chat-edit-normalize` already treats those three
words as the command tools — but the activity set the glyph vocabulary
reuses carries neither, so both rows headed a real command with the
generic-tool wrench.
Named in the vocabulary rather than in that activity set, because that
set also picks the running row's copy and this is only about the glyph.
* fix(mobile): pick the run-header glyph from the call's input, not its word
Codex now names a classified shell row `read` / `search` / `list`, which
lowercase to Claude's own `Read` / `Grep` / `Glob`. Mobile has only a terminal
and a wrench, so keying that choice on the row word gave Claude's filesystem
tools a terminal for a shell that never ran.
The input separates them: Codex keeps the raw command on a classified row,
while Claude's `Read` carries only a file path. `isShellActivityToolCall`
replaces `isShellActivityToolRow` and asks the command tool names first, then
the call's input.
* fix(native-chat): give the projected diff fixture its required digest
* fix(native-chat): head a settled run with a glyph the whole run shares
The settled run header drew the glyph of the run's last tool call while the
text beside it summarizes the run's first three, so a ten-call run ending in a
`read` showed an eye above "shell npm test · shell git status · …" — a category
the summary never described.
Resolve the header's glyph from every call in the run instead: the shared
category's glyph when all agree, the generic tool glyph when the run spans
categories, and no glyph when there are no tool calls. The running header still
names the active call, whose glyph is true of it.
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
1924c8f5b1 |
feat(perf): lint repeated sort setup and schedule regression contracts (#18822)
* feat(perf): audit comparator setup and schedule performance contracts * test(sqlite): close readers after expected busy failures * ci(perf): trigger contract workflow on the contract files themselves Without these paths a contract rename lands green on PR CI and only breaks the next nightly, where nobody owns the failure. Also run the OS-independent source audit once instead of on all three runners. |
||
|
|
61ebffa86e |
fix(runtime): bound terminal-wait blocked-prompt rules to the live screen bottom (#18817)
The Codex prompt rules in terminal-wait-detection scanned the whole retained tail (up to 256 KiB) with lastIndexOf, so any quoted prompt phrase in scrollback registered as a live prompt. A Codex agent working on Orca prints rg hits from this very file; one such line ~300 lines above an idle input box made `orca terminal send` refuse with agent_prompt_blocked and `terminal wait --for tui-idle` report codex-interactive-prompt. `clear` did not help because the detector reads the retained tail, not the visible screen. A prompt that owns the terminal is at the screen bottom, so every blocked rule now runs over the last 12 non-blank lines (real Codex dialogs are 4-8 lines), the way the cursor approval rule already was. The returned index is offset back into full-tail coordinates so ready-header comparisons keep working. The sentinel fast path is unchanged. Line-window primitives move to terminal-wait-tail-window.ts to keep the detector under the max-lines cap. |
||
|
|
9c3d957ae3 | perf(automations): reuse collation setup for list name sorting (#18823) | ||
|
|
062db77118 | reduce: lower tab minimum width from 88px to 72px (#18871) | ||
|
|
af82126058 |
fix(native-chat): give the Claude exit barrier a handle on unpublished exits (#18826)
A first-hand Claude exit is not published where it is observed. `handleExit` re-enters the close ladder and persists the transcript cursor before it emits `ended`, and only that emission reaches the runtime's recovery chain. So the runtime's `waitForRecovery` — whose whole job is to drain an in-flight recovery before teardown stops children — returns immediately for an exit that is still climbing the ladder, and nothing outside the adapter can tell an observed exit from a published one. The integration test for fenced host reconciliation had no handle on that barrier, so it bounded-polled the lease for 100ms instead. Measured under 16x local concurrency, publication alone takes 77-204ms: 19/24 runs failed. Retain the ladder-then-settle tail on the exit record and expose `drainObservedExits`, fold it into `waitForRecovery`, and export the barrier so a caller that needs the settled lease can await it. Codex publishes inside its own exit callback and needs nothing. The test now awaits the barrier: 0/24 under the same load, and it fails on an idle machine without the drain. |
||
|
|
265871c53d |
fix(native-chat): stop a settling handoff throwing an unhandled rejection at teardown (#18824)
* fix: stop a handoff flow from outliving the host that owns its session A structured handoff runs on the session's serialized chain and nothing in production awaited it. When the client-side deadline for the switch expired first, teardown dropped the session map out from under a live flow, and the flow's own failure notification then threw `agent_session_ownership_unknown` out of a status publish — an unhandled rejection, plus journal rows written into a directory that was already being removed. Three fixes, each with a regression test that fails without it: - The status publish is a notification, not a mutation: it now reads the fence without requiring an attached session, so an evicted or torn-down session makes it a no-op instead of a throw. - `track` used `.finally`, which forwards a rejection onto a promise nobody awaits. `drain` settles flows through `allSettled`, so the bookkeeping chain is now settle-only and cannot resurface one. - Host teardown drains in-flight handoffs before dropping the session map. `drain` existed for exactly this and was never wired up. The integration test's `vi.waitFor` is dropped rather than widened: the request enqueues the flow on the session's serialized chain before it returns, so the status read is already ordered behind it. The poll only added a wall-clock deadline that a loaded runner missed. * fix: bound the handoff drain so a wedged flow cannot hold the quit open |
||
|
|
a823f97d63 | perf(worktree): skip remote probes with no possible result (#18821) | ||
|
|
1a76a11e39 | feat(i18n): localize onboarding flow to Japanese (#18787) | ||
|
|
0bbf86bb7a |
fix(renderer): stop a Node-only process-table module blanking the app at boot (#18814)
Every Electron E2E spec that boots the app has been failing on `workspaceSessionReady did not become true`, and the app itself has been launching to a blank white window: the renderer threw `ReferenceError: process is not defined` while evaluating a shared chunk, so React never mounted and no startup step ever ran. `agent-completion-poll-interval.ts` (renderer) imported one constant, `PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS`, out of `shared/process-table-snapshot-reader.ts` — a `node:child_process` / `node:fs/promises` module whose dependency evaluates `process.platform` at module scope to pick `ps` columns. The renderer runs sandboxed with contextIsolation, where `process` is undefined, so that module-scope read threw and took the whole chunk with it. Introduced by #18742; #18780 added a second module-scope read next to the first. The constant now lives in `shared/process-table-snapshot.ts`, the environment-neutral half of the pair, and the reader re-exports it so host callers are unchanged. The two `ps` column sets read the platform behind a `typeof process` guard, which defuses the same landmine for any future renderer import of that module — only hosts ever run the argv. The regression test walks the renderer import graph (lazy routes included) from all three entries and refuses any module that reaches a `node:` builtin. It fails on the pre-fix import with the full 10-hop chain from `main.tsx`. |
||
|
|
06ca54ae7c |
fix(test): stop detached git maintenance racing the divergence fixture teardown (#18810)
`worktree-base-divergence-real-git.test.ts` builds cap-sized histories (100 and 101 commits). Every `git commit` detaches `git maintenance run --auto`, whose commit-graph task arms at 100 new commits, so the fixture reliably spawns a background `git commit-graph write --split` that keeps creating `.git/objects/info/commit-graphs` entries after the synchronous exec returns. The `afterEach` recursive remove is then deleting `.git/objects` underneath a live writer and dies with ENOTEMPTY — which is how "counts drift in both directions" failed on main. Reuse the existing `GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS` (it already covers modern maintenance and legacy auto-gc, so it holds at the Git 2.25 baseline) in the fixture's git helper. Traced spawns of `git commit-graph write` over a full run of this file: 4 before, 0 after. The production path under test only runs `rev-list` and `merge-base`, neither of which triggers auto-maintenance, so there is nothing to fix outside the fixture. |
||
|
|
cec26336e5 |
fix(native-chat): say when a structured launch fell back to a terminal (#18762)
* fix(native-chat): say when a structured launch fell back to a terminal A definitive refusal already opened a terminal instead of the requested structured chat, but said nothing — indistinguishable from the bug where the wrong surface opens. Notify at message severity, since nothing failed. Also stop putting the raw error in the failure toast's description: it carried errnos and absolute paths straight into the UI. The detail moves to a warn log and the toast gets catalog copy, matching how the coded refusals already read. * fix(native-chat): avoid overstating terminal fallback --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
9927edd631 |
fix(ssh): let the host say whether it armed the ready marker (#18802)
#18796 made every SSH Codex background launch wait for the shell-ready marker, but the client cannot see the remote shell. On a host that never publishes one -- fish, sh, Windows, or a relay predating #18796 -- no marker arrives and delivery falls back at 1.5s where it used to write at 50ms. The relay already computes whether it armed the marker; publish that as an optional `shellReadyArmed` on the spawn reply and let the client skip a wait it now knows is pointless. Absent stays UNKNOWN and keeps the client's own guess, so an older host behaves exactly as before; false is only ever an answer a host gave. It rides every reply, false included, or absent would stop meaning "old host". A host that did not arm the marker did not arm bracketed paste either, so the released path still submits raw. |
||
|
|
e95d247be1 |
perf(terminal): cheap-tier process inspection for anchored local agent panes (#18780)
* perf(terminal): cheap-tier process inspection for anchored local agent panes Every idle local pane's completion cadence ran a full whole-host `ps` (with `tty=` and `command=`, 0.34-0.50s on a 1,900-process Mac, 1.15s on Linux) purely to build `foregroundProcessEvidence` that the renderer then discards for local ids. Add a cheap tier (same job-control columns, no tty/command, 0.03s) gated so that it introduces no user-facing trade-off: - Only a pane whose last FULL capture proved a recognized agent may take the cheap tier. Panes with no anchor always take the full capture, so start discovery keeps today's exact behaviour. - The cheap tick compares a per-pane fingerprint (root shell pid+start, tpgid, every descendant's pid+start+pgid+job-control state). Any change, a changed node-pty foreground name, an unreadable capture, or an incarnation mismatch escalates to the full capture. A recognized agent's exit is always a pid vanishing, which the fingerprint always sees. - A cheap answer OMITS evidence rather than fabricating a tty-less fence. Remote/restore consumers never send `steadyState`, so they keep the full capture unchanged. - `steadyState` is a new optional request field; an old daemon ignores it and answers with the full capture. Measured (8 idle panes, 60s, idle cadence, forks counted by column set): 30 full -> 1 full + 29 cheap. * fix(terminal): route the cheap ps capture through runProcess The cheap-tier reader imported node:child_process directly, which the child-process import-boundary and windowsHide ratchet tests reject (CI shards 1/8 and 3/8). Use Orca's single spawn entry point instead; it pins windowsHide and encodes argv. Map its result onto the capture-error vocabulary: outputTruncated -> capture_truncated, timedOut -> capture_timeout, non-zero exit -> ps_exit_<code>. Tests mock at the runProcess seam. * fix(perf): refuse a pane fingerprint when any descendant start marker is missing `buildPaneProcessFingerprint` rejected only a missing root start marker; a missing descendant marker was stamped as `?`. Two captures that both failed to read the same descendant therefore compared equal, which removes the pid-reuse protection the fingerprint exists to provide: a recycled pid could make a vanished agent look unchanged, and the cheap tier would keep serving its name instead of escalating. Reachable on Linux, where `readLinuxProcStartTime` legitimately returns null when a process exits between the `ps` capture and the `/proc/<pid>/stat` read. Every subtree member now needs a start marker or the fingerprint is refused, which sends the caller to the full capture — the same conservative default every other uncertain path takes. Reported by CodeRabbit on #18780. The two new tests fail against the previous code with `expected '4242@2400#4300:|4300@?:4300:+' to be null`. |
||
|
|
ccf3e27800 |
perf(relay): bound the symlink directory probes a remote readDir fans out (#18752)
`readRelayDir` issued one `stat` per symlinked entry and awaited them all in a single `Promise.all`. A pnpm `node_modules` is hundreds-to-thousands of package symlinks in one directory, so expanding it over SSH put that many stats in flight at once, saturating libuv's four-thread pool and delaying every other relay filesystem operation — including the interactive reads `fs-list-files-scan-coordinator` exists to protect. The probes now run through `forEachWithConcurrency` at 8, the cap every other bounded probe in this codebase already uses (`GIT_COMMON_SNAPSHOT_CONCURRENCY`, `PRUNABLE_EXISTENCE_PROBE_CONCURRENCY`, `SPARSE_CHECKOUT_DETECTION_CONCURRENCY`). Results and ordering are unchanged: every symlink still resolves to its target's kind, and `sortDirEntries` still runs afterwards. The new test builds a 60-symlink directory and asserts the same 60 stats happen with exactly 8 in flight at peak — the probes overlap, and never past the cap. |
||
|
|
cc07249e78 |
fix(agent-session): refuse a pre-commit structured create with an envelope (#18697)
* fix(agent-session): refuse a pre-commit structured create with an envelope The create route refused by throwing, which reaches a client as a generic transport error indistinguishable from a lost answer — so desktop parked the launch as visibility-unknown with no chat and no terminal. Convert the whole pre-commit span, everything before `attach`, into a refusal envelope carrying a code, and name the definitive-refusal allowlist the fallback decision needs. * fix(agent-session): gate legacy fallback on definitive refusals * fix(mobile): preserve unknown structured create outcomes --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
4c5077d57a |
perf(persistence): skip rewriting unchanged terminal scrollback snapshots (#18764)
* perf(terminal): tighten the partial-escape-tail benchmark and equivalence test
* perf(terminal): spell the ESC gate the same way as the sibling ingest gates
* test(terminal): differential-fuzz the ESC-free partial-escape-tail gate against the unguarded fold
* test(terminal): make the escape-tail fuzz exhaustive at symbol depth, and cap the fold expectation
Two review findings on the differential fuzz, both about the test faithfully modelling the
function it guards.
The odometer generated strings by symbol depth but the caller filtered on `chunk.length`, which
is the UTF-16 code-unit count. An astral symbol is two code units, so every depth-4 string
containing one was silently skipped and the corpus was not exhaustive at depth 4 the way the
test name claimed. The generator now yields `{ depth, text }` and the caller filters on depth.
That restores the missing strings and takes the pinned corpus from 516,566 to 593,468 - exactly
the count CodeRabbit derived for the intended corpus.
The pairing assertion in the sibling suite compared the capped `advancePartialEscapeTail`
against an uncapped `extractPartialEscapeTail(pending + chunk)`. It passed only because no
pairing in that corpus crosses MAX_PARTIAL_ESCAPE_TAIL_LENGTH; it would have stopped modelling
the function the moment one did. The cap now lives in the expectation, matching the fuzz
oracle.
Re-verified the fuzz still fails on a wrong guard: mutating the gate to a bracket check fails
all four tests with a `gate diverged` assertion on a lone ESC chunk.
Reported by CodeRabbit and pullfrog on #18748.
|
||
|
|
6dc7e9fb6a | fix(sidebar): stop ⌘⇧↓ worktree navigation jumping to the first row (#18804) | ||
|
|
e89deb63c9 |
Show Claude background task status in Native Chat (#18757)
* feat(native-chat): show Claude background task status * fix(native-chat): carry background task fence forward * fix(claude): bound background task stop requests * Show running Claude background task details * Harden Claude background task status updates --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
320f4c8b87 |
perf(renderer): index four projections that rescanned their inputs per keystroke (#18747)
Four renderer projections scanned a collection inside a loop over another collection, each on a
path that reruns per keystroke or per store write. All four now build the index once per stable
input, which is what the surrounding code already does for its other lookups.
`workspace-kanban-search.ts` called `searchWorktrees`, the convenience wrapper that builds the
palette document index inline. The board's filter hook memoized the whole call on the query, so
every character re-normalized and re-segmented every indexed field of every worktree — and did
it again on every agent-status tick while a query was active, since those churn board
identities. `buildWorkspaceBoardPaletteDocuments` splits out, memoized on
`[worktrees, repoMap]`; only the match reruns per keystroke. This is the shape
`worktree-jump-palette-document-index.ts` already provides for Cmd-J.
`useTabGroupItemProjections` resolved each editor tab against `state.openFiles` — the global
list across every worktree — and each `tabOrder` entry against the group's tabs, with a `.find`
per element. Both are now `Map` lookups, alongside the `terminalTabById` index the same file
already built. The memo key `groupTabs` gets a new identity on any unified-tab write, so this
ran on title, label and colour changes.
`buildSourceControlTree` rebuilt every ancestor path with `segments.slice(0, i + 1).join('/')`
per segment, making tree construction O(files x depth^2) in characters copied — on the path the
Source Control file filter rebuilds per keystroke. The path now accumulates.
`worktree-header-section-boundaries.ts` ran a full `findIndex` over the render rows for every
header row, plus an `indexOf` over the bucket ordering, in two `useMemo`s keyed on `renderRows`
— so it recomputed on every sidebar row-model change, not just during a drag. One indexing pass
each, first-match-wins to match `findIndex`/`indexOf`. The successor index is keyed per bucket
because a repo or group id can appear in more than one bucket ordering; a flat id-keyed map would
pick whichever bucket was iterated first. `worktree-header-section-boundaries.test.ts` pins that
and the first-wins duplicate-header case.
Measured by `pnpm bench:renderer-quadratic-scans`. Three scenarios time the production export
against a reproduction of the pre-change function; the tab-group scenario is modelled on both
sides because the projection lives inside a React hook. Each asserts before/after agree first.
| projection | drives | scale | before | after | |
| --- | --- | --- | --- | --- | --- |
| workspace board filter (per keystroke burst) | production | 300 worktrees x 12 keystrokes | 15.4 ms | 4.4 ms | 3.5x |
| tab-group projections (per unified-tab write) | modelled | 60 tabs x 120 open files | 1.23 ms | 0.34 ms | 3.6x |
| source-control tree build (per filter keystroke) | production | 5000 changed files | 6.2 ms | 3.9 ms | 1.6x |
| sidebar header boundaries (per row-model rebuild) | production | 80 repos x 600 rows | 2.0 ms | 0.8 ms | 2.6x |
The tree and sidebar wins are smaller than the scans they remove because the rest of each
function (tree finalize/sort, per-row size estimation) is linear and now dominates.
4,537 existing sidebar, tab-group and right-sidebar tests pass unmodified.
|
||
|
|
c36dd5f4c7 |
perf(terminal): skip the partial-escape-tail walk on ESC-free PTY chunks (#18748)
`advancePartialEscapeTail` runs once per PTY chunk, on the main thread, for every terminal — visible, hidden or parked — inside `HeadlessEmulator`'s write path. It unconditionally concatenated the pending tail with the whole chunk and then walked the result one code unit at a time through a VT500 state machine. `extractPartialEscapeTail` only leaves `ground` on an ESC byte, so with no pending tail and no ESC in the chunk the answer is always ''. Taking that case up front skips both the full-chunk concat and the walk. `String.prototype.includes` is a native scan, so the gate costs essentially nothing on the chunks it does not short-circuit. This is the same gate its two neighbours on the very same ingest path already apply — `TerminalOscCwdTitleScanner.scan` and `TerminalMouseModeMirror.scan`, both carrying a comment citing their measured share of a 2.2x ingest regression. This call was simply missed. Measured by `pnpm bench:terminal-partial-escape-tail` over 640 x 16 KB chunks (10 MB), median of 7 rounds: | stream shape | before | after | | | --- | --- | --- | --- | | ESC-free (build logs, `cat`, piped output) | 40.3 ms | 0.19 ms | 213x | | SGR-coloured output (gate does not apply) | 30.6 ms | 32.1 ms | 1.0x | The benchmark proves equivalence over a 226-case corpus before timing, and a new unit test pairs every pending-tail state the scanner can be left in against every chunk shape, asserting the gate is indistinguishable from the unconditional fold. |
||
|
|
12a3e64177 |
perf(native-chat): stop rebuilding every transcript row on every stream frame (#18744)
The structured-session read owner emits per SDK event with no coalescing, so a working turn re-renders `NativeChatMessageList` tens of times a second. `MessageRow` was a plain function component, so every one of those frames re-ran `nativeChatProseToMarkdown`, an image-block scan, and a provider-frame lookup for every message in the window — up to the 300-message pagination limit — even though only the streaming tail had changed. `MessageRow` moves to its own module and is memoized, and its four derivations fold into the `useMemo` that already keyed on `message.blocks`. All of its props are primitives or already-stable references (`onScrollMessageToTop` is a `useCallback`, `onLinkClick` comes through `useShallow`), so the memo holds for every settled row. The move also takes `NativeChatMessageList` back under the max-lines limit rather than bumping it. `NativeChatToolRun` gets the same treatment: it was unmemoized and made four independent passes over its block list per render. `useNativeChatTurnStatus` allocated a fresh slice of the whole current turn and ran a nested `.some()` over it on every render; both now memoize on `[messages, latestUserIndex]`. Measured by the new `NativeChatMessageList.stream-render.perf.test.tsx`, a 120-message transcript with a 20-frame streaming turn: | markdown rebuilds per stream frame | before | after | | --- | --- | --- | | 120-message transcript | 120 | 1 | The test counts real per-row work rather than a render counter, and it fails on the pre-change component (`expected 120 to be less than 12`), so it is a genuine guard. Pure memoization on unchanged props: no visual, ordering, scroll-anchoring, or lifecycle change. All 997 native-chat tests pass unmodified. |
||
|
|
e1599c94b8 |
perf(terminals): let idle panes share one process-table capture instead of forking their own (#18742)
* perf(terminals): let idle panes share one process-table capture instead of forking their own Every visible local pane runs an agent-completion cadence that resolves through `getStrictProcessTableSnapshot`, and the inspection queue already collapses every shared-observation task enqueued in the same tick onto a single whole-host `ps`. Independent ±10% jitter per pane defeated that: the jitter was re-rolled on each reschedule, so panes drifted permanently apart, each landing in its own tick and each missing the snapshot's 500 ms TTL. Four idle panes cost four captures where one would have served all of them. Idle panes now aim at a deadline grid anchored at the epoch. The pull-forward is clamped to the snapshot TTL, so no interval is ever longer than its tier and none is more than 500 ms shorter: a pane off the grid walks onto it over at most `tier / TTL` steps, costs at most one extra inspection in total, and no inspection is ever delayed. Scoped deliberately. A pane with a foreground agent, or one still inside the 10 s post-activity hot window, keeps its exact interval and its own phase, so the bounded hot cadence is unchanged. The error-backoff path keeps its jitter, where spreading retries across panes is the point. Measured by `pnpm bench:agent-inspection-cadence` — whole-host `ps` captures over 60 s at the 2 s idle tier, median of 21 rounds: | visible panes | before | after | reduction | | --- | --- | --- | --- | | 1 | 29 | 29 | 0% | | 2 | 42 | 30 | 29% | | 4 | 62 | 31 | 50% | | 8 | 82 | 32 | 61% | `process-table-snapshot-reader.ts` measures the `command=` column at 1.15 s of work for 1,948 processes, so these are captures a quiet app was paying for continuously. All 4,202 existing terminal-pane tests pass unchanged, including the no-evidence cadence suite that pins the relaxed and hot intervals. * test(terminals): report n/a instead of dividing by a zero baseline in the cadence benchmark A window shorter than one cadence tier leaves the baseline capture count at zero, and the reduction line then divided by it and printed a meaningless percentage. Reported by CodeRabbit on #18742. |
||
|
|
7856e5a677 |
fix(sidebar): confirm filter reset before revealing active workspace (#18708)
* fix(sidebar): confirm filter reset before revealing active workspace * Polish workspace reveal confirmation and focus primary action * Fix reveal confirmation CI: commit-phase ref and sidebar test provider |
||
|
|
36a826ff48 |
fix(ssh): compile node-pty from the host's own Node headers instead of nodejs.org (STA-6674) (#18774)
* fix(ssh): compile node-pty from the host's own Node headers instead of nodejs.org
STA-6674: a Linux SSH host that cannot reach nodejs.org never came up. node-pty
ships no Linux prebuild, so npm hands it to node-gyp, and node-gyp's default is
to download node-v<ver>-headers.tar.gz before configuring. The host refused
that connection (ECONNREFUSED) and the relay deploy failed inside npm install,
which the UI showed only as "Disconnected".
Every official Node build and every version manager that unpacks one already
has those exact headers at <prefix>/include/node. Export node-gyp's nodedir to
that prefix, on every command that can compile node-pty (npm install, npm
rebuild, the cloexec patch's rebuild), when the shipped node_version.h matches
the running Node. Both npm_config_nodedir (node-gyp 10, Node 20) and
npm_package_config_node_gyp_nodedir (node-gyp >= 11.4) are set so every Node
the relay runs on reads it. A version mismatch leaves it unset, which is the
existing behaviour.
When a host is both header-less and offline, name that in the deploy error
instead of forty lines of gyp http output, with the two remedies.
Reproduced and verified with a Docker sshd whose nodejs.org resolves to
127.0.0.1, on node:24.12.0 (the user's version), node:20 and node:26:
ssh-relay-offline-node-headers.docker.test.ts.
* fix(ssh): fail loudly when node-gyp ignores the exported Node headers dir
The headers export relies on npm forwarding npm_config_nodedir /
npm_package_config_node_gyp_nodedir into lifecycle scripts. If a future npm
drops that, node-gyp would silently fall back to downloading, and an offline
host would fail with the same "install an official Node" diagnosis -- wrong,
since the host did ship headers.
The prefix now echoes ORCA-NODE-HEADERS:<dir|none> into the command's output
before the compile, and the download-failure diagnosis reads it back: an
exported dir plus a download attempt is reported as an Orca defect naming
the dir, not as a host problem. Nothing else changes when it works.
* fix(ssh): address review on the relay node-headers export
- Unset any inherited npm_config_nodedir / npm_package_config_node_gyp_nodedir
before the conditional export, so a stale header dir from the remote profile
cannot bypass the version check and build a wrong-ABI binding (CodeRabbit).
- Require `gyp ERR! configure error` and a real network errno in the
headers-download matcher; node-gyp's fetch client logs retried attempts it
recovers from, and a FetchError can be a non-2xx mirror answer (pullfrog).
- Say "no local headers matching its own version", since the probe also
rejects a version mismatch, not only absent headers (CodeRabbit).
- Log the same diagnosis from the non-fatal `npm rebuild` fallback (CodeRabbit).
- Docker test waits for the SSH banner on the mapped port before connecting
instead of trusting `docker run -d` (CodeRabbit).
* fix(ssh): read the node-headers marker from the host output, not the quoted command
execCommand rejects with `Command "<command>" failed (exit N): <output>`, and
<command> quotes the whole prefix, marker echo included. The first-match
parser hit that copy and returned `${ORCA_NODE_HEADERS_DIR:-none}"; ...` as a
"dir", so every real no-headers failure was misreported as an Orca defect
(measured by an independent Docker exercise of
|
||
|
|
58553bfe1c | fix(recovery): fail a renderer recovery reload that never loads, instead of leaving a dead window (#18466) | ||
|
|
172aa1ac35 |
feat(native-chat): render agent file edits as inline diff cards (#18765)
* feat(native-chat): render agent file edits as inline diff cards An agent's file edit rendered as a flat list of every removed line followed by every added line, with no interleaving, no file header, and no line numbers. A Codex edit on the transcript lane rendered no diff at all: the patch arrives wrapped in the source string of its `exec` tool, which matched none of the shapes the old parser looked for. Adds one diff model shared by every edit shape the supported agents produce: - `native-chat-edit-lcs` interleaves a snippet pair, falling back to a linear prefix/suffix diff above the quadratic guard. - `native-chat-unified-patch` keeps the `@@` ranges as per-row line numbers instead of parsing them into display text and discarding them. - `native-chat-begin-patch` recovers the `*** Begin Patch` envelope from the JavaScript string literal Codex sends it in, so that lane renders a diff. - `native-chat-edit-normalize` folds all of it into one model, including the two Codex shapes that do not look like diffs: add and delete arrive as raw file content, and a rename is appended to the body as prose. Claude reports an edit as a snippet pair, which cannot locate the change in the file, so its result's resolved hunks are now carried on the tool-result block and preferred when present. The field is optional, so an older client reading a newer journal simply drops it. Where no resolved ranges exist the gutter stays blank rather than showing a snippet-relative number, which would read as a file position. The card renders the verb from the observed change kind rather than the tool name, pairs an edit's call and result into a single row, and takes its row and gutter grounds from new tokens derived from the git status palette, replacing the hardcoded Tailwind tints the old view used. Desktop only; mobile chat keeps its existing renderer and parser untouched. * fix(native-chat): stop the diff card from asserting an edit it cannot prove Every defect here shares one failure mode: the card stated something the input did not support, and stated it confidently. Parsing: - A hunk no longer ends on `--- `, `+++ ` or `\ No newline`. The first two are what a removed `-- comment` (SQL/Lua/Haskell) looks like once the marker is prepended, so they truncated the whole diff; the no-newline marker is emitted mid-hunk, between the removed old last line and the added new one. Real headers are recognised through `isFileHeaderPair`, lifted out of `native-chat-diff` so the rule has one home. - A `*** Begin Patch` envelope with no `*** End Patch` is declined. With no closing marker `indexOf` returned -1 and the slice swallowed the rest of the command line, so `… +y" && echo ok` rendered as file content the agent never wrote. - One splitter serves every shape, so a CRLF patch no longer keeps a `\r` on each row, in the phantom-row guard, or in the clipboard. It also tests for the trailing newline on the clipped body: on the un-clipped string that test deleted a real line whenever the slice fired. - Truncation is carried from each slice site to the card, so content past the character cap can no longer render as a complete unchanged file with no "Diff truncated" footer. Attribution: - A failed or still-running edit renders no card. It kept the generic tool view, whose result block carries the provider's own error — the card had been drawing "Edited file +1 −1" from the input while hiding the red error body, which is worse than what preceded this feature. - The result-as-patch fallback is scoped to `Diff`, the one tool whose call carries only a path. Any command tool's output could previously be read as a patch, so `git diff` through `exec` was reclassified as an edit of a file named "file" and its command line disappeared with the result. - A whole-content write claims a creation only on evidence — the editor tool's own `create` command, or the provider reporting one. Overwriting a large existing file had always read as "Added file". - `MultiEdit` reads its `edits[]`, and `NotebookEdit` leaves the set: it carries only the new cell source. Both previously fell through to the old renderer, so one turn could show two diff presentations at once. - Snippet-relative numbers are dropped at the model layer rather than hidden by a zero-width gutter, which the flex min-width floor re-exposed on top of the marker and the first characters of the row. The run memoizes its edit model, so a collapsed group no longer re-diffs on every streaming token, and the card's copy button says what it copies. * fix(native-chat): keep every edited file, and mark where the diff breaks A run of hunks was concatenated into one flat row list, so the gutter jumped from one region of the file to a distant one with nothing between them and the reader saw two unrelated spans as one continuous block. Rows now carry an explicit break: it holds no text and no position, counts toward neither side of the change, is trimmed from the end where it would mark nothing, and is left out of the copied text. The patch envelope lost files, and lost them silently: - An update chunk may carry no hunk header at all. The parser required one, returned nothing, and the caller dropped that file from a multi-file envelope with nothing to say it had gone. A header-less body now opens as a hunk of unknown position, and whether the rows are locatable is read off the rows themselves rather than off the header. - The envelope's own control lines rendered as content rows in the card. - A delete names its file and carries no body, which rendered as a card with an empty expandable row list. The header states the change and offers no disclosure behind it. - The header patterns are anchored and `.` excludes a carriage return, so a CRLF envelope matched no header at all and produced no card whatsoever. The envelope is split on both newline forms once, up front, rather than each pattern having to tolerate the extra character. A tool call's argument payload arrives as a string holding JSON. It was passed along undecoded, which is the only reason this code carried a hand-rolled string-literal unescaper. It is decoded once at the transcript decoder now — defensively, since the transcript is untrusted, so anything that is not a JSON object is left exactly as it arrived — and the unescaper is gone. Recovering the envelope no longer guesses at argument names either: it looks at the values, including the words of an argument vector, which is where the envelope actually sits once the payload is decoded. * fix(native-chat): only read a patch where a patch was actually run Recovering the patch envelope from any value of a tool's payload meant a write's own content was searched for one. A file documenting the patch format rendered a card for the file its example names, while the file actually written never appeared at all — the call and its result were consumed by that card, so nothing was left to correct it. Two changes: the envelope is recovered only for the tools that run one, never for a file edit whose payload is content; and only patch- or command-bearing arguments are searched, still including the words of an argument vector, which is where the envelope sits when a command tool applies it. The call payload is decoded back where it is needed rather than at the transcript decoder. Decoding it there changed the shape every reader of a tool's input sees, including the surface that recognises a question payload from any tool by shape alone: a tool whose arguments happened to carry that shape raised a question card pinned over the composer. That decode now happens inside the envelope recovery, the one consumer that needs the structure. A card also states an edit as made, so it now takes evidence that it landed — the provider reporting the call complete, or a result that is not an error. A turn that stopped before its call was answered reported an edit that may never have applied. This replaces the working-turn heuristic in the view, so the rule lives in one place. Two files still went missing. A multi-file patch has no per-file split, so it rendered as one card under the first file's name, with the later files' rows and their gutter numbers beneath it — a card asserting a false file position. Patch text is now split on its file boundaries, one card per file, each named by its own header, with a rename and a `/dev/null` side read from the same headers. And an envelope section that names a file but carries no body was dropped rather than reported, which is the same silent loss the delete case was fixed for. * fix(native-chat): type the patch-section scan and its test helper call The section under construction was only ever assigned inside the helper that opens one, which control-flow analysis does not see, so the variable stayed narrowed to its initial null and reading a field off it did not compile. The helper now only builds and records a section; the loop owns the assignment, which also fixes a real leak in the fall-through row: it opened a section it never made current, so the next row opened another one. The multi-file case also passed a possibly-undefined slice to a helper that takes an array or null. * fix(native-chat): stop the patch lane naming files it cannot name Splitting a patch into its files only ever looked for a boundary outside a hunk, and nothing reopened that state once the first hunk began, so every file after the first was swallowed as the first one's body. A `--- `/`+++ ` pair inside a hunk is now a boundary too, but only when a hunk header follows it immediately: a removed `-- x` over an added `++ y` is never followed by a column-0 header, which is what keeps the guard against reading content as structure intact. One producer cannot be recovered by any parser: it joins several files' patches and keeps a count where the path goes, so nothing in what reaches here names a file. That shape is refused rather than rendered under a name no file has. Recovering the per-file paths belongs to the producer and is filed separately. A clipped body carries its own marker in its text, and the bound that clips it is six times smaller than this module's, so it fires first. Read as content, the marker became a numbered line of the file and the rows before it were reported complete. It is recognised at the end of the text, removed, and reported as the truncation it is — the footer says so and the copied text no longer carries it. Also: a move appended to the body as prose is now read as a rename on every lane that carries the body as text, not just the one that also carries the destination as a field, where it had been rendering as a numbered line of the file it moved. The call's own path no longer wins over a rename's destination, which is only ever in the header, and only sections that name a file count toward deciding whether the call names the one file at hand. A command that merely quotes an envelope — writing documentation about the format — must now also invoke the tool that applies one. And two compared directories are no longer called a rename: only a header that states both sides as such is evidence of a move. * fix(native-chat): anchor the move marker to its own line The marker a producer appends to say where a file moved was matched anywhere on the body's last line, so a row whose own content mentions a move was cut in half at that point and the file it named claimed as the destination of a rename that never happened. It is now anchored to the start of the final line, on both lanes that carry the body as text. The command that applies a patch envelope has a second spelling the runner accepts and runs; requiring the first one refused a patch that really landed. Both are accepted, still matched against whole argument words rather than the payload at large. A clipped diff also said so only under its own rows, where a collapsed card — or one clipped down to no rows at all — showed nothing. It sits beside the change counts now, which are visible either way. * refactor(native-chat): tidy what the diff-card work left behind The copy text is joined from every row of the diff, which a collapsed card renders none of, and it was rebuilt on every render to seed a prop. It is memoized on the rows, matching how the run memoizes its edit model. The two scanners that read patch text kept the same file-section alternation verbatim, so they could drift apart while both looking correct; there is one definition now, beside the header-pair rule that already lives there. Also: the row that marks a break between regions is built in one place, so it is no longer exported; the move destination in the envelope reader was a function-wide binding written and read within one iteration, which read as if a move carried between sections; and a test comment named the wrong mechanism for keeping a card collapsed. Adds the missing pin on what the copy affordance actually copies. --------- Co-authored-by: Merge Sim <sim@local> |