* Derive per-agent skill discovery folders and home env overrides from the provider table
Skill discovery and the env-var home overrides now read the skill install
provider table instead of repeating each agent's folders by hand. Output is
unchanged: same sources, labels, ids, paths and scan order. A drift test fails
when a table entry lacks discovery coverage or is not an Orca agent id.
* Simplify provider-table discovery: table order, no pinned slots, Muse hand-written
Provider home folders now follow the table order right after the shared
.agents root, with the one-off folders after them. Drops the pinned scan
slots and the discovery-only field; the provider id list is checked against
Orca agent ids at compile time.
* Derive provider ids from the table; scan the Codex plugin cache before agent homes
The provider id union now comes from the table itself, checked against Orca
agent ids, so adding an agent is one edit. The plugin cache is scanned before
agent homes so a skill reachable from both keeps its plugin source kind.
* Type the provider table by inference and drop the id cast
The provider definition and id types are read straight off the frozen
table, and the id guard compares ids without a type assertion.
* Label skill sources with the agent display name
Claude and Gemini skill sources now read "Claude Code home" and "Gemini CLI
home", matching the install picker; the separate short-name field is gone.
* test: remove repeated waits and fixture work
* test: give pure queue pause comparisons a unique suite title
* test(ci): remove repeated fixtures and deadline waits
* fix(native-chat): show and focus the field in pi text dialogs
Pi sends an empty placeholder, which removed the only hint on a borderless field.
Cards with no options now put the cursor in the field.
Fixes#28666
* fix(native-chat): move focus to the field when a number key advances to it
* fix(native-chat): show the default hint in multiline answer boxes
* fix(native-chat): send focus to the answer field when find closes over its card
* fix(mobile): draw answered approvals and questions as receipts, and a pending question as its ask row
The phone projected a structured chat's journal with the host's plain
projection: an answered approval read "Allow Bash? {json} resolved", an
answered question listed its choices without the answer, and every
AskUserQuestion call stayed a raw tool line, also while its card was open
above the composer.
Desktop's question projection and receipt rules move to src/shared
(structured-agent-question-projection, native-chat-resolution-receipt,
with the receipt subject and answer heading and their words). The phone's
structured session now projects through them and hands each row its
receipt; new MobileNativeChatResolutionReceipt and
MobileNativeChatAwaitingInputRow draw them as desktop does. In a terminal
chat, a question call draws as "Awaiting user input:" until answered and
"Asked:" after.
* fix(mobile): say a question was asked, not awaited, once its agent has ended
A structured chat's question stays pending in the journal after the agent
that asked it ends (a restart, a crash, a new generation). The host stops
listing it as actionable, so the answer card goes away, but the phone's
row still read "Awaiting user input" and announced itself as live.
The session hook now returns the host's actionable prompt ids next to the
receipts, and each row asks the same shared
isActionableStructuredAgentSessionPrompt desktop's transcript row calls.
A question nobody waits on reads "Asked: …", as on desktop. The row
callback moves to its own hook so the chat view stays within its size.
* fix(mobile): decide a native chat's open question with desktop's selector
The phone decided whether a question call was still awaited with its own
rule (no answer yet and the turn working). Desktop asks the shared
selectActiveToolCall, which also reads the call's state, so a call marked
completed with no output reads "Asked" there and "Awaiting" on the phone.
Native chats now call the same selector. Terminal chats keep their rule.
* fix(mobile): zoom a receipt and its question row with the chat
Before receipts, an answered question or approval was a faint system line
that grew with the chat's pinch zoom. The receipt and the question row drew
at fixed sizes, so zooming no longer enlarged them. Both now take the
chat's zoom, through the same scaledType the notice row uses, which moves
next to the other row styles.
* fix(mobile): list a group's questions once nobody can answer them, and zoom the row's icon
A grouped prompt's row names only a count ("Asked: 2 questions"). Once it
is cancelled, or the agent that asked has ended, the answer card is gone
and the receipt lists no answers, so the phone showed the questions
nowhere. The row now lists them under its line, following desktop's
listsQuestions rule: the receipt asks for the list only when it shows no
answers, and the transcript's own question rows keep it off, as desktop's
tool run does. While the host waits, the card shows them.
The row's icon also grows with the chat's zoom, as the notice row's icon
does, so it stays level with its line.
* refactor(mobile): trim comments and simplify the question row and receipt
Comments that restated the code go. The answer heading takes the subject
its caller already has, dropping two conditions that never changed the
result, on desktop and the phone. The question row lists a group's
questions whenever its caller asks, as desktop's does, and the receipt
asks only while nobody waits. fontScale is required on both new rows.
The previous tab's hidden composer still held focus when the reveal hook ran,
and was treated as a live text field, so the new composer never got focus.
Fixes#28157
* 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.
* fix(mobile): show skill commands you ran in the chat
Claude records a skill run (/simplify, /review-pr 123) as a command
envelope, which the noise filter hides. Desktop surfaces it as the typed
`/name args` before folding; the phone only normalised image markers, so
the turn vanished and the reply answered nothing.
Both apps now prepare a terminal agent's transcript through one shared
prepareNativeChatMessages: skill envelopes, reporting-command output and
image marker turns.
* fix(native-chat): keep a streamed sentence from coming back after a tool or thought row
Both chats hide the live preview once the transcript shows it, but each
only compared the preview with the last row. When a tool call or thought
landed below the sentence, the last row had no words and the earlier
sentence came back at the bottom as a live message.
latestNativeChatReplyText reads back past those rows to the newest
assistant words in the turn; desktop's preview check and the phone's
streaming gate both use it.
* fix(mobile): draw errors, warnings and status notices as notices, not agent replies
The phone drew every system row through the agent's markdown, so an API
retry warning, a sign-in failure, "Context compacted" or "Stopped manually"
read as the agent's own words. Only the clear boundary and two host
notices were special-cased.
Desktop's notice rules move to shared native-chat-notice: which block of a
system row is its notice, and the look it gets (divider, command output,
host status, plan, aside, or a toned line). Desktop's notice row switches
on it with no change in what it draws; the phone's new notice row draws
the same looks, with words in faint type when a row has no notice.
* fix(native-chat): show a new reply that follows a tool call on desktop
The earlier fix read back past a tool call or thought to the newest
assistant words, then kept desktop's length rule. Commentary written
before the tool is often longer than the reply after it, so a new,
shorter reply stayed hidden until the transcript caught up.
The last-row rule is back as it was. Reading back now only hides the
preview when the earlier words already contain it, which is the sentence
still on screen while the tool runs. The phone's gate reads only rows
that landed after the current reply began, so it was not affected.
* fix(mobile): open the actions sheet from a long press on a plan's link
A plan card's Markdown was drawn without the row's long-press handler, so
on Android holding a link or file path inside a plan no longer opened Copy
message / Select text, as it did before plans were drawn as notices. The
notice row now passes the handler to the plan's Markdown.
* 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.
* 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.
* test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times
* 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
* 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.
- Opening a side preview of an already-open document creates no split group (#28939).
- Removing a project clears terminal and editor state under a worktree id that another host also publishes (#28892).
- Quit capture keeps an unmounted leaf that still has a terminal binding in the stored tree (#28882).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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.
* 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.
* test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times
* fix(sidebar): reuse the palette's default host instead of reading the focus setting again
* 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.
* Use Jira favourite filters within their source site
* Keep favourite recovery actions available across host updates
* Bound favourite names within the Jira toolbar
* Fix static gates for Jira favourite filter UI
* Keep Jira fixture storage cleanup in test suites
* test(browser): await loaded guest and own find results
- Nested lineage children now inherit the parent card's group and content offsets through the shared child wrapper, instead of adding their own depth-based padding.
- The legacy card style renders lineage children from the card surface, the same place as the experimental style, so both styles share one layout path.
- Removes `getLineageNestedRowGeometry`, which double-counted the parent's inset for experimental cards and used a different path for legacy cards.
- Titles of nested workspaces no longer drift right with each lineage level, which left them misaligned in grouped sidebars.
* fix(ssh): stop stale saved state from blocking an SSH host's move to a managed server
Leftover records nobody can see or close (empty host tab rows beside the
desktop's older copy, phone tab selections naming missing tabs, agent "seen"
markers, client-hosted pages, isolated-profile browser tabs) now move or stay
behind in the retained profile instead of refusing the move. What still
refuses is live or unsaved work, and the refusal now names each item and how
to clear it.
Fixes#27281Fixes#28946
Co-authored-by: akushonkamen <akushonkamen@163.com>
* fix(ssh): build the export copy without editing saved layout fields in place
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: akushonkamen <akushonkamen@163.com>
A connect that found its own build already active launched nothing, so a
server killed or stopped by a reboot failed on its stale record (#27284).
A stopped server whose recorded port another runtime took failed the
tunnel's identity check before any serving check could start it (#28013).
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: yuzhichang <153784+yuzhichang@users.noreply.github.com>
* fix: avoid blank shells beside new workspace chats
* Carry promised agent chat through worktree creation
* Use live agent launch inventory for new workspace surfaces
* Type live agent launch owner from runtime contract
* Assert one recovery shell after failed agent launch
* Keep worktree activation adapters out of creation flow
* Restore ordinary workspace setup handoff after launch correction
* Cover the phone's agent.launch workspace create in the first-surface flow test
The last reply's controls hang 20px below their row (-mb-5), but the transcript only reserved 16px of bottom padding, so they crowded the composer. Raise the bottom padding from pb-4 to pb-10 so the controls have room.
* refactor(daemon): rename the PTY process ceiling modules to terminal scopes
Pure move ahead of making per-terminal scopes the Linux default.
* fix(linux): run each terminal in its own systemd scope
The daemon and every terminal shared one systemd scope, so one kernel OOM
kill stopped the whole scope, systemd-oomd killed every terminal at once,
and all terminals shared one TasksMax. When the daemon runs in the fresh
scope its launcher made, each terminal now gets its own transient scope
under a slice bound to the daemon's scope, as the opt-in process ceiling
already did. OOMPolicy=continue is used where a probe scope accepts it,
the ceiling becomes TasksMax per terminal, and oomd is asked to avoid the
daemon's scope. The slice's binding replaces the death watch's slice
argument and the exit-time slice stop.
* test(linux): cover per-terminal scopes in the daemon scope Docker harness
Runs on systemd 249, 252 and 257: an OOM kill in one terminal leaves the
daemon and the other terminals running, the process ceiling applies per
terminal, and stopping the daemon scope or SIGKILLing the daemon reaps the
slice and every terminal. The baseline reports today's whole-daemon kill.
* chore: prune the sync-spawn baseline after removing the exit-time slice stop
* fix(linux): read the terminal scope wrappers as no foreground process yet
systemd-run and env own a new terminal until the shell execs, which took up
to ~270 ms per terminal when 50 started at once on Ubuntu 24.04. Reported as
the foreground, they read as running work and could pass the four-poll
child-process readiness gates before the shell existed.
* fix(linux): simplify terminal scope state and keep scopes without pids delegation
One nullable runtime replaces the unread ceiling state; losing the slice
logs once and later terminals stay in the daemon scope. The probe no longer
fails when pids.max is absent, so only the ceiling turns off there. The
resolution is one on/off value, the launch id comes from the daemon scope
alone, and the ManagedOOMPreference request no longer blocks startup. The
foreground filter matches Orca's exact wrapper paths, so a user-run
systemd-run stays visible.
* refactor(linux): name the terminal slice from the daemon scope and start the oomd hint at init
The slice and scope names now come from the daemon scope's own unit name, so
nothing parses its prefix back out. The ManagedOOMPreference request moves
out of the resolver into initializeTerminalScopes, where a spawn error can
no longer land in the resolver's catch after the slice exists.
* test(linux): run the terminal scope wrapper foreground test off Windows only
* test(linux): tolerate an already unloaded scope after the baseline OOM stop
* fix(linux): read Orca's scope wrapper as the shell, and bound startup systemd calls
node-pty names the wrapper path both while it execs the shell and as its
spawn file once the foreground group's leader has exited (true | claude,
seq | less). Hiding it skipped the agent scan; reporting the shell instead
matches what the unwrapped spawn reports. Each startup systemd call now
times out after 1.5 s, so a wedged user manager turns terminal scopes off
inside the launcher's 10 s wait instead of failing the launch.
* fix(linux): mark a timed-out terminal scope setup in the daemon log
A busy user manager and one that refused both reported slice_unavailable.
The log-only off detail now carries timedOut: true for the first; the
reported ceiling reason and the wire are unchanged.
* refactor(ssh): stop the window clearing SSH terminal bindings on disconnect, connect and wake
The window dropped every SSH tab's terminal binding when an SSH host disconnected,
reconnected or the computer woke. Main's rule is the opposite: only a host-confirmed
stop withdraws a binding, because losing contact is not evidence the remote shell
exited. Startup already moves restored SSH bindings out of the tab, so the clear had
nothing left to act on. Delete it, its scope resolver and the wake listener.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ssh): reduce the direct-SSH scope to the worktree ids its one caller reads
Removing the binding clear left terminalWorkspaceKeys and the folder evidence path
with no reader; gitRepos, lineageWorkspaceKeys and the owner counts had none
already. SSH target cleanup only reads the worktree ids, so return just those.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The model pill in the native chat header showed the full effort name, such as "Medium", which crowded the pill. It now uses the same compact label as the effort segments, such as "Med", so the pill fits.
* Bump Electron to 43.7.8 to fix find-in-page looping
Find-in-page could loop when the host page contained iframes beside a
webview, so searches in the guest did not finish or wrap correctly.
Electron 43.7.8 includes the upstream fix (electron/electron#54199).
- Pin electron to 43.7.8 in package.json, pnpm-lock.yaml, and the
Node runtime pin.
- Raise the runtime floor in electron-runtime-floor.test.ts to 43.7.8.
- Add an Electron regression test that runs a find traversal in a
background window with host iframes before, after, and around a
webview, checking forward and backward matches wrap correctly.
* rm junk file
* test(layout): list the window-parity differences that are not bugs separately
The parity test listed five differences between the window's startup view of a stored
session and of the shared Loader's rewrite of it. Three are not bugs: the row terminal
rewrite (the pane binding wins on mount), legacy surface tombstones (the window is the
outlier) and label over title (only phones and the CLI show the row title). They move
to ACCEPTED_DIFFERENCES with a reason each; KNOWN_DIFFERENCES keeps only the ghost tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(layout): pin the values of accepted window-parity differences
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(persistence): one-time profile backup before boot-time saved-data migrations
Adds ensureProfileStatePreMigrationBackup: a validated copy of the profile database,
written next to it as profile-state.db.pre-migration.db, taken once before the first
boot-time rewrite of saved layouts and never replaced afterwards. It returns 'failed'
instead of throwing so the caller skips the migration when the copy cannot be written
(e.g. disk full). Nothing calls it yet; the startup layout migration will.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(persistence): name the lock that keeps one owner per profile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(native-chat): record which agent run wrote each journal item
Each journal item row, subagent roster entry and background-task entry now
records ownerFence: the lease fence of the agent run whose output produced it.
Optional, persisted, no row version bump and no migration. The journal's
itemFence() returns it, so the turn-verdict rules and per-run provider activity
read the producing run rather than the writer's fence.
* feat(native-chat): one current-work projection for every host reader; record death first
The host decides what work is current in one place
(structured-agent-session-current-work.ts) from the lease, the exit this host
saw, the runtimes it replaced, its live child and each item's provenance.
Every host reader (queue, Stop, prompt answers, commands, status feed, idle
sweep, teardown snapshot, history pages and frames, orchestration mailbox)
reads it, so a dead agent's leftovers stop reading Working or holding the queue
even when their cleanup write failed. The journal's activeTurnId()/liveTurnScope()
become the raw runningTurn().
On exit the host records the child's end first and signals generationEnded,
which republishes frames, the status feed and settlement waits and wakes the
queue drain with no journal row. Pages and frames carry optional working and
actionablePromptIds fields.
* feat(native-chat): clients read the host's working answer; refuse answers to a stopped agent plainly
Desktop and mobile prefer the host's working and actionablePromptIds fields
when present (merged across batches like latestTurn) and fall back to today's
row derivation against an older host. A prompt an ended agent raised stops
looking active; answering it is refused before any provider sees it, with
"<Agent> has stopped, so your answer was not sent. Send a message to continue."
Cancelling it only dismisses it in the journal: it no longer triggers the
chat's Stop, pauses the queue or withdraws queued cards.
* refactor(native-chat): keep structuredAgentSessionHandsOver file-local until a caller needs it
Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row.
* test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case
Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one).
* test(native-chat): drop current-work mirrors: the viewKey unit case and the still-live rewind case
The flow cases keep every outcome: dead leftovers read as nothing, Stop has nothing to stop, a waiting card drains, a live owner publishes as before, and a rewind never promotes dead work.
* test(native-chat): fewer client tests: one flow per client, one fallback each
Keeps desktop and phone Working from the host (with the older-host fallback), the reducer merge contract, the ended question Cancel that only dismisses, the agent-named refusal and the resolution receipt. Drops per-helper prompt tests, the transcript-placement test, the stop-note client reduction, the Steer cards that match main, and per-reason refusal rows.
* test(native-chat): an unstamped roster entry from an older build reads as its row provenance
Moved here from Part 4 so it sits beside stampJournalEntryProvenance.
* test(native-chat): build the older-build roster row inline, after main removed its body helper
Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads.
* refactor(native-chat): provenance is per item only; drop per-entry stamping
Main's #27947 writes one saved row per subagent, and each background task already has its own row,
so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps
inside a row mattered only for the shared roster rows saved before #27947, and no run writes into
those any more, so the row's own provenance settles them correctly. Removes the entry stamping,
the optional ownerFence on roster entries and background tasks, and their tests.
* fix(native-chat): an item's provenance is set when it is created; revisions keep it
A newer run's provider can revise an item an older run created: Pi's start and an ACP
session/load both report context usage onto the newest saved turn. Restamping that turn with
the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted.
Only creation states an owner now, as main's itemFences rule did.
* test(native-chat): keep the provenance test below the production lines it covers
* fix(native-chat): an ended agent's card Cancel commits with its receipt, through dismissPrompt
The ended-generation dismissal wrote the cancelled resolution without the Cancel's operation
receipt and without the dismissal's failure handling, so a receipt write failing after it would
answer "outcome unknown" over a card already closed. It now goes through dismissPrompt, which
carries the uncommitted receipt in the same append and answers dismissalNotSaved on failure; a
null `answer` skips the adapter, since no live provider holds an ended generation's card (Pi's
adapter refuses a fence with no live session outright).
* fix(browser): settle client-hosted download toasts after their page closes
The download progress toast for a page this desktop hosts for a remote
workspace was driven by a listener inside that page's pane. Switching
pages or closing the tab unmounted the listener mid-download, so the
finish never reached the toast and its spinner (no close button, no
timeout) stayed for the session. An interrupted download had the same
result because Chromium sends no finish for it.
Own the toasts from one app-level listener that claims a download when
it starts, and end the spinner on an interrupt.
* test(browser): type the store stub without an assertion
* fix(browser): wait for download finish before settling notice
* fix(browser): end downloads that are waiting for a manual resume Orca never offers
* test(browser): drop type assertions from the manual-resume download test
* test(browser): focus manual-resume interruption regression
* refactor(agent-launch): a paired-workspace launch stops closing local agent tabs
The prune existed because a local tab in a paired workspace once could not be closed (its close went
only to the host). Today a local id closes locally and the host close no-ops, and every present-day
launch into a paired workspace creates its tab through the host, under a host-surface id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(agent-launch): say how a local tab in a paired workspace closes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Align the release channel strings in es, fr, ja, ko, and zh with the
English source, which now says "version" where the translations said
"build". Reword the warnings, the downgrade note, and the install hint so
they state the actual risk. Also reword the chat input placeholder, which
used "mention" for `@` file references.
* fix(browser): agent typing into a cross-site frame no longer borrows the user's focus
Keys and text for a focused cross-site iframe went through the focus-borrowing
fallback, because widget input reaches only the guest's own renderer. They now
go to that frame's own CDP target session, found by following the page's focus
through frame elements, which reaches the frame without touching the user's
focus. The proxy keeps those Orca-owned sessions' events from the agent's client.
* fix(browser): retry frame focus emulation after a failed enable
* fix(browser): retry a failed frame focus-emulation enable before sending input