Skip to content

Integrate reviewed mobile steering, browser, Chinese and trust features - #2208

Merged
milind-soni merged 54 commits into
mainfrom
codex/oct3-mobile-pr-integration
Oct 3, 2026
Merged

milind-soni merged 54 commits into
mainfrom
codex/oct3-mobile-pr-integration

Conversation

@milind-soni

@milind-soni milind-soni commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Scope

Author-preserving integration of the requested conflicting PRs:

Source Included work
#750 Native queued-send steering and exact-thread receipts
#1644 Native browser watching/control, independent device grant, bounded transport and gestures
#1719 Simplified/Traditional Chinese across desktop, iOS and Android
#903 / #933 Reviewed Activity, team memory, outbound confirmations/app scopes and ordered startup-only backups

#2164 (empty digest leak), #1876 (Jump to latest), and #2189 (iOS responsiveness) are already on main; this preserves them rather than duplicating them. #933 was merged into #903's feature branch, not main. This also preserves the newer main fixes for expired mobile proposals and workspace restore. No dependencies, version bump, release, deployment or protection changes.

Review repairs

  • Browser control stays separate from computer access, defaults off for old devices, rechecks revocation after awaits, releases the exact viewer on close/background, and uses bounded frames, requests and input queues. Desktop Settings exposes the existing grant through owner-only IPC.
  • Steering stays on the selected thread. Queued receipts, attachments, Stop and share-extension completion retain their ownership and actual delivery state.
  • Reuse main's opt-in TurnNotStartedError recovery for ordered backups. No replay after accepted prompts, partial output, tools, Stop or a newer user message; bot defaults and sibling threads stay unchanged. Reuse existing account-management routes instead of restoring obsolete generic account APIs.
  • Outbound permissions/capabilities are rechecked immediately before provider dispatch, after session discovery and held approval. Daily allowance is durably reserved before dispatch, with persistence failures refused. Every batch send has its own bounded argument preview. Existing per-tool grants and Full Access semantics remain intact.
  • All bot-proposed team-memory additions/replacements require admin review; explicit admin additions are immediately accepted. Restricted conversations cannot publish section-wide facts. Both card-answer routes enforce the same admin boundary as memory editing.
  • Memory writes roll back on failure, unreadable files are not overwritten, ordinary prototype-like team names survive restart, and Activity keeps separate runs when providers reuse item IDs across turns. Pending UI writes are serialized and stale-host completions cannot overwrite/unpair the new host.

Validation

All server/conversation and renderer checks use isolated fake-engine homes and synthetic HTTP providers. No user data, accounts, paid model calls, microphone or physical phones were used.

  • Final server coverage: 1,331 selected checks (including a fresh full 278-test API run) and 71 actual isolated workflow tests covering browser, steering, permission, memory, fallback and visibility; actual HTTP revoke/expiry/cap races and approval-to-idle/Stop workflows.
  • Real disposable desktop renderer: Activity refresh, intersecting app/tool grants, outbound settings, backup persistence, serialized memory CRUD and independent browser grant.
  • Real disposable iOS simulator: browser watch/take/type/release/background/reconnect and team-memory edit/stale-computer races. Generic app/extensions compile at the existing iOS 16 floor.
  • Combined Android: 758 core + 1,127 app tests, Preview/Debug builds, synthetic Compose/HTTP browser lifecycle, both Chinese locales, expired-proposal safety, signatures/IDs/labels.
  • Final combined iOS: 851 Swift tests, unsigned simulator app + Share + Widget extensions build. The existing iOS 16 app floor and responsiveness batching remain intact.
  • Final combined desktop: 217 state/browser/grant checks, 69 locale/phone checks, 541 Electron checks (3 existing platform skips), and the actual mounted trust-controls renderer workflow pass.
  • Final production UI build, full typecheck, lint, 10 locale catalogs (3,467 English strings), 146 Electron module syntax checks, 59 CI/verification-contract tests, 68 offline eval tests, six offline scenarios, golden replay, and the standalone packaged-server smoke pass.
  • Three integration-test repairs retain production boundaries: the relay fixture explicitly allows bounded sends but holds/denies opaque requests; the Android lifecycle fixture waits for an enabled takeover button and forces the connecting window (five repeated runs plus the full app suite pass); an explicit eval assertion checks launch instructions or the exact leading Claude volatile-update envelope, never plain user text. Existing strict system-prompt assertions remain unchanged. The adapter/scorer and Android scheduling counterproofs fail before their respective repairs.

Complete exact-head CI is still required before merge. Two confirmed fixture failures and an invalid scorer assumption are repaired without disabling tests, weakening permissions, or changing the reviewed app implementation. The exact CI eval schedule was not reproduced locally; its instruction pin still fails if the text is absent from both delivery surfaces, with full evidence in the failure. Physical-device audio/gesture behavior and mobile store publication are not claimed.

Merge

Use a normal merge commit, not squash, to preserve the source authors' commits. Auto-merge stays off until the complete exact-head CI and selected native, docs, packaging and smoke checks pass. Close unchanged open source PRs only after incorporation is actually on main; do not label those originals separately GitHub-merged.

