Jinwoo Hong 60a774c30c feat(mobile): client operations and dev probe for the desktop-served mobile web bundle (OTA phase A, 5/5) (#21374)
* chore(rpc-contract): provisional catalog entries for the mobile web bundle methods

PROVISIONAL, and the only commit on this branch that must not survive the merge
as written. `rpc-params-catalog.generated.ts` is generated from the host method
registry, and A5's client operations cannot name `mobileWeb.bundle.manifest` or
`mobileWeb.bundle.chunk` until A3 registers them: `defineRpcOperation` constrains
`method` to `RpcMethodName`, which is `keyof typeof RPC_PARAMS_BY_METHOD`.

These two entries are what the generator emits once A3 lands. After merging A3,
run `pnpm run generate:rpc-params-catalog` and keep its output, not this.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): checked client operations for the desktop-served mobile web bundle

Two `defineRpcOperation` descriptors over the A1 contract, both
`require-result-or-throw` at `on-settle`: there is no partial success in a bundle
read, and a salvage policy would produce a half-bundle that fails a hash check far
from the cause.

Readers are hoisted `looseObject`s that require only what this client reads, so a
later optional member stays a Rule 1 addition for released phones; the host's own
schemas stay strict. `dataBase64` is bounded by the contract's chunk size, so a
host that overshoots is refused at the boundary rather than at reassembly.

`readMobileWebBundleErrorCode` maps the host's six codes out of the thrown
`code: message` diagnostic and answers null for everything else. Membership comes
from the contract's own enum, which is built from its `hostUnionArms` record, so
the arms here cannot drift from the host's union.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): fetch and verify a whole mobile web bundle over the paired connection

`fetchMobileWebBundle` reads the manifest, pages every asset at the chunk size the
host advertised, and verifies each reassembled asset against the manifest's sha256
before returning it. Nothing is cached and nothing is rendered: this is Phase A's
proof that the pipe carries a bundle intact.

Four asset reads run at once and no more, because the host refuses the fifth
concurrent read on one connection with `mobile_web_bundle_read_limited`; paging
inside an asset stays sequential, since the next offset is only known to be wanted
once a reply says it is not the last.

Every chunk reply restates its build, path and offset and the whole asset's length
and hash, and all five are checked. A desktop that auto-updates mid-download
answers a later chunk from a different build, and nothing else in the reply says so.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): dev-only troubleshooting row that fetches the mobile web bundle

The Phase A proof that the pipe works on a device. Tapping it fetches the whole
bundle from the paired desktop and reports the build, asset count, byte count and
elapsed time, or the host's error code.

`TroubleshootView` gains a `developerRow` slot and the route fills it only when
`__DEV__` is true, so a shipped build mounts nothing: no host lookup, no client
acquisition, no request. The row reuses the screen's existing button and check-row
styles, so it adds no visual vocabulary.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): recording scenarios for the mobile web bundle operations

Two families over the real product modules: `mobileWeb.bundle-manifest` drives the
manifest descriptor alone, so the loose reader's verdict on one reply is the whole
observation, and `mobileWeb.bundle-fetch` drives the paging flow over a two-asset
bundle whose entrypoint spans two chunks.

The fetch family's state carries the decoded bytes of every asset rather than a
count. A reassembly that misplaces a chunk still has the right length, so only the
bytes say so.

Goldens land with the repin in the next commit: the recorder fences on the pinned
tree, and these modules are not in it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the recording corpus and record the mobile web bundle goldens

`--record` refuses on any tree but the pinned one, and the pin predates this
branch's product modules, so the corpus is repinned to `bbf8264425` — the last
commit here to touch a fenced path — and re-recorded whole, the way
`rpc-recording/README.md` prescribes for a product change.

The delta is the clean one that repin predicts. All 778 existing goldens move
exactly one line, `baseline`, and nothing else: no body moved, no other header key
moved, none was deleted. Nine are added, two pilot per family plus the five reply
matrices the two families derive.

The fetch adapter projects its result rather than returning it whole. The result
carries a Map of Uint8Arrays, the observation refuses a non-plain object, and the
first recording lost the settlement and filed an unhandled rejection in its place.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): read the fake host's params through a boxed field read

