Skip to content

[upstream #30318] fix(mobile): match the terminal home-indicator inset to the input panel - #324

Open
innocarpe wants to merge 7931 commits into
mainfrom
ios-command-dock-inset-20261018
Open

innocarpe wants to merge 7931 commits into
mainfrom
ios-command-dock-inset-20261018

Conversation

@innocarpe

@innocarpe innocarpe commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Upstream: stablyai#30318

ELI5

The input dock reserves the bottom safe-area inset but its container is transparent. The home-indicator strip therefore shows the terminal background instead of the adjacent input panel color.

What Changed

Give the dock container the existing panel background token so its safe-area padding blends with the accessory/input surface.

Why

Paint the surface that already owns the safe-area padding without changing keyboard lift, padding or terminal viewport dimensions.

Linked Issue

Fixes stablyai#30308

Visual Proof

The actual before/after dock styles rendered with the same inset and panel content.

Before After
inset-before inset-after

Isolated browser surface rendering; keyboard lift and native safe-area timing are not measured.

Testing

Application typecheck and changed-file lint passed. This is a one-property surface fix; component rendering is checked separately.

Changed-file formatting and lint passed. The full mobile suite and a release build were not completed locally; the focused checks above are the local evidence. Physical-device behavior remains unverified. The existing host protocol is retained; remote-host interaction has not been exercised on a physical device.

AI Disclosure

Prepared with OpenAI Codex.

Review

One independently reviewable user behavior.

Agent skill upstream boundary

  • Not applicable; no upstream skill-installer material is copied.

Checklist

  • Small and focused
  • Explained behavior, mechanism and approach
  • Automated regression or rendering evidence described above
  • Self-reviewed for correctness, security and performance
  • Cross-platform, remote-host and compatibility impact considered
  • Full application build; CI will cover this

brennanb2025 and others added 30 commits October 16, 2026 16:24
…arts re-read nothing (stablyai#29401)

* Persist session parse results beyond the memory cache limit

* Expire Session History parse rows by idle time instead of a count cap

The parse cache dropped its oldest row once it held 4,096, so a scan over
more transcripts evicted its own upcoming hits and re-parsed every file on
every scan. Rows now stay until no reader has used them for 24 hours, swept
after each completed History scan and each search-index pass. Only the live
parser state keeps a 4,096 count bound; dropping it costs one whole re-read
of a file that changed, never a re-parse of one that did not.

Removes the overflow directory and the orcad persistence wiring from the
earlier revision of this change.

* Expire only rows History lists, and keep the newest transcripts under bounds

The search pass ran the idle sweep every 20 s but never re-stores History's
unchanged rows, so with search on a day away from History dropped them all
and reopening it re-parsed everything. Only a completed History scan sweeps
now, and it expires only rows History listed (on a monotonic clock). Rows
only the search index or a title lookup created keep main's 4,096 count
bound, so an index build can't grow the scanner process without limit.

Scans store newest transcripts first, so every store-order bound dropped the
newest, most active files: the saved snapshot kept the oldest ~11k rows and
the resume bound stripped the newest files' parser state. The snapshot is now
ordered by transcript mtime, and both count bounds drop the tenth of their
rows with the oldest transcripts.

* Drop the 24 h idle sweep; History rows live for the process

The sweep let whichever listing ran last decide when a row died, and
listings differ in scope: a smaller one (the phone's Agent History, a lower
desktop limit) expired rows only a larger one used, so the next Unlimited
scan re-parsed them. Rows History lists now live as long as the scanner
process; the snapshot's newest-transcripts cap bounds what survives a
reload, and rows other readers made keep the 4,096 bound.

Also clears the resume bookkeeping for a row the unlisted bound drops in the
same store.

* Skip resume bookkeeping for a row the unlisted bound just dropped

Running the resume trim first let a row the unlisted trim then deleted push
the resume set over its bound and cost 409 live resume points.

* perf(ai-vault): store the Session History parse cache one row per transcript

The parse cache was persisted as one JSON file, rewritten whole after any
change and capped at 64 MB / 1M tokens (about 11k rows). Past that cap, a
restart re-read the oldest unchanged transcripts, and an agent writing while
History was open rewrote 10-27 MB every few seconds.

The scanner child now keeps the rows in their own SQLite file. A miss in the
in-memory map reads the row by path; changes are written per row, debounced
and chunked; deletes run immediately. A used_at column, refreshed at most
daily, expires rows nothing has used for 30 days, so rows for transcripts
deleted outside Orca have a way to die. The old JSON file is imported once
and removed. Electron main only records the path for the child.

The rebuild-on-corruption open moves out of the search index into a shared
versioned cache database helper used by both.

* fix(ai-vault): import the old parse cache at open and expire rows only for missing transcripts

Review round 1 on the per-row parse cache store:
- The old JSON was imported only by a History scan or search pass, and only
  into an empty database, but deleted either way. Tab-title sync writes a row
  first, so the import was skipped and the first History open after updating
  re-read everything. The import now runs when the database opens, before any
  lookup, write or delete can reach it, keeping rows already present.
- A 30-day last-use expiry also dropped rows for transcripts that still exist,
  so opening History less than monthly re-read them. Rows unparsed for 30 days
  are now checked on disk and dropped only when the transcript is gone, then
  the freed pages are reclaimed. The daily last-use rewrite goes away.
- 8 KB pages (a ~2.3 KB row filled a 4 KB page), busy_timeout before the WAL
  switch, and lookups served from rows still waiting to be written.
- Revert accidental pnpm-lock.yaml churn.

* chore: revert accidental pnpm-lock.yaml churn

* fix(ai-vault): never stat WSL transcripts from the parse cache recheck

A stat on a \\wsl.localhost path outside the scanner's running-distro
filter and gated WSL lane could boot a stopped distro or hang on a stuck
one. WSL rows now skip the existence check and age out after 180 days
unparsed. SQLite-backed session keys use the shared OpenCode path splitter.

* fix(ai-vault): register the parse cache database with the WSL mover census

Each schema keeps its own meta table again, so the shared open helper
creates none and the search schema's SQL is unchanged. The parse cache's
tables are recorded as not moved: a cache rebuilt from transcripts.

* fix(ai-vault): a forced whole read never reuses a row a failed database delete left behind
…g the payload (stablyai#29092)

* fix(native-chat): decide provider failures by kind, never by searching the payload

A Claude chat opened with a red "claude · message:system:init" card whenever one of the
user's MCP servers failed to connect. The classifier searched every nested field of a
frame for failure-looking values and let that outrank the per-kind catalogue, so the
session-start roster's `status: "failed"` turned the whole frame into an error row. The
same search caused the earlier failed-background-task opcode cards.

A frame is now a failure only because its kind is one, or because a field its kind
declares as its outcome says so (Claude `hook_response.outcome`, Codex `hook/completed`
`run.status`). A Claude turn result is a failure when the result journaling says so,
since that depends on whether the user stopped the turn. With nothing left to override,
the typed-translator coverage list, its option and the task-frame sentence seam go.

Failed MCP servers write no row for Claude or Codex.

* test(claude): an uncatalogued frame keeps its sentence without becoming a failure

* fix(native-chat): keep Claude authentication errors visible
… permission tests (stablyai#29891)

stablyai#29335 gave Grok chats their own permission modes, so on a paired host
that predates per-chat modes a Grok chat is now checked like Claude and
Codex; that is intended. stablyai#29442's tests, merged a day earlier, used Grok
as the agent without modes. Use Gemini, which still has none.
…yai#29860)

* fix(mobile): stop rewriting the cached manifest on a same-build hit

A same-build cache hit rewrote manifest.json (write next, delete, move)
while the native view was reading it. A read inside the gap reported
generation-unreadable, which discarded the cache and redownloaded the
same build. A matching build id now writes nothing to the generation
directory; the fresh routes still apply to the current session.

* refactor(mobile): drop code the manifest rewrite left behind

Opens a same-build hit straight from session.cached, folds the manifest
file name and parser back into the generation store, removes the unused
moveFile adapter and fake hooks, and trims tests that passed on main.
…tablyai#29874)

A server-pushed recovery the host marks unavailable carries an empty
image that is not the buffer. Applying it cleared screen and scrollback.
Classify recovery replies like tagged requests and retry instead.

Co-authored-by: Claude <noreply@anthropic.com>
…fore typing (stablyai#29892)

* fix(terminal): replay a viewport claim made during remote recovery before typing

Recovery released typing at the host's subscribed ack, but claims and
resizes still returned early until the snapshot finished, and nothing
replayed them. Typing then reached the host at another viewer's grid.
Defer the intent and replay it on the stream before releasing input;
send claims directly once the subscription accepts input.

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

* fix(terminal): spend a deferred claim only once the host acks the subscribe

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

* fix(terminal): stop the ackless replay from marking a closed stream healthy

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

* fix(terminal): drop a queued resize when recovery defers the viewport

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

* fix(terminal): keep a deferred viewport claim when its replay write fails

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…pt running agents on windowless hosts (stablyai#29885)

orcad built its runtime without the persistent claim signer, so each restart
minted a new key and the surviving terminal daemon could not match the old
owner: resuming the same conversation started a second writer. Load the same
signer the desktop uses from orcad's own user data.

A workspace-slept running agent's record was left for a window wake that a
windowless host never has, so opening its tab started a fresh agent. Such a
host now resumes it in the pane, under the existing duplicate-writer checks
and recorded-flags-only rule.
…hichever machine connects first (stablyai#29883)

* fix(ssh): leave a stopped pre-claim install on a shared home unclaimed until its machine is known

* fix(ssh): judge a pre-claim install by its activation record; the printed claim creates its directory
…riment was turned off during (stablyai#29889)

A failed or deferred first add kept its detached distro holder with no record to find it; an add in flight when WSL hosts were turned off still finished as a ready host.
…s the migration lock (stablyai#29875)

The test raced two prepare() calls for the migration lock; when the failing-keyring start won,
it correctly started a fresh key and the old credential could not open.
…-owner build only) (stablyai#29886)

* feat(layout): report window saves that still change main's layout (dark)

In a layout-owner build, each window session save (session:set, session:patch, the shutdown
checkpoint) is projected to main's published layout before and after it is written. Any workspace
whose layout the save changed is logged, and appended to ORCA_LAYOUT_SAVE_DIVERGENCE_LOG when set,
so evidence runs can show which tab changes still reach main only through the save.

* feat(layout): name the fields a diverging save changed

* fix(layout): the save check diffs main's whole layout model and never fails the save

Review: a failed log write could reject a save that already landed; and the published layout omits
editor views, pane records (sleeping agents, incarnations, titles), closed-tab tombstones and the
workspace records, all of which a later save trim would drop.

* fix(layout): the save check covers record-only workspaces and row owners, and counts rebinding last

* fix(layout): the save check counts first binds as rebinds and keeps the count past the cut
…tablyai#29873)

A visible repo on another host sharing the retained source's repo id un-hid
the retired SSH host and its worktrees in CLI and phone listings.
stablyai#29853)

* fix(wsl): run wsl.exe probes from the system root, never the profile

WSL holds wsl.exe's working directory busy for about its idle timeout
after the probe exits. Since stablyai#29209 the orcad startup probe ran in the
profile, so a temporary orca serve profile could not be deleted for about
a minute (diagnosed in stablyai#29845).

* test(orcad): drop the WSL profile-hold wait from serve parity

Reverts stablyai#29845's extended EBUSY retry: wsl.exe no longer runs in the
profile, so nothing outside the test's process tree holds it.

* build(cli): list the WSL spawn-directory module in the CLI typecheck project
… is selected again (stablyai#29893)

* fix(session-tabs): seed the first terminal when a warm empty worktree is selected again

A warm stream that got its only empty answer while its worktree was
unselected correctly held the bootstrap, but selecting the worktree again
reused the stream without re-deciding it, so the workspace stayed empty.
Keep the held verdict and re-decide it when the warm entry is selected.

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

* fix(session-tabs): drop a held first-terminal verdict superseded by newer host evidence

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

* fix(session-tabs): hold only single-worktree frames as deferred seeds

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…d-start scan (stablyai#29894)

On cold start the conversion skips its own worktree fetch and waits for
startup's scan, so the one rehome attempt found no server row and left
the active workspace on the retired SSH owner. Retry the move once the
startup scan completes, while the conversion is still current.

Co-authored-by: Claude <noreply@anthropic.com>
…adless host (stablyai#29881)

* fix(orcad): resolve folder workspaces for scoped browser commands

Both orcad browser adapters resolved --worktree with the git-only selector, so
a folder workspace failed with selector_not_found before reaching the provider.

* test(orcad): parse browser results instead of asserting their types
…and cancel (stablyai#29023)

* feat(acp): a managed runtime installs only when asked, with progress and cancel

One installation per runtime owns every change to its releases: it reclaims old ones once, first,
then installs one at a time. An install checks free space, downloads with byte progress, extracts,
hashes each member (abortable), writes the receipt, then publishes by rename, the commit point: a
cancel after it reports success. Reinstall rehashes the pinned release without downloading. A
status read waits, bounded, for a newer revision and rereads the installed release from disk.

* fix(runtime): Install repairs a damaged copy, says when unzip is missing before downloading, and reads a failed download plainly

- Install over a damaged pinned copy downloads and verifies a fresh one, then swaps the damaged
  folder into staging, so there is no dead end; status reports the damaged fact.
- A host that cannot unpack the archive (no unzip) reports it in status and refuses Install
  without downloading.
- A network failure while downloading reads "Couldn't download …"; the raw error goes to the log.
- Reclamation skips the whole pass when a release folder could not be read.

* fix(runtime): a status read that can't read the installed files fails instead of saying not installed, and extraction uses the extractor the preflight found

* fix(runtime): a host that can't report free disk space still installs, and says so in the log

* fix(runtime): a download that can't be fixed by retrying says so instead of blaming the network

* fix(runtime): Install unpacks the release itself, checks it starts before publishing, and Cancel answers with the outcome

- The managed install unzips in-process (fflate, bundled into main), so a host without unzip can install; the unzip preflight and the missingProgram status field are gone. The Node runtime download keeps its own extractor.
- A runtime may provide validate(): Install starts the downloaded copy once before publishing it, and an existing copy before reusing it; a failure reads "<agent> couldn't start on this host".
- No 'cancelling' phase: Cancel resolves once the work has unwound and returns cancelled, or succeeded when it had already committed.
- Byte progress every 250 ms; status rereads disk and PATH only on a first read or a phase change.
- The declared download size is checked only for an uncompressed body.
- An existing copy whose files hash correctly but whose receipt is missing gets its receipt rewritten instead of reading as damaged forever.

* refactor(runtime): move the steps that put one release on disk out of the installer

The installer class now holds only its lifecycle: the shared operation state, the one-at-a-time
queue, install, cancel, status, reclaim and progress publishing. Reusing a whole copy, the free
space check, the download, unpacking, checking each file, the start check, the receipt, the
publishing rename and the failure sentences move unchanged into managed-runtime-release-install.ts,
which gets the pin, folder, label, signal and two callbacks (progress, and the commit point right
after the rename) instead of the class. No behavior change; this keeps the installer well under
the file-length limit once Remove lands in it.

* chore(deps): lock fflate for the in-process runtime unzip

* fix(runtime): a failed download blames the release only for a proven mismatch, so a refused proxy reads as a network problem

* fix(runtime): runtime downloads skip the HTTP cache, so Remove frees the space and nothing is stored twice
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Bound desktop catch-up after independent request support

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Accept readonly client capability fixtures

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
* feat(mobile): Resume in New CLI from a phone chat tab

A chat tab on the phone had only Close in its long-press menu, so a chat
that would not load left no way to reach its conversation. The phone now
offers Resume in New CLI, like the desktop tab menu: the chat's host
prepares the conversation and the phone opens a copy in a new terminal tab.

The resume and fork startup builders move to src/shared so the desktop and
the phone build the same command.

* test(mobile): type the fake terminal create instead of casting its calls

* fix(mobile): pass the host's fork source as is; match the quoted args in the test

* test(mobile): give the fake host client the untyped mock the other client fakes use

* style: format the shared resume builder and phone fork files

* Fix mobile chat CLI fork creation on owning host

* Fix mobile chat fork lint and split create owner helpers

* Keep chat fork beside phone anchor across tab groups

* Combine mobile terminal runtime type imports

* Repair mobile create CI contracts after chat fork extraction

* Classify chat fork preflight refusals and preserve mobile create recordings

* Keep route-owned create cleanup quiet after unmount

* Preserve typed runtime repair refusal in chat fork preflight

* Preserve checked store through mobile chat fork preflight

* Register mobile chat fork runtime fixture before test setup

* Remove unused mobile create recording value

* style(mobile): format the chat fork create files; move fork reply messages into the fork feedback module

Formatting pushed the create hook past its line limit, so the fork-only reply message, partial-success warning and host capability check now live beside the other fork messages. Same order and copy as before.

* Keep quick command mutation aligned with formatted request

* Keep mobile chat CLI forks in terminal view

* Pass mobile chat resolution its existing controller arguments

* Match the mobile create fixture mock layout

* Keep mobile chat CLI forks open when placement is unavailable

* Brace placement contract fixture checks

* test(mobile): keep the prompt controller mock under its line limit after the main merge

* Expose pane admission in the runtime test fixture type

* Complete the chat fork contract controller fixture
…ver a stale lock without displacing a live one (stablyai#29902)

* fix(orcad): never claim or reclaim another machine's orcad, and serialize stale-lock recovery

* fix(orcad): judge data-root locality without a synchronous spawn

* fix(orcad): break a recovery lock only once its owner provably exited

* fix(orcad): publish the recovery lock with its owner in one rename

* fix(orcad): recover a stale lock in generations so no live reclaimer is displaced

* fix(orcad): keep a released recovery generation until the stale lock is replaced

* fix(orcad): fail closed on an unreadable recovery owner, and never let cleanup fail a won lock

* fix(orcad): key recovery locks by the stale record alone
stablyai#29268)

* fix(native-chat): a dot between the queue header's label and its count

* style(mobile): wrap the queue header dot's props

* refactor(mobile): move the queued card's text into its own file

The queue file sat at the 400-line cap, so the header dot had no room.
…whose program Orca provides (stablyai#29024)

* feat(acp): hosts install a managed runtime over RPC, and mark agents whose program Orca provides

managedRuntime.status / managedRuntime.install / managedRuntime.cancel, negotiated by
managed-runtime.install.v1, act on the host that receives them, the one that runs the agent's
chats, with no workspace needed. A status read naming the revision it shows is a long-poll on its
own call-queue lane, so every client sees another's start, progress and outcome.
agentSession.agents marks an agent whose program Orca provides on that host.

* fix(runtime): install status says a copy outside Orca's runs only when a start would really run it

* fix(runtime): install status is pushed to subscribers instead of long-polled, and Cancel answers with the outcome

managedRuntime.subscribe {agent} sends a full snapshot on subscribe, then one
snapshot per install state change, and closes with the transport. It replaces
the managedRuntime.status long-poll and its slot registrations.
…stablyai#29148)

* fix(mobile): a streaming reply reveals word by word, like the desktop

The phone drew each streamed chunk as it landed, so a reply arrived in a few
jumps. Move the desktop's paced reveal into src/shared and use it on both
surfaces: the phone paces the growing block of the newest reply, remembers
progress per chat list so a recycled row resumes, and draws everything at once
with reduced motion. The terminal-backed chat's live row now counts as growing.

* fix(mobile): hold the reply reveals in the chat overlay

The list's turn resolver must not scan terminal-backed transcripts, and the
overlay outlives the list across chat and terminal toggles, so it owns the
reveal memory and the reduced-motion read. Also fix the moved hook's import
in the onboarding route, and mock the OS read where tests render the overlay.

* test(mobile): mock the message row's other children in the reveal test

Their real modules reach React Native's Flow sources, which Bun cannot load;
the other message row tests mock them the same way.

* style: format the desktop paced-text hook

* Keep phone reply reveal progress owned by its message

* Add braces around phone reveal test assertion

* Start phone prose streaming only at a text block

* Find the mocked phone tool row with a node predicate

* refactor(mobile): move chat prose rendering into its own file

Keeps MobileNativeChatMessage.tsx under the 400-line limit after the main merge.
…SH server (stablyai#29908)

A folder workspace has no repo row, only a synthetic repoId, so the panel's repo lookup came back
empty and it said "No workspace selected", hiding the SSH forwards and the server's listeners.
…scoped by identity (stablyai#29909)

When two hosts share a worktree id, the caller's cwd resolves to an identity: selector, which
carries no repo id, so worktree create reported "Missing repo selector". Keep the selected
worktree id alongside the selector and infer the repo from it.
…ared by every surface (stablyai#29048)

* feat(acp): the renderer follows a host's managed runtime install state, shared by every surface

One entry per host runtime instance and agent holds the host's answer. While any shown surface
observes it, one managedRuntime.status long-poll follows the host, idle included, so another
client's install shows up. A failed first read is its own state, never "not installed"; a reply
older than the one shown is dropped. Observed only for agents the host marks as Orca-provided, on
hosts that advertise managed-runtime.install.v1.

* fix(settings): keep one install status wait per host, retry it when the host is back, and wait for the host's agent list

- Hiding and showing a chat no longer leaves a status wait behind on the host for each cycle: the
  wait is never aborted, and a surface shown again reuses the one in flight.
- The host answering again after an outage retries a failed status read at once.
- While the host's capabilities or agent list are still loading, the install state is pending
  rather than "not provided", so a chat doesn't flash its composer; a failed list read reads as
  a failed status read, so Settings says so instead of hiding Install.

* fix(chat): only a reachable host's outstanding agent list holds the Install slot; an offline host or a list without the managed mark keeps the composer

* fix(acp): the renderer follows install status over one host subscription instead of a long-poll

Each host and agent shown on screen holds one managedRuntime.subscribe stream, ended when the last surface hides it. A lost stream resubscribes with backoff, or at once when the host is back; every subscription is a new generation, so a restarted host's snapshot replaces the old state even though its revision count restarted, and a late frame or install reply from a superseded subscription is dropped. Cancel's reply is no longer applied: the stream shows its outcome.
…en, and an exited or abandoned pane never comes back (stablyai#29910)

A reattach applies the relay's screen from main's output stream instead of the invoke reply, so its clear never erases output printed after the snapshot. An endpoint refresh re-reads leases after asking its relays, so a pane that exited meanwhile is not republished live. An attach answered after its pane left hands its viewer back to main.

Co-authored-by: Claude <noreply@anthropic.com>
…ther desktop's restart can clear it (stablyai#29915)

* fix(ssh): re-check a macOS host's ended launch-session warning so another desktop's restart can clear it

The ended verdict was cached for the whole SSH session, so when another desktop restarted the server under a live session, this desktop kept the warning. It now expires like every other verdict: a live re-probe clears the note, and an unanswered one keeps the proven verdict.

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

* fix(ssh): only a live probe clears a proven launch-session end, and a failed recheck republishes it

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

* fix(ssh): a failed launch-session recheck restores the warning only for the same live connection

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
brennanb2025 and others added 29 commits October 18, 2026 00:40
… agent holds the send (stablyai#30134)

* fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send

Each adapter reports the sends it still holds (Claude's dispatch waiters, Pi's sends until
settlement, ACP's unsettled prompt and steers). A late settlement's write runs inside the
per-send handover window, so a send reads held until its row lands, and a failed write
reads as doubt at once. Removes the idle-release API and its callers.

* Fix interruption reporting from held send ownership

* Simplify ACP reading control cleanup registration

* Preserve provider child identity when reattaching

* Replay original attach in recovery overlap regression
…ore its teardown runs (stablyai#30245)

The Disconnect is recorded synchronously but the teardown that disposes the connection waits
behind the target's lifecycle; until then a drop's backoff, a wake retry or reconnect() could dial
the host again. SshConnection now consults the same per-host record the connection manager does.
…ogle sign-in (stablyai#28650)

* feat(antigravity): sign this host's Antigravity chats in with the server's own Google sign-in

Start spawns the Antigravity ACP server exactly as a chat start would (pinned
runtime, private temp, owner-only chat folder), behind the launch gate that
first stops this host's running Antigravity chats, and asks it to authenticate
with oauth-personal. A host without a pinned server build refuses Start, as it
refuses chats. The server's sign-in line now yields its link (validated as
Google's sign-in page back to a loopback port here) instead of ending the
connection signed out. Link long-polls the flow's state (25 s, early on change),
Cancel ends it, a new Start replaces it, and it expires after 300 s. All run on
the execution host behind accounts.antigravity-chat-sign-in.v1 and need
accounts-admin. List reports chatSignIn {saved, signingIn}; saved is a stat of
the server's own token file, which Orca never reads.

* Retain unclosed sign-in children and detach ended polls

* Narrow sign-in regression launch fields

* Keep fake sign-in fixtures independent of host support

* fix(antigravity): chat sign-in never installs the server and keeps its flow on the shared revisioned state

Sign-in resolves the server exactly as a chat start does and fails with the
not-installed reason when no copy is found, instead of downloading one. Its
flow state is a RevisionedState, the same publish-and-wait primitive the
runtime installer uses; replacement, close proof and the Link wire are unchanged.

* fix(antigravity): the chat sign-in status names the running sign-in, so a client that comes back can follow and cancel it

chatSignIn gains an optional flowId while a sign-in runs. Before, a sign-in nobody watched held the
chats' fence for up to 5 minutes and no client could see or cancel it.

* feat(antigravity): clients follow this host's chat sign-in by revision, as they follow an install

* fix(antigravity): name the sign-in state apart from the sign-in function so the module parses

* fix(antigravity): chat sign-in status is pushed to subscribers, failures read as plain sentences

accounts.antigravityChatSignInSubscribe (no params) sends the full sign-in
status as a snapshot on subscribe, then one frame per state revision, and
closes with the transport. It replaces the accounts.antigravityChatSignInStatus
long-poll, its long-wait lane and the bounded revision wait, as the install
status already did.

A failed sign-in now tells the person what happened in plain words (no
subscription, account not eligible, not approved, not installed, or a
generic retry) and logs the raw error instead of showing it.

Stopping the running chats before a sign-in waits for every close to settle
before it reports, so the fence never lifts while a close is still running.

* feat(antigravity): finish a chat sign-in from the address pasted by hand

When the browser runs on another computer than the host (SSH or a paired remote host), Google's last page points at a port on the host that the browser cannot reach, so the sign-in never finished. accounts.antigravityChatSignInComplete takes that pasted address, checks it is Google's answer to this flow's own link (same loopback origin and path, one matching state, one code or error, no userinfo or fragment, a Google issuer if any), and sends it once to the server's listener on the host with a single direct GET. The flow shows a new verifying phase while the server finishes, and refuses a second paste.

* fix(antigravity): Cancel answers once the sign-in has unwound, so a chat sent right after it is not refused

* fix(antigravity): the sign-in reads its launch sources through the shared helper, keeping the session create support within its line limit

Main's file already sits at the limit, and the runtime method only forwarded to the helper.

* fix(antigravity): a rejected pasted sign-in address says what is wrong with it

Every bad address used to get one sentence. It now gets the specific one:
too long, not a complete address, not this sign-in's, not exactly one
Google answer, or not from Google. The checks themselves are unchanged.

* fix(antigravity): chat sign-in follows the server's answers and belongs to the client that started it

The host's chat sign-in status was a check that the server's sign-in file
existed, which says nothing about whether the sign-in still works. The
link came only from one printed line, a new Start from any device
silently replaced a running sign-in, and a second link replaced the first.

- The server's BROWSER is now a one-line helper run by Orca's own
  executable in Node mode, which prints the link on error output; it is
  checked before every server start, and the printed line is still read,
  on standard output as well as error output. No browser opens on the
  host, Windows included.
- A sign-in belongs to the client that started it. Others see it in
  progress without its link and cannot start, cancel or complete it; it
  ends on its own after five minutes.
- After Google answers, a session on the same server must start before
  the sign-in reads as signed in.
- The host's status is the server's latest answer: signed in when a chat
  session or the sign-in's session starts, signed out when a chat hits
  the sign-in prompt or is refused for sign-in, and unknown until then.
  Nothing is read from the sign-in file and nothing is saved.
- A second, different sign-in link fails the sign-in.

* test(antigravity): the launch parity record shows the sign-in link helper as the server's browser

* fix(antigravity): a sign-in owns its work until it has unwound, and reads only whole, valid sign-in lines

Problems in the previous round: a sign-in could read as finished while its
server or pasted-address request still ran, a server refusing a sign-in
after a chat started left the host shown as signed in, the checking
session stayed saved in the server's own store, and a broken sign-in line
could count as the server asking to sign in.

- The browser helper is POSIX only, with Node mode set inside the BROWSER
  command; Antigravity chats are not offered on Windows.
- Sign-in lines are read by source: standard output carries the printed
  line, error output the printed line and the helper's. Only a whole,
  valid link is an answer; a malformed one on error output is dropped, and
  on standard output it ends the server. Standard output goes through the
  protocol's own line reader, including a last unterminated line, and
  error output keeps a whole link while its diagnostic tail stays small.
- Any request the server refuses for sign-in marks the host signed out.
- A sign-in is the host's one operation from Start until its work has
  unwound; a Start before then is refused, and Cancel or expiry also stops
  a pasted address on its way to the server.
- A different second link fails the sign-in at any point, including while
  it is being checked.
- The checking session's saved conversation is removed once the server is
  proven gone, only when its metadata names the sign-in's own folder.
- A pasted address is checked for length before it is trimmed.

* Preserve ACP auth answer ordering and sign-in cleanup

* Fix ACP settlement lint and await sign-in cleanup in fixtures

* test: update Antigravity browser launch snapshots
…frame before sending it (stablyai#30249)

A manifest under the 768 KiB validation cap can still overflow the host's 1 MiB inbound
frame once sealed (base64 adds a third), more so when a commit carries its blob list. The
host answers such a frame by closing the socket, so the move failed with a lost answer.
The catalog client now measures the request against REMOTE_RUNTIME_MAX_INBOUND_REQUEST_JSON_BYTES
and refuses it with the existing 'too large' guidance, unsent.
…ree ids (stablyai#30238)

Worktree ids built as `${repo.id}::${path}` by hand are how one worktree got two
spellings (stablyai#30150). New code composes them through src/shared/worktree/id.ts; the
ratchet counts the remaining hand-built ids per file (18) and may only go down. Six
byte-identical `${repo.id}::${repo.path}` sites move to getRepoMainWorktreeId.
…lyai#30258)

Every split pane's tab strip stays mounted, so focusing a pane used to add or remove its action buttons and reflow the tabs. Quick-command shortcuts also fired from hidden or unfocused groups.

- Keep the Pane Actions and quick-command buttons mounted in every strip. Unfocused strips dim instead of disappearing, so focus no longer reflows the tabs.
- Hide the button labels by strip width with container queries, so narrow split groups stay icon-only.
- Only the focused, visible group registers quick-command keyboard and Electron shortcut listeners. An open menu closes when its group loses focus.
- Add "Close split pane" to the tab context menus, shown only when the group is one of several splits.
- Stop Radix from restoring focus to a stale group when the pane actions menu closes.
…st one just failed on (stablyai#30253)

failedBefore read the recorded failure before the host lock, so a check waiting behind one that failed still tried the update and noted a stale reason. Rename retryRecordedFailure to failedThisLaunch and read both inside the lock. Also share withDetail and drop a test that could not fail.
…yai#30263)

Its private POSIX copy predates areWorktreePathsEqual's POSIX guard and WSL UNC fold, which now give the same answers; the one difference is macOS treating /tmp and /private/tmp as one directory, which they are. Also reuse looksLikePosixAbsolutePath in the frozen comparison key.
…cks any more (dark) (stablyai#30259)

* feat(sidebar): sort stamps reconcile stored tiers at startup against live hook rows and terminals (dark)

* test(sidebar): a restart clears an unbacked local tier and keeps an SSH host's

* fix(sidebar): run the sort tier reconcile once hook rows and PTYs settle, seeding hydrated rows

* fix(sidebar): clear a stored tier only on proof its terminals died; a rest seed adds no clear

* chore(sidebar): merge origin/main; locality helper moved to shared
… already covers (stablyai#30256)

Since stablyai#30232 it accepts null and covers both remote: and app SSH ids, so the extra isRemoteRuntimePtyId / parseAppSshPtyId / null guards never change the result.
… on the host can still run it (stablyai#29139)

* feat(runtime): remove an agent's downloaded runtime only once nothing on the host can still run it

managedRuntime.remove {agent} (host-admin, capability managed-runtime.remove.v1)
deletes the agent's releases inside the launch gate. Before anything is deleted the
gate aborts starts under way (chat starts and conversation copies), stops the
agent's loaded chats on this host, and proves every retained child gone: failed
chat starts and sign-in children whose close was never proven. Every close settles
before the gate releases; if any owner stays unproven, Remove fails and keeps the
files. Stopping chats by agent is now one generic host helper that sign-in shares.
Install refuses while a removal runs, Remove refuses while an install runs, and a
removal cannot be cancelled.

* fix(runtime): status says whether a copy is there to remove, damaged copies included

canRemove is set whenever Orca's copy is present, whole or damaged, so Settings offers Remove for a
damaged copy too. The failed-start retain is dropped: the launch gate already holds every child it
admitted until it exits.
…roject views (stablyai#29977)

* fix(session-history): find every agent's older sessions in Workspace and Project views

Session History kept only the newest sessions across every agent, then filtered
by workspace; only Claude, Pi and CodeBuddy got their older in-scope sessions
back. Every agent now declares how its cwd is found without a full parse, and
the scan selects in-scope sessions before the recency cap: Codex and other
header agents by their opening record, SQLite stores by their directory column,
the rest by their cached parse. The scan and the cached-list cut share one
scope rule.

* fix(session-history): keep discovery under the file length limit

* fix(session-history): type the scope walk's file list

* Keep session history scope complete before applying the display cap

* test: align session history scope fixtures

* perf(session-history): settle known out-of-scope transcripts without a per-file read

A scoped scan walks every transcript, and each one cost an await, a parse-cache
database lookup and, for Codex, two head reads every scan. Remember each file
version's settled cwd (from its opening records or its parse), drop settled
out-of-scope files synchronously before the async walk, and read Codex's
roughly 20 KB first record in one 64 KB read.

* fix(session-history): let the parse cache decide scope before transcript heads

The settled-cwd prefilter let a transcript-only verdict reject files whose
current parse (hot or cold) says otherwise: a Cursor chat whose meta.json
appeared later, or a Codex session whose parsed latest cwd moved after its
row left memory. Restore parse-cache-first selection; keep Codex's single
64 KB first read.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
…k relay gate for what it gates (stablyai#30264)

Importers call worktreeIdsEqual from shared/worktree/id directly (the alias was a renamed
re-export). isRemoteAgentHooksEnabled becomes isWslAgentHookRelayEnabled; the env var and its
parsing are unchanged. ws-transport inlines its one-line HTTP server factory, and a duplicated
acquisition-id doc comment goes.
…re-export shims (stablyai#30265)

Deletes orcad-runtime-lifecycle.ts and the re-export blocks in orcad-managed-tunnel,
orcad-local-build-hash, orcad-remote-launch, orcad-remote-node-runtime, orcad-update-plan,
relay-protocol and shared/orcad-migration-manifest; importers and test mocks name the real modules.
…0267)

Parity test: one string-skip helper, no infinite loop on an unterminated block comment, no internal shorthand. Serve test: orcadServeArgs takes a bindHost instead of the test splicing argv, typed readiness, and stderr back in the daemon failure. Watcher test: a sentinel at both watched levels.
…tablyai#30274)

The folder is the cloud relay that reaches phones; under its old name it read as the legacy SSH
relay. git mv plus import and path-literal rewrites only.
…n index barrel (stablyai#30269)

Deletes remote-layout-snapshot/index.ts and workspace-layout/old-client/index.ts. The compat-boundary
check now lists each folder's entry modules, so deleting the folder plus its listed importers'
import lines still removes the path.
…nt's turn (stablyai#10923) (stablyai#30273)

OMP ends every user bubble with OSC 133 B C D;0 to close its own prompt zone.
Orca read that D as the shell's command finishing, retired the pane's hook
authority mid-turn and deleted its status row, so the phone's chat view lost the
provider session it reads the transcript by. A D;0 directly after B C is now an
app's zone close, not a command exit.
…e window saves to (dark, layout-owner build only) (stablyai#30196)

* refactor: move the contested-workspace primary-host rule to src/shared (move only)

* fix(layout-owner): main routes a twin repo's workspace to the same primary host the window saves to

* fix(layout-owner): route a twin repo's single-host worktree to the host that holds it; hydrate twins the same way

* fix(layout-owner): twin-repo routing counts only runtime owners from the catalog, matching the window; one table checks both sides

* fix: drop the duplicate layout-owner switch import after merging main

* fix: type the twin-routing fixture and partition set
…ost/ (stablyai#30275)

git mv plus import and path-literal rewrites only.
… in place (stablyai#30271)

* fix(runtime): show Re-pair required for revoked servers and re-pair in place

A server that revokes this computer's pairing token was labeled "Update
required" because auth refusals and protocol mismatches shared one blocked
state. Carry the blocked reason locally, label revoked hosts "Re-pair
required", and add a Re-pair action (sidebar host menu + Settings server
row) that verifies a new pairing link reaches the same server before
replacing the saved pairing.

* fix(sidebar): stop host header menu clicks from toggling or dragging the row

* fix(sidebar): vertically center host header label and menu button
… from their owning modules (stablyai#30279)

Removes the re-export lines in relay-region-preference, relay-session-broker,
browser-host-page-placement and browser-host-page-reconciliation-plan (no barrels).
…i#30205)

Retain reveal sequence coverage until a parsed snapshot covers it, preventing a duplicate text-only replay from clearing newer terminal images. Keep conservative fallback recovery for unknown episodes, missing metadata and different terminal processes.

Verified against current main: 406 tests, full application typecheck, changed-code quality and four fresh hidden rendered restore checks pass. No xterm patches or dependency changes.
…rk, layout-owner build only) (stablyai#30281)

* fix(layout): an SSH pane's terminal binds in its own session partition (dark, layout-owner build only)

Main keeps its own layout over a window save, so a binding only a save carries is lost. A window
or runtime spawn of an SSH pane bound in the local partition; it now binds where the pane's layout
lives.

(cherry picked from commit beb2275)

* test(layout): the SSH binding test uses the shared spawn commit dependencies

(cherry picked from commit 672b641)

* fix(layout): main moves a tab between a local and an SSH workspace itself (dark, layout-owner build only)

moveTabToWorkspace across two partitions was refused as other_partition and left to the window's
save. Main now reads both partitions, moves the tabs as within one, and writes both in one durable
mutation.

(cherry picked from commit ee27a0b)

* chore(layout): one binding write for the partition choice; shipped builds keep their exact replaced-binding read

* revert(layout): drop the SSH binding partition change; SSH terminal starts are refused before any binding is saved

getProvider refuses every non-local host, so neither spawn path reaches persistPtyBinding for an
SSH pane, with the switch on or off.
… through re-export shims (stablyai#30255)

Deletes ssh-system-fallback.ts and the re-export lines in ssh-config-parser.ts, ssh-connection.ts,
system-ssh-file-binary-transfer.ts and ssh-remote-powershell.ts; importers name the real modules.
SshConnection tests mock each real module, keeping the parser stub that holds ~/.ssh/config out.
…oticed (stablyai#30286)

tests/ is typechecked by nothing in CI, so the Windows CLI matrix spec imported a helper that never existed (stablyai#30231). Per-file error counts under tests/ (tsc -p config/tsconfig.e2e.json) may now only go down.
@innocarpe innocarpe added the bug Something isn't working label Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(mobile): match the terminal home-indicator inset to the input panel

9 participants