* feat(mobile): transcribe dictation on device with SpeechTranscriber on iOS 26
* fix(mobile): fall back to the dictation model, reserve locales and stop a finishing session on cancel
* fix(mobile): resolve on-device speech locales deterministically and report the simulator unsupported
* refactor(mobile): simplify the on-device speech engine and skip conversion when formats match
* feat(mobile): define the on-device speech contract for the native audio module and page bridge
* feat(mobile): dictate on the phone first and replay to the computer when the phone fails
* refactor(mobile): type the on-device speech module without a cast
* fix(mobile): point dictation's finish ref at the committed callback
* test(mobile): wait on the takeover request instead of a fixed sleep
* fix(mobile): release each dictation request's own share of a replayed backlog
* fix(mobile): let the computer's text replace the phone's after a fallback
* feat(mobile): recognize dictation on-device on Android with ML Kit
* feat(mobile): fall back to Android's on-device recognizer on phones without AICore
* fix(mobile): keep on-device speech events and finishes tied to the current session
* fix(mobile): let an ended on-device session release the recognizer before the next starts
* refactor(mobile): trim the Android on-device speech engines
The New workspace and New issue discs on the host and Tasks screens were
a pale #e5e5e5 fill that glared on the dark page. Dark mode now uses the
same accent fill as the Home + button; light mode keeps its dark fill.
Refs #37401
* feat(mobile): exchange the scanned pairing code for a token kept on the phone
When the desktop advertises pairing.ticket-exchange.v1, the phone sends a
token it generated as the last call on the winning pairing socket and saves
that token instead of the scanned one. For Relay pairings the journal is
rewritten first so recovery republishes the same token. Any failure keeps
the scanned code, which the desktop still accepts.
* fix(mobile): say a refused pairing code may already have been used
Single-use pairing codes are refused once another device signs in with
them, so the scan error now mentions that case alongside expiry and
replacement, and still asks for a new code from the desktop.
* feat(antigravity): run Chat on the host's agy acp when compatible
New Antigravity chats prefer a compatible host-local `agy acp` (initialize
must answer as "Antigravity ACP Server" >= 1.5.2, protocol 1, loadSession and
session/resume) and fall back to Orca's managed runtime otherwise. The chat's
account home pins its backend, so resume and auth failures never switch it.
Managed chats and their <userData>/antigravity-chat sign-in are unchanged.
Refs #38833, STA-14062
* test(acp): launch an attached host rig with its spec
* fix(antigravity): keep Orca's Chat sign-in, back off a hung agy, and never downgrade permissions
The host's agy takes a new chat only when nobody is signed in to Orca's own
Chat and agy has its own credential. A failed check waits 5 minutes before
retrying (5 s timeout). Full-access chats stay on Orca's runtime, and a host
chat refuses a permission mode agy lacks instead of asking.
* fix(antigravity): check agy's sign-in without a keychain prompt, ignore inherited API keys, allow own mode
agy falls back to an inherited GEMINI_API_KEY, which its models reject, so
host chats drop it. The macOS presence check reads only the keychain item's
attributes. Chats in agents' own permission mode may use the host's agy.
* fix(native-chat): show Orca's install card only for chats on the managed runtime
The status summary marks chats pinned to a host program (hostProgram), so a
chat on the host's agy is no longer blocked when Orca's runtime is missing.
* fix(mobile): show host agy sign-in copy for Antigravity chats
The desktop host now reports antigravitySystemNotSignedIn when a chat on the
host's agy is signed out; give the phone the same copy so its failure table
stays complete.
Each chunk RPC holds one of the page bridge's 60 desktop request slots until
the host answers, through a reconnect too, so the page drained the microphone
only every 500 ms. Cap chunk sends at four per dictation and merge audio
captured meanwhile into the next request, then drain every 150 ms.
* fix(automations): send managed SSH host automations to its Orca server
A managed SSH host's automations live on its Orca server (#24863), but the
create dialog, CLI and mobile still saved desktop copies that never ran.
- Desktop dialog: the host's desktop SSH entry is no longer offered; its
key resolves to the host's server entry.
- CLI create routes to the server by project id, or asks to move the
project there first; mobile hands the create to the server's editor.
- Existing desktop copies skip with "{host} now runs automations on its
Orca server" and offer "Move to {host}" through the editor's move save.
STA-13422
* fix(automations): address review on managed SSH host routing
- CLI: refuse a desktop copy when the desktop reports a managed host this
CLI holds no server for; pass the workspace by id to the server; check
field support on the server too.
- Main: the skip follows the owner selector, so local rows pinned to a
managed host's workspace skip too.
- Editor: a destination host whose project table loads after open keeps
the chosen project.
* fix(automations): refuse new desktop rows on a managed SSH host and move leftovers
Per review and the owner's follow-up:
- The desktop's automation create/update refuses a row on a managed SSH
host ("Move this project to {host} first."), covering CLI, mobile, raw
RPC and older clients.
- Existing desktop copies move to the host's Orca server on startup, on
managed connect and on their due tick: created there keyed by the
desktop row id, then removed here. A server without the project leaves
the skip and the Move button.
- Mobile drops the Save-time hand-off; its picker no longer offers a
managed host's desktop SSH setup.
- CLI routes only for a local client, using that client's profile.
* fix(automations): drop the automatic leftover move
A managed host's leftover desktop rows keep the skip reason and the
Move to {host} button; moving them automatically will extend the existing
managed-host migration in a follow-up.
* fix(cli): resolve an automation's SSH destination from the repo's routing id
* chore: prune the repo host read baseline for the CLI destination
* fix(automations): migrate a leftover automation in one click
The detail banner's Migrate runs the editor's own move-to-server save without
opening the dialog. Leftovers show Needs migration instead of Enabled, and
their skipped runs read Needs migration to {host}.
* fix(automations): wrap the Needs migration status instead of cutting it off
* fix(automations): name a leftover's project from its migrated copy
* fix(automations): save a host move against the project table loaded after the editor opened
Migrate loads the server's repo table first, but the save transaction read the
destination captured before that load, so the first click on a cold catalog was
refused. Save now reads the same derived destination the form already shows.
* fix(automations): drop the leftover project-name fallback
The global id lookup could name another host's project. Scoping it to the
leftover's server is not worth the code for a display-only label, so a leftover
whose project moved shows Unknown project again.
* fix(automations): name a leftover's project from its migrated copy
A managed SSH leftover's project always moves to the host's Orca server with
the same id, so only those rows fall back to the id lookup; any other row with a
missing project stays Unknown project.
* feat(speech): let several devices dictate at once on one recognizer
The host ran one dictation at a time, so a phone dictating blocked the
desktop, the web client and every other phone. Each dictation now opens
its own stream on the one loaded recognizer, capped at four, and a start
that needs a different model is refused while others use the loaded one.
* feat(dictation): explain the device limit and a busy model on desktop and phone
* docs(web): say the host has several dictation slots
* fix(speech): free an idle recognizer for cloud dictation and drop canceled starts from the device count
* test(speech): drop a deletion test that only pinned the old single-session limit
* refactor(speech): drop a canceled start from the map instead of flagging it
* refactor(speech): tag stop, discard and feed requests with their stream on the new base
* fix(speech): satisfy the type-aware lint on waiting for stopping sessions
* fix(speech): copy only the fed view of a sub-buffer, and call it a speech model in the busy copy
* fix(speech): bind a starting session to its recognizer in the step that picks it
An unconfirmed send settled as posted when the refreshed thread had any new
comment with the same text, so another person's identical comment cleared the
draft. The host now passes GitHub's viewerDidAuthor, and the phone settles only
on a comment the viewer wrote. Without it (GitLab, REST fallbacks, older hosts)
the draft stays with the check-the-thread notice.
* 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(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
* 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
* 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.
* 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
* 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(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
* 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(pairing): unscanned desktop pairing offers expire (QR 10 min, link 60 min)
Headless serve/orcad mint offers without a TTL, so they never expire.
Paired rows and rows without offerExpiresAt are unaffected.
* feat(pairing): refresh the mobile QR before it expires and show runtime link expiry
The QR re-mints a minute before expiry while visible; an unused runtime
link shows its expiry and, once expired, is replaced by a notice. Web
clients rejected while pairing now suggest asking for a new link.
* feat(mobile): say a refused pairing code expired or was replaced
A first pairing the desktop refuses (direct or Relay) now asks for a new
code instead of showing the raw transport error.
* refactor(pairing): replace type assertions in code-refusal detection and tests
* test(mobile): re-record pairing race golden for refused-code error
* fix(pairing): keep pairing codes valid across sleep and stale registry writers
- QR refresh rechecks the wall clock every 15 s and on window focus, so a
computer waking from sleep never keeps showing an expired code.
- A registry save from a process with an older copy can no longer reset a
paired row back to unscanned (which would let its offer expire).
- The QR caption says how long a copied code works.
* test(pairing): annotate the IPC handler stub cast
* Offer terminal sign-in from native chats and retain refused worker tasks
* Handle sign-in terminal failures and complete failed-start cleanup callers
* Read retry task identity from its dispatch context
* Assert retained transcript identity through the bounded worker read
* Remove obsolete private structured retry teardown assertion
* Use real signed-out Pi and OMP error frames in regression coverage
* Route chat sign-in through explicit host terminal creation
* Keep paired agent launch owner explicit in the tightened terminal contract
* Keep focus baseline pruning limited to the removed reads
* Keep terminal login recovery available for unresolved model notices
* Keep the Grok diagnostic assertion scoped to its refused delivery
* Limit chat sign-in subscriptions to eligible failure rows
* Pass the execution host in terminal creation fixtures
* Scan Pi OAuth failures linearly and verify shared sign-in bounds
* Assert failure metadata and diagnostic routing without translated copy
* Bound diagnostic matcher steps at the Pi failure boundary
* Type the diagnostic matcher receiver explicitly
* Check sign-in button elements with a DOM type guard
* Recognize Pi credential failures and keep one recovery action
* Keep Pi sign-in diagnostics behind failure details
* Verify recovery action changes while refusal wording stays stable
* Use the recorded refusal sentence in the recovery fixture
* Type the recovery fixture as a submission rejection
* Make chat sign-in actions large primary controls
* Reuse primary action styles without duplicate notice subscriptions
* Keep translated primary chat actions within narrow notices
Multi-repo folder workspaces can parent workspaces on this machine and on paired servers, and their children nest under them in the sidebar and on the phone. Task parents are inferred at creation instead of on every lineage refresh.
Slash and mention suggestion menus in the phone chat composer stay reachable above the on-screen keyboard while typing.
Co-authored-by: Guderlilian <73724370+qq2144@users.noreply.github.com>
* feat(mobile): sign Android release builds with a dedicated release key
Add an Expo config plugin that gives the generated app/build.gradle an
env-driven release signing config, and have the Android release workflow
require the keystore secrets, fail before building when they are missing,
and check the APK's signing certificate against a pinned fingerprint.
* fix(mobile): keep the signing setup hint intact in the workflow error
* test(mobile): type the plugin import without a cast
* feat(mobile): keep debug signing until release signing is enabled
Release signing now turns on when the ORCA_ANDROID_RELEASE_CERT_SHA256
(or ORCA_ANDROID_REQUIRE_RELEASE_SIGNING) repository variable is set.
Until then the workflow keeps today's debug-signed build and adds a
warning and job summary note; once enabled, missing secrets fail.
* feat(mobile): enable v2 and v3 APK signing for the release key
* fix(automations): run lists send byte-bounded previews; Run Details reads one full row
* fix(automations): retry a failed full run read on the next history read
* fix(automations): show a full-output read failure with Retry in Run Details
* fix(automations): Run now returns the saved pending row; quiet, titled, retained run terminals
Run now answers with the durable pending row instead of waiting behind
the automation's lane; one detached handler records a launch failure.
Existing-workspace launches no longer surface their owner, a new
workspace's startup tab carries the run title, and finished-run cleanup
keeps a terminal a user opened after its process started.
* fix(automations): keep launched runs and user-opened run terminals intact
A failed flush after a launch no longer rewrites a dispatched run as a launch failure.
A user tab open marks the pane activation shows, read after any spawn it starts.
* fix(automations): own launch-failure recording in the service; quiet pending pane notice on mobile
* Add session Build and Plan choices to native chat
* Refresh planning choices across connected composers
* Give composer catalog tests a complete transport fixture
* Preserve planning choices through fresh turns and older hosts
* Type planning delivery fixture choices explicitly
* Honor mode capability for retained queued command picks
* Update existing native mode contract expectations
* Keep proposed plans visible in completed chat turns
* Group chat mode with permissions and goal controls
* Keep chat composer controls accessible in narrow panes
* Preserve composer overflow ownership and editor focus
* Complete composer localization catalog and subscription checks
* Keep chat composer controls contained in narrow panes
* Use the button spacing for compact Save
* Place chat mode immediately before Goal
* Restore Plan status test dependencies after main sync
* Show an exit pill only while chat Plan mode is active
* Remove obsolete composer tooltip translations
* Remove unused Build toggle catalog entries
* Apply Plan commands immediately from composer picks
* Preserve picked skill origin in composer admission
* Supply phone platform to composer regression fixture
* Preserve mode picks until their writer admits them
* Narrow the phone mode command before its draft transform
* Document the option fixture mutation shape
* Match phone Plan controls to the composer mode pills
* Assert optimistic mode feedback in the phone fixture
* Retain the phone mode pick recovery regression proof
* Use the relocated observation imports after the main merge
* Restore the composer IME module import after merging
* Read the queue assertion helper from its relocated owner
* Point the mode fixture at the relocated selection controller
* Register the phone Plan pill localization keys
* fix(mobile): a failed page remounts instead of re-downloading; deadlines count only foreground time
Page deadlines now pause in the background and restart on return, so React Native firing overdue timers on resume no longer reads as a page failure. A failed document remounts the same files once, then shows the failure screen; it never deletes and re-downloads on its own. Try again after a page failure or an unreadable generation deletes the host cache first, so it downloads fresh. Replaces #34550 without its file-size check.
* fix(mobile): drop reports a remounted page's retired view already queued
A failed page remounts at once, minting the replacement's session before
React commits it. A fault or ready the old view had already queued then
landed on the replacement: a second fault showed the failure screen
before the new page loaded, and a late ready marked it ready and
cancelled its load deadline. Each report now carries the session of the
view that rendered it and is dropped once that view is retired.