Commit Graph
3165 Commits
Author SHA1 Message Date
Brennan Benson f89b4ea989 fix(chat): preserve complete writes and keep detail current 2026-10-16 23:31:29 -07:00
Brennan Benson 7c5006c083 fix(native-chat): format, keep the sink and projection under the line limit, update method count and tests 2026-10-16 22:47:28 -07:00
Brennan Benson 65fb0709ed fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients
Orca cut Codex replies, every agent's reasoning, plans, prompts and all tool
output to a 16 KB head when it saved them, and threw the rest away. The
limit existed so a reconnecting client is not replayed a huge row, but it
was applied at save time, so every transport limit became permanent data
loss.

The journal now keeps what the agent said. One outbound form, applied to
every history page, live batch and catch-up, sends a client the same 16 KB
head of each tool output and diff it always got, and marks the item
`shortened`. A new capability-gated `agentSession.item` RPC returns the
whole item, and the desktop and phone show "Show full output" / "Show full
message" when the copy was cut. Items past half a page are shortened and
marked instead of replaced with "too large to show".

Two bounds stay, both named: a 4 MiB ceiling per text (head and tail),
applied where every provider's rows enter the write queue so one write can
never stall a chat, and a preview on Grok/OpenCode/OMP/Pi calls while they
run, whose closing row is built from the whole value the translator held.
2026-10-16 22:27:17 -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
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
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 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
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
Jinwoo Hong 7bf3e150ed refactor(sidebar): move the sidebar's time rules into shared pure reducers (#29943)
* refactor(sidebar): move sort settling, the Smart latch and sweep retention into shared pure reducers

* refactor(sidebar): let the settle reducer own the Manual and mode-switch rule
2026-10-16 19:59:14 -07:00
Jinwoo Hong f7a8e633af Send a terminal tab's own changes to main first, from every caller (dark, layout-owner build only) (#29904)
* feat(layout): route a terminal tab's own changes to main first (dark)

Rename, color, agent and vault titles, view mode and an ended launch
agent now reach main as layout commands from every caller, in
layout-owner builds only. Main's create also takes the window's
createdAt, and frames keep the window's pane mount generation.

* fix(layout): toggle from the mode sent to main, give each agent title its own answer (dark)

A second view toggle before main answers the first now flips from the
mode already sent, so the tab ends where a window-owned build would.
An agent-title batch applies each tab's title when main answers that
tab, and a paired server's tab at once. Drops the frame merge's
generation field: the window's own save already carries it. Reverts
unrelated formatting.

* fix(layout): clear a pending view mode only on its own send's answer (dark)

* chore: restore three files the commit hook reformatted outside this change

* fix(layout): send one agent title per tab while main has not answered (dark)

The window keeps a tab's first generated title, so a second prompt's
title sent before main answered the first would leave main on the
second and the window on the first.
2026-10-16 19:56:25 -07:00
Jinwoo Hong 9cc5d6b427 Keep an editor tab Orca moved to its file's workspace across restart; record editor renames in main (dark) (#29930)
* fix(editor): keep an editor tab Orca moved to its file's workspace across restart

The move saved the tab under the file's owned id and left the tab's
workspace pointing at the old one. Restore rebuilt a local file under
its bare path, so the tab named no open file and was dropped. Restore
now maps a saved owned id to the id it rebuilds, and the move gives
the tab its new workspace.

* refactor(editor): delete reorderFiles, which nothing calls

* chore: prune the renderer layout-writer baseline

* fix(layout-owner): keep diff, preview and conflict records when a frame redraws editor tabs

Switch-on builds only. A frame rebuilds the window's tabs through the
restore builders, which drop every diff, conflict and check tab (restore
has no record for them), and the store patch dropped every open file
the frame did not carry. Frames carry only edit-mode files, so the next
frame after opening a diff closed it in the window while main kept it,
and a markdown preview kept its tab but lost its record.

