* feat(session-history): resume a native chat's conversation in a new CLI session
Agent Session History offered "Resume in New Chat" to move a terminal session
into native chat, but nothing went the other way: once a native chat owned a
conversation, its row only reopened the chat, and Orca refused any terminal
resume of it so two programs never write one conversation.
Rows a native chat owns (Claude, Codex) now offer "Resume in New CLI". It forks:
the terminal runs `claude --resume <id> --fork-session` or `codex fork <id>`,
so the CLI gets the full history under a new id and the chat is unchanged. The
host's resume guard lets a Claude fork through, and the fork tab records no
provider session, so a restart never re-enters the chat's conversation.
"Resume in New Chat" is renamed "Resume in New Native Chat".
* fix(session-history): size the Resume in New CLI tooltip to its text
The tooltip balanced one wrapped string inside a box sized before balancing,
leaving a gap beside its lines. It is now two block lines with no fixed width.
* fix(session-history): run a CLI fork under the selected Codex account and tighten the fork guard
- Resume in New CLI now asks the host for the same Codex account home plain
Resume uses, so a fork no longer starts under whichever account's copy of the
rollout sorted first. The preparation call carries an optional `fork` flag
that skips only the owned-conversation refusal; plain resume still refuses.
- An older paired host that refuses the fork now shows "Update Orca on the host
that runs this chat to resume it in a new CLI." instead of a raw error code.
- The host guard admits `--fork-session` only before a `--` terminator and
never alongside `--session-id`, since either shape lets Claude write the
owned conversation.
- Tests for the click path, the menu item's eligibility, the fork home
preparation, and the tightened guard.
* fix(session-history): read a Codex rollout's identity from its first session record
A fork of an older (non-paginated) Codex thread writes its own session record
first and then copies the parent's history, the parent's session record
included. The history scanner kept the last one it saw, so the fork's row took
the parent's id, title, folder and branch: on a native chat's fork it looked
like the chat itself, and Resume on it opened the chat instead of the fork.
Codex itself treats the first session record as the thread's own and ignores
later ones; the scanner now does the same, as Orca's duplicate-rollout check
already did.
* fix(session-history): set up status hooks in the Codex home a CLI fork keeps
When Resume in New CLI can't move a Codex fork to the selected account (that
account has no copy of the conversation yet, or the system default is
selected), the fork runs in the account home the history row came from. A new
Codex tab only gets Orca's status hooks in the selected account's home, and the
fork carries no provider session, so nothing set up the hooks in that row home.
In a home only native chat had used, the fork tab could show no working or
idle status.
The fork's home preparation now readies that home the same way a resumed
session's home is readied (hook install or refresh, plus the daemon-socket
guard). That code is moved into prepareCodexPinnedLaunchHome and shared by both
paths. The fork tab still carries no provider session, so it never reopens the
chat's conversation on restart.
* fix(session-history): ready only a host-verified Codex home for a CLI fork
The fork's hook setup ran in whatever Codex home the request named, so a paired
client could have Orca write hook and config files into any directory. Plain
Resume only readies a home it has verified: one this host lists for Codex
sessions that actually holds the rollout. The fork now uses that same check,
through a shared trustedCodexResumeHomes list, and readies the home only when
the verified home is the one the fork will run in. With no Codex runtime home
service it does nothing, as plain Resume does. A failed check or setup is
logged and never stops the fork from launching.
Tests cover the untrusted home, a missing runtime home service, a rollout on
another host, a WSL home, a fork moved into the real home, and a failed setup.
* fix(session-history): show the host-update message when the desktop wraps the fork refusal
When the local preparation step for Resume in New CLI is refused, Electron
hands the renderer the error as "Error invoking remote method '…': Error:
agent_session_conflict". The fork's failure text matched only the bare code,
so the toast showed that raw line instead of asking the user to update Orca on
the host. It now unwraps the message with the shared IPC error helper before
matching.
* fix(native-chat): disable Send for unavailable agents
* test: update sign-in copy expectations
* fix: show automatic chat availability only on Send
* fix: keep signed-out evidence when a live session lists models
* refactor: split Send availability presentation and tests
* refactor(native-chat): keep the sign-in verdict on the catalog's failure record
The probe's "signed out" / "CLI missing" verdict now lives on the account's
existing model-catalog failure record instead of two extra maps, with the
failure TTL as its only lifetime:
- Only the probe sets or clears the verdict; a chat's own model listing
(which works while signed out) never clears or renews it. An aged-out
verdict makes the next read re-probe, however fresh the catalog.
- A healthy fresh catalog is no longer re-probed every 30 s; it keeps the
normal 10 min fresh window. A Claude start refused as signed out ages the
catalog so the next read re-probes.
- A waiting picker again answers from whichever catalog lands first and never
waits on a probe. A read that still owes the probe's verdict says so, and the
composer waits for it once with the new optional waitForAvailability param.
- The open-time start-failure line and its transcript row are hidden only
while Send is disabled for the same reason; otherwise they show as before.
- A host verdict lifetime longer than the client's hold is clamped, not
dropped. The catalog read watches only the account settings and the paired
host's contact instead of every store write.
* fix(native-chat): one send-gate rule, and a missing CLI as its own failure
- The sign-in/CLI verdict is gated once where the session hands it to the
composer: while a turn runs it neither shows nor refuses, so a follow-up
queues as before; goal mode is held by the same gate. The start-failure line
and row hide only while that same value is up.
- A start whose CLI was never found (a direct spawn's ENOENT, or the one a
provider supervisor reports) is now a `cliMissing` failure with the disabled
Send's sentence, instead of a generic "couldn't start" beside it. An older
client shows the host's sentence for the new kind.
- Signed-out words for a slash command keep "Run /x again".
- The Claude catalog reads its models only from the reply to its own
list_models request, never from the initialize reply.
- The Codex probe lists models while its account check runs; a signed-out
verdict still wins.
- The missing-executable check moves to provider-process, out of native chat.
* test(native-chat): signed-out command failures name the command to run again
* fix(native-chat): probe the Codex home a launch would read
- Before the catalog probe's read-only app-server reads Orca's shared Codex
home, it runs the same sync a launch and the usage poll run, so a
`codex login` made in ~/.codex is seen at once instead of leaving
"Run codex login" up until something else re-syncs (forever on a headless
host). Per-account homes are left alone.
- A "no account" answer counts as signed out only when the home's auth.json is
absent or holds no credential; one caught mid-write by a token refresh, or
holding a credential, is unknown.
* test(codex-accounts): type the node:os mock with a type import
* fix(native-chat): keep the sign-in verdict until a newer answer replaces it
- No gap while signed out: the host serves an aged verdict while the probe that
re-derives it runs, and the composer never clears its last answer before a
read; only an answer without a verdict, an error, lost contact or an account
change does. Send stays disabled with its reason through every re-check.
- After sign-in the stale "isn't signed in" start-failure line stays hidden:
a probe that found the account fine now says so (`accountVerified`), and the
line shows only when the host has no answer at all.
- A Claude start refused as signed out, or any start whose CLI was never
found, is recorded as that home's verdict, and the chat reads it once when
that start failure arrives.
- The re-probe mark is read only where the catalog read starts its probe, so a
Codex chat's own picker no longer re-lists for it on every read.
- Choosing an account or signing in again in Accounts settings re-derives the
verdict at the next read; the account key now compares settings by value.
- A re-read that brings an identical catalog keeps the option state, so focus
re-reads re-render nothing.
* fix(native-chat): Codex's signed-out words name only the step that unblocks this chat
A chat is pinned to the Codex home it started in, so choosing another account
in Codex Accounts settings never unblocks it. The system-account sentence now
says only to run `codex login`, in every locale.
* refactor(native-chat): give an account's sign-in status its own owner
The sign-in verdict lived inside the model catalog's failure records, so
the store needed guards for who wrote a failure, which listing may clear
it, and whether a verdict was served while re-probing. Each guard fixed one
way a chat's own listing or an older probe overwrote the account's answer.
The answer now lives in AgentAccountStatuses: one in-memory, capped status
per account home (ready, not signed in, or CLI missing). Only the
session-less probe, a refused start and an account change write it. A
blocked answer older than 30 s, or one an account change marked, makes the
next read start the probe whatever else is listing, and it is served until
that probe answers. Older evidence never replaces newer. The catalog's
failure code is main's again, and agent-model-catalog-failures.ts is gone.
The wire carries one optional `availability` field in place of
`unavailable` and `accountVerified`. The renderer holds its answer per
host, agent and chat, re-reads only on that agent's account inputs, and
re-reads on a timer only while blocked. After an account change, Send
stays disabled with the previous words until the re-probe answers, then
changes once. Account switches and re-sign-ins mark the status before the
settings change a chat's read follows.
* fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked
Round-4 review findings on the account status owner:
- An account switch or re-sign-in made while a probe was running left the
old answer up for up to 30 s: the next read joined that probe, whose
answer was then dropped as older than the change. A read now chains
exactly one fresh probe after a probe that began before the change.
- Removing the selected Claude account swapped another sign-in into the
shared config dir without marking the answer; it now marks it first.
- Evidence was ordered by wall-clock time, so a clock that stepped back
froze a blocked answer. It is now ordered by a sequence and aged on a
monotonic clock; a negative age reads as due.
- A chat left open while signed out started a CLI probe every 30 s.
Probes that keep finding the same block now space out (30 s, 60 s,
2 min, 4 min, then every 5 min), carried to the client as recheckInMs.
An account change or a different refusal starts the hold over.
- The renderer dropped no settled wait from its leave set; it now does.
- A failed Claude probe logged its whole stdout, which now includes the
initialize reply naming the account. Control replies are dropped from
the log and the failure message.
- The account services take the status re-check as an injected
onSignInChanged hook instead of importing the native-chat store.
- Imports moved below file headers; comments no longer say "ready"
means re-checked after the failure.
* test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added
* fix(native-chat): type the held-answer handle as the account status
* fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe
Round-5 review findings:
- After a chat sat blocked for a few minutes, the backoff made every
read wait out a hold of up to 5 minutes, so someone who ran `/login`
or `codex login` and came back still saw Send disabled for minutes.
The backoff now applies only to the client's own timer read, which
it marks with an optional `scheduledRecheck` param. A read from
someone looking (window focus, the pane becoming visible, a new chat,
a start failure) re-probes once the block is older than 30 s. The
param is sent only on that timer, which only a blocked `availability`
answer arms, so a host that predates the param never receives it.
- An account change made during a home's first probe marked nothing,
because only existing statuses were marked, so the old account's
answer was written as current. Each agent's last account change is
now one sequence number that every status and running probe is
compared against; statuses are no longer mutated in place.
- Removing an account that nothing had selected no longer re-checks.
* refactor(native-chat): keep the sign-in and missing-CLI words in their own module
Main's retry wording pushed the failure-words table past its line budget.
* refactor(native-chat): build the structured chat's view session in the view-state module
Main's send-reveal work brought NativeChatStructuredSession.tsx and the
session controller to their line budgets.
* fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe
- A chat whose open-time start failed signed out had that line hidden
while the host said the same. When a new signed-out answer landed
during a running turn, the line came back for the whole turn: the
hide read Send's gate, which a running turn suspends. The hide now
reads the host's answer itself (blocked for the same reason, or
ready); Send's gate still steps aside while a turn runs. Both are
views of the one answer the session holds.
- An account change during a home's first probe dropped that probe's
answer but left nothing to re-check the new account, and the picker's
reply said no probe was pending, so the chat stayed unknown until the
catalog aged out. A read now chains one fresh probe whenever the
running probe began before the change, and its reply says that probe
is still owed so the client asks once more for its verdict.
- The client's timer read no longer carries `scheduledRecheck`. The
timer already fires only when the host's backed-off `recheckInMs`
says, which is past the 30 s after which any read re-probes, and
every answer re-arms it. One fewer strict-schema param.
* build(cli): typecheck the missing-executable check the provider process now imports
Main's #25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.
* fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing
What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.
How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.
* fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send
A chat whose account is signed out and has no model list no longer shows the model picker as
loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that
already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard
focus (so its tooltip opens) without becoming a second button; the inner button keeps its label.
Trims tests that repeated rules other tests already prove.
* fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send
The composer shows Stop while the host's queue is about to send its next card, but the sign-in
gate stayed on in that window, so Enter was refused with no message.
* fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead
The host's pre-send check can be wrong while the chat would work (saved
Arguments it never sees, project settings it can't read, gateways that
accept a signed-out Codex), and a disabled Send left no way past it.
- Delete the Send gate: the composer, Enter and goal mode no longer read
the verdict, and the Send tooltip reason and focusable wrapper go back
to main.
- Show Codex not-signed-in and Claude/Codex not-installed as one entry in
the chat's existing notice card. Dismissed per verdict; shows again
when the verdict changes or clears and returns; steps aside when the
failed start (launch line or latest failed-start row) says the same.
- Claude: drop the probe's signed-out verdict (initialize-on-probe stdin,
request-id filtering, control-reply log scrubbing). The chat start's
refusal, with its account kind, is the only Claude sign-in signal.
Missing-CLI from the probe stays.
- Restore the failed-start line and transcript row for these causes
(no more hiding on desktop) and drop the chat-root focus re-check; a
window-focus re-read stays while a notice is showing.
* fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list
Review round 1 of the notice rework.
- A chat that proves its start (at acquire, or on its later "started"
event) marks the catalog's held reason for that account due, so the
next read re-probes instead of trusting it until the TTL; the chat
re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
Codex-only; the say-once rule ignores a failed start a turn has run
past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
chat. Install it, or check its Command in Settings → Agents." (6
locales).
* fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter
Review round 2 of the notice rework.
- A re-attach to a live child proved nothing new, so only an attach that
acquired a child which proved its start marks the reason due.
- Reword: a start proves the program exists; for a Codex sign-in it only
prompts an earlier re-check.
- Drop the notice's Claude signed-out filter: no host sends that verdict.
- Tests: a re-attach does not notify; a turn ending re-reads once more.
* fix(native-chat): never newly show a catalog reason the host is still re-checking
After signing in to Codex, a new chat flashed the old "isn't signed in"
notice for about half a second: past its 30 s lifetime the host answers a
plain read with the held reason while its probe re-checks. A chat now
adopts such a reason only when it already shows it; otherwise the joined
read's answer decides.
* test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge
* test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests
* Give Windows Electron writer probes a larger test budget
* Drain queued writer replies across two poll turns before timing out
* Check writer shutdown after both reply-drain turns
* Add inline always-force-delete preference to workspace recovery toast
* Use split-button menu for saved force-delete preference
* Await durable force-delete preference writes before deleting
* Restore absent settings keys after failed durable writes
* Place deletion translations beside worktree copy
* Give Windows Electron writer probes a larger test budget
* Drain queued writer replies across two poll turns before timing out
* Check writer shutdown after both reply-drain turns
* feat(file-drop): add element owner preparation plumbing
* Fix terminal and chat file drop destination ownership
* fix runtime terminal drop ownership and queued chat retries
* fix(file-drop): preserve feedback acceptance and destination ordering
* fix(file-drop): attach chat and composer files at the drop surface
* fix(file-drop): preserve project destinations and live chat availability
* fix(file-drop): keep path resolution in filesystem namespace
* test(file-drop): use filesystem path bridge in PR3 fixtures
* fix(file-drop): bind terminal drops to pane elements
* fix(file-drop): compare terminal pane identity across public views
* test(file-drop): align composer lifecycle with element-owned drops
* Fix interrupted chat composition and refuse unused quick-create drops
* Type the quick-create drop regression transfer
* fix(file-drop): let the explorer, project sidebar, tab strip and editor own OS drops
STA-6940 PR5. The file explorer tree, the project sidebar, each editor
group's tab strip and its editor area now take OS file drops on their own
element through the shared owner hook, with identity from their own render
(the explorer's shown workspace and target row, the tab strip's and editor
group's worktree and group) instead of the active worktree. The explorer and
sidebar broadcast subscribers and their legacy drop markers are deleted;
editor-open moves out of useGlobalFileDrop into editor-dropped-file-open,
which the legacy unmarked-chrome route still uses until PR6.
* fix(file-drop): keep editor delivery tied to its destination group
* fix(file-drop): guard delayed opens and share floating ownership
* fix(file-drop): remove legacy OS drop routing (STA-6940 PR6/6)
* test(file-drop): finish owner test and comment cleanup
* Clarify dropped-file default editor destination
* Retain UI test namespace for terminal resume coverage
* feat(omp): open OMP in the structured chat over ACP
OMP joins Grok and OpenCode on the ACP lane: `omp acp`, with the user's
environment passed through and the chat pinned to PI_CODING_AGENT_DIR
(default ~/.omp/agent), the directory OMP's terminal chats read. A create
and every launch ask `omp --version` of the binary the Command setting
resolves to; stable releases from 17.0.5 run the chat, older ones keep the
terminal-backed chat. Like every ACP agent it runs only with the updated
structured native chat setting on.
* fix(omp): show a shell command's output and exit code in its row
OMP's ACP server sends a command's result as rawOutput {content, details}
with a non-zero exit at details.exitCode, and repeats it as content behind
a "$ <command>" echo. The shared reader takes content text first, so the
row's output began with the command and carried no exit code. The OMP
dialect drops the echo, moves the result text to stdout without OMP's
"Wall time" and "Command exited with code N" notice lines, and maps
details.exitCode. Recordings of omp acp 17.0.5 (exit 3, exit 0, Stop
mid-command) replay through the adapter.
* feat(opencode): run OpenCode 2.x in the structured chat too
`opencode acp` on stable 2.x starts its own private `opencode serve --stdio`
child with the chat's environment and ends it when stdin closes, so the
chat's account pin reaches it just as it does on 1.x. Admit stable 2.x from
2.0.14 beside stable 1.x from 1.18.31; pre-releases, older releases, and
other major lines keep the terminal chat. The `opencode2` agent is unchanged.
Restart recovery already reads a 2.x session's own tables first; tests now
cover a database an upgrade left with both generations, and one whose 2.x
message table has an unknown shape (nothing found, no error).
* fix(opencode): show OpenCode's own permission option names
* test(opencode): run the 2.x recovery tests with the real-SQLite Node project
* feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card
The resume-on-restart dialog listed workspaces as a bare name and glyph, so users could not
tell their sessions apart. It now renders each workspace with the sidebar's own WorktreeCard
in a new read-only mode, with the offered chats in place of its live agent rows, and nests
child workspaces under a listed parent the way the sidebar does. The dialog is wider and the
chat rows use the sidebar's compact agent row layout.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(native-chat): name the nesting helper for what it returns
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(native-chat): resume dialog cards show no status
The dialog's cards stand in for the chats before the restart, so the sidebar's live status
(Failed, now Interrupted) read as a verdict on chats the dialog offers to resume. A read-only
card shows no status, and in the new card style its PR joins the badges because the status
lane that normally holds it is gone.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(sidebar): a read-only card is inert and shows no live state, whatever its caller passes
A read-only card stayed inert only because each control checked `readOnly` on its own and the
caller left out the handlers that would turn the rest on: passing `onLineageToggle` put a live
child-workspaces toggle on it. It also still painted live state over the resume dialog's chat
rows: the sleep dim, the "Queued for deletion" overlay (which blocked unticking a pre-ticked
chat) and the prompt-cache countdown.
WorktreeCard now reduces a read-only card's props to what it pictures (every handler, selection
and active flag dropped), and the controller resolves `readOnly` once into what the views
already read: card properties without status, ports or live agents (and with the host), no
sleep or delete state, no cache countdown, and one `interactive` flag for its own controls.
A contract test renders a read-only card with every feature and handler on in all three card
styles and fails on any control outside the caller's rows or any live state.
* fix(native-chat): nest and indent the resume dialog's child workspaces as the sidebar does
A child workspace whose parent has older metadata with no host id stayed at the top of the
dialog, though the sidebar nests it: the dialog copied a strict same-host filter, while the
sidebar treats a hostless row as belonging to the host. The nesting rule now lives in one
sidebar lineage helper, getWorktreeLineageAncestorsOnHost, which scopes by host the way the
sidebar's lineage inputs do.
In the legacy card style a nested child was inset for its parent's depth instead of its own,
so it sat one sidebar step too far left (0px instead of 14px), outdented into the parent's
padding. Each child is now inset for its own depth, as a sidebar row is.
A groups-level test renders real sidebar cards from the store, with the child nested under a
hostless parent, and checks every offered chat's checkbox renders and the legacy inset.
* fix(native-chat): the resume dialog places every workspace even if lineage loops
Nesting followed each workspace's parent chain, so two workspaces listed as each other's
ancestors (possible only when two hosts report opposite lineage for the same ids) were placed
under no top-level entry and vanished, while their pre-ticked chats would still be resumed.
The loop is now cut where the walk first comes back, so every workspace renders.
* fix(native-chat): keep "Don't ask again" above the resume dialog's buttons when narrow
Below the small breakpoint the footer stacks bottom-up, so the option moved into it rendered
under Resume and Dismiss all. It now reads top to bottom: the option, then the actions; wider
windows keep the single row.
* test(sidebar): import the card statically in the read-only contract test
Its dynamic import ran in a beforeAll with a 20 s timeout, below the suite's 60 s hook timeout,
and timed out under parallel load, which skipped all six contract tests.
* fix(sidebar): legacy lineage children step in as far without a status lane
Legacy cards outdent their nested child list by 1.125rem to pull it back over the 22px status
lane. A card without that lane (a read-only card in the resume dialog, or the sidebar with the
Status property off) kept the outdent, so a child's title sat 3px right of its parent's instead
of 25px. Without a lane the list now sits at ml-1, which gives the same 25px step (39px at depth
2). Cards with a status lane, the default sidebar, render exactly as before.
* fix(native-chat): nest resume dialog children by the sidebar's own row rule
Round one nested a child under a parent whenever either had no host id, but the sidebar's row
builder looks a parent up on the child's own host, so a row with no host id nests only under a
parent with none either. The dialog showed a parent/child pair nested that the sidebar shows side
by side.
The row builder's parent lookup is now one function, getSidebarLineageParent, and the dialog
walks ancestors with it (getSidebarLineageAncestors) over the same non-archived rows and cycle
set. The chat's workspace is still found on its host, falling back to a row with no host id.
A test checks the four host pairings against the real row builder, and the dialog test renders
each pairing with every chat's checkbox.
* test(sidebar): pin child-workspace indent with and without a status lane
Covers legacy, compact and new card styles with the Status property on and off. With Status on,
the legacy and compact sidebar keep their outdent unchanged; with it off they now step children
in by the same amount; the new card style is identical either way. The file now imports the
card statically, since its 15 s per-test dynamic import timed out under load.
* fix(sidebar): a read-only card's child workspaces step in as the sidebar's do
The legacy outdent on a card's child list cancels its status lane, which in the sidebar is the
unread button plus the row gap: 1.125rem, exactly the outdent, not the 22px the previous fix
assumed. Its ml-1 for a card with no lane put the resume dialog's children 4px deeper per level
than the sidebar's (25px against 21px). With no lane there is now nothing to cancel and no
margin, which gives the sidebar's 21px (35px at depth 2).
Status is a fixed card property, so every live card has the lane: only a read-only card takes
this branch, and the live sidebar is unchanged. The tests now pin the outdent for live legacy
and compact cards and no margin for a read-only one, instead of an unreachable Status-off state.
* test(sidebar): a read-only card never carries main's multi-selected marker
Main now styles a multi-selected card through data-worktree-card-selected. The contract test
passes isMultiSelected, so it now asserts the marker stays off a read-only card, and drops the
context-menu event constant main removed from that module.
* fix(sidebar): preserve quiet read-only card geometry
* fix(native-chat): match resume cards to sidebar rows
* fix(sidebar): preserve passive SSH identity on read-only cards
* fix(native-chat): keep resume focus and folder host identity
* fix(native-chat): remove resume dialog list border
* fix(sidebar): keep passive SSH pills static
* fix(native-chat): align resume card vertical spacing
* fix(native-chat): preserve cache-only card header spacing
* fix(native-chat): lay the resume dialog out as a checkbox-column list
Every row of the resume list (Select all, workspace, chat) now keeps its
checkbox in one left column; nesting indents only the content after it,
and a divider separates every row. A workspace checkbox is tri-state over
its own chats and those of the workspaces nested under it; Select all
heads the list with an "N of M selected" count. Failures a retry cannot
fix stay out of both. Up/Down move between the list's checkboxes.
The dialog no longer renders the sidebar's WorktreeCard, so the read-only
card mode and the sidebar changes that only served it are reverted to
main. The sidebar lineage helpers stay: the dialog still nests child
workspaces by the sidebar's own rule.
* fix(native-chat): name SSH hosts by their saved label in the resume dialog
The workspace row's host chip spelled an SSH host with its raw target id
(e.g. ssh-1728291234567-abc12d) because it never got the sidebar's host
labels. It now takes them from useSidebarHostScopeOptions, as the delete
dialog does, so the chip reads "devbox" like the sidebar.
Also translates the dialog's five new strings in es, fr, ja, ko and zh.
* fix(native-chat): group the resume list in bands and let a project select its chats
The resume list now follows the common grouped-list pattern: each
workspace heads its chats with a full-width tinted band (lighter for a
nested child, at its own indent), chats are split by plain hairlines, and
groups meet without gaps or rounded row corners.
The project row gets a checkbox in the same left column, tri-state over
every eligible chat in the project (nested workspaces included, failures
a retry cannot fix left out) with an "x of y" count, on a band a step
stronger than a workspace's.
The list takes arrow keys, so it is now a named group (role="group").
* fix(native-chat): no project row for workspaces the resume list cannot place
Workspaces the store cannot place in a repo were grouped under a header
with an empty name, so the new project checkbox was announced as "Select
all chats in ". Main and the earlier heads showed that header as a bare
folder glyph with no name, and there is no id to fall back to. Those
workspaces now get no project row; their own rows and Select all still
cover their chats.
* fix(native-chat): lay the resume list out as nested workspace boxes
Brennan chose the nested-box layout without row dividers. A project is a
tinted band; each workspace is a bordered, rounded box one step in from
its project, holding its header band and its chats, and a child
workspace's box sits inside its parent's, after the parent's chats, at
the parent's chat indent. The boxes are drawn in the content area only,
so every checkbox stays in the one left column outside them. Rows carry
no dividers; the box border is the only line.
* fix(native-chat): show the resume list as a tree
Brennan chose the Resume Tree. The list is now a tree: machine, then
project, then workspace (a child workspace is a node inside its parent,
after the parent's chats), then the chats. Every row keeps its checkbox
in one left column, then an 18px indent per level, a disclosure arrow
(an empty slot on a leaf), the icon and label, and its count. Levels read
apart by icon and weight; there are no boxes, bands or row dividers, only
the divider under Select all.
The machine level shows only when the machine is not obvious (a remote
host, or chats on more than one), named with the sidebar's host labels
and an SSH chip; it replaces the per-workspace host chip. Chats are
grouped per machine first, so one workspace id on two hosts stays two
nodes.
Every node is tri-state over the eligible chats under it, through the
existing per-chat onToggle. Nodes start expanded; collapsing is local to
this opening and keeps the selection. The list is a role="tree" of
leveled treeitems: Up/Down move between checkboxes, Left collapses and
Right expands the focused node.
* refactor(native-chat): read each resume chat through one key helper
Every per-chat read in the resume tree (its tick, its toggle, its
failure, and each group's coverage) now goes through chatState, so a
later change of chat key stays in one function. Tests pin the machine
level's host rules: a runtime host gets a machine node but no SSH chip.
* fix(native-chat): make the resume tree's keys work from its Tab stop and after a click
Tab lands on the tree itself, where the arrow keys did nothing. Down and
Home now enter at the first checkbox, Up and End at the last; Home and End
also jump to the ends from a row.
Clicking an arrow with the mouse left focus on the arrow, where no tree
key works. After a collapse or expand click, focus now moves to that
row's checkbox, or to the tree when the checkbox is disabled.
---------
Co-authored-by: Claude <noreply@anthropic.com>