Codex workspace trust is also written to the launch CODEX_HOME (managed-account home) at spawn, and native Codex chats pick their launch home before writing trust, so managed-account Codex launches no longer stall on "Trust this folder?". Fixes#27235.
* test(mobile-web): act on the HTML preview frame only once the browser routes input into it
Chromium runs the sandboxed preview frame in its own process. The rig decided an arm was
ready once the artifact had parsed, but the embedder can still be showing the frame's white
before its surface is composited, and still keep pointer input over the frame for itself
until it holds the frame's hit-test region. A click in that window landed on the
embedder's <iframe>, Playwright reported success, and the arm waited for a navigation
that never came (or read white as pixelBefore).
The rig now waits for the artifact's paint before reading pixelBefore, and before acting
moves the pointer over the frame until the embedder stops receiving it.
* test(mobile-web): name the arm in the input-routing timeout and trim history comments
Local (in-process) PTY provider: an immediate close now SIGTERMs descendants and waits up to the shutdown grace before SIGKILL (same as the daemon path since #22232), so Claude's SessionEnd hooks run. Refs #23865, #22039. Supersedes #24314.
Co-authored-by: innocarpe <2222333+innocarpe@users.noreply.github.com>
Orca-driven Codex launches that type their prompt after start (workers, automations, continuation/handoff, pane-spawn and other submit-after-ready flows) pass `-c check_for_update_on_startup=false`, so Codex's "Update now" screen can't block them or be accepted by Orca's Enter. User-typed launches and resumes are unchanged. Fixes#15814, #18153. Supersedes #18156.
Co-authored-by: luisgonzaleznf <47302972+luisgonzaleznf@users.noreply.github.com>
Native OMP chat (ACP): when OMP reports its session was closed (-32603 "ACP cancel cleanup timed out" / "Unsupported ACP session: <this session>"), Orca replaces the child so the next send or Retry reloads the session instead of failing forever. Other ACP agents unchanged. Fixes#32684. Refs #32161.
* chore(lint): remove disable directives that suppress nothing
Each was reported unused by the oxlint config that owns its rule (root,
casting, react-doctor, anti-slop, native-plugins), including 17 file-level
react-doctor blanket disables whose rule no longer fires. An unused
react-hooks disable was masking a react/purity finding in
useAgentBucketCounts (compiler rules skip functions with hook
suppressions); suppress that one explicitly where the code explains it.
* test(tab-bar): use typed tab/file fixtures instead of @ts-expect-error partials
* chore(lint): fix misc suppressed lint violations instead of disabling
- consistent-type-imports: the "vi.importActual requires inline import()"
rationale was wrong; use typed vi.mock(import(...), importOriginal) or a
type-only namespace import.
- no-control-regex: source-control commit/push failure summaries, shell PATH
hydration, and ipynb ANSI parsing reuse the shared ansi-escape-sequences
stripper and control-character pattern instead of private copies.
- no-explicit-any / no-useless-spread / no-this-alias: typed `this` in
bridge test mocks, drop blanket file-level disables, drop a this-alias.
* refactor(main): replace avoidable type assertions with checked narrowing
17 production cast suppressions in main/wsl-guest removed: untrusted IPC
and RPC payloads (Jira issue update, WSL guest plugin install, handshake
type) now narrow through isRecord/type guards and build typed results;
redundant casts dropped; hand-rolled object checks and errno casts use the
existing isRecord / hasErrorCode helpers.
* refactor(renderer,mobile): replace avoidable type assertions with checked types
13 production cast suppressions removed: RPC failures read through the
`ok` discriminant instead of casting to RpcFailure; react-native-web host
refs narrowed with instanceof HTMLElement; redundant reply/diff casts
become checked annotations; the TUI agent param uses z.custom(isTuiAgent)
like its sibling schemas; the agent-status slice cast that hid missing
actions is gone so the creator's return type checks the whole slice.
* chore(lint): drop dead tiptap wrapper; scope anti-slop mocking rule to test support
- safe-react-node-view-renderer patched handleSelectionUpdate, which
@tiptap/react 3.31.3 no longer has, so it always returned early. Use the
stock ReactNodeViewRenderer.
- The anti-slop test override only matched *.test.{ts,tsx}, forcing
file-level no-module-mocking disables in test harnesses and fixtures.
Match *-test-harness, *-test-fixture(s), .test-fixture and *.test.mjs,
then drop those disables and the now-redundant per-file exemptions.
* test: replace repeated cast suppressions with typed fixtures (main/shared/e2e)
Net -119 cast directives. Typed fixtures replace recurring casts:
unusedSshConnection/unusedOrcadHostTransport placeholders, a typed
readPersistedState harness read (exposed a test treating optional
httpProxyUrl as always set), real Response objects in fetch spies, an
updater main-window fixture, a CDP webContents adapter, one shared
releaseModuleExport for cross-version tests, and requireOverlayDir in
place of `as string` on possibly-undefined overlay paths.
* fix(runtime): validate loaded device rows and host scope instead of casting
Two cast suppressions claimed checks that did not happen:
- device-registry-file-read: "normalizeLoadedDeviceRow re-validates every
field" was false; deviceId, token, name and timestamps were never
checked. Rows without an identity are now dropped and the file is set
aside through the existing corrupt-file path (registry reads as
doubtful, so no revocation verdicts); a non-string token is stripped
so the row stays listed but cannot authenticate.
- client-ui: "only optionality differs" was false; any string reached
persisted workspaceHostScope. Narrow it with isWorkspaceHostScope and
ignore unknown scopes (wire schema unchanged for mixed versions). The
remaining cast now states what it actually covers.
* test(runtime,ipc): drop redundant cast suppressions; typed runtime store fixture
-98 cast directives across runtime/ipc tests: 61 casts that typecheck
without the assertion; a typed vi.fn for scoped invalidation; a
partialRuntimeStore fixture in place of `store as never` for
OrcaRuntimeService; typed worker terminal/authority builders in the
incarnation-fallback suite (the old stub lacked runtimeId, ptyId,
worktreeId and launchTokenHash) plus removal of stacked duplicate
directives there.
* chore(lint): typed vi.mock imports and explicit listener snapshot in shared-bucket tests
Remaining files from the misc-rule pass that overlapped other buckets:
codex-accounts mocks use the typed vi.mock(import(...)) form instead of
a consistent-type-imports disable; the daemon late-exit fixture copies
its listener set with Array.from instead of a no-useless-spread disable.
* refactor(github,mobile): narrow routing settings type; drop remaining avoidable casts
- work-item-routing cast a one-field object to AppState['settings'] under
a claim that readers only consume activeRuntimeEnvironmentId. Return the
existing RuntimeFocusSettings pick instead and narrow the cache-key
chain's parameters, so the compiler enforces that claim.
- mobile file preview reads refusals through the RpcResponse `ok`
discriminant rather than a cast.
- terminal document host-notify accepts `unknown` errors, removing the
message-bridge cast.
- native chat readability drops a redundant repo-list cast.
* test(renderer): replace repeated cast suppressions with typed fixtures
-140 cast directives (and ~150 unsuppressed casts of the same shapes):
- mockPanePtyArgs owns the connectPanePty fixture casts, so a wrong
argument order or fixture is now a type error; drop binding casts for
members PanePtyBinding already declares.
- Seed AppState tab rows with the typed makeTerminalTab; this exposed a
split direction 'row' that is not a TerminalPaneSplitDirection.
- Partial GlobalSettings use getDefaultSettings overrides; Jira-only
stores use createTestStore; runtime environment rows/statuses come from
a typed paired-runtime-environment fixture.
* chore(lint): drop no-module-mocking disable now covered by the test-fixture glob
* test(e2e): fix all 221 grandfathered e2e type errors; baseline to zero
The e2e typecheck baseline grandfathered 221 errors in 119 files. About a
third were tests drifted from the current API, several of which would
fail or mislead at runtime:
- terminal-shortcuts called a nonexistent xterm buffer method;
- the daemon-lifecycle fake lacked terminateOwnedTree, hidden by importing
its type from a module that does not export it;
- arguments in the wrong position (listProcesses(hostId, opts), a dropped
pty.write inputKind, an extra toWebTerminalSurfaceTabId argument);
- removed enum values ('drawer', 'idle', 'editor',
'session-insecure-origin') and a launch_source the telemetry schema
rejects;
- missing required fields (turnScope, childWorkOpen, providerIdentity,
reviewLookupOutcome, runtimeServiceAccount behind an `as RelayConfig`);
- dead reads (tab.customTitle, bucket.targets, changedPixels).
Also lifts variables that narrowed to `never` after poll-callback
assignment, and adds a typed xterm render-dimensions accessor. Includes
the cross-version suites' shared releaseModuleExport adoption.
* chore(typecheck): gate the now-clean e2e typecheck in local lint
The e2e baseline is empty, so the ratchet now rejects any tests/ type
error. CI already runs it via run-ratchets; add it to pnpm lint's
explicit ratchet list and replace the stale 'not yet clean' note.
* test(mobile): typecheck every mobile test; baseline 105 files -> 0
mobile/tests-typecheck-baseline.txt grandfathered 105 test files (531
errors) whose types CI never checked. Contract changes had landed without
these tests following, and type pins in them silently checked nothing.
- Root cause: salvagedOptional (src/shared/zod-salvage.ts) cast away
zod's optional marker, so every reply type built on it claimed
salvaged fields were always present. Return a real ZodOptional;
runtime is unchanged (the outer optional short-circuits undefined).
- Production now takes the port it uses: RpcOperationSender instead of
RpcClient for send-only callers, a narrow fetch signature, and only the
request fields push-receive reads.
- findAllByHostType/findByHostType replace `'Text' as never` lookups;
stubRpcClient gives tests a typed RpcClient.
- Real test bugs: settings-host-client-lifecycle "relay" hosts were
direct hosts (relayHostId no longer exists); two toEqualTypeOf pins
could never hold; a recovery hook ran with an undefined scope key;
a create-PR test cast to a never-imported type; "no URL" assertions
passed on null; plus stale required props, args and enum values.
* test(e2e): parse workerStart result instead of casting it
* test(runtime): typed partial runtime stores with production-shaped fixture data
Route every partial store passed to OrcaRuntimeService through
partialRuntimeStore with typed row builders, removing 60 cast directives
that hid fixture data production never produces:
- paired-client-navigation-test-harness keyed setWorktreeMeta writes by
`[object Object]` (it took an id where the store takes a target), so
reads never saw them;
- invalid worktree lineage origin/capture and incomplete workspace lineage;
- getWorkspaceSession returning null where the store returns the default
session; getGitHubCache returning undefined;
- orcad migration states missing migrationId/manifest/receipt, with a
toEqual pinned to that incomplete shape;
- partial repos, metadata, folders, project groups and notification rows.
* refactor(renderer): resolve suppressed React hook lint instead of disabling it
109 of 187 react-hooks/react-doctor suppressions fixed:
- ~95 exhaustive-deps disables only hid stable refs and setters passed
through controller objects; list them (identity never changes, so
nothing re-runs), plus a few real stale closures (tabId/worktreeId).
- Effects keyed on one value that read the latest props now use
useEffectEvent (WebConnect, skill freshness, Bitbucket dialog seeding,
Landing preflight, automation catalog, structured-session recovery,
file explorer).
- The conditional layout-owner hook is selected once at module load from
the build constant instead of a rules-of-hooks disable that also hid
the whole controller from compiler and react-doctor rules.
- A hook disable was masking a react/purity Date.now() in the palette's
recent-tabs memo; sample it when the snapshot is captured.
- Three identical debounced-search effects share
useTaskSearchDebouncedApply; layout snapshot merge helpers move out to
stay under max-lines.
Remaining suppressions are mount-once resources, invalidation epochs,
deliberately keyed fields, and test harnesses that mock the dispatcher.
* chore(ratchet): exempt test harnesses and fixtures from the focus-read ratchet
The ratchet skipped *.test-fixture.ts but scanned *-test-harness,
*-test-fixture(s) and *.test-support files. Those seed settings with
getDefaultSettings (which the ratchet discovers as a focus reader) and
never route host work, so 17 baseline entries were false positives, and
replacing a test's `settings: {...} as never` with typed defaults tripped
it. Match the repo's test-support names and prune (which also drops one
stale entry), and rename the frame-ordering harness to *-test-harness like
its siblings. A blanket "-harness" match would also exempt
e2e-terminal-pty-harness, which production imports, so the names stay
explicit.
* test: merge duplicate shared/constants imports
* revert(file-explorer): restore original tree load effects pending SSH e2e investigation
* test(runtime): drop RuntimeStore cast now that the fixture is typed
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(terminal): keep keys typed into a starting agent pane as an unsubmitted draft
* fix(terminal): keys typed after the bind always go live; never paste over a driven screen
* test(terminal): a deferred split with late startup work drops its held keys
* fix(terminal): capture only before the first bind; drop drafts on attach or reattach binds
* fix(terminal): leave paired-server panes' type-ahead to their own transport
* test(mobile-web): click the drawer chip once it is actionable, and honour the wait timeouts
* test(mobile-web): say why the drawer trial click stays, and report a failed open with page errors
Forward the scrub-safe pane identity aliases (ORCA_AGENT_PANE, ORCA_AGENT_LAUNCH) into WSL via WSLENV so hooks inside WSL (DSH, Claude scrub restore, Hermes) can report to the right pane.
Native Claude chat: background task_progress / background_tasks_changed frames refresh task rows without reopening an output turn, so an idle chat stays idle on progress ticks; real wake-ups (task_notification / status-changing task_updated) still open the turn. Fixes#22701.
Devin launch prompts are delivered as argv (`devin [flags] -- '<prompt>'`, Devin's documented interactive-prompt form) instead of typed after start, where they were lost before Devin was ready. Fixes#26302. Supersedes #26387.
Co-authored-by: ggbdpq <67929842+ggbdpq@users.noreply.github.com>
Pi RPC adapter closes each text/thinking block on its text_end/thinking_end, so the 128 open-stream cap only counts concurrent blocks and long turns no longer fail with "Pi message stream capacity exceeded". Fixes#33092.
Native Claude chat drops ANTHROPIC_API_KEY from the SDK child only when the user has explicitly rejected that key in Claude's customApiKeyResponses (same file and last-20-chars match the TUI uses), so a rejected key no longer bills the API. Approved and undecided keys are unchanged. Fixes#29466.
Claude transcript decoder: assistant records containing only thinking/redacted_thinking parts get role 'reasoning', so mobile transcripts fold them into a Thinking row like Codex/Grok. Fixes#33236. Supersedes #9417.
Co-authored-by: kaynansc <33468632+kaynansc@users.noreply.github.com>
The suite loaded the main runtime graph in beforeAll; run beside the rest of
terminal-pane at six workers, that cold load passed the 60s hook default and
failed the suite before any case ran. Loading it at module scope, as other
terminal-pane suites do, takes it off any per-hook timer.
The census built a whole-repo TypeScript program (17,740 root files) inside
a unit worker, peaking at about 7 GB, and ran out of memory on loaded
machines. Its invariant, that a layout-owner build never reads or writes a
PTY record's pane copies, is proven at runtime by
pty-pane-copies-untouched-main.test.ts, which traps every access.
* test: run the terminal-retirement SQLite fixture's suites under Node
The acknowledged-tab retirement fixture opens profile-state SQLite, but the
runtime scan only sees direct imports, so its four suites ran under Bun. On
macOS Bun's node:sqlite is the system SQLite, which cannot open a closed WAL
database read-only, so the quit-flush durability case failed on every local
macOS run. Naming the fixture as a SQLite harness runs them on Node, the
runtime the product ships.
* test: run every suite that opens profile-state SQLite under Node
Tests that import the profile-state SQLite authority directly ran under Bun,
whose macOS node:sqlite is the system SQLite. Listing the authority as a
SQLite module sends them to Node with the other SQLite suites.
Claude AskUserQuestion with option previews: Orca's ask answers now send digit (focus wiggle for option 1 with a note), optional `n`+note, then Enter, since a digit only moves the highlight in Claude's preview layout. A bare digit on a preview question no longer clears waiting. Fixes#16865.
Co-authored-by: Frederic Barthelemy <209712+fbartho@users.noreply.github.com>
* fix(terminal): keep keys typed before a pane's PTY exists
* fix(terminal): hold pre-spawn keys only in panes with no startup work
* fix(terminal): no pre-spawn hold for live-entry resumes or main-started panes
* fix(terminal): keep the deferred-split hold exactly as before
* fix(terminal): drop held keys if a resume or startup work appears before the bind
* Revert "fix(terminal): a file link opens in the workspace it was clicked in (#32320)"
This reverts commit 359fec1a09.
* test: drop redundant openFiles cast restored by the revert
The Windows boundary step's managed-hook stdin test bounds each hook at 10s
to catch launchers that block reading stdin. Its first encoded-PowerShell
launcher was also the worker's first Windows PowerShell start, which took
5.7s on a passing run and 15.3s on a failing one before exiting 0 by itself.
Run that launcher once before timing so the bound measures hook behaviour,
and name the command when the bound trips.
* fix(editor): add bounded Zig syntax highlighting
Recognize .zig files and load the existing pinned Shiki Zig grammar through
the lazy TextMate provider. Map its scopes to stock Monaco theme categories
while preserving the shared tokenizer state and per-line bounds. Keep .zon
and formatting outside this lexical-only change.
Reuse the optional scope-mapping hook reviewed in Haskell PR #25411 and
its refreshed candidate 12908d0341,
retaining the original contribution credit from #25315. Ship the existing
grammar package MIT notice in renderer public assets for desktop and web.
Requested by @thinkAfCod in issue #11313. This addresses highlighting only;
it does not close the formatting portion of the issue title.
Co-authored-by: davidwis <davidwis000@gmail.com>
* fix(build): serve Zig grammar notices through allowed assets
Move the unchanged Shiki notices beneath assets/ and reference both files
with relative HTML metadata links. Preserve reverse-proxy prefixes and let
the existing projection and serving allowlists accept the browser client.
Do not expand either allowlist.
Exercise real Vite output, projection, manifest validation, and static HTTP
serving at root and /orca paths. Follow the relative-assets notice strategy
used by the coauthored Haskell adaptation and retain its contribution credit.
Co-authored-by: davidwis <davidwis000@gmail.com>
* test(editor): verify Zig source and diff highlighting
* chore(editor): drop Zig grammar notice assets
Zig grammar comes from the existing shiki dependency (already shipped via
syntax-highlighter's bundled-language loader), so no separate notice is needed.
---------
Co-authored-by: davidwis <davidwis000@gmail.com>
Antigravity usage: when agy's read times out or fails while showing its interactive browser sign-in prompt, Orca marks it signed out and skips automated re-reads for 1h (manual refresh still bypasses), instead of opening a new sign-in page on every poll. A valid reading always wins; fast-exit signed-out reads poll as before. Fixes#26484.
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 1 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$279 | 0 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$279 |
| Prod | 1 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$58 | $\color{#cf222e}{\Huge{\mathbf{−}}}$13 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$45 |
<!-- /orca-pr-loc -->
## ELI5
When we restart a relay server during a routine rollout, computers that were offline during the rollout still have that server written down as "their" server. If one of them comes back during the ~45 seconds while the replacement server is starting up, the director currently tells it "your server is unavailable, try again" over and over, instead of just sending it to another server. This change lets the director send it elsewhere, but only when it has proof that the old server process is really gone.
## What Changed
**Before (production, 2026-10-24 23:43Z, cell production-gce-c15):** during a same-capacity roll the cell is roll-isolated, drained, then its single VM is replaced. A desktop host that was offline during the drain reconnected in the gap between the old process stopping and the new one being ready. The director refused it for ~26 s (`assignment rejected route=assign lane=placement hinted=true reason=relay_home_cell_unavailable cause=not_ready`, x5, plus `route=resolve ... cause=unheard`, x2). It got a grant somewhere else only once the new process was live.
**After:** while the replacement process is starting up (`not_ready`), the same host is moved to another cell on its first try. A host on a roll-isolated cell can be moved off even though the cell is not live, as long as the process heartbeating for the cell *started after the roll isolated it* (`relay_cell_runtime.started_at > relay_cell_admission.roll_isolated_at`), its heartbeat is fresh, and (for connection-capped cells) its latest heartbeat reports zero connections. Both lanes that move an isolated host apply it: the narrowed isolated-replacement path (`isolatedIncumbentMayMove`) and the all-rows path it falls back to. Together these serve assign and resolve, because resolve hands a non-live cell to `assign`. "Roll-isolated" is the existing predicate: `migration-only` with a roll stamp younger than 2 h.
**Still refused (residual):** the `unheard` window, when the old process's heartbeat is past the 45 s TTL and the new process has not been heard yet. On c15 that was about 5-10 s. Nothing proves the drained sockets are gone there, so the refusal stays, and the client's retry recovers once the new process heartbeats. Also unchanged: an ordinary readiness dip, a `migration-only` cell without a roll stamp, a stale (over 2 h) stamp, and a new process that later goes unheard. Fence handling is untouched.
## Why
The refusal exists because moving a host off a cell we cannot fully trust could leave the host's old socket served by the old process while its clients go to the new cell. Until now, the only proof the code accepted was an operator-written fence. This adds a second proof that costs no operator step:
- **A new incarnation means the drained process is gone.** Each process mints a fresh incarnation UUID and `startedAt` at boot (`cell-heartbeat-client.ts`), and the director rejects a heartbeat with an older `startedAt` once a newer one is recorded. Each cell VM runs exactly one relay container (`docker run --name orca-relay --restart always`, with stale containers force-removed at boot). The cell MIG uses `replacement_method = RECREATE` with `max_surge = 0` (pinned by a Terraform assert), so the old VM is deleted before the new one is created. A same-VM container restart also produces a new incarnation and is just as safe: Docker restarts only after the old process exits, and that exit closes all of its sockets.
- **Whatever the new process holds, it can enforce.** It is heartbeating (fresh), and on capped cells it reports no connections at all. A host that attaches between two heartbeats carries the old epoch, so the epoch bump this move writes makes the cell refuse it or close it with 4409. That check is the same live `verifyCellAssignment` / control-renewal read the existing live-isolated path relies on.
- **Clock skew** between the cell's `startedAt` and the director's stamp is covered by the zero-connections check on capped cells only: a drained process that only *looks* post-stamp still holds its sockets, so it is refused. Uncapped cells report no counts and get no such guard; they already move off without a fence on the dead-cell path today, so this adds no new exposure there.
Alternatives considered: (a) adding an incarnation column to `relay_assignments`: a schema change for no extra proof. (b) Honouring any not-live roll-isolated cell: that drops the proof. (c) Having the roll write a fence for the `unheard` window: the only fence driver refuses enabled cells, a fence is a MIG resize to 0 that would fight the roll's own apply, and the window is a few seconds the client retry already absorbs.
## Linked Issue
N/A (internal production incident, 2026-10-24 c15 roll).
## Visual Proof
N/A: director placement logic only, no UI.
## Testing
- [x] Automated tests added: `cloud/apps/relay/src/assignment-roll-replaced-cell.test.ts` (18 cases; the redial cases run for both hinted assign and resolve):
- not_ready with an incarnation started after isolation: re-placed, epoch bumped.
- Refused as before: not_ready with the pre-isolation incarnation, with reported connections, after the new process goes unheard, with a stale stamp, on an ordinary unstamped readiness dip, and on an unstamped migration-only cell. Also refused: the unheard window between the old and new process.
- One case per lane. The narrowed path's cross-region move fails if only the narrowed-path change is reverted. The all-rows path's move, forced by units no lease backs, fails if only the all-rows change is reverted.
- [x] Full relay vitest suite, `--maxWorkers=2`: 130 files / 1344 tests passed, 39 files (201 tests) skipped. The Postgres-backed suites skip locally (no `ORCA_RELAY_TEST_POSTGRES_URL`); CI runs them.
- [x] `pnpm typecheck` in `cloud/apps/relay`.
## Review
## Agent skill upstream boundary
- [x] Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.
## Notes
Backwards compatibility: no schema, wire or RPC change; old directors keep refusing as today. Performance: the new query runs only when a host's home cell is roll-isolated and not live; the general sticky hot path is unchanged.
## Checklist
- [x] This PR is small and focused
- [x] I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
- [x] Before/after screenshots or videos attached for UI changes, or `N/A` with reason
- [x] Self-reviewed for correctness, security, and performance
- [x] Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
- [x] `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build` pass (or CI will cover; local preferred)
* fix(terminal): keep Mac trackpad flicks continuous in fullscreen TUIs
Chromium reports wheelDelta = -3 x deltaY for continuous devices, so a fast
trackpad event crossed the 100-unit 'notched wheel' threshold and switched to
the compressed, burst-boosted wheel curve mid-flick (2 -> 6 -> 2 reports per
event). Classify the gesture instead: an off-120-grid wheelDelta marks a
continuous device and the verdict sticks until the gesture goes idle.
* fix(terminal): end a trackpad gesture when a Mac wheel notch arrives
An on-grid event now continues the continuous gesture only with the trackpad's
3x wheelDelta shape and only shortly after off-grid input, so a notched wheel
used right after the trackpad keeps its discrete feel.
* fix(terminal): match wheel notches on the zoomed grid and require the trackpad ratio
Chromium divides both deltaY and wheelDelta by page zoom, so a notch reports
120/zoom; derive the grid from the UI zoom. An on-grid event now continues a
trackpad gesture only when 3 x |deltaY| is close to |wheelDelta|, so a
Windows/Linux notch (100 px per 120) right after trackpad input stays discrete.
* refactor(terminal): classify trackpad gestures without zoom or a notch grid
Keep the base rule (a small wheelDelta is continuous) and extend it across the
gesture: a large event within 300 ms of continuous input stays continuous only
while it keeps Chromium's continuous delta shape (wheelDelta about 3x deltaY on
macOS, 1x on Windows/Linux). Both deltas scale with zoom, so the shape check
needs no zoom factor; notches never match it.
* fix(terminal): only small wheel events extend the trackpad gesture window
A notch that happens to match the continuous delta ratio (40 px per 120 on
macOS) is now captured for at most 300 ms after trackpad input instead of
renewing the window indefinitely.
* fix(terminal): keep the whole trackpad gesture continuous through long momentum
Restore the approved rule where every continuous event extends the gesture
window, so a hard flick whose momentum stays above notch size for over 300 ms
keeps its 1:1 row distance. Pins that with a long-momentum fixture.
Codex native chat: follow-ups to a running goal turn are steered into it (turn/steer) instead of waiting, when the follow-up's model/effort match what the turn actually runs (newest handed-over submission, incl. ones rejected after their turn opened). Still waits while an earlier send is pending. Fixes#33056. Supersedes #33057.
Co-authored-by: Nico Hu <296744606+niconicohu@users.noreply.github.com>
* fix(runtime): a workspace sleep no longer fails when another workspace sleeps at the same time
Sleeping two or more workspaces at once failed every one but the last with
terminal_liveness_unavailable, so their terminals kept running. Each sleep asks the terminal
host which PTYs are live; when a newer listing starts while one is in flight, the older answer
is discarded as stale and retried once. Two sleeps leapfrog each other: each retry starts a
listing that makes the other's stale, and the second retry gives up.
A targeted read that a newer listing overtook now waits for that listing and uses its answer,
which is at least as fresh as the read itself, instead of starting another listing. Lifecycle
changes (a PTY spawning or exiting mid-listing) keep the single retry they had.
The listing step moves into its own layer to keep the refresh within the file-size limit.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(runtime): an overtaken PTY listing joins only a newer one that asked the same question
A targeted read that a newer listing overtook adopts that listing's answer. That newer listing
could have asked without foreground-process evidence, or waited less for slow hosts, so the read
could get rows without the evidence it asked for, or fewer hosts than it would have waited for.
It now joins only a newer listing with the same evidence option and a deadline at least as
late as its own; otherwise it keeps its single re-list.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(runtime): a PTY read with no deadline stops after one listing budget, however many it joins
Each listing an overtaken read joined got a fresh 3 s when its caller set no deadline, so
pollers starting listings faster than a slow SSH census finished could keep such a read waiting
without end; before joining it gave up after two listings. The read now fixes one deadline up
front (the caller's, or one listing budget from now) and every listing it joins shares it.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Pi: a finished answer followed by Pi's own threshold compaction is announced (done/unread) when the compaction starts instead of waiting for it to end. Skipped when follow-ups are queued; the settle post still ends the row. Refs #33267 (idle-compaction case unchanged).
Codex native chat shows the existing approval card for MCP tool-call approvals (form-mode elicitations with no fields) instead of auto-declining them; Deny sends Codex's user-cancel reply so the turn continues. Other elicitations and turn-less requests are still declined. Fixes#25045. Supersedes #25988, #25046.
Co-authored-by: u-ichi <82369+u-ichi@users.noreply.github.com>
A hook-reported agent under an older shell title (e.g. zsh before Codex paints its title) keeps its agent status and session id, so mobile can open a fresh Codex chat; PTY titles now record when they were observed. Fixes#32030. Supersedes #32046.
Co-authored-by: dorian-kwon <61652817+dorian-kwon@users.noreply.github.com>
Pi's titlebar extension paints the idle title at session_start (plain Pi only, not OMP) so Pi-based harnesses that brand their own title pass worker-start agent_readiness. Fixes#28273.