The changed-code casting gate refuses the assertion the fake transport used to
type its recorded params. Boxing the value the way `settings-read-operations.ts`
does reads the same fields with no assertion, and a non-object params reads as
absent instead of throwing.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus to this branch's last fenced commit

The casting fix landed under `mobile/src`, which is a fenced path, so the pin no
longer named the tree `--record` runs on. Repinned to `79c3eed6db` and re-recorded.

Every golden moves the `baseline` header and nothing else, which is what a repin
with no product change is: the edited file is a test, and no recording loads one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): mutation evidence that the fetch projection observes the bytes

Writes every chunk at offset 0, so a multi-chunk asset reassembles as its last
chunk over a zero-filled buffer. The length still matches the manifest, so only
the sha256 check and the decoded bytes in the projection can see it, which is
what the fetch family's state exists to show. The mutant is killed.

`mutants/` is outside every golden digest, so this moves no recording.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): stop every worker's chunk reads the moment one asset fails

`stopped` was read only between assets, so the other three workers paged their
asset to the end after the fetch had already rejected: 121 chunk requests where
4 had been issued at the rejection. Each one holds one of the host's four read
slots, so an immediate retry was refused with `mobile_web_bundle_read_limited`
that only the abandoned workers caused.

An internal AbortController now stands beside the caller's signal and is checked
before every chunk request, not just between assets. Also pins the entry abort
check, the overrun check with real bytes, the measured byte total, and a schema
refusal whose message is prose rather than one of the six codes.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the code anchor and both operation descriptors

`RPC mobile_web_bundle_unavailable failed` separates the anchored reader from an
unanchored one; the prose test that claimed to cover it had its first token at
index 0, so the anchor was load-bearing and untested. Also pins that a schema
refusal, which the dispatcher raises with zod prose before the bundle handler
runs, reads as no code, and that both descriptors stay
`require-result-or-throw` / `on-settle`.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): dial the host on tap in the dev bundle row, and name it

Opening Troubleshoot in a dev build acquired a client at mount, which is what
kicks a dial, on a screen that opened no connection before. The probe now
acquires only once the row is tapped, and each request owns its AbortController
so a re-run, an unmount or StrictMode's second mount abandons the previous fetch
and stops its chunk reads instead of holding the host's read slots.

The screen carries no host parameter and troubleshoots every paired host, so
there is no host it is "on": the row still takes the first paired host but now
names it in the result instead of implying it speaks for all of them. The label
says whether it is still connecting or already fetching.

There is no `__DEV__`-conditional `require` idiom in this repo to trim the row
out of a release bundle with, which the route now records.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): refresh the recorder corpus counts

397 scenarios, 787 goldens, 790 tests from the README's own three-file command.
The 44 salvage goldens are unchanged; only the total they are quoted against
moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin and re-record for the mid-asset stop

Baseline moves to c519c2027d, the last commit on this branch to touch a fenced
path, and the whole corpus is re-recorded from it.

Delta against the pin, by the README's four classes: 786 header-only, 1 body
moved, 0 added, 0 deleted. The only key that moved on the 786 is `baseline`;
neither `recorderSha256` nor any `adapterSha256` moved, so nothing this branch
touched is inside a hashed recorder path.

The one body move is the disclosed behaviour change.
`matrix-mobileweb.bundle-fetch-app-js.json` is the reply matrix at the app-js
binding: where a partition leaves the app-js chunk without a result, the fetch
now stops the other workers mid-asset, so the sender list loses the chunk calls
they used to make for a bundle nobody would read.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): hold the rendered tree and the captured signal in boxes

Assigning to a `let` inside a callback leaves it narrowed to `null`, which the
harness was answering with two type assertions. A one-property box is a checked
type and the casting gate no longer has anything to report.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus to the branch's final fenced commit

Removing the two type assertions touched a test file under `mobile/src`, which
is inside the fence, so the pin moves to cae8f4a318 and the corpus is recorded
again from it.

Header-only, as a repin with no behaviour change should be: 787 header-only, 0
body moved, 0 added, 0 deleted, and `baseline` is the only key that moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): let runRpcOperation send a params-less method

