feat(bots): cross-platform bot creation + desktop connector reliability#266
Merged
Conversation
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>
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>
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.
Follow-on work on the same branch after #265 merged. Five commits.
What's in it
02a5fe17e2d99bacfd5b7bf727872bfb--configon every connector restart4209abdf02a5fe17— the stranded-bot bugOnboarding 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:
AgentPresetgainsargs, so opencode renderscommand = "opencode", args = ["acp"].detect_agentsis dropped — it accepted a commandabsolutize_adapter_commandstill 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 clientsCreating 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:
bridge_provideris 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.bridge_connectedinstead 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 runkubectl port-forward.DESIGN.mdalready bans raw field names in UI copy; this pays that down. Code comments keep the precise terms.27872bfb— the connector fixThe CLI's
restartfails with "restart requires --config" when there is no priordaemon.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 callsensure_workspace_dirs_atfirst, so starting from a hand-picked.tomlis zero-prep too.4209abdf— the UI changeOne 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.tsxas self-contained SVG app tiles (no network, no asset files).Verification
cargo test --libconnector_configregression tests)tsc --noEmitPdfViewerbaseline errors only)cargo check --all-targetscargo testnpm run devbuildxcodebuild(simulator)The
02a5fe17root 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
e2d99bacwas 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.cargo testcovers the pureworkspace_dirs_*helpers; it does not reach the three new--configcall sites. Testing them needs a dev Tauri instance, which would have collided with the running productionCheers.app. Worth a local pass on "fresh onboard → restart" before merge.fd5b7bf7— this session only confirmed it builds and launches.02a5fe17prevents the failure; it cannot recover a bot whose token was already overwritten. Those need re-onboarding.Note
openerIcons.tsxheader 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