* feat(dictation): capture desktop audio in an AudioWorklet at 16 kHz
Desktop dictation captured with a ScriptProcessorNode on the renderer's
main thread and shipped hardware-rate buffers, which main then decimated
to 16 kHz with linear interpolation and no low-pass, folding everything
above 8 kHz back into the speech band.
An AudioWorklet now resamples to 16 kHz with a windowed-sinc low-pass,
posts 100 ms frames, and sends a level every 50 ms for the meter and
silence auto-stop. Desktop and the web client share it.
* refactor(dictation): share the sinc resampler and meter window, one frame sink
Move the resampler to src/shared beside the meter it feeds, share the
50 ms meter window with the phone's, drop the constant per-chunk sample
rate from buffered audio, and send frames through one deliverFrame so
the speech-host port can replace it later.
* Show the machine on folder workspace cards, as other workspace cards do
* Build folder fixture rows without passing the array index
* Add a fixture for a folder card on another machine
* Drop the folder card screenshot fixture
* Put an unstamped folder card on the host it runs on
A folder workspace with no host stamp resolved to its project group's host for
labels and sections but to local for its card row. Stamp it the way catalog
adoption does so the card, label and section agree with runtime dispatch.
* perf(mobile): return to LAN soon after joining Wi-Fi
* perf(mobile): allow one arrival per dwell and keep 12 s for Tailscale probes
* perf(mobile): defer a second arrival to the dwell and keep 12 s for non-private hosts
* refactor(mobile): share one network read across hosts and build the fingerprint once
* perf(runtime): resume a phone terminal from what it already received
* fix(runtime): carry a missed cwd change on resume and keep points the ring covers
* fix(runtime): resume only onto the grid the missed output was written for
* test(runtime): type the resume token without a cast
* perf(mobile): ack terminal output after xterm parses it
* fix(mobile): retry refused terminal acks and share one ack across echoes
* fix(mobile): retry a refused terminal ack on the timer, never inline
* test(mobile): keep only terminal credit tests that fail without it
* refactor(mobile): key terminal ack settles by stream token
* test(mobile): type the credit test timer steps
* test(mobile): use T[] for the parsed callback list
React Native Web repeats the last scroll event 100 ms after scrolling
stops. The page list cleared its record of its own scroll write on the
first matching event, so the repeat after each place-keeping correction
counted as the reader scrolling. For a parked reader that froze the
streaming reply below for 300 ms about every 0.7 s and released it in
one step, which showed as lurches of 20-40 px.
* perf(mobile): show the model picker's models as soon as a chat opens
* fix(mobile): retry skill scans and trailing command reads through the query
* fix(mobile): keep menu-read commands across a re-probe and rescan skills on demand
* refactor(mobile): read chat query state from the client and give the model catalog query its own module
* test(mobile): give the reopen seed catalog its fetchedAt
* fix(mobile): keep the query layer out of the phone options hook's module graph
* perf(mobile): load the new-tab agent list with the session and share it with "Fix with" labels
* fix(mobile): wait for host status before detecting agents
* fix(mobile): read the agent list again on each open after a failed read
* refactor(mobile): name the source control agent through the shared label hook
* chore(mobile): type the new-tab recording adapter with a type import
* feat(usage): show Muse subscription quota in the status bar
Reads `muse usage --json` (Muse Code 1.4.13+), which reports the
subscription allowance without a model turn, and maps `allowance.window`
to the five-hour session meter and `allowance.weekly` to the weekly meter.
`rateLimits.muse` now appears in `orca account list --json`.
- Gate on `muse --help` listing `usage`: older builds treat an unknown
subcommand as a prompt, so Orca never sends `usage` to them.
- Run with MUSE_NO_AUTO_UPDATE=1 and MUSE_LOGIN=0 so a background poll
never self-updates the launcher or starts a device sign-in.
- Missing CLI, signed out, API-key login / no allowance, non-JSON output,
and timeouts each map to an explicit unavailable/error snapshot.
- Hidden meters skip the spawn, and Muse publishes after the sibling batch
so a slow CLI cannot delay Grok/Cursor/ZCode/Antigravity.
- Default-on status-bar item with a one-shot migration, CLI-detection
gating, visibility menu and Settings toggle, remote-owner mapping.
Based on the design in #34232 by ShemYu.
Co-authored-by: ShemYu <11490047+ShemYu@users.noreply.github.com>
* fix(usage): cap Muse usedPercent and date readings by fetch time
Muse can report over-quota usage above 100; clamp to the 0-100 meter
contract like the other providers. Use the fetch time for updatedAt so a
cached older asOfMs cannot make every window focus trigger a refresh.
* fix(usage): prevent Muse launcher installation during polling
* fix(usage): gate Muse on its version file and run the active build directly
- Read the launcher's `.muse-version` and require 1.4.13 or newer (the
release that added `muse usage`); only installs without that file fall
back to `muse --help`, cached by launcher path, mtime and size.
- Execute `muse-bin-<version>` directly when it exists, so the launcher's
update, sign-in and install paths never run; the env guards stay for
the launcher fallback. A version change during the call discards the
reading as stale.
- Start the Muse fetch at the beginning of the full cycle and cap it near
8s (6s usage, 2s help), so a slow CLI holds the shared queue far less.
---------
Co-authored-by: ShemYu <11490047+ShemYu@users.noreply.github.com>
* refactor(dictation): run the desktop controller on a shared session state machine
* test(dictation): assert session machine behaviour, not run numbering
* refactor(dictation): allow a new start from the error phase
* test(dictation): drop the error-restart case no desktop path depends on yet
* refactor(dictation): derive stop-during-start from the phase and share the start teardown
This reverts commit 88fa7ed2a9.
Also drops the agyInstall strings a later locale sync added to the
non-English locale files, so no orphaned keys remain.
* perf(mobile): reopen Files folders from memory
* test(mobile): re-record Files explorer goldens for query reads
* fix(mobile): paint cached Files rows before the host client attaches
* refactor(mobile): share query observer options and Files' empty-state rules
* test(mobile): type the Files explorer test host
* perf(mobile): open Review with the Changes data already loaded
* fix(mobile): keep the open diff through re-keys and wait for Review rebuilds
* fix(mobile): release Review writes once their files show, retry only fixed diffs
* fix(mobile): follow language switches in Review's diff states
* fix(mobile): keep Changes' compare key out of render and type the Review adapters
Hold the lead Stop through the shared child-work fold while UUID children
are live between SubagentStart and SubagentStop. A child that sends no hook
for 10 minutes stops holding the row. Interrupt (subscribed async, as Muse
requires), SessionStart and SessionEnd clear tracked children, and the
existing transcript-poll replay of the held Stop then reaches done.
Fixes#28090
* fix(agent-hooks): bound Muse transcript polling when the session goes silent
A Muse session that ended without Stop kept its 1s session-log poll alive
until the pane closed or another hook arrived. Idle polls now probe the
owner's process after 60s without log growth (an exited owner is reconciled,
which clears the poll) and stop after 30 min of silence; the next hook
re-arms polling.
Linear: STA-8313
* fix(agent-hooks): re-arm the Muse poll after a presence probe restates the row
A live owner's suspension flip (Ctrl-Z/fg) makes the idle presence probe write
a new row; the next poll tick saw a superseded row and stopped, and no hook
re-armed it. Re-arm on the restated row, keeping the idle clock, unless a newer
hook already owns the pending poll.
Linear: STA-8313
* fix(muse): back off quiet transcript polling safely
* fix(muse): replace the presence probe with a 5s quiet-log back-off
Back a quiet Muse log off from 1s to a 5s cadence instead of probing
presence; log growth, a status change, or any hook for the session
(including ignored ones) restores 1s. Removes the idle presence probe and
the row-restatement re-arm, which Muse rows never needed: Muse hooks carry
no trusted process identity, so the probe always returned early.
* fix(mobile): reread Source Control settings when a screen opens or the app returns
* test(mobile): use the shared hook harness for the settings reread test
* feat(mobile): show a thin refresh line under the header while cached data refreshes
* fix(mobile): start the refresh line still under unknown motion and key it by execution host
* fix(mobile): draw the refresh line only once the motion preference is known
* fix(mobile): start the refresh sweep at the left edge on each reveal
* refactor(mobile): drive the refresh line from the screen's own refetch and keep its timers in the header
* test(mobile): give the Changes content fixture the new refreshing field
* perf(mobile): stop a hidden host list refetching on every change
The host list's change-event path fetched worktree.ps (and the server rows read) on every worktreesChanged or reposChanged with no throttle, even while the page was buried under a session or the app was backgrounded. Hidden pages now note what they owe and the foreground refresh catches up once; visible pages read at most once per 3 s.
* perf(mobile): count poll reads in the change throttle and queue the catch-up read
* perf(mobile): leave the poll out of the change throttle and queue the catch-up only after skipped notices
* test(mobile): drop the throttle timing test
* refactor(mobile): one owed flag and one full catch-up read for a hidden host list
* perf(mobile): skip the Suspense fallback on page boot
* fix(mobile): keep open-target boots on the lazy path
* test(mobile): pin that the page waits for its held route chunk
* refactor(mobile): preload every page layout and keep screens apart
* perf(mobile): render the cached page build first and talk only after the connection's verdict
* test(mobile): give ready shell states their page window
* feat(mobile): let pages read and write the device store
* fix(mobile): import zod as a namespace in the page store verb
The page bundle check rejects `import { z }`: it ships zod's locale tables.
* Add Git Graph view showing all branches and commit history
Adds a full-pane Git Graph tab that visualizes the whole repository —
all local branches, remote-tracking branches, tags, merges, and HEAD —
instead of only the current branch's linear history shown in the Source
Control sidebar today.
- Add an opt-in allRefs scope to the shared git-history loader so the
graph query enumerates --branches --remotes --tags HEAD; the default
current-branch path is unchanged. The flag is forwarded through the
local IPC, SSH provider, and runtime RPC boundaries so it works in all
environments.
- New git-graph tab content type with a full-pane GitGraphPane that
reuses the existing graph SVG/row components, virtualizes rows, and
opens a commit's combined diff on click. Wired into the tab strip as a
chip (activate/close/keyboard-cycle) mirroring the simulator pattern.
- Entry point: an Open Git Graph button in the Source Control commits
header (Cmd/Ctrl-click opens in a split).
Closes#4521
Co-authored-by: Orca <help@stably.ai>
* Address review: virtual-tab guards, nav-order, truncation copy
- Treat the Git Graph (and mobile emulator) tab chip as a virtual tab:
add an isVirtual contract to EditorFileTab/EditorFileTabContextMenu so
rename, copy-path, copy-relative-path, and reveal-in-finder are disabled
for tabs whose filePath is a display label, not an on-disk path. Prevents
a rename on the synthetic 'Git Graph' path running a real file operation.
- Forward gitGraphIds through the legacy reconcileTabOrder fallback in
getActiveTabNavOrder so keyboard tab navigation does not drop git-graph
tabs when no split-group layout exists yet.
- Make the truncation banner explicit: 'Showing first N commits across all
branches' (was 'of many'), across all locales.
- Tests: virtual-tab hides path actions; git-graph survives the legacy
nav-order fallback.
Co-authored-by: Orca <help@stably.ai>
* Forward allRefs over SSH relay and web runtime paths
Two boundaries re-destructured git.history params and dropped the new
allRefs flag, leaving the repo-wide Git Graph inert (linear default-branch
history) for SSH and web-runtime worktrees:
- src/relay/git-handler.ts: the relay host's git.history handler forwarded
only { limit, baseRef }. Add allRefs so the SSH host enumerates all refs.
Adds an end-to-end relay test (a feature-branch commit absent from linear
history appears only when allRefs is forwarded).
- src/renderer/src/web/web-preload-api.ts: the web preload history shim
destructured only { worktreePath, limit, baseRef }. Forward allRefs.
Also fix a request-id race in useGitGraphHistory: bump requestIdRef before
the null-context early return so an in-flight fetch from a prior worktree
can't resolve and overwrite the 'idle' state after the worktree clears.
Co-authored-by: Orca <help@stably.ai>
* Support filtered and pinned Git history
Resolve branch and tag roots on the host, keep paging tied to the chosen revision set, and echo the applied filter across Git transports.
Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
* Add the Git Graph tab and commit details
Open the all-branch view from Source Control or the command palette, preserve older clients' layouts, and keep the sidebar HEAD-only.
* Restore files the main merge left at stale versions
The earlier merge of origin/main kept older or missing copies of 61 files
unrelated to the Git Graph (CI script tests, reliability gates, shard
timings and five terminal runtime files). Take main's version of each so
the PR changes only its own files.
* Limit pinned history to explicit graph scopes
Keep All and HEAD snapshots free of redundant ref names, and filter ref searches before nested-tag peeling. Default history requests retain unpinned paging without evicting graph tokens.
Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
* Preserve canonical placement for older graph clients
Pass graph-free record deltas through without rebuilding layouts, retain existing empty groups, and translate content creation into canonical groups while preserving hidden graph tabs.
* Retain Git Graph browsing state for the tab lifetime
Own history retrieval, selection and scroll in one renderer controller that retires on close or owner/filter changes. Accumulate parent pages before publishing, share action-time workspace navigation, serialize ref searches, and scope refreshes to the workspace.
* Apply Git Graph branch selections on picker close
Keep checkbox changes local until dismissal, then normalize and apply one chosen ref set. Allow picker reads to use a qualified host scope.
* Keep Git Graph navigation bound to its tab owner
Retain the tab's qualified owner and Git connection across workspace focus changes. Newer selections and refreshes supersede pending parent navigation without losing fetched pages.
Report stale workspace switches and let the commit menu handle empty space in the Branch / Tag cell. Cover twin-owner retention, superseded navigation and picker apply-on-close with renderer regressions.
* Translate desktop layout commands against canonical state
Resolve older desktops' visible groups, tab gaps and split paths inside the canonical mutation, including detach and cross-partition moves. Keep initial admission visibility-aware and cache graph presence for binding-only frames.
* Refine Git Graph picker and owner transitions
Discard branch drafts on Escape or owner changes, follow updated tab ownership, and report workspace lookup failures for the current owner. Keep parent navigation queued behind pending history loads.
* Drop the redundant default from the exhaustive layout command switch
* Align Git Graph with changed-code quality rules
Use typed fixtures and an event guard, keep primitive styling on content wrappers, and prune loaded-commit bookkeeping after render.
* Route Git Graph opening through the existing editor action
Use workspace record selectors for stable owner inputs and remove two
loading labels orphaned by the graph changes from all locale catalogs.
* Preserve Git Graph selection and keep row content visible
Refresh and token recovery retain loaded depth and selection in one
accumulating operation. Keep the published view until it settles, and
only fall back to the first commit after history is exhausted.
Prioritize current and open-workspace refs, reserve their markers, clip
wide graphs and retain filenames when long paths need truncation.
* Bound Git Graph refresh and allow graph column resizing
Restore the loaded depth without searching for vanished selections, retain the published result on retrieval errors, and keep newer selections visible. Allow the bounded graph column to expand to every lane through the existing resize mechanism.
* Retain Git Graph history for errors without a message
Detect failed retrieval independently of its message and cover an empty-message rejection in the existing partial-failure regression.
* Retain Git Graph widths and preserve shifted selections
Separate committed and live resize widths, retaining the chosen width for the graph tab lifetime. Allow one bounded extra history page for selections shifted by new commits, and give the resize separator a translated label.
* Preserve nullable Git Graph owner fallback
* Fix Git Graph layout imports after the RPC method move
* Route the Git Graph owner connection through the source-control connection helper
* Keep the Git Graph button idle in hidden Source Control sections
* Re-probe Git Graph support when the owner's runtime status changes
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
* refactor: organize remaining terminal pane features by owner
* refactor(terminal): consolidate replay guard type imports
* refactor(terminal): retarget notification mocks and generated consumer import
* refactor(terminal): repair benchmark owner lookup and stale mock export
* perf(mobile): trim the page bundle back under its old budget
* build(mobile): simplify the route entry anchors and zod table guard
* fix(mobile): import zod as a type in the RPC result reader
* feat(native-chat): one Hide button and toast for chat cards; PR card setting beside Changed files
The opened-PR card now uses the changed-files card's Hide control and Hidden
toast through a shared NativeChatCardHideButton, and its switch moves to
Settings > Chat UI > Appearance next to Changed files summary, where Reset
restores both. The saved key is unchanged.
* refactor(native-chat): read the opened-PR card switch through its predicate
* test(settings): Reset turns both hidden chat cards back on
* Preserve the existing opened pull request Hide aria label
* fix(native-chat): keep the gap before the PR card's Hide button
Orca now needs macOS 13.4, but depends_on can only say Ventura, and
Homebrew 7 upgrades auto_updates casks by default. A preflight step
refuses the upgrade below 13.4 so Homebrew reverts to the installed app.
* Separate editor tab placement from content ownership
* Show scoped Source Control sections for attached worktrees
* Preserve active diff refresh and validate section catalogs
* Complete folder section test worktree metadata
* Use contextual types for scoped Source Control actions
* Preserve optional scoped action parameters
* Preserve editor ownership through restore, mirroring and workspace removal
* Retire Source Control drafts only after confirmed owner removal
* Limit section work to visible expanded repositories
* Keep editor follow-up actions and bulk closes in their invoking views
* Track the consolidated editor order writer in its owning module
* Use complete typed fixtures for repair contracts
* Preserve live editor placement during layout readback
* Pass projected editor placement directly to serialization
* Read attached worktree lineage from shared graph module
* Retain inferred owner checks and scope placement retirement
* Preserve selection in untouched workspaces during purge
* Exercise editor placements in existing launch and lifecycle fixtures
* Preserve shared buffer prompts and editor strip order
* Pause inactive section context subscriptions
* Keep section recovery and file actions in the folder workspace
* Describe attached Source Control in folder workspace CLI help
* Attribute Git metadata signals once per dispatch
* Watch mounted source control sections outside the warm budget
* Resolve section watches only for served filesystem roots
* Restore restart-stable editor identity across SSH opens
* Apply section terminal routing to explicit Chat choices
* Include cross-store descendants in folder workspace panels
* Keep cross-host recovery on its existing workspace route
* Start cross-host section agents in their workspace without navigation
* Update memoized Source Control labels on language changes
* Remove unused terminal close-to-right implementation
* Suspend inactive Source Control body observers
* Retain base ref query while section observation pauses
* Cover retained base ref search across section visibility
* Resolve note menu ownership from the retained snapshot
* Preserve section agent working directories and background placement
* Keep Source Control sections active while their overlays are open
* Read current settings when Source Control commands execute
* Adapt editor and browser test stores to view subscriptions
* Preserve bound store hooks for ordinary app views
* Trust agent starts outside the placement workspace
* Trust the section worktree through transient launch ownership
* Extract editor follow-up file actions from the panel
* Fail section launches when trust capability lookup fails
Preserve explicit unsupported-host fallback, propagate lookup failures to the existing launch error path, and let the existing transport retry refresh rejected capability probes.
* Follow the moved pane launch test in web configs
* Preserve section file ownership during refresh and close
* Share tab order removal across editor close paths
* Verify companion editor close through the real store
* Close all workspace split copies for legacy mobile file requests
* Keep unmatched mobile close requests within their workspace
* Reuse the agent menu render snapshot
* Keep attached folder roots scoped to their resolved host
* Apply repository trust decisions to the launch trust owner
* fix(mobile): Cmd/Ctrl+Enter on an empty chat composer steers the first queued message
Matches desktop (#38732) in the paired web page: with nothing typed, the
chord steers the head waiting card through that card's own Steer, only
where its other way is steer.
* fix(mobile): empty Cmd/Ctrl+Enter sends the first queued card after a Stop, as desktop does
Desktop's rule (#38877) also sends the head card when no turn runs, and
goal mode keeps the empty chord inert. The queue list's handle now runs the
head card's own Send when idle, and steers mid-turn only where the chord's
other way is steer.
* test(mobile): type the queued-card handle refusal cases
* feat(mobile): holding Cmd/Ctrl in the phone chat composer shows what Cmd/Ctrl+Enter will do, as desktop does
Desktop's held-modifier preview moves into src/shared so the phone web
composer runs the same rule: Send becomes Queue, Steer or Steer queued
and the head queued card lights while the chord's modifier is held
alone on a hardware keyboard. The queue's empty-chord rule is one
function the preview and the press both read.
* fix(mobile): build the held-chord preview with the phone's own React, and offer only presses that act
The shared hook now takes its React hooks from the caller, as the shared
attachment preview does: the phone bundle resolves a different React copy
than the repo root, so importing react from src/shared broke every phone
web composer (Invalid hook call).
The preview also matches what a press does: one pill press per hold (a
double-click can't send a card after the draft went), a picked /plan-style
skill counts as a message, a head card whose own action is on its way shows
no Steer queued, and the lit state lives in the tray so a chat switch keeps it.
* fix(mobile): held-chord pill takes one click like desktop, keeps busy state per chat, lights only the head row
- The pill ignores a click whose detail is above 1, as desktop does, so a
deliberate later click in the same hold still acts.
- In-flight card ids are kept per chat with functional updates, so a chat
settling after a switch can't clear the current chat's record.
- The lit state is a small store only the head row subscribes to, so a hold
re-renders one row, not the whole queue.
- The desktop preview test moves back beside the desktop wrapper it drives.
* refactor(mobile): one owner for queued-card actions, read by both the list and the held-chord cue
The queue list owned its in-flight guard per mount and the tray mirrored it
for the composer's Steer queued cue; each mirror leaked (cross-chat and
same-chat remount overwrites, an all-chats copy per change). As desktop's
queued-messages controller does, the tray now owns one action store: pending
keys per chat, admitted synchronously, released per action, published by
copying only the changed chat. The list takes those actions as a prop (it
keeps its own only when mounted alone), and the empty chord presses the head
through the same run, so the cue and the press can't disagree. The list's
imperative handle goes away.
Also replaces a type assertion on the pill's click detail with an in guard.
* test(native-chat): desktop chord-preview wrapper re-exports the hold delay its test reads
* feat(dictation): stop desktop dictation after a minute of silence
A toggle dictation left running keeps the mic open indefinitely. Stop it
a minute after the last speech, with a toast, like the phone's tap mode.
A held push-to-talk key is never stopped; its release ends the session.
Mobile's timer moves into src/shared so both apps use the same rule, and
the shared meter now records when it last heard speech.
* fix(dictation): gate the silence stop on hold mode, as the phone does
* refactor(dictation): let the desktop silence hook own the hold-mode rule, as mobile's does
* refactor(dictation): move the capture meter into its own hook
* refactor(dictation): initialise the capture meter ref without a render write
* fix(native-chat): drop focus ring that boxed the whole transcript
* fix(native-chat): keep the transcript focus ring for Tab focus only
* fix(native-chat): show the transcript focus ring after keyboard focus, not clicks
* fix(native-chat): keep the transcript focus ring as it was across app switches
* fix(native-chat): never draw a focus ring on the conversation
* fix(web): dictate through the paired host in the web client
The web client's speech API was the fallback proxy and its voice settings were
local-only, so the mic stayed disabled even when the host had dictation set up.
Send audio over the speech.dictation RPCs the phone uses, and follow the host's
voice enabled/model, which the host now publishes in settings.get.
* fix(web): cancel failed starts and keep heard text on a dropped chunk
* fix(web): keep heard text when finish fails and read host voice only from settings.get
* refactor(web): tighten the web speech adapter
* fix(web): keep the focus-read count by reusing the settings already read
* fix(web): import the uuid helper from its new home
* fix(web): cancel the host dictation when finish fails
* fix(web): stop waiting on a stalled chunk backlog after 10 s
* fix(web): match feedAudio's cap result now that the desktop bridge reports it
* fix(mobile): keep speech said while dictation starts and fail fast on a dead host
* fix(mobile): let a tap or long-press end dictation while it starts
* fix(mobile): wait for the host's cancel before the next dictation starts
* fix(mobile): let a start wait out the previous start on the host too
* refactor(mobile): track host dictation work per client, not per screen
* test(mobile): read the start's dictation id without a cast
* fix(mobile): finish instead of cancel when a held mic is released while starting
* feat(web): use E2EE v2 for the pairing token on plain-http pages
Plain-http pages cannot hold a device key, so their sockets always sent
the pairing token over the legacy framing. They now send it over E2EE
v2 (with the usual client capabilities) to hosts that accept it:
- A reconnecting socket tries v2. A host that refuses the v2 hello, or
refuses capabilities on v2 token auth, is noted and the socket
reconnects with the legacy framing, as the device-key migration
already does for older hosts.
- After a v2 token sign-in succeeds, the host's pinned key is kept in
local storage and every socket to it uses v2; it is never downgraded.
- One-shot and Crew sockets, which cannot absorb a refused attempt, keep
legacy framing until the host has proven v2. Keyed (https) pages are
unchanged.
* feat(web): recover a latched host that was downgraded, without a legacy downgrade path
A plain-http page that had signed in to a host over v2 kept retrying v2
forever if the host was later downgraded below v2 token auth. When a
latched host refuses v2, the next socket now only reads the host's status
over legacy framing (keyed to its pinned key), the way a key migration
socket works. If the host's own status lacks
pairing.e2ee-v2-token-capabilities.v1, the latch is released and the page
reconnects over legacy; otherwise it stays on v2. A refusal faked on the
path cannot produce the host's answer, so it cannot force legacy.
The token framing logic moves to web-token-framing.ts.
* fix(dictation): space dictated text from its neighbours and keep text that can't be inserted
* fix(dictation): don't report a contentEditable insert as landed when nothing was inserted
* refactor(dictation): read caret neighbours per block and trim the spacing helpers
* feat(remote-runtime): use E2EE v2 for desktop and CLI connections to remote runtimes
Desktop connections to remote runtimes (orca serve, orcad, managed SSH,
browser bridge) and the orca CLI now use E2EE v2 framing for hosts that
support it, and fall back to the legacy framing for hosts that do not.
- One-shot requests try v2 first. A host that refuses the v2 hello
(hosts from before v2) or refuses capabilities on v2 token auth gets
one transparent legacy redial; nothing but the handshake is sent before
authentication, so the redial is safe.
- After a v2 sign-in succeeds, the host's pinned key is remembered in the
profile (shared by the app and the CLI). Every connection to that host,
including long-lived sockets, then uses v2 and never falls back.
- Long-lived sockets keep legacy framing until a host has proven v2, so
they never see an older host refuse a v2 hello.
- Hosts accept clientCapabilities on direct v2 token auth, so desktop and
CLI clients keep their capability negotiation on v2. Relay token auth
stays exact.
* feat(remote-runtime): recover a latched host that was downgraded, without a legacy downgrade path
A host this profile had signed in to over v2 was refused for good if it
was later downgraded below capability-carrying v2 token auth. Hosts now
advertise pairing.e2ee-v2-token-capabilities.v1. When a latched host
refuses v2, a one-shot request asks the host over legacy framing (keyed
to its pinned key, so only the real host can answer) for status.get:
- no capability: the host itself says it was downgraded; the latch is
dropped and the request goes ahead over legacy;
- capability present, or no answer: the request fails with an error
that says to update the host or check the network path. Only the
read-only status check ever ran over legacy.
A refusal faked on the path cannot produce the host's answer, so it
cannot force legacy framing for a latched host.