feat(dashboard): Telegram & Discord channel toggles with install-state UX - #161
Conversation
…e UX Extends the channel settings panel from the single hardcoded `server:autonomos` entry to a dynamic list fed by `claude plugin list --json`. Users can toggle Telegram and Discord channels on from the dashboard; a toggle is visibly locked when the underlying plugin is uninstalled or disabled, with an in-place tooltip showing the exact fix command. Server side, a new `GET /api/channels/status` endpoint derives a tri-state per channel (ok / disabled / not-installed / unknown-when-detection-fails) backed by a 30s TTL cache with in-flight request deduplication. The `PUT /api/settings` handler now refuses to save channel entries that are malformed by CC's own tag syntax or that would silently no-op at spawn (not-installed or explicitly disabled plugins). Detection failure is tolerated (fail-open) so a flaky `claude plugin list` doesn't block legitimate saves. `server:*` channels short-circuit to "ok" in the status function — they are autonomOS-owned MCP subprocesses, not plugins that `claude plugin list` would ever report. `settings.ts` now sanitizes `channels` on read (filters non-strings and malformed tags with a warning) so bad values written out-of-band don't silently re-persist through `updateSettings()`'s merge, and so non-string entries can't crash the spawn path's `.startsWith()` filter. Refs docs/research/channel-integration.md (Option B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nox-0x
left a comment
There was a problem hiding this comment.
LGTM. Clean implementation — fail-open design is correct, the subprocess dedup + 30s cache prevents runaway processes, and the client-side lock UX prevents config drift. Sanitization on settings read is a good defensive touch.
`/plugin install <id>` is the universal fix — it installs fresh if missing and re-enables disabled plugins in the same step. Using one command across both states avoids the `enable` vs `install` confusion (Terry tripped on this during first-run QA). - Tooltip + server fix field now always returns `/plugin install X` - Docs updated to use slash-command form consistently - Test for disabled-state fix updated Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| @@ -568,7 +638,17 @@ export function SettingsPanel({ | |||
| body: JSON.stringify(body), | |||
| }); | |||
| if (!res.ok) { | |||
There was a problem hiding this comment.
🟡 Warning
Problem: When the server rejects a channel save with a validation error (e.g. plugin not installed), calls and , but stays in the user-toggled (rejected) state. The toggle UI continues to show the channel as pending-ON while the error banner says "Refusing to save channels that would silently no-op" — the toggles don'''t reflect the failure.
Why it matters: The error message is misleading: the banner says the save was refused, but the toggle still shows the channel as enabled. If the user dismisses the error and re-opens settings, is immediately overwritten by the fresh load, so it'''s a one-session desync — but the UX is confusing in that window.
Suggested fix:
"Failed to save (HTTP ${res.status})"
nox-0x
left a comment
There was a problem hiding this comment.
Good PR overall. The channel install-state UX is well-designed — three-state model, lock UX, fail-open on unknown, and server-side validation before persisting are all correct. Left one inline comment about a minor pendingChannels state-desync on save failure.
Telegram and Discord plugins each enforce a single-poller lock (per ~/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6/server.ts lines 56-69: PID file + SIGTERM eviction). With autonomOS resuming ~10 sessions in parallel at startup, every plugin:* channel would cause those sessions to race for the lock, producing nondeterministic "whichever-won-last" routing for inbound DMs. This change introduces `settings.inboxAgent` (default "Dispatcher") and filters plugin:* channels at buildArgs() time: - The session whose agentName matches inboxAgent → gets --channels plugin:* AND --dangerously-load-development-channels server:* - All other sessions → get --dangerously-load-development-channels server:* only, keeping the autonomOS gateway available for inter-agent communication Dashboard settings panel gets an "Inbox Agent" text field below the Channels section, populated with the server-side default. Two new regression tests cover: - plugin channels withheld from non-inbox agents - custom inboxAgent setting honored (non-default agent names work) Updated docs/setup/channels.md with a new "Inbox agent" subsection explaining the mechanism and which agent gets which flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Extends the dashboard Settings → Channels section from a single hardcoded
server:autonomostoggle into a dynamic, installation-aware list that supports Telegram and Discord plugin channels (and any future channel plugin). Implements Option B fromdocs/research/channel-integration.md— lean on Claude Code's native channel protocol, no new transport code.Empirically validated before building (
claude -p --channels plugin:telegram@... plugin:discord@... --dangerously-load-development-channels server:autonomos→ all three load;--channels totally-malformed→ hard error at startup;--channels plugin:nonexistent@...→ silent no-op). These behaviors directly shape the UX choices below.Problem
AVAILABLE_CHANNELSinSettingsStatusBarItem.tsxwas hardcoded to a single entry —server:autonomos. Users could not enable theplugin:telegram@claude-plugins-officialorplugin:discord@claude-plugins-officialchannels from the dashboard, even though the provider's flag-splitting logic (packages/server/src/providers/claude-code.ts:136-146) was already wired for them and settings persistence was already channel-aware. The "90% built" claim in the research doc was accurate — the 10% gap was UI + a save-time guard.A further problem surfaced during review: CC validates
--channelstag syntax at spawn, so a typo saved to~/.autonomos/settings.jsonwould crash every subsequent session spawn. Silent no-ops on uninstalled plugin identifiers would tell the user "I toggled Telegram on, why isn't my bot responding?" without any UI feedback.Solution (high level)
Three-state model per channel — ok / disabled / not-installed / unknown — derived from
claude plugin list --jsonoutput. The dashboard renders this state into the toggle; the PUT handler enforces it.```mermaid
flowchart TD
A[User clicks channel toggle] --> B{Plugin status}
B -->|ok| C[Toggle flips
pendingChannels updated]
B -->|not-installed
or disabled| D[Toggle locked
tooltip shows fix command]
C --> E[User clicks Save]
E --> F[PUT /api/settings]
F --> G{isValidChannelId
tag syntax check}
G -->|fail| H[400: Invalid identifier]
G -->|pass| I[readInstalledPlugins]
I --> J{channelStatus for
each requested id}
J -->|not-installed
or disabled| K[400: Refusing to save
would silently no-op]
J -->|ok or unknown
fail-open| L[writeFileSync settings.json]
L --> M[200: settings saved]
M --> N[Next session spawn
reads settings fresh
gets --channels flag]
```
What's in scope
New code:
Modified code:
Empirical validation that shaped the design
Testing
308 tests pass (307 + 1 new regression for fail-open behavior). `biome` + `tsc` clean.
Automated coverage
Test plan (manual — for Terry)
The Telegram/Discord pairing loop requires a real bot token + chat, so Terry will QA these before merge:
` → subsequent DMs reach the agent as `` eventsRisks
Out of scope (deferred per research Phase 2/3)
Follow-ups worth filing
🤖 Generated with Claude Code