Skip to content

[upstream #30506] fix(mobile): retire stale terminal stream fits and callbacks - #331

Open
innocarpe wants to merge 8006 commits into
mainfrom
ios-terminal-stream-fit-lifetime-20261019
Open

innocarpe wants to merge 8006 commits into
mainfrom
ios-terminal-stream-fit-lifetime-20261019

Conversation

@innocarpe

Copy link
Copy Markdown
Owner

Portfolio mirror of my contribution to upstream stablyai/orca.
Exhibition only — the real review/merge target is upstream.

Upstream

Summary

ELI5 When the phone reconnects to a terminal, a timer left over from the previous connection could still fire and re-fit the screen to an old layout. Now each connection cleans up after itself, so only the current one can resize the screen. ## What Changed New `createMobile

Note

  • Do not merge this into innocarpe/orca main until the upstream PR is merged.
  • After upstream merges: sync fork from upstream, then close this mirror PR.
  • This open PR exists so visitors see in-flight work on this fork's Pull requests tab.

brennanb2025 and others added 30 commits October 17, 2026 00:01
* fix(native-chat): reserve pending chips before checking dropped files

A local file drop checked each file before adding it, with nothing in the
composer meanwhile: Send during the check went without the file, which then
landed in the next draft; unmounting mid-check lost it; and files queued
behind an input-method composition were lost on unmount.

Drops now reserve a pending chip per file before the check, so Send waits
as it already does for server-upload drops. The prepared attachment travels
through the existing composition queue as one operation, settles into its
draft scope after unmount, releases its chips when refused or disabled, and
reads the scope's current image capability when it applies.

* test(native-chat): server-upload chip calls carry no options without paste

* refactor(native-chat): move the composer's image chip list into its own hook

Main's server-owned paste pushed the attachments hook past the file length limit.

* fix(native-chat): a pending attachment chip shows a spinner and its name, not a stacked icon

A local drop now waits as a pending chip, which drew the kind icon, the file name and a centered spinner on top of each other. Without a thumbnail the chip shows the spinner above the name; with one, the dimmed picture keeps a spinner strip along its bottom.

* test(native-chat): a pending chip shows its name and spinner, no kind icon
… like the phone (stablyai#29120)

* feat(native-chat): one queue header that becomes the pause row; no composer Resume or send dialog

The queued cards now sit under one header row on desktop and phone: "Queued"
and the count normally, and "Paused" with the count, Clear all and Resume
while the host holds the queue after a Stop. A chevron folds only the cards;
the choice is local to the view and resets when the queue empties.

The composer's Send no longer turns into a second Resume, and a message sent
into a paused queue no longer asks "Send message?": it sends, its accepted
turn lifts the pause, and the cards follow it. Clear all deletes every card
through the existing per-card delete. No wire or host change.

* test(mobile): find the background-tasks strip's toggle inside the strip, not the queue header's

* Make queued Clear all atomic across clients

* Show queued message failures in collapsed headers

* Use per-card delete for paused queue Clear all

* Join repeated paused queue Clear actions

* fix(native-chat): a queue action still on its way greys its controls, like the phone

The desktop kept in-flight queue actions only in refs (one per card, one for
Resume, a joined promise for Clear all), so on a slow or remote host pressing
Clear all, Delete, Steer or Send changed nothing on screen until the host
answered. One keyed in-flight set, held as a ref and as render state, now
covers cards, Resume and Clear all; Clear marks every card it deletes.

* fix(native-chat): Resume and Clear all refuse each other in the queue guard; the steer chord skips a card already on its way

Resume and Clear all now hold one shared in-flight key, so each refuses the
other in the guard rather than only in the drawn state, and both header
buttons grey on it. Cmd/Ctrl+Enter returns false when the newest card's
action (or a Clear deleting it) is still out, so the chord no longer
scrolls to the latest with nothing sent. Test controllers gain the new
fields.

* test(native-chat): move the slow Clear all test into its own file

The queued-list test file passed its line limit; the in-flight test and the
held-write render it needs now live beside it, and the list test file is
back to the base's helpers.

* fix(native-chat): Clear all waits out a card's own action instead of skipping the card

Clear all now greys while any card in the queue has its own Steer or Delete
on its way, and once that settles it claims every card. Before, it stayed
enabled and quietly left the busy cards out, so a failed Delete left a card
behind after Clear all, and with every card busy the click did nothing.

* chore(native-chat): one comment for Clear all's availability rule

* test(native-chat): update held-start queue fixture after main merge
* fix(mobile): edit queued messages in place

* fix(mobile): use the platform input font size for queued edits

* fix(mobile): handle missing host support while probing queued edits

* Keep phone queued edits owned through save settlement

* Keep phone queued edits local to mounted chat

* fix(mobile): retire queued editor when its card disappears

* fix(mobile): retain queued edits while reconnecting

* test(mobile): await reconnect timer inside act callback

* fix(mobile): publish queued edit transport after commit

* test(mobile): await queued Edit inside act callbacks

* fix(mobile): hand a stale queued Save to the composer and reopen on the newer text

* fix(mobile): close an unchanged queued Save locally and keep stale typing in its own chat

* fix(mobile): explain disconnected queued edit refusal

* fix(mobile): combine queued session contract imports
…ai#28655)

* feat(native-chat): image messages wait in the queue like text

A message with images now queues behind the running turn and the cards
ahead of it, instead of skipping the queue. Cards show an image icon and
count, an image-only card names its image, and editing a card's text keeps
its images. The host publishes no card holding an image to a client that
has not advertised agent-session.queued-image-cards.v1, since released
clients edit by copying a card's text and deleting the card.

* test(native-chat): an image follow-up queues like text

* fix(native-chat): project queued images before phone frame limits

* fix(native-chat): preserve image sends from older clients

* Clarify direct text recovery at send admission

* fix(native-chat): expose queued image names to keyboard users
…and fit the force-delete button to its notice (stablyai#29878)

* fix(sidebar): let kept-branch toast bodies fill the toast width

* fix(sidebar): close the force-delete menu when its toast is hidden in the stack

* fix(sidebar): keep focus out of a hidden toast after its menu auto-closes

When the toast stack hid a kept-branch or delete-failure toast, the menu
closed but Radix returned focus to the chevron inside the now invisible
toast, so Enter or ArrowDown reopened the menu floating over the app with
the "Always ..." item focused.

- Skip the close focus return while the toast is hidden.
- Refuse to open the menu while its toast is hidden.
- Treat toasts past Sonner's visible-toast limit (data-visible=false) as
  hidden too.

* fix(toasts): stop a folded toast's hidden overflow from re-expanding the stack

* fix(toasts): clip a folded toast to its card instead of disabling its content

The pointer-events rule left the unstyled star-nag card, which Sonner never
fades, visible below the stack but dead to the pointer. Clipping the folded
toast removes the overhang for every toast type, visible or not, so nothing
of a folded toast can sit outside the stack and re-expand it.
…ntity rekey (stablyai#30005)

Main's Store.migrateWorktreeIdentity re-keys every stored layout field before it sends the
rename event the window re-keys on, so the window's save only mirrors it. The new oracle checks
main's migrated layout model equals the model of the window's post-rekey save and the model's
identityRenamed transition; the first-work test pins migrate-before-notify.
…ver keeps running (stablyai#30006)

A paired runtime (orca serve, managed SSH/WSL orcad) owns its terminals and keeps
them after this client quits, so only SSH relay terminals, which a relay kills
after its grace period, still raise the quit warning. Window close is unchanged.

Fixes stablyai#29916
…stead of guessing this computer (stablyai#30009)

* fix(renderer): resolveOwner keeps folder and project-group twins ambiguous; deprecate the runtime-owner aliases

* test(store): folder and project-group twins in the twin suite; no-host folder delete refuses

* fix review: a folder's own SSH connection names its owner despite twin project groups
…e stand-in (stablyai#30011)

* test(e2e): open and close structured chat tabs through an isolated Claude stub

* test(e2e): satisfy changed-code lint in the structured chat stub
…#30012)

The hostScope annotation on terminal list, worktree list and worktree ps
named --host local / --host ssh:<id> as the follow-up for an omitted host,
but none of those commands accept --host. Those hosts are reached through
the runtime that answered, so no flag selects them: report a null selector
and say so in the human scope line. Paired servers keep --environment.

Fixes stablyai#29901
…tablyai#30014)

On a headless server (orca serve, orcad) files.open and files.openDiff
answer with the bare renderer_unavailable token, which the CLI printed
as-is. Translate it in the CLI into a clear message with next steps,
keeping a stable renderer_unavailable code. The wire answer is
unchanged because the mobile app keys its preview fallback on it.

Fixes stablyai#29822
stablyai#30002)

* chore(ci): ratchet renderer importers of the deprecated runtime-owner resolvers

getRuntimeEnvironmentIdForWorktree and resolveRuntimeOwnerForWorktree read a
worktree two hosts publish as not-a-runtime, which callers treat as local. The
62 renderer files importing them are grandfathered; the list may only shrink.

* ci: register the deprecated runtime-owner ratchet step in the preflight phase map
…9089)

* fix(native-chat): paste a copied file into Chat the way dropping it attaches it

Copying an image file in Finder and pasting into Chat typed the file name
and attached nothing: the paste only attached a bitmap from the clipboard.
Copied files now go through the composer's drop route, on both the
app-menu paste and the paste event.

Fixes STA-10652

* refactor(native-chat): share the paste guards with the copied-files branch

* test(native-chat): late paste results stay dropped when the image waits for the clipboard read

* Keep copied attachments pending through preparation and composition

* Update file drop fixture settlement results

* Keep pending attachment capability current within its draft scope

* fix(native-chat): reserve pending chips before checking dropped files

A local file drop checked each file before adding it, with nothing in the
composer meanwhile: Send during the check went without the file, which then
landed in the next draft; unmounting mid-check lost it; and files queued
behind an input-method composition were lost on unmount.

Drops now reserve a pending chip per file before the check, so Send waits
as it already does for server-upload drops. The prepared attachment travels
through the existing composition queue as one operation, settles into its
draft scope after unmount, releases its chips when refused or disabled, and
reads the scope's current image capability when it applies.

* test(native-chat): server-upload chip calls carry no options without paste

* refactor(native-chat): move the composer's image chip list into its own hook

Main's server-owned paste pushed the attachments hook past the file length limit.

* fix(native-chat): a screenshot picture pastes as a picture on macOS, even beside its saved file

macOS lists a screenshot tool's saved file beside its picture, so the paste read as a file copy and attached nothing once the file was gone. Only a Finder copy also names its files as text.

* fix(native-chat): a macOS right-click paste lets the clipboard tell Finder files from a screenshot

The paste event hides Finder's file names, so a path-backed picture reserves its chip at once and the native clipboard decides: named files go to the drop route, a screenshot's picture is saved from the clipboard. Both paste routes now share the clipboard-picture save.

* test(native-chat): move the paste hook's test probe into its own fixtures file

The paste tests outgrew the test file length limit.

* test(native-chat): hand the paste probe's hook result out after render

* test(native-chat): give the remote image paste probe the copied-files callback

* fix(native-chat): a pending attachment chip shows a spinner and its name, not a stacked icon

A local drop now waits as a pending chip, which drew the kind icon, the file name and a centered spinner on top of each other. Without a thumbnail the chip shows the spinner above the name; with one, the dimmed picture keeps a spinner strip along its bottom.

* test(native-chat): a pending chip shows its name and spinner, no kind icon

* fix(native-chat): a macOS file paste keeps its own chips until the clipboard decides

A macOS paste of Finder files reserved one placeholder chip, asked the native
clipboard whether it held files or a screenshot picture, and then started a
separate attach for the files. Removing the placeholder did not cancel that
attach, and leaving the chat before the answer lost the paste.

The paste now reserves one chip per copied file before asking, and captures
the drop route's intake (owner, destination, chips) at the same moment. Files
continue those exact chips through the existing local check or server upload,
so removal cancels by the original chip and an unmounted composer still
finishes into its draft. A picture keeps the first chip and drops the rest.
Ordinary drops and the picker keep their fresh-admission checks.

* test(native-chat): a macOS file paste survives removal, unmount and disable through the real intake

Drives the real pending cache, composer attachments, external intake and paste
hook with a delayed clipboard answer: removed chips never attach (before or
after the answer, through input-method composition), an unmount finishes both
the local and paired-server routes into the original draft with the original
chip IDs, and disabling the composer meanwhile does not bring the paste back.
…' visit recency (stablyai#30015)

Converting an SSH host drops its relay-era rows, but recency stayed under
ssh:<target>|<id> keys, which the next startup prune discards because no row
matches them any more. Re-key that host's visits to runtime:<env>|<id> in the
same update, keeping the newer visit on a collision.
…SH host moves again (stablyai#27285) (stablyai#30017)

* fix(ssh): link a folder workspace the server kept from an earlier move instead of conflicting (stablyai#27285)

* fix(ssh): resolve a server folder's host before linking it, and read folder lease owners

* fix(ssh): name a linked folder workspace in the connect's left-behind warning
* feat(native-chat): choose which agents open as a chat

Settings → Chat gets a Providers list. Each agent the host can open as a chat
has a switch; off opens that agent in a terminal instead. Only choices the user
makes are saved. Unchosen: Claude and Codex on, others on when their CLI is
installed on this computer.

The renderer's route and the host's launch-mode decision both read one rule, so
the tab menu, shortcuts, CLI, orchestration and relayed phone launches agree.

* fix: keep chat provider settings with the other chat settings types

* test: route input carries this computer's detected agents

* fix(chat): resolve provider choices from their owning computer

* test(chat): use checked native inventory fixtures

* Fix launch preparation lifetime and execution host startup

* fix(chat): keep unchosen providers on without discovery

* test(chat): type provider routing fixtures against store contracts

* test(chat): align shared launch fixture with private settings reader

* test(chat): give the structured-session RPC fixture the private settings reader

The host now reads saved chat choices through requireStore().getSettings(); the
shared RPC fixture only had getClientSettings, so the launch-mode reader saw no
settings and create support refused Claude. The fixture now follows each test's
client settings.

* fix: preserve provider choice and cached native inventory
…rder, and SSH/WSL host state, checked against the window in dev and test (stablyai#30018)

* feat(sidebar): main publishes desktop focus, pins, project and host order, and SSH/WSL host state, checked against the window in dev and test

* fix(sidebar): read pins through the host-filtered metadata helper; name pins as workspaces
…ly) (stablyai#30019)

* Record chat tab opens, closes and host-listed chats in main (dark, layout-owner build only)

A chat tab the window opens, closes, or adds and removes from a host
listing of this machine's chats now reaches main as openAgentSessionTab,
closeTabs or setTabProps under the window's ids, so the window's save no
longer teaches main a chat tab. openAgentSessionTab takes the window's
place, creation time and new split group, and the same open again
changes nothing. Switch-off builds are unchanged.

* test(layout): a paired server's chat tabs send nothing to main (dark)

* fix(layout): keep a windowless host's chat-tab saves as they were; send synced chat opens in tab order (dark)

Only a window's openAgentSessionTab (main marks it drawn on layout.apply)
takes the new rules: a repeat changes nothing, a taken id is refused, and
a workspace with no group takes the window's. A windowless host's own
saves mint a fresh group and keep a chat whose id another tab holds, as
before. A host listing's new chats are recorded in tab order, so each
open's anchor already reached main.

* refactor(layout): one rule set for a chat tab open; the windowless caller names only a group it holds (dark)

Drops the per-sender drawn flag. The windowless save passes its
remembered active group only when the workspace still holds it, so it
mints a fresh group as before. Refusing a taken chat tab id is gated on
the build switch: shipped builds still save the duplicate id.
…tart or turn signed out (stablyai#28467)

* feat(acp): a sign-in prompt on an ACP agent's stderr ends the start or turn signed out

An agent that waits for a browser sign-in prints a link on stderr and blocks for
minutes. A launch spec can now name that line (signInPrompt); the connection then
closes with a typed sign-in failure through the existing process owner, so a start
reads notSignedIn and a running turn fails signed out instead of settling unknown.
The matched line, which carries the sign-in URL, never enters the retained stderr
tail, errors or diagnostics.

* Prevent sign-in URLs leaking from long stderr lines

* fix(acp): a stderr line cut at the cap that is not a recognised prompt is dropped, so its URL never reaches the kept tail
…pting (stablyai#30025)

* fix(ssh): a host that needs a password waits for the user at launch instead of prompting

Restores the persisted 'last sign-in needed a typed credential' flag that
the relay removal (stablyai#27214) stopped writing, and at launch marks those hosts
as awaiting sign-in so background reconnects (startup restore, managed-server
status checks and tunnels) refuse without dialing instead of opening the
password dialog. The user's own Connect lifts it and prompts. The terminal
toast shows the refusal as an SSH state, not a bug to report.

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

* test(ssh): give the raw-connected fixture the cache query; annotate the harness-shaped registration

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

* refactor(terminal): keep the sign-in branch out of the toast component

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
… against the window in dev and test (stablyai#30027)

* feat(sidebar): main keeps one live set of paired-server rows, checked against the window in dev and test

* fix(sidebar): settle review findings; main also publishes paired-server host facts
…tablyai#28455)

* feat(acp): an agent can enforce the permission mode itself through a session config option

An optional launch-spec field names the session config option and its ask/full values. Every new or
reopened session gets the value for the chat's full-access posture before it is exposed, and a refusal
fails the start. With the field present Orca no longer answers permission requests for the person.
No agent sets it yet.

* fix(acp): reject unconfirmed permission mode during acquisition

* feat(acp): an agent enforcing the permission mode itself runs the chat's own mode

The launch spec maps each chat permission mode to its session option value; a start sets the
value for the mode the host launches with (ask when it names none), replacing the global Agent
Permissions full-access read. A mid-chat change takes effect through the host's existing restart
of a child launched with another mode before the next send.

* Keep ACP session acquisition within the line limit

* fix(acp): a chat's permission mode goes to the agent's mode-category option, whatever the agent names it
…#30008)

* fix(mobile): the chat's Working line uses desktop's spinner (turning circle, hourglass under reduced motion)

* fix(mobile): keep working animations off interaction queue
…tablyai#30029)

Naming a chat runs a one-shot agent CLI request in the chat's folder, and the CLI saves it beside the user's chats. Session History listed it, and opening it from a phone resumed the naming request instead of the chat.

Fixes stablyai#29973

Co-authored-by: Claude <noreply@anthropic.com>
stablyai#30028)

On a host with no window, chat tabs live only in the runtime's session snapshot. A forced rebuild from the saved session (closing a saved editor tab, an expired SSH reattach) dropped them, and every terminal-only group merge removed them from the tab order.

Refs stablyai#29968

Co-authored-by: Claude <noreply@anthropic.com>
…the window in dev and test (stablyai#30026)

* feat(sidebar): main produces per-workspace PR facts, checked against the window in dev and test

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

* refactor(sidebar): share the PR cache key candidates instead of splitting an import

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…on retap and revisit (stablyai#30033)

* fix(mobile): prevent markdown and file tab reload and screen flicker on retap and revisit

* fix(mobile): preserve newer saves and refresh document metadata

* test(mobile): supply current document state in recorded reads

---------

Co-authored-by: Neil <neil@stably.ai>
…top renames (stablyai#30037)

A desktop connected to a remote Orca server kept its own copy of a tab's
manual name, and ranked its saved-session title (a resumed Codex chat)
above the live title the server publishes. So orca terminal rename on the
server never showed on a resumed tab, and a desktop rename never reached
the server.

The server now publishes the tab's manual name as an optional customTitle
on each terminal surface, and the desktop mirror adopts it. Desktop renames
go to the server: terminal.rename by handle, plus session.tabs.setTabProps
customTitle by tab id for panes that are asleep or starting. Older servers
omit customTitle and strip the new param, so the desktop keeps its own
name with them, as before.

Fixes stablyai#29975
younghwan91 and others added 29 commits October 18, 2026 15:30
…i#30379)

* fix(drops): name extensionless dropped images by their bytes

A file dropped onto a remote workspace with no extension (some drag sources
hand images over as `noname`) now gets the extension its PNG/JPEG/GIF/WebP
signature declares, so the agent sees `noname.png`. Repo icon detection
reuses the same signature check.

Fixes stablyai#30378

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

* fix(drops): sniff extensionless images only for .orca/drops and chat attachments

Explorer imports into a user-picked remote folder keep the name, matching
local imports. Chat attachments on a paired server now use the staged name
so they get the extension too.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
* Open file links beside their source tab's group

Clicking a file link in a terminal or agent chat now opens the file in an editor next to that tab, not in whichever group happens to have focus. A new "Open in this group" row pins the file to the source group when that group has neighbors.

- Capture the source tab at click time, so focus changes during path resolution don't redirect the file.
- Rank candidate groups by adjacency: a neighbor already showing the file, then a neighbor showing an editor, then any neighbor, then the source group.
- "Open in this group" never falls back to a browser, system app, or other group. If the group is gone or the target is not an editor, it shows a toast and does nothing.
- Line reveals target the exact view that was opened, and are dropped if that view closes before the editor applies them.

* rm scratch files

* Preserve empty split panes during tab reconciliation

Reconciliation removed every empty group, so an empty split pane the user had made beside a tab was collapsed on the next pass. A file link opened from that tab then landed in the source group instead of the empty pane. Reconciliation now removes only groups that stale-tab cleanup emptied, and keeps panes that were already empty. Tests cover terminal and agent-session sources in both worktree and folder workspaces.

* Preserve runtime owner for local file links
…k, layout-owner build only) (stablyai#30439)

* fix(layout): a CLI split whose pane main closes before it mounts answers at once (dark, layout-owner build only)

* test(layout): the closed-before-mount split fails because of the close
…ght seams from their defining modules (stablyai#30440)

The three ipc modules re-exported symbols owned elsewhere. Importers and test mocks now name the
defining module, and the re-export lines are gone (no barrels).
…ting would stamp (stablyai#30445)

A tab restored before its worktree catalog loads gets a placeholder row. SSH placeholders had no
host, and a server's nested-SSH placeholder carried only runtime:E, so an ssh: host selection found
no matching owner and routed the terminal as a direct dial from this desktop. Stamp the SSH target,
and the server proxying it, from the repo row; leave a repo id that lives on several hosts unstamped.
…s the whole page and clicks land under the cursor (stablyai#30437)

* fix(browser): a remote viewer larger than the host's browser pane sees the whole page and clicks land under the cursor

Fixes stablyai#30341

* test: note why the screencast double is cast

* fix(browser): drop guest frames until the fitted size is applied; forget the cached CSS size when the frame size changes

* fix(browser): keep a press's release and markup inside the letterboxed remote frame

* fix(browser): resume live frames once fitted metrics land; remeasure the page after its frame size changes
…its own host's copy (stablyai#30447)

applyCreatedWorktree merged a create into any same-id row, so a local create could inherit a
server twin's stamp (or wipe it). Lineage refresh stamped a server's reply with the focused host and
replaced every same-id row, overwriting the local twin. Both now match id and host, and the lineage
reply is stamped with the transport it came from.
…e-SSH (stablyai#30441)

* fix(open-in): open a managed server's workspace in VS Code over Remote-SSH (stablyai#30332)

* fix(open-in): only open a server workspace over SSH when its files are on that host; validate the SSH link; let process-host quote code.cmd

* test(open-in): expect the workspace ref in the source-control Open in payload

* fix(open-in): pin an SSH-access server's editor host to the settings it was verified on
…nd cancel, on every Orca sign-in button (stablyai#30410)

* fix(cloud-auth): show the browser sign-in wait and offer its link from Relay sign-in

Clicking 'Sign in for Relay' handed the sign-in page to the OS browser and
then showed nothing, so a browser that never came forward looked like a dead
button, and the 5-minute wait ended silently. The Relay sign-in row now shows
the wait with a Copy sign-in link button, and a wait that times out toasts.

STA-10940

* fix(cloud-auth): show the sign-in wait on every Orca sign-in button

The waiting text and Copy sign-in link now come from one shared notice used by
Mobile Relay, Orca Account, Artifacts (settings, page and publish popover),
Share Skills, the unexpected sign-out card and dev tools. The copyable link is
dropped as soon as the browser answers, and a copy with no waiting link
reports a failure instead of doing nothing.

STA-10940

* refactor(cloud-auth): main owns the waiting sign-in link; renderer only mirrors it

The notice now shows exactly while main holds a live link: main announces the
link when the browser wait starts and clears it the moment the browser
answers, so 'Waiting for sign-in in your browser' no longer outlives the wait
during the token exchange. Only the newest attempt may publish its link, so an
older attempt reporting late cannot replace or later wipe it. The renderer's
separate pending flag and its resets are gone.

STA-10940

* fix(cloud-auth): tolerate a sign-in link read that returns no promise

The app-wide IPC tests stub every bridge method as returning nothing, and the
one-time link read called .then on that result.

STA-10940

* fix(cloud-auth): a torn-down link subscription ignores its late snapshot; restore auth bridge tests

A window's one-time sign-in link read could land after its subscription was
disposed and overwrite a newer value from the replacement subscription. The
auth-status refresh tests the earlier rewrite dropped are back, alongside the
link cases.

STA-10940

* feat(cloud-auth): sign-in buttons show Signing in… with copy-link and cancel links

Every Orca sign-in button greys out to 'Signing in…' with a spinner until the
sign-in ends. While the browser has not answered, a line offers 'Copy sign-in
link' and 'Cancel'; Cancel aborts the browser wait so a lost tab never locks
the button. Main owns the in-flight sign-in (waiting on the browser, then
finishing) and every window mirrors it.

STA-10940
…g unpriced models (stablyai#30421)

normalizeModelForPricing was an ordered chain of regexes in which a new point
release fell through to its major: claude-sonnet-5-5 was billed as Sonnet 5
(cache reads at $0.20 instead of $0.10), and any future opus-5-x / fable-5-x
would be too. claude-haiku-5-5 matched nothing and silently added $0 to a
total that showed no caveat.

The resolver now parses family + major + optional minor (family-first and
legacy version-first IDs) and only accepts an exact MODEL_PRICING key, so an
unknown version is unpriced rather than borrowing another model's rates. The
deliberate opus-4.x -> 4.8 and sonnet-4.x -> 4.6 fallbacks go with it.

Adds Sonnet 5.5 and Mythos 5/5.1 rows. Haiku 5.5 stays unpriced: it reprices a
whole request above 100K prompt tokens, which daily totals cannot express.

The Claude summary now carries hasUnpricedModels (as Codex has since stablyai#22073),
and both panes share one unpricedCostLabel helper for the caveat.

Fixes stablyai#30338
…tablyai#30446)

* Check sign-in duplicates against the listed account identity

A managed account folder's current Claude login can differ from the identity saved for it. The account list shows the current login, but the duplicate check compared the saved identity, so a sign-in could be missed or wrongly flagged. Duplicate detection now uses the same listed accounts as the account list, and the duplicate helper accepts account summaries. Tests cover a folder whose login email changed, and one where only the organization changed.

* Tolerate invalid Claude account rows during sign-in
… Haiku 5.5 (stablyai#30427)

* fix(usage): price Claude long-context requests per request, and price Haiku 5.5

Haiku 5.5 bills a whole request at higher rates once its prompt (input plus
cache reads and writes) exceeds 100K tokens. Claude usage was only kept as
daily totals, so that could not be priced and Haiku 5.5 stayed unpriced. The
existing Sonnet 4.5/4 200K tier had the opposite problem: it was applied as a
marginal threshold to each day's totals, so a day of short requests could be
surcharged and a single long request was under-charged (the Claude side of
STA-8117, fixed for Codex in stablyai#22360).

Each turn is now classified when it is aggregated, against its own model's
threshold, and daily and per-location rows carry the long-context subset as
an optional `longContext` field. A streamed turn that grows across the
threshold moves its whole share in the resume correction. estimateCostUsd
takes one token object and bills that subset at the long-context rates and
the rest at base, replacing the per-bucket tiering.

Saved per-file projections carry a projection version; files from an older
version are reparsed on the next scan, so history picks up the split without
touching the signed cache schema.

* fix(usage): keep the projection version out of the integrity hash

Hashing it made older inline reports fail validation and show the
'could not be validated' error; the scanner's version check alone decides reparse.
stablyai#30450)

* feat(sidebar): the owning host raises the workspace unread flag (dark)

* fix(sidebar): phone session open clears and spares the flag; refresh views; blur rescan

* fix(sidebar): throttle failed view retries, resend failed empty views, hold presence one refresh longer

* test(sidebar): switch-on window writes neither the unread flag nor a stamp
stablyai#30456)

* fix(native-chat): clicking Send or Stop keeps focus in the composer

A click focused the Send/Stop button; when it disabled (empty draft after the
turn, or while a Stop is in flight) Chromium dropped focus to <body>. A pointer
press on it no longer moves focus, matching Enter. Adds an e2e regression with
a turn-capable Claude stub. Fixes STA-10897.

* Tighten comments
… PowerShell (stablyai#30424)

* fix(windows): codex and claude get a prompt's double quotes in Windows PowerShell

Windows PowerShell 5.1 (and pwsh before 7.3) does not escape an embedded
`"` when it builds a native program's command line, so every quote in a
launch prompt was dropped, and quotes could split the prompt into extra
arguments. Orca's PowerShell codex and claude wrappers now escape their
arguments at runtime for the PowerShell that is actually running, through
one shared __OrcaNativeArgs function. pwsh 7.3+ already escapes, so it
gets the text untouched.

* fix(windows): escape agent args for .cmd shims under pwsh 7.3+ too

pwsh 7.3+ defaults to the Windows argument mode, which still passes
.cmd/.bat targets the Legacy way, so a codex or claude .cmd shim kept
dropping quotes there. The function now takes the program it escapes
for and names the modes that escape by themselves, and anchors the 5.1
trailing-backslash fix at the true end of the text (`$` also matched
before a final newline). Tests now run the real codex wrapper with a
quoted prompt, cover pwsh's Legacy mode and the newline case, and pin
the claude wrapper's call.

* fix(windows): strip comment lines from the PowerShell bootstrap payload

The shared escaping function pushed the encoded bootstrap to 27,324 of
the 28,000-character budget, leaving almost no room for a startup
command to ride in -EncodedCommand, so short commands fell back to
stdin (the path stablyai#7236 broke). Comment lines document the source but
only spend that budget in the payload; dropping them frees 3,732
encoded characters, more headroom than main had.

* fix(windows): quote agent args with "" where cmd.exe or Windows PowerShell reads them

Escaping embedded quotes as \" let pwsh 7.3+'s .cmd quoting expose a
prompt's `&` to cmd.exe, which then ran the rest as a command. Where
cmd.exe reads the line, or Windows PowerShell counts quotes naively,
the function now quotes the argument itself and writes each quote as
"", as process-host's windows-command-line.ts already does. That also
delivers text 5.1 used to strip, so its special cases go away.

The codex and claude wrappers now come as one fragment with the
function they call, the bootstrap test pins the startup-command budget
instead of restating the strip, and the cwd-restore comment leaves the
payload too.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
…ds, pinned by an acceptance table (stablyai#30451)

* refactor(renderer): one owner adapter for the deprecated-resolver folds, pinned by an acceptance table

resolveOwnerWithLegacyFallback answers as resolveOwner for a named host and for rows that carry a
host stamp, and keeps the deprecated answer (single focused runtime included) for missing and
unstamped rows. A full-owner acceptance table pins all matrix cases and asserts the only changes
from the deprecated resolver are the designed ones: twins and contested SSH hosts become ambiguous,
and a stale hub stamp routes through the server that names the target.

* refactor(renderer): the fold adapter shares resolveOwner's named-host rule

Clean-code pass: one namedHostForOwnerRef for both resolvers, floating counts as host evidence,
and the acceptance test drops non-null assertions.
…ion control holds in both layout builds (stablyai#30432)

The control record named a pane no window or main holds. A layout-owner build keeps resume
records on main's panes, so a record for no pane only exists when loaded from a profile.
Relaunch instead of reload, seed that record into the stopped profile, and read the CLI
tab's PTY from main's inventory, which both builds report.
…t guest (stablyai#30452)

* fix(browser): switch client-hosted profiles with staged guest replacement

Squashed from stablyai#30351 by @oliver-mee onto the current main.

Fixes stablyai#30350

* fix(browser): fence staged profile guests, release switch grants, carry host profile metadata

- A fence during automation retirement now revokes and stops the staged guest.
- A failed or late-settling switch releases its execution-host grant so the page can be recreated.
- Staging cleanup failures before handover stay visible to executor close.
- createPage/restorePage carry host-owned profile metadata to profile-switch-v1 desktops,
  so recovery and restore can rebuild a switched page.

* fix(browser): roll back a switch whose replacement died, and report a lost old guest

* fix(browser): resolve client-hosted pages before the server-local target path when switching profiles

* fix(browser): a staged profile-switch guest owns downloads it starts while loading

* fix(browser): keep the original placement until a timed-out switch settles

* fix(browser): rebind the original guest before releasing a rolled-back staged guest

* test(browser): type the harness guest release mock

* fix(browser): release the replaced generation's download transfers when a switch commits

* fix(browser): admit file transfers from a staged profile-switch guest while its switch is pending

* fix(browser): release only the replaced generation's download transfers after a switch

* refactor(shared): import BrowserNetworkExecutionHost from its own module, no re-export

* fix(browser): adopt a profile switch the client finished after the caller timed out

---------

Co-authored-by: Oliver Mee <102673257+oliver-mee@users.noreply.github.com>
…owner resolver (stablyai#30458)

Open-in owner, execution-host platform facts, doc-preview grants and the activation PTY inventory
read the owner through resolveOwnerWithLegacyFallback. Open in now refuses a twin instead of
launching it as this desktop's checkout; the others keep reading a twin as not server-owned. A row
whose only repo row is a server proxying its SSH target routes through that server. The
creation-action policy stays until browser creation folds with it. Deprecated importers 57 -> 53.
…efore startup continues (dark, layout-owner build only) (stablyai#30429)

* refactor(orcad): move startOrcad's option and handle types into their own module

Move-only: frees room in orcad-entry.ts for the layout-owner load repair.

* fix(orcad): run main's duplicate-terminal pane repair at orcad startup (layout-owner build only)

orcad builds its own runtime and never called the load-time repair, so an
orcad host kept a terminal drawn in two panes.

* test(orcad): type the hoisted test state without an assertion

* fix(orcad): await the startup pane repair so it lands before reconcile and RPC

* fix(startup): desktop awaits the duplicate-terminal repair before the window and RPC start (layout-owner build only)
…e holds (dark, layout-owner build only) (stablyai#30473)

* fix(layout): editor file records keyed by path and owner, so two hosts' files at one path stay apart (layout-owner build only)

Opening a runtime-environment file at a path a local file already held replaced the local record,
the stored-session patch and unsaved drafts matched by path alone, a move took every owner's record
of the path, and closing a bare-path tab removed both owners' records.

* fix(mobile): a windowless host's editor tab under its owned id shows as active, and retiring it clears the active file (layout-owner build only)

* fix(layout): a layout change's tab and pane holds end 15 s after it was sent when main never answers (layout-owner build only)

* fix(layout): main's view reads and prunes keep an active file under its owned id (layout-owner build only)
…-page readers off the deprecated owner resolver (stablyai#30478)

Thirteen read-routing callers (#4, #5, #10-#20 of the round-3 caller list) read the owner through
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. A workspace two hosts publish is no longer
searched on either host. The focus-read ratchet now counts the legacy-fallback adapter, which
reads the focused runtime inside an object literal its discovery skipped; the five F1 entries
that blind spot pruned come back. Deprecated importers 53 -> 40.
…h/connection/ and ssh/targets/ (stablyai#30479)

* refactor(ssh): move the ssh-connection and ssh-target cluster into ssh/connection/ and ssh/targets/

Pure git mv plus import and path-literal rewrites. ssh-connection-utils stays put; the next PR splits it.

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

* test(e2e): keep the release-checkout import at the v1.4.218 path

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
12.10.1 repairs a global-virtual-store slot an interrupted install left
without dependency links (pnpm/pnpm#16642); 12.12 adds addMissingPeerTypes.
The CI store-mode and terminal-perf gates follow the pin they were measured
against, and mobile/ and config/audit-tools record the same pnpm.
… a host's own re-dials (stablyai#30487)

* fix(relay): judge the reserve watch's 503 rule on non-drain 503s, not a host's own re-dials

The watch counted every director 503, and on 2026-10-18 ~98% were host-rate-limited re-dials that
grow with traffic, so it flipped production-gce-c7 back twice with no fault. It now uses the roll
ladder's non-drain measure (request_count 503s less drain-return deferrals and host-rate-limited /
host-in-flight re-dials from the director's runtime metrics) for both the background and the live
samples. A judged minute no runtime-metrics line covers is unverifiable, not zero.

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

* fix(relay): state the reserve watch's metrics coverage honestly and share the metrics read limit

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
@innocarpe innocarpe added the bug Something isn't working label Oct 19, 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.