Skip to content

feat(tui): integrations hub - #328

Open
plombeer31 wants to merge 2 commits into
feat/composio-mcp-accessfrom
feat/integrations-hub
Open

feat(tui): integrations hub#328
plombeer31 wants to merge 2 commits into
feat/composio-mcp-accessfrom
feat/integrations-hub

Conversation

@plombeer31

Copy link
Copy Markdown
Collaborator

Stacked on #327. Base is feat/composio-mcp-access, so the diff here is just the hub. Retarget to main once #327 lands.

What

A new Integrations tab — the single place an operator puts third-party credentials. Composio is its first tenant; Telegram and Discord follow in their own PRs.

Reachable as the integrations Manage tab, /integrations, or ctrl+g y (i was already Import's chord).

Why a hub rather than a Composio pane

Every integration so far grew its own surface: Telegram has a tab, LLM providers have a wizard, Composio had nothing. "Where do I put my key" required already knowing which kind of thing a given service was.

An integration now declares itself as a descriptor — id, label, summary, docs URL, credential fields, and a status() projection. The hub renders any descriptor without a bespoke pane, so adding an integration is a descriptor file plus one line in the registry, not a new TUI slice.

Integrations                    1/1 configured

> Composio  · connected
    ~1500 SaaS toolkits (Gmail, Slack, Notion, Linear, Jira…) with OAuth handled for you

  ↑/↓ move · enter open · r refresh

Design notes

  • Credentials live in <stateDir>/.env, via setDotenvKey (0600, atomic), never in config.json — and process.env is updated in the same breath, so the running process sees a new key without a restart. Field env vars are pinned to the shape setDotenvKey accepts, which would otherwise throw in front of the operator at save time.
  • A secret is never rendered in the clear except in the edit buffer being typed: values are masked and length-capped, starting an edit opens an empty buffer rather than seeding the stored value, and the buffer is cleared when the action settles so a key never lingers in UI state.
  • Edit mode swallows the whole keyboard. d, e, r are bindings on this tab; inside the editor they are key material. A paste that silently triggered "clear field" halfway through would be both baffling and destructive.
  • A re-sync clamps the cursor instead of resetting it — a background refresh must not move the operator's place mid-edit.
  • Changing the Composio key drops the cached tool-router session. A session belongs to the key that created it; reusing it across a key swap would keep talking to the old account.
  • IntegrationsOrchestrator is the only module touching credential storage or the live McpManager; the reducer and component are pure.

Slash palette

/integrations seats after /mcp, so ranks from 26 up shift by one. The pinned palette list and the two menu-children lists move with it — the repo's own uniqueness invariants (chord, rank, slash name) caught the initial i/26 collisions.

Testing

npm run lint and npm test green (7205 tests). 48 new tests across six files: secrets round-trip, registry invariants, the Composio descriptor, the reducer, key bindings, and panel render.

Verified against a live Composio key on a scratch state dir, driving the real orchestrator against a real runtime:

step tab MCP servers
start not configured — "no key — Composio tools are not loaded" []
save key connected, value shown as •••… ["composio"]
save ak_“fancy” refused: "API key must be ASCII — check for a smart quote…" unchanged
clear key back to not configured []

All of it live — no restart at any point.

Adds an `Integrations` tab: the single place an operator puts
third-party credentials. Composio is its first tenant; Telegram and
Discord follow in their own PRs.

Before this, every integration grew its own surface -- Telegram had a
tab, LLM providers had a wizard, Composio had nothing -- so "where do
I put my key" required already knowing which kind of thing a given
service was.

An integration declares itself as a descriptor: id, label, summary,
docs URL, credential fields, and a status() projection. The hub
renders any descriptor without a bespoke pane, so adding one is a
descriptor file plus a registry line rather than a new TUI slice.

Reachable as the `integrations` Manage tab, `/integrations`, or
ctrl+g y (`i` was already Import's chord).

Design notes:

- Credentials live in <stateDir>/.env via setDotenvKey (0600, atomic),
  never in config.json, and process.env is updated in the same breath
  so the running process sees a new key without a restart.
- A secret is never rendered in the clear except in the edit buffer
  being typed: values are masked and length-capped, starting an edit
  opens an empty buffer rather than seeding the stored value, and the
  buffer is cleared when the action settles.
- Edit mode swallows the whole keyboard. `d`, `e` and `r` are bindings
  on this tab; inside the editor they are key material, and a paste
  that silently triggered "clear field" halfway through would be both
  baffling and destructive.
- A re-sync clamps the cursor instead of resetting it, so a background
  refresh cannot move the operator's place mid-edit.
- Changing the Composio key drops the cached tool-router session: a
  session belongs to the key that created it, and reusing it across a
  key swap would keep talking to the old account.
- Field env vars are pinned to the shape setDotenvKey accepts, which
  would otherwise throw in front of the operator at save time.

Slash-palette ranks from 26 up shift by one to seat `/integrations`
after `/mcp`; the pinned palette and menu-children lists move with it.

Verified against a live Composio key: with no key the tab reads
"not configured" and the runtime has no MCP servers; saving the key
mounts the server and flips the badge to "connected" with the value
masked, no restart; a non-ASCII key is refused at entry with a
readable message; clearing unmounts the server and returns the tab to
"not configured".
Registering a MenuPlaceNode with a `slash` puts the command in the
palette and binds its ctrl+g chord, but execution is a separate
switch: without a case, typing /integrations answered "command
/integrations not yet implemented" while still advertising itself in
the palette.

Found by driving the real TUI rather than by a test, so this also adds
the generic guard that would have caught it: every command SLASH_COMMANDS
advertises is dispatched and none falls through to "not yet
implemented".
@plombeer31

Copy link
Copy Markdown
Collaborator Author

Added a commit after driving the real TUI: /integrations was answering "command /integrations not yet implemented".

Registering a MenuPlaceNode with a slash lists the command in the palette and binds its ctrl+g chord, but execution is a separate switch in slash-command-handler.ts — the menu registry is the source of truth for discovery, not dispatch. Every test passed with the command dead, because nothing asserted that a listed command does anything.

So the fix comes with the generic guard: every command SLASH_COMMANDS advertises is now asserted to dispatch, and none may fall through to "not yet implemented". That would have caught this, and catches the next one.

Screenshot of the working tab (real TUI, 120×40 PTY, live Composio key):

  atomic-agent v0.5.5  |   MANAGE ▸ Integrations
  ────────────────────────────────────────────────────────────────────────────
    Tasks  |    Skills  |    Memory  |    MCP  |  ▸ Integrations (1)  |    LLM  |    Telegram  |    Import  |    Privacy
  Integrations  1/2 configured

  > Composio  · connected
      ~1500 SaaS toolkits (Gmail, Slack, Notion, Linear, Jira…) with OAuth handled for you
    Telegram  · no bot token
      Drive the agent from Telegram — pair and start from the Telegram tab

  ↑/↓ move · enter open · r refresh

and the detail view, key masked:

  Composio  · connected
    https://composio.dev

  > API key  •••••••••••••••••••••••
      Free tier: 100K tool calls/month. Get one at composio.dev.

  ↑/↓ field · e edit · d clear · esc back

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