The rebuild now keeps a transient tab whose record the window holds,
and a held record goes only with the last tab showing it; a record no
tab shows (a conflict review's selected file) stays.

* feat(layout-owner): record renamed open files and closed conflict placeholders in main

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

A rename or move in the explorer rekeys open editor, preview and diff
tabs in the window and main learned it only from the window's save.
The window now records the rekey as a `renameEditorFiles` command,
under the ids it chose (bare path, owner-qualified id, diff id), one
per workspace, with each edit-mode file's new record. It records
rather than asks first: the rekey is atomic with drafts and the
autosave echo gate, and its answer gates the move coordinator. Main's
path-keyed `editorFilesRenamed` transition, which nothing produced and
which could not match owner-qualified or diff ids, becomes this command.

A git status poll that sees a deleted-side conflict resolved drops its
placeholder record but leaves the tab to the tab reconcile; frames then
brought main's edit record back. Main now closes that tab.

* test: type the rename command as a window layout command

* fix(editor): map a moved tab's owned id on restart only; main records a renamed file only for a tab it shows

* fix(editor): restore a moved tab's file under the owned id the tab names

Restart and frames now agree: neither rekeys the tab, and a frame that
redraws one workspace never points its tab at a same-path file in
another. A rename that changes nothing leaves no empty file list.

* test: a moved tab's owned id stays apart from a sibling's same-path file in any frame
2026-10-16 19:54:52 -07:00
Jinwoo Hong 776c791554 feat(orcad): ship the mobile web page so phones paired to orcad get it (#29882)
* feat(orcad): ship the mobile web page so phones paired to orcad get it

orcad never carried out/mobile-web, so it did not advertise
mobileWeb.bundle.v1 and phones paired directly to an orcad server fell
back to the app's built-in native screens.

The built page now ships in every orcad package at out/mobile-web, the
path getBundledMobileWebBundleRoot already probes under the install
root. Its manifest is a fixed artifact; its content-addressed assets
join the hashed artifact list from that manifest, as one shared copy in
the template's common files. build-orcad fails when the page is missing
or stale, and CI lanes that build orcad now build the page first.

* ci(orcad): check out mobile/ for the Windows host template build

* refactor(orcad): copy the page tree whole and build it once per CI run

The guard already refuses unlisted files, so staging copies the
bundle directory as native-chat-visuals is copied. The shipped template
takes the page the qualification lanes tested, the e2e build artifact
carries it, and the PR smoke builds it through install-mobile-dependencies
behind its own scope step. Drops tests that duplicated the guard's own.

* fix(orcad): refuse a template page asset name that escapes the slot
2026-10-16 19:29:41 -07:00
Jinwoo Hong 0013011409 refactor(sidebar): move the pure workspace-list builder into src/shared/sidebar-list (no visible change) (#29906)
* test(sidebar): capture golden fixtures of today's sidebar row model

Golden output of the unmodified window pipeline (filter, sort, grouping,
pins, lineage, host sections, Cmd+1-9 targets) across every groupBy x sortBy
x projectOrderBy x pinned policy, plus host-filter, repo-filter, hide-sleeping
and hide-default-branch scenarios, each expanded and collapsed. Kept as the
correctness reference for moving the row builder.

* refactor(sidebar): move the pure row builder into src/shared/sidebar-list

The sidebar's list rules (visible filter, grouping, pinned rows, project
group sections, placeholders, host sections, collapse keys) move to
src/shared/sidebar-list as pure functions. Headers carry an icon key instead
of a React component; the window resolves glyph and tone when it paints, and
passes localized header labels in. The window keeps calling through thin
adapters (build-rows, compute-visible-worktrees); sort latch, settled epoch,
sleeping retention and pending creations stay where they are.

github-cache-key moves to src/shared/github because PR-status grouping needs
it; the renderer path stays as a re-export so its callers are untouched.
Other edits outside the sidebar are import paths only. The golden fixtures
from the previous commit pass unchanged.

* refactor(sidebar): resolve header labels at render, add an inbox golden

Header labels are read on every re-emit again, as before the move. Settings
and labels types move next to the other row types; header visuals no longer
export what nobody imports. Adds a golden for the new-external-worktrees inbox
row, captured on the pre-move pipeline.

* refactor(sidebar): shrink the shared builder's inputs to plain readers

- The builder takes a PR-lane reader instead of the PR cache and settings;
  getPRGroupKey stays in the window, so github-cache-key no longer moves.
- Pinned, All and PR-lane headers carry only an icon key; the window names
  and draws them at paint (status headers keep their data label).
- The status icon key carries the status id; the window looks up its look.
- Window callers use planSidebarRows directly; the positional helper is a
  test fixture.
- Folder path-status routing and rendered-row keys stay in the window.

Goldens pass byte-identical.

* refactor(sidebar): point stale builder comments at planSidebarRows
2026-10-16 19:28:58 -07:00
Neil 6bbc3cbb5b fix(gitlab): load MR reviewer choices from the paired server that owns the repo (#29917)
The runtime GitLab API answered listAssignableUsers with an empty list, so the reviewer picker
was always empty for a paired server's repo. Add an optional gitlab.listAssignableUsers RPC on the
host; a host that predates it answers method_not_found and the picker stays empty as before.
2026-10-16 19:23:49 -07:00
Brennan Benson b61b9da778 fix(mobile): a streaming reply reveals word by word, like the desktop (#29148)
* fix(mobile): a streaming reply reveals word by word, like the desktop

The phone drew each streamed chunk as it landed, so a reply arrived in a few
jumps. Move the desktop's paced reveal into src/shared and use it on both
surfaces: the phone paces the growing block of the newest reply, remembers
progress per chat list so a recycled row resumes, and draws everything at once
with reduced motion. The terminal-backed chat's live row now counts as growing.

* fix(mobile): hold the reply reveals in the chat overlay

The list's turn resolver must not scan terminal-backed transcripts, and the
overlay outlives the list across chat and terminal toggles, so it owns the
reveal memory and the reduced-motion read. Also fix the moved hook's import
in the onboarding route, and mock the OS read where tests render the overlay.

* test(mobile): mock the message row's other children in the reveal test

Their real modules reach React Native's Flow sources, which Bun cannot load;
the other message row tests mock them the same way.

* style: format the desktop paced-text hook

* Keep phone reply reveal progress owned by its message

* Add braces around phone reveal test assertion

* Start phone prose streaming only at a text block

* Find the mocked phone tool row with a node predicate

* refactor(mobile): move chat prose rendering into its own file

Keeps MobileNativeChatMessage.tsx under the 400-line limit after the main merge.
2026-10-16 18:51:18 -07:00
Brennan Benson 634ca45fbb feat(acp): hosts install a managed runtime over RPC, and mark agents whose program Orca provides (#29024)
* feat(acp): hosts install a managed runtime over RPC, and mark agents whose program Orca provides

managedRuntime.status / managedRuntime.install / managedRuntime.cancel, negotiated by
managed-runtime.install.v1, act on the host that receives them, the one that runs the agent's
chats, with no workspace needed. A status read naming the revision it shows is a long-poll on its
own call-queue lane, so every client sees another's start, progress and outcome.
agentSession.agents marks an agent whose program Orca provides on that host.

* fix(runtime): install status says a copy outside Orca's runs only when a start would really run it

* fix(runtime): install status is pushed to subscribers instead of long-polled, and Cancel answers with the outcome

managedRuntime.subscribe {agent} sends a full snapshot on subscribe, then one
snapshot per install state change, and closes with the transport. It replaces
the managedRuntime.status long-poll and its slot registrations.
2026-10-16 18:47:12 -07:00
Brennan Benson 64e544a442 feat(mobile): Resume in New CLI from a phone chat tab (#29783)
* feat(mobile): Resume in New CLI from a phone chat tab

A chat tab on the phone had only Close in its long-press menu, so a chat
that would not load left no way to reach its conversation. The phone now
offers Resume in New CLI, like the desktop tab menu: the chat's host
prepares the conversation and the phone opens a copy in a new terminal tab.

The resume and fork startup builders move to src/shared so the desktop and
the phone build the same command.

* test(mobile): type the fake terminal create instead of casting its calls

* fix(mobile): pass the host's fork source as is; match the quoted args in the test

* test(mobile): give the fake host client the untyped mock the other client fakes use

* style: format the shared resume builder and phone fork files

* Fix mobile chat CLI fork creation on owning host

* Fix mobile chat fork lint and split create owner helpers

* Keep chat fork beside phone anchor across tab groups

* Combine mobile terminal runtime type imports

* Repair mobile create CI contracts after chat fork extraction

* Classify chat fork preflight refusals and preserve mobile create recordings

* Keep route-owned create cleanup quiet after unmount

* Preserve typed runtime repair refusal in chat fork preflight

* Preserve checked store through mobile chat fork preflight

* Register mobile chat fork runtime fixture before test setup

* Remove unused mobile create recording value

* style(mobile): format the chat fork create files; move fork reply messages into the fork feedback module

Formatting pushed the create hook past its line limit, so the fork-only reply message, partial-success warning and host capability check now live beside the other fork messages. Same order and copy as before.

* Keep quick command mutation aligned with formatted request

* Keep mobile chat CLI forks in terminal view

* Pass mobile chat resolution its existing controller arguments

* Match the mobile create fixture mock layout

* Keep mobile chat CLI forks open when placement is unavailable

* Brace placement contract fixture checks

* test(mobile): keep the prompt controller mock under its line limit after the main merge

* Expose pane admission in the runtime test fixture type

* Complete the chat fork contract controller fixture
2026-10-16 18:10:24 -07:00
Brennan Benson dd3bbf680e Catch up desktop chats from the newest page (#29125)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Bound desktop catch-up after independent request support

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Accept readonly client capability fixtures

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 17:46:54 -07:00
Neil a2933244e3 fix(ssh): link a moved project only to the server machine's own checkout, never a same-path project on another host (#29896) 2026-10-16 17:39:15 -07:00
Brennan Benson 99788a91fa feat(acp): a managed runtime installs only when asked, with progress and cancel (#29023)
* feat(acp): a managed runtime installs only when asked, with progress and cancel

One installation per runtime owns every change to its releases: it reclaims old ones once, first,
then installs one at a time. An install checks free space, downloads with byte progress, extracts,
hashes each member (abortable), writes the receipt, then publishes by rename, the commit point: a
cancel after it reports success. Reinstall rehashes the pinned release without downloading. A
status read waits, bounded, for a newer revision and rereads the installed release from disk.

* fix(runtime): Install repairs a damaged copy, says when unzip is missing before downloading, and reads a failed download plainly

- Install over a damaged pinned copy downloads and verifies a fresh one, then swaps the damaged
  folder into staging, so there is no dead end; status reports the damaged fact.
- A host that cannot unpack the archive (no unzip) reports it in status and refuses Install
  without downloading.
- A network failure while downloading reads "Couldn't download …"; the raw error goes to the log.
- Reclamation skips the whole pass when a release folder could not be read.

* fix(runtime): a status read that can't read the installed files fails instead of saying not installed, and extraction uses the extractor the preflight found

* fix(runtime): a host that can't report free disk space still installs, and says so in the log

* fix(runtime): a download that can't be fixed by retrying says so instead of blaming the network

* fix(runtime): Install unpacks the release itself, checks it starts before publishing, and Cancel answers with the outcome

- The managed install unzips in-process (fflate, bundled into main), so a host without unzip can install; the unzip preflight and the missingProgram status field are gone. The Node runtime download keeps its own extractor.
- A runtime may provide validate(): Install starts the downloaded copy once before publishing it, and an existing copy before reusing it; a failure reads "<agent> couldn't start on this host".
- No 'cancelling' phase: Cancel resolves once the work has unwound and returns cancelled, or succeeded when it had already committed.
- Byte progress every 250 ms; status rereads disk and PATH only on a first read or a phase change.
- The declared download size is checked only for an uncompressed body.
- An existing copy whose files hash correctly but whose receipt is missing gets its receipt rewritten instead of reading as damaged forever.

* refactor(runtime): move the steps that put one release on disk out of the installer

The installer class now holds only its lifecycle: the shared operation state, the one-at-a-time
queue, install, cancel, status, reclaim and progress publishing. Reusing a whole copy, the free
space check, the download, unpacking, checking each file, the start check, the receipt, the
publishing rename and the failure sentences move unchanged into managed-runtime-release-install.ts,
which gets the pin, folder, label, signal and two callbacks (progress, and the commit point right
after the rename) instead of the class. No behavior change; this keeps the installer well under
the file-length limit once Remove lands in it.

* chore(deps): lock fflate for the in-process runtime unzip

* fix(runtime): a failed download blames the release only for a proven mismatch, so a refused proxy reads as a network problem

* fix(runtime): runtime downloads skip the HTTP cache, so Remove frees the space and nothing is stored twice
2026-10-16 17:38:02 -07:00
Neil e23cfcdde8 test(native-chat): use an agent without chat modes in the paired-host permission tests (#29891)
#29335 gave Grok chats their own permission modes, so on a paired host
that predates per-chat modes a Grok chat is now checked like Claude and
Codex; that is intended. #29442's tests, merged a day earlier, used Grok
as the agent without modes. Use Gemini, which still has none.
2026-10-16 16:47:54 -07:00
Kelvin Amoaba d963f2a23a fix(mobile): draw your own messages as markdown, fold long ones and open attached pictures full size (#29075) 2026-10-16 23:27:45 +00:00
Brennan Benson 027eb0ec83 Session History: save the parse cache one row per transcript, so restarts re-read nothing (#29401)
* Persist session parse results beyond the memory cache limit

* Expire Session History parse rows by idle time instead of a count cap

The parse cache dropped its oldest row once it held 4,096, so a scan over
more transcripts evicted its own upcoming hits and re-parsed every file on
every scan. Rows now stay until no reader has used them for 24 hours, swept
after each completed History scan and each search-index pass. Only the live
parser state keeps a 4,096 count bound; dropping it costs one whole re-read
of a file that changed, never a re-parse of one that did not.

Removes the overflow directory and the orcad persistence wiring from the
earlier revision of this change.

* Expire only rows History lists, and keep the newest transcripts under bounds

The search pass ran the idle sweep every 20 s but never re-stores History's
unchanged rows, so with search on a day away from History dropped them all
and reopening it re-parsed everything. Only a completed History scan sweeps
now, and it expires only rows History listed (on a monotonic clock). Rows
only the search index or a title lookup created keep main's 4,096 count
bound, so an index build can't grow the scanner process without limit.

Scans store newest transcripts first, so every store-order bound dropped the
newest, most active files: the saved snapshot kept the oldest ~11k rows and
the resume bound stripped the newest files' parser state. The snapshot is now
ordered by transcript mtime, and both count bounds drop the tenth of their
rows with the oldest transcripts.

* Drop the 24 h idle sweep; History rows live for the process

The sweep let whichever listing ran last decide when a row died, and
listings differ in scope: a smaller one (the phone's Agent History, a lower
desktop limit) expired rows only a larger one used, so the next Unlimited
scan re-parsed them. Rows History lists now live as long as the scanner
process; the snapshot's newest-transcripts cap bounds what survives a
reload, and rows other readers made keep the 4,096 bound.

Also clears the resume bookkeeping for a row the unlisted bound drops in the
same store.

* Skip resume bookkeeping for a row the unlisted bound just dropped

Running the resume trim first let a row the unlisted trim then deleted push
the resume set over its bound and cost 409 live resume points.

* perf(ai-vault): store the Session History parse cache one row per transcript

The parse cache was persisted as one JSON file, rewritten whole after any
change and capped at 64 MB / 1M tokens (about 11k rows). Past that cap, a
restart re-read the oldest unchanged transcripts, and an agent writing while
History was open rewrote 10-27 MB every few seconds.

The scanner child now keeps the rows in their own SQLite file. A miss in the
in-memory map reads the row by path; changes are written per row, debounced
and chunked; deletes run immediately. A used_at column, refreshed at most
daily, expires rows nothing has used for 30 days, so rows for transcripts
deleted outside Orca have a way to die. The old JSON file is imported once
and removed. Electron main only records the path for the child.

The rebuild-on-corruption open moves out of the search index into a shared
versioned cache database helper used by both.

* fix(ai-vault): import the old parse cache at open and expire rows only for missing transcripts

Review round 1 on the per-row parse cache store:
- The old JSON was imported only by a History scan or search pass, and only
  into an empty database, but deleted either way. Tab-title sync writes a row
  first, so the import was skipped and the first History open after updating
  re-read everything. The import now runs when the database opens, before any
  lookup, write or delete can reach it, keeping rows already present.
- A 30-day last-use expiry also dropped rows for transcripts that still exist,
  so opening History less than monthly re-read them. Rows unparsed for 30 days
  are now checked on disk and dropped only when the transcript is gone, then
  the freed pages are reclaimed. The daily last-use rewrite goes away.
- 8 KB pages (a ~2.3 KB row filled a 4 KB page), busy_timeout before the WAL
  switch, and lookups served from rows still waiting to be written.
- Revert accidental pnpm-lock.yaml churn.

* chore: revert accidental pnpm-lock.yaml churn

* fix(ai-vault): never stat WSL transcripts from the parse cache recheck

A stat on a \\wsl.localhost path outside the scanner's running-distro
filter and gated WSL lane could boot a stopped distro or hang on a stuck
one. WSL rows now skip the existence check and age out after 180 days
unparsed. SQLite-backed session keys use the shared OpenCode path splitter.

* fix(ai-vault): register the parse cache database with the WSL mover census

Each schema keeps its own meta table again, so the shared open helper
creates none and the search schema's SQL is unchanged. The parse cache's
tables are recorded as not moved: a cache rebuilt from transcripts.

* fix(ai-vault): a forced whole read never reuses a row a failed database delete left behind
2026-10-16 16:24:27 -07:00
Brennan BensonandJae 47a5584ccb fix(acp): OMP, OpenCode and Grok chats follow their own permission mode (#29335)
* fix(acp): OMP, OpenCode and Grok chats run their own permission mode

An ACP chat's full access came from its agent's terminal Arguments, so OMP
and OpenCode, which have no bypass flag, asked on every tool call even when
the user chose Yolo. ACP chats now offer Ask for approval and Full access,
seeded from "Permissions for new chats", as Claude and Codex chats do; chats
from before take what they ran with, or the new-chat setting for an agent
that never had a bypass flag. Terminal launches are unchanged.

Co-authored-by: Jae <38941444+JaeCoding@users.noreply.github.com>

* fix(acp): hand the chat's permission mode to the ACP launch

The adapter narrowed the start to its identity and spawn token, so the mode
the host decided never reached the launch. Mobile's picker also showed
Claude's Ask wording for the other chat agents.

* fix(onboarding): Yolo mode also sets new chats' permission mode

Onboarding's Yolo mode says agents run without asking, but it wrote only the
terminal Arguments, so turning it off left new chats on Full access. It now
sets Permissions for new chats too, keeping an Accept edits or Approve for me
choice as it keeps custom Arguments. Pins that every ACP chat agent offers
Full access, and renames the backfill rule for what it decides.

* fix(onboarding): a replay left untouched keeps new chats' permission mode

Replaying onboarding, including from a paired browser whose Yolo switch starts
from its own defaults, rewrote the host's Permissions for new chats. It now
changes only when the person flips Yolo mode in this flow.

* refactor(onboarding): the Yolo switch's state lives in its own hook

Keeps the onboarding flow hook under the file-length limit after it began
passing whether the switch was flipped.

* test(native-chat): pin an OMP chat's pre-mode backfill at the record store

The host-install test's OMP row read no record; the backfill itself is
agent-agnostic, so pin it where the store writes it.

---------

Co-authored-by: Jae <38941444+JaeCoding@users.noreply.github.com>
2026-10-16 16:20:49 -07:00
Neil dd70294f0c fix(ssh): turn a host's orca CLI control on by default and stop resetting saved choices (#29856)
* fix(ssh): allow a host's orca CLI to control Orca by default, and stop resetting saved choices

An absent allowRemoteCliControl now means on; only an explicit false turns the
desktop browser bridge and relay-era orchestration commands off. Removes the
one-time reset of saved true values, the consent stamp writes and the reset
notice, which would have silently turned off hosts upgraded from stable.

* fix(ssh): keep the CLI control choice when adding a host from the sidebar

* fix(ssh): read an older client's omitted CLI control switch as off over RPC

* fix(ssh): check the managed fence before routing settings in browser bridge candidates
2026-10-16 16:08:37 -07:00
Brennan Benson 8c1ade9e44 Keep pending chat decisions available outside loaded history (#29121)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 16:01:58 -07:00
Brennan Benson 6dd7ab5552 fix(native-chat): identify the owner when resume creation is refused (#29119)
* 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): preserve resume refusal behavior and honest transcript feedback

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

* fix(native-chat): restore the refusal error import lost in the main merge
2026-10-16 15:57:54 -07:00
Neil f8089c787b fix(orcad): keep the integration credential key only in the owner-only key file (#29859)
* fix(orcad): keep the integration credential key only in the owner-only key file

Managed servers no longer create, read or wait on the macOS login Keychain or
a Secret Service keyring for their credential master key. A record an hourly
build pointed at the keyring is migrated once: the key is copied into the key
file when it reads back without UI within 5s, otherwise a new key file is
started and old credentials show the usual reconnect error. Removes the
locked-Keychain status fields, retry, the integrationCredentials.moveKeyToFile
RPC and the Settings notice.

* fix(orcad): serialize the keyring-record migration so concurrent starts agree on one key

* docs(orcad): note the migration lock

* fix(orcad): publish the migration lock with its PID and bound every wait

* refactor(orcad): reuse the file transaction lock for the keyring-record migration
2026-10-16 15:53:30 -07:00
Brennan Benson 23b60a5d07 fix(worktree): offer Force Delete with a plain warning when a checkout has submodules (#28878)
* fix(worktree): remove a clean checkout that contains submodules

Git refuses to remove any worktree with an initialized submodule unless --force is set, even when nothing is uncommitted. After that specific refusal, re-check the checkout including files inside the submodule and pass --force once. A dirty submodule still blocks the delete. A second --force is not added, because that would also clear a Git lock.

* fix(worktree): offer Force Delete for a checkout Git refuses over submodules

Drop the automatic --force retry. A forced removal deletes the submodule's
repository under .git/worktrees/<id>/modules, including commits, local
branches and stashes that were never pushed, and the parent's dirty check
cannot see them (the retry #24602 removed for the same reason).

Instead classify Git's "working trees containing submodules cannot be moved
or removed" refusal as a new Force Delete reason, 'submodules', after the
lock check and the force guard. The renderer classifies the error string,
so older SSH and paired hosts get it too with no wire change. The toast,
the delete dialog and the Space manager row show one plain warning naming
what Force Delete would lose instead of Git's text.

Keep --ignore-submodules=none in the pre-teardown dirty check so a user's
ignore setting cannot hide submodule edits from it.

Replace the retry test with one real-Git test: a clean checkout whose
submodule holds a local-only commit is refused without --force, stays
registered, keeps the commit, and its refusal classifies as 'submodules'.

* fix(worktree): clarify the submodule Force Delete warning and hide View for it

The warning now says Git deletes the workspace only with Force Delete, the submodule
refusal no longer offers View (there are no changes to show), the real-Git test pins
that Force Delete then succeeds, and a comment no longer describes the removed retry.

* fix(worktree): name submodules when a changed submodule blocks the delete, and clear a stale delete toast

With a commit inside a submodule that the parent has not recorded, the parent sees `M vendor`, so
Orca's dirty check refused first with the generic "It has changed files". Force Delete then destroyed
the never-pushed submodule commit without warning. On a dirty refusal the check now asks
`git status --porcelain=v2` whether a changed entry is a submodule, and if so reports a refusal the
app shows with the submodule Force Delete warning. The detail keeps the dirty sentence, so older
apps still offer Force Delete.

After the Space manager deleted a workspace (its row Force, or a later delete), the failure toast for
that workspace stayed up and still offered Force Delete. A Space delete now dismisses it.

* fix(worktree): warn about submodules on any refused delete of a checkout that has them

Review found the warning still vanished when a submodule commit was already recorded and some other
file was dirty: the parent's status shows no submodule change, yet Force Delete destroys the commit.
The changed-files check now asks whether the checkout has a populated submodule (a .gitmodules path
holding a .git, as Git's own refusal checks) instead of whether one changed. The refusal keeps the
changed paths after the detail, and View stays available since there are changes to view.

* test(worktree): mock the .gitmodules probe as missing in the preflight unit tests

* refactor(worktree): attach the dirty-check status without a type assertion

* fix(worktree): key the delete failure toast by host and worktree id

A Space delete on one host must not dismiss another host's failure toast for a worktree with the same id.

* fix(worktree): dismiss the failure toast by the store row's host after a Space row Force
2026-10-16 15:23:13 -07:00
Brennan Benson 702ffc7bc4 fix(orchestration): native-chat agents get their mail notice right away, the same way for every agent (#28540)
* fix(orchestration): steer mail notices into a busy native chat's running turn

A native-chat coordinator's mail notice always queued as a card until the whole turn ended, so a
coordinator busy in a long command never saw its workers' reports. Agents that take a message
mid-turn (Claude, Codex, Pi) now get the notice at once; agents that would cancel their turn for
it (ACP) still queue it.

* fix(orchestration): drain mail that arrives while a notice is pending

* Send orchestration mail the same way to every chat agent

A mail notice is a plain send for every agent, as a person's own mid-turn message is.
While a pending approval or question waits on the person it is held, and the turn's
working edge after the answer sends it.

* Hold orchestration mail at live prompt admission
2026-10-16 14:44:59 -07:00
Kelvin Amoaba fe41a9a5e8 fix(clipboard): paste CleanShot JPEG screenshots stored under the macOS PNG type (#29685) 2026-10-16 21:44:15 +00:00
NeilandClaude e0e2001701 refactor(legacy-relay): one ask-wait clamp and no needless exports (#29835)
optionalAskTimeout computed exactly what cappedWait does (an undefined timeout stays undefined;
anything else clamps to 0..ORCHESTRATION_ASK_MAX_TIMEOUT_MS), so the ask call uses cappedWait.
Three functions used only inside their own file lose their export, and the per-host switch's
doc names the orchestration commands it has gated since #29571.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 14:11:36 -07:00
Jinjing 005f06bbc7 Run automated workspace setup in a dedicated Setup tab (#29819)
* Run automated workspace setup in a dedicated tab

* Preserve native-chat setup route coverage after placement retirement

* Preserve Setup tab design and independent review evidence

* Classify activation test fixtures consistently for routing checks

* Sync runtime English catalog after removing setup preferences

* Update listener savings model after retiring setup action
2026-10-16 14:05:50 -07:00
Neil 3fdcbfaa1e fix(terminal): open the picked Windows shell on a managed host instead of typing it into PowerShell (#29836)
* fix(terminal): open the picked Windows shell on a managed host instead of typing it into PowerShell

* fix(terminal): keep the shell pick through materialization and folder runtimes

* fix(terminal): refuse a shell pick on agent launches and update paired-tab expectations

* test(terminal): type the shell-selection runtime fixture instead of casting
2026-10-16 13:45:59 -07:00
NeilandClaude b3299473e6 refactor(resume): build the whole recorded resume recipe in one place (#29830)
resolveResumeLaunchRecipe now also carries the recorded agentCommand and ompResumeFilePath, so
the three resume lanes stop repeating the same conditional spreads. The rule is unchanged: a
resume relaunches with exactly what its launch recorded, or nothing.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 13:33:52 -07:00
NeilandClaude cd42d59a77 refactor: drop the unused sync spool drain and thread-goal wrapper; reuse the shared absence check (#29820)
The synchronous drainAgentHookSpool and currentAgentSessionThreadGoalBySequence lost their
last production callers earlier today; tests now exercise the functions production calls.
git-object-quarantine reuses isDefinitiveAbsence instead of a private ENOENT/ENOTDIR copy.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 13:33:42 -07:00
Neil 1e39482f9d fix(wsl-cli): keep the WSL CLI status check from timing out on a cold or slow-profile distro (#29814)
* fix(wsl-cli): keep the WSL CLI status check from timing out on a cold or slow-profile distro (#29497)

* fix(wsl-cli): run install/remove without the login PATH; a PATH-check timeout reports PATH unknown
2026-10-16 12:45:01 -07:00
Brennan Benson b3048a2b9c Wait for agent readiness without blocking native chat (#27925)
* Finish structured agent readiness before delivering held work

* Adapt permission fixtures to startup lifecycle

* Keep model discovery for chats before their first start

* Use one host readiness promise for provider operations

* Await provider opening in the execution host

* Await Claude initialization in host-owned open

* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Keep startup waits cancellable and preserve operation order

* Keep live choices and delivery order while agents start

* Apply startup picks and let parked messages release operations [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Align startup fixtures and preserve observed Claude exits

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Honor the before-input callback in the startup reply fixture

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Align carried-message fixture with startup event contract

* Keep launch-only permission picks usable while prior writes wait

* ci: rerun on main with the locale fix

* Preserve goal retries and chat launch selections

* Give launch regression its request identity

* test(native-chat): type the provisional-launch hook's session prop
2026-10-16 12:44:35 -07:00
Kelvin Amoaba 4446d4f628 fix(native-chat): stop following the end when the reader scrolls over a visual (#29116) 2026-10-16 18:11:33 +00:00
Neil 8fad95829a fix(agents): direct work-item and checks-fix launches quote for the execution host (#29716)
* fix(agents): direct work-item and checks-fix launches quote for the execution host

GitHub Project Start, PR checks Fix and the source-control/checks preflights
resolved their launch platform from the client OS whenever the repo had no SSH
connection, so a Windows client starting work on a paired Linux Orca server sent
a PowerShell-quoted --prefill to bash. They now read the execution-host facts
#27259 introduced (server-reported OS, SSH-reported OS, WSL), and the
path-shape resolver is removed so one helper decides.

* fix(agents): resolve direct-launch and preflight platforms from the repo owner

Review follow-up: a WSL worktree on a paired Windows server quotes for Linux,
the owner-row fixture reports its server OS, and the source-control preflights
read the repo owner rather than the focus-defaulted worktree route.

* fix(agents): preflight platform honours a worktree's own host

Review follow-up: a per-worktree host outranks the repo owner, the host-platform
test's mutable client platform is typed, and the source-control panel fixture
carries the host slices.

* fix(agents): a server-projected SSH worktree reads its server's SSH state

Review follow-up: the preflight resolver keeps the runtime owner for nested SSH
worktrees instead of reading this client's own SSH connections.

* fix(agents): cmd.exe pastes a multi-line draft instead of prefilling it

Review follow-up: now that direct launches pass the local Windows shell, a
multi-line --prefill under cmd.exe would submit at its first newline.

* fix(agents): direct launches quote for the shell their tab spawns

A project's Windows runtime can override the terminal setting (WSL setting,
Windows runtime spawns PowerShell), so resolve the tab's effective shell before
quoting. The cmd.exe parity row now expects cmd quoting.
2026-10-16 08:46:22 -07:00
Jinwoo Hong 90addf474e Number layout stream updates so a layout command's answer names the update holding it (dark, build switch off by default) (#29696)
* Number layout frames so a layout command's reply names the frame holding its effect (dark, build switch off by default)

* Report every frame's position, test the reply's frame end to end

* Rewrap sendLayoutApply's doc comment
2026-10-16 07:22:01 -07:00
Jinwoo Hong dbc1e18b07 Route the remaining window pane-layout changes through main (dark, build switch off by default) (#29689)
* Route the remaining window pane-tree writers through main (dark, build switch off by default)

* E1b review fixes: valid pane ids, keep a mounted split, automatic closes skip the user mark, sole-pane exit guard
2026-10-16 07:04:34 -07:00
Neil e40f8ec115 test: remove real waits and repeated catalog setup (#29690) 2026-10-16 06:53:02 -07:00
Jinwoo HongandClaude 7b7148d0a9 Open into a new split in one layout command (dark, build switch off by default) (#29655)
* Open into a new split in one layout command (dark, build switch off by default)

Main makes the window's new split group and its first tab (terminal, editor
or browser) in one apply, so main never refuses the open for a group it lacks
and never stores an empty group. The window derives the split from its own
group tree in the three existing switch-point recorders.

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

* Open beside an emptied root split, and into a group main already holds

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

* Describe both openInNewGroup fallbacks in its doc comment

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

* Test: no split is read back beside a sibling that is itself split

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 06:09:24 -07:00
Neil 222ede7f5b fix(ssh): reopening a workspace-slept agent tab on a managed host resumes it, without widening its permissions (#29672)
* fix(ssh): reopening a tab slept from a paired desktop resumes its agent on the managed host

A paired client's worktree sleep stops the host's PTYs, but the resume
record its renderer writes never reaches the host, and the host is what
answers the tab open. Capture the record on the host from its own hook
store before the stop, applying the shared holder rule (an agent the
hook store proved exited leaves no record).

* fix(ssh): keep the launch recipe and record acknowledged stops when inventory fails

* fix(ssh): only a proven agent exit retires a pane's earlier sleep record

* fix(ssh): stamp the captured record's SSH host and keep a saved launch recipe

* fix(ssh): read the session id after the presence probe that may outlast it

* fix(agents): a resume reuses only its launch's recorded args, never permission-widening defaults

Resuming a session started as a hand-typed `claude` filled the missing
launch recipe from settings defaults, which carry the permission bypass.
Host tab-open resume, window cold restore and worktree wake now share
one rule: the recorded recipe, or none.

* fix(ssh): a WSL relay tag on a captured record is local, not another host

* fix(ssh): a capture retires when the hook store names another agent in the pane
2026-10-16 06:05:08 -07:00
Jinwoo Hong ebb9e3243a Record a restored editor tab's move to its owning workspace through main (dark, build switch off by default) (#29653)
* Record a restored editor tab's move to its owning workspace through main (dark, build switch off by default)

* Refuse non-editor and duplicate moves; route the move by workspace

* Type the move routing test's command
2026-10-16 05:17:45 -07:00
Jinwoo HongandClaude dddad7d66a Record sleep and wake through main (dark, build switch off by default) (#29350)
* feat(layout): add the dark layout.apply channel and route pane splits through main behind ORCA_LAYOUT_OWNER

A build made with ORCA_LAYOUT_OWNER=main registers layout.apply: main applies the
window's layout command to the stored partition through the shared applyLayoutCommand,
minting new ids. A user split then waits for main's answer and mounts the pane id main
minted. Every other build substitutes 'window', so neither branch is compiled in.

* fix(layout): write only the records a layout.apply command touched

A load-then-save rewrite also normalizes records the command never touched. The command's
effect is now the diff between the save before and after it, written onto the stored
partition record by record. A split whose main reply says a paired server holds the tab
splits as today.

* fix(layout): write a load-supplied record whole and check that patched partitions reload to the command's result

* feat(layout): close, rename and equalize panes through main behind ORCA_LAYOUT_OWNER

In a layout-owner build the window's pane close, title change and equalize go through
layout.apply and the window shows main's answer. A window pane close keeps the legacy
close's rules: the tab's last pane is refused, a pane restarted since main read the close is
left alone, every partition holding the tab closes it, and the tab's remote session id follows
the remaining pane. The rules sit behind a command field only layout.apply sets.

* fix(layout): close the tab when a racing pane close leaves the window one pane

* fix(layout): leave an unmounted tab alone when main refuses its last pane

* feat(layout): move panes and save divider ratios through main behind ORCA_LAYOUT_OWNER

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): a taken pane drop whose pane closed does nothing

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): pin the window's pane move to main's saved tree, and the drops main cannot take

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* fix(layout): close a user's pane even if its terminal restarted, as today

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): pin the restarted-pane close directly, not through a made-up profile state

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* fix(layout): leave a split to today's path only when the workspace's row names a paired server

The split switch point asked which host to route to with a helper that falls back to the focused
server, so a focused server could claim a workspace whose row names no owner. Use the same owner
test the tab mirror uses.

* test(layout): type the split owner mock without a cast

* test(layout): type the layout-owner command mocks with the real signature

* test(layout): type the gesture command mock with the real signature

* fix(layout): close the dark path's gaps: unmounted split panes, refused or failed splits and moves, ratios during a move, release guard

- A pane main saved but the window could not mount (source pane or manager gone during the await)
  is closed again in main, so it does not come back on restart.
- A split main refuses because its copy lacks the workspace, tab or pane, or a failed call, splits
  as today; a refused or failed move moves as today.
- Divider ratios committed while a move is in flight wait for the window's move, then go to main.
- The orcad bundle gets the ORCA_LAYOUT_OWNER define when the switch is on.
- Packaging and the orcad template refuse ORCA_LAYOUT_OWNER=main and any output a dark build marked.
- workspace-layout-record-patch.ts: no non-null assertions; plain comment for the map depth.

* test(layout): count a split pane as mounted from the split mock's own results

* fix(layout): act as today whenever main lacks the target or the call throws, not just for splits

- sendPaneCommandToLayoutOwner runs asToday for workspace/tab/pane_not_found and a thrown call, so
  close, rename and equalize fall back like split and move; a handler that throws no longer runs the
  fallback too.
- A close main refuses as its last pane, while this window still has others, closes as today.
- A move whose local step throws still releases held divider ratios.
- Drop the orcad output marker: nothing read it; the template build refuses the switch itself.

* test(layout): plain names for the last-pane fallback test and the marker comment

* docs(layout): say plainly what an indeterminate write can leave behind

* fix(layout): finish the main merge with this branch's versions of the layout-owner files

* test(build): copy the layout-owner build guard into the compiler-cache checkout fixture

* feat(layout): send tab rename, color and pin through main behind the layout-owner build switch

In an ORCA_LAYOUT_OWNER=main build, the tab bar's rename, color and pin ask main's layout.apply
first; the window then runs today's action. When main lacks the tab or the call fails the
window acts as today; a paired server's workspace keeps today's path. Pinning a tab now also
ends its preview in main's copy, as the tab bar's pin does. Other builds compile the switch out.

* fix(layout): leave a pinned tab's preview mark to the window until main opens files

Changing the shared pin command would change what a paired client's pin does in every build.

* fix(layout): write a folded legacy row in its loaded form when a layout.apply command touches its claim

A legacy terminal row the load folds into a tab-bar tab stays stored as is. Closing the tab it
repeats, or giving it a tab-bar entry by moving it, let the next load resolve the claim the other
way: the row came back or took the other tab's terminal. Such a command now writes the row as
the load had it; every other record still stays as stored.

* feat(layout): accept tab moves, group splits and group sizes on layout.apply

Group sizes have no tab, so they go to the workspace's routed partition.

* refactor(layout): move the window's layout.apply sender out of the terminal pane folder

Tab groups send through it too; it now also acts as today when main lacks the group.

* feat(layout): reorder, move and split tab groups, and save group sizes, through main behind ORCA_LAYOUT_OWNER

A tab drop and Move Tab to Split wait for main's answer and then show main's placement, with main's
new group id; group divider sizes are drawn while dragging and saved by main when the drag ends.
Closing an empty group needs no command: main never holds an empty group.

* docs(layout): wrap the layout-owner sender's doc comment

* fix(layout): review fixes for tab moves through main

- Read a folded row's terminals with the loader's own claim rule, so rows sharing only an SSH
  session are covered too, and drop the row's remote session entry with it.
- The record-patch test allows a folded row only in its loaded form.
- A tab drag ends at once; main's answer shows the move after.
- A lone group main moved whole is placed so only when this window's group is lone too.

* refactor(layout): adopt only a folded row's pane layout beside its row

A row's remote session entry is carried through load and save unchanged, so adopting it from the
load changed nothing (the earlier commit's message overstated it).

* feat(layout): record a window's terminal tab close through layout.apply

closeTabs can now record each terminal tab it closes, or does not find, as closed for the given
reason (pruned as a partition's records are), so a start that commits later is refused, as today's
close records it. layout.apply takes one tab's close with the window's reason: main forces it in
every holder partition, marks a user close's launches, stops no terminal (the window already did,
sparing any another tab shares), and leaves a workspace it lacks to today's close. A sibling the
load supplied that the close only renumbered is no longer written.

* feat(layout): send the window's terminal tab close to main behind ORCA_LAYOUT_OWNER

The window still closes the tab and stops its terminals itself; main records the close through
layout.apply instead of the close intent, which stays for paired servers and when main lacks the
workspace.

* fix(layout): review fixes for the tab close through main

- The window's close sends through a store-free sender, so the store no longer imports itself.
- Main marks a user close's launches by the terminal's own id.
- A recorded close prunes the whole partition's close records, as today, not one workspace's.

* feat(layout): open and close browser tabs and pages through main behind the layout-owner build switch

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

* feat(layout): open editor tabs under the window's id, per split, and promote previews through layout.apply

openEditorTab takes the window's tab id (the same open again is a no-op, a held id is refused),
dedupes per group so a file open in another split gets its own tab, pins an existing preview on a
non-preview open, and places the new tab before closing the replaced preview so a group whose only
tab is its preview keeps its id. layout.apply accepts openEditorTab, promotePreviewTab and a
closeTabs with no terminal close record.

* feat(layout): record the window's editor tab opens, closes and preview promotions in main behind ORCA_LAYOUT_OWNER

The window still draws first (openFile returns at once and its callers act on the tab by id).
Main then records the tab under the window's ids and decides the group fallback and preview
replacement by the same shared rule. Switch points: the one editor tab create, the unified tab
close, preview promotion on activation, edit and make-permanent, and the reopened tab's position.

* refactor(layout): name the browser sender's no-op so the window bundle keeps its names

* feat(layout): move a window's dragged-out pane into a new tab through layout.apply behind ORCA_LAYOUT_OWNER

* refactor(layout): keep the topology commit under its line limit

* Record sleep and wake through main (dark, build switch off by default)

In a layout-owner build, main records each sleep the window decided and each
wake of the one pane it showed: a workspace sleep or a pane hibernation sends a
'sleep' per tab once the terminals stopped, and a resumed pane sends a 'wake'
naming only itself. The window keeps deciding, stopping terminals and saving
its own records.

* fix(layout): keep a served browser tab's open, address and close in order while main writes it

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

* fix(layout): record sleep and wake in every partition holding the tab

Review fix: a tab a legacy relay reattach left in two partitions gets its
sleeping record in both, as a pane close does. The pane hibernation comment
no longer claims a stop on the branch that stops nothing.

* fix(layout): review fixes for the window's pane drag-out through main

* fix(layout): review fixes for editor tabs through main

openEditorTab refuses a group main lacks instead of placing the tab in another group, and gives
a workspace with no group (the window keeps its emptied root group; main closes it) the sender's
group id. Tests pin close-all, the emptied root group, the refused open into a fresh split, and
that the window sends an edit-mode file's record exactly as its session save keeps it.

* docs(layout): note what a refused served-tab close leaves

* refactor(layout): re-key a moved pane from the move alone

* test(layout): assert the window drew the tab main refused

* refactor(layout): share one record-only layout.apply sender and inline the id schema

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 04:35:28 -07:00
NeilandClaude Opus 5.5 25d83af9a7 fix(main): classify integrationCredentials RPC ownership (#29638)
#29608 added integrationCredentials.moveKeyToFile without an ownership entry, failing host-operation-ownership.test.ts on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:35:25 -07:00