Commit Graph
13274 Commits
Author SHA1 Message Date
Brennan Benson 5ffc8d7972 Keep Chat UI users with a Terminal default on the terminal, with a chat mode switch in the upgrade tip (#26915)
* Keep Chat UI users who opened new tabs in the terminal on the terminal

Before the chat upgrade, Experimental Chat UI had a separate Default view
choice, and it defaulted to Terminal. The upgrade retired that choice so Chat
UI on now means new agent tabs open as chat, which moved people who had kept
the terminal default into chat with no obvious way back.

On load, a profile saved by a pre-upgrade build with Chat UI on and Default
view not set to Chat now gets Chat UI turned off, and is left out of the
one-time native chat upgrade tip. The old key is stripped on the first save,
so the migration runs once and a later opt-in sticks.

* Update the saved-settings test for Chat UI on with Default view Terminal

* Show the upgrade tip to terminal-default users, with a switch to turn on chat mode

People who had Chat UI on with new tabs opening in the terminal now keep the
terminal on upgrade, but they still need the tip's directions for moving
between chat and CLI. They now get the same upgrade tip, plus a note that new
agent tabs still open in the terminal and a "Turn on chat mode" switch.

The switch shows when Chat UI is off as the tip opens and stays put while it is
flipped. The tip is taller in that case so nothing scrolls.

* Decide the chat mode switch from the saved tip audience, not the live setting

The upgrade tip's 'Turn on chat mode' switch showed whenever Chat UI was off
when the tip opened, so a chat-view member who had turned Chat UI off also got
it. Main now answers with one tip variant (none, standard, keep-terminal) from
the saved audience, and only keep-terminal shows the note and switch.

Also moves the feature tip panel styling into a DialogContent surface variant
and drops the Label restyle, and tests that the switch stays put after flipping.

* Let the upgrade tip decide the chat mode switch from its variant

The modal now passes the saved tip variant to the dialog, which decides the
switch itself, so the dialog tests cover that decision.
2026-10-09 14:21:41 -07:00
Jinwoo Hong d358e5973c feat(relay): one-cell flag write workflow with dry run, compare-and-swap and read-back (#26837)
* feat(relay): one-cell flag write workflow with dry run, compare-and-swap and read-back

Operate Relay Production Cell Flags writes cells/<cellId>.json for one
cell: it checks the origin answers as that cell with a flag channel,
refuses a stale expected generation, writes with ifGenerationMatch,
stamps the actor and run on the object, and waits up to 15 s for the
cell's runtime status to report the new generation applied. The deploy
Workload Identity condition admits the new workflow.

* fix(relay): flag workflow retries a transient 5xx, shows the plan before the generation check, and names the failure

Runtime-status reads go through the shared one-retry admin helper, and a
failed read-back poll no longer ends the wait early. The plan line is
logged before the generation check, a failure is logged as one JSON line
the job summary carries, and the object and summary name the triggering
actor.
2026-10-09 16:59:31 -04:00
Jinwoo Hong 1472b6803f feat(relay): Cloud Storage bucket for the per-cell runtime switch files (targeted apply only) (#26835)
* feat(relay): Cloud Storage bucket for the per-cell runtime switch files

<project_id>-relay-control, versioned, uniform access, public access
prevented. Cells and directors read; only the flag workflow's deploy
identity writes. Apply with a targeted apply, never a root plan.

* fix(relay): scope the switch-file writer to cells/ with a minimal custom role; soft-delete keeps history

The deploy identity is shared, so its grant now reaches only cells/<cellId>.json and carries
only get, create and delete. A 90-day soft-delete window keeps every deleted generation
recoverable, since overwrite needs delete and delete also reaches old versions.

* refactor(relay): drop the speculative directors.json read grant from the control bucket
2026-10-09 16:59:19 -04:00
Jinwoo Hong 37d2154765 feat(relay): read-only seat feed on cells for the directors' shadow directory (#26803)
* feat(relay): read-only seat feed on cells for the directors' shadow directory

Cells keep an in-memory log of host joins, leaves and drain notices and
serve it at GET /v1/admin/cell-seats, authenticated with the directors'
existing rehome identity. Nothing reads it yet.

* test(relay): read the seat feed through its published shape so the tests typecheck

* fix(relay): serve the seat feed's full snapshot from the change log's own seats

counts.controls drops when a socket starts closing, but the leave lands
only at its close event, so a full snapshot built from open sockets could
disagree with a reader that applied every change. The log now keeps its
own seats under the readers' generation rule and reports counts.seats.

* fix(relay): report auth-expiry drain-only and its refresh as seat-feed changes

The heartbeat's auth-expiry drain-only and an auth refresh back to active
changed session state without a feed entry, so a reader's seat state
drifted from the cell's. Both now go through the log; the refresh is a
new 'active' kind.

* refactor(relay): cut unread seat-feed fields; verify a feed poll once

fixLevel, counts.splices, counts.enforcedUnits and counts.hardCap had no
reader, so they leave before they become a cross-version contract. The
admin middleware no longer verifies /v1/admin/cell-seats polls, which the
route already gates on the rehome identity.
2026-10-09 16:58:53 -04:00
Jinwoo Hong dc69efa113 fix(relay): a database fault reaching unhandledRejection no longer kills the process (#26817)
* fix(relay): a database fault reaching unhandledRejection no longer kills the process

A rejection is fenced only when the relay's database layer threw or
rethrew it and it is a pool acquire failure, pg's read timeout, a dropped
connection, or a server error in classes 08/23/40/53/55/57/58. Everything
else stays fatal and is named by an orca_relay_process_fatal line first.
Every pooled client also keeps a permanent error listener. Fix level 2.

* fix(relay): name class 23 in the fence comment; move the fix-level bump to the read-timeout fix

R1 alone does not close the lost-reply stall, so the level rises with R2.
The fix-level comment now describes the fixed alert floor.

* fix(relay): survive and answer 503 for 25P03, the idle-in-transaction timeout a VM stall triggers

A whole-VM database stall of 6-7 s outlasts the 5 s idle-in-transaction
limit for a backend caught idle mid-transaction, and the server ends that
session with FATAL 25P03 on resume. The fence now survives that one class-25
code (the rest of class 25 stays fatal), and routes answer it as transient.

* style(relay): one transient SQLSTATE per line

* fix(relay): fence a socket reset or broken pipe the database layer rethrew

pg 8.22 rejects the active query with the raw socket error ('read
ECONNRESET'), which the message-only check let through to a fatal exit:
the 09-28 crash shape. Match ECONNRESET and EPIPE by code, gated by the
database-layer mark.
2026-10-09 16:58:33 -04:00
Jinwoo Hong 80d7e2dded fix(relay): spread drain placement over a load band instead of one empty cell (#26802)
* fix(relay): spread placement over a load band so a drain does not land on one empty cell

Placement took the single least-loaded cell, so a just-rolled cell took a
whole drain and every follow-up serialized on its relay_cells row. Directors
now pick at random among cells within 5% of capacity of the minimum, and skip
a cell that already took over 40% of its pool's placements in the last 10 s
(floor 4). It only reorders: the least-loaded cell is the fallback, the hard
cap and region preference are unchanged.

* fix(relay): size the load band's intake window for the 1,200 s drain pace

At ~0.5 picks/s per director, a 10 s window with a floor of 4 let the floor
decide and the empty cell took ~65% of a c27-sized drain. A 60 s window and
a floor of 2 keep it at 40% at both the 300 s and 1,200 s paces across five
directors. Adds a once-a-minute orca_relay_placement_load_band line (picks,
budget skips, fallbacks, band size, picks per cell).

* chore(relay): log the load-band summary at info, not warning
2026-10-09 16:58:07 -04:00
62878c3b0c fix(folders): folder workspaces can answer "paired server" (#26958)
* fix(folders): folder workspaces can answer "paired server"

* fix(folders): name a server folder's nested target only when its id is unambiguous

* fix(folders): read the folder group only when the answer needs it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 13:50:36 -07:00
Jinwoo Hong 4a964633f4 fix(automations): stop offering Blank Terminal where it cannot be chosen (#26961)
Blank Terminal is now opt-in on AgentCombobox. Only the new-workspace
composer opts in; automations, source-control agent actions, and chat
continuation offer agents only. The props are a discriminated union so
agent-only callers' onValueChange never receives null.

Fixes STA-9868
2026-10-09 16:48:00 -04:00
OrcaWinandm4air b8a0554357 refactor(runtime): runtime git commands use a provider for every host (#26965)
requireRuntimeGitProvider now returns a provider for the local host too (the
local git provider, built per call from the worktree's WSL options and shared
link paths), so the runtime git commands drop their "SSH provider, else local
free function" branches. Push-target materialization stays host-specific and
moves into one helper. Branch-diff keeps its two arms because the local git
function returns one pinned diff, not the contract's list.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 13:27:24 -07:00
Brennan Benson 2356baa181 Open a chat visual in its own tab instead of the sidebar (#26728)
* Open a chat visual in its own tab instead of the sidebar

* Keep chat visual tabs alive across tab switches and finish editor-tab consolidation

- Render chat visual tabs from a worktree-level retained overlay so switching
  tabs or moving the tab to another split never reloads the page.
- Reopening a visual focuses it in whichever split holds it, takes its newest
  title, and opening from the floating panel no longer takes the main
  window's editor selection (check-details opener uses the same helper).
- Visual tabs return to the reopen-closed stack and show in the tab palette.
- One editor-family list and one virtual-tab predicate: no Copy Path, Copy
  Relative Path, Reveal or git status decoration for check-details and chat
  visual tabs.

* Give chat visuals one open path and re-read them when shown

- Reopen-closed (Cmd/Ctrl+Shift+T) of a chat visual goes through the visual
  opener, so it focuses the visual in whichever split holds it and keeps its
  id when a runtime environment is active.
- A visual tab re-asks its host each time it is shown or reopened; an
  unchanged revision keeps the page as is, a changed one shows the new page,
  and an unavailable tab recovers.
- The tab palette no longer shows or matches a virtual tab's internal id; the
  new-tab search shows the chart icon for visuals.
- Cmd/Ctrl +/- uses app zoom and Alt+Z leaves word wrap alone while a virtual
  tab (CI details, chat visual) is active.
- A tab dragged over a visible visual passes through the frame, using the
  existing drag passthrough.

* Slim the chat visual tab back to the minimum

Remove the retained overlay, the visibility re-read, the zoom and word-wrap
rerouting, palette inclusion, reopen-closed routing and the check-details
opener change. The visual tab renders through the editor area again, stays
off the reopen-closed stack, and keeps: one tab per visual across splits,
no main-window selection change from the floating panel, the newest title on
reopen, and no Reveal or git status letter on virtual tabs.

* Re-read an open chat visual when it is opened again; no watcher for virtual tabs

- "Open in new tab" on a visual whose tab is already on screen in another
  split now re-asks the host, so a rewritten visual shows its new page.
- CI details and chat visual tabs no longer start a workspace file watcher.
- Drop the unreachable chat-visual case from the restore-time transient check,
  use the shared virtual-tab check in the header path, and say why visuals
  stay off the reopen-closed stack.

* Let tab drags pass over chat visuals; hide path copies on virtual tabs

- A chat visual's frame joins the existing tab-drag pointer pass-through, so
  a tab dragged over a visual (in its tab or inline in a chat) can drop or
  split there instead of the frame swallowing the drag.
- The tab menu no longer offers Copy Path / Copy Relative Path for CI details
  and chat visual tabs, whose path is an internal id; no stray separator.

* Reuse the hover icon for editor tab icons, so a chat visual shows its icon in both
2026-10-09 13:27:08 -07:00
Kelvin AmoabaandBrennan Benson 2cb2bef67e feat(native-chat): highlight code blocks and shell commands (#26343)
* feat(native-chat): highlight code blocks and shell commands

Uses Shiki rather than the highlight.js already in the preview: it can
resume mid-block while a reply streams.

* fix(native-chat): leave the tool row command unhighlighted

* refactor(native-chat): highlight code through a shared TextMate interface

Chat code now goes through one renderer highlighting interface built on Shiki
core and the Oniguruma WASM the editor already loads for Nim and Typst, with
VS Code Light+/Dark+. Lines that are too long or too slow leave the rest of
the block plain, failed grammar loads retry from a mounted block, every block
shares one time slice, finished-block caching counts retained text, each theme
keeps its own formatting, Vue template expressions are colored, and tool-row
commands take the grammar of the shell they ran in.

* chore(deps): drop pnpm self-install entries from the lockfile

Keeps the lockfile change to Shiki alone; the earlier commit picked up the
@pnpm/exe entries pnpm 12 writes when it runs in a fresh checkout.

* test(native-chat): give cold grammar loads time in highlighting tests

The first colored block in a test file loads its grammar and the regex
engine, which took over a second on a busy CI runner.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-09 13:08:42 -07:00
OrcaWinandm4air 470f685b80 refactor(providers): local git provider behind the host router (#26955)
* refactor(providers): local git provider behind the host router

Adds createLocalGitProvider, which binds this machine's git free functions to
the IGitProvider contract with the admission tiers and WSL options the local
call sites use today. The router's local git route now carries it as a
per-call factory. No caller changes yet.

* refactor(providers): read shared link paths lazily in the local git provider

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 13:06:55 -07:00
Brennan Benson dfffb302f8 Start orchestration workers as native chats for every agent that has native chat (#26776)
* fix(orchestration): start native-chat workers for every registered native-chat agent

* chore(orchestration): correct the restored worker agent fallback comment
2026-10-09 13:06:40 -07:00
Brennan Benson 8defd660c7 A new Pi chat names the model it will run on its first frame (#26821)
* refactor(native-chat): one rule for what a no-pick session says of its configured default

ACP's rule (only a new session that restored no model pick and was sent none
runs its config's default; an effort it picked says nothing) moves beside the
catalog entry types so other agents' adapters report through the same step.
An empty listing (a signed-out agent) now says nothing instead of "no listed
model", so it can never retire a saved default.

* feat(native-chat): check Pi's project config before naming its default model

Pi reads `.pi/settings.json` in the chat's folder and loads project
extensions from `.pi/extensions` (and the `extensions`/`packages` lists).
A settings file that sets defaultProvider, defaultModel,
defaultThinkingLevel, modelThinkingLevels or enabledModels, loads extension
code, or can't be parsed counts as the project picking the model; extension
code may register providers or set the model itself. Key names come from Pi
1.0.4's own settings manager and docs, read without running Pi.

* feat(native-chat): a new Pi chat names the model and thinking level Pi starts on

Pi's model listing marks no model as the one it runs, so a new Pi chat with no
saved pick showed "Model" and then changed to the model its session reported.
A new Pi session (not one resumed or forked from a saved conversation) that
restored no model pick and was sent none now reports the model and, unless it
picked one, the thinking level it started on, through the same host step as
Grok, OpenCode and OMP. The host saves them as the account's default behind
the project-config check, so the next new Pi chat names them on its first
frame. A signed-out Pi, which lists no model, saves nothing.

* fix(native-chat): a newer, coarser effort menu keeps the effort a chat ran

The saved list takes each model's effort menu from the newer listing. Pi's
`--list-models` menu stops at high, while a chat's own menu adds xhigh or max
for models whose thinking map allows them. After a background re-list, a
learned xhigh was no longer in the menu, so the first frame dropped it. When
the configured effort is only in the other listing's menu, that menu is kept.

* fix(native-chat): Pi teaches only the model and thinking level it started on

A Pi chat re-derived its configured default from Pi's current state on every
options read, so a model an extension switched to mid-chat (extensions can
call setModel) was saved as the account default, and the next new chat named
the wrong model. The choice is now computed once, after start-up restores the
chat's saved picks, and every later read reports that snapshot. A pick made in
the chat no longer matters to it. Tests cover an extension switch, a pick made
in the chat, a forked chat, and a learned xhigh surviving a newer re-list.
2026-10-09 13:06:28 -07:00
Brennan Benson 573ff0bca3 Save a new chat's first message during create (#26332)
* fix(native-chat): the host owns the send queue; the window keeps no saved outbox

The desktop kept each structured chat's unsent messages in localStorage and
sent them in order, so one message whose fate was unknown froze every later
send, Retry dropped it silently, failed sends could not be discarded, and an
offline chat could send hours later. The host already records every message
and owns the queue; the window now only sends.

- One in-memory sender for composer, launch prompts and messages sent from
  outside the chat. One send in flight per chat; a transport failure resends
  the same id for up to 30 s; a refusal that proves nothing was recorded puts
  the text back in the composer with the reason; a send that went out and was
  never answered shows an in-doubt line with Send again and holds nothing up.
- The host's "unknown" rows get the same in-doubt line and Send again, from
  the journal, in every window.
- A message an older build left in localStorage is never sent: the host's
  conversation outline decides what goes back to the composer, and the copy is
  deleted once that is saved.

* fix(native-chat): send through the structured chat RPC wrapper, with its timeouts

The sender called the runtime RPC directly, skipping the per-method timeouts
every other structured chat call gets. Only a remote host's request skips the
compatibility check the sender already ran.

* fix(native-chat): an unconfirmed send goes back to the composer, with no new row line

The common pattern draws nothing extra on a message whose delivery is in
doubt once its turn is over, and puts a failed send's text back in the
composer with the reason. So a send nobody answered in time, or one a host
answers in a way that proves nothing, goes back to the composer worded as
unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt
phase, Send again, and its strings.

A host's made-up row for an id its journal lost now reads as unconfirmed, not
as recorded, so that message comes back instead of vanishing.

* test(native-chat): pin that nothing resends a send given back as unconfirmed

* fix(native-chat): sends survive a tab close, never resend after a Stop, and keep remote images

- A normal tab close lets sends on their way settle; what the host never took
  goes back to the conversation's draft. Only a cancelled launch or a worktree
  purge drops them.
- After any Stop, a send already on its way is never sent again under its id:
  a doubtful answer, or a resend that was due, hands its text back worded as
  unconfirmed.
- A returned image keeps the SSH connection it lives on; the connection never
  goes to the host.
- An older build's saved message the host recorded and then rejected is left
  to the host's own row, never handed back.

* fix(native-chat): a Stop or a tab close never resends a send already out, and a kept card is the card's

A Stop that landed while a same-id resend was being readied (its timer
fired, its request not out yet) let that resend go out after the Stop.
The sender now tracks whether a request is awaiting its answer: a Stop
hands back every send between attempts as unconfirmed, lets one whose
request is out settle from its answer, and never issues a request after
it. A normal tab close withdraws the same way instead of doing nothing,
so nothing more goes out and nothing is dropped.

A send the host rejected but kept as a card (keptAsQueuedMessageId) is
the card's, from its reply or the journal, and never goes back to the
composer.

Adds the freeze tests: a send whose fate is unknown holds later sends
no longer than its deadline, and one the host can neither confirm nor
deny releases the next at once.

* fix(native-chat): read a resend's turned-away call or reused id as unproven

Ports the send-answer proof contract. A call the host turned away before
running it (method_not_found, invalid_argument, unauthorized) proves only
that this request wrote nothing, so it reads as never sent on a first
attempt only; on a resend an earlier attempt may have landed, and it goes
again under the same id. A resent id the host says was already used for
other content (messageIdReused) proves nothing either, like an expired or
conflicting id.

Pins the rest of the contract: any row the host returns is its own, a
thrown error is no answer whatever its code, and an older build's
refused, held or outlived-Stop copy is handed back once and never sent.

* refactor(native-chat): type the structured composer's send with its attachment type

Keeps NativeChatStructuredSession.tsx within the file length limit.

* refactor(native-chat): drop the kept-card guards the sender never needed

A kept send's row is a rejection that was never a Stop's, so the sender
already reads it as recorded, from its reply or the journal. The test
that pins it stays; the two extra checks only covered a kept row that is
also a withdrawal, which the host never writes.

* test(native-chat): route launch tests' sends through the client wrapper the sender calls

The sender sends through callStructuredAgentSession, but these launch
tests replaced that module with a factory that answered nothing (and still
named a probe that no longer exists), so every launch prompt resent until
its 30 s deadline and the tests timed out. Each factory now hands sends to
the runtime RPC mock the tests already answer, and expectations of a local
send no longer ask for the remote-only compatibility option.

* fix(native-chat): hand a message back without importing the composer's attachment hook

The worktree purge reaches the launch prompt, which hands text back, and
the attachment hook's imports reach the store. A test that builds the
real store behind a mocked one then waited on itself and hung. Handing
back now writes images to the draft store directly, as the hook's helper
did, and a test pins that each returned image keeps its SSH connection.

* fix(native-chat): one send per chat, with no line of sends behind it

A chat with a send out took further messages into an in-memory line and
sent them one by one. A message typed behind one in doubt then hit its
own 30 s deadline and came back as not sent without ever going out.

Now, as the common pattern does, a chat takes one send at a time: while
it is out, Send is disabled and Enter leaves the text in the box. The
sender refuses a second send instead of lining it up, so the 30 s
deadline always runs from the send itself. A Stop or a tab close stops
the one send: settled from its answer if its request is out, handed back
as unconfirmed between attempts, or silently if it never went out.

Notes sent from outside the chat while its send is out stay with their
sender (not ready); a launch prompt that meets the person's own first
message waits in the composer instead of being lost.

* fix(native-chat): give a Stop-withdrawn note back to its chat once its notes were cleared

Notes sent from outside a chat clear once their message is recorded. A
message the host recorded as pending and a Stop then withdrew came back
only to its sender, which had already let go of it, so the text was
lost. The chat's draft now takes it, as an earlier build's outbox did.

* test(native-chat): type the composer-actions probe without a cast

* chore(native-chat): drop outbox wording left in comments and an empty locale group

* fix(native-chat): pace failed checks, keep the host's reason, and hold the chat for its launch prompt

- A send whose checks failed before its request went out (an unreachable
  or incompatible host, an unreadable history) was tried again at once,
  about a thousand times a second for 30 s. Attempts are now paced by
  the attempts made, whether or not their request went out.
- A host that refuses every resend by throwing (native chat turned off,
  a journal that won't open) gave back only "couldn't confirm". The
  host's reason now comes first, still without claiming not sent.
- A launch's prompt now holds the chat's one send from the click, drawn
  as sending, so a message typed while the chat starts can't overtake
  it; it goes out once the chat exists, and a cancelled launch frees it.
- Notes whose send nobody could confirm say so instead of "did not
  accept", and notes launched into a new chat let go of their text once
  that chat's composer holds it.
- An open chat keeps drawing a recorded send until its row arrives, so a
  reply that beats the history no longer makes the message flicker out.

* fix(native-chat): hold a chat's sends until it has started, and send a failed chat's message with its restart

A message sent to a chat still starting went out at once to a host that
had no record of the chat yet, read for a fence it could not get, and
came back as not sent. A message sent to a chat whose start failed
restarted it but no longer went with the restart.

While a chat starts, Send stays off and Enter leaves the text in the box,
as with a send already out. A text message sent to a chat whose start
failed restarts it and goes as the restart's first message, through the
same staged-prompt path a launch prompt takes: it holds the chat's one
send slot, drawn as sending, until the chat exists, and comes back to
the composer if the restart fails again. Notes wait while a chat starts
and ride a failed chat's restart, keeping their text until it is sent.

* chore(native-chat): test the queue request, rejection words and card hand-offs; drop an unused clear

- Pin which sends ask the host to queue, the moved rejection wording,
  and that a queue send whose card was handed off and then refused or
  withdrawn, or whose replay names a withdrawn card, is never handed
  back.
- The send-at-most-once gate now says what the desktop promises after a
  reload: it never sends the id again, and hands the text back.
- The legacy read names when it goes, and drops the notice clear nothing
  called.

* fix(native-chat): keep a sent launch prompt's entry, and give back at once what can't go out

- Cleaning up a launch prompt released its send slot even after the
  prompt had gone out, which deleted the entry the sender keeps once the
  host records it. A launch prompt recorded as pending and then
  withdrawn by a Stop was lost from both the chat and the box, and an
  open chat dropped the new chat's first message before its row arrived.
  The slot now gives back only a reservation that was never sent.
- A send stopped before its request went out by something trying again
  won't clear (this client and the server can't talk, or the host
  refused the history read) kept Send off for 30 s and then said Orca
  couldn't reach the agent. It now comes back at once with its own
  cause: not sent, since nothing went out, or unconfirmed if an earlier
  attempt did. Transport errors keep the paced retries.

* fix(native-chat): notes keep their own text through a new agent's launch

Notes sent to a New agent whose start failed came back to the notes and
also sat in the new chat's composer, so sending both delivered the text
twice. The notes keep their text until it goes out (they hold it from
the click), so the launch now says so and no composer gets a copy, on a
failed start or a refused prompt alike. The tests that asserted the copy
in the composer pinned the old double ownership and now assert the
notes are its only owner.

Notes that rode a failed chat's restart and ended unconfirmed now say
Orca couldn't confirm them, as notes sent directly do, instead of that
the agent did not accept them.

* chore(native-chat): the send-once gate says what the desktop keeps across a reload

The desktop keeps a send's id in memory only: a send still unsettled at a
reload or crash is not resent and not handed back. The coverage notes no
longer credit the renderer tests with durable identity across a remount.

* fix(native-chat): say once why notes sent to a new agent did not go

Since notes keep their own text through a new agent's launch, a prompt
the host refused, nobody could confirm, or that found the chat's send
taken came back to the notes with nothing said anywhere: the chat shows
no notice for text its caller keeps. The notes menu now reports it once,
with the toast a send to an existing chat already uses: not accepted,
couldn't confirm, or not ready. A start that failed still says so in the
new chat instead.

* fix(native-chat): retry a history refusal the host says clears, and name it at the deadline

A send stopped before its request went out came back at once for any
refusal of the history read, including ones the host names as clearing
(its journal briefly unavailable, a chat detached while the host quits),
so the automatic retry was lost. Only a version mismatch and refusals
that won't clear come back at once now; the rest go again on the paced
schedule, and if the deadline still finds nothing sent, the words are
the host's refusal rather than Orca couldn't reach the agent.

* feat(native-chat): a send makes one request, and nothing ever sends it again

The desktop resent a message under its own id for up to 30 s when the
answer was lost. A send now makes exactly one request, as the common
pattern's clients do:
- An answer that is lost, dropped or proves nothing hands the text back
  at once with "couldn't confirm… check the chat".
- A failure before the request goes out (the environment check, the
  history read for the fence, a refused connection) means nothing went
  out: the text comes back at once with its own reason, or as not sent.
- The 30 s cap stays on the one request, so a host that never answers
  can't hold Send.

Nothing ever resends, so nothing can go out after a Stop: a Stop only
takes back a send still in its pre-send checks. The resend state goes
with it (tries, generation, awaiting, stopped, the resend timer, the
send-answers-proof probe, and the first-attempt/resend split in the
evidence), and the send-once gate says the desktop never resends.

* fix(native-chat): notes keep the chat's line, and a send held behind a rewind says it was not sent

- Only a send whose text belongs to the chat's composer clears the chat's line. Notes sent from
  outside the chat no longer wipe a "couldn't confirm... Check the chat" line that explains text
  already back in the box.
- A send the host turns away behind a rewind it could not confirm went back as "not sent" but said
  "couldn't confirm what happened. Check the chat". It now gives the rewind's reason and says the
  message was not sent.
- Reliability gate names the single-request test and records a fresh evidence run; the sender
  test drops its leftover resend mocks and a duplicate Stop test.

* fix(native-chat): a send ends even when putting its text back fails

If writing the returned text into the chat's draft threw, the send never settled: it stayed
"sending", the chat refused every later send until a reload. The send now always ends after a
hand-back, the failure is logged, and the chat's line adds "Couldn't save your message."

* fix(native-chat): a message typed during /clear stays in the box and follows the chat

A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A
message sent then went out, came back refused into the old conversation's draft with its line, and
vanished when the chat moved on: the box and the line now belong to the new conversation.

- A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and
  the text stays in the box, as for a send that is out. Notes sent from outside get "busy".
- Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and
  what is left of its draft moves into the new conversation's draft, after anything there: when the
  composer's /clear settles, and again when the view moves.

* fix(native-chat): no "Send message?" or queue clear while the chat's send is out

While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still
opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now
does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out
after the question opened.

* refactor(native-chat): take the /clear draft carry out of this change

Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too
(text left in the box during a /clear stays with the old conversation), so it goes in its own change.
Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from
takes no send until the view leaves it.

* fix(native-chat): a /clear's hold on sends always ends

- A view that unmounted while a /clear that moves the chat was out left the old conversation
  holding its sends until a reload: the late reply kept the hold for a view that was gone. The
  reply now releases it.
- The local call for a conversation command has no deadline of its own, so a /clear that never
  answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one
  the remote call already uses), whichever comes first.

* test(native-chat): opening a chat an older build left stuck; hand back its copy in send order

Pins, through the real chat hook, sends, legacy recovery and draft store,
what opening such a chat does: the queued messages behind a message the
host recorded in doubt come back to the composer once, in order, with the
"not sent" or "couldn't confirm" wording; the chat is free to send under
its read's fence; nothing happens while the chat has no fence here.

The legacy reader now hands entries back in the order they were sent
(queuedAt), as the older build's reader did, instead of array order.

* fix(native-chat): a send this window turned away for a re-paired server comes back as not sent

A managed server's update rotates its pairing, and this window's main
process then answers the next call itself, before forwarding anything,
with runtime_environment_changed. The send read that thrown answer as
proving nothing, so the person was told Orca couldn't confirm a message
that never left. It is now handed back as not sent, with the reason.
Every other thrown answer still reads as unconfirmed once the request
may have gone out.

* Save a new chat's first message during create

* Align opening-message types with the user-message wire

* Keep chat publication and cleanup outside store initialization

* Complete the create refusal fixture ledger

* test(native-chat): prove a created first message waits for the agent's start and is handed over once

* refactor(native-chat): keep the host and session hook under the line limit after the main sync

* test(native-chat): a chat created with its first message and no pick still teaches its agent's default

After the main sync, a created chat's agent starts from the delivery loop rather than the create
call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter)
each still report the model their own config resolved to once the chat reads its options.
2026-10-09 13:05:35 -07:00
Neil 35b145dbc8 [CI] Run Activity tests together and replace the real idle wait (#26949) 2026-10-09 12:59:42 -07:00
OrcaWinandm4air 6a929b2023 fix(hosts): host registry stops minting client SSH hosts from a server's nested target (#26952)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 12:59:19 -07:00
Jinwoo Hong 8b2a5e45f1 Re-land Claude per-account folders (#24434, #26405) with the upgrade and proxy fixes (#26801)
* Revert "Revert Claude per-account folders (#24434, #26405) before release (#26638)"

This reverts commit 9ca9b4e36b.

* Move the terminal daemon protocol to 44 for the re-landed claude function

v43 daemons (the revert) lack the claude account function, so new terminals must not reuse them. v42 and v43 stay attachable.

* Move the local build compatibility contract to daemon protocol 44

* Point the old-terminal test at daemon protocol 44

* Claude accounts re-land: never sign anyone out, keep proxies working, accounts mirror ~/.claude (#26769)

* Run a Claude account on System default until it has its own login

An account saved before per-account folders has no login in its folder. While System default (~/.claude, or the user's own CLAUDE_CONFIG_DIR) is signed in to the same email, the router sends every launch there and writes that choice into the pointer file, so nobody is logged out by the update. Once the account signs in to its own folder, the next launch uses it. The WSL router follows the same rule with the guest's ~/.claude.

* Refresh each Claude account folder from ~/.claude before every launch

~/.claude (or the user's own CLAUDE_CONFIG_DIR) is now the master copy. Each account folder gets its whole settings.json, including the proxy address and its key, and every .claude.json key except the login, Claude's account caches and install ids, so first-run questions are not asked again. Every other top-level entry is linked (folders) or copied (files), except Claude's per-folder daemon, jobs, live sessions, login files and throwaways. sessions is no longer shared, and an earlier link is undone.

The refresh runs before every Claude Orca starts, for each new terminal and on account switch. Orca never writes back to ~/.claude. The ledger that tracked selective sharing is gone.

* Route chat auth and unselected-account usage through the Claude router

Chats decided whether to drop inherited Anthropic auth from the selection alone, so an account running on System default lost the user's own key. They now ask the router, as terminals, AI commit messages, automations and usage already do. Usage for an unselected account is read where its launches would run. A test reads every entry point through the fallback and fails if a launch path outside the router reads the Claude selection or builds an account folder. The router's two launch errors move to their own file to keep it under the line limit.

* Show Claude account sign-in prompts only when an account cannot run

With the fallback, an account System default is signed in to already works, so:
- The one-time "Finish setting up your Claude accounts" toast is gone.
- Accounts System default covers no longer need a sign-in: no Sign in in the status bar menu or Settings, and they can be selected.
- The old-terminal banner shows only when claude in that terminal would run a different account than the selected one, and asks again after a switch. Its wording is unchanged.

The retired toast's saved flag stays in the paired-client schema so an older client's write is still accepted. The email comparison moves beside the login reader to keep the router under the line limit.

* Keep the user's shell Anthropic auth on Claude account launches

A signed-in account's launches used to drop ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN from the inherited environment while ANTHROPIC_BASE_URL passed through, so a proxy set in the shell got the subscription login and answered 401. Host launches on an account now keep the shell's auth with its address, as System default does. Chats still name the account kind in a failed sign-in, now from the router rather than from whether auth was dropped.

* Share from ~/.claude only what Claude itself would seed into a new folder

The deny list missed much of Claude's runtime state, so tokens, locks and live files such as .session_ingress_token, server-sessions.json and remote-control were copied or linked over each account's own every launch. The list now mirrors the one Claude uses when it seeds a fresh config folder from ~/.claude: its runtime entries, every hidden entry, daemon files and agent memory. teams, ide, downloads and scratch are on it, so they stay per folder.

* Merge trusted folders and MCP servers into an account instead of replacing them

Each refresh replaced the account's projects and mcpServers with ~/.claude's, so folder trust and allowed tools recorded only in the account were lost, and a refresh landing between Orca's folder-trust write and Claude's start brought the trust dialog back. Both are now merged per folder path or server name: ~/.claude wins for entries it has, the account keeps the rest, and a folder trusted in the account stays trusted. Both writers take Claude's lock on the file, so the order no longer matters.

* Require the same organization before System default stands in for an account

The fallback compared emails only, so one email saved for two organizations made both accounts run on whichever organization System default was signed in to. When the saved account and System default's login both name an organization, it must now match too, on the host and in WSL, and the old-terminal banner uses the same comparison. The saved-account lookup the three checks repeated is now one helper.

* Treat v42 terminals as having the claude account function

v42 daemons shipped the same claude function and pointer path as v44, so claude in those terminals already follows the selected account. The old-terminal banner now shows only for daemons before v42 and for v43, the revert, which dropped the function.

* Fail the routing test when a new module resolves the claude binary

The census only caught code that read the Claude selection, not a new launcher that runs claude without asking the router. A second check now lists every module that resolves the claude binary or loads the Agent SDK, each with why it is routed, and fails on any new one, or on a listed one that no longer matches.

* Skip the account refresh on launches that run on System default

A launch that falls back to System default never uses the account folder, so starting a setup for it on every launch was wasted work. Account switches still set the folder up for a later sign-in.

* Give a terminal opened before the login shell's env only the account pointer

At startup, a pane could be routed before the login shell reported the user's own CLAUDE_CONFIG_DIR, so the fallback compared against ~/.claude.json instead. Until that env arrives a pane gets only the pointer, and the pointer is rewritten once it does, so claude typed there reads the right folder. The recent claude --version answer moves beside the version probe to keep the router under the line limit.

* Drop the chat-only Anthropic auth stripping and override refusal

Chats no longer drop the shell's Anthropic auth on any account, so the chat launch's override refusal and the policy's stripAuthEnv could only be reached from test fixtures. The structured auth policy is now just which login a failed sign-in names. Terminals keep their refusal, which still guards WSL launches; the failure reason stays in the shared vocabulary for older peers.

* Publish a new account state file whole

The first .claude.json write went straight to the target, so a crash mid-write could leave Claude a truncated state file. It is now written to a staged file and linked into place without replacing one Claude created meanwhile.

* Read an unselected WSL account's usage where the WSL router would run it

Usage for an unselected WSL account read its saved folder directly, so an account the guest's ~/.claude covers showed no usage, and the read bypassed the router. It now asks the WSL router, after the same check that the distro is already running.

* Keep the shell's Anthropic auth when the claude function runs an account

The claude shell function (POSIX, fish and PowerShell) unset ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK and auth-like ANTHROPIC_CUSTOM_HEADERS before running Claude in an account folder, so a proxy set in the shell lost its key while keeping its address. It now only sets CLAUDE_CONFIG_DIR and Orca's marker, as on System default. Protocol 44 is unreleased, so no new bump; the shell-wrapper snapshots are regenerated.

* Never drop the user's Anthropic auth from a Claude launch

Orca's launches stripped ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK and auth-like ANTHROPIC_CUSTOM_HEADERS for WSL terminals, terminal splits and through the preparation's stripAuthEnv flag, and refused a launch whose agent env set them. If the user's shell overrides the login, that is their setup: every launch now keeps it, on accounts, System default and WSL alike. The flag, the strip paths and the override refusal are gone; the refusal's reason stays in the shared vocabulary so older peers still decode it.

Orca's own process env and the claude --version probe still drop these variables: they are Orca's, not the user's launch.

* Leave out of an account's state everything Claude resets when it signs out

The .claude.json copy left out the login and keys matching cache patterns, but still carried account state such as additionalModelOptionsAnsweredAt, artifactRosterDenied, lastSeenOrgDefaultUpdatedAt and the subscription notices. The copy now leaves out the exact keys Claude resets on logout, beside the install ids, Console API key and first-token date; the cache patterns stay for account caches logout does not reset. Onboarding is still copied.

* Judge an old terminal's Claude banner by the pane's own runtime

The banner compared every old pane with the host selection and showed whenever only a WSL account was selected, so old host panes showed it with nothing to say and old WSL panes were judged by the host's account. The pane now names its runtime: a host pane is compared with the host selection, a WSL pane with its distro's selection through the WSL router, and a pane whose runtime is unknown, or with no account selected for it, stays hidden.

* Leave an account's settings file alone when only Orca's hooks differ from the default home's

* Reread a WSL account's Claude state only when the file changes

* Move tests merged from main onto the account-only Claude auth policy
2026-10-09 15:57:47 -04:00
OrcaWinandm4air ce9eb662bb refactor(renderer-ui-store): clean up Phase 3-3.8 code (#26903)
Share one bounded generation map across SSH, runtime SSH, slug resolution
and session-tab tracking; reuse the SSH target generation helpers; replace
the removed-flag chain in SSH target cleanup with a patch accumulator; derive
purge recency targets from the rows the drop already removed; share the
add-repo stale guard and runtime target; share the host-setup availability
rule; drop the dead sync session persister; reuse host-id constructors in the
file explorer; trim remote browser stream comments.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 12:38:29 -07:00
Jinwoo Hong 65b2fcf89a Start AI commit messages from the same shell environment chats use (#26808)
* Start AI commit messages from the login-shell environment chats use

A Dock-launched Orca only takes PATH from the user's shell, so variables
exported in the shell rc (proxy base URL, auth token) never reached the
commit-message agent even though chats and terminals saw them. Local host
generation now starts from the same cached login-shell base env as chats,
with account overrides applied on top; WSL and SSH paths are unchanged.

* Prefer the login shell's config directory over Orca's inherited one for commit messages
2026-10-09 15:35:36 -04:00
OrcaWinandm4air 6dab0cb7fe refactor(ipc-persistence-shared): clean up Phase 3-3.8 code (#26902)
- Install store domain contexts with one generic symbol copy
- Share one import-source classifier and one copy-name deconflicter across local, SSH and runtime-upload imports
- Collapse kill.ts synthetic-exit and stop-failure repetition into helpers
- Use Promise.allSettled in SSH transport teardown
- Use the shared errorMessage() instead of inline copies
- Fold isLiveSshPtyLease into ssh-types and drop the duplicate predicate

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 12:33:19 -07:00
Jinwoo Hong a8ce574c77 Add inert layout commands and runtime transitions (#26749)
* feat(layout): add the inert workspace layout model, Loader and Serializer

One model of a workspace's tab layout (all tab kinds, groups, split trees,
pane bindings, titles/colors/pin, sleeping records) in src/shared/workspace-layout,
with a Loader from today's stored session partition and a Serializer back to it.
The structural rules check and the pure pane/group tree helpers move into the
same folder so the module can run them; their callers import the new paths.
Nothing in the app calls the new module yet.

* feat(layout): add inert layout commands and runtime transitions

applyLayoutCommand applies every layout command a client can send (create,
split, close, move, reorder, rename, recolor, pin, open editor/browser/agent
tabs, start/restart, sleep/wake) to the layout model, and
applyLayoutTransition applies the facts the runtime sees (process started or
exited, SSH lease ended, orphan adopted, owner removed or renamed, agent launch
verdict, sleeping records). Pure tab-order, group-move and pane-split helpers
move into the shared module so the renderer, today's headless host and the new
commands share one implementation. Nothing in the app calls the new commands.

* fix(layout): avoid toSorted in shared code and drop an unused helper

The mobile bundle includes src/shared and Hermes lacks Array#toSorted.

* refactor(layout): name tree comparisons for what they ignore

* fix(layout): one stored home per layout fact; Loader reports every rule it applies

The row's terminal id, preview flag, worktree id and execution host are now
derived on save instead of kept a second time. Groups never stay empty. The
Loader reports row/tab-bar disagreements, re-minted ids, worktree id and host
disagreements, keeps a row stored in two workspaces where the tab bar names it,
and gives a pane id repeated in a second tab a new unbound pane. The rules check
runs on what the Serializer writes. Fixtures become test fixtures, and a test
lists every model field with the one fact it holds.

* fix(layout): commands keep one home per fact and refuse instead of throwing

createGroup is gone (a group only exists while it holds a tab); a lone tab
split beside another group moves its group leaf and keeps the id; moveTab
refuses when the move cannot be built; renaming onto an existing workspace
is refused; worktree and execution host ids come from the workspace.

* fix(layout): derive each tab's host from its owner and keep one preview fact per editor tab

An editor tab names its file's owner (an SSH target or paired runtime), every
other tab its partition. Where the stored tab and file preview flags disagree
the tab loads permanent, so a draft can never be replaced. A row terminal that
differs from the one derived from its focused pane is now reported.

* fix(layout): one binding kind, one title, one host rule; records follow a reassigned pane

A legacy row with no pane layout now loads as a one-pane tab bound to its
terminal (reusing the leaf its records name), so the null-panes branches and
legacyPtyId are gone. A pane given a new id because two tabs shared it takes
its sleeping and incarnation records, scrollback and focus with it. A terminal's
live title is one fact (the tab-bar label wins on load). Every tab-bar entry
names its execution host by one rule. The fact-location test is now keyed by
on-disk field and checked by mutating every source.

* fix(layout): commands drop the legacy binding branches; rename carries closed-tab records

Every terminal tab has panes now, so commands lose their null-panes branches
and a close always releases the tab's terminals. The pane-record rekey is
shared with the Loader. Renaming a workspace renames its closed-tab records'
worktree id, so a later removal or partition move still selects them.

* fix(layout): one change report by construction; records without copies

The Loader now picks values by fixed precedence only and reports one
field-by-field diff of the stored session against what the next save
writes, over the disk-field tables (now in workspace-layout-disk-fields.ts,
with session fields derived from SESSION_FIELD_OWNERS). Sleeping and
closed-tab records live in their workspace without the pane key, tab and
worktree id their key and workspace already give; the Serializer fills them.
Tab-bar entries are written in the one tab order.

* fix(layout): drop transitions with no producer yet; group moves hold no selection

orphanAdopted, legacyWorkerRecovered, clientHostedPageAnnounced,
agentLaunchVerdict, sleepingRecordsCaptured and moveWorkspaceToPartition
ship with the PRs that produce them. The shared group moves take and return
LayoutGroup only; the headless host keeps its selected tab beside them.
Commands keep sleeping and closed-tab records in their workspace, so rename
and removal need no record patching.

* fix(layout): name the change report's key-list parameter's type

* refactor(layout): key pane data by leaf, check the model itself, one legacy record

- Each pane's data (terminal, incarnation, title, sleeping agent) is one leaf record keyed by
  leaf id, and scrollback beside it is keyed the same way. Pane keys are derived only when
  saving. The Loader gives a repeated leaf id a new id before attaching records, so
  rekeyPaneRecords is gone; records naming no pane are carried as stored.
- The rules check runs on the model directly (ungrouped tab, empty group, group tree that
  disagrees with the groups, pane data without a pane); the disk-format rules stay separate.
- The row-ownership marker and topology revision move into one legacy record outside
  WorkspaceLayout, updated by updateLegacyPersistence after each apply.
- Fixtures include a terminal tab whose tab-bar id and terminal id differ.

* refactor(layout): commands on leaf-keyed pane data and the one legacy update

- Commands and transitions read and write each pane's one leaf record; drag-out moves only the
  tree, so the pane's data and scrollback follow by structure. No command advances the topology
  revision itself: applyLayoutCommand and applyLayoutTransition run updateLegacyPersistence after
  each apply, which also marks a workspace as owning its rows once it holds a terminal.
- Preview replacement closes the preview through closeTabs, so an unsaved draft refuses it as
  any close does.
- Tests: create then close then save with and without a reload, commands on a tab whose
  tab-bar and terminal ids differ, drag-out scrollback, the preview refusal.

* fix(layout): join the group ids the tree-mismatch detail names

* fix(layout): leave the stored revision map as is when no repo advances
2026-10-09 15:22:39 -04:00
OrcaWinandm4air cce31b4b4d refactor(renderer-terminal-runtime): clean up Phase 3-3.8 code (#26900)
- One helper plans and runs a paste into a terminal pane (five copies before)
- Reuse getShortcutPlatform instead of three private copies
- runtime-git-client becomes plain re-exports
- TerminalErrorToast builds each token's test/replace regex from one helper
- Reuse PreconnectInputWriter for the recovery input hold
- Name the session-tabs snapshot callback type once
- Narrow isTerminalDriverState with 'in' instead of a cast
- Inline the single-use terminal-pane-client-host wrapper

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 12:09:56 -07:00
Willian FelipeandNeil 150bf49cb2 feat(kiro): report Kiro agent status and list Kiro sessions in history (#26695)
* feat(kiro): report Kiro agent status through managed hooks

Kiro keeps hooks inside each agent config, so Orca installs one managed
hook per event into $KIRO_HOME/agents/*.json (locally and on SSH hosts
under the remote KIRO_HOME) and normalizes the payloads on /hook/kiro.
Pets, sidebar, notifications and mobile now see Kiro working and done.
Kiro resume is gated on a new agent-session.kiro-resume.v1 capability.

* feat(kiro): list Kiro sessions in Agent session history

Reads $KIRO_HOME/sessions/cli/<id>.json metadata plus the streamed
<id>.jsonl transcript locally, under WSL and over SSH, skips subagent
child sessions, and resumes with kiro-cli chat --tui --resume-id <id>.

* fix(kiro): report a remote agent deleted mid-install as unreadable

* fix(kiro): resume history rows by the validated session file name

* fix(kiro): preserve hook configs and restore sessions correctly

* test(kiro): use typed filesystem callbacks

* test(kiro): preserve older-peer search expectations

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-09 12:08:18 -07:00
Jinwoo Hong 9021133929 Add the inert workspace layout model, Loader and Serializer (#26748)
* feat(layout): add the inert workspace layout model, Loader and Serializer

One model of a workspace's tab layout (all tab kinds, groups, split trees,
pane bindings, titles/colors/pin, sleeping records) in src/shared/workspace-layout,
with a Loader from today's stored session partition and a Serializer back to it.
The structural rules check and the pure pane/group tree helpers move into the
same folder so the module can run them; their callers import the new paths.
Nothing in the app calls the new module yet.

* fix(layout): avoid toSorted in shared code and drop an unused helper

The mobile bundle includes src/shared and Hermes lacks Array#toSorted.

* fix(layout): one stored home per layout fact; Loader reports every rule it applies

The row's terminal id, preview flag, worktree id and execution host are now
derived on save instead of kept a second time. Groups never stay empty. The
Loader reports row/tab-bar disagreements, re-minted ids, worktree id and host
disagreements, keeps a row stored in two workspaces where the tab bar names it,
and gives a pane id repeated in a second tab a new unbound pane. The rules check
runs on what the Serializer writes. Fixtures become test fixtures, and a test
lists every model field with the one fact it holds.

* fix(layout): derive each tab's host from its owner and keep one preview fact per editor tab

An editor tab names its file's owner (an SSH target or paired runtime), every
other tab its partition. Where the stored tab and file preview flags disagree
the tab loads permanent, so a draft can never be replaced. A row terminal that
differs from the one derived from its focused pane is now reported.

* fix(layout): one binding kind, one title, one host rule; records follow a reassigned pane

A legacy row with no pane layout now loads as a one-pane tab bound to its
terminal (reusing the leaf its records name), so the null-panes branches and
legacyPtyId are gone. A pane given a new id because two tabs shared it takes
its sleeping and incarnation records, scrollback and focus with it. A terminal's
live title is one fact (the tab-bar label wins on load). Every tab-bar entry
names its execution host by one rule. The fact-location test is now keyed by
on-disk field and checked by mutating every source.

* fix(layout): one change report by construction; records without copies

The Loader now picks values by fixed precedence only and reports one
field-by-field diff of the stored session against what the next save
writes, over the disk-field tables (now in workspace-layout-disk-fields.ts,
with session fields derived from SESSION_FIELD_OWNERS). Sleeping and
closed-tab records live in their workspace without the pane key, tab and
worktree id their key and workspace already give; the Serializer fills them.
Tab-bar entries are written in the one tab order.

* fix(layout): name the change report's key-list parameter's type

* refactor(layout): key pane data by leaf, check the model itself, one legacy record

- Each pane's data (terminal, incarnation, title, sleeping agent) is one leaf record keyed by
  leaf id, and scrollback beside it is keyed the same way. Pane keys are derived only when
  saving. The Loader gives a repeated leaf id a new id before attaching records, so
  rekeyPaneRecords is gone; records naming no pane are carried as stored.
- The rules check runs on the model directly (ungrouped tab, empty group, group tree that
  disagrees with the groups, pane data without a pane); the disk-format rules stay separate.
- The row-ownership marker and topology revision move into one legacy record outside
  WorkspaceLayout, updated by updateLegacyPersistence after each apply.
- Fixtures include a terminal tab whose tab-bar id and terminal id differ.

* fix(layout): join the group ids the tree-mismatch detail names

* fix(layout): leave the stored revision map as is when no repo advances
2026-10-09 15:05:22 -04:00
Jinwoo Hong e5412cccec test(e2e): headless layout oracle covers tab moves between groups (#26857)
Two headless scenarios drive session.tabs.move (split into a new group,
move into an existing group, reorder, and a move that empties a group) on
orcad and Electron serve, and check group order, the selected tab per group,
and a warm restart. The temp repo is resolved to its real path so macOS runs
list its worktree without a TMPDIR workaround. Two new main findings are
listed as known: headless saves groups without a tab bar, and move-to-group
leaves the target group's previous selection.
2026-10-09 15:05:17 -04:00
Jinwoo Hong 6013d093e2 fix(mobile): one host sidebar on iPad, with the page owning the host area (#26761)
* feat(mobile): let the page own the wide host area, one sidebar only

On wide layouts a detail route's page session ran app/h/_layout.tsx inside
the detail pane, judged itself wide, and drew a second host sidebar beside
the native one.

- The page draws the host sidebar only when init carries the new optional
  `ownsHostArea` fact, never from its own width.
- The page manifest's `/h/[hostId]` route declares `canOwnHostArea`.
- On wide layouts the native host route mounts one host-area page session.
  A declaring bundle serves it with every served route's grants, so hops
  stay in the page, and the native layout drops its sidebar. Without the
  declaration, or without a bundle, the session answers native-route and
  every wide detail route stays native.
- The owner is recorded per host so the layout and switches agree and a
  relaunch does not flash.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): decide the wide host area per session, without a stored owner

Round 1 kept a per-host owner record that the native layout trusted. When
the record said the page owned the area, a detail route opened natively
(deep link, notification, handoff, narrow-to-wide with a detail pushed)
showed no sidebar at all.

- Drop the record. A wide session decides from the manifest it already
  read: it is served only when the host route declares canOwnHostArea,
  and it rebuilds when the layout class changes.
- The native layout hides its sidebar only on the host route, while that
  host's page session is serving, as the shell screen reports through a
  layout context. Detail routes keep the native sidebar, and a page there
  draws none, so there is exactly one.
- Phones read and render nothing new.
- The host-area page can read and write every workspace-scoped key of its
  host (chat tabs, live-input handles), as a per-workspace session does.

Claude-Session: lane-ipad-page-owns-host

* test(mobile): type the layout-class doubles rather than assert them

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): keep detail pages through a layout flip, guard capped keys

- A wide/narrow flip rebuilds a session only on the host route or under a
  bundle without canOwnHostArea, where the answer changes. A detail page
  under a declaring bundle keeps its document, scroll and draft.
- In the host-area page, a workspace key the 32-entry init cap leaves out
  is named in storageOversize, so the page cannot replace the device's
  chat-tab list from a default. The init frame bounds that list to the
  page's schema; the host still refuses every key in the full list.
- A leaving host-area session clears only its own report.

Claude-Session: lane-ipad-page-owns-host

* refactor(mobile): host-area ownership from the reducer, storage read on demand

- The serving report is a focus-scoped boolean, so a host pushed over
  another no longer leaves the first one's area unclaimed after the pop.
- The reducer takes `layout-changed {wide}`: it keeps the session when the
  route view is unchanged and restarts it otherwise; `routeViewOf` returns
  `ownsHostArea`.
- One web override, `usePageOwnsHostArea`; the native rule is inline in the
  host layout.
- A host-area page reads a host workspace key from the shell on demand
  (`native.storage.read`, granted to that session only) instead of init
  enumerating the store.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): host-area page reads back its own workspace-key writes

A page read right after a page write of the same key is answered from the
page's own value, not from a shell that may not have the notify yet. A
refused read crosses as an error, never as an unset key.

Claude-Session: lane-ipad-page-owns-host

* refactor(mobile): read-your-writes from the shell's mirror, final review fixes

- The shell answers a workspace-key read from the mirror, which every
  shell-side write updates synchronously (removals held as known-absent),
  and reads the store only on a miss. The page shim's written-keys set
  goes.
- `onLayoutChanged` compares the route view field by field; the session
  is created with its layout and the layout effect fires on `wide` alone.
- The device verb dispatcher's type excludes the storage read.
- The sidebar matrix mounts the real shell screen to decide the report.

Claude-Session: lane-ipad-page-owns-host

* feat(mobile): host-area page takes the whole screen from the download on

The native sidebar steps aside once the manifest says the page owns the
host area: fetching, activating, ready and the failure screens after them
are full screen, as on a phone. It stays while the host is being checked.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): host-area workspace-key read survives a write during it

On a mirror miss the read went through the mirror's hydrate, which drops
its answer when any mirrored write lands meanwhile, so a stored key read
as unset right after a relaunch. A miss now reads the store directly, and
a store failure rejects rather than reading as unset.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): a wide detail session hands the host route to the shell

A session opened on its own under a page that can own the host area
kept a hop to the host route in-page (a missing worktree's notice, or
leaving the session), drawing a second host list beside the native
sidebar. Its pairs now leave the host route out, so the hop goes to the
shell, which opens the area-owning session. Phones keep the hop in-page.

Claude-Session: lane-ipad-page-owns-host

* test(mobile): a phone mount's layout dispatch leaves the session alone

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): leaving a wide detail session returns to the host area

The leave-session chevron and the missing-worktree bounce hand the host
route to the shell on a wide layout, which pushed it: a second host-area
session over the session, with Back reopening the session. On a wide
layout the shell now dismisses to this host's route instead, taking the
href's params, or replaces the session when nothing is below it. Phones
still push.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): a narrow-wide flip restarts a detail session's page

The route view comparison now counts the paired routes, so a detail
session under a page that can own the host area restarts on a flip
instead of keeping the narrow pairs, whose host-route pair let the page
draw a second host list beside the native sidebar.

Claude-Session: lane-ipad-page-owns-host
2026-10-09 14:57:07 -04:00
1f8dce85d4 refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code (#26895)
* refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code

- One admission prologue and one unary invocation shared by both RPC dispatchers
- Delete rpc schema re-export shims; import from the shared params catalog
- Delete the unregistered duplicate markdown tab method module
- Collapse multiplex installer stage types; add isMultiplexStreamAttached
- One source-identity guard in the ACK recovery snapshot
- Single stream teardown in the renderer multiplexer
- De-duplicate RPC method declarations; derive dispatch options from RpcContext
- Share viewport-frame decoding and one head-trim loop for pending queues
- Single sendTerminal call in sendTerminalStreamInput

* test: point cross-version and scope checks at the moved browser tab-create schema; run transport parity test under Node

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:50:31 -07:00
OrcaWinandm4air fbcf0edc60 refactor(pty): one remote-runtime PTY id predicate (#26930)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:46:12 -07:00
OrcaWinandm4air 54b623b6a3 refactor(execution-host): one connection-to-host helper (#26929)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:45:37 -07:00
cbc6f54e1c refactor(runtime-core): clean up Phase 3-3.8 code (#26894)
* refactor(runtime-core): clean up Phase 3-3.8 code

- worktree.ps: summary lookup is a closure, not a 4-arg callback
- relay: shared RelayStreamAckWindow for fs and git response streams
- relay: terminal-frame slots live on RelayStreamRegistry
- one isBinaryBuffer/BINARY_PROBE_BYTES in shared/binary-buffer
- one automation owner fence on the controller, no casts
- drop unchecked Store cast in remote repo registration
- speech download reuses the shared abort-aware delay, no casts
- RelayRuntimeServices: drop leftover handler list and param casts
- use shared errorMessage() in audited files

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

* test(cross-version-wire): pass worktree ps summary lookup in both builds' shapes

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:40:51 -07:00
Jinwoo Hong 3eea6a5368 fix(mobile): size onboarding slides to the modal sheet on iPad (#26919)
* fix(mobile): size onboarding slides from the modal viewport

* fix(mobile): ignore zero-width onboarding viewport layouts

* test(mobile): model slide progress in onboarding width test
2026-10-09 14:38:04 -04:00
github-actions[bot] 5da51ea6b2 Update README downloads badge 2026-10-09 18:35:35 +00:00
NeilandJoaquin Terrasa d2d17a3e47 fix(orcad): show help before server startup
Make orcad --help print usage and exit before native preflight or server startup, preserving existing flag parsing and managed-runtime startup. Verified against the built server and launcher.

Adapted from #26621 by @espetro.

Co-authored-by: Joaquin Terrasa <josocjoq+dev@pm.me>
2026-10-09 11:33:44 -07:00
NeilandJustas Brazauskas 65580c5315 Honor OpenCode 2 launch mode when resuming sessions (#26843)
Let the configured launch command own the server mode when resuming OpenCode 2 sessions. Resume arguments select only the session, while Session History uses the existing default launch command so default standalone behavior is preserved.

Fixes #25003. Retains the contribution from #25004.

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>
2026-10-09 11:16:28 -07:00
Neil 077b62dee0 fix: use alternate browser for rich Markdown modifier clicks (#26920) 2026-10-09 11:02:58 -07:00
Neilandm4air 7d24509000 Reject terminal file drops from a different execution host (#26830)
A workspace file dragged into a terminal now pastes only when its source
host (and paired-server namespace) matches the receiving terminal's
transport; otherwise the drop is refused with a toast.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 10:59:11 -07:00
OrcaWinandm4air 1b2d07591d refactor(ssh-connect-managed): clean up Phase 3-3.8 code (#26890)
Remove the dead connectViaSystemSsh startup path, collapse SshConnection's
repeated transport teardown and duplicated passphrase rung, share one
execHostCommand across relay install modules, share the orcad preflight
short-circuit, single-source telemetry pick/libc, drop dead exports and the
TCP_NODELAY socket probe, and trim sftp-upload and reconnect-classifier
duplication. No behavior change.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 10:42:37 -07:00
Neilandm4air 51e7181850 Keep remote terminal path links on their own host (#26696)
* Keep terminal workspace root links on their execution host

* Resolve terminal root-link source host from the owner, not a missing connection id

A direct SSH pane whose worktree id also exists on the desktop has no
connectionId (ambiguous owner), which was read as local and offered the
desktop workspace. Resolve the source via the owner-route resolver and
skip the root shortcut when the source host is unknown.

* Resolve terminal file links against the source owner's host

A same-id SSH workspace left connectionId unset, so a rejected root link,
the default-app row and Shift-click fell through to this machine's files.
The terminal file context now names the resolved direct SSH host and
refuses file access when no owner can be determined.

* Keep an ambiguous terminal link owner unresolved instead of local

The connection lookup ignores detected worktree rows, so a registered local
row could override an owner the full resolver found ambiguous (registered
local, detected SSH) and send link stat/open to this machine. Only a missing
route may now defer to the connection lookup.

* Re-resolve a held chat link's owner before rechecking it

Native chat rechecks held link targets when a turn ends. A target resolved
while its owner was local kept that context after a same-id worktree was
detected on an SSH host, so the recheck still asked this machine. Each
existence ask now re-resolves the workspace owner and refuses when it is
unresolved or names a different host.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 10:33:42 -07:00
5f62f498f3 refactor(orcad-daemon-startup-cli): clean up Phase 3-3.8 code (#26892)
* refactor(orcad-daemon-startup-cli): clean up Phase 3-3.8 code

- One helper (getAllDaemonAdapters / listEveryDaemonGeneration) for enumerating
  daemon generations, replacing five hand-rolled copies.
- Shared serve pairing/endpoint readiness for Electron serve and orcad.
- One option list builds both --serve-* and orcad serve argv.
- holdDaemonAdoptionLease takes an options object; reuse childProcessHasExited.
- Drop orcad-entry's dead re-exports and repeated version fallback.
- Reuse the shared withTimeout in daemon retirement.
- Share the install-wait step in the serve update supervisor.
- Trim stale or misplaced comments.

Behavior unchanged; production code is net -132 lines.

* test(serve): keep real serveOptionArgs in launch mock; pin Electron serve argv

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 10:15:07 -07:00
Brennan Benson 7b91405547 Let in-page links switch sections inside a chat visual (#26729)
* Let in-page links switch sections inside a chat visual

* Make in-page links work in chat visuals on mobile and in WebKit

The first fix (an about:srcdoc base in the visual) only reached desktop
Chromium. On mobile, the page hosting the visual carried a policy with
base-uri 'none', and a srcdoc visual inherits its host's policy before
its own, so the base was refused and an in-page link still took the
visual off its page and showed it as failed. The mobile web shell sends
the same rule as a header. Separately, WebKit (iOS, and the web client
in Safari) fires the frame's load event for an in-page link inside a
sandboxed frame, and both hosts treated any second load as the visual
navigating away and retired it.

- The mobile host page and the mobile web shell header now allow only an
  about: base, so the shell's own base applies; the visual's own policy
  still refuses any base it declares.
- Instead of retiring a visual on its second load, the host now asks the
  visual's shell after every load. A later load must be answered by the
  same run of the shell within the plugin panel watchdog's 5 s deadline;
  no answer, or an answer from a new run (a reload), still retires it.
- Tests: unit tests for the policy, the answer reader and the frame's
  keep/retire rules, and a Chromium + WebKit test of both mobile surfaces
  that runs in the mobile web app CI job.

* Keep any answered later load, and say plainly what the check is for

The previous commit tagged each run of a visual's wrapper with a random
id so a reload would still retire it. The visual shares a window with
its wrapper, so it could read or set that id itself, and a reload only
reruns the same Orca-written document, which is not an escape. Drop it:
after any load past the first, the host asks once, and any answer on
this visual's channel within the wait keeps it; silence retires it.

- Comments now say the check is a tripwire for a frame gone blank or to
  another page; the frame guard (desktop), the host page's policy plus
  the app's load filter (mobile) and the sandbox are the boundaries.
- Visuals get their own 5 s wait instead of borrowing the plugin panel
  watchdog's, because a 1 s wait retired a busy visual in WebKit.
- Tests: a reload is now kept; going blank, or dropping the base and
  following a link, still retires. The CI routing for the shared wrapper
  is now pinned in the path-classification test.

* Load the visual links check's mobile modules only where they can build

The unit shards run every config/scripts test with only the root
dependencies installed. This check imported a mobile/ module at load,
and transforming it reads mobile/tsconfig.json, which extends a package
those shards do not have. The transform error could not be sent back to
the test worker under Bun, so the whole shard stopped with
"DataCloneError: The object can not be cloned".

Import the visual shell and the mobile host page inside the dependency
gate, as the session-dictation check already does, so the shards skip
the file without loading them. The mobile web app job, which installs
mobile dependencies, still runs it in full.
2026-10-09 10:07:59 -07:00
OrcaWinandm4air 7e621d5a4a refactor(ssh-orcad-lifecycle): clean up Phase 3-3.8 code (#26891)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 09:50:51 -07:00
Jinwoo Hong 94ed1675e3 refactor(mobile): let the build alone decide the OTA shell (#26825)
Remove the Troubleshoot hybrid shell toggle and its stored flag. The shell
mounts iff the binary was built with EXPO_PUBLIC_MOBILE_SHELL=ota, decided
synchronously, so route switches no longer need a pending frame.
2026-10-09 12:47:48 -04:00
1c773a3e97 refactor(ssh-relay-runtime): clean up Phase 3-3.8 code (#26889)
- Pinned-relay planning throws its PinnedRelayFallbackError instead of returning a host-node plan
- One shared execHostCommand replaces eight private copies
- Install lock shares its post-acquire GC recheck and unconfirmed-termination catch
- Relay session tracks unsupported optional methods in one set
- Previous-relay census carries one status instead of two booleans
- Remote-node resolution drops the signal-only option shims

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 09:24:07 -07:00
Neil bd85767ee7 [CI] Use public threads for mobile unit tests (#26899) 2026-10-09 07:51:31 -07:00
OrcaWinandm4air 40716d702e fix(remote-terminal): keep the disconnected-input grace honest across retries (#26871)
* fix(remote-terminal): keep the disconnected-input grace honest across retries

A web pane's Reconnect cancelled recovery to idle before replaying its attach, which
reset the 5-minute grace, so each failed Reconnect extended how long held keys could
wait. And a retry started inside the grace discarded every held key once the grace
ended, including keys typed during that retry.

Held input is now split into cohorts at each retry from the latch: the sealed cohort
keeps the grace of the latch it was held through, keys typed during the retry are only
charged to a later latch, and the web Reconnect cancel no longer resets the grace.

* fix(remote-terminal): a failed in-flight write from an expired cohort keeps newer input

Dropping an expired cohort discards the journal, which fails a held accepted write
still in flight; the suspended flush then treated that as a broken link and cleared
the keys typed during the retry. The dropped write is now retired, so the flush pauses
or continues with the newer cohort instead.

* fix(remote-terminal): keep held input whole across a retry begun inside the grace

Dropping an expired older part of the hold while delivering keys typed after it is
unsafe: held input is one ordered stream, and the dropped part may be the Ctrl+C the
newer keys rely on. Replace the cohort split with an order-safe bound: a retry begun
inside the grace carries the whole hold until it ends, and the next latch measures
from the original start, so the overrun is at most one recovery window.

* fix(remote-terminal): replay a web pane Reconnect inside a recovery epoch

The web Reconnect path cancelled recovery to idle before replaying its attach, which
cleared the deadline: an attach whose snapshot stalled could release held input at any
later time. It now begins a fresh recovery epoch, as auto-recovery replays do, so the
deadline bounds the attempt and fences a late snapshot, and the cancel no longer counts
as a recovery that resets the grace.

* docs(remote-terminal): grace reset wording matches its reset points

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 06:13:20 -07:00
Neil 9d1cd1e8b1 [CI] Consolidate related tests and advance transcript waits with the existing virtual clock (#26874)
* [CI] Consolidate related tests and advance transcript waits

* [CI] Refresh reliability gate references after test consolidation
2026-10-09 06:12:22 -07:00
OrcaWinandm4air 3f37f7ea6d Keep an unplaceable row owner out of desktop Open in launches (#26797)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 06:02:49 -07:00
Jinjing c9c6a00449 Keep usage overflow chip width stable as hidden count changes (#26873)
The "+N" overflow chip in the status bar changed width whenever its count changed, so the density probe that decides how many usage chips fit kept re-measuring. Reserve the width of the provider-count label with an invisible sizer span, so the chip keeps the same size across counts.

- Pass the total provider count into UsageOverflowChip and render it in an invisible, grid-stacked sizer span.
- Overlay the visible "+N" label on the same grid cell so the chip width tracks the total, not the hidden subset.
- Add an e2e test that loads ten usage providers, shrinks the viewport to collapse them, and checks the chip width stays stable and no "Maximum update depth" or React #185 errors occur.
2026-10-09 05:58:50 -07:00
Neilandm4air 05ab94bd65 Move shared file watches when the selected remote host changes (#26632)
* Invalidate shared file watch targets when the selected host changes

* Watch the selected host's own workspace root when hosts share a workspace id

* Watch each open editor on its own execution host instead of the selected host

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 05:47:35 -07:00