* fix(terminal): send only the text a Linux IME commits, not the candidate key
fcitx4 with Sogou reports a single-character candidate pick as a plain
Space/digit keydown whose keypress and insertText carry the candidate.
xterm encoded the keydown instead: under kitty keyboard mode the Space
leaked and the candidate was lost, and the old Linux digit window
cancelled the keydown that carried the commit.
Install the native text forwarder on desktop Linux (terminal pane and
dashboard preview) so printable bytes come only from committed text, and
delete the Linux timer heuristics it replaces. Pair an XIM pick's keyup
with its code-less press, and stop dropping unclaimed input while an
xterm composition transaction settles.
Replay tests use real fcitx4 recordings (Sogou via GTK module and XIM,
Google Pinyin in-client preedit).
Fixes#28213Fixes#26352
* test(terminal): replay Chromium's text insertion for uncancelled keys in the Hangul sequence
The observed IBus Hangul replay dispatched a bare `a` keydown with no text insertion,
which only matched reality while xterm cancelled that keydown. Now that printable
Linux text comes from the committed insertText, supply it when the keydown is not
cancelled and open the next session at the caret that follows it.
Point the IME reliability gate at the fcitx4 replay test that replaced the deleted
Linux candidate-state test.
* feat(wsl): open WSL host paths from Windows through one owner-routed capability (WSL-via-orcad C8)
* test(wsl): typed shell handler lookup for the WSL owner tests
* fix(wsl): read known WSL hosts through a registered accessor; e2e for the Windows folder picker
* fix(wsl): open the host-path entry filled in after a Windows pick; e2e screenshots
* test(wsl): read the e2e picker's default path through the main process env
* refactor(wsl): read a WSL host's distro from its record instead of a new accessor
* fix(wsl): skip the host browser only for a path Windows' picker or an Explorer drop filled in
A path left filled from an earlier pick on an SSH or paired host opens the host browser again, as it did before.
* Show folder workspaces in the Cmd+J jump palette
Cmd+J only listed git worktrees, so folder workspaces could not be found or opened from it. The palette now includes folder workspaces alongside git worktrees and renames the section from "Worktrees" to "Workspaces" to match.
- Folder rows are searchable by name, comment, and attached GitHub item, and follow the sidebar's host scope.
- A blocked activation (for example, a folder path that no longer exists) keeps the palette open and shows only the path error, instead of closing or showing a second generic error.
- Folder path-status caching and sidebar reveal are keyed by execution host, so folders with the same id on different hosts no longer share cache entries or reveal the wrong group.
* Consolidate runtime target imports in folder-path-status
`getRuntimeTargetHostId` now comes from the same module as `runtimeTargetForOwnerEnvironment`, so both are imported from `runtime/runtime-client-target` instead of splitting them across two modules.
* Wake sleeping folder workspaces from Cmd+J
Picking a sleeping folder workspace from the jump palette did not restart its terminal or restore its Files explorer view. Folder and git worktree activation now share one code path in workspace-activation.ts, so both wake the same way.
- Wake a sleeping folder the same way as a worktree: bump the tab generation and tag the pending spawn when no PTY is live, including when the workspace is already selected.
- Restore the remembered right-sidebar explorer view for folders.
- Skip the GitHub PR refresh for folder workspaces, which have no PR.
- Clear unread only on the folder entry whose execution host matches the activation.
* Reveal folder workspaces on their catalog owner host
Cmd+J activation revealed folder workspaces against the focused runtime
server's host, so a folder owned by a local or SSH project could be
revealed under an unrelated server. The reveal host now comes from the
folder workspace's catalog owner, and an explicit caller host still wins.
Add a regression test covering local and SSH owners while another
runtime environment is focused.
* Keep shared workspace activation in the existing layout writer
* feat(wsl): Add WSL distro host UI, host row status and fake-transport e2e (WSL-via-orcad C7b)
Experimental toggle (Windows only), an Add WSL distro choice beside SSH host and remote server,
the WSL host's own row in host pickers with its liveness state, a Connect action that starts a
stopped distro only on request, a WSL hosts list in Settings, and the renderer report of which
WSL hosts an open project or terminal needs.
* feat(wsl): show WSL hosts as turned off while the experiment is off, and confirm their removal
With Experimental → WSL distros as hosts off, a WSL host keeps its row so its projects never
seem to vanish, says it is turned off, and offers Turn on (opening that setting) instead of
Connect; Settings lists it the same way with Connect disabled. Removing a WSL host now asks
first in the dialog SSH hosts use, with WSL copy. The e2e covers both.
* fix(wsl): skip the WSL host status bridge when the preload lacks it
* fix(wsl): show when this build cannot run WSL hosts instead of failing an add
The UI reads main's template capability once. Without it, the add dialog says this build can't
run WSL hosts and disables Add, existing hosts' rows and Settings say the same with Connect
disabled, and the Experimental toggle notes it. The e2e covers a fake build without the files.
* fix(wsl): re-read the WSL distro list when the add dialog's window returns, and on Refresh
The add dialog read the registered distros once, so one imported while it was open never
appeared. It now re-reads when the window regains focus or becomes visible, and from an explicit
Refresh button, never on a timer. The selection is derived from the current list, so a refresh
cannot leave a stale pick. The e2e imports a distro mid-dialog.
* fix(wsl): list WSL hosts only as WSL hosts, not among the generic remote servers
Settings → Remote Orca Servers, the server-update checks and the status bar's server list now
leave out environments whose deployment record is a WSL host (by kind, never by name). The WSL
hosts section reads them from the store itself.
* fix(wsl): say a WSL host's removal ends its terminals, and report what it left running
The confirmation now says removing ends the host's open terminals as well as Orca's server, and
a removal main could not complete in the distro (a terminal or the daemon kept running) is
reported instead of passing silently.
* fix(wsl): ship the removal copy that says a WSL host's terminals end
The build resolves copy from the English catalog, which still held the old text under the
reused key, so the new inline default never reached the dialog. The copy gets its own key, the
stale entry is dropped from both catalogs, and a test reads the shipped catalog.
* fix(wsl): leave a removed host's workspace, keep its removal warning up, and prove removal in e2e
After removal the active workspace is left when it lived on that host, so nothing keeps calling a
host that no longer exists, and a warning about what still runs in the distro stays until
dismissed. The e2e now checks that removal ends the host's terminals on the server standing in
for orcad, shows no warning, leaves the workspace, and starts nothing for the host afterwards.
* test(wsl): read the shipped removal copy and the fake's connect log without reflection
* fix(wsl): say a WSL host is turned off in its terminal instead of 'Reconnecting'
With the experiment off (or without server files) nothing retries, so a WSL host's terminal now
says so, with Turn on, rather than claiming Orca is reconnecting.
* test(wsl): reproduce project-removal survivors and orphan terminals in e2e
The headless server standing in for orcad now carries a project whose terminal must end when the
project is removed (and leave the host's own catalog), and an orphan terminal whose project the
host already forgot, which removing the host must still end. Both checks fail without the fixes.
* fix(wsl): drop the IPC wrapper from file-explorer and editor load errors; offer Turn on when WSL hosts are off
The file explorer showed "Error invoking remote method 'runtimeEnvironments:call': Error: ..." for a
WSL workspace whose host was turned off. Explorer read errors now go through the shared IPC-error
unwrapping, and when the workspace's WSL host is turned off the empty pane shows the same turned-off
copy and Turn on button as the terminal banner instead of a read error and a Retry that cannot work.
The editor's file-load error view drops the same wrapper at render time (stored text is unchanged
for retry classifiers).
* test(wsl): open the internal gate for the WSL host e2e, and prove a closed gate shows nothing
The fake-transport e2e launches its desktop with ORCA_INTERNAL_WSL_HOSTS=1.
A new case leaves the gate closed with a WSL host already saved and the
Experimental setting on: no WSL host API, list entry, host row, Add WSL
distro action, Experimental toggle or Settings section appears, the fake
never starts a distro, and the saved host stays on disk.
* test(wsl): count only live shells when proving a removed project's terminals ended
On Linux CI the hub keeps a pty-less tab record for the removed worktree after terminal.stop ended its shell, so counting every terminal.list row never reached zero.
* test(e2e): read the parked-reveal decoy grid only after it settles at the new fit
* test(e2e): name the settle wait and give it the file's reveal timeout
* fix(floating-workspace): reveal workspaces where they live instead of selecting the floating one
Every renderer path that takes the user to a workspace hand-wrote the main-window
selection sequence, so a floating workspace id (terminal switch, CLI focus, agent
launch, notification and mobile reveals, terminal handle links) became the main
window's selection and blanked it while the floating panel stayed closed.
revealWorkspaceSurface is now the one way to show a workspace: the floating
workspace opens its panel and leaves the main window alone, any other workspace
becomes the main window's selection and a visit. selectWorkspaceSurfaceOwner is
its non-navigating twin for tabs opening beside the user. The ad-hoc floating
checks in worktree activation, http links, the editor close queue, Activity and
open-terminal-beside-tab now go through the same module.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(notifications): bind clicks for floating and folder workspace terminals
A terminal notification only bound its click when the worktree id carried a
repo ("repoId::path"), so floating-workspace and folder-workspace terminal
notifications did nothing when clicked. ui:focusTerminal now reveals any
workspace where it lives, so the click binds whenever there is a pane to focus
and ui:activateWorktree is sent only when a repo exists.
Co-authored-by: YuriNachos <19365375+YuriNachos@users.noreply.github.com>
* refactor(floating-workspace): tighten the workspace surface reveal
Move the floating-owned check into floating-workspace-panel-reveal as
revealFloatingWorkspacePanelFor so worktree activation no longer depends on
workspace-surface-reveal, rename selectWorkspaceSurfaceOwner to
selectWorkspaceSurface with a named state type, reuse the panel visibility
selector, and flatten the IPC bridges' surface-owner branches.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(floating-workspace): leave the floating setting alone before settings hydrate
Host reveals (CLI focus, mobile, notification clicks) can arrive before
settings load; re-enabling the floating feature then would overwrite a
saved `false`. Unhydrated settings now skip the reveal.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* refactor(floating-workspace): drop extra reveal concepts
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(floating-workspace): keep main's setActiveWorktree call shape for plain reveals
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
---------
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
Co-authored-by: YuriNachos <19365375+YuriNachos@users.noreply.github.com>
* fix(ssh): migrate released profiles with retained client state
* fix(ssh): reuse the shared adoption rule when the local and host copies overlap
The host copy replaces the older local one only where it holds tabs, the
catalog places the workspace on that host, no other host claims it, no
worker fence or close record disagrees, and local drafts are kept. Elsewhere
both copies export and must agree, as on main.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(terminal): show a host-created setup split in the window that holds its tab (STA-9417)
Main only revealed a split of a saved tab when its copy of the window's graph
listed the source pane. That copy lags the tab the create reveal just added, so
the window never drew the setup split and its first-activation sweep left the
setup terminal with no pane or minted a second tab for it. Reveal unless the
window's graph shows the source pane running a different terminal, and reject
a split into an unknown tab before the window switches workspace.
* refactor(terminal): return only the split-source fields a caller reads
* test(terminal): explain the internals cast in the projected split-source test
* fix(terminal): keep a host-made terminal's title across a serve restart
A title given to terminal.create was kept only on the live process record, so
after an orca serve restart the tab read "Terminal N" and terminal.list
listed no title. The runtime now writes it as the tab's saved name when it
admits the pane, and terminal.list falls back to that saved name for a
terminal that has no title of its own yet, as a window does.
* test(e2e): record the kept terminal titles in the layout-compat baselines
* fix(terminal): save a created terminal's name only when the window would show it
A background create (a worker the window never reveals) keeps its live title:
a window reads a saved name on hydration and shows it over the agent's own
title. Also corrects two comments and drops a key count.
* fix(runtime): stop a project's terminals before the host forgets the project
Removing a project on a runtime host (an SSH-managed or paired server) sent repo.rm first; the
host resolves terminal.stop through the project's workspace rows, which repo.rm deletes, so the
stop failed and the shells kept running on the host. Terminals stop first now. The removal toast
also drops the IPC wrapper from its message.
* fix(runtime): only stop host terminals when the host will accept the project removal
Probe with repo.show (same resolver as repo.rm) so a removal the host rejects, such as an
ambiguous project id, no longer kills the shells first. Also poll through terminal_not_writable
in the paired-client e2e.
* test(e2e): read the host shell pid through sh so non-POSIX login shells work
* fix(terminal): keep the tab when a reattach finds its process gone
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): read the paired retention heap after two collections
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): clear the absent owner through the replaced-pane binding writer
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): fail the paired retention check when the heap cannot be read
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): refuse the respawn without a Store; tighten the binding fixture and stop docs
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(layout): save terminal rows in the tab groups' order
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): read the paired retention heap after two collections
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): fail the paired retention check when the heap cannot be read
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(layout): name the headless flow behind the row-id group fallback
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: upgrade terminal dependencies to the matching payload betas
* test: harden xterm payload contract, align mobile pins, fix serialize-at-ref across version bumps
- Cover foreign-payload replacement and the shared blank-cell reset in the
installed image payload contract (both mutants previously survived).
- Bump mobile's xterm, webgl and unicode11 pins to the root's betas.
- Resolve the ref's own serialize version and patch so the differential
fuzz baseline builds across a version bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step.
* ci: ratchet synchronous child-process launches in main and shared code
The Electron main process freezes every window while a synchronous child
process runs, and on Windows each launch is slow because antivirus scans
it. Nothing stopped new synchronous launches from landing in main-process
code, and the spawn observer behind ORCA_MAIN_THREAD_DIAGNOSTICS only saw
async spawns, so the worst blocks were invisible to it.
Ratchet: check:main-sync-spawn-ratchet parses production src/main and
src/shared files (test-only paths excluded) and fails when a file not in
config/main-sync-spawn-baseline.txt reaches execFileSync, spawnSync or
execSync from child_process, runProcessSync from @orca/process-host, or
one of the synchronous secure-file writers (they run icacls on Windows).
Named, aliased, namespace, default, require() and import() forms count.
A baseline entry whose file no longer launches synchronously fails as
stale, so the list can only shrink. It runs in lint and in the PR
static-analysis job next to the process-host import check.
Observer: runProcessSync now reports to the spawn observer, timing the
whole call (that is how long the thread was stuck) and reporting from a
finally so failing, throwing and timed-out children are counted too.
Runtime behavior is unchanged except that the diagnostics observer now
sees synchronous launches; with no observer registered the sync path
pays one extra branch.
* ci: count sync launches per file and gate the repo's sync wrappers
The sync-spawn ratchet only tracked which files launched synchronously, so a
file already on the list (wsl.ts, win32-utils.ts, git-exec-file.ts) could add
more launches unnoticed, and new callers of the repo's own synchronous
wrappers passed CI.
The baseline now stores a per-file count of uses, in the same format as the
focus-read ratchet: a rise or a new file fails, a lower or zero count fails
until `pnpm check:main-sync-spawn-ratchet --prune` lowers it, and --prune
never raises an entry. Every value reference to a gated function counts
(call, hand-off, alias and its uses, re-export); imports alone count nothing.
Gated wrappers: gitExecFileSync (and the git/runner re-export),
restrictWindowsPathSync, hardenSecurePath when called with sync: true,
grantDirAcl, getWslHome, listWslDistros, wslUncDirectoryExists,
isWslAvailable (and the wsl.ts re-export), isPwshAvailable. Their 18
current caller files join the baseline.
Failure messages now give advice per family: the async process-host API for
child-process launches, the async twin for wrappers, and for the secure
writers a note that no async secure writer exists yet, so the sync one stays
and must never become a plain write (that drops the Windows ACL hardening).
Also refresh two stale blockMs descriptions and drop a no-op line from the
spawn-observer "stays silent" test.
* ci: count hardenSecurePath unless visibly async, and tighten ratchet tests
hardenSecurePath was counted only when `{ sync: true }` was written inline,
so moving the options into a variable or a spread dropped the count and the
check then asked for a prune. It now counts every use except a call with no
options or a spread-free object literal whose `sync` is absent or false.
Also: resolve backslash paths in the importing file as well as the
specifier; still visit heritage and instantiation expressions
(`extends mixin(fn)`, `fn<T>`); state the reference-only counting limit;
and point the secure-writer advice at raising this file's baseline count.
Tests cover the hardenSecurePath shapes, the alias chains that need the
fixpoint loop, and a prune that must not raise a grown row.
* test(ci): pin the main sync-spawn ratchet background step
* fix: count secure hardening calls with spread arguments
* perf(notifications): save mobile dismissal history off the main thread
Every agent notification recorded its id in mobile-notification-dismissals.json
through the secure-file writer. On Windows that writer runs about five
synchronous icacls launches per write, so each notification froze every Orca
window (STA-9692).
The file holds notification ids, sequence numbers and session ids, not
credentials, so it now goes through the existing AtomicSnapshotWriter: async
temp-plus-rename, mode 0600, bursts coalesced into one write of the latest
entries. Memory stays authoritative, so delivery and retirement are unchanged.
The pending write is flushed in the quit barrier after chat and RPC teardown,
and in the pre-relaunch hook, since renderer relaunch exits without the quit
pipeline.
* perf(notifications): flush dismissal history when a headless host shuts down
orcad never runs the desktop quit barrier, so its cleanup list awaits the
pending dismissal write after RPC stops.
* fix(persistence): drain a snapshot write requested just after a drain ends
AtomicSnapshotWriter cleared its pending-write promise one microtask after
the drain loop saw no more requests. A write() in that gap set
writeRequested, got the settling promise back, and nothing drained it, so
the newest bytes stayed unwritten and waitForPendingWrite() (the dismissal
store's flush) resolved without them. The drain now clears the pending
promise in the same tick its loop exits, on success and failure, so a later
write() always starts a new drain.
* test(notifications): flush dismissal writes before deleting temp state dirs
Dismissal history now saves asynchronously, and the writer recreates its
directory before each write. Tests that deleted their temp dir with a write
still pending left recreated dirs behind in the system temp folder and could
race the delete on Windows. They now await the controller's flush first.
* perf(notifications): close dismissal history at committed exits
The shutdown flush only waited for writes already queued. A notification
recorded after it (orcad cleanups still running, or the 150 ms before a
renderer relaunch calls app.exit) started an async write the exit dropped,
so the phone could keep an alert the desktop could no longer withdraw.
The store's flush() becomes close(): it waits for the pending write, and
every later record saves synchronously through the snapshot writer's
existing writeSync (plain fs, no icacls; a failure only warns). It is called
only where the exit can no longer be vetoed: the will-quit teardown, a new
exit-only hook that app:relaunch runs before app.exit(0) (restart and
profile switches share onBeforeRelaunch but quit through will-quit), and
orcad's cleanup after RPC stops.
The quit test now holds chat teardown and RPC stop and proves the close
starts only after both settle, in either order. The orcad push-startup
runtime mock gains the method the entry now calls.
* fix(notifications): bound the dismissal close before a renderer relaunch
app:relaunch waited on the dismissal store's close with no limit, so a
stalled userData disk left Restart stuck on "Restarting…" with a half-quit
app saving synchronously. The close now races a 2 s cap through the
existing settleWithinMs helper, the same cap the restart path's auth
preservation uses, and logs when it gives up.
* test(notifications): keep async dismissal writes under test between reads
Mid-test disk reads used closeDismissals() as a barrier, which switched
every later record to the exit-only synchronous path. The store gains a
non-closing flush() (close() = mark closed + flush), exposed on the
controller as flushDismissals for tests; mid-test barriers use it, and
close stays only where a test simulates an exit or teardown. Also notes why
the writer's pendingWrite assignment is safe, renames a store test to what
it covers, and groups the orcad cleanup-order test under a describe.
* fix(ssh): never start a hidden orcad shell for a pane the legacy relay bridge serves
* fix(ssh): a skipped bridged activation still supersedes an earlier tab's pending focus
* fix(ssh): a click on a whole mirrored tab never activates its bridged pane on orcad
* fix(ssh): load the bridge pane list lazily from tab activation
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(ssh): a bridged pane reattaches after a disconnect instead of going deaf
* fix(ssh): a bridged pane asks again when it turns live during an earlier ask
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* wip: legacy relay bridge client
* Revert "fix(ssh): end an older Orca's relay terminals from the connect refusal (STA-10166) (#27748)"
This reverts commit d4a55f583e.
* feat(ssh): bridge terminals an older relay still runs into their old tabs
* test(ssh): cover the legacy relay bridge client, target bridge, transport and gate
* test(ssh): type the bridge test fakes without assertions
* test(e2e): a 1.4.224 relay's live terminal survives the upgrade on a Docker SSH host
* ci(e2e): route legacy relay bridge changes to its Docker spec
* fix(ssh): bridge delta moves too, and never read a non-exhaustive relay listing as exit
* fix(ssh): keep retrying a bridge for panes that mount before connect; end panes proven gone
* fix(ssh): bridge 1.4.224's detached leases through export preflight; publish every leased pane before the first look
* test(e2e): build the released relay out of process so Playwright never loads ESM build tooling
* fix(renderer): drop the recovery hook main removed from PtyTransport
* test(e2e): show the converted worktree before reading the bridged tab
* test(e2e): reconnect and wait for the converted host before opening the bridged tab
* fix(renderer): match bridged leases to tabs mirrored from the managed server
* test(e2e): find the mirrored relay-era tab under any worktree key
* test(e2e): open the converted worktree before looking for its mirrored tab
* test(e2e): a retired lease is terminated or pruned
* fix(ssh): back off attaches a listed terminal refuses; one pane per bridged shell
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Move the 52 non-runtime, non-ipc importers of src/main/providers/types.ts
onto the per-domain contract files. The barrel itself stays until the
runtime and ipc importers move (3.4b/3.4c).
Co-authored-by: m4air <m4air@Mac.localdomain>
* Add host-owned agent launch settings mutations
* Edit agent launch defaults on the selected execution host
* Await durable writes for host agent launch settings
* Fence agent detection by execution-host pairing
* Acknowledge host settings before hook reconciliation
* Preserve Reset after pending agent launch saves
* Simplify agent detection re-pair handling to the clear and the transport fence
A re-pair already clears every detected-agent list, flag and pending probe for
the environment, so the per-record pairing owner keys, the owner-keyed pending
probes, the settle-time pairing checks and the hook's ownership gating were
never reached. Detection now captures the pairing before the capability wait
and fences its RPCs with it; the refresh fallback still refuses after a
re-pair. Pins the fence across a capability wait, the needs-update clear, and
the agent-launch RPC permission levels.
* Open launch settings on the Active Server without the focus-routing helper
Main's new owner-routing ratchet counts getActiveRuntimeTarget calls, and this
pane's no-workspace default added one. It now reads the Active Server field the
way the Agents pane did before host selection, so the default is unchanged.
* Re-probe mounted launch surfaces after a re-pair; rely on the transport fence for the refresh fallback
A re-pair clears an environment's detected-agent lists, but a mounted tab bar,
quick launch or composer that was still waiting for its first list (or whose
first probe had failed) never asked again, leaving "Can't reach this
workspace's host" until another surface mounted. The hook now re-runs its
effect when the target environment's pairing revision changes.
The refresh fallback no longer re-checks the pairing in the renderer: it
already carries the pairing captured before the wait, and main refuses any
mismatch. Pins the Settings host form's remount key on re-pair.
* Share immutable historical source across local cross-version test caches
* Add preview-first cleanup of old historical test caches
* Include historical test cache cleanup in worktree space maintenance
* Report and retain read-only Electron installs during cleanup
* Protect read-only installs under root and isolate cleanup CLI fixtures
* fix(bench): resolve dynamically imported sources as file URLs
`await import(path.join(ROOT, '...ts'))` hands Node a bare Windows path
(`C:\...`), which the ESM loader rejects as an unsupported URL scheme
("Received protocol 'c:'"). The two benchmarks that load production
modules this way crashed on Windows before running a single scenario,
while working on macOS and Linux where an absolute path is a valid
specifier.
Wrap each computed specifier in `pathToFileURL(...).href`, the form the
other benchmark scripts already use for the same purpose.
Verified on Windows: bench:session-write-hot-path and
bench:agent-inspection-cadence now run and print their measurement
tables. The fish mode-2031 trace capture resolves its import too, but its
harness targets macOS/Linux shells and cannot complete on Windows.
Fixes#25235
* style(bench): format the session-write benchmark's file-URL import
---------
Co-authored-by: Paolo <paovalletta@hotmail.it>
Co-authored-by: Orca Worker <orca-worker@localhost>
* test(e2e): record mixed-version, two-client and phone layout baselines on main (P3)
* test(e2e): P3 review fixes: reconnect and I/O proof, restart order bounds, Linux-only baselines
* test(e2e): re-record Linux phone and serve-restart baselines
* test(e2e): P3: echo drawn in the desktop pane; bound the group selection after restart
* test(e2e): re-record serve-restart group selection
* test(e2e): name P3 baseline helpers for what they record
The layout oracle checks the saved profile's round trip at every settled step and the live layout's model rules on every published frame, which covers what this 1 s settle logger did.
* feat(web): the browser signs in with a non-extractable device key, or pairs for this session only
- On a secure page with WebCrypto Ed25519, the browser registers a non-extractable key
(stored in IndexedDB, create-exclusive across tabs) over a v2 token socket, then
reconnects with key auth and replaces its stored pairing code with a placeholder.
Requests on a keyed socket carry no token.
- On a plain-http page on another machine, or without Ed25519, existing stored pairings
keep working unchanged (with a warning); new pairings stay in memory and, when the host
supports it, register a session-only key.
- Only a sealed device_revoked naming this browser's key deletes it; a downgraded host,
a bad proof or host storage trouble keep it.
- The shared handshake core gains an async device-key credential (v2 only).
* fix(web): mark web sockets, bind keys to their pairing, survive a broken key store
- Every web socket carries ?orca-client=web so hosts gate only the web bundle.
- A key record belongs to one pairing revision: a re-pair uses its fresh code.
- A code that never migrated connects without reading IndexedDB; a migrated
pairing whose key store fails retries instead of going dead.
- A tab that finds the key already active forgets its code, and saving a stale
copy of the same pairing never puts the code back over a placeholder.
* fix(web): a stale tab never deletes or overwrites a newer pairing
A key record from a newer pairing revision is left alone (the tab stays on its
code), and saving an older revision never replaces a newer stored pairing.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* test(e2e): open the oracle's headless layout stream with a paired connection
The remote subscription now takes a paired connection rather than a pairing
offer; build one from the offer the way the CLI does.
* test(e2e): build the paired mobile client's connection from its offer
Same stale call as the oracle stream after the subscription and request
functions took a paired connection.
* feat(runtime): session-only browser keys and an opt-in for pairing from plain-http pages (host)
- pairing.registerDeviceKey accepts ephemeral: true (advertised as
pairing.device-key-ephemeral.v1). Such keyed rows are dropped when the host
restarts and after 12 hours with no live socket; a sweep removes them and cuts
any socket still bound to them, so later key auth gets device_revoked.
- The WebSocket upgrade Origin is recorded per socket. A first pairing (a
never-used pairing code) from an http:// page on another machine is refused
with a sealed insecure_origin error unless the operator sets
ORCA_ALLOW_INSECURE_WEB_PAIRING=1. Already-paired browsers, https and loopback
pages, and native clients are unchanged.
- Registry file reading moved to device-registry-file-read.ts to keep the
registry under the size cap.
* fix(runtime): gate first pairings only for the marked web client, never phones
React Native sends an http Origin on every LAN WebSocket, so the Origin alone
refused first-time QR pairing from released phones. The web bundle now marks
its upgrade with ?orca-client=web; only a marked socket from an http page on
another machine is gated. Released hosts ignore the query string.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* test(e2e): read the live runtime layout in the layout oracle
The oracle now reads the runtime's layout from a layout.subscribe consumer
(the window's renderer, or a headless host's pairing offer) instead of
session:get or the saved profile. Model rules run on every published frame,
replacing the 300 ms settle read and the 100 ms rule sampler; disk-format
rules and the round trip run on the saved profile at each settled step.
Views are compared to the live layout exactly for every group and tab kind.
The known-on-main list is refilled from runs of this oracle on main.
* test(e2e): known-on-main differences do not hold the oracle's settle wait
A difference main is known to keep never converges, so a step settles once
only known-on-main differences remain. The orcad title-after-restart entry
is narrowed to the scenario a run on main reproduced.
* test(e2e): oracle settle counts frames across sleeps, pairs renamed groups, flushes saves
The settle wait carries the frame count across reads, so a frame during the
sleep restarts the quiet window. The client check pairs a group listed under
another id with the runtime group holding the same tabs in the same order,
so only that rename is known on main and a dropped group still fails. The
window flushes main's debounced save before the saved-profile read.
* test(e2e): refill known-on-main from Linux CI and post-flush runs on main
Removes the two-writers saved-profile entry (gone once the window flushes
main's save). Re-adds client-reorder tab order and the cold-relaunch client
list, and adds the cold- and live-daemon relaunch tab-bar entries and the
Electron rename-after-restart entry, each reproduced by Linux CI run
38170663248 or a local run on main's code.
* test(e2e): scope known-on-main entries to the OS that reproduced them
Entries reproduced on one OS only carry a platform and match only there.
The cold-relaunch restart entry now matches only a single group whose tabs
and order are unchanged, so a reorder still fails.
* test(perf): in-app layout latency probe, event-loop delay on main and orcad, and a main baseline
* test(perf): plain report comment
* test(perf): name the latency report type
* test(perf): add the Linux CI baseline; routine runs take 10 samples
* test(perf): stamp close pane on the w key, keep probe work out of samples, label first drawn and Linux flood
The host side of the terminal source-range ACK protocol had one producer: the
relay-fed SSH output intake, deleted in #27515. Since then nothing installs the
runtime's source-range hooks and nothing passes source ranges into PTY output,
so every terminal.multiplex subscribe that offers ackOutputSourceRanges is
already answered without it and uses the byte-window ACK.
Delete the ledger, the per-connection registry, the consumer hook surface on
the runtime, the stream-level reservation/commit/rollback plumbing in flow
control, initial snapshot and cleanup, the source-range ACK frame schema, and
the range slicing in output chunks, batching and replay. The subscribe schema
still accepts the ackOutputSourceRanges capability, and the client keeps its
range-ACK support for older hosts. A new golden test pins that an offering
subscribe gets the same reply and frames as a byte-ACK one.
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients
Every paired client now builds its hello, checks the host's ready, seals its
auth frame and opens host frames through src/shared/paired-e2ee-client-handshake.
Platforms inject their own NaCl/base64 primitives. The v2 client session and its
pure-JS key schedule (@noble/hashes 1.8.0, the version mobile pins) move to
src/shared, and the per-host v2 latch policy is shared with an injected store.
No wire change: frames match the captured legacy and v2 fixtures byte for byte,
and a cross-version test drives the core against the released host and the
released client against this host. The auth step is now async so a device-key
signer can slot in later.
* fix(remote): keep @noble/hashes out of the CLI's unbundled runtime
The packaged CLI loads src/shared output from disk, so a bare @noble/hashes
import failed the packaged-runtime check. The v2 key schedule is now a
primitive: Node clients use the node:crypto schedule (moved to src/shared so
the CLI can reach it), while the browser and phone keep the pure-JS one.
* fix(mobile): re-check the v2 latch before sending a legacy auth frame
The auth step is async, so another socket can latch the host to v2 while a
legacy auth frame is being sealed. Refuse it at send time, as before.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(native-chat): a closed chat tab stays closed while the host is still closing it
Closing a chat tab removes it from the window at once. A tab-list frame the
host publishes before it has handled the close (a working chat's status
update, for one) still listed the chat, and the window appended it back at
the end of the strip until the host's own removal arrived.
The window now leaves out a chat whose close it has not heard back on, using
the in-flight close record it already keeps, and re-reads the host's tab list
once the close settles so a chat the host kept comes back.
* fix(native-chat): hide a closing chat tab through the existing close intent
Record a close intent for agent-session:<id> under the chat's host when a chat
tab close begins, so a host frame sent before the host handles the close cannot
re-add the chat at the end of the strip. The intent lifts when a host frame
stops listing the chat, or by its TTL. Only a failed host close clears it and
re-reads that worktree at the version already applied, so the chat the host
kept comes back.
Replaces the close-owner gate and the re-read after every close.
* fix(native-chat): end a chat close intent even when no host frame follows
Re-read the closed chat's worktree after every settled close, not only after a
failure, so a close the host had nothing new to publish for still ends its
intent; otherwise the same chat reopened within the intent's 20 s limit stayed
hidden. Skip the intent for floating-panel chats, whose frames are never
applied. Share one one-worktree local refresh with the Session History reopen
path. Reset close intents between tests in the reopened-tab e2e file, whose
harness tears the host down before a close's follow-up re-read lands.
* fix(native-chat): re-read after a successful chat close only for paired servers
This machine's host emits its removal frame before the close answers, and the
window applies it, so a local chat re-reads only when the close fails. A paired
server's frames travel apart from its answer, so, as for its terminal closes,
every close is followed by a read, and that read no longer joins a tab-list
request that began before the close. Floating-panel chats record no intent and
get no read.
* fix(native-chat): pin a chat close intent to the pairing the close began under
Record and clear the intent with one owner captured at close start, as paired
terminal closes do, so re-pairing a server mid-close cannot miss the clear, and
pass that pairing to the paired re-read.
* refactor(pty): delete the SSH PTY provider and its registry slot
Nothing in production registers an SSH PTY provider, so the registry's SSH
map was always empty: every SSH lookup answered undefined or threw the
'No PTY provider for connection' error. Delete the provider stack, its
consumer-session and source-recovery contracts, the registry maps and the
getSshProvider dependency, keeping each caller's existing result for an
absent provider. The registry now dispatches only 'local'; any other host
throws PtyHostNotDispatchableError, which keeps the renderer-matched
message for SSH ids and is never read as a gone or exited PTY.
* test(pty): keep the no-provider SSH routing cases in local-only form
* test: reword a comment naming the deleted SSH PTY provider
* test: drop an import the rebase left unused
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* feat(runtime): device-key authentication and per-installation key migration (host)
* fix(runtime): corrupt registry is never a revocation verdict; keyed sockets dispatch without a token; forks keep their Relay binding
* fix(runtime): keep an unparseable device registry aside; an unlisted key is never 'revoked' while it exists
* fix(runtime): a set-aside corrupt registry keeps verdicts doubtful even when the registry file is missing
* test(runtime): registerDeviceKey reaches the bound socket through the dispatcher for runtime and mobile rows
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(terminal): panes have two transports, local or remote runtime
* fix(terminal): keep local-only startup gates off unconverted SSH panes
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(mobile): direct connections use v2 framing and never fall back once v2 is seen
* test: drop casts in direct v2 tests
* fix(mobile): count v2 handshake failures against the auth retry budget; latch only after host auth
* fix(mobile): pick direct framing at open, recheck the latch before legacy auth, and keep old hosts off the auth budget
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
The terminal gate's relay lister always resolved to null in production (no SSH PTY
provider is ever registered), so its relay and previous-relay branches, the legacy
relay route/router/routing, the previous-relay census and the once-connected
re-check could never run. The gate now takes only the host census; census-only
verdicts are pinned by a characterization test written before the change.
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>