Jinwoo Hong ddbb194585 feat(mobile): page-side RpcClient over the web shell bridge (OTA phase C, C0.4) (#21467)
* feat(mobile): RN bridge host for the web shell page (OTA phase C, C0.3)

One page document's end of the bridge: page frames in through the C0.1
reader, one RpcClient behind it, host frames out. Requests forward with the
arity the page used and answer with the verbatim RpcResponse, chunked when it
is over the frame cap; a rejection crosses as the five-field capture instead.
Subscriptions carry a seq and an unacked window, and end with `overflow`
rather than dropping frames a reader cannot see are missing.

The fence is structural: the protocol names no host, so the client is
whichever this host was built with, and the in-flight caps the page is told
about in `init` are enforced here rather than trusted from there.

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

* feat(mobile): wire the bridge host to B4's hybrid shell screen (OTA phase C, C0.3)

The channel opens on the session B4 put on screen and closes with it. The
session id is B4's: nothing new is minted, and a remount is a new one, which
is what makes a dead page's frames fail the native origin check.

Both halves are stamped with the session they belong to, because React swaps
refs during the commit and runs the retiring effect's cleanup after it — a
host disposing on a remount would otherwise post its teardown into the page
that replaced it. `bridgeEnabled` is derived from the session step alone,
since the native side treats a prop change as a reload.

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

* test(mobile): prove the bridge fence holds for traffic, not just for answers

A mutation that dropped the post-teardown guard in `receive` survived: the
teardown case only fed a frame whose answer the outbound guard already
swallowed, so nothing observed that a dead page could still reach a live
client. Both teardown paths now feed a request, a subscribe and a notify,
and assert the client saw none of them.

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

* test(mobile): read the hook's frames through the page's own reader

`JSON.parse` returns `any`, and taming it with an assertion is a cast the
gate refuses and a check nobody gets. Reading each posted frame through
`readBridgeHostMessage` types it and proves the same thing the host's own
suite does: a frame the page would refuse is a frame that never arrives.

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

* chore(mobile): list the bridge host as a raw request port owner

The boundary ratchet reads a `.sendRequest` access as a call site, and the
host has three: one per arity the page can use. It is not a call site. It
picks no method, reads no reply and decides no acceptance — the page names
the method and runs the typed operation over the client this carries, which
is what the C0 design put page-side so `runRpcOperation` stays unchanged
there. That makes it an owner, beside the socket and relay senders, not a
migration backlog entry.

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

* feat(mobile): page-side RpcClient over the web shell bridge (OTA phase C, C0.4)

Every member of the native contract, carried over the C0.1 envelope so the
screens above it cannot tell a bridge from a socket: requests keep the arity the
caller used, a host RpcFailure resolves as data while a rejection is rebuilt with
its class and its delivery-unknown mark, subscriptions stream with periodic acks,
and the synchronous getters read a cache primed by init rather than answering
before they know.

A state whose generation went backwards is refused and re-asked for, because a
shell rebuilt under the page makes what the page holds the newer of the two.
close settles what the page owns and never touches the shell's client, which the
native screens and the host catalog still share.

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

* test(mobile): run the page client against the shell host over an in-memory port pair

One FIFO per direction and delivery on a microtask, which is what C0.5's golden
replay needs: a subscribe that overtook a sendRequest would move the recorder's
shared ordinal, and anything stronger than a microtask moves a virtual
millisecond. Every member round-trips through the real host over a fake client;
the frame-level suite covers what no pair can reach, including the handshake
backoff, refusals and the binary lane C6 will fill.

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

* chore(mobile): list the page bridge client as a raw request port owner

Both ends of the bridge hold the port as a transport: one forwards raw requests
and the other offers them, and neither picks a method or reads a reply.

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

* refactor(mobile): drop the assembler discard no abandoned request can reach

A request is only abandoned when its frame never left the page, so the shell was
never told the id and no part can have arrived under it. Says what actually keeps
an omitted param omitted while it is here: JSON drops an undefined value, so the
spread states the intent rather than producing the result.

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

* test(mobile): close the three gaps a mutation sweep found in the page client

A settled id has to give its assembler slot back, or 64 replies that were cut
short before an error leave the page unable to read the next chunked one. Close
says goodbye once rather than cancelling each stream first. And the read guard is
only observable through a port that ignores its own unsubscribe, which is what
the harness can now be.

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

* test(mobile): prove a stream that overflows inside subscribe is unsubscribed

A client that emits synchronously from `subscribe` can retire a stream before
its unsubscribe exists to be stored. The identity check that calls it instead
had no test; deleting it left the suite green while the client's stream leaked.

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

* fix(mobile): hand the bridge host over in the commit, not after it

A client swap that keeps the session id leaves the handler's own fence inert:
until the passive effect ran, a native frame reached the retiring host and the
client it closed over. A layout effect swaps both inside the commit.

Teardown on unmount now runs while the view is still attached, so a pending
request is answered delivery-unknown instead of being dropped on the floor.

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

* fix(mobile): hold a refused page frame to one warning per page

A page that sends one bad frame usually sends many, and a line each buries the
first — the one that says why. Same bound the host already keeps on a failing
post, applied per kind and reset when a new page gets a new host.

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

* fix(mobile): bound the page's terminal viewport at the bridge contract

A viewport crossing the bridge is written into the cached subscribe params of
every stream naming that terminal, including the native terminal screen's, and
the desktop refuses cols over 1000 or rows over 500 when those streams
resubscribe. Unbounded, one page could kill streams it never opened; the frame
is refused instead, and the bound is pinned to the desktop's own.

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

* fix(mobile): stop the page's close from latching the bridge host shut

One view carries every document the shell loads, so the page that says `close`
is not the last one. A latched host dropped the next document's `ready` in
silence, and a page that re-sends `ready` on a backoff would retry forever with
nothing posted and nothing logged. Close now cancels what the page owned and
leaves the host live; only dispose shuts it, and a frame arriving after that is
diagnosed rather than dropped.

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

* fix(mobile): keep a throwing client or post inside the bridge host

The `state` frame is sent from inside the client's own state-change fan-out and
a notify runs on the native event handler that delivered the page's frame, so a
synchronous throw from either escapes into a loop the bridge does not own and
takes unrelated listeners with it. Both are fenced and reported once.

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

* test(mobile): prove an ack releases the stream's unacked bytes

The frame window reopens on ack through the splice, so deleting the byte
release left every existing test green while a long-lived stream of large
frames would end with overflow on its first frame after an ack.

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

* fix(mobile): pass the commit-window harness its children as a prop

`createElement`'s variadic children do not satisfy a props type that declares
`children`, so the file dropped out of the tests typecheck ratchet.

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

* fix(mobile): render the harness from the commit-window wrapper, not as children

A props type that declares `children` is what `createElement`'s variadic form
does not satisfy, and passing it as a prop instead trips the react rule. The
wrapper renders the harness itself, which is the parent position the layout
effect ordering needs anyway.

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

* fix(mobile): pin the desktop viewport bound by reading it, not importing it

Mobile may not pull an rpc-contract *value* into its bundle, and the boundary
test that enforces that scans this test file too. The pin reads the schema's
own source instead, so drift in either bound still fails loudly.

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

* fix(mobile): settle a refused subscribe as the stream it was

The shell answers a refused `subscribe` with `error` on the stream's id.
Routing that to the pending requests dropped it, because no request is
open under that id: the page heard nothing and kept the slot forever.

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

* fix(mobile): report a reply or an error the page has no id for

Silently dropped before. Nothing recovers it in place, but a frame the
page cannot place means the two ledgers disagree, which is worth a line.

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

* fix(mobile): say disconnected on close instead of going silent

Every native client publishes the transition and keeps answering its last
snapshot; the screens read both. The page's client cleared the cache
instead, so a closing page left its listeners on a dot that never moved
and every getter throwing underneath it.

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

* fix(mobile): let a closed page client go inert, not throw

An unmounting screen still calls, and nothing on a teardown path catches.
Subscribe hands back a no-op dispose and the notifies do nothing, as the
native client's do, and a request rejects rather than throwing past the
caller's catch. A call before init still throws: that one is a bug.

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

* refactor(mobile): lift the init handshake out of the page client

The backoff that asks the shell for a session is its own concern, and the
client had grown past the file's line budget holding it.

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

* test(mobile): pin the cancel a page owes for a stream already ended

A screen unmounts on its own schedule, routinely after the shell gave up
on the stream. Only the double-dispose order was covered.

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

* docs(mobile): state what the page client does after close

The doc gave the pre-init rule and stopped; the after-close rule is the
opposite one, and subscription failures have no channel but a diagnostic.

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

* feat(mobile): read the shell's page channel as a client transport

The document-start installer leaves `postMessage` and one `onmessage`
slot, the intersection of what the two platforms inject. A page opened
outside the shell has no global at all, so reading it answers null rather
than throwing: the bundle still has to open in a browser.

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

* feat(mobile): give the page its bridge client instead of a placeholder

The web provider now builds BridgeRpcClient over the shell channel and
mounts nothing until `init` lands: every member throws before a session,
and a screen that rendered first would record its first frame against a
client that has none. Outside the shell there is no session coming, so
the placeholder stays and the route tree mounts at once, which is what
the Route A render check exercises.

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

* fix(mobile): declare the page provider test's probe instead of casting it

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

* fix(mobile): serve the bridge to one document at a time

A page's `close` now ends that document's turn: until the next `ready`
claims the view, every other frame is dropped and diagnosed instead of
reaching the client, and nothing is posted. Without the fence a straggler
from the closed document was still forwarded, and a `state` frame from the
still-running client landed in the replacement document before its `init`.

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

* fix(mobile): hold the request cap against the calls, not the page's ledger

`sendRequest` has no cancel, so a request the page cancelled or closed out
keeps running on the desktop until it answers. The cap now counts those
calls until each settles; counting the pending map let a page interleaving
`close` with batches hold many more than the cap `init` advertises.

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

* test(mobile): pin the ack ratio to the shell's window, not a copy of it

The ack interval test held 256 and 4 MiB as literals, so narrowing the
shell's window would have left the page acking too late with the test still
green. The comment naming the test that pins the ratio pointed at the wrong
file.

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

* fix(mobile): give back the slot of a subscribe that never left the page

A post that threw left the stream in the page's ledger with nothing open on
the shell's side, so 32 of them exhausted the subscription budget for the
life of the document. The slot goes back and the listener hears a terminal
error result, which is what the native client does with the same failure.

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

* fix(mobile): end a page stream through its listener, not only the log

A stream the shell ends or fails now reaches its listener as a terminal
error result, the way the native client's emitError does. A consumer reads
that result: host-worktree-refresh clears the flag that says the event
stream is live, and without it the worktree list stops updating for the life
of the document. A dispose the page asked for stays silent.

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

* fix(mobile): settle the old shell's work before adopting a new session

A second `init` naming a different sessionId is a rebuilt host with empty
tables: every pending request and every open stream the page still held
belonged to the shell that is gone. They now settle delivery-unknown and end
through their listeners before the new session is adopted. A second `init`
for the same session is what a re-asked `ready` earns, and keeps everything.

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

* fix(mobile): take the page's streams out of the ledger before failing them

A listener that resubscribes while the old shell's streams are being ended
is opening one against the shell that is arriving; draining the map first is
what keeps this loop from tearing that one down too. Fixes the lint the
previous commit left behind.

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

* docs(mobile): say how long a reply assembler's refusal actually lives

The tombstone is not kept forever: the request ledger discards the id as it
settles the caller, so it normally outlives only the rest of the reply that
raised it. The bounded map is there for the ids nothing settles.

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

* test(mobile): pin the ceiling the ready backoff stops widening at

An unclamped backoff reads the same for the first minute and then leaves a
page asking once an hour into a shell that is still booting behind it.

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

* docs(mobile): say why the document fence carries no epoch

Page frames reach the shell through one native listener per platform, so a
straggler from the closed document lands before the next document's `ready`
and the flag alone catches it. An echoed epoch would be a wire change for
nothing.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-18 10:51:38 -04:00
2026-09-18 12:35:55 +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.1%
JavaScript 4.1%
Swift 0.2%
CSS 0.2%
HCL 0.1%