* fix(worktree): refuse a parent loop even when the worktree cache is cold
The parent cycle walk read each workspace's current instance from the
resolved-worktree cache, which the reparent itself invalidates, so it only
knew the two endpoints and accepted A->B->C->A. Read saved worktree
metadata instead.
* fix(worktree): fall back to locator metadata and tolerate loops saved earlier
* fix(worktree): skip another host's locator metadata in the parent loop walk
* fix(worktree): read loop-check identities from a read-only, ownership-scoped view
The per-host metadata accessor migrates a hostless legacy row into the
queried host, so the loop check could adopt another host's worktree and
refuse a valid parent. Read one read-only host snapshot instead, counting
a row only when its project is registered on that host (the resolved
list's ownership rule).
* fix: import host-qualified worktree meta helpers once
* test: match main's host-targeted lineage write contract
* feat(terminal): the window's store shows main's pane start status (layout-owner build)
A status-only frame changes one store entry and no pane tree, so the pane's view stays mounted.
* refactor(terminal): keep the frame sync within its line budget
* feat(terminal): every pane main starts shows 'starting' from before it is written (layout-owner build)
* feat(terminal): a pane main is starting waits and attaches; a failed start shows why (layout-owner build)
Fixes owner-visible row 15: a failed agent or setup start no longer opens a plain shell.
* test(terminal): iterate store listeners directly
* fix(terminal): an exit that reports no code keeps its pane, as lost contact (layout-owner build)
* test(terminal): a pane main started attaches to main's terminal at its own grid
* test(terminal): detach census tests assert main's move and frame with the switch on
* test(terminal): exited-pane tests assert main's close with the switch on
These 8 have failed switch-on since #31130 moved the exit close to main.
* feat(terminal): main writes and starts every split of a pane it holds; the window attaches (layout-owner build)
The window's split, a startup split with its command, and a CLI split all go through main's
terminal.split; main's frame mounts the pane, with or without a window intent. Deletes the
window's split wait and withdraw, the split appear intents, and the queued-split skip and cancel.
The e2e helper uses the real split. A startup split leaves focus on its source pane.
* fix(terminal): the attach-only wait leaves sleeping, resuming and held panes to their restore path
* fix(terminal): a split pane attaches to the terminal main bound it to; a taken pane id is refused
Also restores the proof that a split closed while it starts gets one answer and leaves no
pane or start status behind.
* fix(terminal): a window split main cannot start still shows its pane, failed, with why
* fix(terminal): check a taken pane id first, keep the start's own error, clear a gone pane's status
Three test stores gain the pane-start field the window's store always has.
* test(terminal): the admission stub answers whether its pane is still written; a closed pane's failed start clears
* refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module
* test(terminal): a pane main wrote and the user closed mid-start drops its failed status
* fix(terminal): a split main starts is listed to phones and session.tabs without a window's report
* fix(terminal): a reconnect shows its pane starting and why it failed; a lost owner race shows no failure; a split no one asked to see keeps the phones' tab
* fix(terminal): a kept create another owner won shows no failure; the window attaches to the winner
* fix(terminal): a pane that loses its attach to another connection attaches to the winner
When a terminal pane reconnected to a saved process that had exited, and
another writer re-bound the pane meanwhile (most often the window's own
session save), main threw terminal_pane_owner_changed. The window showed it
as a raw red banner over a pane that was otherwise working, and nothing
cleared it.
Main now re-resolves the pane's owner once and attaches to it, with fresh
absence evidence even when only the saved binding changed. Retiring a dead
id's binding is fenced on its incarnation so it can never clear a newer
process that reused the id. A second loss is still reported; the window
asks once more for a local pane (never a fresh launch) and otherwise shows
the existing reconnect banner with Retry instead of the raw code.
Tracked in STA-4139.
* refactor(terminal): move the stable-pane attach step into its own file (max-lines)
* fix(terminal): fence an absent owner's retirement on an explicit incarnation, not the binding's own field
* Complete bounded pane ownership recovery through host publication
* Apply repository style rules to pane recovery changes
* fix: preserve terminal adoption type contracts
* fix: complete terminal recovery ownership and acceptance contracts
* fix: keep terminal registration evidence with its candidate
* fix: satisfy terminal recovery lint gates
* fix: consolidate terminal recovery imports
* test: explain bounded terminal fixture assertions
* test: handle optional pane binding maps in assertions
* fix: scope remote pane ownership refresh to recovery epoch
* fix: reject remote pane replies after recovery epoch ends
* Fix static checks for pane recovery
* Reuse session partition assignment in binding retirement
* fix: acquire returned terminal receipt before rejecting early exit
* Release superseded terminal spawn preparation before retry
* test: type preparation cleanup callbacks as functions
* test: initialize telemetry classifier in preparation collision cases
* test: identify preparation resources in reentry assertions
* fix(terminal): let a recovered pane replace the binding its host proved dead
Reopening an agent terminal whose process had exited showed "Orca couldn't
verify this terminal's owner", and the next relaunch lost the tab. The pane
operation proved the old PTY absent, but a window session save wrote that dead
binding back before the fresh commit, and the commit treated it as a rival
owner. The refused candidate was then shut down without an intentional-stop
mark, so its exit retired the whole tab.
The operation now carries its absence proof into the fresh launch, and both the
pre-launch owner check and the commit accept a replay of that dead binding. A
different or unproven owner is still contention. Discarded candidates are
marked as replaced, so their exit keeps the tab.
* test(terminal): type the discard test's runtime mocks
* fix: expose reconnect for retained pane recovery
* test: expect replaced stop for discarded fresh terminals
* fix(terminal): type the absent owner's retirement fence after the main merge
* test(terminal): follow main's Reconnect handler in the pane binding ownership test
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* test(layout): terminal reply fixtures captured from the window-owned build, and main's switch-on proofs
* wip(layout): main's terminal surface index feeds the runtime graph (dark)
* wip(layout): main's panes stand in for the window's terminal list (dark)
* test(layout): the window sends main-listed panes as mount facts (dark)
* test(layout): surface proofs use public runtime calls
* chore: lint
* feat(layout): close by handle closes in main's layout; the close-time relist goes (dark)
* feat(layout): main's binding outranks a caller's pane claim on the terminal record (dark)
* test(layout): window-list tests stay on the rollback build; switch-on mount-fact cases (dark)
* wip(layout): main's terminal index reads E1's shared projection and follows its operation ends (dark)
* refactor(layout): the main-surfaces host lives beside the session-tabs readers, keeping the graph file under its line cap (dark)
* fix(layout): main's panes skip the window-era membership fence, follow workspace routing, and list a terminal when it registers; window close only hands the graph back (dark)
* refactor(runtime): explicit-close PTY stops move unchanged to their own module, for the file's line cap
* test(layout): the cold-restore table runs in a layout-owner build too: one push per registration
* test(layout): the parked and automation pane tests assert the same window report in both builds (mount facts with the switch on)
* chore: one layout-owner switch import after the restack on D1a
* feat(layout): main's terminal.list rows carry the host's verdict; a pane with a status is never live (dark)
* chore(layout): fit main's surfaces under the lint gates after the D1a restack
* feat(layout): every pane status D1a publishes keeps a row from reading live; remote host answers pass through (dark)
* feat(layout): a drag-out moves main's own binding and refuses the home copy's last pane; the window's terminal claim is not consulted (L127, dark)
* fix(layout): a tab close by a starting pane's handle stops the tab's terminals; a starting pane is never exited on the inventory alone; terminal.list lists every saved pane main owns; the pane claim lookup leaves the output path (dark)
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
* fix(layout): main's index reads routing only for a workspace two partitions hold, the fence reads ownership once, a legacy pane keeps its claim; switch-on tests assert main's answers after a window closes (dark)
* refactor(runtime): a worktree stop's terminal collection moves unchanged to its own module, for the file's line cap
* fix(layout): main's index routes every workspace as the layout stream does, re-reading routing only on a write or a terminal registration; a worktree stop names every terminal main binds; a starting pane is unverifiable (dark)
* feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark)
---------
Co-authored-by: E2E Test <e2e@test.local>
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(automations): headless keeps finished per-run sessions asleep instead of closing them (#24208)
* fix(automations): a declined sole-terminal sleep never counts as slept
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(persistence): worktree meta and lineage writes take the row's host, not the id (R4-2b)
* fix(persistence): review fixes for host-targeted worktree writes
* test: SAFETY notes on partial runtime store mocks
* test: drop imports duplicated by the rebase onto #30797
* fix(runtime): a terminal spawn names its host; a removed project's fence is per (project, host)
acquireWorktreeTerminalSpawn takes the spawn's HostWorktreeTarget instead of a
bare id plus an optional connectionId that defaulted to this machine. The
removal fence records ExecutionHostIds and only counts surviving rows that run
in this process, so a paired server's same-id residue row no longer readmits a
removed local project's spawn.
* fix(runtime): keep every surviving row in the spawn fence; normalize the spawn connection (review)
* fix(persistence): repo-row store writes take the row, not the id; ratchet the rest (R4-2a)
* fix(persistence): host-filter the runtime nested import; widen the id-only write ratchet (review)
* fix(runtime): nested import keeps matching runtime-stamped native rows on a paired host
* fix(persistence): class-scope the ratchet param exemptions; update a new main test to the target shape
* fix(ipc): fence a project-group move by the resolved twin host, so an SSH twin is not WSL-fenced
* test(ci): pin the id-only store write ratchet step to the static phase
* wip: orcad client-hosted browser pages
* wip: tests and renderer placement
* wip: engine-free client tab queries; keep paired server intent
* wip: one logical tab list for orcad engine tabs and desktop-hosted pages
* wip: one selection rule for engine tabs beside desktop-hosted pages
* refactor: one owner per mode for orcad tab selection; hide reopen-on-server without a server browser
* fix: an engine tab create or switch takes workspace selection from desktop-hosted pages
* refactor: one logical tab list and one selection rule for orcad engine tabs and desktop-hosted pages
* fix: keep the engine's per-workspace current tab beside desktop-hosted pages
* fix: an unscoped current tab matches unscoped automation routing
* fix: retained desktop-hosted pages stay listable and closable after their desktop leaves
* fix(projects): refuse to set a folder up under a project its own GitHub remote contradicts
Setting an existing folder up for a project stamped the project's identity
as the folder's upstream even when the folder's own remote named another
GitHub repository, so the folder was silently relabelled into the wrong
project (fabricated upstream, wrong projectId on its setup and workspaces).
Fixes#30663
* test: type the folder-remote spec store without casts
* fix(projects): judge a registered folder by its current remotes; ignore host aliases
* test: cover the desktop re-read of an already-registered folder
* fix(persistence): repo writes take the row's identity, never a bare id
updateRepo(target, updates) now takes Pick<Repo,'id'|'connectionId'|'executionHostId'>
(RepoHostTarget) and resolves the host from it, so a caller that holds the row can no
longer drop its host and write a same-id sibling on another host. The id-only callers
(project host setup controller and IPC handler, folder-to-Git upgrade watch, both
clone paths, registration adoption, fork backfill) pass the row they resolved.
Co-Authored-By: Claude <noreply@anthropic.com>
* test: migrate the remaining Store.updateRepo callers to row targets
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Programs can tell Orca what they are doing through the Program Status
Protocol (OSC 7501); Claude Code, Codex, Grok, pi and oh-my-pi already do.
Orca answers the protocol's feature-detection probe (visible xterm and the
hidden-pane emulator), parses reports from PTY output on the execution host
(src/shared/program-status/), and keeps two lanes:
- Lifecycle state keeps one owner per pane. Hooks win; OSC 7501 drives the
row when hooks are off, missing, or silent on a state the program has
left (server/program-status/hook-precedence).
- The program's own report (progress, its one-line message, blocks such as
sign-in) is a live-only `programStatus` facet kept per pane
(server/program-status/facet-store), stitched onto rows at publish time
and carried to the renderer store, mobile, paired clients and
`worktree ps`. Facet-only changes patch just the facet.
Title-driven readers (idle edges, delivery, tui-idle, exit inference)
classify by the program's OSC 7501 activity while it reports one and fall
back to title glyphs only for agents that report nothing explicit, so they
keep working when Claude stops painting its title glyph after the probe.
* refactor(pty): one spawn-options step for both spawn lanes, and one first-size rule
The window lane (pty:spawn) and the host lane (runtime terminal create)
each built the provider's spawn options with their own copy of the same
steps. Both now call one shared assembly for the env deletions and the
shared provider fields; each lane keeps only what is its own (pane
reservation, Antigravity prep, agent-session claims). The duplicated
provisional-size restore moves to the same module, and the window lane
drops its second, identical hidden mark.
A terminal's first size now comes from one rule: the caller's measured
pane, then the measured group, then a lone group, then 120x40. Host
creates accept the caller's measured size for the callers that move
onto them next.
* fix(pty): keep terminal-create under the file-length limit
* fix(pty): preserve hidden replacement and settings timing
A client's pane rename already lands in the host's saved layout (titlesByLeafId),
but terminal.list/show never read it, and clearing a remote tab's last pane title
sent no titles at all, which every host reads as 'unchanged'.
- terminal summaries carry an optional paneTitle from the host's saved layout;
the CLI list prints it ahead of the terminal title. Older hosts omit it.
- clearing the last pane title of a remote tab pushes titlesByLeafId: {}, which
old and new hosts both persist as cleared.
Supersedes #30333, which renamed the whole tab via terminal.rename.
Co-authored-by: wangshifeng <shifeng.wang@magic-shield.com>
* fix(layout-owner): main repairs a terminal stored in two panes once at startup, after moving the dropped pane's owners (dark, layout-owner build only)
* fix(layout-owner): the load repair moves the dropped pane's records and skips the write when an owner move fails (dark, layout-owner build only)
* fix(layout-owner): the load repair leaves a paused WSL move's workspace untouched (dark, layout-owner build only)
* refactor(pty): one spawn step stamps a terminal's pane identity for both spawn lanes
The window spawn controller and the host runtime's caller each wrote
ORCA_PANE_KEY/ORCA_TAB_ID/ORCA_WORKTREE_ID and jcode's runtime dir, in
different layers, so per-pane env behavior drifted (the jcode daemon
pre-warm exists only on the window lane). stampPtySpawnPaneEnv now writes
them in both controllers from the spawn's own tab/leaf/worktree ids;
buildTerminalWorkspaceEnv no longer stamps them. No behavior change: a
TEMPORARY prewarmsJcodeOnDaemonHost input keeps host daemon spawns from
pre-warming until its own PR.
* test(runtime): read a fake spawn's pane env without type assertions
* test(pty): host serializer-settlement spawns name their pane by tab and leaf, as the runtime does
* fix(pty): preserve caller runtime directory after managed preparation fails
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#29975
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 #29968
Co-authored-by: Claude <noreply@anthropic.com>
* 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
A host that could open a window but had none left a workspace-slept running
agent's record for a window wake that never came, so opening its tab from a
paired client or phone launched a fresh agent with settings defaults (for
example --dangerously-skip-permissions instead of the saved --permission-mode
plan). The pane now resumes it with only the recorded flags. Before the spawn
it durably claims the record by rewriting it into the host's own pane-owned
capture form, which a window wake never relaunches (no second writer) and
which still resumes the pane on a later open if the PTY is lost or the spawn
fails. With a window present the window still owns the wake, and the pane
opens a plain shell instead of a fresh agent.
Since #27609 a server serves no SSH target and refuses repo.addRemote, yet the add-project dialog
still offered "Project on this server's SSH host" and the refusal read "SSH connection not found
or not connected". The row is now disabled with a message pointing to adding the SSH host on this
computer, and the server's refusal says the same for older clients that still offer it.
The runtime GitLab API answered listAssignableUsers with an empty list, so the reviewer picker
was always empty for a paired server's repo. Add an optional gitlab.listAssignableUsers RPC on the
host; a host that predates it answers method_not_found and the picker stays empty as before.
orcad built its runtime without the persistent claim signer, so each restart
minted a new key and the surviving terminal daemon could not match the old
owner: resuming the same conversation started a second writer. Load the same
signer the desktop uses from orcad's own user data.
A workspace-slept running agent's record was left for a window wake that a
windowless host never has, so opening its tab started a fresh agent. Such a
host now resumes it in the pane, under the existing duplicate-writer checks
and recorded-flags-only rule.
* fix(projects): refuse a folder-to-Git type change that would hide folder workspaces (STA-10638)
A folder project's extra workspaces exist only as metadata rows that the folder listing shows. Changing the project to Git switches it to `git worktree list`, so those workspaces drop out of the next refresh and their saved tabs are purged as if they had been deleted. Only the automatic upgrade checked for this; `orca project setup-update --kind git`, repo updates and clone-into-folder did not.
The check now lives in the one repo write every type change goes through, so any caller gets the same refusal and nothing is written. The automatic upgrade and both clone paths use the same check and keep the project as a folder project.
* test(repos): give the repo handler store mock the metadata read the clone path now uses
* fix(projects): a manual folder-to-Git change proves the folder is the Git root first (STA-10638)
Only the automatic upgrade checked that a folder project's path is the top of a Git repository and recorded Git's spelling of that root. A manual change (orca project setup-update --kind git, repo updates, clone into a folder project) flipped the type without either, so a project inside another repo became a broken Git project, and a project whose path Git spells differently lost its main workspace's id and tabs on the next refresh. All of them now run the same proof and write the proven root with the type change. SSH projects keep the unverified change because their Git runs on a host this process can't probe.
* Revalidate folder-to-Git conversion targets after Git proof
* Explain when a project changes during its Git check
* Fix clone conversion for replaced folder project
* test(runtime): create real Git root for clone upgrade fixture
* test: normalize runtime clone root stamp expectation
* test: check clone registration identity without unknown access
* chore: restore main's pnpm-lock.yaml that the main merge reverted
* Run automated workspace setup in a dedicated tab
* Preserve native-chat setup route coverage after placement retirement
* Preserve Setup tab design and independent review evidence
* Classify activation test fixtures consistently for routing checks
* Sync runtime English catalog after removing setup preferences
* Update listener savings model after retiring setup action
* fix(terminal): open the picked Windows shell on a managed host instead of typing it into PowerShell
* fix(terminal): keep the shell pick through materialization and folder runtimes
* fix(terminal): refuse a shell pick on agent launches and update paired-tab expectations
* test(terminal): type the shell-selection runtime fixture instead of casting
* fix(agents): a resumed agent's chat is readable on the phone before its first new turn (#10923)
* fix(agents): skip a dropped Codex resume and keep the seeded identity across restart
* fix(agents): seed resume identity before the spawn's first await and keep it through terminal evidence
* fix(agents): seed after the spawn commits, and name the session on host-built resumes
A pane whose shell outlived its agent kept counting as the session's live
holder because its PTY was alive, so a paired client got a plain shell.
Ask the hook store: a holder it proves exited no longer blocks the resume;
unverifiable still does.
* fix(terminal): read a Windows ConPTY terminal as painted, not with PSReadLine's overwritten history prediction
* fix(terminal): bound the ConPTY rendered read, honor visible-only probes, and fill the line limit
* test(serialize): record the ConPTY capture's pre-existing wrap-pending cursor restore
* fix(browser): defer publication cleanup until native destruction completes
* test(browser): reproduce the serve tab-close SIGSEGV with a hidden page window
Headless serve backs each page with a hidden top-level BrowserWindow, whose
Aura view is what crashes; the <webview> guest the fixture used never reached
that path, so the test passed against the original code. With the serve shape
the test segfaults (fault 0x1268, RIP +0x55a8d7b) without the fix.
* test(runtime): wait for the deferred publication restore after paired create and close
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* perf(worktrees): a Smart-sort save on a paired host no longer refetches every repository or schedules the next save
The host now publishes the saved sortOrder values on the order-only
worktreesChanged event, so a new desktop patches its rows in place instead of
re-reading each repository. A refetch whose only difference is sortOrder no
longer bumps sortEpoch, so an older host's save cannot trigger another Smart
re-sort and save.
Refs #27268
* fix(worktrees): keep lineage retry and receipt order for published sort-order saves
* fix(worktrees): refetch rather than apply sort-order values published on an earlier connection
* fix(worktrees): fence published sort orders on the event-stream subscription key
* fix(worktrees): refetch a published sort-order save when its rows were never loaded
* fix(remote): opening a slept agent pane from a paired client resumes its session
When a paired client opened a pane whose PTY was gone, the host started a
bare shell (or a fresh agent) instead of resuming the agent session it had
recorded for that pane. Resume from the pane-owned record the way the host
window's cold restore does.
* fix(remote): label a resumed pane with the agent it resumes
* fix(remote): do not resume a conversation another pane also records
* fix(remote): send a pane resume through restore admission
* fix(remote): do not resume a conversation already live in another pane
* fix(remote): block a pane resume while another holder is not proven exited
* fix(remote): do not resume a record the pane's hook identity has superseded
* fix(remote): an unhydrated session holder blocks a pane resume
* fix(remote): compare full provider-session identity in the resume guards
* Allow worktree parentage across projects on one host
* Keep sidebar placement and project ownership consistent
* Keep project labels on phone rows outside their own section
* Derive sidebar placement and reveal from shared occurrences
* Preserve project text across workspace card styles
* Resolve saved lineage peers without rebuilding host projects
* fix: preserve independent project deletion progress
* fix: satisfy mobile formatting and test lint checks
* test: declare project fixture arrays without assertions
* Align reveal fixtures with rendered workspace occurrences
* fix: reuse project name for cross-repo child cards
* fix: satisfy mobile section lint after main merge
* fix: retain project label for nested mobile rows
* test: keep nested card owner assertion resilient
* test: assert project badge visibility across card styles
* test: drop lineage tests already covered by the whole-flow suites
The host-peer projection is exercised by the runtime and SSH listing
integration tests, and the simple phone nesting case is covered by the
deep-chain, Pinned and hidden-project flow test.
* test: reuse automation fixtures and narrow long-poll setup
* test: load terminal RPC methods only in consuming scenarios
* ci: skip packaged Orcad lanes for unrelated unit test edits
* style: format the WSL Orcad workflow
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters
A phone created through a desktop now records whom it acted for, so the
desktop's Hide other-client workspaces filter keeps the user's own phone
workspaces (locally and on servers) and hides only other people's.
The desktop relay marks each server row its sidebar would hide, and the
rows its own browser or chat tabs keep awake; the phone applies both.
* test(e2e): dedupe sidebar row ids in the visibility spec
* fix(mobile): address review: owner naming, old-server relayed phones, scope trim
* refactor: move the sidebar's project section grouping into shared
The project sections the desktop sidebar draws under Group by Project are
built from the project/setup projection, the per-host catalog fold, the
endpoint host stamp, and the section labelling and ordering rules. Move the
pure parts of each into src/shared (no behaviour change) so the paired phone
can call the same code instead of guessing the grouping.
* fix(mobile): group by Project with the desktop sidebar's own sections
The phone grouped Project sections by repo display name, so it merged
checkouts the desktop keeps apart (several local stablyai/orca checkouts,
or a server's copy) and split what the desktop merges. The phone now reads
each host's repo.list, project.list and projectHostSetup.list (the desktop's
own and each server's through the relay), folds them with the renderer's
shared catalog fold and projection merge, and builds sections with the
desktop's shared grouping, labelling and ordering code. Keys, labels and
Recent order equal the sidebar's; a host that predates the project lists
gets the repo-derived projection, as the desktop does.
Header colour and icon come from the section's anchor repo and rows from
their own repo by id, as on the desktop. The repo filter lists every
catalog repo by id per host instead of merging same-named repos. Group-by
labels read Project and None. project.list and projectHostSetup.list are
routed for paired phones (read-only, workspace permission).
* refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag
A phone now records its owner as the creator (this host for a directly
paired phone, the relaying desktop for a delegated one), so the shared rule
is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved
browser/chat tabs into hasHostSidebarActivity, so the phone's status dot
follows too, and local worktree.ps counts the same signals. The old-server
fallback is removed.
* test(e2e): poll the phone and the sidebar together in the visibility spec
* fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order
Review fixes. The sidebar's buildOrderedGroups now calls the shared
buildProjectSections for Group by Project (notice and pending repos are
optional inputs), so desktop and phone run one function. The phone ranks
manual header order by its folded catalog order, which is each host's
repo.list order with the desktop's own repos first, as the renderer loads
them; only the desktop's cross-host manualRepoOrder (pairing-local) is
unknown to it. The projection selector is now a memo over the shared
resolver. A transient project-list failure keeps that host's last catalog;
only forbidden or method_not_found falls back to the repo-derived
projection. Catalog rows are checked on every member grouping reads, and
the parity test answers the renderer's real catalog fetch, includes a
server's own project rows, and fixes Manual and Recent orders that differ.
* fix(mobile): use the repo-derived catalog until a host's project lists first answer
* fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it
The phone fetched repo.list, project.list and projectHostSetup.list from
the desktop and every relay-ready server each minute and folded them
itself, so rows from offline or older servers fell back to repo sections
and keys moved on cold start.
The desktop renderer now publishes the inputs its own Project headers use
(state.repos in sidebar order, the projection selector's projects and
setups, projectOrderBy) on the runtime window graph it already sends to
main, only when they change. Main keeps the last one and answers it on a
new paired-desktop read, mobileRelay.projectSections. The phone reads it
with its repo metadata and groups with the shared builder; a desktop that
predates the read (forbidden or method_not_found) leaves the phone grouping
rows by repo from the desktop's repo.list, and a transient failure keeps
the last catalog. The payload carries only grouping and label fields, no
hooks, credentials or uploaded icons. The phone's own catalog fetching and
its project.list/projectHostSetup.list routes are removed.
* fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests
* test(e2e): the desktop window serves its sidebar's Project catalog to the phone
* chore(i18n): regenerate the runtime English catalog (drift already on main)
* fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow
The extra read inside the repo-metadata refresh stalled its recorded RPC
sequence. It now runs beside it with its own throttle. The repo-metadata
goldens are re-recorded for the intended state change: the name-keyed
colour, icon and id maps became desktopRepos.
* refactor(sidebar): one shared Workspace options filter and sort evaluator
The sidebar, the jump palette and the options badge each applied the
Workspace options filters with their own copy of the rules. They now
call one pure evaluator in src/shared/workspace-view, which reads each
row through a small fact reader so the phone can call it next with the
facts its host serves. The sort comparator moves there too, with each
surface supplying its own Agent activity rank.
No behaviour change: a parity test runs the old sidebar chain and the
new evaluator over every toggle combination, and the old comparator
against the new one for each sort mode.
* wip: workspace view descriptors
* refactor(sidebar): address review of the shared Workspace options evaluator
One predicate builder instead of a per-row scope argument; one
exemption-narrowing function taking the hide flag; consumers import the
shared predicates directly; Agent activity ties fall to the label in the
shared comparator. The activation filter lift reuses the shared
predicates. The parity test now covers other-client rows, live agent
attention classes and the sort-label map.
* fix(mobile): fetch the Project catalog at once when the screen moves to another host
The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers.
* wip: desktop option items from shared choices
* refactor(sidebar): the Workspace options menu renders from one shared option list
src/shared/workspace-view/workspace-view-options.ts lists every Workspace
options control (Hosts, Projects, Group by, Sort by, Project order, Card
layout, Show properties or Card display, and each filter) with its
section, its English copy, its choices and when it shows. The desktop
menu walks that list and renders each option through a table keyed on
the option key, so a new option fails to compile until the desktop has
a control for it; the phone sheet will do the same.
The choice lists (sort modes, project order, card layout, agent activity
layout, card properties) are derived from the shared ids, and a test
checks the desktop's English copy matches the shared copy. The unused
classic branch of the card property list is removed.
No visible change.
* wip: row facts
* refactor(sidebar): address review of the shared Workspace options list
The show rules get their own tests with fixed expectations (the cases
the old filter-section test covered, plus the rest), and the menu test
checks each option's own control rather than any matching text. Card
display rows toggle one property each, with the folder-path copy in
shared. The unused Status and Tasks copy left by the removed classic
card list is deleted from the catalogs. Filter rows write through the
store directly; files are named for what they hold.
* feat(runtime): worktree.ps carries the Workspace options row facts
Each worktree.ps row now says whether it is the project's default-branch
workspace, created by an automation, created by the CLI, or on a
detached HEAD, answered by the same shared predicates the desktop
sidebar filters with. The phone has none of these facts today, so its
copies of the filters either guess (default branch) or are missing.
Additive and inert: optional fields that nothing reads yet. An older
host omits them; readers must treat absence as 'not that kind', so the
row stays visible. Server rows relayed through the desktop pass them
through unchanged.
* feat(mobile): the desktop's own phone reads and writes its other-client filter
ui.get and ui.set strip the pairing-local UI fields for every paired
client. That rule exists for clients with their own host catalog (a
desktop paired to a server, a web client). A phone has none: it shows
its desktop's view, so 'Hide other-client workspaces' must be one shared
value. For the host's own phone (clientKind mobile, no relaying parent)
ui.get now always answers the field and ui.set persists it; delegated
phones, paired desktops and web clients keep the stripping rule.
The phone's view-settings schema reads the rest of the Workspace options
values it does not read yet (the automation, CLI and detached-HEAD
filters, project order, card properties, agent activity layout), each
salvaged to absent so an older desktop keeps the phone's local value.
* feat(runtime): address review of the worktree.ps row facts
Name the fact defaultBranch, and say what absence means for it: a reader
keeps its own guess, because 'hide default branch' must not start hiding
or showing rows an older host never described. Folder workspace rows get
the facts too, so every row from a newer host carries them. The
desktopOtherClient comment now says it is a verdict, not a fact. The test
covers the rows where the host disagrees with the phone's current guess.
* wip: phone uses the shared evaluator
* feat(mobile): address review of the phone's other-client filter mirror
One isHostOwnPhone predicate decides both who shares the filter value
(ui.get/ui.set) and who gets the other-client row marks, so the two
cannot drift. The pairing-local field list notes the exception. Tests
cover a paired desktop still being stripped, and a phone write that
leaves the field out not touching the host value.
* feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules
The phone's own filter chain and comparator are replaced by the shared
evaluator, reading the facts the host stamps on each row. Phone filter
state takes the desktop's setting names and carries every filter; the
badge and Clear filters match the desktop's. The desktop's other-client
marks become facts the phone filters with the shared value.
A parity test runs the same workspaces through the desktop's reading
and the phone's (facts stamped as the host stamps them) over every
filter combination, folder projects included.
* fix(mobile): host scope in shared filters, patch-key writes, live view push
Folds the phone's host filter into the shared filters so Clear and the badge agree,
writes back only the touched filter keys, and pushes workspaceViewChanged so an open
phone list re-reads desktop Workspace options.
* fix(mobile): Clear sends only changed filters; skip a view re-read older than a local edit
An older host's strict ui.set refuses a key it lacks, so Clear names only what differs. A ui.get
that started before a local edit no longer overwrites it; the edit's own change event re-reads.
The change publisher's field list now lives with it and derives the filter fields.
* fix(workspace-options): all hosts and clear reach the desktop; one filter key list drives every surface
Host visibility: the legacy host scope fallback moves to shared code; the phone reads it, and the
host resets the scope whenever a client writes the host list, so All hosts and Clear stick.
An older desktop that keeps the other-client filter to itself has its marks treated as hide.
Badge counts, is-anything-filtered, Clear and reveal derive from the shared filter keys on the
desktop sidebar and the kanban filter menu. The phone uses the desktop's group and sort values,
holds its view in one state value, and reads its UI fields from one shared list.
* feat(workspace-options): Status and PR grouping read the shared tables
The phone groups by status with the desktop's status helpers and default catalog (its own copy
had the defaults in reverse order) and by PR with a shared lane table, so its labels now read
In review / In progress as the desktop's do. The desktop's PR lane decision ends in the same
shared state-to-lane step.
* test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times
* fix(mobile): write the host scope with the host list, and wait for the first read before forcing the other-client filter
An older desktop narrows back to its legacy single-host scope when the phone
writes only a null host list, so the phone now also writes the scope for a null
or one-host list. The other-client fallback waits for the first ui.get so a new
desktop's marks don't hide rows before its filter value arrives. Inline the
clear-filters key list and note the PR state asymmetry.
* test(mobile): re-record view-settings goldens for the unknown other-client state
* fix(sidebar): reuse the palette's default host instead of reading the focus setting again
* fix(sidebar): read the host options once in Workspace options and pass them to the Hosts item
* fix: parse the ui.get reply into a named type, fix the row memo test's filters, prune the focus-read baseline
* refactor(sidebar): one switch-to-kind table, and fold the leftover helper files back
- The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds;
the shared evaluator and main's phone-push check both apply it, and the filter
type derives its switch fields from it.
- git-identity.ts folds back into git-identity-display.ts, as on main.
- SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone.
- buildWorkspaceRowPredicate is module-private.