Skip to content

feat(bots): cross-platform bot creation + desktop connector reliability#266

Merged
haowei2000 merged 6 commits into
developfrom
fix/desktop-connector-zero-prep
Jul 19, 2026
Merged

feat(bots): cross-platform bot creation + desktop connector reliability#266
haowei2000 merged 6 commits into
developfrom
fix/desktop-connector-zero-prep

Conversation

@haowei2000

@haowei2000 haowei2000 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Follow-on work on the same branch after #265 merged. Five commits.

What's in it

Commit Change
02a5fe17 connector: resolve the opencode adapter before minting a token
e2d99bac bots: make creating a bot work the same way on web, macOS and iOS
fd5b7bf7 ios: unstick touch input, tame auto-scroll, finish channel management
27872bfb desktop: carry --config on every connector restart
4209abdf desktop: split-button "Open in …" with app-tile icons

02a5fe17 — the stranded-bot bug

Onboarding a bot as "opencode" left it permanently unable to sign in (4401 invalid or revoked token).

The gateway preset named opencode-acp, a binary that does not exist — OpenCode exposes ACP as a subcommand of its own binary (opencode acp). The comment on that preset said "best-effort name; verify against your build", and it never was.

That alone would only be a failed setup. The damage came from ordering: redeeming an enrollment code mints a new bot token, destructively replacing the old one and kicking any live connector. The desktop app then failed while writing the config, because it could not resolve the adapter — after the token had already rotated. Net effect: the gateway expects a token no host holds, the connector that was working is offline, and the single-use code is spent. Trying a different agent type on an existing bot was enough to break it.

Three parts, and fixing any one alone is insufficient:

  • AgentPreset gains args, so opencode renders command = "opencode", args = ["acp"].
  • The adapter is pre-flighted before any server call, keeping "not installed" a side-effect-free error.
  • The opencode fallback in detect_agents is dropped — it accepted a command absolutize_adapter_command still refused, so detection and the write path disagreed about what "installed" meant. Left in, it would have let the new pre-flight wave the same broken config straight through.

e2d99bac — bot creation across the three clients

Creating a bot had two implementations sharing no code, and iOS had none. Underneath, the only real variable is where the connector runs; this makes that the explicit question.

Safety, since these paths mint irreversible credentials:

  • The agent type now travels with the bot. bridge_provider is read instead of re-defaulted — a bot created as codex was getting a claude connector, and nothing downstream catches that, because the connector runs whatever adapter the config names.
  • The bot is created when a mode is picked, not on Continue — backing out used to leave an orphan bot with no delete affordance.
  • The wizard ends on live bridge_connected instead of a Done button that closed the dialog and confirmed nothing. The step was already called "Connect".

iOS can now create a bot and hand it off, but never hosts a connector — a phone cannot run a long-lived ACP child process. The steps mirror the web wizard; the carrier changes, because a shell one-liner is useless on a phone: the code renders as a QR, configs go through the share sheet.

That needs somewhere to land, so macOS gains "I have a code" — the missing half of the remote-machine story. The desktop could previously only mint and redeem for itself in one shot, so a code from anywhere else had nowhere to go.

Also drops jargon from user-facing copy: "Rust ACP connector", "Agent Bridge token", bot_token_env, "0600", and a reachability warning that told whoever was adding a bot to run kubectl port-forward. DESIGN.md already bans raw field names in UI copy; this pays that down. Code comments keep the precise terms.

27872bfb — the connector fix

The CLI's restart fails with "restart requires --config" when there is no prior daemon.json — exactly the case right after onboarding or after a crash, which is when a revive matters most. Resolves the config path (the daemon's recorded one, else the onboarded ~/.cheers/cheers-daemon.<safe>.toml) and passes it on three paths: connector_restart, connector_add_allowed_roots, and the supervisor's revive loop. Each also calls ensure_workspace_dirs_at first, so starting from a hand-picked .toml is zero-prep too.

4209abdf — the UI change

One icon per installed opener crowded the file header. Now a split button: the remembered app opens on click, the chevron drops down the full list. Glyphs moved to openerIcons.tsx as self-contained SVG app tiles (no network, no asset files).

Verification

Target Check Result
Gateway cargo test --lib 176/176 (incl. 2 new connector_config regression tests)
Web tsc --noEmit clean (2 pre-existing PdfViewer baseline errors only)
Web rebuild → kind deploy → sign in passes, zero console errors
macOS cargo check --all-targets passes
macOS cargo test 20/20
macOS full npm run dev build binary builds and launches
iOS xcodebuild (simulator) BUILD SUCCEEDED
iOS simulator install + launch passes, login screen renders

The 02a5fe17 root cause was confirmed against production logs (two redeems 33s apart on one bot) and reproduced independently with a raw websocket handshake, so the diagnosis does not rest on the connector's own error reporting.

Not verified — please check before merging

  • The bot-creation UI in e2d99bac was never opened. That commit is almost entirely user-visible copy and layout across three clients, and it has only been type-checked and compiled. The web wizard and the new iOS screens both want a visual pass; several strings got materially longer.
  • The connector restart paths were not exercised at runtime. cargo test covers the pure workspace_dirs_* helpers; it does not reach the three new --config call sites. Testing them needs a dev Tauri instance, which would have collided with the running production Cheers.app. Worth a local pass on "fresh onboard → restart" before merge.
  • iOS was not functionally re-tested for fd5b7bf7 — this session only confirmed it builds and launches.
  • Existing broken bots are not repaired. 02a5fe17 prevents the failure; it cannot recover a bot whose token was already overwritten. Those need re-onboarding.

Note

openerIcons.tsx header says the glyphs are "stylized marks, not the exact trademarked logos", but the VS Code glyph is the verbatim simple-icons logo path. Either the comment or the path should change.