aivsomkar and others added 30 commits September 4, 2026 00:05
A message sent to a busy bot from the phone could simply disappear. The
harness answers such a send in one of three ways, and all three live only
in the POST's 202 body: the line landed, the engine took it INTO the
running turn (`steered`), or the harness is holding it off-transcript
until the turn settles (`queued`, with a `queueId`). The iOS client threw
that body away. A held message therefore left no trace anywhere — the
composer cleared and the words reappeared minutes later, or not at all
after a restart. Only the Claude driver can steer, so on every other
engine the queue is the only path and mid-turn sending looked broken.

Read the receipt, and give the phone what the desktop has had:

- `SendReceipt` decodes the 202 leniently — an older harness that answers
  `{ok:true}` is a plain send, not a failure.
- `CompanionState.pendingQueued` holds what the harness is holding, keyed
  by queueId. A drained line names its entry, so the ghost retires when
  the real bubble lands; a drain that beats its own POST leaves a bounded
  tombstone so the late receipt cannot resurrect it. Hydrates and page
  merges reconcile for a window that slept through the drain.
- The chat draws held sends as dashed bubbles below the transcript, each
  with its own cancel (DELETE …/queue/:id, newly allowed through the
  sidecar for bots and rooms).
- The composer says which of the three will happen, and the send key
  becomes a clock when the message will wait. With something already
  waiting, the microphone becomes a green inject: the harness keeps its
  steer queue across an interrupt, so stopping the turn is what makes
  those words run now. Send stays available throughout.
- A line the engine took mid-turn is marked "sent mid-turn", which is why
  the reply above it can read as if the bot had not seen it.

Inject is bot-only: the phone has no way to interrupt a room, and adding
one is a wider change than this bug. Rooms still get the ghost and cancel.

The steer e2e test now asserts the two fields a client needs from a
queued receipt, and that the drained line carries the same queueId.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidecar answers 404 "no route: DELETE …" for a route it does not
allow, and the harness answers 404 "no such queued message" when the
entry is already gone. Treating those alike took the held message off the
phone while the computer still intended to run it — and it then arrived
anyway, which is worse than the vanishing this branch set out to fix.
Any computer whose sidecar predates the queue-cancel allowlist hits this,
including the currently released one.

Match the harness's own wording positively, so an unfamiliar 404 fails
safe: the ghost stays and the person is told the computer needs updating,
rather than being shown a cancel that did not happen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mid-turn controls were glyphs: a clock on the send key, a green arrow
in the composer. Omkar read them on a real phone and said "don't add the
clock or the lightning, I don't understand" — and the held message itself,
drawn as a dashed outline, he could not see at all on a dark transcript.

So the queued message moves out of the transcript and sits directly above
the chat bar, where a thing you have not said yet belongs: the text, a
labelled `↳ Steer` button, and a bin. Steer stops the turn so those words
run now — the harness deliberately keeps its queue across an interrupt,
which is what makes stopping a send. The send key goes back to being one
arrow with one meaning; what will happen to the message is said in the
placeholder instead ("Sends into this turn" / "Sends after this turn").

Both platforms, because the desktop had the same puzzle: its green
"inject now" arrow is gone and its queued rows gain the same Steer button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tapping Steer did nothing visible. Measured against a live codex turn, an
interrupt takes about 6.3 seconds to actually stop the stream — and for
those six seconds the button looked untouched while the bot kept talking,
which is indistinguishable from a broken button. Omkar read it as "it did
not steer at all", and he was right to.

The button now goes to "Steering…" and disables the moment it is pressed,
on both platforms. It clears when the queue drains OR when the turn ends —
both, so an interrupt that fails to stop the turn cannot leave the row
spinning for ever either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two faults, seen together on a phone: a codex turn died mid-essay, and
from then on the chat streamed that half-written essay for ever while the
Steer button span for ever beside it.

The queued row was mine. A `bot` frame carrying a whole transcript
replaces `messages[…]` outright instead of appending, so it never runs the
retirement that `append` does — the held message stayed above the chat bar
long after its line had landed. The desktop has guarded this since the
feature shipped (reconcileSnapshotQueues on bot add/patch/task switch); the
port covered hydrate and page-merge and missed this path.

The stream was older. The live bubble was drawn whenever the buffer held
text, gated on nothing, and nothing cleared the buffer when a turn ended
without a settled reply — an engine that dies mid-sentence reports the
failure as activity, not as text. So the buffer kept its last tokens and
the bubble streamed them for ever. Now an idle bot clears the buffer, and
the bubble is gated on `busy` as well, so neither alone can strand it.

Also a floor under the spinner: it clears after twenty seconds even if
neither the drain nor the turn-end frame ever arrives. A control that
spins for ever is worse than one that admits it does not know.

The desktop needs none of this: it gates its working row on bot.busy and
never renders from the stream buffer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It only words the composer. The cursor commit — and with it the whole
stream going live — must not wait on a request that says nothing about the
transcript. Found while porting to Android, where blocking hydrate on it
failed two Session tests outright.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lready exist

