Commit Graph
13125 Commits
Author SHA1 Message Date
Brennan Benson 884be9ad39 feat(native-chat): show each chat's resume as it happens, and start more at once (#26361)
* feat(native-chat): show each chat's resume as it happens, and start more at once

Resuming interrupted chats sent one request for the whole batch, and the
host answered only after every agent had replied, so "Resuming 21 chats"
stayed up until the slowest chat answered and the dialog could only say
"Resuming…" for every row. The host also started 3 agents at a time.

The desktop app now sends one request per chat, so each chat's result
lands as soon as it has one. The host keeps one start limit across all
of those requests, sized to the machine's cores instead of a fixed 3.

The status bar reads "Resuming chats 9/21". Reopening the dialog mid-run
shows a gray spinner where each resuming chat's checkbox was, with its
step (waiting to start, starting, waiting for a reply) and time in the
tooltip, then a check or the failure row when it settles. A line above
the list counts where the chats stand, with filters for In progress,
Resumed and Need you.

* fix(native-chat): publish restart progress from one host action

* Keep chat resume progress stable through reply handoff

* Fix restart progress ownership after skipped requests

* Keep completed chat resume summaries until viewed and closed

* chore(i18n): drop status-bar keys the merge restored
2026-10-08 10:13:54 -07:00
Brennan Benson 7bdb0b67e6 Native chat: inline visuals for Grok, OpenCode and OMP chats (#26530)
* Native chat: give Grok, OpenCode and OMP chats inline visuals

Each ACP agent loads the chat-visuals skill its own way for one launch,
without touching the user's agent config: Grok through agent
--plugin-dir, OpenCode 2.x through inline skills.paths, OMP through a
PI_CONFIG_FILES overlay that keeps the user's own skill folders. A chat
whose agent can't load it starts without visuals.

* Native chat: load OMP chat visuals through --plugin-dir

OMP's settings layers replace lists, so the PI_CONFIG_FILES overlay hid
a repo's or profile's own skill folders in Orca's OMP chats, and its
files were never removed. omp acp takes --plugin-dir like Grok, which
adds the skill beside the user's own plugins and skill folders and
leaves their config untouched.

* Native chat: note OMP releases that skip --plugin-dir skills
2026-10-08 10:12:44 -07:00
51e28956eb fix(native-chat): never refuse a queued card because 20 were waiting, and keep the queue from covering the chat (#26553)
* fix(orchestration): keep automatic mail out of the user's message queue

Orchestration mail reached a busy chat as ordinary queued message cards, and
the queue refused at 20 unsettled cards or 1 MiB with a bare invalid
refusal, so automatic notices could fill it and refuse the user's own next
queued message. Mail now stays unread while the chat is working, waiting on
a prompt or holding unsettled cards, and is coalesced into one delivery when
it becomes free; batch identity is recorded so nothing is delivered twice.
Human drafts no longer share an aggregate quota, and a failed settled
receipt keeps its code, details and message for diagnosis.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native-chat): never refuse a message because automatic mail filled the queue

Drop the 20-card queue limit and the mail-delivery rework; keep only a byte
bound on what queued cards publish (it rides every opening frame beside the
history page), refused with a reason that says what to do. The desktop queue
list now scrolls instead of squeezing the transcript.

* fix(native-chat): keep room for the person's message in a queue other agents fill

Other agents' cards stop one maximal message short of the queued-bytes bound,
so background work alone can never refuse a person's send. The person's newly
queued card scrolls into view, and the refusal's step and host message now name
only what the person can do.

* fix(native-chat): open a loaded queue on its next card and scroll before paint

---------

Co-authored-by: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:04:28 -07:00
Brennan BensonandClaude Opus 5.5 17703d0f53 fix(native-chat): never refuse a user's write because the retry record is full (#26543)
* fix(native-chat): never refuse a user's write because the retry record is full

The record that lets a retried chat write or agent launch replay instead of
running twice refused every new write once it held 512 ids for one caller or
4,096 in total. Background agent traffic fills that in a normal day, so the
user's own message was refused with "too many requests in the last day".

Remove the count limit from the durable record, the in-memory launch record
and the SSH relay's launch record; each entry still lives exactly as long as
the retry window it protects. Clients still understand the refusal from older
hosts, and its message now says to update Orca on that computer.

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

* fix(native-chat): drop leftover cap wording and dead launch fallback; cover create records past the old cap

* fix(native-chat): keep refusal and mailbox comments true after merge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:55:51 -07:00
Neil 907cb6e603 ci: restore five unit runners and prune source-only tests (#26437)
* ci: reduce unit runner demand and reuse the loaded preview frame

* test: remove source spelling probes without behavioral assertions
2026-10-08 07:10:24 -07:00
Neil 89ba968230 ci: keep shared unit fixture out of package and browser jobs (#26575) 2026-10-08 06:14:29 -07:00
e3639ef8d6 Phase 3.5: fix the top SSH/remote issues at the root cause (#26483)
* fix(remote-runtime): stop arm64 Linux servers dropping clients every heartbeat (empty WebSocket write EFAULT) (#26200)

* fix(remote-runtime): never write an empty WebSocket payload on its own (arm64 Electron EFAULT)

Patch ws@8.22.0 Sender.sendFrame so a frame with an empty payload is written
as its header alone. Under Electron on arm64 kernels with 39-bit virtual
addresses, socket.write() of a zero-length buffer fails with EFAULT and kills
the connection, so every empty heartbeat ping, auto-pong and close dropped
the remote runtime with 1006. Wire bytes are unchanged.

* test(remote-runtime): run the ws empty-payload guard on real Node

Bun resolves 'ws' to its built-in client, so under the Bun test project the
guard exercised Bun's implementation instead of the patched package.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): ship the session scanner child so agent session history lists on orcad hosts (#26309)

orcad built every forked child except session-scanner-service-entry.js, so
aiVault.listSessions always failed on an orcad host. The resolver also skipped
the adjacent entry whenever isPackaged was true, so shipping the file alone
would not have been enough.

- build every ORCAD_CHILD_ENTRY_POINTS entry from one loop and add the scanner
- add it to ORCAD_ARTIFACTS so a deploy missing it fails integrity checks
- share one forked-child path resolver (asar layout vs flat app root) between
  the watcher and the scanner
- smoke-fork the built scanner during build-orcad under both runtimes

Fixes #24105

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(remote-runtime): one owner for a remote pane's binding and recovery (#26308)

A remote pane no longer latches in connecting on terminal_not_found, holds
keystrokes typed during same-terminal recovery instead of dropping them,
reports transport-owned retries so the pane is not remounted mid-retry, and
no longer infers absence from an inventory that predates the leaf.

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(e2e): move apt off the Azure mirror on runner images that use a mirror list (#26335)

Ubuntu 24.04 runner images resolve apt sources through /etc/apt/apt-mirrors.txt, so rewriting only the source lists left package downloads on azure.archive.ubuntu.com, which intermittently fails ('Ign:' on every package). Rewrite the mirror list too, and retry the install once with --fix-missing.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): remote snapshot replay keeps scrollback it cannot replace (#26319)

Desktop subscribers of a remote terminal received screen-only snapshots,
and every pushed snapshot replay cleared the pane's scrollback with ESC[3J.
A reconnect, a remount, or a recovery after a dropped frame therefore wiped
history the host never resent.

- Host: desktop subscribe snapshots use the same scrollback-row ladder as
  mobile, and SnapshotStart now carries an optional scrollbackRows field.
- Client: a pushed snapshot clears scrollback only when the host says it
  carries history; screen-only images (scrollbackRows 0, or older hosts
  that omit the field) repaint the screen and keep the pane's history.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(daemon): clean launch env, validate client frames, fit the socket path (#26306)

* fix(daemon): clean launch env, validate client frames, fit the socket path

- Drop Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: marker from the daemon,
  its systemd-run scope launch and every PTY env (#25580, #21119).
- Use the durable user-manager scope only when that manager outlives the
  caller: lingering on, or the caller already runs inside it (#24201).
- Shape-check hello and request frames, and catch request rejections, so a
  malformed frame closes one connection instead of the daemon (#17841).
- Move the daemon socket to a short per-uid private dir when the data root
  would overflow sun_path (#17840).

* fix(daemon): validate a relocated socket dir before any connect, not just before bind

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): keep crediting a PTY after its recovery delivery is canceled (#26341)

A canceled recovery delivery released its source spans while the renderer
and model could still settle them; the stale projection then threw on every
later ACK, so the next delivery was never credited and the relay paused the
PTY for good. Settling or transferring a span of a closed delivery is now a
no-op.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential (#26337)

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential

Every RPC method now declares a required permission, and RpcDispatcher checks it
against the caller's scope on every transport (owner, mobile, paired runtime,
SSH bridge). The SSH relay bridge no longer spawns the client CLI with owner
authority: each invocation gets a short-lived credential bound to its SSH target,
revoked when the child exits. Without the new per-host opt-in, that credential
reaches only its own host's terminals; with it, workspace methods but never
desktop control, accounts, settings writes, skill install/share, pairing or host
admin. Paired runtime clients need a pairing-time grant (orca serve
--grant-desktop-control) for computer.* actions.

Fixes #23835
Fixes #25259
Refs #18269

* fix(rpc): let a bridged SSH worker report to its own coordinator; allowlist paired-runtime permissions

Without the per-host opt-in, orchestration send/ask/reply/check/inbox now pass when the caller is a
terminal on the bridged host, binding pane key, payload Dispatch, run and recipient to that caller's
own orchestration (recipient off-host only as the creator of its active Dispatch).

Paired runtime clients get an explicit standard permission set; pairing-admin and desktop-control
are grant-only.

* fix(rpc): resolve a bridged SSH caller's own Run and Dispatch mailboxes before admission

Mail routing stores a worker's follow-ups under dispatch:<id> and worker
questions under run:<id>, so comparing the original recipient to the caller's
terminal handle refused an SSH worker replying to its coordinator, an SSH
coordinator answering its own Run's question, and explicit sends to the
caller's own run:/dispatch: address. The bridge now resolves the exact
mailboxes the caller's live pane and process read (as check does) and admits
replies and recipients against those.

* refactor(rpc): parse SSH bridge selectors with a typed schema instead of Reflect.get

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(projects): remove a project only on the host the user picked (#26340)

* fix(projects): remove a project only on the host the user picked

removeProject now requires a hostId and matches that host's row exactly.
The bare-id fallbacks (unique candidate, focused host) could resolve to
another host's live project and delete it. Orphaned SSH purge and group
deletion now pass each row's own host.

Fixes #13071

* fix(projects): group delete removes only the group's member rows

Same-id rows on other hosts outside the group were removed because membership
was rebuilt from bare ids after the cascade cleared it. Capture (id, host)
member pairs before deletion and remove only those.

* fix(projects): never let a stale catalog widen a project removal to every host

The renderer chose id-only `repos:remove` when its own catalog showed no
same-id row on another host. A stale catalog (another window, a paired
client, or a pending refresh) hid that row, and main then deleted every
host's registration and saved workspace metadata.

- The local catalog route now always calls `repos:removeForHost` with the
  owner row's host, for local, direct SSH, git and folder projects.
- `Store.removeProject` refuses an id that rows on more than one host share,
  so no id-only caller can delete another host's row.
- Setup rollbacks remove the just-created row by its host.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows (#26348)

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows

- launchServeMode registers the SSH handler layer before RPC binds, so a windowless
  'orca serve' lists and connects the profile's SSH targets (#25886, #8489).
- Under serve, the app-menu Quit/Exit, tray Quit and renderer-recovery Quit close the desktop
  windows instead of quitting the process (#15537). Signals, supervisor stops, relaunches and
  update installs still quit normally; on macOS a signal reaches before-quit the same way
  Cmd-Q does, so the guard lives on the user commands, not in before-quit.

* fix(serve): SSH profiles serve on Electron; native macOS quits and frozen windows no longer trap or stop serve

- orca serve: a profile with saved SSH targets stays on the Electron host by default, since
  orcad has no SSH stack yet (#25886, #8489). ORCA_SERVE_RUNTIME=orcad still forces orcad.
- macOS serve: Dock > Quit, app-switcher Cmd-Q and other native terminates only close the
  desktop windows (#15537). Signals, supervisor stops, relaunches, update installs and
  logout/shutdown still quit.
- The serve signal listeners now reclaim SIGINT/SIGTERM/SIGHUP from Electron's native handler.
  Any signal that had a Node listener before app ready was left with Electron's handler, so
  these listeners never ran.
- A serve user Quit closing a frozen renderer's window arms the same 10s ack deadline as a
  desktop quit, and skips minimize-to-tray.

* fix(serve): stay on Electron when the profile's SSH targets cannot be read yet

A valid older-schema profile is rejected by the read-only probe, and the
selector took that as "no SSH", so a default serve before the first
migration started orcad with an empty SSH registry. Fail closed instead.

* refactor(window): type user-Quit close marks as a named window slice, not object

* test(serve): pass the desktop-control grant default in the headless SSH launch fixture

* test(serve): run the serve-selection SQLite fixtures in the Node runtime project

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods (#26316)

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods

Full-width in-place row rewrites rebuilt the row string per character
(O(width^2)), a long chunk past the line cap spliced the row array once
per newline (O(lines x cap)), and multi-frame chunks summed every
cursor-up so they fell back to the whole-tail path. Rows now hold
mutable cells joined once, capped rows are dropped in batches, and the
redraw window uses the net cursor reach.

Fixes #11315

* fix(terminal): do not rebuild a redraw row the cursor only revisits

Rows stayed cell arrays once touched and re-joined on every newline, so a
cursor revisiting one wide row thousands of times cost O(width) per
newline. Runs now splice into the row string, a row switches to cells
only after 32 edits between snapshots, and cells are joined once and
dropped.

* fix(terminal): keep one cached row per chunk so unchanged redraw revisits cost O(1)

A row now keeps its cells, right-trimmed snapshot and content end across
newlines and only invalidates them on a real content change; identical
writes leave it clean. Adds a work-accounting matrix over restore, the
append path and live onPtyData, plus a per-character row oracle fuzz.

* fix(terminal): return redraw rows to flat strings with cached snapshots

Cells made every changed snapshot a full-width join, which was far slower
than the old flat-string splice. Rows are flat strings again, written one
run at a time; each keeps a cached right-trimmed snapshot and content end
that only a real change invalidates, so identical rewrites and cursor-only
revisits stay O(1). A wall-time test now times every review probe against
a vendored copy of the old row model on restore, append and onPtyData.

* test(terminal): read runtime leaf and PTY tail through typed members

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): relay fallback runs Orca's pinned Node by default, never a host npm install (#26303)

* fix(ssh): relay fallback runs the pinned Node ladder by default, never a host npm install

A host whose connect falls back to the relay route now gets the A/B/C ladder
(Orca's uploaded Node and prebuilt addons) unless it opts into Host Node. A
refused rung C steps to rung D with an actionable reason instead of compiling
native deps on the host.

* fix(ssh): mark the opt-in Host Node relay runtime as unsupported on the SSH host card

The relay fallback now defaults to Orca's pinned Node ladder; the host-npm path is
reachable only by an explicit per-host Runtime or ORCA_SSH_REMOTE_RUNTIME override.
When a relay actually launches on it, main publishes hostNodeRuntime on the
connection state (optional, admitted field-by-field for paired clients) and the
SSH host card shows an unsupported-configuration warning.

* fix(ssh): keep the relay runtime ladder honest about what it cannot verify

- NixOS's stub loader refusing Orca's generic Linux Node (exit 127, 'Could not
  start dynamically linked executable') is classified wrong_libc, so the ladder
  steps on to the host Node instead of failing the connect at rung A.
- In plain SSH mode the shell provider answers absent only for shells it saw
  exit; a PTY id it never minted (one an earlier relay may still run) is
  unverifiable, never exited.
- The rung D message offers Host Node only when the host's own Node refused the
  prebuilt addons, and calls it an unsupported configuration; a download,
  unresolved-host or Windows refusal gets no Host Node advice.

* fix(ssh): keep the host-Node relay where the ladder can't judge the host

A client without Orca's runtime artifacts (a dev, e2e or source build with no
orcad template, or an offline client) refused rungs A and C with
artifacts_unavailable, and the connect settled on rung D. That reason describes
the client, so it now lands on the host-Node relay it had before the ladder,
marked unsupported, and persists no host decision. A proved noexec still lands
on D.

Windows hosts have no rung B or C yet, so any rung A refusal there keeps the
host-Node relay too. The rung D message is worded from the host OS: Windows
gets no noexec-mount wording and is offered Host Node.

* fix(ssh): a host-Node fallback with no Node settles rung D, and mutable refusals expire

The host-Node relay the ladder chooses in place of D (a client artifact gap,
or a Windows rung A refusal) hard-failed the connect when the host had no
Node: the resolver threw a plain Error, which plain SSH mode does not admit.
The fallback now proves host Node with a strict probe first; an answered
"no Node" settles D with a classified, host-OS worded reason, and an
unanswered probe stays a retryable failure. A gap no longer rewrites D to
host Node once rung C proved host_node_missing.

Mutable pinned-runtime refusals (missing_lib, security_software, and noexec
on Windows) are no longer persisted and replay from memory for ten minutes
only, so a repaired host gets rung A back. The in-memory refusal cache moves
to ssh-relay-pinned-refusal-cache.ts.

* fix(ssh): no host does worse than the pre-ladder host-Node default

Restructure the relay runtime ladder around one invariant: A, B and C are tried
first, and any refusal past them falls back to exactly the pre-ladder default,
the host-Node relay marked unsupported. That covers an unidentified libc, a
client artifact gap, Windows, mutable refusals and a C N-API floor the npm build
would not have hit. Rung D is reached only when that fallback proves the host
has no usable Node, or a proved noexec home would defeat it too.

Strict host-Node resolution now treats only a command that ran and exited as an
answer, so a refused channel (MaxSessions), session limit or timeout stays a
retryable failure instead of a 'no Node' verdict. The launch reuses the Node the
fallback probe found.

Mutable refusals (missing_lib, security_software, Windows noexec) persist again
with their proof time and replay for 24 hours, so a host that still blocks
Orca's runtime is not re-uploaded to on every connect, while a repaired one gets
rung A back. A table-driven test asserts the invariant per host class.

* fix(ssh): every answered rung failure reaches the host-Node default, and D only past it

Two shortcuts skipped the pre-ladder default. An answered but unclassified
install failure (tar ENOSPC, an extract or hash marker, an unverified Windows
promote, a self-test that failed outside every refusal class) aborted the
connect on every attempt; it now steps down as a non-persisted install_failed.
Only unanswered failures (abort, timeout, channel-open, session limit, an
unconfirmed termination, SFTP connection loss, ssh exit 255) stay retryable,
and nothing steps down once a relay launch was issued.

A proved noexec no longer jumps to D: exec denial can be per binary (AppArmor
allowing /usr/bin/node), so it skips B and C, which load from the same tree,
and reaches the host-Node fallback. D now follows only that fallback answering
with a failure, so its outcome is a superset of the default's by construction.

The never-worse test is now the cross-product of every A, B, C and host-Node
answer on Linux and Windows, plus named review cases.

* fix(ssh): system-ssh upload failures and a broken cached runtime step down before launch

System-ssh directory upload and system sftp threw plain Errors that dropped the
host's exit status, so a remote tar ENOSPC or a Windows sftp permission refusal
aborted the connect instead of stepping down. The remote side now throws
SystemSshCommandExitError (ssh exit 255, signals and lost-connection
diagnostics stay retryable), and a local tar failure is still blamed on the
client rather than the remote tar that saw its truncated stream.

The warm runtime check only tested that the verified executable existed, so a
cached runtime that the host's exec policy or a removed library had since broken
was launched and hung on every reconnect. The same command now also runs it and
reports a refusal the way promotion does, so it steps down before any launch,
with no extra round trip.

* fix(ssh): keep the remote answer when local tar only failed writing to it

When the host's tar exits early (ENOSPC, a refusal), BSD tar on macOS exits 1
with 'Write error' and no signal, so the upload rethrew the local error and
dropped the typed remote exit, aborting the connect. The remote answer now wins
whenever local tar finished, was signalled, failed after ssh closed, or reported
a write or pipe error; a local tar that failed on its own input first is still
the client's fault.

* test(ssh): hostile-host cells allow read-only version probes, never installs; probe each Node once

The noexec-home cell failed on 'npm --version': under the never-worse ladder a
proved noexec reaches the strict host-Node fallback, whose probe asks npm its
version before settling D. The cells now judge install and compile calls (npm
install/ci, node-gyp, cc, make) and allow '<tool> --version' / '-v'.

The probe ran twice because the login shell named the Node the path probe had
already rejected. Candidate checks are now memoized per resolution, for the
host-Node fallback and rung C alike.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* chore(i18n): regenerate the runtime-required English catalog after the main merge

* fix(serve): serve on the profile the CLI selected, and refuse any other before the instance lock (#26424)

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(remote-runtime): open terminal tabs reattach after a managed server update re-pairs the host (#26433)

* fix(remote-runtime): a remote pane rebinds after a server update re-pairs its host

A pane captured the host's pairing revision once, when it mounted. A forced
managed-server update re-pairs the host under the same environment id, so every
later request from that pane was refused as stale, and the refusal counted as
fatal: the pane stopped retrying and Reconnect replayed the same stale revision.
Each request now carries the current revision, a pairing refusal re-reads the
catalog before the retry, and the pane treats it like lost contact.

* fix(remote-runtime): close a stream left on the old pairing and ignore a 0x0 grid hold

A stream that reopened before main re-paired stayed open while sendFrame refused
its stale revision, so typed keys vanished. A revision change now closes it as
recoverable so the pane rebinds on the new pairing. A just-restarted server also
published a 0x0 hold that shrank the visible pane to one cell; holds with no grid
are ignored.

* fix(remote-runtime): a pane follows only its own machine's re-pair

A re-pair under the same environment id to a different host key published its
revision before the catalog retired the old tabs, and the pane recovered at
the new revision, so its next call was authenticated as the replacement peer.
The catalog now classifies each re-pair before publishing revisions: a
replaced or removed machine's terminal streams end for good first, and a pane
only advances its pairing along a proven same-host rotation (same registration
and host key). An unresolved identity holds the pane until it is proven.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(rpc-permissions): Phase 3.5 elegance pass (#26471)

* refactor(rpc-permissions): drop never-read incarnation and invocation id from bridge credentials

* refactor(rpc-permissions): define runtime device grants once and reuse bridge scope fixtures

* refactor(rpc-permissions): decide terminal host membership in one predicate and share the opt-in hint

* refactor(rpc-permissions): parse bridge selectors once per call

* refactor(rpc-permissions): admit and bind unopted bridge methods from one table

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(remote-pane-binding): Phase 3.5 elegance pass (#26470)

* refactor(remote-pane-binding): keep the pane's pairing revision in one slot

* refactor(remote-pane-binding): share one hold decision across the input entry points

* refactor(remote-pane-binding): define the persisted-handle adopt once in attachPane

* refactor(remote-pane-binding): build the pushed-snapshot meta once

* refactor(remote-pane-binding): drop the test-only peerReplacedEnvironmentIds wrapper

* test(remote-pane-binding): share the input and subscribe frame readers via stream fixtures

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(relay-ladder): Phase 3.5 elegance pass (#26474)

* refactor(relay-ladder): put the host-Node fallback in the ladder before D

The Auto ladder now lists legacy as the rung before D, so the step function
is one rule plus the next rung. Deletes nextRelayRuntimeStep, the hostOs
step context and the mutable hostNodeFallback flag (now a constructor-time
laddered field). Windows still ends at legacy: B and C refuse there before
any host I/O.

* refactor(relay-ladder): one rung-D reason and message over reachable states

D is reached only from the host-Node fallback, so its refusal is
host_node_missing or install_failed. Merges the reason and message
functions, drops three message branches that could not run, and folds
noexecRemembered/noexecProved into one noexec field.

* test(relay-ladder): one never-worse cross-product with a remembered axis

Drops the named cases (each matches a cross-product outcome), the transport
case that only checked the model against itself, and the third case the rank
check already implies. Adds rung B's noexec answer and per-rung remembered
flags for A and B, the only rungs that replay a cached refusal.

* refactor(ssh): one answered-miss rethrow for the node resolution probes

Four catch blocks repeated the same session-limit, abort and strict checks.
They now call rethrowUnlessAnsweredMiss and keep their own fallthrough.
Also drops a stale 'Caches nothing' comment.

* refactor(ssh): share the host-answered system-ssh exit rule; flatten the upload throw cascade

isHostAnsweredSystemSshExit owns the 255/null transport rule for both the
upload pipeline and isAnsweredHostFailure. settleUploadPipeline drops a
branch that could not run: a failed tar is judged once, then ssh, then pipe.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(serve-daemon-misc): Phase 3.5 elegance pass (#26478)

* refactor(ssh-pty): build obligation transitions behind one closed-delivery guard

* refactor(build-orcad): table-drive the shipped worker smokes

* refactor(daemon): pass scope probe seams as one deps object and share cgroup line parsing

* refactor(repos): route every project removal through removeProjectForHost

* refactor(build-orcad): type the worker smoke table for the type-aware gate

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(terminal-tail): Phase 3.5 elegance pass (#26465)

* refactor(terminal-tail): share mulberry32 and compare whole results in redraw equivalence tests

* refactor(terminal-tail): keep one splice ceiling at the onPtyData entry point

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): bound the backing a redraw row can pin across chained erases (#26547)

Each erase that keeps at least half the row skipped the copy, so a chain
of them left nested slices pinning the original wide row. Rows now track
an upper bound on the string they slice and copy once the cumulative
shrink passes half.

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 05:36:25 -07:00
OrcaWinandm4air f5ece0f912 test(e2e): retry Hangul-digit attempts the input method never delivered (#26566)
* test(e2e): retry Hangul-digit attempts the IME itself never delivered

The nested Wayland lane failed ~1 in 6 repetitions on main because mutter+IBus
occasionally discarded the preedit or the digit keydown before Chromium saw it.
Each attempt now checks what the page actually received; only those upstream
drops are retried, and any line Orca loses after the page received it still fails.

* test(e2e): keep the engagement receipt call the gate contract pins

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 05:32:06 -07:00
Neil 47165c90b4 test: load session owners directly and skip mocked recovery pacing (#26567) 2026-10-08 05:29:23 -07:00
Neil 7c50009ca4 test: remove duplicated cases and source-text checks (#26546)
* test: remove copied logic and literal-only checks

* test: remove duplicated SSH cases and source-text inventories

* test: retire removed SSH literal check from reliability gate

* ci: skip Ruby release checks for reliability metadata
2026-10-08 04:51:48 -07:00
Neil 1e76c87f47 Verify terminal reference deduplication after settlement 2026-10-08 04:28:07 -07:00
Neil 4aa361c435 Close the SDK test server before destroying its connections 2026-10-08 04:28:07 -07:00
Neil 5125f518cf Bound multi-reference lookup work and normalize by identity 2026-10-08 04:28:07 -07:00
e0a98a4fc1 fix(hooks): speed up guarded Unicode Windows hook launches (#26382)
* fix(hooks): run safe Unicode Windows hook paths directly

* fix(hooks): preserve guarded Unicode launch behavior

* fix(cli): include extracted Windows hook command builders

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <neil@stably.ai>
2026-10-08 03:51:49 -07:00
Brennan Benson 496ee9bd7e feat(session-history): Resume in New CLI forks a native chat's conversation into the terminal (#26336)
* feat(session-history): resume a native chat's conversation in a new CLI session

Agent Session History offered "Resume in New Chat" to move a terminal session
into native chat, but nothing went the other way: once a native chat owned a
conversation, its row only reopened the chat, and Orca refused any terminal
resume of it so two programs never write one conversation.

Rows a native chat owns (Claude, Codex) now offer "Resume in New CLI". It forks:
the terminal runs `claude --resume <id> --fork-session` or `codex fork <id>`,
so the CLI gets the full history under a new id and the chat is unchanged. The
host's resume guard lets a Claude fork through, and the fork tab records no
provider session, so a restart never re-enters the chat's conversation.
"Resume in New Chat" is renamed "Resume in New Native Chat".

* fix(session-history): size the Resume in New CLI tooltip to its text

The tooltip balanced one wrapped string inside a box sized before balancing,
leaving a gap beside its lines. It is now two block lines with no fixed width.

* fix(session-history): run a CLI fork under the selected Codex account and tighten the fork guard

- Resume in New CLI now asks the host for the same Codex account home plain
  Resume uses, so a fork no longer starts under whichever account's copy of the
  rollout sorted first. The preparation call carries an optional `fork` flag
  that skips only the owned-conversation refusal; plain resume still refuses.
- An older paired host that refuses the fork now shows "Update Orca on the host
  that runs this chat to resume it in a new CLI." instead of a raw error code.
- The host guard admits `--fork-session` only before a `--` terminator and
  never alongside `--session-id`, since either shape lets Claude write the
  owned conversation.
- Tests for the click path, the menu item's eligibility, the fork home
  preparation, and the tightened guard.

* fix(session-history): read a Codex rollout's identity from its first session record

A fork of an older (non-paginated) Codex thread writes its own session record
first and then copies the parent's history, the parent's session record
included. The history scanner kept the last one it saw, so the fork's row took
the parent's id, title, folder and branch: on a native chat's fork it looked
like the chat itself, and Resume on it opened the chat instead of the fork.
Codex itself treats the first session record as the thread's own and ignores
later ones; the scanner now does the same, as Orca's duplicate-rollout check
already did.

* fix(session-history): set up status hooks in the Codex home a CLI fork keeps

When Resume in New CLI can't move a Codex fork to the selected account (that
account has no copy of the conversation yet, or the system default is
selected), the fork runs in the account home the history row came from. A new
Codex tab only gets Orca's status hooks in the selected account's home, and the
fork carries no provider session, so nothing set up the hooks in that row home.
In a home only native chat had used, the fork tab could show no working or
idle status.

The fork's home preparation now readies that home the same way a resumed
session's home is readied (hook install or refresh, plus the daemon-socket
guard). That code is moved into prepareCodexPinnedLaunchHome and shared by both
paths. The fork tab still carries no provider session, so it never reopens the
chat's conversation on restart.

* fix(session-history): ready only a host-verified Codex home for a CLI fork

The fork's hook setup ran in whatever Codex home the request named, so a paired
client could have Orca write hook and config files into any directory. Plain
Resume only readies a home it has verified: one this host lists for Codex
sessions that actually holds the rollout. The fork now uses that same check,
through a shared trustedCodexResumeHomes list, and readies the home only when
the verified home is the one the fork will run in. With no Codex runtime home
service it does nothing, as plain Resume does. A failed check or setup is
logged and never stops the fork from launching.

Tests cover the untrusted home, a missing runtime home service, a rollout on
another host, a WSL home, a fork moved into the real home, and a failed setup.

* fix(session-history): show the host-update message when the desktop wraps the fork refusal

When the local preparation step for Resume in New CLI is refused, Electron
hands the renderer the error as "Error invoking remote method '…': Error:
agent_session_conflict". The fork's failure text matched only the bare code,
so the toast showed that raw line instead of asking the user to update Orca on
the host. It now unwraps the message with the shared IPC error helper before
matching.
2026-10-08 03:29:36 -07:00
OrcaWinandm4air e428179270 test(native-chat): make the in-place transcript replacement test deterministic (#26532)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 03:28:08 -07:00
Brennan Benson 5252884a9c Native chat: say when Codex is signed out or the agent isn't installed, without disabling Send (#25666)
* fix(native-chat): disable Send for unavailable agents

* test: update sign-in copy expectations

* fix: show automatic chat availability only on Send

* fix: keep signed-out evidence when a live session lists models

* refactor: split Send availability presentation and tests

* refactor(native-chat): keep the sign-in verdict on the catalog's failure record

The probe's "signed out" / "CLI missing" verdict now lives on the account's
existing model-catalog failure record instead of two extra maps, with the
failure TTL as its only lifetime:

- Only the probe sets or clears the verdict; a chat's own model listing
  (which works while signed out) never clears or renews it. An aged-out
  verdict makes the next read re-probe, however fresh the catalog.
- A healthy fresh catalog is no longer re-probed every 30 s; it keeps the
  normal 10 min fresh window. A Claude start refused as signed out ages the
  catalog so the next read re-probes.
- A waiting picker again answers from whichever catalog lands first and never
  waits on a probe. A read that still owes the probe's verdict says so, and the
  composer waits for it once with the new optional waitForAvailability param.
- The open-time start-failure line and its transcript row are hidden only
  while Send is disabled for the same reason; otherwise they show as before.
- A host verdict lifetime longer than the client's hold is clamped, not
  dropped. The catalog read watches only the account settings and the paired
  host's contact instead of every store write.

* fix(native-chat): one send-gate rule, and a missing CLI as its own failure

- The sign-in/CLI verdict is gated once where the session hands it to the
  composer: while a turn runs it neither shows nor refuses, so a follow-up
  queues as before; goal mode is held by the same gate. The start-failure line
  and row hide only while that same value is up.
- A start whose CLI was never found (a direct spawn's ENOENT, or the one a
  provider supervisor reports) is now a `cliMissing` failure with the disabled
  Send's sentence, instead of a generic "couldn't start" beside it. An older
  client shows the host's sentence for the new kind.
- Signed-out words for a slash command keep "Run /x again".
- The Claude catalog reads its models only from the reply to its own
  list_models request, never from the initialize reply.
- The Codex probe lists models while its account check runs; a signed-out
  verdict still wins.
- The missing-executable check moves to provider-process, out of native chat.

* test(native-chat): signed-out command failures name the command to run again

* fix(native-chat): probe the Codex home a launch would read

- Before the catalog probe's read-only app-server reads Orca's shared Codex
  home, it runs the same sync a launch and the usage poll run, so a
  `codex login` made in ~/.codex is seen at once instead of leaving
  "Run codex login" up until something else re-syncs (forever on a headless
  host). Per-account homes are left alone.
- A "no account" answer counts as signed out only when the home's auth.json is
  absent or holds no credential; one caught mid-write by a token refresh, or
  holding a credential, is unknown.

* test(codex-accounts): type the node:os mock with a type import

* fix(native-chat): keep the sign-in verdict until a newer answer replaces it

- No gap while signed out: the host serves an aged verdict while the probe that
  re-derives it runs, and the composer never clears its last answer before a
  read; only an answer without a verdict, an error, lost contact or an account
  change does. Send stays disabled with its reason through every re-check.
- After sign-in the stale "isn't signed in" start-failure line stays hidden:
  a probe that found the account fine now says so (`accountVerified`), and the
  line shows only when the host has no answer at all.
- A Claude start refused as signed out, or any start whose CLI was never
  found, is recorded as that home's verdict, and the chat reads it once when
  that start failure arrives.
- The re-probe mark is read only where the catalog read starts its probe, so a
  Codex chat's own picker no longer re-lists for it on every read.
- Choosing an account or signing in again in Accounts settings re-derives the
  verdict at the next read; the account key now compares settings by value.
- A re-read that brings an identical catalog keeps the option state, so focus
  re-reads re-render nothing.

* fix(native-chat): Codex's signed-out words name only the step that unblocks this chat

A chat is pinned to the Codex home it started in, so choosing another account
in Codex Accounts settings never unblocks it. The system-account sentence now
says only to run `codex login`, in every locale.

* refactor(native-chat): give an account's sign-in status its own owner

The sign-in verdict lived inside the model catalog's failure records, so
the store needed guards for who wrote a failure, which listing may clear
it, and whether a verdict was served while re-probing. Each guard fixed one
way a chat's own listing or an older probe overwrote the account's answer.

The answer now lives in AgentAccountStatuses: one in-memory, capped status
per account home (ready, not signed in, or CLI missing). Only the
session-less probe, a refused start and an account change write it. A
blocked answer older than 30 s, or one an account change marked, makes the
next read start the probe whatever else is listing, and it is served until
that probe answers. Older evidence never replaces newer. The catalog's
failure code is main's again, and agent-model-catalog-failures.ts is gone.

The wire carries one optional `availability` field in place of
`unavailable` and `accountVerified`. The renderer holds its answer per
host, agent and chat, re-reads only on that agent's account inputs, and
re-reads on a timer only while blocked. After an account change, Send
stays disabled with the previous words until the re-probe answers, then
changes once. Account switches and re-sign-ins mark the status before the
settings change a chat's read follows.

* fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked

Round-4 review findings on the account status owner:

- An account switch or re-sign-in made while a probe was running left the
  old answer up for up to 30 s: the next read joined that probe, whose
  answer was then dropped as older than the change. A read now chains
  exactly one fresh probe after a probe that began before the change.
- Removing the selected Claude account swapped another sign-in into the
  shared config dir without marking the answer; it now marks it first.
- Evidence was ordered by wall-clock time, so a clock that stepped back
  froze a blocked answer. It is now ordered by a sequence and aged on a
  monotonic clock; a negative age reads as due.
- A chat left open while signed out started a CLI probe every 30 s.
  Probes that keep finding the same block now space out (30 s, 60 s,
  2 min, 4 min, then every 5 min), carried to the client as recheckInMs.
  An account change or a different refusal starts the hold over.
- The renderer dropped no settled wait from its leave set; it now does.
- A failed Claude probe logged its whole stdout, which now includes the
  initialize reply naming the account. Control replies are dropped from
  the log and the failure message.
- The account services take the status re-check as an injected
  onSignInChanged hook instead of importing the native-chat store.
- Imports moved below file headers; comments no longer say "ready"
  means re-checked after the failure.

* test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added

* fix(native-chat): type the held-answer handle as the account status

* fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe

Round-5 review findings:

- After a chat sat blocked for a few minutes, the backoff made every
  read wait out a hold of up to 5 minutes, so someone who ran `/login`
  or `codex login` and came back still saw Send disabled for minutes.
  The backoff now applies only to the client's own timer read, which
  it marks with an optional `scheduledRecheck` param. A read from
  someone looking (window focus, the pane becoming visible, a new chat,
  a start failure) re-probes once the block is older than 30 s. The
  param is sent only on that timer, which only a blocked `availability`
  answer arms, so a host that predates the param never receives it.
- An account change made during a home's first probe marked nothing,
  because only existing statuses were marked, so the old account's
  answer was written as current. Each agent's last account change is
  now one sequence number that every status and running probe is
  compared against; statuses are no longer mutated in place.
- Removing an account that nothing had selected no longer re-checks.

* refactor(native-chat): keep the sign-in and missing-CLI words in their own module

Main's retry wording pushed the failure-words table past its line budget.

* refactor(native-chat): build the structured chat's view session in the view-state module

Main's send-reveal work brought NativeChatStructuredSession.tsx and the
session controller to their line budgets.

* fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe

- A chat whose open-time start failed signed out had that line hidden
  while the host said the same. When a new signed-out answer landed
  during a running turn, the line came back for the whole turn: the
  hide read Send's gate, which a running turn suspends. The hide now
  reads the host's answer itself (blocked for the same reason, or
  ready); Send's gate still steps aside while a turn runs. Both are
  views of the one answer the session holds.
- An account change during a home's first probe dropped that probe's
  answer but left nothing to re-check the new account, and the picker's
  reply said no probe was pending, so the chat stayed unknown until the
  catalog aged out. A read now chains one fresh probe whenever the
  running probe began before the change, and its reply says that probe
  is still owed so the client asks once more for its verdict.
- The client's timer read no longer carries `scheduledRecheck`. The
  timer already fires only when the host's backed-off `recheckInMs`
  says, which is past the 30 s after which any read re-probes, and
  every answer re-arms it. One fewer strict-schema param.

* build(cli): typecheck the missing-executable check the provider process now imports

Main's #25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.

* fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing

What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.

How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.

* fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send

A chat whose account is signed out and has no model list no longer shows the model picker as
loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that
already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard
focus (so its tooltip opens) without becoming a second button; the inner button keeps its label.
Trims tests that repeated rules other tests already prove.

* fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send

The composer shows Stop while the host's queue is about to send its next card, but the sign-in
gate stayed on in that window, so Enter was refused with no message.

* fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead

The host's pre-send check can be wrong while the chat would work (saved
Arguments it never sees, project settings it can't read, gateways that
accept a signed-out Codex), and a disabled Send left no way past it.

- Delete the Send gate: the composer, Enter and goal mode no longer read
  the verdict, and the Send tooltip reason and focusable wrapper go back
  to main.
- Show Codex not-signed-in and Claude/Codex not-installed as one entry in
  the chat's existing notice card. Dismissed per verdict; shows again
  when the verdict changes or clears and returns; steps aside when the
  failed start (launch line or latest failed-start row) says the same.
- Claude: drop the probe's signed-out verdict (initialize-on-probe stdin,
  request-id filtering, control-reply log scrubbing). The chat start's
  refusal, with its account kind, is the only Claude sign-in signal.
  Missing-CLI from the probe stays.
- Restore the failed-start line and transcript row for these causes
  (no more hiding on desktop) and drop the chat-root focus re-check; a
  window-focus re-read stays while a notice is showing.

* fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list

Review round 1 of the notice rework.

- A chat that proves its start (at acquire, or on its later "started"
  event) marks the catalog's held reason for that account due, so the
  next read re-probes instead of trusting it until the TTL; the chat
  re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
  so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
  reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
  Codex-only; the say-once rule ignores a failed start a turn has run
  past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
  chat. Install it, or check its Command in Settings → Agents." (6
  locales).

* fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter

Review round 2 of the notice rework.

- A re-attach to a live child proved nothing new, so only an attach that
  acquired a child which proved its start marks the reason due.
- Reword: a start proves the program exists; for a Codex sign-in it only
  prompts an earlier re-check.
- Drop the notice's Claude signed-out filter: no host sends that verdict.
- Tests: a re-attach does not notify; a turn ending re-reads once more.

* fix(native-chat): never newly show a catalog reason the host is still re-checking

After signing in to Codex, a new chat flashed the old "isn't signed in"
notice for about half a second: past its 30 s lifetime the host answers a
plain read with the held reason while its probe re-checks. A chat now
adopts such a reason only when it already shows it; otherwise the joined
read's answer decides.

* test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge

* test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests
2026-10-08 02:28:13 -07:00
Neil 8ae5264c63 Record confirmed PR observations from their terminal pane 2026-10-08 02:04:04 -07:00
Neil 6e39b8ecb2 fix: admit relay bulk frames once under backpressure (#26333)
* perf: stop retrying relay bulk frames after admission

* fix: prevent relay bulk retries during admission

* test: prove relay bulk admission races remain exercised

* test: limit teardown watchdog to rejected admissions
2026-10-08 01:59:46 -07:00
Neil 216ef2985a fix(terminal): avoid replaying completed strings after snapshots 2026-10-08 01:55:06 -07:00
Brennan Benson a747c1c013 Allow another phone send after delivery is unconfirmed (#26392)
* Allow another phone send after delivery is unconfirmed

* Update phone image recordings after journal removal
2026-10-08 01:46:31 -07:00
Neil b1cd85820b Show multiple reviews and tasks in workspace cards and details 2026-10-08 00:52:08 -07:00
Brennan Benson a53967fc05 Native chat: add an inline visuals switch and simplify the skill (#26358)
* Native chat: let users turn off inline visuals for new chats

* Native chat: keep inline visuals guidance light

* Move inline visuals setting to Chat settings
2026-10-08 00:48:05 -07:00
Neil 952898b8b9 Add always force delete dropdown to workspace recovery dialog (#26476) 2026-10-08 00:38:27 -07:00
Neil 0eee1cebac Remove repeated row copying from terminal redraw ingestion (#26118) 2026-10-08 00:22:54 -07:00
Neil 67dbd2fd06 perf: reuse chat tab ownership across unrelated app updates (#26150) 2026-10-08 00:21:54 -07:00
Neil 07c2379395 Encode checkpoint strings in bounded native JSON chunks (#26177) 2026-10-08 00:02:56 -07:00
Neil a4854ac1e9 perf: skip the OpenCode reader worker when its database is absent (#26113) 2026-10-07 23:55:00 -07:00
Neil 10ac42010c Release completed provider write payloads (#26208) 2026-10-07 23:50:45 -07:00
Neil e97d18df06 Avoid repeated session copies when deleting project groups (#26230) 2026-10-07 23:48:20 -07:00
Neil f052e2f1ff perf: skip unused Grok login-shell probe when installing agent hooks (#26241) 2026-10-07 23:46:47 -07:00
Neil a211489230 Add saved force-delete preference to workspace deletion menu (#26425)
* Give Windows Electron writer probes a larger test budget

* Drain queued writer replies across two poll turns before timing out

* Check writer shutdown after both reply-drain turns

* Add inline always-force-delete preference to workspace recovery toast

* Use split-button menu for saved force-delete preference

* Await durable force-delete preference writes before deleting

* Restore absent settings keys after failed durable writes

* Place deletion translations beside worktree copy
2026-10-07 23:42:11 -07:00
Neil 69900f0232 Share provider search between workspace creation and reference editing 2026-10-07 23:34:00 -07:00
Neil 46ef7c4990 Drain queued SQLite replies before faulting after a main-loop stall (#26444)
* Give Windows Electron writer probes a larger test budget

* Drain queued writer replies across two poll turns before timing out

* Check writer shutdown after both reply-drain turns
2026-10-07 23:28:38 -07:00
Brennan Benson e4cd14d914 Remove legacy OS file-drop routing (STA-6940 PR6/6) (#26385)
* feat(file-drop): add element owner preparation plumbing

* Fix terminal and chat file drop destination ownership

* fix runtime terminal drop ownership and queued chat retries

* fix(file-drop): preserve feedback acceptance and destination ordering

* fix(file-drop): attach chat and composer files at the drop surface

* fix(file-drop): preserve project destinations and live chat availability

* fix(file-drop): keep path resolution in filesystem namespace

* test(file-drop): use filesystem path bridge in PR3 fixtures

* fix(file-drop): bind terminal drops to pane elements

* fix(file-drop): compare terminal pane identity across public views

* test(file-drop): align composer lifecycle with element-owned drops

* Fix interrupted chat composition and refuse unused quick-create drops

* Type the quick-create drop regression transfer

* fix(file-drop): let the explorer, project sidebar, tab strip and editor own OS drops

STA-6940 PR5. The file explorer tree, the project sidebar, each editor
group's tab strip and its editor area now take OS file drops on their own
element through the shared owner hook, with identity from their own render
(the explorer's shown workspace and target row, the tab strip's and editor
group's worktree and group) instead of the active worktree. The explorer and
sidebar broadcast subscribers and their legacy drop markers are deleted;
editor-open moves out of useGlobalFileDrop into editor-dropped-file-open,
which the legacy unmarked-chrome route still uses until PR6.

* fix(file-drop): keep editor delivery tied to its destination group

* fix(file-drop): guard delayed opens and share floating ownership

* fix(file-drop): remove legacy OS drop routing (STA-6940 PR6/6)

* test(file-drop): finish owner test and comment cleanup

* Clarify dropped-file default editor destination

* Retain UI test namespace for terminal resume coverage
2026-10-07 23:11:37 -07:00
Brennan Benson 2caea6ce93 feat(omp): open OMP in the structured chat over ACP (#26401)
* feat(omp): open OMP in the structured chat over ACP

OMP joins Grok and OpenCode on the ACP lane: `omp acp`, with the user's
environment passed through and the chat pinned to PI_CODING_AGENT_DIR
(default ~/.omp/agent), the directory OMP's terminal chats read. A create
and every launch ask `omp --version` of the binary the Command setting
resolves to; stable releases from 17.0.5 run the chat, older ones keep the
terminal-backed chat. Like every ACP agent it runs only with the updated
structured native chat setting on.

* fix(omp): show a shell command's output and exit code in its row

OMP's ACP server sends a command's result as rawOutput {content, details}
with a non-zero exit at details.exitCode, and repeats it as content behind
a "$ <command>" echo. The shared reader takes content text first, so the
row's output began with the command and carried no exit code. The OMP
dialect drops the echo, moves the result text to stdout without OMP's
"Wall time" and "Command exited with code N" notice lines, and maps
details.exitCode. Recordings of omp acp 17.0.5 (exit 3, exit 0, Stop
mid-command) replay through the adapter.
2026-10-07 23:10:30 -07:00
Brennan Benson b1e0092d5d feat(opencode): open OpenCode 2.x in the structured chat too (#26395)
* feat(opencode): run OpenCode 2.x in the structured chat too

`opencode acp` on stable 2.x starts its own private `opencode serve --stdio`
child with the chat's environment and ends it when stdin closes, so the
chat's account pin reaches it just as it does on 1.x. Admit stable 2.x from
2.0.14 beside stable 1.x from 1.18.31; pre-releases, older releases, and
other major lines keep the terminal chat. The `opencode2` agent is unchanged.

Restart recovery already reads a 2.x session's own tables first; tests now
cover a database an upgrade left with both generations, and one whose 2.x
message table has an unknown shape (nothing found, no error).

* fix(opencode): show OpenCode's own permission option names

* test(opencode): run the 2.x recovery tests with the real-SQLite Node project
2026-10-07 23:09:40 -07:00
Brennan BensonandClaude 4bd4f8cf06 feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card (#25652)
* feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card

The resume-on-restart dialog listed workspaces as a bare name and glyph, so users could not
tell their sessions apart. It now renders each workspace with the sidebar's own WorktreeCard
in a new read-only mode, with the offered chats in place of its live agent rows, and nests
child workspaces under a listed parent the way the sidebar does. The dialog is wider and the
chat rows use the sidebar's compact agent row layout.

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

* test(native-chat): name the nesting helper for what it returns

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

* fix(native-chat): resume dialog cards show no status

The dialog's cards stand in for the chats before the restart, so the sidebar's live status
(Failed, now Interrupted) read as a verdict on chats the dialog offers to resume. A read-only
card shows no status, and in the new card style its PR joins the badges because the status
lane that normally holds it is gone.

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

* fix(sidebar): a read-only card is inert and shows no live state, whatever its caller passes

A read-only card stayed inert only because each control checked `readOnly` on its own and the
caller left out the handlers that would turn the rest on: passing `onLineageToggle` put a live
child-workspaces toggle on it. It also still painted live state over the resume dialog's chat
rows: the sleep dim, the "Queued for deletion" overlay (which blocked unticking a pre-ticked
chat) and the prompt-cache countdown.

WorktreeCard now reduces a read-only card's props to what it pictures (every handler, selection
and active flag dropped), and the controller resolves `readOnly` once into what the views
already read: card properties without status, ports or live agents (and with the host), no
sleep or delete state, no cache countdown, and one `interactive` flag for its own controls.
A contract test renders a read-only card with every feature and handler on in all three card
styles and fails on any control outside the caller's rows or any live state.

* fix(native-chat): nest and indent the resume dialog's child workspaces as the sidebar does

A child workspace whose parent has older metadata with no host id stayed at the top of the
dialog, though the sidebar nests it: the dialog copied a strict same-host filter, while the
sidebar treats a hostless row as belonging to the host. The nesting rule now lives in one
sidebar lineage helper, getWorktreeLineageAncestorsOnHost, which scopes by host the way the
sidebar's lineage inputs do.

In the legacy card style a nested child was inset for its parent's depth instead of its own,
so it sat one sidebar step too far left (0px instead of 14px), outdented into the parent's
padding. Each child is now inset for its own depth, as a sidebar row is.

A groups-level test renders real sidebar cards from the store, with the child nested under a
hostless parent, and checks every offered chat's checkbox renders and the legacy inset.

* fix(native-chat): the resume dialog places every workspace even if lineage loops

Nesting followed each workspace's parent chain, so two workspaces listed as each other's
ancestors (possible only when two hosts report opposite lineage for the same ids) were placed
under no top-level entry and vanished, while their pre-ticked chats would still be resumed.
The loop is now cut where the walk first comes back, so every workspace renders.

* fix(native-chat): keep "Don't ask again" above the resume dialog's buttons when narrow

Below the small breakpoint the footer stacks bottom-up, so the option moved into it rendered
under Resume and Dismiss all. It now reads top to bottom: the option, then the actions; wider
windows keep the single row.

* test(sidebar): import the card statically in the read-only contract test

Its dynamic import ran in a beforeAll with a 20 s timeout, below the suite's 60 s hook timeout,
and timed out under parallel load, which skipped all six contract tests.

* fix(sidebar): legacy lineage children step in as far without a status lane

Legacy cards outdent their nested child list by 1.125rem to pull it back over the 22px status
lane. A card without that lane (a read-only card in the resume dialog, or the sidebar with the
Status property off) kept the outdent, so a child's title sat 3px right of its parent's instead
of 25px. Without a lane the list now sits at ml-1, which gives the same 25px step (39px at depth
2). Cards with a status lane, the default sidebar, render exactly as before.

* fix(native-chat): nest resume dialog children by the sidebar's own row rule

Round one nested a child under a parent whenever either had no host id, but the sidebar's row
builder looks a parent up on the child's own host, so a row with no host id nests only under a
parent with none either. The dialog showed a parent/child pair nested that the sidebar shows side
by side.

The row builder's parent lookup is now one function, getSidebarLineageParent, and the dialog
walks ancestors with it (getSidebarLineageAncestors) over the same non-archived rows and cycle
set. The chat's workspace is still found on its host, falling back to a row with no host id.
A test checks the four host pairings against the real row builder, and the dialog test renders
each pairing with every chat's checkbox.

* test(sidebar): pin child-workspace indent with and without a status lane

Covers legacy, compact and new card styles with the Status property on and off. With Status on,
the legacy and compact sidebar keep their outdent unchanged; with it off they now step children
in by the same amount; the new card style is identical either way. The file now imports the
card statically, since its 15 s per-test dynamic import timed out under load.

* fix(sidebar): a read-only card's child workspaces step in as the sidebar's do

The legacy outdent on a card's child list cancels its status lane, which in the sidebar is the
unread button plus the row gap: 1.125rem, exactly the outdent, not the 22px the previous fix
assumed. Its ml-1 for a card with no lane put the resume dialog's children 4px deeper per level
than the sidebar's (25px against 21px). With no lane there is now nothing to cancel and no
margin, which gives the sidebar's 21px (35px at depth 2).

Status is a fixed card property, so every live card has the lane: only a read-only card takes
this branch, and the live sidebar is unchanged. The tests now pin the outdent for live legacy
and compact cards and no margin for a read-only one, instead of an unreachable Status-off state.

* test(sidebar): a read-only card never carries main's multi-selected marker

Main now styles a multi-selected card through data-worktree-card-selected. The contract test
passes isMultiSelected, so it now asserts the marker stays off a read-only card, and drops the
context-menu event constant main removed from that module.

* fix(sidebar): preserve quiet read-only card geometry

* fix(native-chat): match resume cards to sidebar rows

* fix(sidebar): preserve passive SSH identity on read-only cards

* fix(native-chat): keep resume focus and folder host identity

* fix(native-chat): remove resume dialog list border

* fix(sidebar): keep passive SSH pills static

* fix(native-chat): align resume card vertical spacing

* fix(native-chat): preserve cache-only card header spacing

* fix(native-chat): lay the resume dialog out as a checkbox-column list

Every row of the resume list (Select all, workspace, chat) now keeps its
checkbox in one left column; nesting indents only the content after it,
and a divider separates every row. A workspace checkbox is tri-state over
its own chats and those of the workspaces nested under it; Select all
heads the list with an "N of M selected" count. Failures a retry cannot
fix stay out of both. Up/Down move between the list's checkboxes.

The dialog no longer renders the sidebar's WorktreeCard, so the read-only
card mode and the sidebar changes that only served it are reverted to
main. The sidebar lineage helpers stay: the dialog still nests child
workspaces by the sidebar's own rule.

* fix(native-chat): name SSH hosts by their saved label in the resume dialog

The workspace row's host chip spelled an SSH host with its raw target id
(e.g. ssh-1728291234567-abc12d) because it never got the sidebar's host
labels. It now takes them from useSidebarHostScopeOptions, as the delete
dialog does, so the chip reads "devbox" like the sidebar.

Also translates the dialog's five new strings in es, fr, ja, ko and zh.

* fix(native-chat): group the resume list in bands and let a project select its chats

The resume list now follows the common grouped-list pattern: each
workspace heads its chats with a full-width tinted band (lighter for a
nested child, at its own indent), chats are split by plain hairlines, and
groups meet without gaps or rounded row corners.

The project row gets a checkbox in the same left column, tri-state over
every eligible chat in the project (nested workspaces included, failures
a retry cannot fix left out) with an "x of y" count, on a band a step
stronger than a workspace's.

The list takes arrow keys, so it is now a named group (role="group").

* fix(native-chat): no project row for workspaces the resume list cannot place

Workspaces the store cannot place in a repo were grouped under a header
with an empty name, so the new project checkbox was announced as "Select
all chats in ". Main and the earlier heads showed that header as a bare
folder glyph with no name, and there is no id to fall back to. Those
workspaces now get no project row; their own rows and Select all still
cover their chats.

* fix(native-chat): lay the resume list out as nested workspace boxes

Brennan chose the nested-box layout without row dividers. A project is a
tinted band; each workspace is a bordered, rounded box one step in from
its project, holding its header band and its chats, and a child
workspace's box sits inside its parent's, after the parent's chats, at
the parent's chat indent. The boxes are drawn in the content area only,
so every checkbox stays in the one left column outside them. Rows carry
no dividers; the box border is the only line.

* fix(native-chat): show the resume list as a tree

Brennan chose the Resume Tree. The list is now a tree: machine, then
project, then workspace (a child workspace is a node inside its parent,
after the parent's chats), then the chats. Every row keeps its checkbox
in one left column, then an 18px indent per level, a disclosure arrow
(an empty slot on a leaf), the icon and label, and its count. Levels read
apart by icon and weight; there are no boxes, bands or row dividers, only
the divider under Select all.

The machine level shows only when the machine is not obvious (a remote
host, or chats on more than one), named with the sidebar's host labels
and an SSH chip; it replaces the per-workspace host chip. Chats are
grouped per machine first, so one workspace id on two hosts stays two
nodes.

Every node is tri-state over the eligible chats under it, through the
existing per-chat onToggle. Nodes start expanded; collapsing is local to
this opening and keeps the selection. The list is a role="tree" of
leveled treeitems: Up/Down move between checkboxes, Left collapses and
Right expands the focused node.

* refactor(native-chat): read each resume chat through one key helper

Every per-chat read in the resume tree (its tick, its toggle, its
failure, and each group's coverage) now goes through chatState, so a
later change of chat key stays in one function. Tests pin the machine
level's host rules: a runtime host gets a machine node but no SSH chip.

* fix(native-chat): make the resume tree's keys work from its Tab stop and after a click

Tab lands on the tree itself, where the arrow keys did nothing. Down and
Home now enter at the first checkbox, Up and End at the last; Home and End
also jump to the ends from a row.

Clicking an arrow with the mouse left focus on the arrow, where no tree
key works. After a collapse or expand click, focus now moves to that
row's checkbox, or to the tree when the checkbox is disabled.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-07 23:06:45 -07:00
Brennan Benson 4da1324b5a test: isolate hook fixture connections from fake timers (#26369) 2026-10-07 23:01:12 -07:00
Neil 10a2a6c2af perf: reuse Markdown fence scans across sibling toggles (#26324) 2026-10-07 22:53:01 -07:00
Neil 665fbc4a83 Release clipboard previews when the chat composer closes (#26234)
* Release native chat clipboard previews when the composer unmounts

* Assert late previews retire before explicit cleanup
2026-10-07 22:50:09 -07:00
Neil b41e2730db Persist multiple workspace references with atomic edits 2026-10-07 22:44:25 -07:00
Neil d9ab505ea5 perf: skip assembling deferred Codex text checkpoints (#26199) 2026-10-07 22:44:17 -07:00
Neil b4e99fcea2 perf: group Space decision inputs by workspace (#26297) 2026-10-07 22:42:14 -07:00
Neil 8155e2e69f perf: preserve unrelated authorization roots after desktop removal (#26294) 2026-10-07 22:42:11 -07:00
Neil 9f64453943 Release feedback requests after response handling completes (#26274) 2026-10-07 22:42:08 -07:00
Neil 408aa1383a fix(explorer): reject foreign watch events before deferring (#26265) 2026-10-07 22:42:05 -07:00
Neil 8060ca5bfa perf: persist completed notebook runs in one document update (#26254) 2026-10-07 22:42:01 -07:00
Neil 8e22ad2d0a Scope completed worktree removal authorization refresh to its repository (#26253) 2026-10-07 22:41:58 -07:00
Neil 4d7193c34d perf: skip image prewarming parse for image-free markdown (#26240) 2026-10-07 22:41:55 -07:00