🤖 Generated with Claude Code

haowei2000 and others added 5 commits July 19, 2026 17:01
Onboarding a bot as "opencode" stranded it on a token nobody held.

The gateway preset named `opencode-acp`, a binary that does not exist —
OpenCode exposes ACP as a subcommand of its own binary (`opencode acp`).
The comment on that preset said "best-effort name; verify against your
build", and it never was.

That alone would only be a failed setup, but redeeming an enrollment code
mints a new bot token, which destructively replaces the old one and kicks
any live connector. The desktop app then failed writing the config,
because it could not resolve the adapter — after the token had already
rotated. Net effect: the gateway expects a token no host has, the
connector that used to work is offline, and the code is spent. Trying a
different agent type on an existing bot was enough to break it.

- Give AgentPreset an `args` field and render it, so opencode is
  `command = "opencode", args = ["acp"]`. It is a real resolvable command
  now, so it no longer needs the hand-edit banner.
- Drop the opencode fallback in `detect_agents`. It accepted a command
  that `absolutize_adapter_command` still refused, so detection and the
  write path disagreed about what "installed" meant — which would have
  let the new pre-flight check wave the same config straight through.

Both halves are required: fixing either alone leaves the disagreement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a bot had two implementations that shared no code, and iOS had
none at all. Underneath, the only real variable is where the connector
runs — this makes that the explicit question and routes on it.

Safety first, since these paths mint irreversible credentials:

- Carry the agent type through the "Set up on this Mac" hand-off, and read
  an existing bot's bridge_provider instead of re-defaulting. A bot created
  as codex was getting a claude connector; nothing downstream catches that,
  because the connector runs whatever adapter the config names.
- Create the bot when a mode is picked, not on Continue. Backing out of the
  wizard left an orphan bot with no way to delete it from there.
- Pre-flight the adapter before any server call, so "not installed" stays a
  side-effect-free error rather than a spent code and a rotated token.
- End the wizard on live `bridge_connected`, not a Done button that closed
  the dialog and confirmed nothing. The step was already called "Connect".

iOS can create a bot and hand it off, but never hosts a connector — a
phone cannot run a long-lived ACP child process. The steps mirror the web
wizard; the carrier changes, because a shell one-liner is useless on a
phone: the code is shown as a QR, and configs go through the share sheet.

That needs somewhere to land, so macOS gains "I have a code" — the missing
half of the remote-machine story. The desktop could previously only mint
and redeem for itself in one shot, so a code from anywhere else had
nowhere to go.

Also drops jargon from user-facing copy: "Rust ACP connector", "Agent
Bridge token", "bot_token_env", "0600", and a reachability warning that
told whoever was adding a bot to run kubectl port-forward. DESIGN.md
already banned raw field names in UI copy; this pays that down. Code
comments keep the precise terms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anagement

Three real-device bugs and the feature gaps behind them.

**Touch input died app-wide.** The drawer's drag offset was `@State` and only
reset in `.onEnded`, which SwiftUI never calls when a gesture is cancelled — the
system swipe-back steals an in-flight edge drag, the offset sticks non-zero, and
the drawer backdrop stays mounted over the whole screen at ~7% opacity,
swallowing every tap and scroll. `@GestureState` resets on cancellation, which
is what it exists for; `allowsHitTesting(drawerOpen)` is the belt-and-braces.

**Streaming yanked the reader back.** Every `message_stream` delta bumped one
scroll tick that the view honoured unconditionally, so reading history while a
bot typed was impossible. Split into `forceBottomTick` (the reader's own action:
sending, opening a channel) and `followBottomTick` (incoming: honoured only
while already parked at the bottom), plus a jump-to-latest button so suppressing
auto-follow can't strand anyone.

**Workspace switching was long-press only** despite rendering a chevron. A
chevron promises a dropdown, so a tap now delivers one; long-press still works.

**Audit rows carried no information** — every row read "Resolved /
allow_always". Rows now lead with what was actually approved (dug out of
`detail.tool`, never the server's hardcoded `detail.title`), who asked, and the
outcome; the rest moved to a detail sheet.

Channel management was never implemented on iOS (settings/files/workbench all
rendered the same placeholder). Adds the real Members sheet — direct invite, add
bot, invite links, role changes, removal — and Channel settings, with
`canManage` derived client-side from the member list exactly as the web does,
since no DTO carries it. Adds the four missing ViewBoard boards, which needed
the gateway's `resource_req`/`resource_res` verbs implemented on the socket
first (15s timeout, req_id correlation, `board_signal` refresh coalescing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CLI's `restart` fails with "restart requires --config" when there is
no prior daemon.json — exactly the case right after onboarding or after a
crash, which is when a revive matters most. Resolve the config path (the
daemon's recorded one, else the onboarded ~/.cheers location) and pass it
on the restart/add-allowed-roots/supervisor-revive paths, and ensure the
workspace dirs exist first so a hand-picked .toml is zero-prep too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One icon per installed opener crowded the file header. Replace the row
with a split button: the remembered app opens on click, the chevron drops
down the full list. Move the glyphs to openerIcons.tsx as self-contained
SVG app tiles (no network, no asset files) so each opener is recognizable
instead of a generic lucide stroke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@haowei2000 haowei2000 changed the title fix(desktop): reliable connector restarts + split-button "Open in …" feat(bots): cross-platform bot creation + desktop connector reliability Jul 19, 2026
The Desktop Check `cargo fmt` gate failed: collapsing the tuple onto one
line in 02a5fe1 exceeded what rustfmt keeps inline. Formatting only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@haowei2000
haowei2000 merged commit a7a7d38 into develop Jul 19, 2026
12 checks passed
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.

1 participant