A bot's Activity panel: every tool it used and every approval it asked
for, newest first, grouped by day, each row saying what ran ("Gmail ·
Search threads", "Ran a command"), the arguments the broker recorded,
and the outcome — ran, failed, running, allowed, denied, or needs you.

Nothing new is captured. server/activity.ts folds the per-thread runtime
events (what ran, did it finish) with the fleet-wide decision log (what
was asked, with which arguments, who allowed it), attaching an approval
to the tool run it unblocked when the two land within a short window in
the same thread. Connector calls never pass through the harness — the
agent CLI talks to Composio's MCP endpoint directly — which is why this
reads logs rather than tapping a proxy that does not exist.

GET /api/bots/:id/activity is read-only over the bot's own threads (its
DM and every task) plus the decision rows that name it. The panel sits
beside the inspector and the computer panel, opens from a header button,
re-reads when a turn settles or a card is answered, and stays available
on remote clients since it is the user-facing view. Mobile gets the same
API; a screen there is a follow-up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… allowance

A connector call that sends something — an email, a message, a post, an
invite, a refund — is its own confirmation in every approval mode, Full
access included. The relay every Composio call already passes through
holds such a call on a "Send on your behalf?" card and waits for the
answer; Deny (or nine minutes of silence) comes back to the bot as an
ordinary tool error it can read and act on, never a transport failure it
retries. A bot can instead be given a daily allowance, counted per local
day on disk, after which the relay refuses with a message naming the
limit. Every branch lands in the decision log under source "outbound",
so the Activity panel shows it.

shared/outbound.ts is the one definition of "outbound": the action part
of a tool name, whether Composio's TOOLKIT_ACTION or a provider's
mcp__server__action, with a read verb first meaning read and DRAFT
meaning not sent. The auto-approve rules gain the same guard, so Approve
for me and a remembered Always allow never wave a send through on a
provider's own connectors either; explicit Full access on those stays a
documented gap, since no request reaches the broker there.

The bot record carries the policy (ask, the default, or allow with a
cap), validated on PATCH like autoReview, with a read-only route for
today's count. Bot settings get a "Sending on your behalf" control under
the approval level, deliberately outside that selector so nothing
implies Full access covers it. The card is a standard options card, so
the phone apps already render it.

Tests point the fixture's Composio Session MCP URL at the local stub,
which needed one honest change: a backend the operator configured
explicitly may hand back a Session on its own origin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Which connected apps a bot may use, and whether it may only read them.
Absent means what it has always meant — every connected app, read and
write — so nothing changes for a bot nobody has scoped. Present means a
list: an app not on it is off for this bot, "read" refuses anything
that is not a read, and a batch that mixes an allowed read with a
forbidden write is refused whole rather than half-run.

Enforced at the relay every Composio call already passes through, seeing
through the session's meta tools the same way the outbound gate does, and
checked before that gate: an app the bot may not touch is refused
outright, never turned into a question for the person. A connection
request for an app outside the bot's list is refused before any card
appears. Every refusal lands in the decision log under source
"connector-scope", so the Activity panel shows it, and the bot's prompt
names what it may use so it does not spend a turn finding out.

Bot settings gain a "Limit this bot to specific apps" switch inside the
Connected apps card, with Off / Read / Read & write per connected app;
an app the bot was scoped to that is no longer connected still shows,
so a grant never silently disappears. null on the wire clears the scopes,
normalized to an absent field the way `computer` already is.

shared/connector-scopes.ts stands alone rather than importing from
outbound.ts: shared/ is compiled by both the server (which needs .ts
import extensions) and the app (which forbids them).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…chain

A second account on Claude or Codex is another instance of its driver
with its own login directory (CLAUDE_CONFIG_DIR / CODEX_HOME under the
data dir), so two Max plans sit side by side. POST /api/instances adds
one beside the default fleet, inheriting the base engine's CLI override
since a versioned binary is about the machine, not the account; DELETE
removes only a profile, never a default-fleet instance. The instances
section of config.json merges per id, so a removal is named explicitly
to leave the file. App Settings → Engines gains "Add another account"
and a Remove button on profile rows; the profile signs in from its own
row like any engine.

A bot's fallback chain says where a task carries on when its engine
hits a usage limit, is rate limited, or cannot be reached. The
turn.completed fold reads why the turn died from the thread's last
runtime.error, and if the reason is one of those it moves the bot to
the next available entry, posts a "switched to …" chip, and starts a
control-plane continuation turn; the transcript replays into the new
engine because it is fresh, and the person's message appears once. Hops
are counted per thread so a chain whose every engine is down stops
after one pass; a successful turn resets it. A turn the person stopped
is never routed around. Bot settings → Model gains the ordered list.

A subscription's usage limit is now classified as quota, not a 429 to
retry through: the window is hours away, and the chain is the answer.

The end-to-end test boots an isolated harness whose fixture Claude is
scripted to die on a 429, adds a profile, chains it, sends one message,
and watches the task finish on the second account.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… section shares

The layer between a bot's private MEMORY.md and the person's own section
brief: bot-fed, person-reviewed, read by all. A bot that learns who
someone is, where a document lives, what was decided, or what a term or
nickname means proposes it with propose_team_memory. A place or a term
lands at once, because the cost of a wrong one is a wasted lookup; a
person or a decision waits for a "Remember this for the team?" card,
because those shape what every teammate does next. Proposing an existing
name again updates it rather than adding a twin. Every accepted entry
rides into every section bot's prompt as a compact block under a byte
budget, newest kept when it has to cut, with decisions dated — which is
what lets a Chief of Staff answer "what happened today" without a
digest routine.

