Skip to content

Terminal Control CLI Architecture

Goal

buttonsclictl gives scripts and AI agents a small local command surface for controlling an already running ButtonsCLI instance.

The design is control-core-first, but the app now also installs a local stdio MCP helper under ~/.buttonscli/control/buttonscli-mcp.mjs so users can point MCP-capable agents at the same backend primitives without rebuilding the contract again.

Why this shape

  • The app already owns the PTY lifecycle in Rust.
  • The app already captures terminal output and can safely write to the PTY.
  • A local CLI is easier for agents to use than repeatedly loading MCP schemas.
  • The app should keep typed, app-owned control primitives instead of collapsing into a generic run arbitrary shell abstraction.

Current architecture

The implementation has five layers for the shipped runtime, plus optional repo-local helper layers for agent ergonomics:

  1. TerminalPane and the tab store continue to manage visible tabs in the React app.
  2. The frontend now sends a lightweight tab snapshot into Tauri whenever the tab list or active tab changes.
  3. The Rust backend keeps a control-state registry keyed by PTY id, captures recent PTY output, and exposes an authenticated loopback HTTP API.
  4. scripts/buttonsclictl.mjs reads the local connection file and turns CLI commands into HTTP requests.
  5. The app can install ~/.buttonscli/control/buttonscli-mcp.mjs, which exposes the same control surface as a local stdio MCP server.

For Claude-specific usage, repo-local terminal-control skill scripts can still add higher-level workflow wrappers that compose the same control core without changing the backend contract.

For tab creation and layout actions, the Rust control API now also uses a small request-response bridge into the main frontend window. That keeps tab and layout state owned by the existing Zustand store instead of duplicating it in Rust.

Control model

The backend control registry stores:

  • PTY id
  • tab id
  • tab title
  • shell label
  • active-tab flag
  • recent input
  • recent output tail
  • truncation state for long output
  • last update timestamp

This data is enough for a practical first control surface without depending on the React window at request time.

Local transport

The control API binds to 127.0.0.1 on an ephemeral port.

On startup the app writes ~/.buttonscli/control-api.json with:

  • baseUrl
  • authToken
  • updatedAtMs

The CLI reads that file and sends Authorization: Bearer ... with each request.

This is local-machine control only. It is not a remote API.

Exposed commands

The first implementation focuses on the commands that are immediately useful for agents and automation:

  • status
  • tabs
  • create-tab
  • rename-tab
  • open-layout
  • read
  • wait-for-text
  • wait-for-quiet
  • send
  • run
  • key
  • presets
  • preset-run

The two explicit wait operations are implemented as CLI-side polling over the existing read endpoint. That keeps the backend API small while still giving agents a usable confirmation primitive.

The run operation is the higher-level primitive for agents. It sends text, presses Enter by default, waits either for quiet output or an optional match string, and returns the latest captured output in one response.

The naming and layout operations are intentionally explicit:

  • create-tab requires a name
  • rename-tab requires a name
  • open-layout requires two or more names plus a layout choice

That reduces ambiguity for agent-driven sessions and makes title selectors more dependable.

Selectors resolve in this order:

  • active
  • numeric PTY id
  • exact tab id
  • exact tab title, case-insensitive

If multiple tabs share the same exact title, title resolution now fails with an ambiguity error instead of silently picking one.

Why presets are backend-loaded

The CLI needs to list and run presets even when no separate frontend bridge is available.

Instead of duplicating preset state in the control sync payload, the backend reads the active profile config directly from the existing profile storage layout and exposes a small preset view.

Frontend bridge for layout operations

Rust already owns PTY IO, but the app's visible tab graph and split layout live in the React store.

To avoid forking that state model, the control API uses a narrow bridge for operations that must mutate frontend layout state:

  • create named tab
  • rename existing tab
  • open several named tabs and arrange them as horizontal, vertical, or grid layout

The main window handles those requests through the same store methods the UI already uses.

MCP helper

The app ships a self-contained stdio MCP server at ~/.buttonscli/control/buttonscli-mcp.mjs.

It is installed from Settings → Advanced → Automation (or on first app launch after update). The Settings Automation section shows the installed helper path and a copyable mcpServers JSON snippet you can paste into any MCP-capable agent's config.

The helper reads ~/.buttonscli/control-api.json automatically and exposes the same commands as the CLI as MCP tools, including help, status, tabs, create_tab, rename_tab, open_layout, read, wait_for_text, wait_for_quiet, send, run, key, presets, and preset_run.

The helper is intentionally standalone: it does not import from the repo's node_modules, so it works from any working directory without a separate install step.

Deliberate omissions in this phase

The first pass does not try to do everything from the brainstorming notes.

It intentionally leaves out:

  • secret injection
  • full history paging by absolute line block
  • remote networking
  • UI for configuring control access

UI for configuring control access is intentionally absent because local loopback + token-file auth is the right default for a developer tool. Remote networking is out of scope for this phase.

Those should be layered on the same control core instead of changing the CLI contract again.

Next logical extensions

  • better output slicing and chunked history reads
  • arrange existing tabs into a chosen layout without creating new ones
  • explicit focus/show operations for hidden tabs in split layouts
  • secret slot listing and guarded secret injection
  • explicit audit logging for control actions
  • richer MCP metadata (prompts/resources) if they add practical value beyond the current tool surface