mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
220165366c2d9594fa0cd5dc3745eef557c2dae0
292
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0024735e82 |
fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling (#27409)
* fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling * refactor(ssh): drop snapshot apply tokens and terminal finalization only the sync used * test: drop leftover sync-status mock --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
0081ea0941 |
fix(clipboard): paste a macOS image copied beside its file URL (#27346)
* fix(clipboard): paste a macOS image copied beside its file URL When a screenshot tool saves a file and also copies the image, the pasteboard holds public.png and public.file-url, and Chromium reports only text/uri-list from availableFormats(). readClipboardImageSource() then saw no image and Cmd+V dropped the paste, although readImage() returns it. On macOS, also check public.png and public.tiff with clipboard.has() before giving up. Fixes #27345 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzCzr11hHmaEhiUoGJCrDy * test(clipboard): cover macOS file URL image paste safety --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
e4f599df05 |
perf(worktrees): preserve Git scans when saving sidebar order (#27343)
* perf(worktrees): preserve Git scans when saving sort order Credit @mmarabel for identifying the repeated scans and the host-only approach in #27271. Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com> * perf(worktrees): skip Git scan invalidation for order saves on desktop hosts too The desktop window notifier ran the worktree-change invalidators unconditionally, so a desktop (or paired-host) Orca still cleared Git scan caches on every sidebar order save. Carry the invalidateGitScans flag through the notifier; every other caller keeps the default. --------- Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com> |
||
|
|
1912c90141 |
Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging. Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan. Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews. Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations. |
||
|
|
7b1242ba94 |
[CI] Reduce repeated test work and use pinned Bun for localization extraction (#26806)
* [CI] Reduce repeated test setup and retire source inventories * [CI] Give consolidated floating panel suites distinct titles |
||
|
|
d4acd11ae5 |
fix(window): stop a covered window drawing after a phone stream, and keep a streamed foreground tab live (#26657)
* fix(window): re-hide a covered window when the renderer-throttle lease is released Restoring background throttling only clears Electron's disable_hidden flag; the occlusion or minimize hide swallowed while the lease was held is never replayed, so a covered window kept compositing at ~60 rAF/s until the next cover cycle. After the last release, a discarded empty-rect stayHidden capturePage makes Chromium's capturer-count decrement re-apply the current visibility, so the pending hide lands without the page ever reporting visible. Skipped for a focused renderer, best-effort and never awaited. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(window): let a broken re-hide capture surface instead of swallowing it Drops the synchronous try/catch around the stayHidden capture so a missing capturePage fails loudly; only the promise rejection is swallowed. Test doubles that reach a final lease release now provide capturePage. Keeps the focused-window skip (a covered window loses focus) and the empty rect. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(browser): keep a streamed active tab painting, and re-hide it when the stream ends When the streamed tab is the foreground desktop tab, the guest's own widget is what a cover hides, and lifting the window throttle does not reach it, so the phone froze after one or two frames. The page stream now re-applies the guest's setBackgroundThrottling(false) at start and on the window's hide/minimize while live. At stream end it drops those listeners, re-throttles the guest and replays its swallowed hide with the same stayHidden capture the window lease uses (now exported as replaySwallowedHide), so the guest stops painting. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(browser): hold the streamed guest through the renderer-throttle lease The page stream now takes a rendererPublicationThrottle lease on the guest instead of a bespoke guest-paint helper. The start unthrottle sets the guest widget's disable_hidden flag (copied onto new widgets), so the window hide/minimize re-apply listeners were no-ops and are gone. The guest gets the lease's end-of-stream re-throttle and stayHidden re-hide, including the focus skip, which holds for a guest because its isFocused requires the embedder's window to be key and its root view focused. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(runtime): give the mobile-session window doubles a capturePage Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
cc291dba1d |
Keep picked skills and pending uploads when native chat composers close (#26538)
* Preserve native chat skills and uploads when composers close * Own menu image pastes before saving begins * Keep paste ownership independent of image presentation * Show pending pastes so stalled saves can be removed * Register confirmed image pastes and preserve preview ownership * Keep clipboard image presence and saving on one source selector * Describe clipboard image formats in upload routing fixtures |
||
|
|
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> |
||
|
|
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 |
||
|
|
c785c986c2 |
feat(native-chat): teach chat agents to show inline visuals in their own folder (#26099)
* feat(native-chat): visual directive grammar and host read for a chat's visuals folder
A shared grammar for the ::orca-visual{file="..." title="..."} reply line,
the per-chat visuals folder location on the owning host, and the
agentSession.readVisual runtime method that reads one visual with lexical and
canonical containment, a 512 KiB bounded read and UTF-8 refusal.
* feat(native-chat): render chat visuals inline and in the right sidebar
Native-chat assistant replies render a ::orca-visual{...} line as the chat's
HTML visual in an opaque, scripts-only sandboxed frame: CSP first, the host
frame navigation guard registered before content runs, live theme without a
reload, fitted height, links opened in the viewer's browser only from a real
gesture, lazy mount, and one muted line when the visual cannot be shown.
Open in sidebar shows the same frame in the right sidebar, widened while it
is open and restored after.
* feat(native-chat): teach chat agents to show inline visuals in their own folder
Native-chat Claude and Codex sessions now get a per-chat visuals folder on
the host that runs them, write access to exactly that folder, its path in
ORCA_CHAT_VISUALS_DIR, and an Orca skill that teaches the ::orca-visual line.
- Skill ships as an unpacked plugin folder in desktop and headless builds.
- Claude: --plugin-dir via SDK plugins, behind the CLI version probe (now one
shared probe for every version-gated flag); folder added to
additionalDirectories beside the user's own.
- Codex: skills/extraRoots/set and the folder appended to the user's own
writable roots, between initialize and the thread open, under a 2 s budget;
unsupported, failed or hung setup opens the chat without visuals.
- A host sweep removes folders no chat record maps to, and folders whose
local workspace is provably removed; anything unproven is kept.
* test(native-chat): the visuals skill names only theme variables the frame sets
* fix(native-chat): visual CI fixes, shared height governor, live-turn streaming hold
Registers agentSession.readVisual from the methods index so the structured
method file stays under its line budget, replaces reflective reads with checked
narrowing, moves the pure height governor to src/shared for mobile, and holds a
half-written directive tail while the turn works (structured text rows carry no
running state).
* fix(native-chat): harden the visual read and link opening
Re-checks after the open that the chat's visuals folder is still the real
directory at Orca's path, reports unexpected filesystem faults by code without
host paths, and lets one click in a visual open at most one page.
* fix(native-chat): keep visual lines out of plain-text reply surfaces; review fixes
One shared helper drops visual lines (outside fenced code) from reply text where
it becomes plain text: the structured status summary that feeds the sidebar row,
dashboard, notifications, phone rows and handoffs, and AI Vault reply previews.
Review fixes: height also counts a pinned body's overflow, only the live
frontier row holds a half-written visual line, the runaway-height stop needs the
same step repeated, and any host refusal evicts the cached revision.
* fix(native-chat): review round 1 for chat visuals delivery and cleanup
- Visuals sweep: a workspace counts as removed only when no profile's
catalog holds it (chats and visuals are shared by every profile, catalogs
are not); a worktree in a known project is removed only when git no
longer records it; any unreadable profile decides nothing; one catalog
snapshot per run; a symlinked visuals root is never walked.
- The other-profile catalog reader moves out of window/ and also returns
project ids.
- A folder Orca itself inherited is stripped at the spawn layer for both
agents, not only from the launch overlay.
- Claude version probe: a probe that gave no version is never remembered;
the plugin check waits up to the probe's kill time so a slow first probe
no longer costs a chat its skill.
- Skill: kept out of Claude's / menu, filename and theme guidance matched to
the renderer, refused writes are not retried.
- Opt-in real Codex test for skill discovery and the writable root.
* feat(native-chat): ask the Claude CLI its version when native chat starts
The first chat after Orca starts usually finds the version known, so the
plugin and thinking-display checks answer at once instead of probing a
cold binary while the chat waits.
* fix(native-chat): resolve the visuals folder without the removed journal-paths helper
Main removed the per-chat journal paths and the journal database's state
directory; the visuals folder keeps the same sha256 layout on its own and the
read method uses the profile state directory the chat host is opened in.
* fix(native-chat): review round 2 fixes; copy a reply without its visual lines
Reply previews in Agent Session History drop visual lines per text part before
lines are folded; the frame adds a body's overflow only when the body really
overflows; fence tracking follows CommonMark closers and openers; the copy
button copies a reply without visual lines; a coded read fault keeps its cause.
* fix(native-chat): review round 2 for chat visuals delivery and cleanup
- Read git worktree records written relative to their own folder (git 2.48+),
so a worktree on an unmounted drive in such a repo is still kept.
- Skip the running profile by its own storage folder, not the profile index a
switch rewrites first; a profile never written to counts as empty, so the
workspace rule is not switched off by a profile that was never opened.
- Ask for readable thinking again once the plugin check has waited for the
version, so a slow first probe no longer drops it for the chat's life.
- Launch flag decisions move to their own module; tests use a typed record
fixture instead of casts.
* fix(native-chat): review round 3 for the visuals sweep and version check
- Resolve a relative git worktree record against its folder's real path, so a
project added through a link still matches and its worktree is kept.
- A profile with only backups of its data file is a lost file, not a fresh
profile: it still stops the workspace rule.
- The readable-thinking re-check reads what is known and never starts a
second version probe.
* fix(native-chat): update the frame's theme ref after render; read the visuals folder pair at once
* test(native-chat): declare agentSession.readVisual on the cross-version agent-session surface
* fix(native-chat): copying a reply keeps its code blocks and indentation
Removing visual lines now closes only the gap each removal leaves, instead of
collapsing blank lines across the whole reply and trimming its indentation; the
visuals folder is checked parent first again so a broken path answers the same
way every time.
|
||
|
|
995ef11ce7 |
feat(native-chat): say in the chat why Orca stopped a reply, and offer Continue (#25675)
* feat(native-chat): say in the chat why Orca stopped a reply, and offer Continue When the Orca that runs a structured chat (this computer or a paired server) quits, updates or crashes mid-reply, the chat's stopped row now names the cause and the machine, and a Continue button sends the existing restart continuation for that cut turn, with or without a restart offer. - Host: a quit/update writes one turn-scoped row for the turn its stop cut, in today's words, with an optional `orcaStop` cause on the providerExited fact; restart adjudication stamps how the previous runtime ended on the deaths it proves (crash, or the quit/update it began), so the crash row names it too. Older clients keep their single row. - Host: agentSession.continueInterrupted, capability-gated, rechecks under the session lock that the chat still sits on that cut, so a second click or a retry sends nothing. - Client: the row's copy names the cause and machine; Continue sits above the composer. * test(native-chat): Continue is not held by a recovery file that never answers * fix(native-chat): bind an Orca stop's cause to the runtime that held the agent; neutral row, Continue explains itself - The cause now rides on the host's row itself (`orcaStop` on the status row, beside today's words), the same row family and id scheme as the reopen's death row. - Each recorded owner is stamped with the Orca runtime that holds it; a death proven later (at restart, or when recovery stops a survivor) names how that runtime ended: the quit or update it began, else a crash. Owners an older build recorded, a terminal's claim, an agent that died while its Orca ran, and unreadable quit records all keep the generic words. - The quitting runtime's word is written first in teardown, before the recovery wait, through a bounded asynchronous writer apart from the chat database. - Row copy: one neutral sentence naming the machine and cause; it drops "You can continue in this conversation." while Continue is offered, and Continue's tooltip says what it does. * test(native-chat): type the Orca-stop test fixtures so the typecheck passes The cut turn's outcome takes the journal's outcome type, and the stand-in close reads the provider sink through a checked lookup instead of an index that may be absent. * fix(native-chat): call a cut a crash only when Orca's runtime started and never ended A chat said "Orca stopped unexpectedly" whenever its runtime left no quit record, and only the desktop quit wrote one, so a headless server's restart or update, the Settings relaunch, and a Windows logoff all read as crashes. Each runtime now records its own start when its chat store opens, and every graceful exit records its end through one synchronous entry point: the desktop quit's teardown, the headless server's stop, the in-app relaunch, the GPU-fallback restarts, the update-install watchdog, and Windows session end. A crash is a runtime that started and never ended; a runtime with no readable record (never written, pruned, unreadable) names no cause, so the chat keeps its generic words. One file per runtime, written durably and only by that runtime, so a damaged file never blocks a later write and two processes never lose each other's record. * fix(native-chat): Continue answers once Orca accepts it, not once the agent has started On a paired server, Continue waited for the agent to start before answering, and the client gives a paired call 15 s. A slow start (account switch, login shell, a long resume) showed "Couldn't continue this chat" while the agent was in fact continuing. Continue now answers when Orca has accepted the message, as a send does. The agent's start and answer settle afterwards, and a start that fails is the chat's own note, as before. The restart dialog's batch still waits for the handover, which is where it counts a start as done. The verdict helpers move to their own module to keep the continuation file within its size limit. * fix(native-chat): a reply the user steered, or a command run after the cut, still offers Continue The cut detector stopped at the first user message after the cut turn, so a steer the turn had taken, or a conversation command such as /context run after the cut, removed Continue while the row still named the cause. The rule for what is no request of its own (a conversation command, a row its turn produced, a send handed into a running turn) moves out of the latest-request reader into one shared predicate, which both that reader and the cut detector use. The host's "still wanted?" check reads the same detector, so the client and host agree. * fix(native-chat): a death proven after an earlier settle explains the turn it ends When a chat was read before the restart proved its old agent dead, the read could only call the turn unverifiable. The proof then revised the turn to interrupted, but the death row was scoped by the turn still marked running, and none was, so it landed on the conversation instead of the turn. That cut never offered Continue, and the row did not name it as the turn's explanation. The row is now scoped to the newest root turn the settle actually ends, running or revised. * fix(native-chat): the cause row's words stay put, and Continue waits out a resume already running The row's "You can continue in this conversation." came and went with the button: it showed while a paired host's answer was still on its way, vanished when the button appeared, and came back the moment Continue was clicked. Continue also appeared on chats the restart prompt or the launch's own resume was already carrying on. The row now drops that sentence wherever the chat's host can continue a cut, counting a host that has not answered yet as able (a host that writes cause rows has Continue), so its words never change on screen. Continue is hidden while a resume is carrying that chat on. * fix(native-chat): a refused Continue says so once, in the composer A Continue the host refused before accepting anything wrote a red note into the chat and brought the button back, so each retry added another identical note; a chat the host had no record of was refused with no word at all. Continue now reports every refusal the same way as a failed request: the existing composer line "Couldn't continue this chat. Try again, or send a message.", which a retry replaces rather than repeats. A refusal before acceptance writes no note. A failure after the message was accepted (the agent could not start) is still the chat's own note, as for any send. * fix(native-chat): the row naming Orca's stop carries its own presentation and never folds A client that re-words host rows it cannot name (the draft that makes these cuts read as interruptions) treated the cause row as an older red row and replaced its words, so the cause never showed there. The row was also folded away under its collapsed turn once shown muted. The host's cause row now names the presentation 'orca-stop' beside today's words, failure fact and red tone, so a client that predates both changes still prints exactly today's row, red and on screen, and a client that re-words unnamed rows passes it through. This build shows it muted, counts it as no failure (the reply it cut stays the turn's answer), and never folds it; the fold field becomes `explainsTurn`, as the other change names it. * test(native-chat): pass the session-end event without a type assertion * test(native-chat): the row naming Orca's stop renders neutral, whoever re-presented it Pins the rendered tone on this build: the stored red row, and the same row after a reader re-presents it in the neutral tone with its presentation and cause kept, both render muted and never fold. The phone draws chat rows without tone styling, so it needs no change. * fix(native-chat): the "Couldn't continue" line goes once the chat is continued The composer line a failed or refused Continue set stayed on screen while the agent carried on: after an answer lost in transit, or once another client or the restart prompt continued the chat. Only the next Continue click or the user's own send cleared it, and a click also wiped an unrelated composer error. The line is now derived: shown only while the chat still sits on the cut that Continue failed on, so it goes as soon as the journal shows the chat continued, from anywhere. A Continue click clears only its own line, and a retry answered "already continued" leaves none. * fix(native-chat): Continue waits while an opted-in launch may still resume the chat With "resume automatically" on, Continue showed on a quit or update cut while the launch was still waiting for its settings and reading the restart offer, then vanished when the launch's own resume began; a click in between sent a competing continuation. The launch's one decision (nothing offered, ask, or resume) is now published, and the chats it resumes are named the moment it decides, with no gap. Until it decides, and while the setting has not loaded or is on, Continue stays hidden on this machine's chats; a paired server's chats are not the launch's to resume and keep it. * fix(native-chat): a runtime's end survives a late reinstall, a failed write and any clean exit Three ways the runtime record could still read a graceful stop as a crash: - A chat host reinstalled during the quit (a request landing after teardown began) recorded the runtime's start again and erased the end it had just written. A second start of the same runtime now keeps that end. - When the end could not be written (a full disk), the start alone stayed and read as a crash. The runtime now removes its record, so its chats name no cause. - Each `app.exit(0)` had to remember to record the end. A process 'exit' with code 0 now records a quit when nothing else did: Electron emits it on every quit and exit once its loop runs (`app.exit` -> Browser::Shutdown -> the app's 'quit' -> process 'exit'), and Node on every `process.exit`. The relaunch and GPU-fallback calls it covers are dropped; the quit teardown, the headless server's stop, the update watchdog and Windows session end keep theirs, which run earlier or say more. * test(native-chat): build the re-presented row as the plain status item it is * fix(native-chat): a Continue click clears the composer's old error, so its own failure shows Since the "Couldn't continue" line became derived, an older composer error (such as "Remove attachments before using a chat-session command.") outranked it: a failed Continue showed the old error instead, and a Continue that went through left the old error on screen. A Continue click is the user's newer action, so it clears the composer's error again, as before; the line then shows the Continue's own failure, if any. That failure still goes away by itself once the chat is continued, and nothing but the user's own Continue click clears an unrelated composer error. * fix(native-chat): a chat start compares the owner process, not the runtime stamped on it A chat start checks that the process it just started is the one the record names, by a deep comparison of the stored owner with the adapter's process. The store stamps that owner with the Orca runtime holding it, so the check passed only because the store happened to return the record from before the stamp; returning the published record would have refused every chat start with agent_session_ownership_unknown. The start now compares the process identity without the runtime stamp, which says who holds the process rather than which process it is. * test(native-chat): count agentSession.continueInterrupted among the structured methods * refactor(native-chat): derive the structured chat's transcript session in its own hook Main's appearance work and this branch's Continue wiring together put NativeChatStructuredSession past the 400-line limit for components. The session the transcript reads moves, unchanged, to use-structured-chat-live-session.ts. * refactor(native-chat): keep the Continue capability in its own module Main grew protocol-version.ts to its line limit; the Continue capability moves to its own module, as other capability groups have, and the runtime list still names it. * refactor(native-chat): keep two shared files within their line limit after the main merge Main left agent-session-record.ts and structured-agent-session-params.ts just under 300 lines, and this branch's additions put them over. The account-home shape check moves next to the account-home type it checks (written without a type assertion), and the Continue params move to their own contract module; the params catalog is regenerated. No behavior change. * test(native-chat): compare the store directory's files without depending on listing order The corruption test checks that no file was created or removed by comparing two recursive listings. Their order is the runtime's: with the per-runtime record directory nested under the store, Bun returns the same entries in a different order than Node. Both listings are now sorted. * fix: share the path bound main's launch-directory check needs * test: give the stop-row fold rows the draws flag main's fold now reads * refactor: mark the launch's resume decision where the resume begins * test: count main's new structured method alongside agentSession.continueInterrupted * fix: the journal database keeps its folder, where runtime end records live Main's #26038 dropped stateDirectory from JournalHostDatabase; this PR's runtime end records are read from and written beside it. |
||
|
|
990c62e6c6 |
feat(native-chat): attach files to a structured chat on a paired server (#25146)
* feat(native-chat): a paired server keeps a store for chat attachments A structured chat on a paired Orca server had nowhere to put a file the client attached. The server now keeps a per-chat attachment store under its userData, filled through agentSessionAttachment.uploadStart/Append/Commit/Abort and read back for previews through agentSessionAttachment.read, behind the structured session gate and advertised as agent-session.attachments.v1. Nothing records cleanup as owed: a sweep re-derives what may go from the host's chat records and journal (unfinished uploads after an hour, uploads for a chat that never existed or that its journal never mentions after a day). The clipboard RPC's in-flight bookkeeping moves into a shared ChunkedUploadRegistry both use. * feat(native-chat): upload attached files into a paired server's chat store Main streams dropped or picked files (fs:uploadPathsToAgentSessionAttachments) and pasted images (clipboard:saveImageAsTempFile with agentSessionAttachment) into the chat's store on its paired server, reusing the file-import slice streamer and pinning every call to the pairing revision and server process. The browser client's paste takes the same route. * feat(native-chat): attach files to a structured chat on a paired server Pasted images, files dropped from Finder/Explorer and the file picker no longer refuse with "Local attachments are not available for remote sessions." in a structured chat on a paired server: they upload into that server's chat store and the chat gets only the stored path. Dropped files show as pending chips at once so Send waits for them; every attached item carries the server, pairing and chat it was stored for, and a send to anywhere else drops it with the existing "changed hosts" notice. Chips and transcript images read stored files back through the server, never from this machine's disk. An older server gets an update notice instead of an upload. The terminal-backed chat keeps refusing. * test(native-chat): type the attachment test doubles without bare casts * refactor(native-chat): keep the clipboard RPC on its own upload bookkeeping The chunked upload registry stays for the chat attachment store only, beside it. * feat(native-chat): claim chat attachments when the host admits a message A message's references into the host's attachment store are claimed in the same journal transaction that makes it durable (a direct send, a queued draft, a /clear carry). The sweep deletes an upload only after marking it in that database while no claim exists, so a send and a sweep can never both win: a client's send that names an expired or foreign upload is refused whole with the new attachmentExpired reason, before anything is recorded. This replaces scanning chat transcripts for paths. The store is now <root>/<upload id>/<name>, refuses uploads for chats the host does not hold, caps stored names in UTF-8 bytes and avoids Windows device names. The preview read goes through the protected bounded read and the request's reply budget, so an image too large for one reply is refused instead of closing the connection. * feat(native-chat): let structured Claude read the host's chat attachment store Attached non-image files live outside the workspace; the store root is passed as an added directory so the agent reads them without asking, the common pattern. * fix(native-chat): skip a dropped folder before staging walks into it * fix(native-chat): pin the browser client's chat paste to its server Every upload call goes to the environment the paste was meant for and checks its pairing and server process, as the desktop upload does; a re-pair ends the upload instead of storing the image on another server. * refactor(native-chat): let the host's claim be the only attachment check The composer no longer records which server and pairing each attachment came from, and Send no longer strips attachments it judges foreign: the host refuses an expired or unknown stored path when it admits the message, which also covers retries, Stop-restores and queued edits the client check missed. Previews of stored files route through the chat's own server by path. Removing a file's chip while it uploads now keeps its @path out of the draft. A dropped or picked file shows its name and kind on its chip while it uploads, with no separate progress toast, and files that did not attach are named in one notice. A rich-text paste into a chat on an older server no longer shows the update notice beside the pasted text. * chore(native-chat): keep the claim hook beside the draft consume and fit the line budgets The submission's claim runs in the same append hook as a draft's consume, owned by the queued-message collaborator, so the journal store stays within its size limit. Formats the new files and regenerates the runtime-required English catalog. * test(native-chat): pin the attachment store grant in the Claude launch, restore upload result types * fix(native-chat): create the attachment store at install and claim only exact store paths Claude drops an added directory that does not exist when it starts, so the store root is created when the host installs it, best effort. A commit keeps its upload in flight until the rename lands, so a sweep never takes a slow upload's part file. Only a path under this host's exact store root is claimed and required; any other mention of a store path is plain text and never refuses the message. * refactor(native-chat): reuse the newer-Orca notice and drop leftover attach options An older server's refusal to store attachments uses the notice every other write it refuses already shows, so one string fewer in every catalog; the attach callbacks lose an options parameter no caller passes since the provenance check went. * fix(native-chat): give a message refused for an expired attachment back to the composer Sending the same message again can never bring the attachment back, so instead of a Retry that always fails the message's text and images return to the composer, as a Stop's withdrawn message does, and the notice says what to remove. * fix(native-chat): keep uploads across a prompt, say why a file did not attach An upload that finishes while a prompt card has replaced the composer lands in the scope's attachment and draft caches, which the composer reads back when it returns, instead of the unmounted composer. The single failure notice names the cause the files share, such as the size limit. Rich text pasted into a chat on an older server no longer flashes an image chip: the chip waits for the server to take the image. A picked command waits, as Send does, while an attachment is still uploading. * fix(native-chat): keep a paste's upload across a prompt; pin the Claude grant hand-off A paste uploading into a paired server's store keeps its chip live when a prompt card unmounts the composer, and its result is filed for the composer's return, as a drop's is. A failure cause ending in full-width punctuation gets no extra full stop. A runtime test pins that the store root reaches Claude's launch resolver through the adapter. * test(native-chat): type the grant hand-off captures without casts * test(native-chat): pin that the composer files a paste's upload across a prompt * fix(native-chat): retry a held rename at commit and keep cut names Windows-safe A commit renames the part file with the Windows retry the app uses elsewhere, so an antivirus or indexer holding it briefly no longer loses the upload. A name cut to the byte limit is stripped of trailing dots and spaces again. The upload pump's result cast states why it holds. * fix(native-chat): keep attachments still on their way in the pane's attachment cache A prompt card unmounts the composer, and an attachment still saving or uploading lived only in that composer: one that came back showed no pending chip, so Send went out without the file and the file then landed in the next draft. Pending chips now live in the pane's attachment cache beside settled ones and settle there whichever composer is showing, so Send waits for them after a remount too. A stored file's @path goes into the pane's draft cache, which keeps it through an input-method composition and a remount. A rich-text paste's image is registered as a hidden pending chip before the server is asked, so Send waits for it from the start, and it shows only once the server takes it. * test(native-chat): a dropped file still uploading stays pending across a remount * fix(native-chat): reveal a rich-text image in a composer that came back; keep caret inserts A rich-text paste's held image is revealed through the pane's attachment cache, so a composer a prompt card remounted during the server's answer shows it rather than holding Send on an invisible chip. A stored file's @path goes in at the caret while the composer is showing and not composing, as every other attach does; only mid-composition or after a remount does it go to the pane's draft. * fix(native-chat): never evict a pane's attachments while a composer shows them or one is on its way The pane attachment cache is now where chips live, so its 128-scope bound passes over a scope a mounted composer subscribes to or that still holds a pending attachment, and evicts the oldest scope nobody uses instead. Protected scopes are bounded by mounted composers and attachments in flight. * fix(native-chat): never evict the scope just written when every older one is in use * fix(native-chat): keep the attachment store out of the RPC method table's imports The method table is imported far and wide (the SSH relay's CLI included), and the attachment RPCs pulled in the store, whose preview read loads modules that read fs constants at load: any test that mocks fs/promises without them failed to import. The installed store now lives in a small registry module; only the host wiring loads the store itself. * refactor(native-chat): the submission hook gets its own module Merging main added the operation receipt to the queued-message insert, which put journal-queued-messages.ts over the 300-line limit. The submission hook only uses the queued-messages object's public methods, so it moves out. * refactor(native-chat): fit the attachment sweep and paste upload into main's line budgets After merging main, the record store and the clipboard handlers each ran a few lines over the 300-line budget. The sweep now asks the record store whether a chat is recorded through the two lookups it already has (readable or unreadable) instead of a new id-listing method, and the paired-server paste upload lives beside the other attachment uploads. No behavior changes. * fix(native-chat): pending chips sit beside main's saved draft, which keeps server-stored images Main now saves a composer's draft (text and settled images) in one store that survives a reload or quit. This branch kept every chip, settled or not, in its own pane cache. After the merge the saved draft owns settled images and the pane cache holds only chips still on their way: they come back pending in a composer a prompt card remounted, hold Send, settle into the saved draft whichever composer is showing, and are never saved themselves, so a restored draft cannot bring back an upload as if it were attached. An image a paired server stored for the chat is saved with the draft as the real image (a pasted one is no longer turned into a "Not kept" placeholder), and the restore check leaves it to that server, whose claim at Send refuses one it no longer holds, instead of asking this machine's disk for a path that only exists on the server. Also: previews and the pending subscription move into their own small hooks to fit the line budget, the "local attachments" notice moves to the composer-target module so the attachments hook no longer pulls in the upload module's imports, and tests follow main's new mocks. * test(native-chat): give the claims test main's provider handle and message source * fix(native-chat): a paste still uploading dies with its tab or workspace A pasted image still uploading to a paired server sits in the pending chip cache, which by design outlives the composer and its tab. Removing the workspace deleted its saved drafts but left those chips, so the upload finishing afterwards recreated and saved the deleted draft. With the workspace's tabs gone it had no owner either, so no later workspace cleanup could ever remove it. A pending chip now records the owner its draft would have, resolved the way the draft store resolves one when the chip is added. Workspace removal and a user's tab close drop pending chips by the same owner, conversation and tab matches they delete drafts by, and a chip that settles after its chat's tab closed writes its draft under the owner it was begun in. * fix(native-chat): the claim type uses the journal's own SQLite type * refactor(native-chat): read the pending-image handlers off the composer's attachments Main's multi-file picker (#23956) and this branch together took NativeChatComposer over the 400-line limit; reading the two pending-image handlers the way the neighbouring ones already are keeps it at the limit. * test(native-chat): pass the launch-args resolver main now requires in the attachment grant test * fix(native-chat): grant the attachment store beside folders the saved Claude Arguments add Main (#25721) now builds additionalDirectories from the user's --add-dir arguments; this branch's attachment-store grant replaced that list instead of adding to it. Both are granted now. Moves the Claude session-id derivation to its own module to keep the resolver under the line limit. * test(native-chat): run the attachment store's SQLite tests in the Node runtime project * test(native-chat): follow main's attach ownership recheck (#25749) in the attachment tests * test(native-chat): a named upload chip still shows when the agent takes no images * fix(native-chat): a paired-server image paste failure keeps its error apart, as main's notice card does * fix(native-chat): the attachment sweep reads the chat list directly now that records have no import still owed |
||
|
|
98f0193afb |
feat(native-chat): show agent-written visuals inline in structured chats (#26103)
* feat(native-chat): visual directive grammar and host read for a chat's visuals folder
A shared grammar for the ::orca-visual{file="..." title="..."} reply line,
the per-chat visuals folder location on the owning host, and the
agentSession.readVisual runtime method that reads one visual with lexical and
canonical containment, a 512 KiB bounded read and UTF-8 refusal.
* feat(native-chat): render chat visuals inline and in the right sidebar
Native-chat assistant replies render a ::orca-visual{...} line as the chat's
HTML visual in an opaque, scripts-only sandboxed frame: CSP first, the host
frame navigation guard registered before content runs, live theme without a
reload, fitted height, links opened in the viewer's browser only from a real
gesture, lazy mount, and one muted line when the visual cannot be shown.
Open in sidebar shows the same frame in the right sidebar, widened while it
is open and restored after.
* fix(native-chat): visual CI fixes, shared height governor, live-turn streaming hold
Registers agentSession.readVisual from the methods index so the structured
method file stays under its line budget, replaces reflective reads with checked
narrowing, moves the pure height governor to src/shared for mobile, and holds a
half-written directive tail while the turn works (structured text rows carry no
running state).
* fix(native-chat): harden the visual read and link opening
Re-checks after the open that the chat's visuals folder is still the real
directory at Orca's path, reports unexpected filesystem faults by code without
host paths, and lets one click in a visual open at most one page.
* fix(native-chat): keep visual lines out of plain-text reply surfaces; review fixes
One shared helper drops visual lines (outside fenced code) from reply text where
it becomes plain text: the structured status summary that feeds the sidebar row,
dashboard, notifications, phone rows and handoffs, and AI Vault reply previews.
Review fixes: height also counts a pinned body's overflow, only the live
frontier row holds a half-written visual line, the runaway-height stop needs the
same step repeated, and any host refusal evicts the cached revision.
* fix(native-chat): resolve the visuals folder without the removed journal-paths helper
Main removed the per-chat journal paths and the journal database's state
directory; the visuals folder keeps the same sha256 layout on its own and the
read method uses the profile state directory the chat host is opened in.
* fix(native-chat): review round 2 fixes; copy a reply without its visual lines
Reply previews in Agent Session History drop visual lines per text part before
lines are folded; the frame adds a body's overflow only when the body really
overflows; fence tracking follows CommonMark closers and openers; the copy
button copies a reply without visual lines; a coded read fault keeps its cause.
* fix(native-chat): update the frame's theme ref after render; read the visuals folder pair at once
* test(native-chat): declare agentSession.readVisual on the cross-version agent-session surface
* fix(native-chat): copying a reply keeps its code blocks and indentation
Removing visual lines now closes only the gap each removal leaves, instead of
collapsing blank lines across the whole reply and trimming its indentation; the
visuals folder is checked parent first again so a broken path answers the same
way every time.
|
||
|
|
91d6ed0f3c |
fix(worktrees): a phone or CLI create puts the agent in the first orca.yaml default tab, as the desktop does (#26106)
* fix(worktrees): the host's create puts its agent in the first default tab, as the window does With orca.yaml default tabs, the window's create starts the agent in the first default tab: that tab takes the template's title and color, and the template's command does not run beside the agent. The host's create started the agent in its own untitled tab and then created every default tab, the first one running its command too, so the workspace had one tab more than the window's create. Now the agent's tab takes the first template's place. * fix(worktrees): title the agent's default tab as a tab, so the phone keeps the agent's status The host's create titled the agent's tab by renaming the terminal, which writes the pane's own title stamped to outrank every title the agent sets later. On a headless host the phone reads status from those titles, so the agent tab showed the template title forever and lost its working/idle status. The window's create only sets the tab's custom title. The host now titles the tab the same way: the window gets the tab's custom title (not counted as a user rename), and a headless host saves it on the tab and shows it on the phone until the agent titles itself. The pane's title is left to the agent. The host also finds a terminal's tab itself from its handle, so the create over SSH, which never passed the tab, colors the agent's tab too. Every default tab is dressed by the same step, so the other tabs' titles are now kept on a headless host as well, and a failed title no longer skips the color. * fix(worktrees): find a default tab through the window's leaf once the window adopts its handle The host found a provisioned terminal's tab only through the runtime's own pty record. Once the window's graph sync adopts the handle, that lookup misses, and the default tab's title and color would be dropped with only a log line. No create reaches it today (the tab is dressed before the window's first graph sync), but any wait added before provisioning would. It now falls back to the window's leaf, as renaming a terminal already does. Tests: the restart case now saves the title to a real session and rebuilds the phone's tabs from it; a create through createManagedWorktree with an agent and default tabs spawns the agent and the second template only and titles the agent's tab "Dev" without counting it as a user rename. * test(worktrees): drop type assertions from the default-tab tests Use a runtime subclass for the protected launch scope and provisioning host, a typed store and notifier, and typed mocks in place of casts, so the changed-code quality gate passes. |
||
|
|
5b1bb78d14 |
[STA-6940] Prepare file drops for element owners (PR 2 of 6) (#25748)
* feat(file-drop): add element owner preparation plumbing * fix(file-drop): preserve feedback acceptance and destination ordering * fix(file-drop): keep path resolution in filesystem namespace |
||
|
|
00e3027762 |
feat(agent-launch): show an agent's tab at once, where the caller asked (#25430)
* feat(agent-launch): host-assigned caller identity and a launch record written when the surface exists Step 1 of the agent-launch unification, on main. - The dispatcher stamps every request's caller from what its connection proved (runtime socket: the local CLI; the desktop's IPC: the desktop; a paired socket: its device). Params never set it. - The launch record is written twice: once when the tab exists (what creation settled: on the launch command, a draft, or a submit still `unconfirmed`), and again once the prompt's fate is known. A restart in between finds the running agent instead of answering "unknown". - A replay re-derives its terminal handle from the pane key in the running host, and shows `unconfirmed` only to callers that advertise agent.launch.prompt-unconfirmed.v1. - The record store opens in its own slot, without building the chat host; the chat host is built on that same store. Rebuilt from this PR's own commits ( |
||
|
|
f7c542c7a3 |
Keep profile saving alive after a stalled main loop (#25318)
* Keep profile saving alive after a stalled main loop After a long main-loop stall (overnight sleep, dark wakes), the profile writer's overdue 30s timeout could run before an acknowledgement that was already queued, permanently retiring the writer until restart. Terminal creation then failed because pane bindings could not be saved. - Writer deadlines measure lateness on the monotonic clock and grant a bounded fresh window when the callback is overdue or the system reports suspended; resume re-arms without spending grace. Applies to initialization, every command, and the close/exit wait. - The "Saving stopped" alert is parented to a visible main window (never a parentless synchronous macOS alert), deferred until shown, deduplicated, and says whether the latest change is unconfirmed. - Timeouts, grace, writer faults, and alert presentation leave sanitized durable breadcrumbs. * Fix profile writer timeout and shutdown races * Run profile writer stall regression on Linux and Windows * Keep Electron probes out of headless runtime qualification |
||
|
|
dc8b3934b2 |
Add chat appearance settings: text size, code size, width (#25657)
* feat(native-chat): add chat-scoped color tokens * feat(native-chat): soften transcript and composer appearance * fix(native-chat): refine code spacing and faint text styling * fix(native-chat): wrap prose links at word boundaries * test(native-chat): refresh background task strip snapshots * Add native chat appearance settings and card scaffold * Persist chat text and code sizes with appearance width controls * Fix chat appearance typing, shortcut routing, and scoped typography * Use chat call identities in typography regression fixture * Keep markdown metadata typography independent of chat text size * Preserve terminal undo chords and stabilize chat resize estimates * Show only primary shortcuts in chat appearance settings * fix(native-chat): preserve appearance edits and configured zoom shortcuts * Remove unused chat appearance summary translation * Preserve command classification when applying chat code size |
||
|
|
135c92e01f |
fix(native-chat): an unsent draft survives a reload or quit (#24905)
* fix(native-chat): an unsent draft survives a reload or quit The composer's draft text, its editor document and settled image refs lived only in module memory, so a reload, quit or crash lost whatever was typed, including text a Stop had just given back. They are now saved per pane scope in localStorage: typing is debounced and flushed when the window hides or closes, text given back to the composer is saved at once, an emptied or sent draft is removed at once, and only the newest drafts within the composer caches' existing bound are kept. * fix(native-chat): save draft images outside the state updater * fix(native-chat): one store owns each unsent draft The draft's text and its image chips were saved by two writers that each rebuilt the stored record from what storage held, so a draft that once went over the size cap, or one refused write, lost its images on the next save. One store now holds each composer scope's whole draft in memory and writes storage from it: typing is coalesced, returned text, chips and clears land at once, a refused write stays pending for the next flush, and all drafts together keep to a budget so the outbox can still save a send. Closing a tab drops its panes' drafts. * test(native-chat): returned text is stored before its images arrive * fix(native-chat): keep returned text and drop launch-seed copies across reloads A message given back from the outbox could be larger than one draft was allowed to store, so the saved draft was deleted and the returned text was gone after a reload. One draft may now use the whole draft budget, which holds the largest message a send accepts. An adopted launch link is also parked in the agent's input line. Restoring it after a reload, with no seed left to replace that copy, typed it twice, so the untouched copy is shown but not saved. A composer reused for another pane now shows that pane's images, so adding one never saves the previous pane's over them. * test(native-chat): a plain 260k given-back message stays saved * fix(native-chat): saved drafts drop pasted images and die with their worktree A pasted image lives in a temp file Orca wrote, and the permission to show it is held in memory only, so after a restart a restored pasted chip showed a generic icon, and once the temp file was cleaned it looked fine but the send failed after the composer cleared. Pasted images now stay in the draft for the current run only; images the user attached from their own files are still saved. Removing a worktree now deletes its terminal tabs' drafts, as closing the tab does. A paste that finished late in a composer that had already been replaced wrote that composer's old images back over a newer send. Dropping a placeholder chip no longer writes the saved draft. The pane-change reload in the images hook is removed: the composer is keyed by pane, so a pane change always mounts a new one. * fix(native-chat): the composer renders the draft store instead of keeping a copy A composer that had been replaced, for example by a question prompt, could still finish an attach or a send later and write its own old copy of the draft over the store, so a sent message or image came back. The composer now reads text and images from the draft store and changes the draft as it is now: an added image or file reference joins what is there, and a send that settles after a round trip, or after its composer was replaced, clears the draft only if it is still what was sent. Only chips still being written and an IME composition in progress stay local to the composer. Closing a tab now deletes drafts by the exact tab id: a second chat for one session has an id that extends the first one's, so a prefix match deleted its drafts too. * fix(native-chat): keep images pasted during a command and on shown composers An image pasted while /compact or /clear was still on its way to the host vanished when the command was accepted: the clear that followed also wiped the chip still being saved. A command that settles now clears only the draft it sent, and an image pasted meanwhile stays. A composer on screen could lose its pasted images, and its unsaved launch text, once 128 other drafts pushed its record out of memory, because those are never stored. A draft that a composer is showing is no longer evicted. * fix(native-chat): a draft being written is never evicted from memory A write to a pane while every other cached draft was on screen could evict the record it had just written, losing it. Also adds the test that a send settling after its composer unmounted clears the saved draft, through the real send hook. * fix(native-chat): restored images Orca couldn't keep come back to attach again A pasted screenshot in an unsent draft vanished after a restart with no notice, and a restored image whose file had been deleted looked fine until the send failed. Both now come back as a placeholder chip that names the image and says it wasn't saved with the draft. Send stays disabled, and the send button says to attach the image again or remove it. Attaching a file with the same name takes the placeholder's place. Two browser tabs of the web client on one chat now follow each other: when one sends or edits the draft, the other drops its copy and shows the new one, unless it has an edit of its own not yet saved. * test(native-chat): no send while an image waits to be attached again * fix(native-chat): pasted images in a draft come back after a restart A pasted screenshot lived in the OS temp folder with a read permission held in memory, so a draft could only bring it back as a placeholder. Local pastes are now written to Orca's own native-chat-pastes folder under the app's user data, and a draft keeps them as real images. On restore the composer asks the main process to re-grant each one; main grants a read only when the file's real path, symlinks and junctions resolved, is inside that folder, and reports anything else as not kept, which becomes the placeholder. Pastes older than 30 days are deleted at startup, far past the host's 24 hour window for a resent message; the sweep never follows a link and never blocks startup. SSH pastes and older temp-folder pastes still come back as placeholders. * test(native-chat): the paste sweep test ages the link itself * fix(native-chat): type the paste-folder path helper with PlatformPath * fix(native-chat): type the paste-folder path helper from path.posix * fix(native-chat): placeholder chips say what happened and only a user attach replaces one A placeholder now shows the image's name and a short hint on the chip itself, "Attach again" for a file and "Not kept" for a pasted image, with the longer explanation in its tooltip. The copy no longer says the image wasn't saved: it says it couldn't be brought back, and for a pasted image it asks only for removal, since a new paste can't match it. The send button now says to remove the image to send. Only an image the user attaches (picking, dropping or pasting) takes the place of a placeholder with its file name. An image Stop gives back is added beside it, so a different file with the same name no longer hides the reminder. * fix(native-chat): only composer pastes use the paste folder, and restore grants only real paths Every local clipboard image save had moved into the paste folder, whose macOS path has a space, so a screenshot pasted into a terminal or sent to a terminal-backed agent no longer attached. Only a native-chat composer paste goes there now, through an optional field on the existing save call; terminal, editor and phone pastes stay in the system temp folder as before. An image path sent to an agent's input is escaped the way a dropped image is. The restore re-grant now grants only the file's real path, and only when both the real path and the path as written are inside the paste folder, which must not itself be a link. The sweep deletes only Orca's paste files and skips a linked folder. * fix(native-chat): a restored paste is readable by the path its draft stored Restore granted only a paste's real path, but the composer reads the preview by the path its draft stored, so with user data reached through a link the restored chip showed a generic icon. The stored spelling is granted too; it is already proven to sit inside the paste folder. A test also pins that a composer paste asks for the paste folder. * fix(native-chat): grant a paste's stored spelling only when it names the same file A grant also covers its path's own real path, so a stored spelling that is itself a link inside the paste folder could have granted an outside file while the path as restored reached a real paste. The stored spelling is now granted only when it resolves to the same file; otherwise only the real path is, and the preview falls back to the generic icon. * test(native-chat): check the outside file by its real path too * fix(native-chat): a structured chat's draft belongs to its conversation Drafts of a structured chat were saved under the pane (tab id plus a hash of the session), so the follow-up that keys them by conversation would have left every draft saved by this build invisible after an upgrade, never shown and never deleted. They are now keyed by the conversation (`agent-session:<sessionId>`) from the start: every composer showing the conversation shares one draft, Stop and a queued card's Edit give text back to it, closing the tab keeps it, and removing the worktree deletes it. Terminal-backed chats keep the pane key and lose their draft when the tab is closed. A send now leaves whatever was added since it was sent, text typed or composed and images attached meanwhile, and clears only what was sent; a draft replaced in the meantime is left alone. Lifted from #25207 ( |
||
|
|
51fe6f3fba |
fix(editor): restored tabs for files outside your projects no longer fail with Access denied (#24489)
* fix(editor): read files outside projects without a grant a restart loses
A file opened from outside every project (e.g. ~/notes.txt from the floating
workspace) read through an in-memory grant. After a restart the restored tab
only renewed that grant when it stored a full path, so a tab saved relative to
the floating workspace folder failed with "Access denied" and Retry repeated it.
Single-file reads (read, stat, exists) and open-editor-tab saves now resolve a
path outside every project in place. Paths inside a project keep the full
containment check, so a project's symlinks still cannot escape it, and every
other write stays inside projects.
* fix(editor): re-grant restored floating-workspace tabs by owner, not path shape
Problem: a file opened from the floating workspace (e.g. ~/notes.txt via
Cmd-click in the floating terminal, the floating markdown picker, or a .md
opened from the OS) loads until restart, then shows "Access denied: path
resolves outside allowed directories". Main's external-path grants live only
in memory. On restore the editor re-granted only tabs that stored an absolute
path, but floating tabs store a path relative to the floating root (~ by
default), which is deliberately not an authorized root, so they were never
re-granted. Restored floating notebooks also failed to start a kernel.
The previous commit on this branch let main read and save any path outside a
project without a grant. That widened fs:readFile/stat/pathExists for every
caller, including automatic reads of untrusted content (markdown preview
images), which opened a Windows UNC credential leak and a /dev/zero
main-process memory blowup. This reverts that model entirely.
Fix: one helper decides which client-local path a tab needs re-granted by
ownership: a floating-workspace tab, or a tab stored outside its own project.
It never grants paths a local project root covers (a grant would also
authorize a project symlink's outside target), and skips SSH-owned,
runtime-owned and not-yet-hydrated owners. Every reader that can touch a
restored tab before or without the editor loader uses it: the loader, the
restored dirty-tab conflict scan, and the paired-mobile markdown bridge.
* fix(editor): let main decide which restored-tab paths a project already covers
Problem: the restore re-grant helper decided "already inside a project" in
the renderer from its worktree list. At startup that list only holds repos
the session references, so a floating tab inside an unlisted repo was granted
(including a symlink's outside target), and the renderer's path matcher
disagrees with main's on WSL \\wsl$ vs \\wsl.localhost, which stranded a
folder-workspace tab with "Access denied" after restart. The helper also
treated a folder workspace with a missing or ambiguous host as local.
Fix: the renderer now decides only by owner (a floating-workspace tab, or a
tab stored outside a project whose owner is explicitly local) and asks main
with `skipIfInsideAllowedRoots`. Main checks the path against its own allowed
and registered roots, in both the named and canonical-parent spelling against
both root spellings: a path a project covers gets no grant, an alias spelling
of a project path gets only that spelling, and a project symlink's outside
target is never granted. Explicit-open grants (Cmd-click, drag, explorer) are
unchanged. Tests now prove each reader waits for the grant before reading.
* fix(fs): decide a restored tab's project membership from every ancestor's real path
Problem: the restore re-grant decided "inside a project" from the named path
and the real path of its parent only. When a tab path crossed a project
directory symlink and named the project through a spelling that was neither
the registered root nor its realpath (a second alias, a `..` segment, a case
variant on a case-insensitive disk, a /var-style alias of an ancestor), no
check matched, the path took the full grant, and the symlink's outside target
became readable and writable.
Fix: a path is inside a project when the named path is inside a root, or the
real path of any ancestor folder is inside a root in its registered or real
spelling. Such a path gets at most its named spelling, never its realpath. An
ancestor that fails to resolve for any reason other than "missing" now fails
closed to the named-spelling grant instead of falling through to the full one.
Tests cover each spelling; the non-symlink case also runs on Windows.
* fix(fs): read local files as regular files only, from one bounded handle
Problem: fs:readFile stat'ed a path and then read it to EOF. A character
device such as /dev/zero reports size 0, passes the size limit and never ends,
so the main process buffers until memory runs out; a FIFO hangs the open.
Writes could also target an existing device or FIFO.
Fix: every local fs:readFile (editor and log snapshot) opens the path once,
non-blocking, refuses anything but a regular file, and reads the size check,
binary probe and content from that same handle, capped at the limit even if
the file lies about its size. The AI Vault log tail opens non-blocking too,
and fs:writeFile refuses an existing non-regular target.
* feat(fs): let desktop file requests declare their shape
Problem: main decided every local file request against one allow-list plus a
set of in-memory grants the renderer had to recreate after every restart, so
a file the user opened outside a project (for example from the floating
workspace) was denied once Orca restarted.
This adds the request shape the common pattern uses, alongside the grants for
now:
- no shape (the default): the path must be inside a project root main
recognises, symlinks included. Desktop requests also accept the app-owned
floating-workspace folder; paired-client RPC never does.
- user-file: a single file the user named by absolute path, used in place.
Only fs:readFile/stat/pathExists and saving (fs:writeFile) accept it.
- document-resource: an image or PDF a document references, limited to every
project root when the document is in one, else to the document's folder,
and refused by path text before any disk or network access.
Notebook kernels and AI Vault log tails check their open file as user-named.
* feat(editor): send each local file request's shape from the renderer
Problem: after a restart, a tab opened outside every project (a floating
workspace file, a file opened by absolute path, an OS-opened markdown) could
only be read if the renderer first re-granted its path, and readers that ran
before the editor loaded the tab had no grant at all.
The renderer now says what kind of request it is making, and main checks that:
- A persisted tab opened outside its owner's root (floating workspace, or an
absolute stored path) whose owner is explicitly local reads and saves as a
user-named file, from every reader: the editor loader, the restored-tab
conflict scan, the change banner and compare dialog, the paired-phone
markdown bridge and the save queue. Project tabs stay inside their root.
- Clicks, drops, typed paths and browser-opened notebooks stat as user-named.
- Markdown preview and rich-editor images are document resources, limited to
the document's roots or folder. Images the user pasted or attached into a
chat show as user-named; agent images stay inside the project.
- The image cache keys on the shape, so one shape's image never answers
another's request.
A ratchet test lists every renderer file allowed to create a user-named
request.
* refactor(fs): delete the in-memory path grant system
Problem: main kept a set of paths the renderer had asked it to allow
(fs:authorizeExternalPath). The set lived only in memory, so a file the user
opened outside every project could be read until Orca restarted and was then
denied, and every new reader of a restored tab had to remember to recreate
the grant first. Three rounds of re-deriving grants at restore each found
another reader or path spelling it missed.
Now that every desktop request declares its shape, nothing needs a grant:
- delete the grant set, authorizeExternalPath, the restore re-grant from the
earlier commits on this branch, the fs:authorizeExternalPath channel and its
preload and web-client entries;
- delete every renderer grant call (terminal and markdown link clicks, drops,
typed paths, the file explorer, AI Vault logs, chat attachments, browser
notebooks) and every main one (floating markdown picker and folder, OS-opened
markdown, keybindings.json, pasted images, import and upload sources);
- the floating workspace's picker-approved folders stay a terminal-cwd
allowlist only.
Main now holds no per-path permission, so a restart can't change any answer.
* feat(editor): open project links that lead outside the project as named files
Problem: a file inside a project that is a symlink to something outside it
opened fine from the file explorer or a terminal Cmd-click, then showed
"Access denied" after a restart: its tab was stored as a project file, and a
project request is refused when it resolves out of the project. A folder link
out of the project expanded in the explorer until restart and then failed with
a raw access error.
Now the click decides and the tab keeps that decision. Both gestures stat the
path inside the project first; if only the user-named check passes, the path
leads out of the project:
- a file opens by its absolute path, so it reads and saves as a file the user
named, the same before and after a restart;
- the explorer does not follow a folder link out of the project and says so
("This folder links outside the project, so it can't be opened here.").
Paths that stay inside the project still open as contained project tabs. Also
drops the AI Vault "path not authorized" message, which nothing shows now.
* chore: drop the casts the changed-code quality gate flags on this branch
The FileContent casts in the editor loader and the paired-phone markdown
bridge were never needed (the read result is already assignable). Tests stub
window.api through vi.stubGlobal and pass narrow stores without casting; the
one test store that still needs a cast states why.
* fix(fs): load chat images by type, and keep escaping project links readable
Problems found in review:
- Chat transcript images were trusted by message role: any user-role
"[Image: source: <path>]" (an injected Claude record, `orca terminal send`,
a paired client's image-ref) became an automatic user-named read as the row
scrolled into view, of any file type, and on Windows a network-share path
would have opened an SMB connection to that host.
- A document image named like an image but linking to a text file
(logo.png -> .env) was read as text.
- Windows device names (NUL.png, COM1.jpg) passed the path-text check of the
automatic image loads.
- A project symlink leading out of the project, opened by a typed path, a
tab-strip drop or a browser file:// notebook, was stored as a project tab
and immediately refused.
Fix:
- New chat-image request shape for every transcript image and the composer
preview, whoever's turn named it: an absolute local path whose requested and
real targets are image files, a regular file, size-capped; network-share and
device-namespace paths and Windows device names are refused by path text
before any filesystem call. Pasted screenshots still show after a restart,
and agent images outside the project now render.
- Document resources check the real target's type too, and refuse Windows
device names by path text.
- Typed paths, tab-strip drops and browser notebooks stat through the same
check as the explorer and terminal, and open an escaping link by its
absolute path.
- Tests pin the shape at the change banner, compare dialog, markdown preview
and image prewarm; a second ratchet lists every file that can open a tab the
tab rule reads as user-named, and its comment says what it can't see.
- Stale grant wording removed.
* fix(fs): tighten automatic image loads and the project-link check
Problems found in review:
- Two unit tests went red on this branch: the browser-share test still
expected reads without a shape, and the rename test's electron mock had no
app, which the desktop root check now needs.
- The device-name check ran on the raw path, so `NUL.png\.` or
`COM1.png\x\..` (reachable from markdown ``) reached the
filesystem; a document image whose real target was a device name passed.
- Chat images in a project that lives on a Windows network share no longer
rendered, though the markdown preview showed them.
- Any failed project check (a missing file, a dropped connection) was taken
as "this link leads out of the project" and opened as an absolute tab.
- Every local read allocated about 2 MiB, even for a tiny image.
Fix:
- Device names and device-namespace paths are checked on the resolved path
and on the real target, for chat images and document resources alike.
- A network-share path in an automatic load is read only inside a project
root (the user chose that share when adding the project); anywhere else it
is still refused by path text before any filesystem call.
- Only main's "outside allowed directories" refusal marks a project path as
leading out of the project; other errors surface as before. The message now
lives in shared code so both sides agree on it.
- Reads size their first buffer from fstat and confirm EOF with a 1-byte
probe; a file that grows past its reported size is still read in bounded
chunks up to the cap.
- Fixed the two red tests.
* refactor(fs): name file access by its role, not its structure
Problem: the static-analysis anti-slop check failed the PR because the new
code named the request's file access a "shape" (`shape`, `RequestShape`,
`TabShape`), which describes structure rather than the role.
Rename the main-process module filesystem-request-shape.ts (and its tests) to
local-file-access-resolution.ts, rename the symbols to fileAccess,
FileAccessResolution and TabFileAccessFields, and say "file access" or
"access kind" in the comments and test names. No behaviour change.
* fix(fs): refuse every Windows device-name spelling in automatic image loads
Problem: the device-name check split a file name only on '.', so names such
as NUL:.png, COM1:.png, NUL:stream.png (an alternate data stream) slipped
through, and CONIN$, CONOUT$, CLOCK$, COM0 and LPT0 were not listed. Those
reached the filesystem from a document or chat image before being refused.
Split on ':' as well, list the missing device names, and test each with
Windows path rules and zero filesystem calls. Also cover the case of a local
link that leads onto a network share outside every project (refused for chat
images), and correct the shared comment on chat-image access.
* fix(editor): let users rename and insert images into files opened outside projects
Renaming a file opened outside every project (tab double-click, editor
header) and inserting an image into such a markdown document failed with
"Access denied", even before a restart: both writes only passed the
project-root check. Document resources and chat images were also limited
by file type more strictly than users expect.
- Add a "document-folder" access kind for writes beside a document the
user opened: main allows renaming only that document, to a name inside
its own folder, and importing new files only into that folder, checked
by path text and again by real path, with Windows device names refused.
The renderer sends it only for local user-named, writable tabs (rename,
its undo/rollback, image insert); SSH and runtime requests never carry it.
- Document resources: drop the image/PDF type allowlist; folder
confinement, regular-file reads, the cap and path-text refusals remain.
- Chat images: judge only the real target's type, against every
previewable image type (AVIF added).
* fix(fs): a declared file-access kind never refuses what the project check allows
A full-path tab for a file inside a project (for example a link that
leads out, opened by its absolute path) was renamed under the
document-folder rule, which limited the new name to the file's own
folder, although the same rename with no declared access could move it
anywhere in the project. Any declared kind could be stricter than the
default in the same way.
Every desktop local file request now goes through one resolver,
resolveLocalRequestPath: it runs the default project check first (roots,
Orca's floating folder, symlink containment, outside-root path text
refused before any filesystem call) and only on a refusal applies the
declared kind's rule, which adds paths outside projects. Reads, saves,
rename source and target, and import destinations all use it, so a new
kind gets the rule for free. Automatic loads (document and chat) still
refuse Windows device paths and names by text first, even inside a
project; a device is never a file to show.
The document-resource rule no longer needs its own project branch, and
chat images no longer re-run the roots check for shares.
* fix(fs): symmetric outside-project renames, notebook real folder, same-share images
- Renaming a file opened outside every project accepted a name in a
subfolder (`archive/todo.md`), but the Undo and the rollback rename,
declared from the moved file, were then refused and the file stayed
moved. A rename under document-folder access must now land directly in
the document's own folder (checked by path text before any filesystem
call), so rename, Undo and rollback are symmetric. Image import still
accepts the folder or a folder under it. Inside projects the default
check still allows any in-project target.
- A notebook opened through a link inside a project started its kernel in
the link's folder instead of the real file's folder (main's behaviour),
because notebook and AI Vault log-tail paths skipped the project check.
Both now resolve through resolveLocalRequestPath (project check first,
then the user-file rule).
- A markdown file opened from a Windows share outside every project could
not show the images beside it. Document images on a share are now
allowed inside the document's own folder; the folder text check refuses
every other host and share before any filesystem call.
- resolveDesktopAuthorizedPath is async, so a synchronous failure in the
default check rejects like any other refusal.
* fix(fs): refuse share images outside projects again; keep renames and kernels as on main
- Reverts the same-share document image rule from the previous commit.
Its folder check compared hosts case-insensitively, so a host spelled
with U+212A KELVIN SIGN (or a decomposed accent) passed as the
document's own share and was contacted, reopening the network
credential leak. Document and chat images on a share outside every
project are again refused by path text before any filesystem call;
tests now cover the look-alike hosts with zero filesystem calls.
- Renaming a file opened outside every project into a project folder
passed the project check, but its Undo (declared from the new path)
was refused and the file stayed moved. When the rename source is
allowed only as the opened document, the new name must now land
directly in the document's folder even if a project would accept it
(resolveLocalRenamePaths).
- A notebook opened through a link outside every project started its
kernel in the link's folder; main used the real file's folder. The
kernel cwd is now the real file's folder in every case.
* fix(fs): a file opened outside every project renames to any path, and keeps its access
Renaming a document the user opened (floating workspace or full-path tab) now
follows the user-file rule: the source must be the opened document, and the
new path can be any absolute path, so a rename into another folder, a
subfolder or a project works, and its Undo (declared from the moved file)
comes back from there. Any other rename keeps the project check only. Remove
the same-folder rename rule and its tests; image import stays in the
document's own folder.
After a move, a tab stored by its full path keeps its full path instead of
being recomputed project-relative, so it keeps user-file access for save,
the next rename, image insert and restore after restart. Folder moves go
through the same remap.
Also un-export unused resolver exports and avoid a copy for single-chunk reads.
|
||
|
|
70cf91299b |
Clean up retired OpenCode configuration copies safely (#25222)
* fix: wait for OpenCode worker composer before first dispatch Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * feat(opencode): probe execution-host CLI capabilities * fix(opencode): select plugin default for execution host loader * fix(opencode): limit prompt prefill capability to verified release * feat(opencode): probe launch capabilities on the execution host * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(opencode): preserve launch environment deletion boundaries * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * fix(opencode): confine overlay manifest cleanup to owned directories Co-authored-by: Adnan Khan <adnank11427@gmail.com> * fix: wait for OpenCode worker composer before first dispatch Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * feat(opencode): probe execution-host CLI capabilities * fix(opencode): select plugin default for execution host loader * fix(opencode): limit prompt prefill capability to verified release * feat(opencode): probe launch capabilities on the execution host * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(opencode): preserve launch environment deletion boundaries * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * fix(opencode): unsubscribe hook settings during async host shutdown * STRICT launch CI contract correction * CAPS launch CI contract correction * INTENT launch CI contract correction * test: initialize Claude prompt state in output retention fixture * Wait for OpenCode location hydration in intent startup * Bind OpenCode startup readiness to the current location in intent startup * Collect retired source-scoped OpenCode configuration overlays conservatively Credit brennanb2025 for the original bounded, delayed overlay garbage-collection contribution in PR #7627. Preserve ambiguous legacy and shared-service state. * Correct inaccessible-source fixture without spying on native ESM exports * Keep delayed OpenCode cleanup within existing file limits * Reuse the overlay manifest module for existing owned-entry operations * Use the existing filesystem import in the ownership mock * test(opencode): keep overlay GC link tests portable --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com> Co-authored-by: Adnan Khan <adnank11427@gmail.com> Co-authored-by: Orca startup hydration review <agents@stably.ai> Co-authored-by: OpenCode Campaign <opencode-campaign@users.noreply.github.com> |
||
|
|
87bc51d371 | Reduce test deadline waits and exact byte comparison costs (#25187) | ||
|
|
8cd9751963 |
fix(updater): keep macOS Orca open when background instances block updates (#24952)
* fix(updater): guard macOS installs against running app instances * fix(updater): match native app blockers and preserve quit lifecycle * fix(updater): keep ordinary macOS quit on Squirrel's install-on-exit path Converting every quit with a staged update into quitAndInstall made Cmd+Q relaunch Orca, refused the quit when background instances existed, and hijacked app.relaunch()+app.quit() restart flows (profile switch, admin restart) into an update install racing the relaunched old app. Only Update & Restart runs the running-instance preflight now; the quit-without-install allowance is no longer reachable and is removed. * fix(updater): preserve quit intent through macOS staging * test(native-chat): explicitly model legacy published tab ownership --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
73904fcdac |
Release completed updater setup timers and callbacks (#24920)
Cancel completed deferred updater fallbacks in finally, clear only their exact pending callback, and drop the captured timer before the original fallback guard runs; preserve destroyed admission, successor ownership and updater/quit callbacks. |
||
|
|
b49abdb1f4 |
fix: recover renderer launch failures in the running app (#24250)
* fix(recovery): back off a launch-failed renderer instead of tripping the crash breaker
A renderer that the OS refused to spawn (macOS exit 1003 = LAUNCH_RESULT_FAILURE; field
cause: per-user process limit, posix_spawn EAGAIN) burned the 3-reload crash-loop budget
in ~750ms and raised a "graphics driver" prompt, while the condition lasted minutes.
- launch-failed retries in place on a 250ms..60s backoff (~2 min), outside the breaker;
a loaded document resets it. Other crash reasons keep the breaker.
- Each launch failure records renderer_launch_failed_probe {spawnError} from a cheap
spawn probe, so bundles name EAGAIN/EACCES/ENOENT directly.
- The exhausted prompt says the process limit was hit (probe EAGAIN), drops the
graphics-driver wording, keeps Try Again as default, and offers no Restart:
app.relaunch also needs a free process slot and silently fails without one.
* fix(recovery): skip the launch probe on Windows and probe the prompt once
- Re-check quitting after the prompt's probe; don't re-probe on Copy Commands.
- recordRendererLaunchFailureProbe never rejects (breadcrumb write guarded).
- Windows: no spawn probe; a child per failed launch is the per-operation burst EDR scores.
* test: cover quitting and duplicate renderer launch failures
* test: use typed access in PTY delay regression fixture
* fix: scope extended launch retries to POSIX hosts
* test: cover launch probe behavior on native Windows
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
|
||
|
|
f20836c296 |
fix(recovery): prompt instead of reloading into a repeat Windows OOM when commit is exhausted (#23886)
* fix(recovery): ask instead of reloading into a repeat Windows OOM with exhausted commit When another program exhausts Windows commit (RAM + page file), the renderer OOMs, Orca auto-reloads 250 ms later, and the new renderer OOMs again within seconds (launch 13084: 3.5 s after the reload; launch 22912: 34 s). The crash-loop breaker (3 in 60 s) never opens for this cadence, so the user is never told the machine is out of memory. Keep the first automatic reload, but when a win32 reason=oom death follows another OOM within 5 minutes and the pre-gone host sample shows under 512 MB of available commit, escalate to the existing recovery prompt with a new 'low-commit' cause that names the MB left and suggests closing apps or growing the page file. Records renderer_recovery_low_commit_prompt. No-op on macOS/Linux and when commit is healthy. * fix(recovery): gate low-commit prompt on post-OOM readings and recovered deaths only - Reject pre-gone samples taken at or before the previous OOM; they miss the commit that corpse released. - Record an OOM for the repeat window only once recovery actually runs, so skipped teardown OOMs cannot suppress the next first reload. - Skip the install-ACL diagnosis on the low-commit prompt, whose text would not explain Copy Commands. * fix(recovery): read commit at gone time when no sampler tick followed the previous OOM The 10 s pre-gone sampler lands between ~3.5 s repeat OOMs only ~35% of the time, so the gate usually fell back to a silent reload. A gone-time read can only over-report free commit (the corpse already released its pages), so it can miss a prompt but never raise a false one. * fix: reject invalid low-commit readings and clarify recovery advice --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
449b8ca17d |
fix(drop): route local terminal and composer drops through the resolver (#24009)
* fix(drop): copy macOS drag-temp files so the PTY daemon can read them
macOS screenshot thumbnails live in $TMPDIR/TemporaryItems/NSIRD_*, which
only processes attributed to Orca main may open. The detached PTY daemon is
not, so agents in local terminals get EPERM and Claude Code silently drops
the paste.
fs:resolveDroppedPathsForAgent now copies those files, and only those, into a
private per-user orca-drops-<uid>/orca-drop-XXXXXX/ directory, keeping the
original name. It streams from an O_NOFOLLOW handle capped at the inspected
size, so no xattrs (com.apple.macl) come along. The copy is 0600 in a 0700
directory, bounded by the remote-import per-file and per-drop limits, and
rechecked for changes. Other paths pass through. The local branch returns
per-item results, authorizes what it returns, and accepts no worktreePath.
Expired copies are swept 7 days later.
No renderer calls the local branch outside WSL yet, so this ships dark
until the renderer routes local drops through it.
* fix(drop): route local terminal and composer drops through the resolver
Local terminal drops pasted the dropped path directly, and composer drops
attached it after a per-path authorize call. So a macOS screenshot thumbnail
reached Claude Code as a path in a folder the PTY daemon can't open, and the
paste was silently dropped.
Every local terminal drop now calls fs:resolveDroppedPathsForAgent, pastes
what it returns, and reports skips and failures with local wording ("Could
not prepare N dropped files"). The WSL-only branch and the direct-paste
branch are gone; the local-WSL path mapping stays as a step after
resolution. The composer resolves the whole drop in one call, without a
project path so its attachments never get the WSL rewrite, stats only the
resolved paths, and folds resolver skips and failures into its existing
toast. The pane, transport, mounted and owner checks still run after the
await.
* test: verify resolver and write errors surface independently on drop
Add a test case ensuring that when path resolution and PTY write both fail during a file drop, the UI reports both failures separately rather than letting the write error mask the resolution issues. Refactor error handling in pasteLocalDropPaths to catch IPC resolution errors immediately, then handle paste errors separately, so skipped/failed files are always reported via the finally block regardless of outcome.
* test: extract transport variable in drop resolution test
Improves readability by extracting the terminal transport creation from the Map initialization.
* refactor(drop): extract native file drop relay and temp staging utilitie
- Move the native file drop relay queue from attach-main-window-services into
a dedicated native-file-drop-relay module so it owns the async copy and
forward pipeline for drag-temp files, separate from main window setup.
- Extract shared temp-directory management (ownership checks, sweeps,
permissions) into owned-temp-staging-root, used by both drag-temp copies
and remote clipboard staging.
- Simplify dropped-path-resolution to handle only the WSL path rewrite on
local worktrees; the relay handles macOS drag-temp copying before it
reaches terminal/composer drop handlers.
* fix(drop): pass drag-temp files through uncopied with timeout and budget
Large files exceeding the copy budget are now passed through uncopied instead
of rejecting the drop, so copy failures don't lose the entire interaction.
Copy timeout prevents hung copies from blocking subsequent drops, and budget
tracking accounts for retained copies to prevent disk fill.
Extract darwin-user-temp-dir to resolve the correct macOS per-user temp dir
rather than relying on $TMPDIR.
* fix(drop): discard queued drops on renderer reload
Capture the renderer's lifetime when a drop is enqueued. When the
renderer reloads before a copy completes, the operation cancels and
queued drops are discarded, preventing stale content from reaching
the reloaded document.
* fix(drop): serialize drag-temp copies and localize failure reasons
Main no longer sends user-facing failure messages; instead it sends reason tokens
that the renderer localizes. Drag-temp copies run serially under one byte budget
with a pending-copy limit, so non-temp drops can overtake. When a copy stage
aborts, remove any partial copies made so far. Distinguish 'uncopied' (original
handed over) from 'failed' (couldn't get it at all), and add specific reasons for
storage, permission, timeout, and budget exhaustion.
* fix(drop): serialize drops and extend TTL to 7 days
Ensure drops reach the renderer in arrival order by queuing all path drops,
not just copies. Extend TTL from 24h to 7d to support lazy readers like
drafts and startup prompts. Withhold uncopied files from agents that can't
open originals (terminal, composer), keeping editor-only access working.
|
||
|
|
a4606ccae3 |
fix(cli): orca file open no longer moves your view unless you pass --focus (#24244)
* docs(cli): file open/diff/open-changed say they switch the user's view and are for user requests only Refs #9944 * fix(cli): file open/diff/open-changed leave the user's view alone unless --focus `orca file open`, `file diff` and `file open-changed` always switched the desktop to the target worktree, selected the tab and revealed it in the sidebar. An agent skill that opens its answer pulled the user out of whatever they were typing in (#9944), and a phone opening a file moved the desktop too. The commands now add the tab in its worktree without changing anything on screen, including when that worktree is the one being viewed: the new tab is added to the tab bar but the active tab, tab type and focus stay put. In a worktree the user is not viewing, the tab becomes that worktree's selection so it is in front when they go there. `--focus` keeps today's behavior. files.open / files.openDiff take an optional `navigation` target (the existing RUNTIME_NAVIGATION_TARGETS vocabulary); the CLI sends 'all' for --focus, like `worktree create --activate`, and nothing otherwise. The renderer moves the host view only when the target reaches the host; a missing field (phones, older CLIs) leaves it still. Editor opens for a worktree other than the on-screen one no longer write the global activeFileId/activeTabType. Refs #9944 * test(cli): justify the window and runtime stubs in the file-open notification test * fix(cli): keep phone file opens switching the desktop; the CLI asks for 'caller' Phone opens send no `navigation` field, and the phone's diff-review "Open in session" relies on the desktop selecting the diff it opened. A missing field now keeps the original switch exactly; the CLI says what it wants instead: 'caller' (no host move) by default and 'all' for --focus. Older CLIs, which send nothing, keep switching as they always have. Refs #9944 * fix(cli): background file opens select the tab without counting as a visit A CLI open into a worktree the user is not viewing selected the new tab with the same activation a user click uses, which stamps lastFocusedAt and the group's recency list. The worktree jump palette sorts recent tabs by that time, so every agent `orca file open` into another worktree jumped to the top of the user's recent tabs. Editor opens now take a selection mode: 'focus' (default, unchanged), 'background' (select within its worktree without recording focus or recency) and 'none' (add only). createUnifiedTab and activateTab gain recordFocus:false for the background case. Also: tests for reopening an already-open file or diff without --focus, a comment that file opens move only the host window ('all' acts as 'host'), root help lines back under 100 columns, and an accurate remote test title. Refs #9944 * fix(tabs): a background-selected tab still joins its group's tab history recordFocus:false skipped both the focus-time stamp and the group's recentTabIds append while still making the tab the group's active tab. Ctrl+Tab looks the active tab up in that history, so after a background CLI open it did nothing (or went to the wrong tab) once the user switched to that worktree, and hydrate kept the broken history across a restart. Only the focus-time stamp is skipped now; the jump palette's recent rows sort by that alone, so the palette fix stands. Refs #9944 * fix(cli): file open/diff/open-changed --focus help says it brings the user to the file The three commands borrowed the shared --focus line written for terminal create ("Reveal the created terminal session in Orca"). They now use the per-command flag help table; terminal create's line is unchanged. Refs #9944 |
||
|
|
5b93c6216a |
Fix Chat UI paste intake and pane routing (#23784)
* fix(chat): separate text paste from attachments and route by pane Keep composer text independent of image checks and saving, and route pastes caught underneath chat to the originating pane's mounted input. Preserve native event data, selection replacement, undo, and target lifetime checks. Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: lurunzi <lurunzi@gmail.com> * fix(chat): keep focus and quiet text paste after routing it to chat - A paste inserted into the composer now moves focus there, as the old menu-paste insert did; otherwise a paste routed from the hidden terminal left the next keystrokes going to that terminal. - With a remote-server or not-ready workspace, pasted text no longer shows the "Local attachments are not available" refusal because the clipboard also held an image rendition (common for Office copies). The menu path probes for an image only when the text read is empty, so a paired browser does one permission-gated clipboard read for a text paste, not two. - Latest-value refs update in a layout effect instead of during render. * perf(clipboard): answer "is there an image?" from the format list The chat composer asks the main process whether the clipboard holds an image before explaining an image-only paste on a remote-server workspace. That probe decoded the whole image (readImage().isEmpty()) on the main thread just to return a boolean. Read clipboard.availableFormats() instead, and share the MIME check with the paired-web probe. * fix(chat): a chat cover owns focus, so input never reaches the hidden terminal When a Chat UI tab opened over its terminal, the terminal's xterm kept keyboard focus until the composer claimed it a frame later, and forever if the composer never became ready (still starting, a question card, a phone holding input). The previous commits rerouted paste from that hidden terminal to the chat, but typing and Enter still went to the terminal, an image-only or refused paste left focus there, and about twenty terminal.focus() call sites could put it back. Make "a covered terminal cannot hold focus" structural instead: - The chat cover takes focus in the commit that mounts it and marks the covered xterm inert, so every terminal.focus() path is refused by the browser. Split siblings are untouched. When the chat goes away the xterm is un-inerted, and gets focus back only if focus was inside that chat. - Terminal paste listeners skip anything inside a chat cover (previously only inside a mounted chat root). The reroute from terminal to chat is gone; terminal-only paste is back to main's code. - A paste that finds no chat input (before the chat mounts, or an approval card with no text field) gets a visible refusal from the cover. A disabled composer shows the same notice inline instead of dropping the paste. New copy: "Can't paste — this chat isn't accepting input right now." (the old "Worktree not ready" toast was wrong for a chat that is still starting). - The terminal context menu, which names its pane, keeps a small request event to that pane's chat, now without a clipboard payload and using the existing covered-pane check. - Cmd/Ctrl+V or Shift+Insert on a non-input part of the chat focuses the composer (or question answer) first, so the paste lands there. - The composer-scope check used to decide whether a text field inside the chat keeps its own paste matched the whole pane (the file-drop surface carries the same attribute). It now asks the composer whether the target is inside its input. * fix(chat): don't paste a copied file's name next to the file Copying a file in Finder or another file manager puts its name on the clipboard as text/plain beside the file itself. Since text and images are now pasted independently, pasting such a copy into a local or SSH chat inserted the file name into the prompt as well as attaching the image. On the paste-event path, text/plain that is exactly the names of the pasted files (one per line) is the file's label, not prompt text, so it is dropped when an image from that paste is being attached. Rich-text copies (text plus an image rendition) still insert their text, and a copied non-image file, which is not attached, still pastes its name as before. * fix(chat): don't type a Finder file's name on Cmd+V either On macOS, Cmd+V in the chat goes through the app-menu paste, which reads the clipboard text and saves the clipboard image separately. A file copied in Finder also puts its name on the clipboard as text, so the composer typed the name next to the attachment. On main the menu path never read text once an image saved. The main process now reports the paths of the files a file manager copied (macOS filenames plist or file URL, Explorer's FileNameW, a Linux uri-list). Text that only labels those files waits for the image outcome: dropped when an image is attached (or refused on a remote owner), typed when none came. The same label rule now also accepts a path or file URL per line, which is how Linux file managers label copied files on the paste-event path. * fix(chat): pane focus aimed at a chat lands on the chat Since the covered terminal became inert, focusing a pane that shows a chat (keyboard pane navigation, focus-follows-mouse, split activation) was refused and focus stayed on the pane the user left, so typing went to that visible sibling terminal. The one place a pane's focus is requested now puts it on the pane's chat cover, which hands it to the composer when the pane is revealed. Focus already inside the chat is left alone. * test(terminal): give fake panes the container pane focus now reads Pane focus checks the pane's container for a chat cover, and these two fixtures built panes with only a terminal, so four tests threw. * refactor(native-chat): move composer paste handle and chat-root key routing into their own modules Brings NativeChatComposer.tsx and NativeChatResolvedView.tsx back under the 400-line limit after merging main. No behavior change. --------- Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: lurunzi <lurunzi@gmail.com> |
||
|
|
59ef74876f |
fix(terminal): the terminal's owner answers colour queries for the terminal's whole life (#23925)
* fix(terminal): the PTY owner answers OSC 10/11 for the terminal's whole life Codex and Claude's `theme: auto` ask the terminal for its foreground and background colours (OSC 10/11) and pick their colours from the reply. Orca answered in the process that owns the PTY only for agent launches and only for 5 s; after that the query was handed to whichever viewer was attached. On Windows ConPTY the owner kept swallowing the query but stopped answering it, so a Codex started from an older shell tab lost its message shading (#22332). On a headless `orca serve` host no viewer existed yet, so a Codex started before anyone attached got no reply at all (#22500). The owner (in-process provider, terminal daemon, SSH relay) now answers every OSC 10/11 query for the PTY's whole life and strips it, so no downstream view ever sees one to answer twice. It answers from, in order: the host-wide viewer theme pushed to that process, the creating viewer's colours sent at spawn (now for every PTY, not only agents), and Orca's default dark theme. The desktop pushes its renderer theme to every owner on change and on (re)connect: a daemon request gated on protocol v38, and an SSH relay notification that older relays ignore. The answer-once rule, the 5 s colour window and the colour-authority handoff are removed; Kitty keyboard queries keep their startup window. Viewer-side answerers (renderer xterm, main's hidden-pane model responder, the mobile webview) stay as the fallback for older owners, which still hand queries off; they are never reached for a new owner. * fix(terminal): answer OSC 10/11 with the colours the pane is really painted with Review follow-ups to the lifetime PTY-owner colour answerer. - The theme catalog moves to src/shared so the renderer and the PTY owners read one source; the owner's last-resort default is derived from it rather than copied. - Main seeds every owner from the host's saved theme settings (light or dark, custom themes, colour overrides) at startup, so a headless host and a desktop pane that queries before the renderer's first push are not told dark to a light-theme user. The renderer's push replaces it. - Colours an app sets with OSC 10/11, and clears with OSC 110/111, are tracked per terminal and reported back, as a viewer paints them; a theme change drops them, as a viewer's theme apply does. - A terminal a paired client created with its own colours answers with those, not the host's theme (`colorSource: 'remote-viewer'` on the spawn intent), so a light client on a dark host is told light. - After the 5 s startup window a reply's echo is watched for 512 bytes instead of 256 KB, and a torn query candidate is released after 500 ms rather than held indefinitely. * fix(terminal): keep the long echo watch for relayed replies; one theme lookup The 512-byte post-startup echo watch now applies only to replies the PTY owner produced itself. A viewer's reply relayed through answerLiveQueryReply keeps the 256 KB watch, because a cooked-mode app can keep printing after it queries and the echo then trails that output. The renderer's getTerminalTheme now calls the shared lookupTerminalTheme, so the custom-vs-built-in theme lookup exists once. * perf(terminal): scan colour overrides in one pass over each PTY chunk Two indexOf searches per OSC went quadratic on long runs of ST-terminated hyperlinks, and the tracker now sees every chunk of every terminal. * fix(terminal): one host viewer colour value, set by whichever viewer acted last A paired client's colours reached the host only as frozen spawn colours on terminal.create, tagged remote-viewer. UI-started agent sessions on a headless host answered OSC 10/11 with the host's saved theme, and a client's theme flip never reached panes it had created. The host now holds one viewer colour value that every PTY owner answers with. The desktop renderer's push, a window focus on the host, the new terminal.setViewerColors RPC, and terminal.create colours from older clients all set it; equal values do not re-notify daemons or relays. The remote-viewer tag (colorSource / terminalColorQuerySource / spawnFromRemoteViewer) is gone; it never shipped in a release. * fix(terminal): paired clients push their terminal colours on connect, change and focus The renderer publisher now hands each published fg/bg to subscribers. A new remote-runtime-terminal-color-push module calls terminal.setViewerColors on every host this client is connected to when it connects (or the host restarts), when the colours change, and when the window gains focus. A host that answers method_not_found or forbidden is not asked again until it reconnects. The app shell also republishes terminal view attributes on settings and system theme changes, so a theme change reaches main and paired hosts with no terminal pane open. * fix(terminal): a host with its own window answers OSC 10/11 with its own theme Round 1 kept one host-wide viewer colour value set by whichever viewer acted last, so a paired client's push (reconnect after sleep, a dusk theme flip) took over the host desktop's own panes until its window regained focus. The value is now derived: this host's renderer colours when a local window has pushed, otherwise the last paired client's push (terminal.setViewerColors or terminal.create colours), otherwise the saved theme. Only a headless host takes a client's theme. The window-focus reassert and the identical-re-push takeover are gone; owners are notified only when the derived value changes. * perf(terminal): scan only OSC starts for colour queries once the Kitty window closes The PTY owner answers OSC 10/11 for the terminal's whole life, and it tried every ESC as a query start: a 240 KB SGR-heavy read cost about 2 ms and 256 KB of bare ESC about 15 ms, long after startup. Once the Kitty query window closes only an OSC colour query can match, so the scan jumps between ESC ] starts, plus a trailing lone ESC so a query torn right after its ESC still resolves on the next read. Output and replies are unchanged. |
||
|
|
993183afd7 |
fix(terminal): every explicit terminal close commits through one main transaction (#22929)
* fix(terminal): every explicit terminal close commits through one main transaction
A renderer save cannot shrink terminal membership once main owns a repo's
topology, so desktop tab and pane closes, CLI split-pane closes and mobile
split-pane closes only became durable when the killed process's exit retired
the surface. A close whose kill failed or threw, or whose exit was never
certified, came back after a reload.
Every close now reaches closeTerminalSurface: the renderer sends an explicit
intent for user and cleanup closes, the CLI and mobile split-pane closes commit
the pane after their stop, and the headless and relayed mobile closes reuse the
same commit. A failed flush keeps the in-memory removal and no longer cancels
the kill. Exit retirement is unchanged.
* fix(terminal): tell the desktop renderer to drop a split pane main closed
A CLI or mobile close of one pane in a split commits the pane in main, but the
desktop kept showing it until reload when no exit arrived to remove it. The
close now sends a leaf-addressed notice: a mounted pane closes by leaf id, and
a parked tab collapses its stored layout. Addressing by leaf makes the notice
and the renderer's exit handling no-ops after each other, which replaces the
numeric pane-id notice that could close the whole tab when the exit won.
* fix(terminal): a pane close never widens into a whole-tab close
A leaf-addressed close fell through to the whole-tab close whenever main's layout no longer
held that leaf as one of several. Main's exit handling retires an exited split pane from the
saved layout, so closing that pane afterwards (the exited-pane overlay's Close, or a CLI close
whose stop delivers the exit first) removed the whole tab, live sibling included, and the
next renderer save could not restore it. A pane close is now a no-op unless its leaf is in a
multi-pane layout.
Also updates two mobile split-close assertions to expect the leaf-addressed notice, and adds a
test that a relayed mobile close of a renderer-listed tab still reaches the renderer's pin guard.
* test(terminal): cover the PTY-handle branch of a CLI split-pane close
The existing CLI split test resolves its handle through the renderer graph, so the branch
that closes a runtime-owned pane by its PTY handle had no test failing without its commit.
* fix(terminal): a CLI pane close with an unconfirmed stop closes only that pane
`orca terminal close <handle>` on one pane of a split used to close the
whole tab, live sibling included, whenever that pane's stop could not be
confirmed (for example an unreachable SSH host). An unconfirmed stop is
unverifiable, not a reason to drop siblings: the close now commits only
that leaf, tells the renderer to drop that leaf, and leaves the owed kill
to the controller's existing SSH pending-kill path.
On a host where no renderer lists the tab, main now also removes the
closed pane from the paired-client snapshot (with its retirement proof),
since no exit may arrive to do it.
* refactor(terminal): one resolver decides whether a pane close becomes a tab close
Every explicit close now states its target as `{kind:'tab'}` or `{kind:'pane', leafId}`; no
optional leaf id silently means the whole tab. Main resolves a close it started in exactly one
place, reading the copy of the tab's panes its layout owner holds (the renderer-published layout
for tabs the desktop renderer lists, main's session layout otherwise). Only `last-pane` escalates,
through the existing tab path so the renderer's pin guard still runs; an unknown pane never widens.
- The CLI and phone paths drop their per-site sibling counts for the resolver.
- The notifier splits into a tab-only close and a leaf-addressed pane close.
- The headless tab closer takes a parent tab id, so a pane row cannot reach it.
- A phone close of one pane on a host with no desktop window now stops and closes only that pane.
- A phone close of one pane with no live process record closes that pane, not its tab.
* fix(cli): an unverifiable stop says the close happened
`orca terminal close` still exits 1 when the process stop cannot be verified, but its message now
says the terminal was closed and names the host's reason, instead of "close failed". It promises
that the kill retries on reconnect only when the SSH relay itself never answered the stop, the one
case a recorded kill order backs.
* fix(terminal): a phone pane close commits even when its kill fails
A paired client's close of one pane threw `terminal_close_failed` before committing anything when
the controller reported the kill failed, so the pane stayed. The kill is now best-effort, as it is
for a whole-tab close: the pane's removal always commits and the failure stays on the PTY's
liveness verdict.
* fix(terminal): a pane close widens only when a copy shows it is the last pane
The close resolver read an owner copy that records no panes as "the tab has
one pane", so a CLI close of one pane of a split, addressed while the
renderer listed the tab before publishing its panes, closed the whole tab.
Every copy now counts only if it records at least one pane, read in the
owner's order with the published rows as the last fallback, and a pane
close widens only when a copy lists that pane as the tab's only one. An
unsplit tab whose saved layout predates its pane still closes: its
published row names the pane.
* fix(cli): promise a kill retry only when the host recorded the kill
The close receipt inferred "the kill retries when the host reconnects" from
the stop reason's text, which a new transport message or a reworded error
would silently break.
An explicit close now records the replayable kill order when its stop goes
unconfirmed, before sending the follow-up kill (whose own failure is
recorded only once its RPC settles), and reports that on the receipt as an
optional `pendingKillRecorded`. The CLI promises the retry only from that
field, so an older host, which never sends it, gets no promise.
* test(pty): justify the controller cast the recorded-stop tests extend
* fix(terminal): parse the close target with typed narrowing
The low-evidence lint gate rejects Reflect.get and broad object parameters,
which failed static analysis. Narrow with 'in' checks instead and cover the
boundary parser's accept and reject cases.
* fix(terminal): a desktop tab close is not refused by a split that bound while it waited
The renderer has already removed and killed a tab it closes, so its close intent now skips the
owner fence phone and CLI closes use. Before, a split pane whose binding was admitted between the
close request and its durable write made main refuse the close, and the tab came back on the next
launch whenever its processes did not exit.
* chore(terminal): note that closedByLayoutOwner goes away once main owns the terminal layout
* test(terminal): reload the close-intent fixture through the SQLite profile store
Main now requires a SQLite profile-state authority for a writable Store, so the save-and-reload
close tests build and reopen their store through the shared SQLite test harness.
|
||
|
|
f5f537ef14 |
Revert "Support mouse Back/Forward buttons in shortcuts (#23287)" (#23350)
This reverts commit
|
||
|
|
a86fae0889 |
Support mouse Back/Forward buttons in shortcuts (#23287)
* feat: support mouse Back and Forward shortcut bindings * fix: ignore duplicate mouse shortcut presses until release |
||
|
|
4cafa50ec0 |
fix(windows): reuse shared PowerShell literal quoting at every hand-rolled escaper (#23083)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
82412dab8b |
Persist profile state in SQLite with background writes (#22612)
Migrate profile state to SQLite and move writes and backups into a background worker. Acknowledge terminal, SSH and automation changes only after durable saves. Preserve JSON import, recovery, rollback and compatibility exports. Validate migration, worker failures, maintenance, cross-profile moves and terminal lifetime races with unit, integration and end-to-end coverage. |
||
|
|
f2ac9f29b2 |
fix(browser): let pixel capture hold its own page drawn, without the desktop window (#22534)
* fix(browser): let pixel capture hold its own page drawn, without the desktop window Screenshots were the last browser commands that still borrowed the desktop window: they took the per-page automation-visibility lease, which waits for two desktop-window animation frames (capped at 2 s) and never arrives when the window is minimized or throttled. Only pixel capture actually needs a page drawn — input, scripts, layout, the accessibility tree and PDF all work on a hidden page. Capture now takes a main-owned paint hold: a synchronous, per-page ref-count that tells the renderer one way (no reply awaited) to keep the page drawn and keeps the desktop renderer unthrottled while held. Both Orca's full-page capture and the agent-browser helper's screenshots take it in cdp-screenshot.ts and retry on a bounded schedule until the page answers with a frame; a CDP error fails fast. Deleted: the queue's needsPaint lease, the executeJavaScript acquire path and its two racing 2 s timeouts and late-token cleanup, the renderer's rAF wait and window bridge, the capture commands' own leases, the fixed 300/500 ms settle waits, and the global one-screenshot-at-a-time lock. Rebased onto main after #22528 landed; content identical to the reviewed branch head 7b390ed6a8. * fix(browser): probe for a frame instead of repeating the full capture Retrying a capture resent the caller's full request, so on an already drawn tall page (a full-page capture takes ~0.5 s) the 250 ms retry started a second full beyond-viewport capture while the first was still running. Measured on Electron 43: any later request makes a held page produce a frame, and that frame answers every pending capture with a full, correct image. So the capture is sent once and 1x1 probes follow until it answers; their results are ignored. Also report a detached debugger as detached rather than destroyed, and give the layout-metrics timeout its own "did not respond" message, since that request doesn't need a drawn page. |
||
|
|
800d33e5c9 |
feat: name runtime machines (#22094)
* feat: name runtime machines
* fix: preserve pairing address optionality
* fix(cli): keep host and environment listings local
Listing paired servers read each one's machine name by dialing it, so both listings made a network
round trip per server and waited out a timeout on any that were offline. They answer from this
machine's own pairing store; `orca host name --environment <name>` reads one server's name.
* fix(settings): caption the machine name paired devices actually receive
The caption read the runtime's published name once, when the pane opened, so saving an override
left it naming the old computer while phones already showed the new one. It now re-reads whenever
the saved override changes; the settings write lands in the main process before the store publishes
it, so that read already sees the new name. The name is interpolated rather than baked into the
fallback, and the caption, label and placeholder are in the English catalog.
* refactor(settings): normalize the machine name in one place
The trim and length rules for `machineName` were spelled out separately at the
renderer IPC (trim + 255), the settings load path (trim only, no cap), the RPC
schema (zod trim + 255) and the runtime reader (trim). A hand-edited or legacy
profile could therefore load a longer name than any writer accepts.
`src/shared/machine-name.ts` now owns `MACHINE_NAME_MAX_LENGTH` and
`normalizeMachineName`, and every writer and the load path use it. The RPC
schema keeps rejecting over-long names but derives its cap from the constant,
and the runtime settings controller normalizes an RPC write before storing it.
* fix(runtime): detect the machine name once and label handoffs with it
Every runtime constructed in a process (the app, plus each one a test builds)
ran its own `scutil` lookup. The friendly name is a property of the host, so the
lookup is now a single shared promise; construction still never blocks on it,
and a rejected lookup can no longer surface as an unhandled rejection.
The structured-chat handoff banner ("Agent is open in terminal on X") named this
host with the bare `os.hostname()` while paired devices saw the published name.
The transport now reads the same `RuntimeMachineName`, through a getter so a
rename in Settings is reflected without rebuilding the transport.
* fix(cli): print the name the runtime publishes and keep its envelope
`orca host name --name X` printed `undefined`: `settings.update` replies with
`{ settings }`, but the handler read a bare `machineName` off the reply, and the
test fixture mirrored the wrong shape so it passed. After a write the command
now re-reads `status.get` and prints what the runtime publishes, so a blank
`--name` prints the detected name it returned to rather than an empty string.
The read path wrapped a possibly routed answer in a local envelope, stamping
`_meta.runtimeId: "local"` on a reply from another server. It now returns the
`status.get` envelope itself, and an unreachable runtime is reported as the
usual error instead of an invented "unknown" name.
`environment list` had gained machine-name and platform columns that no caller
populated, so every row printed "platform unknown"; the columns are removed.
* refactor(settings): give the machine name field its own component
The caption under the field re-read runtime status every time the saved value
changed, relying on a comment about write ordering to show the new name. A saved
override already is what paired devices see, so the hook now derives the caption
from it and asks the runtime only for the detected name; a stale status read can
no longer show the previous name.
`MobileMachineNameField` owns the store read, the published-name hook and the
debounced input, so `MobilePairingSetupSection` returns to its prop shape and
the pass-through `MobilePanePairingOutput` wrapper is gone. Paired-device
revocation moves into `useMobilePairedDeviceRevocation`, which keeps
`MobilePane` within its line budget with an extraction that carries behavior.
The web client mounts this pane too, but its settings store kept the name
locally where nothing published it. `machineName` now rides the existing
runtime-backed settings sync so the field renames the paired runtime.
* refactor(settings): normalize the machine name at the store boundary
Every writer (desktop IPC, web RPC, CLI) reaches the store through
updateSettings, which already normalizes the other free-text settings
there. Trim and bound the machine name in that one place instead of at
two upstream edges, so a future main-process writer is covered too.
* test(settings): pin machine-name routing and detection, and make the field searchable
The shared machine-name lookup test spawned the real `scutil` twice and compared the answers, so a
slow runner could time one spawn out to the hostname and fail. It now mocks the subprocess, proves
the hostname answers until the one shared lookup lands, and that a second runtime does not spawn
again.
`host name` is no longer pinned local, but only the explicit `--environment` route was covered; an
ambient `ORCA_ENVIRONMENT` now has its own test so the pin cannot silently grow back.
The Machine name field is added to the Mobile pane's search catalog at the tail, keeping every
existing row's tie-break index.
* fix(runtime): wait for the machine-name lookup before publishing status
A status read answered in the first few milliseconds after launch published the bare
hostname because the friendly-name lookup had not landed yet, and a caption fetched in
that window never corrected itself. RuntimeMachineName now exposes the settled lookup
as a promise, and both status publishers (the status.get RPC and the desktop
runtime:getStatus IPC) await it before reading. Construction, listen, and every other
method stay unblocked; the worst case is one wait of at most a second on the first read.
* fix(cli): refuse to rename a runtime that does not publish a machine name
An older Orca runtime rejects the unknown settings field with a bare invalid_params, so
'orca host name --name' routed at one failed with no explanation. The runtime that does
not publish machineName on status cannot store one either, so the CLI reads status first
and refuses with incompatible_runtime and a message that says to update that host,
before writing anything.
* fix(ipc): introduce this desktop to remote hosts by its machine name
When this desktop connected to a remote workspace host it announced itself under a
hostname captured once at module load, so a renamed machine kept its old name on every
other device's connected-clients list. The client name is now read at send time from
the runtime's machine name (the configured override, else the detected one), passed in
where the remote workspace handlers are registered, so a rename reaches the next
presence frame without a relaunch.
* fix(runtime): keep the machine-name lookup under the status probe budget
Status publishers now wait for the one-time name lookup, and `orca status`
probes them with a one-second budget. scutil answers in milliseconds, so a
half-second cap keeps a stalled lookup from making a healthy runtime read as
"starting" while still preferring the friendly name.
* refactor(web): drop the unreachable machine-name write path
The Mobile settings section is desktop-only, so the paired web client can
never render the field. Forwarding the name through the web settings sync was
dead code, and against an older host the strict update contract would have
rejected it while the local mirror kept the value. Remove it until a web
surface exists.
* chore(i18n): translate the machine-name strings and document paired-server rows
Add the Machine name field and its Settings search entry to the five non-English
catalogs, explain in the host list spec why paired-server rows report an unknown
platform, and drop a stale timeout figure from a test comment.
* refactor(settings): make the machine name a machine-wide setting with a General home
The name other devices and hosts list this computer under is not a mobile
setting. Rename MobileMachineNameField to MachineNameField, give it a per-mount
id, and put its primary home in Settings > General under "This computer". The
Mobile pane keeps the same field. One shared search entry feeds General, the
Mobile pane, and the copy now says "other devices and hosts" in all six locales.
The web client has no machine of its own to name and its settings mirror cannot
persist one, so the field renders nothing there and General omits the section.
* feat(mobile): name this computer in the Orca Mobile pairing step
The "Pair this computer" step now shows the same machine name field above the
connection choice and code, so a user pairing a phone from the sidebar page can
name the computer right there.
* feat(settings): name this host when sharing it with other devices
Share this host produces the access link other devices use to reach this
machine, so it mounts the machine name field first. The pane's search entry
takes the shared machine-name keywords so a search lands there.
* feat(sidebar): name this desktop when adding a remote host
This desktop introduces itself to a new SSH host or remote server under its
machine name, so the Add Remote Host dialog mounts the field once, between the
header and the host fields, in both modes. Submit logic is unchanged.
* feat(settings): name this computer in the SSH pane add form
The SSH pane's add form mounts the machine name field above the host fields.
Editing a saved host leaves it out; that host already met this computer.
* fix(mobile): drop the empty machine-name grid row on the web client
The pairing step wrapped MachineNameField in its own grid-area div. On the
web client the field renders nothing, so the wrapper left an empty row and
an extra row gap between the copy and the connection options. The field now
takes a className for its root, so the grid slot disappears with it.
* fix(settings): let Enter in the machine name field submit its form like sibling inputs
The field intercepted Enter to blur and commit instead of submitting the enclosing
SSH add form. The draft is already flushed on blur and on unmount, and the name is
read from the store whenever a peer asks, so nothing is lost when the form submits
first. Enter now behaves like the neighbouring inputs; the test proves the submit
fires and the name still commits when the form closes.
* fix(mobile): keep the machine name inside the pairing copy cell
A dedicated grid row stayed in the template on the web client, where the field
renders nothing, adding an empty track and a second row gap between the copy and
the connection options. The field now sits at the end of the copy cell with the
same 18px rhythm, so an absent field leaves nothing behind.
* fix(runtime): retry a failed machine-name lookup instead of latching the hostname
On a loaded Mac the scutil lookup missed its 500 ms cap during app boot, and
because the fallback was memoized for the process, every status read and the
Settings caption showed the bare hostname for the rest of the session.
The lookup now gets a 5 s timeout, a failed attempt (timeout, spawn error,
non-zero exit, empty output) clears the shared memo so a later ready() retries
after a 30 s interval, and status publishers wait only up to a 750 ms publish
budget before answering with what read() has now. A friendly name and the
non-darwin hostname stay final.
* refactor(settings): show the machine name only where other devices join this computer
The Add Remote Host dialog, the SSH pane add form, and General all describe
another machine, so a field about this computer's own name read as a third
kind of label there. The field now mounts only where other devices pair with
or connect to this computer: the Mobile pane, the Orca Mobile pairing step,
and Remote Servers > Share this host.
|
||
|
|
35fe67b610 |
fix(perf): measure terminal latency with presented CI frames (#22096)
* fix(perf): present benchmark frames only on isolated CI display * fix(perf): wait for the benchmark page before presenting its window * docs(perf): record full scale pass with unchanged latency budgets * test(perf): document and verify the isolated display exception |
||
|
|
7da9788c83 |
fix(updater): send the gh token and cache the release picker's build list (#21902)
* fix(updater): send the gh token and cache the release picker's build list The dev build picker listed releases through api.github.com with no Authorization header, so it spent GitHub's 60/hour per-IP bucket that every unauthenticated caller on the same network shares, and it refetched on every settings mount and channel click. When that bucket ran dry the picker showed "No builds found" with a rate-limit line even though GitHub was healthy and the user's own token had its full quota. Attach the local `gh auth token` when there is one so the request draws from the user's 5000/hour bucket, fall back to unauthenticated on a rejected token or a spent token bucket, cache the list per channel for five minutes in the main process (the refresh button forces a reload), classify 403 by the rate-limit headers, and say when the limit resets. Fixes #21898 * fix(updater): don't trip breaker for secondary rate limits GitHub sends x-ratelimit-remaining: 0 on both primary and secondary limits. Secondary limits carry Retry-After and shouldn't block all core gh commands — only the primary limit should trip the shared breaker. * Scope gh rate limits to execution environment * Add build list cache hint to release channel settings Inform users that build lists are cached for 5 minutes and they can refresh to check for new builds immediately. This makes the cache behavior visible and explains why a manual refresh is necessary to bypass the cache. |
||
|
|
1ff4fe677c |
fix(main,preload): tear down renderer relay and preload listeners (#20909)
* Clean up renderer relay listeners on teardown * fix(main): guard empty markdown relay results * test: document relay window test double safety * fix(relay): retain web contents through window destruction --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
78609330d1 |
Fix browser viewport presets incorrectly scaled by UI zoom (#20962)
* Fix browser viewport presets scaled incorrectly by UI zoom Browser viewport presets must remain in window DIP (native) coordinates but scale in CSS pixels as UI zoom changes. Store preset dimensions as CSS variables in DIP, then divide by the live UI zoom factor in the stylesheet. Also consolidate zoom factor calculations across the app to use a shared `uiZoomFactorFromLevel()` function and add `windowDipToCssPx()` for converting native coordinates to CSS pixels. * Move viewport preset zoom compensation to CSS class Inline width/height styles outrank class rules, preventing the zoom compensation from applying. Using a class rule ensures the viewport scales correctly as the UI zoom factor changes. |
||
|
|
47bb473ec6 |
Remove agent map from dashboard popout (#20929)
The agent map view was not functional and its components have been removed entirely. The dashboard popout now only supports the kanban board view, with all map-related code, utilities, types, and translations cleaned up accordingly. |
||
|
|
6fe140ded8 |
Report clipboard and composer drop failures (#20795)
* refactor(renderer): give the IPC error reader a clamped and an unclamped shape * fix(composer): name the attachments a drop could not add, in one toast * fix(composer, source-control): use one stable failure toast slot - Replace per-worktree toast IDs with single slot that replaces on each failure - Remove destructive retry actions; discard must confirm in dialog - Consolidate filesystem import types to shared location - Add compactIpcErrorMessage for string error handling * refactor: centralize filesystem import types and clarify failure naming Move import result types from main/ipc to shared layer so they're available across preload and renderer. Rename uniformFailure → commonFailure and skippedOrFailed → failureCount for clarity. Simplify preload/API type definitions by reusing shared types directly instead of duplicating inlined union shapes. * Reuse single toast slot for composer drop failures Multiple drop failures now replace the previous toast instead of stacking, preventing notification clutter. Uses a dedicated toast ID separate from Source Control's stage/discard notifications. * fix(source-control): surface a failed notes copy instead of swallowing it * Simplify diff comment notes copy error message Replace parameterized translation template with a direct string. Add explicit type annotations in tests to improve type safety. * Sanitize clipboard write error messages for user display - Only user-friendly messages for recognized errors - Native failures logged but not exposed to UI - Prevents information disclosure (CWE-209) |
||
|
|
12f53da542 |
Remove settled-worker automatic resume and hibernation fences (#19544)
* Remove settled-worker automatic resume and hibernation fences * test: retirement rollback case follows the no-fence policy Case 4 seeded and asserted automaticResumeBlockedBy, which this branch deletes. A rolled-back settled worker is now an ordinary done record that wake clears as passive evidence, same as any finished agent pane. * chore(i18n): regenerate the runtime-required catalog for the contrast floor strings * test(orchestration): give the stopping-worker guard fixtures a Run |
||
|
|
1e693edee4 | fix(clipboard): route runtime-owned SSH image paste through the runtime (#17679) (#19352) | ||
|
|
06a607a1d7 |
feat(orchestration): make multi-agent workflows durable (#16904)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 225 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$21666 | $\color{#cf222e}{\Huge{\mathbf{−}}}$2820 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$18846 |
| Prod | 348 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$17107 | $\color{#cf222e}{\Huge{\mathbf{−}}}$4706 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$12401 |
<!-- /orca-pr-loc -->
## ELI5
Orca now treats orchestration like a durable control plane instead of inferring success from terminal keystrokes. Agents can tell whether a prompt was accepted or a turn started, replay an ambiguous request without sending twice, and recover coordinator mail after a crash. Completed workers can be inspected, released, or retained, and their panes no longer auto-resume as if the work were still running.
## What changed
- **Run receipts** from `run-create/use/current/show/list` are the row without routing plumbing (`home_database`, `coordinator_pane_key`) and without the duplicate `binding` object.
- **`terminal send` receipts are honest and idempotent.** `input_accepted` and `turn_started` are the only stages; `--wait-submit` observes without resending; `--retry-request <uuid>` replays the exact request against the same process incarnation. A transport timeout keeps the retry ID; only a different runtime answering strips it. Value-less or non-UUID `--retry-request` is rejected on the CLI and the SSH shim.
- **Mailbox delivery is committed before wakeup.** Pointer writes are staged in the DB before any PTY byte, replayed once after restart, and never emit a naked Enter. The watermark that parks concurrent deliveries is released with the DB reservation. Restart rescans pointer-pending and `dispatch:` mailboxes.
- **Lifecycle is a guarded transition graph** (`lifecycle-transition.ts`) with a table-driven test over every caller edge. Task reopen/overturn stays in the public contract. A PTY exit during `worker-stop` is the stop succeeding, not a failure.
- **Worker lifecycle CLI:** `worker-start` (`--spec` creates Task + attempt in one call), `worker-show`, `worker-read` (provider transcript first, bounded terminal fallback with a typed reason, local/WSL/SSH), `worker-stop`, `worker-abandon`, `worker-release`, `worker-retain`, `worker-list` (rowid-fenced pagination, fleet liveness, `attention`, literal `nextAction`).
- **Release is an explicit ownership table** (`decideWorkerTerminalRelease`): only an `owned` resource can be settled, the archive is mandatory where reachable, and an owner whose process is proven exited can always get out of `retained` via `archive_status: unavailable`. User-taken-over, external, and transferred panes stay retained.
- **Settled-worker resume fence** (folds in #17651): a settled dispatch whose pane is still open is fenced at settlement, on stop/abandon/exit, and at startup; lifted on release, retain, takeover, and pane reuse.
- **Liveness is `live` / `unverifiable` / `exited` only**, from execution-host evidence. Fleet projection reads the evidence clock, not the relay delivery clock. A host-certified exit outranks the worker's settled state. `unverifiable` never authorizes stop, abandon, retry, or release, in code or in the guide.
- **Federation:** structured reads negotiate by `method_not_found` so every shipped host keeps transcript-first output; exited remote workers are closed before being reported closed; epoch fencing holds across peer restart, downgrade, and pairing rotation; no per-second forced capability probe.
- **Schema v35:** repairs databases stamped v34 by the pre-fix branch (mailbox_handle default, index predicates), drops the write-only `lifecycle_transition_receipts` ledger and five never-read v31 identity columns.
- **Schema v36:** `dispatch:<id>` mailboxes get a real consumer generation on `dispatch_contexts` and `remote_dispatch_attachments`, bumped and fenced in the same transaction on every re-attach (manual inject, worker-start, federated attach). A stale worker whose Dispatch moved to another process now gets `consumer_fenced` instead of silently acking the new worker's Delivery. Run mailboxes already worked this way.
- **Schema v37:** `dispatch_contexts` records its creator (`creator_handle`, `creator_pane_key`), so a coordinator's context-only self-dispatch is bookkeeping rather than a nesting parent; before this, one self-dispatch made every later `worker-start` from that coordinator fail the depth cap. Pre-v37 rows keep counting (fails closed).
- **Dispatch-mailbox ownership is checked, not inferred.** A `check` from a process whose pane no longer holds the Dispatch, or whose last Attempt was abandoned/failed and moved to another terminal, gets `consumer_fenced` instead of an empty inbox that reads as "no mail yet". `--peek`/`--all` stay readable. A paneless caller still gets `stable_pane_required` with the rebind recovery.
- **Liveness certification is stricter:** a `process_exited` stage whose termination reason is `unknown` (a stop that was issued but never observed) projects `unverifiable`, not `exited`. Federated `worker-show` carries the execution host's verdict and host kind instead of a local guess. A live, ready worker with nothing pending has `nextAction: none` rather than pointing at the `worker-show` that produced it.
- **Wire:** `workerShow` keeps `dispatch.task_id` next to `taskId` for shipped CLIs. `ask --json` uses the standard `{ok, result}` envelope like every sibling verb.
- **Migration start-version detection** treats the two v32 recovery columns as versioned. Before this, every shipped database stamped below 32 resolved to the v6 floor and replayed the whole chain (the v23 backfill synthesized 68 phantom retained workers on a real v30 profile). Verified on a copy of a real 62 MB v30 profile: starts at 30, no row delta, integrity ok, 11 ms.
- **Skill guide** rewritten as a ≤200-line kernel plus seven references, to the outcome-first standard (Result / Done / Safe failure first, conditions not case lists, one done bar, references loaded at the point of use). The canonical loop uses `worker-start --spec`, names `worker-list` for completion accounting, documents `--retry-request` / `request-show` / `--wait-submit`, and requires positive evidence before any stall action. The other seven guides get the same treatment in #18724, split out so this PR stays orchestration-only.
- **`rpc/methods/orchestration-*`** (126 flat files) regrouped into `orchestration/{worker,federation,messaging,runs,gates}/`.
## Why
User reports showed the same boundary failures: false `agent_prompt_stalled` causing duplicate sends (#15180), coordinators unable to trust screen scrapes, cold-parked terminals receiving a pointer without the submit, settled workers accumulating as live tabs and auto-resuming after restart, and no way to tell a stalled worker from a working one.
## Linked issues
Fixes #15180. Fixes #17935 (orchestration skill description is 866 characters; a guard now caps every bundled skill at 1,024). Supersedes #17651 (fence folded in). Advances #16660, #16522, #14907, #13047.
## Review record
This PR was reviewed adversarially after revival: eight independent lenses (lifecycle, mailbox, send, worker, federation, transcript, complexity, live ergonomics), each required to prove findings with a failing test. That produced 16 proven blockers, all fixed with red-then-green regression tests, followed by two re-review rounds and a third fix wave that caught 3 regressions introduced by the fixes and 7 fixes that missed their target; all closed. A final pass (five lenses incl. a live built-runtime smoke, then a re-review of the fix wave) found and fixed seven more, chiefly the stale-worker mailbox steal, the self-dispatch depth wedge, and the unproven-exit certification. Three independent Codex (gpt-6-astra) passes followed: the first found nothing new, the second found and fixed 3 defects (task-status reachability, WSL-local host classification, peer-capability epoch), the third found and fixed 6 (production PTY controller never installed settled writes, ambiguous in-flight pointer failures allowed duplicate replay, SSH/relay deadlines cut off a valid `--wait-submit`, stop-vs-exit race during inspection, and two release-recovery paths for vanished or exited terminals). The full record (findings, proof tests, triage, declines with reasons) is archived outside the repo.
**Rework after the live smoke.** A first live cross-host run on the shipped adhoc build (this Mac, a paired Windows host on the same build, a paired Mac on 1.4.195, and an SSH host) found a P1: a running local worker read `unverifiable`/`missing_status` because the fleet snapshot rows lacked the terminal handle the matcher keyed on. A 59-row failure table over every bug fixed during review showed the same two classes recurring: a fact dropped in transit through optional fields, and two authorities for one fact. Two blind designs (Opus, Codex) converged on the same mechanisms, and the scoped tranches landed here with red-then-green seam tests from the real producer to the real consumer, faults injected only at the transport or hook-ingest boundary:
- **Settlement (data-loss class):** one three-valued `WriteSettlement` (`accepted | refused{reason} | unverifiable{reason, bytesHandedToTransport}`) from the SSH multiplexer through daemon client, providers, controller, to pointer staging. No boolean, no rejection-as-third-state. The two silent degrades that fabricated a handoff are deleted; a provider that cannot settle refuses before any effect. Pointer text and Enter share the contract; a partial flush is `unverifiable`, never `refused`.
- **Evidence identity (false-liveness class):** fleet agent-status evidence is a tagged union (`binding: worker | pane | unresolved{reason}`, `clock: observed | delivery`) minted once at ingest, so a hook row captured on one process incarnation can never bind to a later dispatch on the same pane. The matcher's `!worker.paneKey ||` defaults are gone. One host-scope parser replaces two.
- **Small pre-merge items:** `capability_unsupported` from an old peer is no longer relabelled `host_unavailable`; a producer census test asserts every agent-status consumer path projects a pane-only hook row as `live`.
Two ergonomics defects the second live run surfaced on a real database are fixed here too: a pre-v3 dispatch already marked `completed` projected as `outcome_unknown` / `requiresAction: true` forever (three copies of the outcome ladder disagreed on legacy rows; now one resolver, legacy `completed` reads `succeeded` with nothing to act on, legacy `failed` stays actionable on the failure), and an unscoped `worker-list` enumerated the entire database (now defaults to the Run bound to the calling terminal, `--run` overrides, and the receipt's additive `scope` field says which).
A third live round on the shipped adhoc build of `b082443e1f` (same four hosts) plus an unscripted run in the user's own prompt style (a plain Claude Code shell, `/orchestration`, three workers, zero errors, bound-Run default confirmed) found two more branch defects, fixed with red-then-green tests: a worker freshly started on a paired server projected `unverifiable`/`host_indeterminate` with `requiresAction` for ~3 minutes, including after its own `worker_done`, because the host's federation observation returned `missing_liveness_verdict` for any PTY the liveness register had not yet swept (the host now reads a connected pane it owns locally as `live`; disconnected or SSH-scoped panes stay `unverifiable`); and six pre-v3 completed rows still carried an `input` category because settling through the task-status path or `failDispatch` never closed the Dispatch's pending question threads (both paths close them now, and schema v38 closes threads already pending on settled rows). The guide's `worker-start` examples now show `--model sonnet`, since an omitted model inherits the launcher's default.
A Codex adversarial pass on the tranche diff found one real design hole (identity minted at read time instead of ingest, now closed) and two daemon settlement paths that threw instead of settling (fixed). Two `@ts-nocheck` runtime mixins on these paths were extracted into checked modules; the repo-wide `@ts-nocheck` count is unchanged at 171.
Deletions during review: ~1,900 lines (write-only ledger, unread columns, dead v1 archive path, test harnesses shipped in prod, duplicated liveness and state-machine copies, self-capability checks that were compile-time true).
## Testing
- `pnpm typecheck:tsc:node|cli|web` clean
- `pnpm run check:code-quality:changed` 0 findings; `check:react-doctor:changed` 0
- `pnpm verify:bundled-skill-guides`, `verify:skill-bundle-manifest`
- full `pnpm test` on the integrated head: 72,332 pass / 292 skipped; the only failures were three non-PR files (two zsh live-shell suites hit a node-pty spawn-helper ENOENT while a concurrent native rebuild ran, 44/44 in isolation; `release-checkout.unit.test.ts` is a known 30 s load timeout that passes in isolation on `origin/main` too).
- CI on
|
||
|
|
681119dc05 |
test: isolate window mocks from inherited launch flags (#18989)
* test: isolate mocked window activation from inherited launch flags * Preserve background window regressions added on main |
||
|
|
a730becd7a | fix(automation): keep explicit background launches off screen (#18898) | ||
|
|
58553bfe1c | fix(recovery): fail a renderer recovery reload that never loads, instead of leaving a dead window (#18466) | ||
|
|
436ef827dd |
fix(browser): present Electron's own user agent so Cloudflare Turnstile clears (#18749)
Orca rewrote every browser session's UA to look like plain Chrome by stripping the Electron and app tokens. That rewrite is what Cloudflare rejects: a Chrome UA that ships no client hints reads as a spoof and Turnstile returns 600010, while the same binary on the same IP clears every challenge with its stock UA. PR #885 added the rewrite to fix 600010 and was treating a symptom it created; issue #11518 later found the same rewrite is what broke Google sign-in. - Keep the stock Electron UA on every partition. The webRequest handler now only owns the host-scoped Google auth Firefox switch, which stays unchanged. - Delete the anti-detection script. Measured on Electron 43: plugins are already a real PluginArray, window.chrome exists, and navigator.webdriver is false even with the debugger attached, so three of its four premises were wrong, and the overrides it installed (instance-level webdriver, non-native Permissions.query, stubbed chrome.csi/loadTimes) are themselves published bot signatures. - Stop attaching a CDP debugger to every browsing guest. Only the auth-UA detach listener remains, because a detach clears Chromium's standing UA override. - Stop sending Runtime.enable into cross-origin iframes when the agent bridge auto-attaches. The challenge widget is one, nothing reads iframe Runtime events, and the Runtime domain's serialization side effect is the documented Cloudflare CDP tell. - Add a real-Electron test proving the wire identity: stock UA to ordinary hosts, Firefox with no client hints to accounts.google.com. Verified in the dev build: dash.cloudflare.com/login no longer shows "There was a problem with verification" and scrapingcourse.com's managed challenge clears, both failing deterministically before. Fixes #13822 |