The Team map page gains a Memory dialog per section beside the shared
context: what is waiting for a tap, then people, places, decisions and
terms with inline editing, removal, and a way to add an entry by hand
(the person's own entries never wait on the person). The card is a
standard options card, so the phone apps render it already. Proposals
and answers land in the decision log under source "team-memory".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s they call

The phone twins of the desktop's Activity panel and Team map → Memory.
From a bot's profile: "Activity" lists what the bot did, newest first by
day, each row with the app and action in words, the recorded arguments,
and an outcome chip (ran, failed, running, allowed, denied, needs you).
"Team memory" lists the bot's section's shared people, places, decisions
and terms, answers proposals waiting for a tap (Remember / Skip), removes
entries with a swipe, and adds one by hand. Both are content surfaces;
the sending policy, app scopes and fallback chain stay on the desktop,
where the companion allowlist has always kept execution policy.

The sidecar allowlist gains GET /api/bots/:id/activity and GET/POST/
PATCH/DELETE /api/team-memory, pinned in routes.test.ts. Two fixtures
captured from a disposable harness pin the wire shapes for both phone
suites: bot-activity.json (an empty page — a fixture harness runs no
tools) and team-memory.json (one entry of each kind, added by hand).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A pure GestureCore in each shared module turns normalised touch samples
into abstract intents, so iOS and Android cannot drift and phase 2's VNC
surface becomes a second sink rather than a second gesture layer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve TDD tasks: the iOS core in full, the Kotlin port, and a shared
parity fixture that fails the build when the two disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ViewportMapping folds letterboxing, zoom and pan into one place, so a
coordinate can only be got wrong once and is tested once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A sequence continues only while both the 450ms gap and the 0.02 slop
hold, and wraps at three: a quadruple click means nothing to a browser.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tick(at:) is how a duration-based gesture stays testable in a core with
no clock. A cancelled drag still releases, so the remote is never left
holding a button nothing will lift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The finger is a rate control, not a position: the core owns the cursor
and emits absolute coordinates, so both modes share one sink.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zoom stays local so a small target becomes reachable without reflowing
the remote page. A scroll or a fired hold suppresses the click on lift,
so a flick through links cannot open one.

Records in the plan that parity must compare floats with tolerance:
normalising through a division yields -0.09999999999999998 and -0.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
driving is off by default, so a caller that forgets to set it fails safe
rather than handing the remote away. flush() releases what is held and
abandons momentum on release, backgrounding and disconnection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eight cases across both modes in one JSON file the Kotlin runner will
read verbatim. GestureIntent gets hand-written coding because Swift's
synthesised enum shape nests under _0, which no Kotlin decoder reads.

Verified the gate fails: widening multiClickWindow to 0.6 breaks the
"tap after the window" case by name and step index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors CompanionCore's RemoteGestures, including the intent wire shape,
so one parity fixture can serve both platforms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Click sequencing, long press and drags, trackpad cursor and
acceleration, scroll and momentum, local zoom, driving gate and flush —
31 tests against the same behaviours the iOS suite asserts.

Tasks 9-11 of the plan landed as one cycle: it is one file and one port,
and Task 12's fixture is the gate that actually proves parity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No copy task needed: core/build.gradle.kts already points the test
source set at ios/Tests/CompanionCoreTests/Fixtures, so one fixture
directory already serves both platforms.

Verifying the gate can fail caught a hole in the fixture itself. The
long-press case ticked 0.4 then 0.5 and compared only the accumulated
output, so a 300ms threshold produced the same three intents as 500ms
and the sabotage passed. Added a case that stops short of the threshold
and asserts nothing happens; the sabotage now fails by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
browserControlAccess mirrors cloudDesktopAccess exactly: off on every new
and migrated device, granted only from the loopback control page,
revocation tears down live streams.

It is a separate grant on purpose. A cloud desktop is a disposable VM; a
bot's browser is normally signed into the person's real accounts, so a
device trusted with the first must not inherit the second.

No harness change was needed: request-auth re-runs the sidecar's own
denyReason, so allowlisting the two routes opened both sides at once.
Corrected the design doc, which claimed otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sink is the only place normalised intents become device pixels, and
it routes an unmodified single character to char rather than a raw
keyDown the server would hold forever.

The queue ports src/lib/browser-input-queue.ts: one request in flight,
movement coalesced, wheel deltas summed, a 32-item ceiling that halts
rather than banking input, and releases that survive the halt.

settle() and drain() are deliberately different: drain abandons stale
travel for hand-back, settle waits for everything. Writing the tests is
what surfaced that they cannot be one method.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports CompanionCore's sink and queue with the same assertions: pixel
denormalisation, the char-not-keyDown rule for unmodified characters,
movement coalescing, summed wheel deltas, the ceiling, and releases that
survive a halt.

A mutex stands in for the actor; the launch happens under the lock so
two callers cannot both start a pump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SSE frame stream plus the action channel on both platforms. The decoder
drops an unknown message type rather than tearing down a stream someone
is watching, and refuses a frame with no metadata, which cannot be
mapped to coordinates anyway.

Byte-at-a-time SSE parsing on iOS for the reason SSE.swift documents:
AsyncLineSequence folds blank lines, which are what end an event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The adapter makes no decisions: raw touches become TouchSamples and a
CADisplayLink drives the core's clock so long press and momentum work.
A second finger flushes the tracked touch rather than half-tracking a
pinch.

The screen adds the modifier bar the design calls load-bearing, a
locally drawn cursor for trackpad mode, a hidden field that turns soft
keyboard text into char events, and take/release with a flush on
backgrounding. The 429 from a full viewer table gets a real sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the iOS side: a pointerInput adapter that only produces
TouchSamples, a frame-rate loop driving the core's clock, the modifier
bar, a locally drawn cursor, and take/release with a drain on leave.

The transport hangs off CompanionClient so it inherits the endpoint,
scoped-IPv6 DNS and streaming timeouts rather than keeping a second copy
of that setup.

The Browser destination encodes and decodes with the rest of the stack,
so restoring after process death does not silently drop the screen. Its
entry point sits outside the cloud-desktop gate: any bot with a browser
can be driven, not only cloud ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dreamfarer-space and others added 18 commits September 26, 2026 17:34
- Add Simplified and Traditional Chinese translations across desktop, iOS, and Android
- Integrate with companion mobile apps (Android Compose strings and iOS xcstrings)
- Consolidate Android review resources into base and localized strings.xml
- Fix BotThreadRow Compose status coloring and preserve upstream snooze logic
- Update source-hashes.json and validate catalog completeness via generate-locale.mjs
Resolves the locale catalog conflicts:
- zh.json / zh-tw.json: drop the retired onboarding.reel.apps.* and Box keys,
  keep upstream's Box-to-Boat copy updates, and translate the 47 new English
  strings (OMB Cloud account, interface font, automatic recovery, /setup).
- source-hashes.json: rebuild the zh and zh-tw acceptance hashes against the
  merged English catalog; other locales keep upstream's records.

node scripts/generate-locale.mjs --check passes (zh/zh-tw 2351/2722).
…heet

- mobile_settings_notifications_footer said "while OpenMausMobile is
  connected" while OnboardingCopy.NOTIFICATIONS_BODY renders
  SettingsPolicy.NOTIFICATIONS_FOOTER ("while MausBot is connected"):
  OnboardingRoutingTest could not find the body text on screen, and the two
  surfaces showed different copy. The English resource now matches the
  constant byte for byte, with the Chinese catalogs following.
- Route the delete/rename/snooze error text through localizedMobileCopy so
  the "Couldn't update this thread. Try again." fallback reaches the Chinese
  catalogs (CodeRabbit review).
- Map "Rename thread" so the rename dialog heading translates (CodeRabbit
  review).
- Give the snooze dialog's title, presets, "Stop snoozing", the working
  notice and Cancel real string resources with Simplified and Traditional
  Chinese.

English resource values stay byte-identical to the pinned constants and test
copy ("Rename thread", "Until 6 PM", "Until new activity",
"Stop this thread before snoozing it.", "Cancel").
Against the English source, eleven Simplified keys (and their Traditional
twins, translated in the same pass) read wrong or dropped content:

- installation rendered as "workspace" (voice.grok.sharedKey,
  settings.section.workspaces, settings.profiles.ownerOnly)
- Servers / Installations section titles mistranslated
  (settings.section.desktopWorkspaces, workspaces.title)
- bots mistranslated as "items" (team.moveSelected)
- "Task folder" rendered as a workspace (composer.tray.privateWorkspace)
- "Member (chat only)" gained an approval right it does not have
  (workspaces.chatOnly)
- Threads mistranslated as a UI thread (backup.threads)
- verb "Draft" used as a noun (botAccess.grants.verb.draft)
- organization.disconnectWarning dropped the daily-backups sentence

Terminology now follows each catalog's own established usage.
One glossary, applied everywhere the Chinese catalogs mix terms:

- thread (and conversation) -> 对话 / 對話 (was 线程, 讨论串, 对话线程,
  執行緒, 對話串 across the catalogs)
- installation(s) -> 安装实例 / 安裝實例 (was mistranslated as 工作区)
- workspace stays 工作区 / 工作區 where the English says workspace
- default -> 默认 / 預設 (zh 缺省 merged into 默认)
- "bot folders" -> 机器人文件夹 / 機器人資料夾 (was 工作区)
- honorifics -> 你 (您 normalized away)
- quotes: "..." in Simplified, 「...」 in Traditional
- stray Traditional vocabulary in the Simplified catalog normalized
  (纪录->记录, 帐户->账户, 设置档->配置, 软体->软件, 网路->网络)

186 zh keys, 85 zh-tw keys, 52 Android string entries and 2 iOS
xcstrings entries changed. English sources, placeholders and source
hashes are untouched; node scripts/generate-locale.mjs --check passes.
…s they call (#933)

The phone twins of the desktop's Activity panel and Team map → Memory.
From a bot's profile: "Activity" lists what the bot did, newest first by
day, each row with the app and action in words, the recorded arguments,
and an outcome chip (ran, failed, running, allowed, denied, needs you).
"Team memory" lists the bot's section's shared people, places, decisions
and terms, answers proposals waiting for a tap (Remember / Skip), removes
entries with a swipe, and adds one by hand. Both are content surfaces;
the sending policy, app scopes and fallback chain stay on the desktop,
where the companion allowlist has always kept execution policy.

The sidecar allowlist gains GET /api/bots/:id/activity and GET/POST/
PATCH/DELETE /api/team-memory, pinned in routes.test.ts. Two fixtures
captured from a disposable harness pin the wire shapes for both phone
suites: bot-activity.json (an empty page — a fixture harness runs no
tools) and team-memory.json (one entry of each kind, added by hand).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openmausbot-docs Ready Ready Preview Oct 3, 2026 10:58am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 3, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0f9c769b-132a-499d-8a49-e3077cf83299
📥 Commits

Reviewing files that changed from the base of the PR and between b86e4cf and 0f99881.

📒 Files selected for processing (182)
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/AgentProfileSheet.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/AttentionInbox.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/BotOverviewScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/BotThreadRow.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/BotThreadTree.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/BrowserControlScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/BrowserTouchSurface.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/ChatScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/Chrome.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/CommandHud.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/ComposerAttachments.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/ComputerScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/ConnectedAppsScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/FilePreview.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/LocalizedCopy.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/LocalizedSemantics.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/MessageRow.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/Navigation.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/NewGroupSheet.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/NewSectionSheet.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/OnboardingScreens.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/PairingScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/QrScannerScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/QuestionCard.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/QueuedSendRow.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/QuickRepliesEditor.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/RootScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/RosterScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/RoutineEditorSheet.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/SettingsScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/ShareSheet.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/TaskSheet.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/TasksRoutinesScreen.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/TranscriptCardViews.kt
  • android/app/src/main/kotlin/com/openmausbot/companion/ui/UpdatesSheet.kt
  • android/app/src/main/res/values-b+zh+Hans/strings.xml
  • android/app/src/main/res/values-b+zh+Hant/strings.xml
  • android/app/src/main/res/values/strings.xml
  • android/app/src/main/res/xml/locales_config.xml
  • android/app/src/preview/res/values/strings.xml
  • android/app/src/test/kotlin/com/openmausbot/companion/ui/BrowserControlWiringTest.kt
  • android/app/src/test/kotlin/com/openmausbot/companion/ui/ChineseLocalizationTest.kt
  • android/app/src/test/kotlin/com/openmausbot/companion/ui/PhoneSectionCopyTest.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/BrowserInputQueue.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/BrowserLiveClient.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/BrowserLiveSink.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/Client.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/RemoteGestures.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/Session.kt
  • android/core/src/main/kotlin/com/openmausbot/companion/core/Sse.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/BrowserInputQueueTest.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/BrowserLiveDecoderTest.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/BrowserLiveSinkTest.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/BrowserLiveTransportTest.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/RemoteGestureCoreTest.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/RemoteGestureMappingTest.kt
  • android/core/src/test/kotlin/com/openmausbot/companion/core/RemoteGestureParityTest.kt
  • companion/src/control.ts
  • companion/src/devices.ts
  • companion/src/proxy.ts
  • companion/src/routes.ts
  • companion/src/wire.ts
  • companion/test/browser-revocation.test.ts
  • companion/test/devices.test.ts
  • companion/test/proxy-response.test.ts
  • companion/test/proxy.test.ts
  • companion/test/relay-auth.test.ts
  • companion/test/routes.test.ts
  • companion/test/upstream-failure.test.ts
  • companion/test/wire.test.ts
  • docs/superpowers/plans/2026-09-18-gesture-core.md
  • docs/superpowers/specs/2026-09-18-mobile-touch-control-design.md
  • docs/verification/browser-live.md
  • electron/companion-browser.node-test.mjs
  • electron/companion.mjs
  • electron/main.mjs
  • electron/preload.cjs
  • electron/preload.node-test.mjs
  • ios/App/AgentProfileView.swift
  • ios/App/AppLanguage.swift
  • ios/App/BotActivityView.swift
  • ios/App/BrowserControlView.swift
  • ios/App/BrowserPreview.swift
  • ios/App/BrowserTouchSurface.swift
  • ios/App/ChatView.swift
  • ios/App/CompanionApp.swift
  • ios/App/ComputerView.swift
  • ios/App/Localizable.xcstrings
  • ios/App/MemoryPreviewProtocol.swift
  • ios/App/Session.swift
  • ios/App/TeamMemoryView.swift
  • ios/ShareExtension/ShareRootView.swift
  • ios/ShareExtension/ShareViewModel.swift
  • ios/Sources/CompanionCore/BrowserInputQueue.swift
  • ios/Sources/CompanionCore/BrowserLiveClient.swift
  • ios/Sources/CompanionCore/BrowserLiveSink.swift
  • ios/Sources/CompanionCore/Client.swift
  • ios/Sources/CompanionCore/Models.swift
  • ios/Sources/CompanionCore/RemoteGestures.swift
  • ios/Sources/CompanionCore/SSE.swift
  • ios/Sources/CompanionCore/Store.swift
  • ios/Tests/CompanionCoreTests/ActivityClientTests.swift
  • ios/Tests/CompanionCoreTests/BrowserInputQueueTests.swift
  • ios/Tests/CompanionCoreTests/BrowserLiveDecoderTests.swift
  • ios/Tests/CompanionCoreTests/BrowserLiveSinkTests.swift
  • ios/Tests/CompanionCoreTests/BrowserLiveTransportTests.swift
  • ios/Tests/CompanionCoreTests/DecodingTests.swift
  • ios/Tests/CompanionCoreTests/Fixtures/bot-activity.json
  • ios/Tests/CompanionCoreTests/Fixtures/gesture-parity.json
  • ios/Tests/CompanionCoreTests/Fixtures/team-memory.json
  • ios/Tests/CompanionCoreTests/QueuedSendTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureClickTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureFlushTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureLongPressTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureMappingTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureParityTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureScrollZoomTests.swift
  • ios/Tests/CompanionCoreTests/RemoteGestureTrackpadTests.swift
  • ios/UITests/BrowserControlUITests.swift
  • ios/UITests/TeamMemoryUITests.swift
  • scripts/capture-companion-fixtures.mjs
  • scripts/testing/trust-controls-ui.e2e.test.ts
  • server/activity.test.ts
  • server/activity.ts
  • server/auto-approve.test.ts
  • server/auto-approve.ts
  • server/automatic-recovery.e2e.test.ts
  • server/composio.test.ts
  • server/composio.ts
  • server/decision-log.ts
  • server/drivers/agents-call.test.ts
  • server/drivers/agents-call.ts
  • server/drivers/agents-catalog-goldens/direct-full.tools-list.json
  • server/drivers/agents-catalog-goldens/profiles.json
  • server/drivers/agents-catalog-goldens/room-full.tools-list.json
  • server/drivers/agents-catalog-wire.test.ts
  • server/drivers/agents-catalog.ts
  • server/drivers/agents-proxy.test.ts
  • server/drivers/retry.test.ts
  • server/drivers/retry.ts
  • server/index.ts
  • server/outbound-counts.test.ts
  • server/outbound-counts.ts
  • server/outbound-requests.test.ts
  • server/outbound-requests.ts
  • server/request-auth.test.ts
  • server/steer-e2e.test.ts
  • server/system-prompt.ts
  • server/team-memory.test.ts
  • server/team-memory.ts
  • server/thread-events.ts
  • server/trust-controls.e2e.test.ts
  • shared/activity.ts
  • shared/connector-scopes.test.ts
  • shared/connector-scopes.ts
  • shared/outbound.test.ts
  • shared/outbound.ts
  • shared/wire.ts
  • src/App.tsx
  • src/components/ActivityPanel.tsx
  • src/components/ChatView.tsx
  • src/components/CompanionSection.reveal.test.ts
  • src/components/CompanionSection.tsx
  • src/components/PhoneSetupFlow.tsx
  • src/components/TeamCanvas.tsx
  • src/components/TeamMapPage.tsx
  • src/components/TeamMemoryDialog.tsx
  • src/components/bot-settings/AccessSection.tsx
  • src/components/bot-settings/ModelSection.tsx
  • src/components/bot-settings/PermissionsSection.tsx
  • src/components/bot-settings/useBotSettingsDerived.ts
  • src/lib/activity.test.ts
  • src/lib/activity.ts
  • src/locales/en.json
  • src/locales/source-hashes.json
  • src/locales/zh-tw.json
  • src/locales/zh.json
  • src/state/bot-patch-queue.test.ts
  • src/state/bot-patch-queue.ts
  • src/state/store.test.ts
  • src/state/store.tsx
 __________________________________________________
< My OKRs are all about finding bugs in your code. >
 --------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@milind-soni
milind-soni merged commit d7e5463 into main Oct 3, 2026
21 of 27 checks passed
milind-soni added a commit that referenced this pull request Oct 4, 2026
…the next one (#2270)

* fix(ios): make the iOS 16 shims ViewModifiers so a chain of them does not blow up IRGen

The "Swift tests + iOS build" job went from 5 minutes (b455e0f) to 26
(4302a9c) and then past its 30-minute timeout on every main run since
20:55 UTC Oct 3, which marked main's run "cancelled" and hid its verdict.
The log stopped for 27 minutes inside the x86_64 batch "ActivityRunChip
… CompactRoster".

That batch is not a slow expression. The stuck swift-frontend, sampled,
sits in IRGen emitting an outlined destroy for one view type:
IRGenSILFunction::emitSILFunction → StructTypeInfoBase<NonFixed…>::destroy
→ callOutlinedDestroy → MultiPayloadEnumImplStrategy::destroy →
forNontrivialPayloads → … forty frames deep. Every compat shim in
BackDeployCompat.swift was a `@ViewBuilder` extension on View that
branched on `#available`, so each call returned a `_ConditionalContent`
whose payloads each wrap the whole view it was applied to: `onValueChange`
has three branches and triples the caller's type, the others double it.
ChatView.body chains 19 of them (16 before #2208), so its type is
~3^19 the size of the view it describes.

The compiler only sees that type when this file and the caller are
primaries of the same compile batch: then the opaque `some View` is
looked through and the enum is expanded. A 10-core Mac splits the app
into 10 batches and never puts the two together (every local build here
took 27-36 s); the 3-core CI runner makes 4 batches of 22 files, and
batch one holds both. Forcing `-driver-batch-count 4` locally reproduced
it: the batch ran 9+ minutes on an M5 before being killed, every other
batch finished in 10 s, and the pair {BackDeployCompat, ChatView} alone
takes 100+ s while every other {file, ChatView} pair takes 11 s.

Each shim is now a ViewModifier that branches over its placeholder
content, so a call wraps the view once and the type grows linearly with
the chain. Call sites are unchanged. With the fix the 22-file batch
compiles in 11 s, the pair in 5 s, and the exact CI command with four
batches builds both slices in 39 s here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(ios): build one simulator slice and print the build timing summary

`generic/platform=iOS Simulator` builds arm64 and x86_64, and
ONLY_ACTIVE_ARCH=YES is a no-op with a generic destination (checked:
still 16 + 16 SwiftCompile tasks). Nothing runs the x86_64 slice — the
runner, the UI-test simulators and current Macs are all arm64 — so it
only doubled the compile and ran six swift-frontends at once on the
3-core, 7 GB runner. ARCHS=arm64 keeps the one that matters.

-showBuildTimingSummary adds per-task-type totals at the end of the log,
so the next slow batch is read off the summary instead of inferred from
a gap in the timestamps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ios): split the view bodies the type checker spent 0.3–1.1 s on

`-warn-long-function-bodies=100` on the simulator build named nine
bodies between 0.2 and 1.1 s (M5, one slice): ChatView.body 1.1 s,
GitPRDiffCardView.body 0.75, AgentThoughtChamberView.expandedContent
0.71, ActivityRunChip.body 0.59, AgentProfileView.body 0.51,
NewSectionSheet.botCell 0.34, CredentialRequestCardView.body 0.30,
RoutineEditorView.body 0.28, PredictiveActionChipsView.body 0.22.
Each was one expression: a stack of sections, rows, colour ternaries
and multi-statement closures the solver searched as a whole.

Each is now the same view tree in named pieces: a section, a row or a
pill is its own property or function; a closure body of more than one
line is a method; a colour chosen by a ternary is a typed `let` or
property. No literal moved out of its Text/Label/Button call, so the
localisation keys are unchanged, and no view gained or lost state.
After: the slowest of the nine is ChatView.body at 0.14 s, and the
slowest body in the app is DigestSheet.body at 0.21 s.

These were not the cause of the 25-minute CI build (that was IRGen on
the @ViewBuilder shims, the previous commit); they are what the
type-check limit the next commit adds would otherwise trip on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(ios): fail on a slow Swift expression; cap the job at 15 minutes

ios/project.yml (Debug) passes -warn-long-expression-type-checking=500
and -warn-long-function-bodies=500, so Xcode shows a slow body as a
warning at its line, and the CI step after the simulator build reads
those warnings out of the build log and fails with the file:line. Swift
6.3 has no diagnostic group for these two warnings (-Werror <group>
answers "unknown warning group"), so the log is read rather than the
compiler asked to make only them errors.

timeout-minutes 30 → 15: the job is about five minutes (tests ~1.5,
xcodegen ~1, build ~2 on the runner), and the old cap is what let a
5× regression read as "cancelled" for a day.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ios): keep main's RoutineEditorView and PredictiveActionChipsView

Their bodies type-check in 263 ms and 216 ms on an M5 (main's code, limit
set to 100 ms); at the runner's 1.2–1.6× that is at most 421 ms and
346 ms, under the 500 ms guard with margin. The split bought no CI time
and put ~400 changed lines into two files other iOS PRs touch daily.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(ios): refuse an #available branch inside a View extension

The incident class the type-check guard cannot see: a @ViewBuilder View
extension branching on #available doubles the caller's view type at
every call, and IRGen on the batch holding both ran until the job cap.
A build killed at the cap prints nothing, so the rule is checked at the
source, before the build: scripts/check-ios-view-shims.sh fails with
file:line on any `#available(` inside an `extension View { … }` block.
The two widget background helpers were the last of that shape; they are
one ViewModifier now, so there is no allow-list.

Drop -showBuildTimingSummary: it prints only after a finished build (so
never for the case it was meant for) and gives per-task-type totals, not
the slow batch; the guard steps name the file:line instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(ios): fail the type-check guard at 1500 ms, list the 500 ms warnings; ignore braces in strings

The guard step failed on 60766d9 (run 37172158801) with no Swift change
from the green runs: RoutineEditorView.body 600 ms and
PredictiveActionChipsView.body 551 ms, the two bodies kept as on main at
review. They measure 263 and 216 ms on an M5 and were under 420 ms on the
calibration run; this runner was 2.3x the M5. The compiler reports
wall-clock time, and the shared 3-core runner swings about 2x run to run,
so 500 ms is a developer-Mac bar, not a CI verdict.

ios/project.yml keeps the 500 ms warning (Xcode shows it at the line).
The CI step now prints every body over that limit and fails only on one
over 1500 ms, three times the bar: no body in the app comes near it, and a
type-check problem on its way to a timeout runs past it. The ms is parsed
from the warning text with awk; checked against the real log (551/600 pass
and are listed), a synthetic 1600 (fails), 1500/1501 (pass/fail), and an
empty log (pass).

scripts/check-ios-view-shims.sh: string literals are blanked before the
line-comment strip and the brace count, so `var brace: String { "}" }`
inside an `extension View` no longer closes the block early and hides a
later #available (CodeRabbit on d8a90a1). Mutation-tested: the old
script passed that file, the new one fails it at its line; the plain shim
shape still fails; braces and // inside strings alone still pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 0f998817 Deployed Oct 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants