Repository navigation
Upstream sync: OpenMausBot 0.1.95 (04a8bef8) - #122
Merged
Merged
Conversation
…nd-soni#2164) * fix(chat): omit empty work receipts and discard digest reply tails * docs: retain isolated digest presentation screenshots
…-soni#1876) The iOS chat had no way back to the newest message once you scrolled up, unlike the desktop chat. A pill now appears above the composer when the end of the transcript is more than 160pt below the screen, and tapping it scrolls to the end, past any reply still streaming. Position is tracked with onGeometryChange on an end-of-transcript marker and the scroll view, so it works on iOS 16 and updates while scrolling. Refs MOCA-249 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…282) (milind-soni#2200) * fix(ios): stop counting expired proposals as waiting on you (MOCA-282) When a routine, profile or team-setup proposal goes stale, the computer marks its card `expired` and clears its options, without setting `answered` or `dismissed`. The phone did not know the field, so the card stayed "waiting on you" with no buttons to tap, and kept the chat in Needs you for good. OptionCard now decodes `expired` and treats it as settled, the same rule the desktop uses, and the card says it expired instead of going blank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): stop counting expired proposals as waiting on you (MOCA-282) Same gap as iOS: OptionCard ignored the `expired` flag the computer sets when a routine, profile or team-setup proposal goes stale, so the card stayed pending with no options to tap. It now counts as settled and says it expired. Also adds the iOS after screenshot for the PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…es (milind-soni#2208) * fix(ios): show and steer messages sent mid-turn 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> * fix(ios): do not fake a cancel a computer cannot perform 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> * feat: say Steer in words, above the chat bar 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> * fix: say Steering while the interrupt is still landing 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> * fix(ios): retire the queued row, and stop streaming a dead turn 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> * perf(ios): keep the engine-capability read off hydrate's critical path 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> * feat(activity): a per-bot activity log, read from the two logs that already 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> * feat(outbound): sending on your behalf always asks, or spends a daily 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> * feat(scopes): per-bot connection scopes, enforced at the connector relay 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> * feat(fallback): second accounts on an engine, and a per-bot fallback 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> * feat(team-memory): people, places, decisions and terms every bot in a 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> * feat(ios): Activity and Team memory screens, with the companion routes 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> * docs(mobile): design for a shared touch gesture layer on remote surfaces 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> * docs(mobile): implementation plan for the shared gesture core 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> * feat(ios): gesture value types and viewport mapping 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> * feat(ios): click sequencing for direct-mode taps 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> * feat(ios): long press, drag threshold and held-button drags 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> * feat(ios): trackpad mode with a virtual cursor and acceleration 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> * feat(ios): scroll, momentum and local zoom state 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> * feat(ios): driving gate and held-input flush 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> * test(ios): shared gesture parity fixture and runner 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> * feat(android): gesture value types and viewport mapping 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> * feat(android): gesture core mirroring CompanionCore 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> * test(android): run the shared gesture parity fixture 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> * feat(companion): per-device browser control capability 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> * feat(ios): browser-live sink and input queue 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> * feat(android): browser-live sink and input queue 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> * feat(mobile): browser-live transport and message decoding 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> * feat(ios): browser control screen and touch adapter 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> * feat(android): browser control screen and touch adapter 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> * fix(mobile): typing on Android, cursor placement and pan Three gaps a self-review found after the screens compiled: - Android declared a typed buffer but had no field, so there was no way to type at all. Committed text now leaves as char; Backspace stays on the bar because an empty field reports no deletion. - The Android cursor was positioned against a hardcoded 1000px rather than the box it lives in, so it only landed right by accident. - Neither zoom path applied pan on Android, which stranded a zoomed-in person at the top-left with no way to reach the rest of the page. Also, on both platforms the page's own navigation could overwrite an address someone was halfway through typing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mobile): make the browser stream actually decode, and 8 more A code review found the client halves were broken end to end. Most severe first: - The SSE decoder keyed on a `type` field inside the payload, but the server strips the type into the `event:` name and both SSE parsers discarded `event:` lines. Every message decoded to nil, so iOS never left "Connecting..." and Android never got a viewer id, which left Take control permanently disabled. Both parsers now carry the event name, and the decoders key on it — including `ready`, which is what the server actually calls the viewer-id message, plus `control` and `heartbeat`, which were not handled at all. - Two Kotlin string templates were written as literals ("Bearer \$it", "https://\$address"), so every Android browser request went out unauthenticated and scheme-less navigation went to a bogus host. - The Android tick clock and touch clock had different origins, and pointerInput restarts on a driving change — so after taking control every tap read as older than the long-press threshold and fired a right click instead. - Direct mode set `scrolled` on any nonzero move, so a tap that wobbled a pixel scrolled sub-pixel and suppressed its own click. Now behind the drag threshold, on both platforms. - pan() and the render transform measured against the view while the mapping used the aspect-fit drawn size. On a portrait phone showing a 16:9 page those differ by 3.6x, so zoomed taps landed far from the pixel touched. ViewportMapping now exposes drawnWidth/drawnHeight and all three use it. - The Android dispose-time release launched on a scope cancelled in the same pass, so it never left the phone. - The Android mapping was rebuilt only on layout, so a viewport other than the 1280x720 default never reached the core. - The sidecar's SSE scrub ceiling was 1 MiB against browser-live's 3 MiB frames; one large frame would have torn down the stream. The proxy test that covers this hardcoded 2 MiB and now derives from the constant, so it cannot rot the same way again. Regressions added for the wobble-tap and letterboxed-pan cases: the existing tests used a square viewport where drawn size equals view size, so neither bug could have been caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mobile): revocation, stale clicks, held buttons and a pump race A second review pass found eight more. Most severe first: - Revoking browser control did not close a browser-live stream the device already had open, contrary to the comment promising it: disconnectDevice only ever reached /api/events and viewer relays. A phone kept watching a signed-in browser after the grant was taken away. The proxy now registers capability-gated streams per device and closes them on revoke, with a test. - A trackpad tap that never moved the finger emitted press/release with no move, so the sink stamped the click at its last coordinate — (0,0) on a fresh session — while the reticle sat where the person was aiming. Both platforms, and the parity fixture asserted the wrong shape. - iOS never called Coordinator.flush(), the only source of a release for a held button, so backgrounding or leaving mid-drag left it down. Android had this right; the design doc named the parity gap. - The Android dispose-time release enqueued into a queue built on the composition scope that the same pass cancels, so the pump never ran and drain() would have spun forever. It now sends directly. - A stale pump cleared `active` unconditionally, wiping the replacement clear() had just started and letting two pumps drain concurrently. - The Android trackpad reticle ignored letterboxing. - The 403 told people to use a Settings switch that does not exist; it now names the button, which every first-time user will need since the grant is off by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(companion): give proxy fixtures the browser capability field The field stays required on authenticate's return type, because the registry always supplies it. tsconfig.server.json type-checks these fixtures; tsc -b does not, which is why the packaged build caught this and the earlier check did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mobile): show the server's refusal and let a dead stream retry The first hands-on test reported "browser control is off" after the grant was already on. The server path was fine: a device with the grant opens the stream, receives ready/control/status/frame, and take returns 200. The phone was at fault twice over: - The stream threw a bare status and discarded the response body, so every 403 read as "browser control is off" whatever the server said. Both clients now surface the sidecar's or harness's own error text. - Nothing restarted a stream once it failed, and toggling the grant deliberately disconnects the device. The screen sat dead until the person backed out. It now shows Try again, which starts a fresh viewer with no state carried over from the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(i18n): add complete Chinese localization across desktop and mobile - 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 * fix(android): align the notification footer and localize the thread sheet - 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"). * fix(i18n): correct Chinese mistranslations found in cross-PR review 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. * fix(i18n): unify Chinese terminology across desktop, Android, and iOS 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. * feat(ios): Activity and Team memory screens, with the companion routes they call (milind-soni#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> * fix(companion): expose separate browser access grant in desktop settings * fix: harden mobile browser authorization and lifecycle --------- Co-authored-by: aivsomkar <aivsomkar@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: 择梦舟 <158067205+dreamfarer-space@users.noreply.github.com> Co-authored-by: LeaningLearner <1016823240@qq.com>
…ilind-soni#2207) The decisions.csv test asked for from=today&to=today. A run that crosses midnight UTC writes the earlier rows on the previous day, so the export returned fewer lines than GET /api/decisions and the test failed. Ask for the span of UTC days the rows carry instead. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilind-soni#2195) The bot list now scrolls on under the sidebar's head (top row, search, pinned panels) and foot (Routines, Triggers, Apps, profile), and a conversation scrolls on under its header, in one-to-one and group chats. Each bar blurs whatever passes beneath it, tinted with the surface it sits on, so at rest it reads exactly as before and only turns to glass once something scrolls under it. Its inner edge fades over 20px, so text melts into the glass instead of being sliced at a hard line. GlassScrollFrame measures its bars and publishes --glass-top and --glass-bottom. The scroller pads its content, its scroll-padding (so keyboard focus and jumps land clear of the glass) and its scrollbar track by them, so the scrollbar never runs under a bar that would swallow the drag. The glass is a pseudo-element, so the bars never become containing blocks for the fixed menus they open. Measuring lives in the frame, so the Sidebar's own hook order (which its tests mock by call order) is unchanged. Chat and group headers sit at z-[25]: above anything raised inside the transcript (the room set-up card is z-20 so its menus clear the composer), below the call overlays (z-30). The sidebar bars stay at z-10 under the resize handle. The universal pins cap moves from 42% to 40vh, because a percentage has no height to resolve against inside the head. Reduced transparency and forced colours get a solid bar. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…-soni#2217) * fix(ios): show and steer messages sent mid-turn 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> * fix(ios): do not fake a cancel a computer cannot perform 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> * feat: say Steer in words, above the chat bar 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> * fix: say Steering while the interrupt is still landing 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> * fix(ios): retire the queued row, and stop streaming a dead turn 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> * perf(ios): keep the engine-capability read off hydrate's critical path 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> * feat(activity): a per-bot activity log, read from the two logs that already 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> * feat(outbound): sending on your behalf always asks, or spends a daily 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> * feat(scopes): per-bot connection scopes, enforced at the connector relay 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> * feat(fallback): second accounts on an engine, and a per-bot fallback 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> * feat(team-memory): people, places, decisions and terms every bot in a 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> * feat(ios): Activity and Team memory screens, with the companion routes 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> * docs(mobile): design for a shared touch gesture layer on remote surfaces 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> * docs(mobile): implementation plan for the shared gesture core 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> * feat(ios): gesture value types and viewport mapping 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> * feat(ios): click sequencing for direct-mode taps 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> * feat(ios): long press, drag threshold and held-button drags 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> * feat(ios): trackpad mode with a virtual cursor and acceleration 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> * feat(ios): scroll, momentum and local zoom state 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> * feat(ios): driving gate and held-input flush 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> * test(ios): shared gesture parity fixture and runner 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> * feat(android): gesture value types and viewport mapping 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> * feat(android): gesture core mirroring CompanionCore 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> * test(android): run the shared gesture parity fixture 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> * feat(companion): per-device browser control capability 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> * feat(ios): browser-live sink and input queue 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> * feat(android): browser-live sink and input queue 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> * feat(mobile): browser-live transport and message decoding 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> * feat(ios): browser control screen and touch adapter 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> * feat(android): browser control screen and touch adapter 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> * fix(mobile): typing on Android, cursor placement and pan Three gaps a self-review found after the screens compiled: - Android declared a typed buffer but had no field, so there was no way to type at all. Committed text now leaves as char; Backspace stays on the bar because an empty field reports no deletion. - The Android cursor was positioned against a hardcoded 1000px rather than the box it lives in, so it only landed right by accident. - Neither zoom path applied pan on Android, which stranded a zoomed-in person at the top-left with no way to reach the rest of the page. Also, on both platforms the page's own navigation could overwrite an address someone was halfway through typing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mobile): make the browser stream actually decode, and 8 more A code review found the client halves were broken end to end. Most severe first: - The SSE decoder keyed on a `type` field inside the payload, but the server strips the type into the `event:` name and both SSE parsers discarded `event:` lines. Every message decoded to nil, so iOS never left "Connecting..." and Android never got a viewer id, which left Take control permanently disabled. Both parsers now carry the event name, and the decoders key on it — including `ready`, which is what the server actually calls the viewer-id message, plus `control` and `heartbeat`, which were not handled at all. - Two Kotlin string templates were written as literals ("Bearer \$it", "https://\$address"), so every Android browser request went out unauthenticated and scheme-less navigation went to a bogus host. - The Android tick clock and touch clock had different origins, and pointerInput restarts on a driving change — so after taking control every tap read as older than the long-press threshold and fired a right click instead. - Direct mode set `scrolled` on any nonzero move, so a tap that wobbled a pixel scrolled sub-pixel and suppressed its own click. Now behind the drag threshold, on both platforms. - pan() and the render transform measured against the view while the mapping used the aspect-fit drawn size. On a portrait phone showing a 16:9 page those differ by 3.6x, so zoomed taps landed far from the pixel touched. ViewportMapping now exposes drawnWidth/drawnHeight and all three use it. - The Android dispose-time release launched on a scope cancelled in the same pass, so it never left the phone. - The Android mapping was rebuilt only on layout, so a viewport other than the 1280x720 default never reached the core. - The sidecar's SSE scrub ceiling was 1 MiB against browser-live's 3 MiB frames; one large frame would have torn down the stream. The proxy test that covers this hardcoded 2 MiB and now derives from the constant, so it cannot rot the same way again. Regressions added for the wobble-tap and letterboxed-pan cases: the existing tests used a square viewport where drawn size equals view size, so neither bug could have been caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mobile): revocation, stale clicks, held buttons and a pump race A second review pass found eight more. Most severe first: - Revoking browser control did not close a browser-live stream the device already had open, contrary to the comment promising it: disconnectDevice only ever reached /api/events and viewer relays. A phone kept watching a signed-in browser after the grant was taken away. The proxy now registers capability-gated streams per device and closes them on revoke, with a test. - A trackpad tap that never moved the finger emitted press/release with no move, so the sink stamped the click at its last coordinate — (0,0) on a fresh session — while the reticle sat where the person was aiming. Both platforms, and the parity fixture asserted the wrong shape. - iOS never called Coordinator.flush(), the only source of a release for a held button, so backgrounding or leaving mid-drag left it down. Android had this right; the design doc named the parity gap. - The Android dispose-time release enqueued into a queue built on the composition scope that the same pass cancels, so the pump never ran and drain() would have spun forever. It now sends directly. - A stale pump cleared `active` unconditionally, wiping the replacement clear() had just started and letting two pumps drain concurrently. - The Android trackpad reticle ignored letterboxing. - The 403 told people to use a Settings switch that does not exist; it now names the button, which every first-time user will need since the grant is off by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(companion): give proxy fixtures the browser capability field The field stays required on authenticate's return type, because the registry always supplies it. tsconfig.server.json type-checks these fixtures; tsc -b does not, which is why the packaged build caught this and the earlier check did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mobile): show the server's refusal and let a dead stream retry The first hands-on test reported "browser control is off" after the grant was already on. The server path was fine: a device with the grant opens the stream, receives ready/control/status/frame, and take returns 200. The phone was at fault twice over: - The stream threw a bare status and discarded the response body, so every 403 read as "browser control is off" whatever the server said. Both clients now surface the sidecar's or harness's own error text. - Nothing restarted a stream once it failed, and toggling the grant deliberately disconnects the device. The screen sat dead until the person backed out. It now shows Try again, which starts a fresh viewer with no state carried over from the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(i18n): add complete Chinese localization across desktop and mobile - 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 * fix(android): align the notification footer and localize the thread sheet - 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"). * fix(i18n): correct Chinese mistranslations found in cross-PR review 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. * fix(i18n): unify Chinese terminology across desktop, Android, and iOS 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. * feat(ios): Activity and Team memory screens, with the companion routes they call (milind-soni#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> * fix(companion): expose separate browser access grant in desktop settings * fix: harden mobile browser authorization and lifecycle * test: align integration fixtures with consent and reconnect readiness * test: align integration fixtures with consent and reconnect readiness --------- Co-authored-by: aivsomkar <aivsomkar@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: 择梦舟 <158067205+dreamfarer-space@users.noreply.github.com> Co-authored-by: LeaningLearner <1016823240@qq.com>
…reaks the bot (milind-soni#2220) A Claude, Codex, ACP or Pi bot set to Works on: Cloud (Hosted desktop) had its turn handed to the Computer engine (turnInstance), which posted the prompt to Boat's own runner with Boat's default model. On an OMB Cloud that runner has no AI sign-in, so every turn failed with a bare provider_not_configured, and the saved choice (Works on, a composer pin, or a select_computer Auto pin) sent every later turn the same way. - The Boat cloud computer is now one more stdio computer server for every engine with computer tools (harness-mcp-proxy computer -> POST /api/internal/computer/mcp), in the same localComputer slot a Local VM or VPS uses. The agent process holds only a turn-scoped capability; the Boat credential stays in the harness. Arguments are checked against the tool schema before the control gate and the Boat. - turnInstance, the model/effort/variant swap and cloudComputerMcp are gone; usesCloudComputer is the one rule (computerMcp or the Computer engine), checked before anything is provisioned (cloudPlaceDriverError, replacing vpsDriverError). - An explicit place that cannot be attached fails with its cause plus the one control that changes it; a failed Auto-recorded pin is cleared so the next message runs on Auto. A person's pin stays. - Computer engine: Boat's 409 envelope becomes a sentence with the next action; on an included (OMB Cloud) Boat account it reports unavailable. - browser-proxy becomes harness-mcp-proxy (browser|computer, OMB_MCP_TOKEN); chat-boat-tools becomes server/cloud-computer-tools.ts, ChatBoatClient deleted. Stale prompt text for removed tools deleted. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(server): stop re-reading config.json on every broadcast frame Every stream frame and every request rechecks email sessions against the sign-in list. With at least one email session that meant reading, parsing and schema-checking config.json each time (about 20 us per frame, so per streamed token). The list is now worked out again only when config.json changes: a new file (every save renames one into place), a new size or a new modification time. A missing or unreadable file, or one saved in the last two seconds, is read on every call, so removing someone always takes effect on the next frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server): never cache a config.json the server could not use The sign-in list cache kept the empty list it got from a config.json that loadConfig() could not read (permissions, a failed read, invalid JSON), because stat still worked on that file. Fixing the permissions with chmod or chown does not change the size, the time or the file, so nobody could sign in by email until the next write or a restart. Now a reading where loadConfig() ignored the file is never kept, and a change of permissions or owner (the file's change time) also counts as a change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…op remounting (milind-soni#2216) ChatMarkdown handed react-markdown a components map built inline on every render. Each renderer is an element type, so any re-render with unchanged text gave React new types and it rebuilt the whole message. Re-renders come from the thread list (a thread opened or renamed anywhere, a bot renamed, the selection moving), so every visible bubble was rebuilt on those events: code blocks dropped to plain text until an effect re-applied the cached highlight, and wrap, copy, spoiler and save state were lost. - The renderers are module constants; the per-message values they need (streaming, message, image offsets, threads) reach them through one small context. - Only a message whose text can hold a thread link ("#" or an openmausbot link) reads the thread list, via a conditional use(); the rest keep bailing out in memo. - remarkPlugins is memoised on its inputs; rehypePlugins is a constant. - CodeBlock and MermaidDiagram read their caches in the useState initializer, so a remount paints highlighted in its first frame. - CodeBlock and MermaidDiagram keep a stable dangerouslySetInnerHTML object per string: React 19 compares that prop by identity, so a fresh object rebuilt the highlighted DOM on every re-render. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilind-soni#2213) Every Chief of Staff turn ended its team section with an 803-byte "TRUSTED OPENMAUSBOT STATUS" block read from ~/.local/state/aos-session-bridge/openmausbot/latest.json. That path belongs to an external session bridge; OpenMausBot never wrote it, so the block always said freshness=unknown, reason=missing_or_insecure. Delete openmaus-status-capsule.ts and its tests, both call sites in index.ts (settings preview and real turn), and the now-unused trustedOpenMausStatus parameter of chiefOfStaffSystemPrompt. The Chief section now ends at the roster. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Four renderer files were reached only by their own tests: ComposerTray.tsx, SidebarMoreMenu.tsx, the use-native-view-obscured hook (LocalVmWorkspace.tsx keeps its own simpler hook) and browser-panel-operation.ts. Deleting the hook also orphans aspectFitNativeViewBounds in local-vm-workspace.ts, so it goes too. The two tests that imported the deleted files lose only those assertions: the workingFolderLabel case in BotThreads.test.ts and the SidebarMoreMenu negative check in SidebarFooterNav.test.ts (the ">Tools<" text check still covers it). The production renderer build is byte-identical before and after. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilind-soni#2209) * fix(chat): keep SQLite's active leaf equal to memory's after a late screenshot When the settle-time screenshot lands behind a follow-up the person already sent, insertMessageAfter left memory's leaf on the newest reply but mdb.appendMessage moved SQLite's leaf to the screenshot. A restart, or the first page of a thread that is not cached, then opened on the screenshot and hid the follow-up and its reply. mdb.appendMessage now takes the leaf to record (the new message by default), and insertMessageAfter passes memory's leaf in the same transaction. That also covers threads with no stored leaf, where a reload would otherwise fall back to the newest row, which is the screenshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(chat): insert the late screenshot without touching the leaf Review fixes. SQLite's leaf already equals memory's when insertMessageAfter takes the behind-a-follow-up branch: memory's leaf only moves through appendMessage, branchMessage and setActiveLeaf, and each writes the same leaf to SQLite. So the insert only has to leave the leaf alone. It now calls the existing mdb.insertMessage, and mdb.appendMessage goes back to main's two-argument form. The no-stored-leaf test is gone: it reached its state only by clearing the leaf by hand, which the server never does at this point. The reload check moved into the existing chain-insert test instead of a near-copy of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ate parallel work) (milind-soni#2203) * fix(coordination): follow-ups continue the same teammate thread everywhere A follow-up to work a Chief already handed out opened a second teammate thread, so the same task ran twice in parallel. Since milind-soni#2061 the direct coordinate_bots route created a thread for every call (server/index.ts, store.createTask per assignment), and each person message is a new request tree, so nothing could reach the earlier thread. The route is the same server code on the desktop app, a VPS and a Cloud home; the difference people saw was version skew (pre-0.1.92 apps still used the old pair thread). Now a conversation has one thread with each teammate: the first request opens it (openedBy.threadId names the conversation), every later request from that conversation continues it and waits behind work still running there (the existing thread-busy check). The teammate is told the request continues its earlier work; the receipt names the thread. A finished thread is not folded away while its next request is waiting. The server derives each request's identity from its destination and text within the request tree, so request_key is gone from the tool, the proxy and the route, along with its four duplicate strings, the "already used for different work" and "already started" refusals, the speculative-thread cleanup and the dead send_to_thread/wait_thread names. Tests: direct coordination (headless server and desktop-app mode via a new launcher knob), Cloud home (cloud-personal), thread-aware bots, room coordination, catalog goldens regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(coordination): review fixes for one thread per conversation - A repeat after the request finished or failed runs again with rework=true (a re-check after a fix, a retry after a failure); without it the call is refused with that instruction, never receipted as delivered. A live repeat still lands on the running request. - enqueue recognises a repeat from the fields every node already stores (teammate, place, text) instead of a route-built sha256 key; a room call's recipients share a per-call batch id. - Drop the "continues your earlier work" scan and sentence: unrelated requests share the thread too, and the teammate has the history. - Drop label: every direct thread is "@<sender> · work". - Restore the cleanup of a thread opened for a repeat that landed on an existing request (reachable when the person archived the thread). - The coordinate_bots result is receipts plus message or error; the accepted/errors lists are gone. The error keeps the full refusal text. - State the one-thread rule once, in the coordinate_bots description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Memory: never lose a note at the size cap
updateMemory refused any write that left MEMORY.md over the load budget
(200 lines / 24 KB) and the tool closed itself after three refusals in a
turn, so a full file deadlocked: removing a few rows never made enough
room, and the notes were lost ("I will retry next turn"). Capture
dropped its facts the same way, silently.
Now a write never fails for size. In the same synchronous call the
oldest dated entries (struck-through first, then expired, then the
oldest live ones) move to memory/archive.md, marked "moved <date>";
hand-written lines, health and safety facts, a fenced line and the new
entry never move. The archive is written first, with no await before
MEMORY.md. session_search still finds archived lines, and the bot is
told exactly which lines moved. One budget: what is stored is what
loads. One fact is at most 1,000 characters.
Removed: the over-budget refusal and its shrink exception,
MemoryBudgetRefusal, MEMORY_REFUSAL_RECENT_ENTRIES, recentEntries,
MEMORY_CONSOLIDATE_HINT, the "Consolidate it now" prompt text, the 413
mapping, MAX_MEMORY_REFUSALS_PER_TURN and memoryRefusalsThisTurn, the
capture "MEMORY.md is full" flag, and the tidy-only archive writer
(tidy and the size rule now share appendMemoryArchive, which no longer
replaces an archive it cannot read).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Memory: keep memory_update's description within the catalog budget
The new "never fills up" sentence pushed every tools/list profile past
its 2% wire budget. Say the same in fewer words instead of raising the
baseline: drop "Never overwrite the full file from a stale thread
snapshot" (covered by "never direct file writes"), "marks the entry
updated" and "rather than was mistyped". The description is now shorter
than on main; goldens regenerated (only memory_update changes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Memory: move whole entries, cap entry lines, short replies (review fixes)
Review fixes for the size rule:
- An entry now owns the lines under it: indented ones (how a code block
is written into an entry now, Markdown's list rule) and a code block it
opens, through the closing fence (how such entries were written
before), never past the next dated line and never through a fence that
does not close. makeRoom moves and counts whole entries, so a fence is
never left behind and the `includes("```")` exclusion is gone.
- memory_update text is at most 20 lines as well as 1,000 characters, so
one code block cannot fill what loads.
- The reply names the first 5 moved entries by their first line and
counts the rest, instead of echoing every moved line.
- The success result is { ok, truncated, entry, moved }: no re-read of
MEMORY.md, no unused text/bytes.
- The rule is stated once, in memory_update's description; the managed
prompt only says to change MEMORY.md with memory_update.
- One archive label from code in the topic index; the archive is created
without a header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d-soni#2219) - Delete grokagent.ts (unused re-export) and scripts/repro-closed-threads.mjs. - Delete exports used only at their definitions and 15 production functions reached only by their own tests. Tests of thin wrappers now call the production function they wrapped. - Claude refreshes the recorded system prompt on every turn; drop SendTurnInput.refreshSystemPrompt (both builders always set it). - usesCloudComputer() is derived from remoteAgent || cloudComputerMcp in contracts.ts; drivers no longer declare it. - The lending-memory walker reads WORKING_FOLDER_FILES/DIRS, so the security boundary has one list. - Agents MCP serverInfo is openmausbot-agents; header no longer lists stale tools. Cerebras points at Settings -> API keys. - Six env reads nothing sets become constants (PROMPT_IDLE stays). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t again (milind-soni#2226) The merge of main into milind-soni#2219 took main's side of contracts.ts, index.ts, openai-chat.ts and capability-fields.test.ts, which brought back SendTurnInput.refreshSystemPrompt (now read by nothing) and kept usesCloudComputer as a field six drivers declare. - usesCloudComputer() is derived in contracts.ts as remoteAgent || computerMcp, main's rule since milind-soni#2220. The six declarations are gone; cloudPlaceDriverError and every attach/readiness read in index.ts call the function. The fleet test checks no driver declares the property. - Delete SendTurnInput.refreshSystemPrompt and both `true` assignments again; Claude refreshes the recorded prompt on every turn. - Claude: the version-cache comment, the Engines update notice ("resumed turns", not "coordinated resumed turns") and a test comment match the unconditional refresh. - CONTROL_REFUSAL_PLAIN's comment no longer contrasts it with a wait tool that no longer exists. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilind-soni#2222) Review follow-up to milind-soni#2210, which deleted ComposerTray.tsx and SidebarMoreMenu.tsx: - SidebarPopoverMenu: the trigger's data-tour id and the attention summary handed to renderTrigger were read only by SidebarMoreMenu. The two remaining menus (the profile row and the chat header's More button) pass no id and read only `open`, so both go and renderTrigger receives `{ open }`. The header and the attention note now describe the two real users; an item's own attention dot stays. - Locales: the five composer.tray.* strings belonged to the deleted tray. privateWorkspace lost its last reader with ComposerTray.tsx; the other four had none. Removed from en, zh, zh-tw and uk, and the three packs re-accepted with the locale script, which drops exactly their 14 hash entries. A test now pins that an item's attention dot is drawn on the item in its tone, and not on the trigger. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ni#2223) react-markdown's synchronous Markdown builds a new processor and re-parses on every render and never looks at the plugin arrays' identity, so the memoised remarkPlugins and the hoisted rehypePlugins constant bought nothing. Only the components map's identity matters, and it stays a module constant. - Inline remarkPlugins and rehypePlugins again; drop the useMemo, the REHYPE_PLUGINS constant and the Options type import. - Drop the renderers-test assertion on rehypePlugins identity, which pinned no behaviour. - Fold the cached-highlight first-frame check into the existing code palette test instead of repeating its setup, and drop its setTimeout(0) sleep. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ooks as their task (MOCA-204) (milind-soni#2227) * fix(ios): fold Updates lines by the Activity setting and show webhooks as their task (MOCA-204) The line under a chat on the Updates pill and sheet, the island, Live Activities, the home-screen widget and Walkie came from Updates.swift, which read the raw last message. Two leaks followed: - A webhook run ending on its prompt showed the whole envelope ("[DEFAULT WEBHOOK INSTRUCTIONS] ... Delivery ID: ... Sender: ..."), though the chat list already reduces it to the task. - A tool step's name, often a raw shell command, showed even with Activity = Hidden, so Hidden meant hidden in the chat but not on these surfaces. updates(detail:) now takes the reader's Activity setting. The to-review line reads by the roster's rule (rosterPreview: webhook task, folded tool runs, no digest), and the working line names a tool step only when Activity is not Hidden; a status notice still shows. Every app caller passes the stored setting. The widget extension cannot read the app's settings, so a WidgetSnapshot now records the setting it was written under and the extension's own refresh reuses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): fold Updates lines by the Activity setting and show webhooks as their task (MOCA-204) Port of the iOS change. The Updates pill and sheet read the raw last message: a webhook run ending on its prompt showed the whole envelope, and a tool step's raw command showed even with Activity = Hidden. updates(detail) now takes the reader's Activity setting. The review line reads by the roster's rule (rosterPreview: webhook task, folded tool runs, no digest), and the working line names a tool step only when Activity is not Hidden; a status notice still shows. The roster and the sheet pass the setting they already collect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(screenshots): MOCA-204 Updates sheet at Full and Hidden (iOS) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilind-soni#2211) * fix(ios): put Stop in the composer while a turn runs (MOCA-148) The desktop shows a Stop button in the composer whenever a bot or room is working. On the phone the only way to stop a turn was Interrupt in the + menu, which few people found ("Cancel exists on macOS, missing on iPhone"), and rooms had no way at all. The composer now shows a Stop button beside the mic while the conversation is busy. It stops that conversation's own thread: a bot through /api/bots/:id/interrupt, a room through the new interrupt(groupId:threadId:) on /api/groups/:id/interrupt, which the companion already allows. The mic stays, so a steer can still be dictated mid-turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): put Stop in the composer while a turn runs (MOCA-148) Port of the iOS change. The Interrupt chat action was the only way to stop a turn, and rooms had none. The composer now shows Stop beside the mic while the conversation is busy; it stops that conversation's own thread: a bot through interrupt(botId, threadId), a room through the new interruptRoom on /api/groups/:id/interrupt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mobile): offer Stop while a room works between speakers (MOCA-148) Review follow-up. The phones counted a room as busy only while busyBotId named a speaker, but a room run also routes, waits on a member busy elsewhere (up to 30 minutes) and hands off between speakers. The desktop shows Stop through all of it (group.working || busyBotId), and the server sends `working` on every room; the phones never decoded it, so the new room Stop vanished in those stretches. Both phones now decode Room.working and gate Stop on a new Chat.canStop (busy for a bot; busyBotId or working for a room). `busy` keeps its meaning everywhere else. Android's projection of a sibling room thread drops `working` as it drops busyBotId. Android: Stop now takes the mic's slot while it shows, as on the desktop. A fourth 48dp target left a 360dp phone's field about 60dp wide. A running dictation keeps its mic so it can be stopped. Also adds the iOS screenshot for the PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
milind-soni#2229) Review fixes for milind-soni#2220 (merged), on top of main. - Auto attaches a Boat only for the Computer engine, whose turn runs there. Every other engine reaches the Boat only when the conversation is on it (Works on, a pin, a cloud routine, select_computer), so an Auto turn adds no Boat call, can't be failed by a Boat's deletion fence, and records no place nobody chose. Restores the pre-milind-soni#2220 Auto behaviour for Claude, Codex, ACP and Pi, and drops the Sep 23 Auto reuse for API engines. - findBoat's direct read of a known Boat has the same 20 s deadline as every other Boat read. - One predicate, canWorkOnCloud (shared/cloud-computer.ts), for the server and the renderer. It replaces contracts' derived usesCloudComputer() (milind-soni#2226) and the renderer's three copies; cloudRunner becomes boatCapableEngine. - Dead code out: canMount (attachBotBoat, boatTurnLifecycleAction), SurfacePlan.clearPin, CLOUD_PLACE_DRIVER_ERROR, PlaceUnavailableError.source, the routine interruptTurn runOn argument; parseHarnessMcpKind un-exported. - One asPlaceFailure wrapper for bot threads and rooms; the routine refusal takes its words from cloudPlaceDriverError. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ass sidebar, no status capsule); drop tests for untaken upstream renderers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…under Sagax per-person bot visibility Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges upstream OpenMausBot main
04a8bef8(0.1.95) into Sagax with a real merge, no rebase. The previous sync wasff01be8a(#116). This covers 30 upstream pull requests.versionandbaseVersionare now 0.1.95, "Last sync" in AGENTS.md is updated, and origin/main is merged up to #121.Upstream changes brought in
config.jsonis not re-read on every frame (Stop re-reading config.json on every broadcast frame milind-soni/OpenMausBot#2215).boatCapableEngine); Auto never reaches a Boat.Dropped or kept ours, and why
SAGAX_LIVE_CALLS, and no new hosts were found (check:no-phone-homepasses).Conflicts
openmaus-status-capsule,browser-proxy.testandchat-boat-tools.test; those deletions are accepted. Our deletions of the Simple-mode tests are kept.Verification
i18n:check, build,build:server,check:no-phone-home: passtest:electron: 696/696test:unit: passindex.test). The upstreamtrust-controlse2e was adapted to our per-person bot visibility: members get 404 instead of 403, and one scenario is skipped because its loopback-owned bot is private to its owner.card-answerersandbot-visibilityfailed once under load and pass alone.🤖 Generated with Claude Code