A3 declares `mobileWeb.bundle.manifest` with `params: null`, so the generated
catalog types its send params as `void` and the two call sites that pass an
explicit `null` stopped compiling.

`bindDeferredRpcOperation.request` already solved this: `RpcSendArguments`
admits `null` exactly where the catalog declares no params, because
`params: null` is not the frame that omits the key and narrowing it would
rewrite bytes shipped senders already put on the wire. `runRpcOperation` was
the one send entry point that never adopted the tuple, having had no
params-less caller until now. The compile fence pins all three accepted
shapes and that a params-bearing object is still refused.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus after merging main

The merge brought A3's host methods and the generated catalog, and the
follow-up widened runRpcOperation, so `mobile/src` and `src/shared` both
moved. Repins `baseline` to 5be50beb41, the last commit to touch a fenced
path, and re-records everything.

Delta against that commit: 787 header-only, 0 body moved, 0 added, 0 deleted.
The only header key that moves is `baseline` — the transport change is
type-only, so nothing a screen observes changed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): bound the bundle a manifest can make the client allocate

M1: the loose client reader kept every ceiling A1 declared except the one
that bounds their product. A manifest could pass `totalBytes` 0 alongside
256 assets of 10 MiB each and the fetch would allocate 2560 MiB against a
32 MiB contract. The reader now sums `assets[].byteLength` against
MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES. A ceiling rather than the host's
sum === totalBytes equality, because this client never trusts `totalBytes`
for anything and bounds what it will actually allocate instead.

L1: a tap dials the host, and nothing bounded that wait. A host whose client
never arrives left the row reading `Connecting…` with its button disabled
for the life of the screen. A deadline through the diagnostics folder's own
`startDiagnosticFetchTimeout` settles it to a failure and drops the
acquisition. Ten seconds, because acquiring a client is local work: the
connect and request timeouts live below this and only apply once one exists.

L2, four survivors now pinned: the eof break against a zero-byte asset end to
end, the offset half of the chunk echo check on its own, the anchor that
keeps `rpc (mobile_web_bundle_unavailable)` from reading as a code, and both
`abandoned` guards against a run the screen moved on from.

Also: the stop check moves above the per-asset buffer, which makes the
worker loop's copy redundant; drops the unreferenced chunk reply type; and
restores the comment pairing in operation-mutations.ts, where the bundle
entry had been inserted between the catalog mutation's comment and its entry.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus after the round-2 fixes

Repins `baseline` to 3252779fa7, the round-2 product commit, and re-records
everything.

Delta against that commit: 787 header-only, 0 body moved, 0 added, 0 deleted,
and `baseline` is the only header key that moves. `recorderSha256` holds even
though `mutants/operation-mutations.ts` changed, because the mutant directory
is excluded from the recorder digest on purpose — nothing on the recording
path reads it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus onto the merge that carries A4

