Commit Graph
14785 Commits
Author SHA1 Message Date
Jinwoo Hong fa962dd1cd Record chat tab opens and closes in main (dark, layout-owner build only) (#30019)
* Record chat tab opens, closes and host-listed chats in main (dark, layout-owner build only)

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

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

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

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

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

Drops the per-sender drawn flag. The windowless save passes its
remembered active group only when the workspace still holds it, so it
mints a fresh group as before. Refusing a taken chat tab id is gated on
the build switch: shipped builds still save the duplicate id.
2026-10-17 00:57:12 -07:00
Jinwoo Hong 6c7ed18e20 feat(sidebar): main publishes desktop focus, pins, project and host order, and SSH/WSL host state, checked against the window in dev and test (#30018)
* feat(sidebar): main publishes desktop focus, pins, project and host order, and SSH/WSL host state, checked against the window in dev and test

* fix(sidebar): read pins through the host-filtered metadata helper; name pins as workspaces
2026-10-17 00:51:57 -07:00
Brennan Benson 251516de60 feat(native-chat): choose which agents open as a chat (#29071)
* feat(native-chat): choose which agents open as a chat

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

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

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

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

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

* test(chat): use checked native inventory fixtures

* Fix launch preparation lifetime and execution host startup

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

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

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

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

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

* fix: preserve provider choice and cached native inventory
2026-10-17 00:51:43 -07:00
Neil 8f2810ab23 fix(ssh): link a folder workspace the server kept when a re-created SSH host moves again (#27285) (#30017)
* fix(ssh): link a folder workspace the server kept from an earlier move instead of conflicting (#27285)

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

* fix(ssh): name a linked folder workspace in the connect's left-behind warning
2026-10-17 00:50:27 -07:00
Neil 824ca9545f fix(ssh): a host converted to its managed server keeps its workspaces' visit recency (#30015)
Converting an SSH host drops its relay-era rows, but recency stayed under
ssh:<target>|<id> keys, which the next startup prune discards because no row
matches them any more. Re-key that host's visits to runtime:<env>|<id> in the
same update, keeping the newer visit on a collision.
2026-10-17 00:42:34 -07:00
Brennan Benson 0f9c12db17 Chat: pasting a copied file attaches it, like dropping it (#29089)
* fix(native-chat): paste a copied file into Chat the way dropping it attaches it

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

Fixes STA-10652

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

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

* Keep copied attachments pending through preparation and composition

* Update file drop fixture settlement results

* Keep pending attachment capability current within its draft scope

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

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

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

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

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

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

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

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

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

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

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

The paste tests outgrew the test file length limit.

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

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

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

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

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

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

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

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

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

Drives the real pending cache, composer attachments, external intake and paste
hook with a delayed clipboard answer: removed chips never attach (before or
after the answer, through input-method composition), an unmount finishes both
the local and paired-server routes into the original draft with the original
chip IDs, and disabling the composer meanwhile does not bring the paste back.
2026-10-17 00:38:42 -07:00
Neil ee56703d51 chore(ci): ratchet importers of the deprecated runtime-owner resolvers (#30002)
* chore(ci): ratchet renderer importers of the deprecated runtime-owner resolvers

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

* ci: register the deprecated runtime-owner ratchet step in the preflight phase map
2026-10-17 00:38:41 -07:00
Neil 83baa08352 fix(cli): explain why file open fails on a server with no app window (#30014)
On a headless server (orca serve, orcad) files.open and files.openDiff
answer with the bare renderer_unavailable token, which the CLI printed
as-is. Translate it in the CLI into a clear message with next steps,
keeping a stable renderer_unavailable code. The wire answer is
unchanged because the mobile app keys its preview fallback on it.

Fixes #29822
2026-10-17 00:37:16 -07:00
Neil e1a7c9725d fix(cli): stop suggesting --host to listings that reject it (#30012)
The hostScope annotation on terminal list, worktree list and worktree ps
named --host local / --host ssh:<id> as the follow-up for an omitted host,
but none of those commands accept --host. Those hosts are reached through
the runtime that answered, so no flag selects them: report a null selector
and say so in the human scope line. Paired servers keep --environment.

Fixes #29901
2026-10-17 00:27:55 -07:00
Jinwoo Hong 95580d9dd8 test(e2e): open and close structured chat tabs with an isolated Claude stand-in (#30011)
* test(e2e): open and close structured chat tabs through an isolated Claude stub

* test(e2e): satisfy changed-code lint in the structured chat stub
2026-10-17 00:25:36 -07:00
Neil 57e8c604f9 fix(renderer): resolveOwner refuses folder and project-group twins instead of guessing this computer (#30009)
* fix(renderer): resolveOwner keeps folder and project-group twins ambiguous; deprecate the runtime-owner aliases

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

* fix review: a folder's own SSH connection names its owner despite twin project groups
2026-10-17 00:22:15 -07:00
Neil 29fa3cf9fb fix(terminal): quit no longer warns about terminals a paired Orca server keeps running (#30006)
A paired runtime (orca serve, managed SSH/WSL orcad) owns its terminals and keeps
them after this client quits, so only SSH relay terminals, which a relay kills
after its grace period, still raise the quit warning. Window close is unchanged.

Fixes #29916
2026-10-17 00:16:24 -07:00
Brennan Benson 035588e914 test(cross-version): read isStructuredTab from the shared module the newest release defines it in (#30000) 2026-10-17 00:11:43 -07:00
Jinwoo Hong 5733887967 test(layout-owner): prove main and the window agree on a worktree identity rekey (#30005)
Main's Store.migrateWorktreeIdentity re-keys every stored layout field before it sends the
rename event the window re-keys on, so the window's save only mirrors it. The new oracle checks
main's migrated layout model equals the model of the window's post-rekey save and the model's
identityRenamed transition; the first-work test pins migrate-before-notify.
2026-10-17 00:10:27 -07:00
Brennan Benson f4aa82874b fix(mobile): show the same Working line in terminal-backed chats and drop the floating 'Agent is working' pill (#29972) 2026-10-17 00:09:10 -07:00
Brennan Benson ac34ffaa81 fix(toasts): stop the notice stack flickering under a resting mouse, and fit the force-delete button to its notice (#29878)
* fix(sidebar): let kept-branch toast bodies fill the toast width

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

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

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

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

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

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

The pointer-events rule left the unstyled star-nag card, which Sonner never
fades, visible below the stack but dead to the pointer. Clipping the folded
toast removes the overhang for every toast type, visible or not, so nothing
of a folded toast can sit outside the stack and re-expand it.
2026-10-17 00:08:26 -07:00
Brennan Benson 32a53d3ec6 feat(native-chat): image messages wait in the queue like text (#28655)
* feat(native-chat): image messages wait in the queue like text

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

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

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

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

* Clarify direct text recovery at send admission

* fix(native-chat): expose queued image names to keyboard users
2026-10-17 00:07:29 -07:00
Brennan Benson 58f1f7de1e fix(mobile): edit queued messages in place (#28646)
* fix(mobile): edit queued messages in place

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

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

* Keep phone queued edits owned through save settlement

* Keep phone queued edits local to mounted chat

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

* fix(mobile): retain queued edits while reconnecting

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

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

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

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

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

* fix(mobile): explain disconnected queued edit refusal

* fix(mobile): combine queued session contract imports
2026-10-17 00:06:07 -07:00
Brennan Benson 8a4c031d8e fix(native-chat): a queue action still on its way greys its controls, like the phone (#29120)
* feat(native-chat): one queue header that becomes the pause row; no composer Resume or send dialog

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

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

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

* Make queued Clear all atomic across clients

* Show queued message failures in collapsed headers

* Use per-card delete for paused queue Clear all

* Join repeated paused queue Clear actions

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

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

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

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

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

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

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

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

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

* test(native-chat): update held-start queue fixture after main merge
2026-10-17 00:04:37 -07:00
Brennan Benson ed3c3cc41f Chat: dropped files hold Send until they're attached (#29173)
* fix(native-chat): reserve pending chips before checking dropped files

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

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

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

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

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

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

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

* test(native-chat): a pending chip shows its name and spinner, no kind icon
2026-10-17 00:01:27 -07:00
Jinjing 05086a892e Let Cmd+J open hosting-provider domains such as github.io (#29989)
Typed hosts like github.io, vercel.app, or readthedocs.io were treated as bare public suffixes and sent to search. They are registrable domains, so they now navigate directly.

- Suffix check now uses ICANN-only rules. The private-section list made `foo.github.io` a domain, but it also let the bare suffix pass, so the classifier now treats the hosting-provider suffix itself as a navigable domain.
- Website navigation now outranks a name match only when the query equals a worktree, tab, or page name in full. Partial matches such as "project.dev server" no longer block the "Go to" row.
- Browser tab creation from a URL forwards the target group as `clientTargetGroupId`, so older hosts that publish the page before the create response still place the tab in the invoking split.
2026-10-16 23:46:30 -07:00
Jinwoo Hong 77c511b316 feat(sidebar): main produces per-workspace activity facts, checked against the window in dev and test (#29980)
* feat(sidebar): main produces per-workspace activity facts in shadow mode

* perf(sidebar): agent pings keep last-read surfaces; shadow counts comparisons

* fix(sidebar): settle review findings in the activity-facts producer and shadow check

* fix(sidebar): refresh on inventory verdict clears; resync agents only when tabs move

* fix(sidebar): scope the shadow comparison by each row's owner, not the focused host

* refactor(sidebar): name the shadow scope after what it keeps

* fix(sidebar): classify sidebarInputs as host-owned in the operation ownership census
2026-10-16 23:40:35 -07:00
Brennan Benson 88f27711fd feat(native-chat): a chat whose host has no program to run shows Install in place of its composer (#29051)
* feat(native-chat): a chat whose host has no program to run shows Install in place of its composer

The chat's one input slot is chosen in one place: a final read refusal, then an owned prompt card,
then Install while the host has nothing runnable (its install included), then the composer. Until
the first status answer the slot stays empty instead of flashing either. Send and slash commands are
refused while Install holds the slot, so nothing hidden reaches the host. The draft stays in its
store and is there when the composer returns. A failed status read leaves the composer; the host
remains the final refusal.

* fix(native-chat): keep the composer slot reserved until the host's agent list says, and say nothing about sending while Install shows

- While the host's capabilities or agent list are still loading for a chat's agent, the slot stays
  quiet instead of flashing the composer.
- While the install row holds the composer's place, a failed start's notice ("Send your message
  to try again", Retry) is left out: nothing starts until the install lands.
- The chat install card's title is semibold like the other cards in that slot.

* fix(native-chat): keep the chat session view within its line limit after the install slot

* fix(native-chat): the composer stays while the host's first install answer is out, its sends held, so no agent's chat loses its input box

* fix(native-chat): the install slot holds Send through the composer's existing held-send reason instead of a flag of its own

* fix(native-chat): the install slot hands the chat its held-Send reason, keeping the chat session view under its line limit
2026-10-16 23:32:04 -07:00
Neil 7bb83b47c8 test(store): host-twin fixture and destructive/selection twin isolation suite (#29991)
Seeds local+ssh, local+runtime and folder twins (one id on two hosts) with live
shells, a dormant tab, dirty docs, drafts, sleeping-agent records and module-level
stores, then runs a hand-declared action table (remove, folder delete, purge,
kill, sleep, select) on one twin and asserts the other twin is untouched.
Today's leaks are listed cell by cell and asserted to still leak, so a fix
turns them red until the entry is removed.
2026-10-16 23:26:46 -07:00
Jinwoo Hong a349bfa6cc Agree with main on first-group ids, tab order and the preview flag (dark, layout-owner build only) (#29987)
* Agree with main on a workspace's first group and the tab order, and stop writing a preview flag main never stores (dark, layout-owner build only)

Switch-on builds only; switch-off behaviour is unchanged.

Group ids: a window save made before main's frame with a workspace's first
tab carries none of that workspace's tab-bar records, so the Loader rebuilt
the tab in the group it names for the workspace while main had minted a
random one: every workspace's first save replaced main's group. Main now
names a workspace's first group as the window's root group and the Loader
do (`workspaceRootGroupId`), or takes the sender's when the window drew the
tab in a group main lacks (the window keeps its emptied group); the
window's `ensureGroup` names it the same way. The detector reads a lone
group with no stored tree as that group's leaf, as main's own save writes
it. Empty groups: the window never saves a group it holds empty, so a save
cannot bring back one main closed (locked by a test).

Tab order: a reorder the window makes itself (an anchored CLI or agent
create, a reopened tab's old slot) is recorded in main as `moveTab` steps
at the store boundary; callers that already asked main pass
`layoutOwnerHandled`. The reopened-editor record this replaces is deleted.
A paired client's move (`ui:moveSessionTab`) goes to main first through
the tab-bar drop path.

Preview: activating a tab removed its preview flag by writing
`isPreview: false` on every tab; main stores no flag, so every activation
changed the tab in main's model. The window now drops the flag instead.

* Record a reopened tab's place in main every time, leave a move into a tab's own group alone, and keep a preserved preview untouched (dark, layout-owner build only)

Review fixes, switch-on only:
- A reopened tab's place is recorded as one moveTab whenever its old
  position is known, as the deleted editor-only record did, now for
  every kind; the reorder it may make is not recorded twice.
- A move into the tab's own group (a paired client can send one) goes
  to today's no-op instead of a moveTab that reordered the tab.
- Activation that preserves a preview returns the tab unchanged.
Tests: switch-off first group stays minted; tabOrderMoves refuses a
duplicate order.

* Send a paired client's tab reorder to main as its whole order (dark, layout-owner build only)

A client reorder names the group's full order, but it went to main as
one moveTab of the named tab, so [a,b,c] -> [b,a,c] naming c moved
nothing anywhere. It now takes the store's reorder path, whose record
turns the whole order into main's moves (tabOrderMoves), the one rule
for every window-side reorder. Moves and splits still go main-first.
2026-10-16 23:25:10 -07:00
Jinwoo Hong 5b69634c0a Agree with main on main-made terminal tabs and sleeping-agent records (dark, layout-owner build only) (#29967)
* feat(layout): main-made terminal tabs drawn with main's facts; resume records reach main record-only (dark)

In layout-owner builds only:
- A terminal tab main made itself (drag-out, CLI or host terminal, live-surface
  adoption) is drawn with main's createdAt and creation fields from the frames,
  and the window sends no create for it.
- An editor tab main records takes the time the window drew it.
- Every change of the window's agent resume records (live and quit captures,
  dropped records) reaches main through record-only sleep and wake commands,
  which stop or start nothing.
- A terminal row's remount generation is declared the window's.

* fix(layout): real wake reaches main before the record drop; drag-out keeps a frame-drawn tab (dark)

In layout-owner builds only:
- A resumed pane's wake is sent before the window drops its record, so main's
  wake still finds the pane sleeping.
- A drag-out whose new tab main's frame already drew shows the pane in that
  tab instead of throwing "Terminal tab already exists".
- A main-made tab keeps the window's shell, cwd, host-runtime and launch agent
  where main's copy names none (a runtime create records a shell only when
  its launch named one).
- No record-only wake for a tab the window closed; main dropped its records
  with the tab.
- Tests for the resume-record follower.

* fix(layout): a dragged-out chat pane's new tab opens in chat view in main (dark)

Main's movePaneToNewTab now gives the new tab viewMode 'chat' when the moved
pane is its source tab's chat pane, as the window's own drag-out does. In a
layout-owner build the window adopts the row main's frame drew, so without
this the tab opened in terminal view.

* fix(layout): a bound pane with a resume record starts as a no-op; resume records only for held tabs (dark)

- startPane checks the pane's live terminal before its resume record: a
  running agent's live or quit record does not make its pane asleep. A pane
  with no terminal and a record is still refused.
- The window sends record-only sleeps, like record-only wakes, only for tabs
  it still holds; a capture from a stale agent status no longer names a tab
  main already closed.
2026-10-16 23:23:04 -07:00
Neil 2d33cd965f refactor(identity): brand host-scoped worktree and repo keys for new code (#29990) 2026-10-16 23:19:42 -07:00
Jinwoo Hong 37c314bcc9 Send a browser tab's window times to main with its open (dark, layout-owner build only) (#29988)
* fix(browser): keep slept workspace browser tabs off the reopen-closed list

Sleeping a workspace closed its browser tabs as if the user had closed them, so
Reopen Closed Tab could bring back a tab that was only slept. Sleep now closes
them with a 'sleep' reason that skips the reopen stack while still releasing
host pages, doc grants and webviews as before.

* feat(layout): send a browser tab's window timestamps with its open (dark)

A layout-owner build's openBrowserTab carried no createdAt, so main stamped the
tab-bar entry and the browser record with its own apply time and the window's
next save diverged on createdAt for both. The window now sends both times, as
createTerminalTab does; only switch-on builds send them.

* Revert "fix(browser): keep slept workspace browser tabs off the reopen-closed list"

This reverts commit 6798c3c701.

* test: drop the slept-tab close test that went with the held sleep fix
2026-10-16 23:15:43 -07:00
Neil de1f6e3d81 fix(release): publish tags when main requires pull requests (#29985) 2026-10-16 22:57:25 -07:00
Neil 76fc1e5acd fix(web): pair and stay paired from plain-http LAN and Tailscale pages again (#29984)
Partially reverts #27540 and the plain-http part of #27552, which broke web
pairing on upgrade for 1.4.227 users who open Orca at a LAN or Tailscale address:

- The host no longer refuses a first pairing from a plain-http page on another
  machine (insecure_origin, ORCA_ALLOW_INSECURE_WEB_PAIRING).
- Such a page keeps its pairing code in local storage and signs in with it, as
  in 1.4.227, so reloads, new tabs and bookmarks stay paired; no notice.
- Session-only browser keys (pairing.device-key-ephemeral.v1, the idle sweep)
  stay for browsers without Ed25519.
- A web client whose credentials are refused shows the host's reason instead
  of an empty workspace and "Session restore failed".
2026-10-16 22:50:27 -07:00
Jinwoo Hong a87aa4ccdb ci(mobile): merge the Android APK download bump on the site after its checks pass (#29981) 2026-10-16 22:28:40 -07:00
Neil 8612588816 fix(release): retry mac build run identity propagation (#29974) 2026-10-16 21:59:32 -07:00
NeilandClaude a82298ebe6 fix(runtime): the host stops a removed project's shells and agents itself (#29964)
* fix(runtime): the host stops a removed project's terminals itself

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

* fix(runtime): a runtime-stamped project still stops the shells that run here

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

* fix(runtime): refresh the PTY inventory and sweep again after a project's removal

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

* fix(runtime): bound the project inventory refresh and recheck the WSL move fence

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

* fix(runtime): fence a project's removal sweep by its SSH connection and wait out in-flight spawns

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

* fix(runtime): bound the project stop's lease wait by the removal deadline

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

* fix(runtime): refuse spawns into a removed project and skip a stop a same-id sibling shares

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

* fix(runtime): bound each project stop by the removal deadline

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

* fix(runtime): pass the removal deadline to the lease so a timed-out waiter is dequeued

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

* test(runtime): type the project-removal fixtures instead of casting

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 21:54:47 -07:00
Neil 0d9beb603e fix(ssh): reconnecting a stopped managed host keeps the server's project groups (#29962)
Relinking projects the server already held still re-imported this desktop's
retained copy of their project groups, so a group renamed on the server while
it was managed failed the reconnect with orcad_migration_project_group_id_conflict.
A relinked move now leaves to the server every group its projects sit in (read
from repo.list's projectGroupId) and those groups' ancestors unless a moving row
needs one, journals those ids, and keeps them out of the exported rows while
still reading them for folder-workspace host ownership. Later delta moves
subtract them as they do moved groups.
2026-10-16 21:54:37 -07:00
Neil dd2bd0b0b7 fix(terminal): recovering one split pane keeps its healthy sibling in the saved layout (#29961) 2026-10-16 21:54:27 -07:00
Brennan Benson 1ed735b5e6 feat(acp): one install row shows and drives a managed runtime install, for Settings and chat (#29049)
* feat(acp): one install row shows and drives a managed runtime install, for Settings and chat

The row reads the shared host state: Install, byte progress with Cancel, extract and verify lines,
Retry after a failure or cancel, and in Settings Reinstall or Update once installed. A failed status
read says it could not check, never that nothing is installed. Strings in all six locales.

* fix(settings): the install row says when a copy is damaged, when a PATH copy is used, and when the host needs unzip

- A damaged copy reads "This copy of {agent} is damaged." with Install, which repairs it.
- A copy found on this host outside Orca's reads "Uses the copy of {agent} found on this host."
  with no Install, since a launch would never run the download.
- A host without the program to unpack the archive says so, with no Install.

* fix(settings): say why a host running its own copy is offered no download

* fix(settings): the install row says Cancelling only where Cancel was clicked, offers Orca's copy beside one found on the host, and keeps its status line after a failed install

The host no longer publishes a cancelling phase or a missing unzip, so the row drops both: the
row whose Cancel is still out says Cancelling and holds its button until the host's cancel
settles. A copy found on PATH or in an install folder still gets Install of Orca's copy in
Settings (the chat keeps its composer). An older installed release reads Installed. with Update.
A failed first install keeps Not installed. beside the failure and Retry. The status line is a
polite live region.

* fix(settings): the install row says it is retrying a failed status check, and names the step after the download

* fix(settings): a Cancel that fails for no stated reason says the cancel failed, not the install
2026-10-16 21:32:46 -07:00
Brennan Benson 5e8d40d61b fix(mobile): remove the dead space around the chat composer (#29224)
* fix(mobile): remove the dead space around the chat composer

The Tools toggle floats over the transcript and shows only when the chat
has tool calls; the composer is one row (attach, message, mic, send) until
focused or drafted; Stop takes Send's slot while there is nothing to send;
the bridge lane's working dots move to the transcript's tail.

* fix(mobile): name the composer's content facts for their role

* fix(mobile): preserve composer controls and picker focus

* fix(mobile): typecheck working indicator test lookup

* test(mobile): find the floating Tools pill when checking a chat switch keeps it expanded
2026-10-16 21:27:06 -07:00
Brennan Benson 7b6f02fdbf fix(native-chat): idle cleanup and commands read the helpers the agent reported, not the capped status store (#29905)
* fix(native-chat): decide owed helper work from what the provider reported, not the capped store

Idle cleanup, replacing an outgrown agent, /clear, /compact, rewind,
command handover and the teardown resume marker decided whether a chat
still had helpers running by reading the agent-status store. That store
is a bounded display projection: it refuses records when it is full or
for per-record reasons, and a refused helper is still running.

The host now keeps an uncapped per-session set of the child work its
provider reported running and has not reported over, fed from every
child-work edge before the store sees it. Every owed-work decision reads
that set. An entry leaves on the child's own end (a late end of an
earlier run leaves the current run open, as the store's run verdict
does), on a Stop the store settled, or on the session's end.

The strip's "incomplete" note and Stop all's unlisted route are now
derived: open work that no live current-run record answers to. This
replaces the refused-for-room side list, so every refusal reason is
covered and nothing needs separate bookkeeping.

* test(native-chat): the real-store test sink reads recorded helper handles, as production's does

* fix(native-chat): keep helper run identity through delayed events and Stop

* fix(native-chat): name the host helper lifetime token

* test(native-chat): explain the Stop adapter fixture boundary

* fix(native-chat): derive helper coverage from running store records

* docs(native-chat): clarify running helper coverage

* test(native-chat): use an explicit helper end event

* Scope background Stop to captured child work

* Exercise refused helper Stop race through status store
2026-10-16 21:26:34 -07:00
Jinwoo Hong 442491be11 test(sidebar): golden for accented, mixed-case and numbered sidebar names (#29966)
Records the post-#29946 text order for workspace rows (Name, Recent and
project-name sort), project group names and folder workspace names, ungrouped
and grouped by project, so a future ordering change shows up as a golden diff.
2026-10-16 21:25:08 -07:00
Brennan Benson a4532faa30 fix(workspace-cleanup): delete the active or a running-agent workspace like a sidebar delete (#23593)
* fix(workspace-cleanup): let select-all include the workspace you are in

Deleting the current workspace is allowed like any other, so select-all now
includes it; a live agent stays excluded. Cleanup removal now hands focus to a
surviving sibling after the batch, as the sidebar's batch delete does.

Linear: STA-4686

* test(workspace-cleanup): cover focus handoff after the dialog closes and after Delete anyway

* fix(workspace-cleanup): move focus off a deleted active workspace as soon as its row is gone

* fix(workspace-cleanup): don't reopen a workspace the user left while a cleanup batch runs

The per-row focus handoff fired whenever no workspace was selected, so
closing the successor's last tab mid-batch re-activated it (and spawned a
new terminal) on the next row. Move focus only when the workspace the
batch left the user on is actually gone and nothing else took focus.

* refactor(workspace-cleanup): move focus off the active workspace when a cleanup delete starts

Switch to the successor once, right after the batch rows are marked deleting, instead of
repairing an empty selection after each row settles. The active row is no longer active when
it is removed, so the store never clears the selection and no later handoff is needed.

* fix(workspace-cleanup): never hand focus to a workspace in the same cleanup batch

The scan qualifies local rows as host `local`, so their delete state lives
under a host-qualified key, while a sidebar row with no stored host only
matches an unqualified key. The successor picker therefore saw batch rows as
not deleting and could focus one about to be removed, leaving an empty screen
once it went. Exclude the batch's own ids directly.

* test(workspace-cleanup): use scan-shaped hosts in the active-workspace focus tests

Candidates now carry the `local` host the scan publishes, which is what
exposed the picker gap. The other-host case also stamps the viewed row with
its host so it resolves; before, the lookup missed and the test passed with
the host check removed.

* feat(workspace-cleanup): confirm before a cleanup delete stops a running agent

A cleanup delete (bulk, single row, or "Delete anyway") that includes a
workspace with a running agent now shows a step naming those workspaces
before anything is queued. The check reads live agent state at click time,
attributed like the sidebar so structured chats count too, and re-asks if
another agent starts while the step is open.

The approved candidate records which agents the user agreed to stop; the
per-row preflight that runs right before each delete refuses a row whose
agent started after confirmation. With the step in place, live-agent rows
join select-all like the active workspace.

* refactor(workspace-cleanup): name the agent-stop same-action check

* fix(workspace-cleanup): drop an open agent-stop step when the dialog reopens mid-batch

* style(workspace-cleanup): match the agent-stop step's heading to the delete step

* test(workspace-cleanup): type the cache-hydration mocks in the reopen test

* style(workspace-cleanup): let the agent-stop step's heading use the dialog's own typography

The changed-lines design-system gate rejects restyling DialogTitle and
DialogDescription; the delete step's overrides predate that gate.

* fix(worktree-delete): key delete state by the identity the sidebar card reads

A cleanup delete names local rows with the 'local' host, so their queued and
deleting state landed under 'local|<id>' while a card stored without a host
reads the bare id and never showed it. Every delete-state write now derives
its key from the store's own worktree record.

* refactor(workspace-cleanup): reuse the sidebar delete's focus hand-off

Drops the move-focus-before-delete path. Each confirmed row prepares the same
prepareActiveWorktreeFocusAfterDelete committer the sidebar delete uses and
runs it when that row's own removal succeeds, so the user stays on the
workspace while it is queued, lands on the most recently visited surviving
sibling once it is gone, and stays put if its delete fails.

* refactor(workspace-cleanup): reuse the close-terminal stop-agent wording

The stop step's single-agent title is the existing "Stop this agent?" string,
and its description follows the close-terminal prompt's wording.

* test(worktree-delete): cover a local-qualified removal failure on an unhosted card

* docs(workspace-cleanup): describe the after-delete focus hand-off in the bulk-select note

* fix(workspace-cleanup): name one agent in the stop step's button and keep one agent term per locale

* fix(terminal): keep starting terminals in a workspace that is only queued for deletion

A cleanup batch now marks its local rows with the same delete state the sidebar card reads,
so the terminal spawn guard sees them. The guard exists because removal just killed the
workspace's terminals; a queued row still has them and can fail its own delete and stay, so
a new terminal opened there (often the current workspace) stayed blank.

* refactor(terminal): name the teardown-started check the spawn guards share

* chore(workspace-cleanup): keep one agent term in ko/zh and refresh a spawn-guard comment

* fix(workspace-cleanup): refresh consent and focus per removal

* fix(workspace-cleanup): invalidate detected host evidence

* fix(workspace-cleanup): admit removal at transport send

* test(workspace-cleanup): expect the send-time admission callback and a published chat tab
2026-10-16 21:25:00 -07:00
Brennan Benson 002320c17d fix(native-chat): verify the actual CLI conversation during resume (#29124)
* fix(native-chat): identify the owner when resume create is refused

* test(native-chat): consolidate transcript refusal cases

* test(native-chat): keep ownership coverage at the launch boundary

* test(native-chat): update adoption fixture for early ownership admission

* test(native-chat): align resume admission assertions with host ownership

* test(native-chat): keep RPC coverage focused on ownership proof

* test(native-chat): assert blank request presence before optional source

* fix(native-chat): admit the actual CLI source during chat creation

* fix(native-chat): satisfy source admission lint checks

* test(native-chat): provide the waiting hook prompt

* test(native-chat): isolate source admission fixtures from host homes

* test(native-chat): canonicalize adoption fixture directories

* test(native-chat): preserve early ownership admission in source stack

* fix(native-chat): preserve resume refusal behavior and honest transcript feedback

* fix(native-chat): retain import read evidence in typed result

* fix(native-chat): preserve admitted source and prepared execution home

* fix(chat): retain source evidence for dispatched adoption

* fix(chat): keep source capture within module limits

* test(chat): allow host evidence in create routing

* fix(native-chat): admit an unsaved transcript under a linked account folder

A missing transcript stayed unresolved while its account root resolved through
symlinks, so containment failed and a not-yet-saved conversation showed a load
error instead of the unsaved notice.

* refactor(native-chat): move the newest-item import window into its own module

Keeps journal-legacy-import.ts under the line limit after the source
identity observer landed on top of the streaming import.

* fix(native-chat): restore the refusal error import lost in the main merge

* fix(native-chat): admit Pi transcripts against the root Session History walks

A PI_CODING_AGENT_DIR that already ends in sessions, or any custom folder,
made admission expect <dir>/sessions while the lookup searched <dir>, so a
found conversation was refused.

* refactor(native-chat): admit and look up CLI transcripts in one store per account

Each agent's import described where an account keeps its conversations twice,
once for the path check and once for the lookup, and the two could disagree
(the linked-folder and Pi custom-root refusals). The import now names one
conversation store per account; the create path resolves it once and both the
hook-path check and the fallback lookup judge that same store. OpenCode's
store is its exact database.
2026-10-16 21:14:03 -07:00
Jinwoo HongandClaude 550cc5cdcc fix(sidebar): order sidebar names the same on desktop, phone and server (#29946)
* fix(sidebar): order sidebar text the same on every device

Sidebar names, project labels, project-group names and folder workspace
names were ordered with localeCompare, whose result depends on the ICU data
bundled with Electron, the phone WebView or Node. Order them with one
comparator in src/shared/sidebar-list/text-order.ts instead: case-folded and
NFC-normalized, digit runs by value, the rest by code point, and equal text
broken by host-qualified id.

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

* perf(sidebar): fold each sort label once per sort; reuse the phone row identity helper

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

* fix(sidebar): ignore accents when ordering sidebar names

Accents and case now only break exact ties, so "Éclair" sorts beside "eclair".

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 21:07:05 -07:00
Brennan Benson 07d3e33675 fix(tab-bar): stop guessing a launch failed while the new terminal is still starting (#29911)
The + agent menu waited 5 s for the new tab's terminal and otherwise showed
"Couldn't launch X — the terminal did not start." Any slow spawn (Windows,
a cold or busy terminal host, a queued spawn) passed through that state and
got the false toast before the agent came up. The pane's own spawn already
reports a real failure in the pane, so the guess is removed.
2026-10-16 20:55:19 -07:00
Neil 357aa41cb4 fix(terminal): keep Vim scrolling inside shrunken panes (#29960) 2026-10-16 20:45:13 -07:00
Kelvin Amoaba 432c6d0735 fix(mobile): show message times, keep the long-press sheet open, and say when a conversation isn't found (#29105) 2026-10-17 03:37:12 +00:00
Neil 6863096c06 fix(orcad): a client that reconnects during idle shutdown can still open terminals (#29958)
* fix(orcad): replace a retired terminal daemon when a client returns during idle shutdown

* fix(daemon): drop idle-retired legacy generations when replacing the daemon

* fix(orcad): treat a lost or late daemon retirement reply as committed

* fix(orcad): settle a lost retirement reply with the daemon itself before replacing it

* fix(orcad): bind the replacement to the retired daemon and honor the router fence

* test(daemon): type the pending census

* fix(daemon): skip the owner check only for a proven-dead endpoint
2026-10-16 20:25:47 -07:00
Neil 9cc8b80817 fix(worktrees): a preparation another machine owns is never reclaimed as stale (#29957)
The preparation lock reason named only the owner's pid, so a host sharing the repo's disk
(an NFS home) read another machine's in-progress preparation as abandoned and discarded it.
The reason now carries the writer's machine identity (optional, appended after the fields
older builds read); a different or unknown machine is unverifiable, and a reason without
one is judged by pid only on a local disk.
2026-10-16 20:24:04 -07:00
NeilandClaude 15b1217b38 fix(projects): removing one host's copy of a project keeps its same-path twin's shells, tabs and edits (#29954)
* fix(projects): removing one host's project keeps its same-path twin's shells, tabs and edits

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

* fix(projects): close the removed host's browsers, chats and late tabs under a twin id

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

* fix(projects): attribute a twin's terminals by every PTY id it keeps, SSH included

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

* fix(projects): kill a removed local/SSH twin's shells directly and keep its files out of reopen history

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

* fix(projects): prune the removed twin's closed editors from the reopen stack

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

* fix(projects): recheck twin survivors after the host call and keep reopen order in step

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

* fix(projects): close a removed twin's simulators and prune its closed browsers

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

* fix(projects): attribute an unstamped twin browser by its pages' host

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

* fix(projects): plan a twin's terminal teardown so PTYs other tabs bind survive

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

* fix(projects): treat a null browser host as local and close stale stamped editor tabs

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

* fix(projects): a host stamp yields to a PTY naming another host; sweep tabless sleeping records

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

* fix(projects): cancel a twin's pending launches, clear its PTY records, prune its local editors

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

* fix(projects): an ownerless remote pane blocks a stamp; keep the focused surface on background twin removal

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

* chore(projects): leave closeFile's active-surface choice to closeFile

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

* fix(projects): keep the selection fields consistent after twin teardown

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

* fix(editor): closing a background workspace's file keeps the focused surface

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

* fix(projects): a file's own host outranks its tab stamp in twin attribution

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

* fix(projects): kill only a twin tab's live PTYs and honor a browser's own host stamp

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

* fix(projects): skip twin files the removal refresh already closed

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

* fix(projects): kill a batch's live PTYs whichever tab the planner chose; background file close keeps focus

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

* fix(projects): resolve a removed SSH copy's paired-runtime alias in twin attribution

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

* fix(projects): stop the removed host's pane in a tab another host's pane keeps

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

* fix(projects): judge a still-present twin surface on its latest evidence

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

* fix(projects): apply the removed copy's runtime alias to launches and history; layout-only hints prove nothing

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

* fix(projects): repair a selection naming the removed copy's runtime alias

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

* fix(projects): leave single-host selection to the bulk purge; test background file close

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 20:22:28 -07:00
NeilandClaude ad6b318620 fix(mobile): re-read a server's capabilities when the server reconnects, not only the desktop (#29913)
useWorkspaceServer keyed the server's status read on the desktop's connection state, so a server restart or update under a connected desktop never re-read what it supports. It now passes the server's computed state.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 20:07:50 -07:00
Neil 7d077ee9c9 fix(ssh): keep existing hosts' orca CLI control off after the on-by-default flip (#29949)
* fix(ssh): keep existing hosts' orca CLI control switch off after the on-by-default flip

Builds before the flip saved an explicit off as an absent value, so absent=on
silently turned on hosts the user had turned off. On the first load after this
upgrade, pin every saved host's absent switch to an explicit false and record a
one-shot marker in settings so hosts added later keep the new default.

* test(ssh): check the saved profile without a type assertion

* fix(ssh): say the CLI control switch is on by default for new hosts at the call site too
2026-10-16 20:07:35 -07:00