Skip to content

Upstream sync: OpenMausBot 0.1.94 (ff01be8a) - #116

Merged
jencryzthers merged 238 commits into
mainfrom
chore/upstream-sync-2026-10-03
Oct 3, 2026
Merged

jencryzthers merged 238 commits into
mainfrom
chore/upstream-sync-2026-10-03

Conversation

@jencryzthers

@jencryzthers jencryzthers commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Owner report (GOX, org mode): "Validate that we can add plugins and skills as we want. I'm trying to add the GitHub MCP or even log into GitHub ... and it's not letting me."

What blocked each path, and why

  • Remote MCP (GitHub MCP): /api/mcp/servers is admin-only, and its screen sits in Connected apps, which is off by default, so members had no UI. The list is server-wide, so one OAuth token or PAT served every person. GitHub's OAuth has no dynamic client registration, so the sign-in failed with client_required.
  • Local stdio MCP: server-wide entries were spawned by the engine on the Sagax host, which bypasses withholdHostTools. There was no per-person option.
  • Plugins: claude plugin marketplace add/install typed by a bot runs in host Bash, which is denied on an org server (deny beats any SAGAX_CLAUDE_ALLOW rule). The sandbox has no claude, and an install there would not reach the engine anyway.
  • Skills: /api/bots/:id/skills* was admin-only (403 for members), and a private repo could not be read.
  • GitHub login: no gh in the sandbox, no per-person credential, and Bash is denied on the host. Egress was not the blocker: public internet is open (github.com, codeload, objects, raw, ghcr.io and api.githubcopilot.com are reachable).

Fix

  • Settings > Mes connexions (org only, the person themselves):
    • Connecter GitHub through the device flow of the org's OAuth App (SAGAX_GITHUB_CLIENT_ID or Settings > Organization), or a pasted token.
    • The token is encrypted per person (principals/<pid>/connections.enc) and written to /workspace for gh and git.
    • Ajouter un serveur MCP offers three kinds: a URL with OAuth (callback on this server, a per-person sign-in vault), a URL with a token or "my GitHub account", or a command that runs in the person's server environment (new signed sandboxd stdio stream, relayed by sagax-stdio).
    • These servers mount only for that person's turns. Private, loopback and metadata hosts are refused.
  • Server-wide MCP commands are never added (org_host_command) or mounted on an org server.
  • Library > Files | Skills | Plugins:
    • Sagax clones a marketplace itself, using the actor's GitHub token for private repos.
    • Plugins are copied without hooks, .mcp.json, .lsp.json or bin/, and loaded with --plugin-dir; they also appear in the "/" menu.
    • Owner, manage or org admin can change plugins; use is read-only.
    • Admin policy "Marketplaces autorisés" allows any marketplace by default, or a list.
    • Skills routes are opened the same way, and skills from private repos are read with the person's GitHub connection.
  • Sandbox image: adds gh, nodejs and npm.

Tests

  • New: org-connections.e2e (OC-1..6), plus unit tests for the person store, GitHub connect, bot plugins, the stdio relay, sandboxd stdio and the UI.
  • Passing: typecheck, lint, i18n:check, test:unit (915 files), test:electron, and the related e2e (user-sandbox, member-access, harness-commands, mcp-selection).

Deploy

Redeploy the server image (sandboxd is the same image) and rebuild sagax-sandbox. Perspicax PR (optional SAGAX_GITHUB_CLIENT_ID passthrough, docs) is linked separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hd92V2CX6HCgfX8oYnCgMz

aivsomkar and others added 30 commits September 15, 2026 21:34
The composer is one flex row: attach, the Full access and place chips,
the editor, the mic. The editor is the only child that can shrink, so
with a bot's settings panel open beside the chat (or a small window) it
collapsed to a few pixels; its placeholder then stacked one or two
letters per line and the auto-grow made the whole box tall to fit them.

Below a 30rem composer width the row wraps and the editor takes a full
line of its own above the chips, with the mic/send group pushed to the
right of the chip line. Tailwind container query on the composer box,
same mechanism the chat header uses to fold its chips.

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

Measured on a dev instance at the 1100x780 default and the 840x620
minimum, with the settings panel, the inspector and both open. Three
things broke, all from rows or columns that could not shrink:

- Chat and group headers: the chip group is shrink-0, so with a panel
  beside the chat (~330px column) the name truncated to nothing and the
  rename pencil landed under the find button. The row now wraps below
  30rem — name line on top, chips underneath on the right. The query
  lives on a child row: a container query never matches the container.
- Two side panels: bot settings deliberately keep the inspector or
  computer panel open (their controls open settings), but two static
  panels plus the sidebar left the default window a sliver of chat with
  letters stacked vertically. Until the window is 2xl (1536px) wide,
  settings now floats over the chat as a sheet and the other panel is
  still there when it closes. New useMediaQuery hook; the global
  :focus-visible ring is suppressed on the sheet container.
- Composer: shipped in the previous commit (editor on its own line).

Everything else checked at both sizes held up: sidebar, Tools panel,
attention inbox, Settings modal, group setup card.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…able beside any panel

Omkar's second pass on OMB2 found three more breaks and asked for the
header to be simplified now that threads live in the sidebar:

- Header: the thread chip is gone; find, copy/download transcript, the
  token counter and the inspector move into one "more" menu that opens
  on hover (SidebarPopoverMenu gains a downward placement). Model, call
  and computer stay as buttons. Four controls instead of eight, so the
  row no longer runs under a side panel.
- Sidebar: while a side panel is open and the window is narrower than
  1280px, the sidebar folds to its avatar rail; the saved density is
  untouched and returns when the panel closes. The default 1100px window
  keeps a ~600px chat with settings, inspector or the computer panel open.