A4 (#21376) added a mobile/src file inside the recorder fence, so the pin
has to name a commit that contains it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-18 02:47:50 -04:00
2026-09-18 04:38:25 +00:00
2026-05-04 20:42:03 -07:00
2026-03-16 22:27:51 -07:00
2026-03-28 10:19:14 -07:00

Orca Orca

GitHub stars Total downloads across all releases License: MIT Join the Orca Discord Follow Orca on X Supported platforms: macOS, Windows, and Linux

中文 · 日本語 · 한국어 · Español · Français · Português

The AI Orchestrator for 100x builders.
Run Codex, ClaudeCode, OpenCode or Pi side-by-side — each in its own worktree, tracked in one place.

Download Orca

Orca desktop app running agents in parallel worktrees, with the Orca mobile companion app in the corner

Features

Mobile Companion

Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.

iOS App Store · TestFlight · Android APK 0.0.48 · Docs →

Orca desktop with the mobile companion app

Parallel Worktrees

Fan one prompt across five agents, each in its own isolated git worktree — compare the results and merge the winner.

Docs →

Parallel worktree orchestration

Terminal Splits

Ghostty-class terminals with WebGL rendering, infinite splits, and scrollback that survives restarts.

Docs →

Terminal splits

Design Mode

Click any UI element in a real Chromium window to send its HTML, CSS, and a cropped screenshot straight into your agent's prompt.

Docs →

Embedded browser and Design Mode

GitHub & Linear, Native

Browse PRs, issues, and project boards in-app — open a worktree from any task and review without a context switch.

Docs →

GitHub and Linear task workflows in Orca

SSH Worktrees

Run agents on a beefy remote box with full file editing, git, and terminals — auto-reconnect and port forwarding included.

Docs →

Remote worktrees over SSH

Annotate AI Diffs

Drop comments on any diff line and ship them back to the agent — review, edit, and commit without leaving Orca.

Docs →

Annotate AI-generated diffs

Drag Files to Agents

VS Code's editor with autosave everywhere — drag files or images straight into an agent prompt.

Docs →

Drag files and images into an agent prompt

Orca CLI

Agents drive Orca too — script every workflow with orca worktree create, snapshot, click, and fill.

Docs →

Script Orca from the CLI

Also in the box:

  • Quick open — Search across worktrees, files, agents, commands, and repo context without leaving your flow.
  • Account switcher & usage tracking — See Claude and Codex usage and rate-limit resets, and hot-swap accounts without re-logging in.
  • Rich repo previews — Preview Markdown, images, PDFs, and repo docs in the workspace.
  • Computer Use — Let agents operate desktop apps and visible UI when a workflow needs real interaction.
  • Notifications and unread state — Know when an agent finishes or needs attention, then mark threads unread to come back later.
  • And many, many more — we ship daily, so this list is perpetually behind. The changelog is the real feature list.

Supported Agents

Works with any CLI agent — if it runs in a terminal, it runs in Orca.

Claude Code logo Claude Code   Codex logo Codex   Grok logo Grok   Cursor logo Cursor   GitHub Copilot logo GitHub Copilot   OpenCode logo OpenCode   MiMo Code logo MiMo Code   Amp logo Amp   OpenClaude logo OpenClaude   Antigravity logo Antigravity   Pi logo Pi   oh-my-pi logo oh-my-pi   Hermes Agent logo Hermes Agent   Devin logo Devin   Goose logo Goose   Auggie logo Auggie   Autohand Code logo Autohand Code   Charm logo Charm   Cline logo Cline   Codebuff logo Codebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   Kilocode logo Kilocode   Kimi logo Kimi   Kiro logo Kiro   Mistral Vibe logo Mistral Vibe   Qwen Code logo Qwen Code   Rovo Dev logo Rovo Dev   + any CLI agent


Install

Desktop — macOS, Windows, Linux

Or via a package manager:

# macOS (Homebrew)
brew install --cask stablyai/orca/orca

# Arch Linux (AUR) — or stably-orca-git to build from source
yay -S stably-orca-bin

Mobile Companion — iOS, Android

Pair with your desktop app to monitor and steer your agents from your phone.


Community & Support

  • Discord: Join the community on Discord.

  • Twitter / X: Follow @orca_build for updates and announcements.

  • WeChat: Scan to join the Orca community WeChat group 8. Group 8 may be full; if so, scan the Group 9 QR code instead.

    WeChat group 8 QR code for the Orca community  WeChat group 9 QR code for the Orca community

  • Feedback & Ideas: We ship fast. Missing something? Request a new feature.

  • Privacy: See the privacy & telemetry docs for what anonymous usage data Orca collects and how to opt out.

  • Show Support: Star this repo to follow along with our daily ships.


Developing

Want to contribute or run locally? See our CONTRIBUTING.md guide.

The relay that pairs the mobile app with a desktop host is also in this repository under cloud/, with a separate pnpm workspace and setup guide.

Orca contributors

GitHub star history chart for stablyai/orca

Signed Builds

Windows code signing sponored/provided by SignPath.io, certificate by SignPath Foundation.

License

Orca is free and open source under the MIT License.

S
Description
Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.
Readme MIT
1.5 GiB
Languages
TypeScript 95.2%
JavaScript 4%
Swift 0.2%
CSS 0.2%
HCL 0.1%