- Panels sit beside the chat from 768px up instead of covering the
  whole window below 1024px ("why does the whole thing hide behind
  this?"); with the rail that leaves a 350px chat at the 840px minimum.
- The composer's chip group and the header's control group may wrap
  instead of overflowing.

Verified over CDP on a dev instance at 1100, 974 and 840 wide with
settings, inspector, computer panel and the menu open.

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

Omkar looked for the share button in the new menu. The two export
actions now sit under a small "Share" heading with the wording the
share menu used before — "Copy as Markdown" and "Download as .md" —
and the sheet is wide enough that neither clips.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts:
- ChatView header controls: keep find/export in the header menu (this
  branch) and take main's Stop condition, which also covers a bot
  waiting on teammates.
- usageSummary: take main's labelled compact figure (usageChip) so the
  menu keeps the unit, per bfd419a.
- Sidebar: keep main's new TeamMenuItems and this branch's
  collapseToIcons prop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…row-wrap

fix(desktop): keep the chat usable at every window size, and fold the header into one menu
…90% (milind-soni#2118)

* WIP: control plane reclaims idle tunnels, capacity error, alert (unreviewed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Control plane: fix the reclaim race, re-check before DNS delete, observe by default

- The sweep's claim only takes a row still marked for deletion, or whose
  installation was revoked or removed. A row its owner took back (provisioned
  again) after the sweep chose it is left alone. New race test; it fails
  without the guard.
- Re-check that the tunnel is still idle right before the DNS record is
  deleted, so a reconnect mid-cleanup keeps the hostname.
- OMB_TUNNEL_RECLAIM defaults to observe (unset or invalid); production starts
  in observe. An invalid tuning value falls back to its default with a log
  instead of failing every request.
- Migration 0006 tags only the 2026-10-01 10:00-10:30Z operator batch as
  reclaims, using its own mark time.
- Ship OMB_CLEANUP_SWEEP_LIMIT=4 until Workers Paid is confirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Control plane README: observe by default; sweep size per plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Control plane test: pin the Workers Paid sweep size explicitly

wrangler.jsonc ships OMB_CLEANUP_SWEEP_LIMIT=4, so the second half of the
sweep-limit test passes 20 itself instead of relying on the shipped value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Role names: Super admin, Admin, User in UI and server messages

Owner decision: show Postiz's role names everywhere people read them.
Display text only; stored roles, API values and permission checks stay
owner/admin/member. Changed English strings drop their stale
translations so other languages fall back to English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs: describe roles as Super admin, Admin and User

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Role names: keep "owner" and "Full access" where the app means the account holder

The app has two roles, shown as Admin and User. Its "owner" is the person
who holds the server or computer, not a third role, so "super admin" was
wrong in the proposal note, pairing hints, sign-in refusal, command-permission
refusals and the self-hosting and people docs. Restore owner wording, and
say "Full access" where the text is about how a device is paired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The session/prompt idle guard (default 180 s) failed the turn and killed
the child whenever an agent sent nothing for that long. Qwen Code 0.24
is routinely silent longer than that while working: it emits no ACP
updates while compressing history, while backing off a rate limit
(60 s up to 5 min per wait, 10 retries), or while one model request runs
(its OpenAI/Anthropic SDK timeout is 600 s). Long turns were cut off
mid-work and the user's retry redid everything.

Raise the default to 15 minutes, above the longest normal silence. A real
wedge (the OpenCode hang from milind-soni#1595) still trips, just later, and Stop
still ends it at once. OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS still overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ACP has no "still working" message, so a silent agent was a spinner
with no explanation until the stuck guard killed it. While a prompt is
quiet, the driver now probes the agent and posts a plain-words notice
whenever the answer changes:

- retrying: read from the agent's own debug log (Qwen, with
  QWEN_DEBUG_LOG_FILE=1): "Qwen hit a rate limit (HTTP 429) and is
  retrying (attempt 2). Next try in 20 s."
- compressing: when the log says so explicitly.
- waiting on its model: the process tree holds an open TCP connection.
- busy: CPU time is climbing without output.
- no request open: briefly normal (an SDK sleeping between retries looks
  exactly like this), so it is only called "may be stuck" after 3 min.

New lines in the agent's log also restart the prompt's idle deadline, so
a turn that is visibly retrying is never stopped as stuck, and the final
stuck error now says what the agent last looked like.

Verified end to end against real Qwen Code 0.24.7 and a fake model
endpoint: constant 429s (retry notices, turn kept alive), a 90 s slow
model (one waiting notice, then the answer), and a SIGSTOPped process
(stopped at the limit with its last-seen state).

iOS and Android now keep notice rows visible as their own row, like
desktop does, instead of folding them into tool runs or hiding them
with tool calls off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…meout

fix(acp): stop killing quiet agents, and tell the user what they're doing
… of the digest (milind-soni#2139)

* fix(digest): count tool calls by identity, not by the chip's command line

The turn digest keyed its tool tally on the activity chip's display title.
That title is a tool name only for some drivers: Codex titles a
commandExecution chip with the whole command line and the ACP core prefers
`rawInput.command` over the call's own name, because in a transcript the
command is the useful label. So a reported digest read

  [digest] · tools: memory_update ×2 (1 failed), list_routines ×1,
  propose_routine_action ×1, /bin/zsh -lc "jq '.routines[]? | select(.id==…
  {id,name,resultThreadId,sourceThreadId,delivery,destination,resul […] ×1

Two defects, one cause. The command text — paths, ids — rode into a row that
is FTS-indexed and replayed into rebuilt context, and unlike `tool.summary`
the title is neither bounded nor redacted by the driver. And keying on the
command meant counts never aggregated: every distinct invocation was its own
bucket, so a turn that ran one tool twelve times said twelve tools and the
noise competed for the eight MAX_TOOLS slots.

Command-titled chips now count under one `shell` identity, and the rendered
line names what ran from `tool.summary`, which drivers already bound and
redact. `nameIsCommand` — the predicate the renderer has used for this exact
distinction since ToolActivity needed it — moves to shared/tool-name.ts so
both readers share one answer instead of guessing twice.

The transcript chip is unchanged: it still shows the command, which is what
makes it readable. Only the aggregate record changes.

Not fixed here: drivers/codex.ts:1078 stores that title uncapped, where every
other driver slices to 80 — that bounds a stored field and changes the chip
label, so it is its own call.

Verified: server/digest.test.ts (2 new cases, red before green),
digest.e2e, digest-control.e2e, verify-steps, DigestChip — 100 pass;
tsc -b and tsc -p tsconfig.server.json clean; oxlint clean.

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

* fix(digest): keep the shell bucket's command out of the rendered line

The phones split the digest's tools part after "×N" / "(N failed)"
(ios DigestSummary, android TurnDigest). A trailing "(command)" made
Android drop the whole tools list to one unparsed line with no call
count, and iOS merge the shell entry into the next one. The redacted
sample stays in the stored digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ni#2135)

* feat(companion): let a phone with computer access preview a bot's Local VM

Allow POST /api/bots/:id/local-computer/screenshot through the sidecar,
behind the same per-device computer-access capability as the cloud
desktop (off by default, toggled in Settings → Remote access). Only the
still is reachable: the VM's lifecycle routes stay host-only. The 403 now
says "computer access" since it covers both kinds of computer.

* feat(ios): show a bot's Local VM on demand, even while it is idle

ComputerView now asks for a Local VM still every 30 s while it is on
screen (every 3 s while the bot works and the stream has gone quiet),
the desktop panel's cadence, and shows whichever of that and the
streamed frame is newer. A 403 explains where to allow computer access
on the Mac; a 409 for a conversation not on the Local VM, or a 404 from
an older computer, leaves the existing behaviour alone. Polling stops
in the background. Adds the client call, a strict data-URL decoder,
tests, and pt-BR strings.

* docs: describe Local VM stills on the phone behind computer access

* test: isolated fixture for the iOS Local VM view

A disposable fake-engine server, a synthetic docker that serves PNG stills,
and the companion sidecar, so the phone's Local VM view and its
computer-access gate can be checked from a simulator without a real VM.

* fix(ios): picture the opened thread's Local VM and follow access changes live

- Project the bot onto the thread the view was opened from, so a task
  thread pictures its own computer (and its own seat in pool mode).
- Keep checking at the idle cadence while computer access is off, so
  turning it on at the Mac shows up without leaving the view; revoking it
  clears the picture. Only the sidecar's "computer access is off" 403 is
  read as that.
- Stamp a still when it was requested, and label the last picture when a
  refresh fails instead of letting it pass for current.
- Move CloudDesktopSession's doc comment back onto it; say in the docs
  that the thread check is the client's and that polling keeps the VM
  from being reclaimed as idle, like the desktop panel.

* fix(ios): caption a streamed frame when computer access is off; ignore a stale 401

With access off, a streamed frame of a working bot could stay on screen and
hide the access notice. The frame stays (the stream is not gated), captioned
with the notice. A screenshot 401 from the previous computer, landing after a
switch, no longer marks the new session unauthorized.

* test: abort the iOS Local VM fixture cleanly during server startup

A signal during startup now aborts the launch, which stops the server child
and removes its data directory, instead of leaving both behind.

* feat(server,companion): relay a Local VM's live desktop to a phone holding control

Add POST /api/bots/:id/local-computer/join. It answers only a loopback
caller (the address carries the VNC password), only for a conversation
on the Local VM, and only while a person holds that bot's computer, so a
phone never drives the VM alongside the bot. The companion allows it
behind the per-device computer-access capability and rewrites its
loopback noVNC address into the same device-scoped relay path the VPS
viewer already uses.

* feat(ios): an RFB client and the calls to take a Local VM and join its desktop

RFBClient is the protocol half of VNC, bytes in and bytes out: 3.3/3.7/3.8,
None and VNC authentication (CommonCrypto DES), BGRX true colour, and
Raw, CopyRect and DesktopSize, with pointer and key events. Partial
updates are applied only once whole. CompanionClient gains control
take/release under a lease, the Local VM join, viewer-close, and the
authenticated WebSocket request for the relayed viewer; the join only
accepts the sidecar's relay path, never a host.

* feat(ios): take control of a bot's Local VM from the phone

Take control under the Local VM's picture takes the bot's computer under
a fresh lease and opens the relayed desktop full screen: the live
picture with a pointer ring, a trackpad (swipe to move, tap to click,
two fingers to right-click or scroll, hold to drag), the system keyboard,
and a menu of keys and chords. Hand Back, or the app leaving the
foreground, closes the viewer and releases the lease so the bot is
never locked out behind an unused hold.

The verification fixture gains an offline password-protected RFB desktop
that records pointer and key events and paints each click, and a status
endpoint for them and the control hold.

* fix: bind a phone's Local VM desktop to its control lease, and harden the client

Review findings on the phone control path:

- The join now requires the caller's own control lease (not just any
  hold) and a JSON content type. A new read-only control action, check,
  says whether a lease still holds; the sidecar asks it every 3 s for each
  relayed Local VM viewer and closes the relay as soon as the answer is no
  (released or taken over from the Mac), failing closed on any error. A
  Local VM viewer without a lease is never handed to the device.
- The phone keeps one lease per computer and bot across launches, so a
  session the app never handed back can be retaken and released; releases
  on any failure after asking; and finishes handing back in a background
  task.
- RFBClient refuses desktops over 8192 px a side, copies overlapping
  rectangles in place, and consumes its buffer in O(1).
- The read loop holds the desktop only weakly; the trackpad no longer
  jumps when a swipe starts; the disconnected state says what to do.
- Docs no longer claim leaving the screen hands back, and note that in
  shared Local VM mode a hold pauses only that bot, as on the Mac.

* fix(ios): ignore a stale 401 from the previous computer in Local VM control calls too

The same guard the screenshot poll has: a control call answered by the
computer the phone has since switched away from must not mark the new
session unauthorized.

* fix(ios): hand back a take that lands after the person left; leave another holder alone

- The take runs in a task the view keeps, cancelled on leaving the screen
  or backgrounding. If it lands after that, the computer is handed
  straight back instead of opening a desktop nobody is looking at.
- Hand-back after a failure runs in a task of its own, so cancelling the
  take cannot cancel the release.
- When someone else already holds the computer there is nothing of ours
  to release, so no viewer-close is sent that could disturb theirs.

* fix(ios): hand a Local VM back through the computer that granted its lease

Session.withClient read self.client on every call. Settings can switch
computers while ComputerView stays up, so a take acquired on one computer
could join the viewer, or hand back, through the next one: the first
computer stayed paused under a lease it never saw released.

takeLocalVm now captures the client once, keys the lease on that client's
connection, and returns it with the lease. ComputerView keeps it in the
control state and passes it to both hand-back paths, including the one for
a take that lands after the person left. Calls still go down the current
route while that computer is the active one, so a route failover mid-control
does not send the release to a dead address.

* fix(ios): keep Take control disabled until the hand-back has finished

handBack() cleared the control state before awaiting the release, so Take
control came back the moment the desktop closed. The lease id is reused per
bot and computer, so a take started during that window held the same lease
the pending release was about to give up: the in-flight closeViewer and
release would then close the new viewer and release the new hold.

ComputerView now keeps a handingBack flag for the duration of the release;
the button is disabled and takeControl() refuses to start while it is set.
The take that lands after the person left already waits inside
takingControl, so it needed no change.

* feat(server,ios): drive a Local VM from a phone paired with the server directly

A phone paired with `openmausbot serve` itself (a headless server behind
Tailscale Serve or a tunnel) has no companion sidecar, so nothing rewrote
the VM's noVNC address for it: the join route answered 404 to any caller
but loopback, and the app accepted only the sidecar's relay path.

The join now answers a directly paired phone with the server's own
authenticated desktop proxy (/api/desktop-viewer/local/<target>/websockify),
bound to the phone's control lease and to the conversation whose VM seat
the join picked, plus the VNC password. The loopback address never leaves
the server. The proxy settles the seat once at open, then re-checks the
lease and the session every few seconds and closes the socket when either
lapses; hand-back (viewer-close) closes that bot's lease-bound viewers at
once and leaves the session's other viewers alone. Computer access is the
pairing's scope: Full access may, chat-only is answered 403, which the app
shows as computer access being off with a note about re-pairing.

The app accepts this proxy shape alongside the relay shape, carries the
lease binding on the socket, asks for no subprotocol from the proxy, and
sends viewer-close as JSON so the harness acts on it. The sidecar path is
unchanged: a loopback join still gets the raw address to rewrite.

* Refuse phone control of unreserved Local VM pool seats

* fix(ios): shrink the trackpad while the keyboard is up, so the desktop stays full width

On a phone the desktop is drawn as wide as the screen, so it needs only a
couple of hundred points of height. With the keyboard open, the 230-point
trackpad left less than that, and the picture shrank. The trackpad now
collapses to a short strip while typing: the desktop keeps its full width,
and a click is still one swipe away, which typing into a desktop needs
often (a field to focus, a button after the text). It grows back when the
keyboard goes.

* fix(ios): collapse the trackpad inside the keyboard's own animation

The trackpad shrank on the keyboard button's tap with an animation of its
own, while the desktop above only moved when the keyboard arrived and
pushed the inset up: two animations, and on a phone the second one came a
beat later, so the picture popped into place after the trackpad had settled.
The trackpad now follows the keyboard's will-show and will-hide
notifications and changes inside their duration, so it, the keyboard and
the desktop move as one. The first-responder change is made outside
SwiftUI's update pass, where a request can be dropped and the keyboard
never comes, and the trackpad's hint fades rather than reflowing.

* fix(ios): make room for the keyboard in the trackpad's own transaction

SwiftUI's keyboard avoidance and the trackpad's collapse ran on different
curves, so for a few frames the screen between them was shorter than the
desktop needed, the fit flipped to height-bound, and the picture narrowed
before growing back. The view now ignores the keyboard inset and pads its
own bottom from the keyboard's reported frame, in the same transaction and
on UIKit's keyboard spring, so the keyboard, the trackpad and the desktop
move together and the picture keeps its width throughout.

The trackpad's hint is cut as the pad shrinks rather than faded, since a
fading line rode up over the label while the layout moved, and fades back
in only once the pad has grown again.

* fix(ios): fade the trackpad hint back in place once the keyboard is gone

* chore(verify): show a captured Local VM desktop in the iOS fixture instead of coloured tiles

---------

Co-authored-by: Rui Gomes <5658301+ruigomeseu@users.noreply.github.com>
…i#2128)

The back button's height was fixed at 44pt while its width came from the
chevron plus padding (~34pt), so the glass capsule rendered as an upright
oval when there was no unread count. Give it a 44pt minimum width so it
is a circle on its own and still widens into a pill with the badge.
…age prep

windowsKnownDirs() lists the install locations augmentedPath() scans on top of the inherited
PATH, but it didn't include Docker Desktop's bin dir. docker.exe still turns up because
something else on the machine's PATH usually finds it, but docker-credential-desktop.exe
(which docker.exe shells out to for every registry pull) often doesn't, so local VM image
prep fails even though "docker" itself looks installed. Adds the standard install path and
a regression test modeled on the existing Antigravity one. Fixes milind-soni#2117.

(cherry picked from commit eac5c25)
milind-soni#2129)

* fix(ios): open conversations on the newest message when it has a table

A transcript whose last reply holds a Markdown table (or code block)
opened short of the end: the horizontal scroll view the table sits in
threw off the height `defaultScrollAnchor(.bottom)` measured on the
first pass, and the `initial` scrollTo ran before there was anything to
scroll. Scroll to the last row once more after the first layout settles,
and again when the thread's page arrives from the computer.

* fix(ios): don't pull a reader back down when a thread's page arrives late

The settle scroll re-runs when the thread's page arrives, which can be
seconds after the push (slow route, reconnect re-hydration). If the reader
has already scrolled up to read, that scroll yanked them back to the end.
Remember a drag via onScrollPhaseChange (iOS 18+, no-op below) and skip the
late scroll once the reader has moved; reset on thread switch.
deploy-composio-broker runs on every push to main, but without the
CLOUDFLARE_API_TOKEN secret wrangler aborts and the run ends red, so a
missing secret looks like a broken main. Check the token in a first step
and run the remaining steps only when it is present; otherwise emit a
warning annotation saying the broker was not deployed.

The job-level condition, its dependency on control-plane and its absence
from the merge gate are unchanged, and the self-test now pins the guard.

Refs milind-soni#1914

(cherry picked from commit 8a0655b)
* feat(sidebar): add cross-bot Pinned threads section

Implements the feature requested in milind-soni#2142:
a global, cross-bot/cross-room aggregate of every pinned thread,
alongside the existing pinned-bots section. Reuses the AttentionThread
shape and jump action crossBotAttentionThreads already established.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(sidebar): make the Pinned threads panel collapsible

Give the new Pinned threads panel the same chevron toggle every
bot/group section already has, via the existing collapsedSections
mechanism (sidebar-preferences.ts) and a new fixed id
(PINNED_THREADS_SECTION_ID). Reuses the existing generic
sidebar.section.expand/collapse i18n strings, so no new translations
are needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ni#2110)

Copying a failed or missed routine.run card gave the headline plus the
(often stale) summary, never the error the card actually shows, and the
error text sat outside SelectionContainer so it could not be selected.

Include the error in MessageActions.copyableText and wrap it in
SelectionContainer. Follow-up to the cross-bot/expansion items from
milind-soni#1903, which main already covers via RoutineRunCardView (8caa6bf).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ownloads

Reveal completed downloads in the file manager
milind-soni#2125)

The Browser panel's expand button only ever called requestFullscreen(). On
macOS a full-screen element takes the whole window into native full screen,
where the yellow minimize button and Cmd-M are disabled, and clicking the
same button again did nothing — its label and icon never changed. The only
exits were Esc, the hidden green button or View > Toggle Full Screen, none of
which the app pointed to.

The button is now a toggle, like the desktop viewer's: a second click calls
exitFullscreen(), and a fullscreenchange listener keeps its label ("Full
screen" / "Exit full screen") and icon in step with the document, so Esc and
the window's own controls leave it right too. A failed exit says to press
Esc.

Fixes MOCA-266

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(routines): timestamp run thread titles

* test(routines): cover timestamped room task titles
* feat(rooms): show live computer/browser session indicator

- GroupView.tsx: render "screen" messages (live screenshot frames)
  inline, same as ChatView already does in 1:1 chats.
- GroupView.tsx: give a busy member's avatar badge the place icon
  (PlaceIcon) instead of a generic dot when that bot is actively in a
  concrete computer/browser place, mirroring the 1:1 composer's
  PlaceChip live indicator.

Fixes: a bot running a background browser/computer session inside a
room gave no visual indication at all that it was happening, even
though the server already posted the screen messages to the room's
own thread.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(rooms): give the busy-member place badge an accessible label

CodeRabbit flagged on PR milind-soni#2119: the place icon on a busy avatar badge
conveyed its meaning visually (which computer/browser place a bot is
in) but had no screen-reader-accessible text. Reuse the same
place.chipAria label PlaceChip already uses for the 1:1 composer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
milind-soni and others added 27 commits October 2, 2026 17:45
…and-release-lane

ci: keep main to one running CI, and give releases their own CI lane
…d-pr-triage

Integrate reviewed tools, skills and workspace fixes (Oct 2 batch 2)
… prompts for payers

App half of the purchase/activation audit (H3, H9, H10, H11, H12, M6, M8,
M9, M17, M18, L3, L8, L9, L10, L11):

- One rule (src/lib/cloud-plan.ts) decides what every surface says about
  the plan. An offer to buy goes only to someone signed out (who is asked
  "Already have a Cloud plan? Sign in" first) or verified free with no
  Cloud and no payment being linked.
- The Cloud session check no longer blinks to "unavailable": an answer
  counts for 15 minutes, a failed check keeps the last verified snapshot
  and says "Checking with OMB Cloud…" after two failures; a longer outage
  still names the plan last verified (a display-only hint).
- An ended sign-in asks to "Sign in again" (signInAgain(): one step, never
  "signed out" in between), keeping the plan named.
- Move to Cloud measures against the plan's largest disk, asks the Admin to
  grow the disk when it must (POST /api/cloud/desktop/disk), and explains
  plainly when it cannot fit or cannot grow yet.
- Server menu "My Cloud" connects directly; the desktop app pairs a
  /pair#code= link without a second click; a Cloud's pair page says where
  its connection starts (capabilities.cloudHome).
- Setup steps, payment-being-linked, plan on the Cloud's own Settings,
  visible sign-in code, after-move routines and phone steps, "+ tax".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… no disk growth assumed

- Settings → OMB Cloud on a server in the app's window shows only on an OMB
  Cloud home (config.cloudHome); main answers cloud-plan:* for the Cloud this
  account last verified too, so an unavailable or ended sign-in says
  "checking" or "sign in again on your computer"; a refused state shows where
  the plan is managed with no buttons that would fail.
- Only the Admin's own JSON invalid_token ends a sign-in; a 401/403 page from
  in between (Cloudflare, a proxy) is a failed check, never "sign in again",
  and never counts as a revoked token on sign-out or a failed device sign-in.
- Move to Cloud counts on a larger disk only when the Admin says it grows
  (cloud.disk); otherwise today's free space decides and the Cloud's card
  offers no move it would refuse. An Admin that cannot grow the disk gets its
  own code and copy with no "try again".
- "A plan with a larger disk" only when the move exceeds the plan's whole
  disk, and never on Max; otherwise "make room on your Cloud".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t opens

Customers reported switching every thread a Chief of Staff opens with a
teammate from "Ask for approval" by hand. Only Full access flowed down; a
Chief on Approve for me or Auto-accept edits left each teammate thread at
the teammate's own level (Ask by default).

delegatedApprovalMode replaces delegationInheritsFullAccess: work a Chief
delegates (coordinate_bots, delegate_bot, a room handoff) starts at the
level of the Chief conversation it came from. It only raises (a teammate on
a higher level or on Custom keeps its own), a Chief on Ask passes nothing
on, and the teammate's engine caps it, stepping down to the next level it
has (no Full: Approve for me; Codex has no Auto-accept edits). The thread
says where its level came from and the person can still change it there.

Tests: unit cases for every rule; a real-server e2e where a Chief on
Approve for me brings a teammate in and the teammate's run starts in auto,
while a Chief conversation on Ask leaves its teammate on Ask (removing the
wiring fails it); 38 room/group/delegation/team files and the Chief,
coordination and Full-access suites pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-flows-down

A Chief of Staff's approval level flows down to the threads it opens
…se-flow-app

Cloud after purchase: steady plan status, calm sign-in expiry, no buy prompts for payers
…he Meadow skin (milind-soni#2173)

* feat(skins): add Meadow, the green MausBot skin

A ninth skin beside the original eight, which are untouched: white page,
faint-green sidebar, mint selected row, green accent and a green-black
user bubble with its own inverted @scope context. The contrast gate now
measures both inverted bubbles (Daylight and Meadow).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: add the Advanced mode preference

Simple is the default for a fresh install; an existing install keeps
Advanced so an update never hides a control someone already uses. The
default is settled before applySkin writes its own first-run key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(settings): Advanced mode switch in Appearance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(chat): lock usage and inspector behind Advanced mode

In Simple mode the header's More menu still lists Usage and Inspector,
disabled under an "In Advanced mode" heading, and an inspector left open
closes when Advanced mode is turned off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(model-picker): plain-words picker in Simple mode

Simple mode opens the model picker on provider cards, models by name with
a one-line hint, effort as Quick / Balanced / Deep / Max, and a "use for
new chats too" checkbox in place of the scope toggle. More shows today's
full picker in the same popover; Advanced mode opens on it directly.
The friendly steps map one-to-one onto the real effort levels, and a level
a bot already uses is never hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): put the Advanced mode switch at the top of General

Appearance was too hard to find. A paired remote client has no General
page, so it keeps the switch in Appearance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(bot-settings): Details/Library bot panel in Simple mode

In Simple mode the bot settings panel opens on the bot's avatar and two
tabs. Details has Name and Job, the standing instructions (SOUL.md, same
save path and drift banner), "Ask me first" / "Decide for me" mapped to
the ask and auto approval levels (with the local-computer Auto warning),
the bot's skills with the same review-gated on/off switch, and an
"All settings" row that opens today's fold-out view with a Back link.
Library lists images and files the bot made in the open conversation and
jumps to the message; otherwise an empty state. Advanced mode is unchanged.

SkillsSection's skill state and review dialog move into useManagedSkills
and SkillReviewDialog so both panels share them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(computer-panel): Simple-mode tabs, Works-on grid and centred tab bar

Centre the panel's tabs as a pill with close pinned right (both modes).
Simple mode shows Computer | Browser | Files: the Browser tab stays when
the built-in browser is off and offers to turn it on (same PATCH
/api/config as Settings > Experimental, plus the bot's own switch); Files
shows the working folder and the files this chat changed, read from the
turn digests. The Computer tab gets one Take control / Full screen / Sleep
row and a "Where {name} works" 3x2 grid that runs the same selection
code as the Works-on picker, including the auto-approve warning for this
computer. VPS start/prepare/replace, the backend picker, VM settings link,
two desktops, Delete VM, the gear and the Routines card stay Advanced-only;
VPS setup becomes one line with a Show advanced controls button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(routines): week grid with a details drawer and a plain editor

The Automations page becomes Routines: one header with the date arrows,
range and Today, a Day / Week / List switch, Run logs, and New routine.
Webhooks stay in the header in Advanced mode only.

Grid chips take the bot's colour as a soft tint with its avatar; paused
routines keep their upcoming slots, dashed and labelled Paused. Picking an
occurrence rings it and opens a right-hand drawer (an overlay on a narrow
window) with the bot, the schedule in words, what to do, where it runs,
where results go and the last three runs, plus Run now, Pause/Resume, Edit
and the existing join / open / cancel actions. It replaces the details
popover and sends the same requests.

In Simple mode the editor asks only what should happen, who does it, when,
Once / Every day / Weekdays / Every week, and the instructions; everything
else sits behind More options, which opens by itself for a routine that
already uses one of those settings (a cron schedule, Boat runner, run
limit, team goal, attachments). Advanced mode keeps today's editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): liquid glass surface for centred pop-ups

Adds .glass-surface, .glass-card, .glass-rail and .glass-scrim, built only
from each skin's own menu, card and ink tokens, so all nine skins get a
glass of their own colour and no skin block changes. Reduced transparency
falls back to the opaque menu colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(settings): glass pop-up with You / AI / Computers / Account groups

The rail now files every page under a labelled group; the narrow-window
picker uses the same groups. Section ids, visibility filters and
toggleAppSettings deep links are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(apps): one-view Apps pop-up with tiles and your MCP servers

Title Apps, search in the header, All / Connected / MCP servers chips,
a three-column tile grid with Connected state and used-by avatars, and
the MCP servers section on the same scroll with an on/off switch per
server. Connect, accounts, grants and every notice reuse the old logic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(triggers): Triggers pop-up for webhooks

When [source] -> [bot] should [instructions] creates through the same
POST /api/webhooks; the list shows name -> bot, deliveries, Copy link
and an on/off switch, with the editor, event types, rotation and the
delivery log under Advanced options. Webhook actions move into a shared
hook so the Automations tab keeps working unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): Routines, Triggers and Apps as direct footer rows

Replaces the hover Tools menu. Advanced mode keeps Team map behind a
small Tools menu (an icon in the avatars-only density); the failed
routine dot and the guided tour anchors stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(chrome): one sidebar top row on the traffic lights; call from the composer

- Sidebar header is a single row centred on the macOS traffic lights
  (trafficLightPosition y=16, 14px buttons -> 46px row): light inset,
  compact inline server switcher (icon, truncating name, tiny chevron),
  draggable spacer, buttons as no-drag. Windows/Linux get a 48px row; the
  icons rail still stacks with the icon-only switcher beneath.
- Simple mode keeps only "+" in that row (Expand stays on an icons rail);
  Advanced keeps collapse and Active Threads.
- Chat header: All threads picker is Advanced only; the call button moves
  out of the header into the composer beside dictation as a Send-sized
  waveform circle with the same start/hang-up/help behaviour and telemetry.
  Rooms keep their header group call button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(model-picker): Simple picker in columns with effort along the bottom

Providers sit in a quiet column on the left (More is its last row and
carries the "All models, API keys, local models" label), the chosen
provider's models fill the right, and a band across the bottom holds
"How hard should {bot} think?", the new-chats checkbox and Manage AI
accounts. The Simple popover widens to 520px, still clamped to the
window; the full picker keeps 420px. Adds an empty state for a provider
with no models to list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(settings): five-page Simple settings and safe-area glass pop-ups

Simple mode's Settings rail now has at most five pages: General,
Appearance, AI (Model providers, API keys, Decision model stacked under
headings), Computers (Remote access, Servers, Local VM, Built-in browser)
and Account (OMB Cloud, Organization, People, Activity). Usage, Backups,
Experimental and Installations stay Advanced-only; a deep link to one of
them still opens it as a temporary page, and every other section id maps
to its combined page and scrolls that section into view. The remote-client
reduced set and the Advanced grouped rail are unchanged.

The built-in browser switch moves out of Experimental: it heads the Local
VM page in Advanced and is its own block after Local VM on Simple's
Computers page, writing the same features.browser setting. Browser
profiles move with it; Experimental keeps skill drafting.

Settings, Apps and Triggers sit in a safe area that clears the macOS
traffic lights and Windows caption buttons (56px top, 24px elsewhere),
at min(1040px, 100% - 48px) by min(760px, 100% - top - 24px). The Apps
grid sizes its columns by container width, and MCP server rows and the
MCP header wrap instead of overflowing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: browser on by default with its own tab switch; leaner header and sidebar

- The built-in browser is on unless switched off (an explicit false is
  kept). The computer panel always shows the Browser tab, with a switch
  that turns this bot's browser on or off.
- The chat header no longer carries All threads; threads live under each
  bot in the sidebar.
- Team map is its own sidebar row in Advanced mode; the Tools menu is gone.
- The sidebar's top buttons are smaller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(place): Auto as the conversation default; no place chip in Simple

Simple mode no longer renders the composer's "Where this conversation
works" chip: every conversation follows its bot's Works on, which is
Auto by default. Advanced keeps the chip.

For a bot on Auto, the chip's first row now reads "Auto - Picks the
right place for each task" and stays selected while the conversation
has no pin, instead of "Follow this bot's setting - Currently Auto". A
bot set to a specific place keeps the "Follow this bot's setting" row.

The Simple "Where {name} works" card already showed a pin note ending
"Change it from the composer", which pointed at the chip Simple no
longer has. In Simple it now only names the place, using the grid's
labels. There is no one-click "Use Auto" there: the wire task cannot
tell a person's pin from one Auto recorded (surfaceSource never leaves
the server).

New bots already start on Auto on every creation path (New bot dialog,
saved defaults template, POST /api/bots, Chief-created bots, package
import), so no creation code changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): right-align the server switcher beside the top buttons

- Top row order is now [lights] [drag space] [server] [buttons]: an empty,
  draggable flex-1 spacer pushes the compact switcher (max 140px, truncating,
  chevron kept) up against the buttons, in Simple and Advanced alike.
- macOS: the lights span 16-76px on macOS 26; their reserved space is now
  72px from the row's 16px inset, so nothing clickable (and no hover
  background) starts before 88px, 12px clear of them. The browser build's
  placeholder dots keep the same 12px.
- Right padding 16px -> 8px, so the last button sits 8px from the sidebar's
  edge; buttons stay 2px apart and 2px from the switcher.
- In a tight row the switcher gives way first and its button clips instead
  of spilling onto the buttons; the buttons never shrink or wrap. Its error
  note now hangs from the row's right end, so it stays inside the sidebar.
- Windows/Linux get the same right-aligned order with 16px left padding;
  the icons rail keeps its stacked layout.
- Sidebar.header.test.ts: order, padding and clearance assertions follow the
  new layout; the Windows/Linux case renders with its real host label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(model-picker): Simple picker lists every provider, names only, full-width effort

The Simple picker's provider column now lists every provider the picker
knows (sign-ins, then API keys, then local engines) with no top-3 cap; an
API-key provider carries a small key so a sign-in and a pasted key for the
same vendor read apart. The More row is gone: a provider shows its
suggested models and a quiet "Show all {count} models" row opens the whole
list in place, with a search box once it passes 12 models. A provider's
local models follow its own (loaded first), and a local engine lists its
models the same way. "Set up" on a provider that needs setup is now the
only way into the full picker.

Model rows show names only, truncated with an ellipsis; a blurb is the
row's tooltip. The popover narrows to 380px with a 128px provider column.
The "How hard should {bot} think?" label is gone and the effort steps (or
a named-variants select) span the whole bottom band, labelled "Reasoning
effort" for assistive tech. Escape in the Simple view closes the popover
instead of switching to the hidden local pane.

Removes the now-unused model.simple.more, model.simple.moreOptions and
model.simple.think strings; the no-models note no longer mentions More.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(call): set up a voice in a pop-up; keep Apps bot avatars in their rings

Call help: when a call can't start for want of a voice, the help card's
"Open agent settings" (which dropped Simple-mode people into the bot's
full Advanced settings) is now "Set up voice". It opens VoiceSetupDialog,
a centred glass pop-up over the chat holding the bot's own VoiceSettings
card (engine, API key and Save, model/server, voice picker, read replies),
saved through the same useBotSettingsDerived patch as the Voice section.
"All voice settings" closes it and opens the bot's full settings at Voice.
If a call becomes possible while it is open, it says so and offers Start
call, which starts the call exactly as the call button does. Composer,
header and room placements share it; in a room it moves member by member
and stays on the last. "Choose This computer" is unchanged.

Apps pop-up: a "used by" mascot fills its whole square, so drawn at the
ring's own size its corners poked out of the white circle. Each avatar now
sits in a fixed 20px clipping disc and is drawn at 16px, which keeps every
mascot body, and uploaded images, inside the ring in every skin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(browser): take control implicitly; drop the Take control button

The Browser tab no longer has Take control / Return to bot. The page and
toolbar are usable whenever the view is connected and no other window
holds the browser:

- The first real interaction (click, scroll, key, paste, address submit,
  back/forward/reload, tab actions, Type or paste text) sends take and
  buffers itself and everything after it until the take resolves, then
  flushes in order through the input queue and execute(). Hover, a lone
  modifier or a stray release never takes control. A refused take drops
  the buffer and shows the server's message in the error bar.
- After 8 idle seconds with no key or button held, the panel drains its
  input and sends release so the bot carries on. Every input restarts
  the wait. Closing the panel hands back first when nothing is held or
  unsent; the server's disconnect cleanup covers the rest.
- While this viewer holds the browser a small status chip (hand + "You're
  using the browser") sits where the button was; a take slower than
  300ms shows "Waiting for {bot} to finish...". Another window's hold
  keeps the paused live view. The ⋯ menu is unchanged.

The server's exclusive lease is untouched: person and bot input still
never interleave. The lease logic lives in src/lib/browser-control.ts,
one controller per live-view connection. The input queue gains settle()
and size() so a long buffer is fed in batches under its 32-item halt,
and its overflow message no longer says "Release control".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(place): Simple notes only a person's pin, with a way back

Review fixes for the Simple place change.

Clients can now tell an Auto pin from a person's. The task wire gains
surfaceAuto, which toWireTask derives from the private surfaceSource
and never stores. surfaceSource itself stays on the server.

Simple's "Where {name} works" card said "This chat is pinned to ..."
under every pin, including the ones Auto records on a chat's first
computer or browser turn. Those are most pins, and the person never set
them. The note now shows only for a pin that keeps the chat away from
the grid's choice and will not move with it: a person's pin, or one
from before the server recorded who set it. It stays hidden when the
pin matches Works on or Works on is Off. The composer chip is gone from
Simple, so the note gets a "Use {place}" button. It clears the chat's
pin, and the chat follows the grid's choice again. The button is
disabled while a turn runs, since the server refuses that change (409).

The client reducer drops surfaceAuto when the person pins or unpins a
chat. A person's choice is never marked as Auto's, even before the
server answers.

Surface prompts no longer send the person to "the conversation's
computer selector", which Simple does not show. They name the Computer
panel, which both modes have. The select_computer variant now swaps a
shared constant, so the two wordings cannot drift apart.

Advanced keeps its chip and its note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sidebar): show the server switcher as icon + chevron when the top row is tight

Review follow-up to ef4a4ab. On macOS in Advanced at the default 320px
sidebar the slot between the lights and the three buttons is only 121px,
under the switcher's 140px cap, so the drag spacer collapsed to 0 and the
pill still ran from just past the lights to the buttons ("This com..."):
a 12px nudge, not the move to the right that was asked for.

- The spacer and the switcher now sit in one slot that is a named size
  container (sidebar-top). Below 164px (the 140px cap plus a 24px drag gap)
  the pill drops its name and shows icon + chevron; title and aria-label
  keep the full name. Same pattern as the chat header's chathead container.
- Result on macOS: Advanced 320px puts the pill 90px past the green light
  with a 78px drag gap; compact 272px 42px past it; Simple 320px and
  Advanced from about 364px keep the full name with at least 24px of gap.
  At the 240px minimum the pill sits at the 12px clearance, chevron intact.
- The error note still anchors to the row (container-type does not make
  the slot its containing block) and stays inside the sidebar.
- Sidebar.header.test.ts: the macOS and Windows/Linux row tests now require
  `relative` on the row, so the note cannot silently lose its anchor; the
  macOS test checks the slot container, the name's container variant and
  that the title keeps the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: Simple picker reaches every account and local model again

Review fixes for the Simple model picker.

- Every sign-in account is reachable again. When a Claude or OpenAI row
  has more than one account (two Claude accounts, or Codex and the
  ChatGPT plan), an Account select sits above its models. The row opens
  on the account last browsed, else the bot's, else the first one ready
  to use. The Advanced rail is unchanged.
- A signed-out engine that lists local models keeps that list when its
  row is clicked again: what needs setup no longer depends on the hidden
  full picker's pane. A quiet "Sign in for {name}'s own models" row leads
  to the full picker's sign-in.
- Browsing to a provider that runs local models, or opening its whole
  list, looks for local models again. A provider with nothing to list
  offers "Check again".
- A provider the organisation blocks is dimmed, is named "Managed by
  {org}", and shows the policy text instead of Set up.
- Models that share a name (pi's routes) show their route as muted text,
  and every row's tooltip and spoken name carry the route.
- The API key is a badge on the provider mark, and "(API key)" or
  "(API)" leaves the name, so OpenRouter, Mistral and the Claude key fit
  the 128px column.
- "Show all" stays in place as "Show suggested only" once open, so
  keyboard focus is not lost.
- The named-variants select keeps a short "Reasoning" label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep paired desktops on remote voice settings; focus the voice picker once a key is saved

Paired desktops: on a desktop paired to another computer, the call help's
"Set up voice" opened the pop-up, whose saves the pairing refuses (the bot
update is PATCH /api/bots/:id, a 404 there; engine and key writes are PUT
/api/config, a 403), and which had no "Voice output on this Mac" switch.
There the button now opens the remote agent settings again, as "Open agent
settings" did (a room's member chat first). Those save through the host's
/profile route with the card locked. The pop-up stays for everyone else.

Focus: the pop-up started in the API key field whenever the engine has one,
even with the key saved and only a voice missing. It now starts on the voice
picker when VoiceSettings shows it (the engine is set up), else in the first
field (the key, or the Chatterbox server). The voice select carries
data-voice-picker for this.

Tests: paired-desktop cases for a bot's chat and a room; the voice-picker
focus case; and a check against the real VoiceSettings that the picker is
drawn only once the engine is set up, after the key field. The existing
key-field focus test now stubs lookups per selector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: Simple picks become the bot's default; Default model in the bot panel

- The Simple model picker drops "Use for {bot}'s new chats too": every
  model, effort or reasoning-mode pick also sets the bot's default, so a
  new thread starts on what was chosen last. Existing threads keep their
  model. Advanced keeps its own scope choice.
- The bot panel's Details tab gains "Default model" between Instructions
  and "Before {bot} acts": the same plain-words picker, shown in place
  (contained) so the scrolling panel cannot clip it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: browser take-over review fixes (stale input, status pill, Tab, hand-back)

Review fixes for implicit browser control (no Take control button).

- Stale input: the take reply now says whether the grant waited for the
  bot's own browser action (BrowserRuntime.take resolves true when agent
  work was running). If it did, clicks, keys and page-relative toolbar
  actions made while waiting are dropped instead of replayed on a page
  that may have moved; an entered address or New tab still runs, and the
  releases of dropped presses are swallowed. A pill says "{name} was busy,
  so that wasn't sent. Try again." until the next interaction.
- Status at any width: the status moved from the toolbar into a pill over
  the top of the live view (pointer-events-none), so "You're using the
  browser" and "Waiting for {name} to finish..." show in words at the
  default 400px panel. The hint no longer promises the bot carries on:
  "{name}'s browser tools are paused while you use it. Control goes back
  to {name} after a few seconds without input."
- Repeats during a take: toolbar buttons, the address bar and Restart wait
  out a take as they wait out a running action, and the controller keeps
  at most one buffered toolbar action.
- macOS shortcuts: keys the server sends as one whole press (shortcuts and
  discrete keys) no longer count as held, so Cmd+C no longer blocks the
  hand-back (macOS sends no key-up for a key let go while Cmd is down).
- Keyboard: Tab and Shift+Tab pass over the live view until the person
  clicks or types in the page, so tabbing through the panel neither traps
  focus nor takes the browser from the bot.
- Typing dialog: control stays with the person while "Type or paste
  text..." is open; closing it restarts the idle wait.
- Profiles: opening the switcher hands an idle hold back at once (or as
  soon as a running action ends) instead of after 8 seconds, and the
  dialog says why it is locked while another window holds the browser or
  an action runs.
- Server refusals no longer point at a Take control button: a browser an
  interrupted action left uncertain says "Choose Restart browser... in the
  browser menu"; other refusals say "Browser control changed. Try again."
- Docs: the permissions page and the browser verification checklist
  describe the new hand-back and stale-input behaviour.

Tests: new controller, panel, viewport, runtime and live-server cases for
each fix, mutation-checked. Updated because behaviour changed: the status
chip test (now the pill, aria-description instead of title, new hint) and
browser-live's "Take control" expectation (now "Browser control
changed"); the browser-live runtime fake gained interrupted().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(browser): route gates under the browser-on default

The owner-only route test assumed the built-in browser was off by default.
It now checks both gates: on (the new default) reaches the missing-engine
gate, and a browser switched off still refuses first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(smoke): expect the browser on by default in the packaged-server smoke

This branch turns the built-in browser on unless someone switches it off,
but the packaged-server smoke still required a fresh home to report the
browser off ("access remains opt-in"). Package Windows failed on it, and
release.yml runs the same check for macOS, Windows and Linux, so the next
release would have stopped there.

The check stays as strict: a fresh home must still find the bundled engine
at the pinned version, and must now report the browser exactly on.
docs/browser-packaging.md said the same old default and is updated too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): address Simple mode review regressions

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: milind-soni <milindsoni201@gmail.com>
# Conflicts:
#	src/components/CallView.tsx
#	src/components/ChatView.tsx
#	src/locales/en.json
…d-mobile

feat: integrate reviewed Live calls on desktop, iOS, and Android
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Milind Soni <46266943+milind-soni@users.noreply.github.com>
…ilind-soni#2188)

* feat(phone): Connect your phone, to this computer or your Cloud

A paying Cloud customer could only find this computer's phone pairing. It
was four clicks deep for a Cloud (Server menu, My Cloud, Settings, Remote
access) and nothing said where a phone would connect.

- Account menu: "Connect your phone" says where the phone will connect
  (to this computer, to your Cloud, to this server). It opens Settings ->
  Remote access, scrolled to the pairing that fits the window, with focus
  on Pair your phone / Create pairing code. On another server it is shown
  only to a session that may make a pairing code there.
- "Get OpenMausBot for iOS" becomes "Get the phone app": iPhone (App
  Store) and Android (APK), each as a QR code with its link.
- Settings -> OMB Cloud, paid plan: "Use your Cloud on your phone" opens
  the Cloud in this window on its own phone pairing. It uses a new
  no-argument cloud-account:connectHomeForPhone IPC that adds the fixed
  ?desktop-settings=phone, and the pair page carries that on. Before the
  Cloud is Ready, or if opening it fails, the card gives the two steps.
  On the Cloud itself the same button opens the pairing directly.
- Docs: a three-step "Use your Cloud on your phone" section in
  docs/ios-companion.md and docs/cloud-pro.md.
- Strings translated in all nine packs; source-hashes accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(phone): on this computer, offer a paid Ready Cloud first

The customer who asked was on their own computer with a paid Cloud. On this
computer, "Connect your phone" now offers both destinations when the
verified snapshot shows a paid plan (any tier) and a Ready Cloud:

- "to your Cloud (always on)" first, doing what Settings -> OMB Cloud's
  "Use your Cloud on your phone" does (connectHomeForPhone: the Cloud opens
  in this window at its phone pairing). If that fails, Settings -> OMB Cloud
  opens, where the card says what to do.
- "to this computer" second: the local pairing as before.

A paid plan whose Cloud is not Ready keeps the single "to this computer"
line, with a note that the Cloud will show there. Free, signed out, lapsed
or unverified accounts see only "to this computer". On the Cloud itself and
on other servers nothing changes. Get the phone app's dialog offers the same
destinations in the same order.

Menu items get an optional third line (a note), drawn in the tertiary ink.
Two strings added in all ten languages; source-hashes accepted. Docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Real merge of milind-soni/OpenMausBot main (4ed952a..ff01be8, 35
upstream pull requests). Our behavior wins on conflict; upstream
improvements layered in. Locales merged as a union of keys, rebrand pass
run, baseVersion 0.1.94.

Kept out or guarded: upstream Cloud flows (Connect your phone to your
Cloud, purchase, plans) stay behind CLOUD_SERVICES_ENABLED; upstream
App Store and pricing links emptied; Simple mode stays off (Advanced is
the only exposed experience); upstream Live call UI and iOS Live call,
iOS Local VM take control and the week grid drawer not taken.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hd92V2CX6HCgfX8oYnCgMz
Every /api/live route answers 404 and no OpenAI session can be created
unless the server runs with SAGAX_LIVE_CALLS=1; LiveSettings carries
`enabled` so clients know. Android shows its Live call button only when
the computer reports Live calls enabled. Our voice call engine stays the
call path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hd92V2CX6HCgfX8oYnCgMz
Locales merged as a union of keys, #117's strings winning. Upstream's
skills library stays inside the bot's Library > Skills (behind
features.skillsLibrary); its separate Settings page is not mounted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hd92V2CX6HCgfX8oYnCgMz
…nts allows, Sagax data dir in upstream tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hd92V2CX6HCgfX8oYnCgMz
…cal VM image cannot capture the guest turn

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hd92V2CX6HCgfX8oYnCgMz
@jencryzthers
jencryzthers merged commit d322dda into main Oct 3, 2026
2 of 4 checks passed
@jencryzthers
jencryzthers deleted the chore/upstream-sync-2026-10-03 branch October 3, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.