Skip to content

feat: headless bots with a real chat (M4.1) - #9

Merged
httpsphl merged 8 commits into
mainfrom
feat/headless-chat
Sep 28, 2026
Merged

httpsphl merged 8 commits into
mainfrom
feat/headless-chat

Conversation

@httpsphl

Copy link
Copy Markdown
Owner

Bots now run as headless Claude Code and the app talks to them in a chat, like a messaging app, instead of showing a terminal. The design is in ADR 0001 and spec v0.2 (sections 7 to 10, 15, 17 M4.1 and 19).

Daemon

  • Each bot runs claude -p with stream-json on stdin and stdout; no PTY, hooks or named-pipe inbox. portable-pty, the hooks and the inbox code are gone.
  • The daemon turns what the bot prints into chat items: the owner's and other bots' messages, replies, tool calls, approvals, turn ends and notices. Replies are also sent as live text while the bot writes them. Protocol 2 adds chat.history, approvals.answer, attachments.read, chat.item and chat.delta; terminal.* is gone.
  • The courier writes messages into stdin. The replayed user message marks a delivery as read. What a process never read goes back to the queue when the process ends.
  • Tool permissions go through the permission_prompt MCP tool and wait for the owner (60 min by default). A request expires when its connection drops, when the process ends or when the daemon starts.
  • Bot state comes from the stream: busy, needs_approval, rate_limited (with the account usage in system.status) and auth_error. Sessions resume after a restart, and a resume that dies at once starts fresh.
  • Owner messages can carry up to 10 files, saved in the bot's folder; images also go inline.

App

  • Bot chat: the owner's bubbles on the right with delivered and read receipts; other bots on the left.
    • Replies render as markdown and stream in. Links open in the default browser through a new open_url command (http and https only); images in replies are not fetched.
    • Tool calls show as compact lines that open to show input and output. Approvals show as cards with Allow, Deny and a note.
  • Composer: Enter sends and Shift+Enter breaks the line. Files can be picked, pasted or dropped, and images show as thumbnails, read back after a reload.
  • The sidebar lists bots as conversations with their state and last activity. Bot details moved to a side panel. xterm.js is gone.

Checks

cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo test --workspace and pnpm check pass locally.

New tests:

  • daemon: tests/chat.rs (items, deltas, approvals allowed, denied, timed out, unknown and expired on exit), tests/supervisor_turns.rs, and tests/attachments.rs; courier and supervisor tests were rewritten for stdin.
  • app: chat.test.tsx and rows.test.ts.

Tested with real Claude Code (2.1.284)

Done on a dev daemon with a fresh BOTLOFT_HOME and the app preview (pnpm dev, ?live=<port>). Results are in spec section 19. To repeat:

  1. Start a dev daemon with its own BOTLOFT_HOME, open the app against it, create a crew and a bot.
  2. Ask the bot to create a file. Expected:
    • the message shows "Read";
    • the reply streams in;
    • a Write line appears with an approval card, and the bot shows "Needs approval".
  3. Allow, and the turn ends with "Done in …". Then do it again, waiting more than 60 s before allowing: it still goes through.
  4. Attach an image and ask about it, then ask for something that needs a command and Deny it with a note. The bot describes the image, says it did not run the command, and mentions the note.
  5. Reload the page: the image thumbnail is still there.
  6. Restart the bot, and separately restart the daemon. Asking about earlier messages shows the bot resumed its session.
  7. Create a second bot and ask the first to send_message it. The note shows in the second bot's chat, and its answer reaches the first.
  8. Stop the daemon: no claude.exe with mcp__botloft__permission_prompt in its command line remains.

The owner wants to talk to each bot like in a messaging app: bubbles,
what the bot did at a glance, approvals with buttons, images and files.
A terminal cannot give that. Each bot now runs as `claude -p` with
stream-json on stdin and stdout; the daemon turns the events into chat
items, messages go in through stdin (the owner's with full authority),
approvals go through a permission-prompt tool on our MCP server, and
attachments land in the bot's folder.

Every behavior the design relies on was checked with Claude Code
2.1.284 (spec 19): input shape, several turns per process, queued input,
replay with our uuid, image blocks, the permission-prompt tool, and
isolation from the owner's settings. M4.1 is the switch.
Bots now run `claude -p` with stream-json on stdin and stdout instead of
the interactive TUI in a PTY (ADR 0001). The daemon turns what each bot
prints into chat items (replies, tool calls, turns, notices) and live
text deltas, and the courier writes messages into stdin, with read
receipts from the replayed user message.

- protocol 2: chat.history, approvals.answer, chat.item and chat.delta;
  terminal.* is gone
- messages can carry attachments, saved in the bot's folder; images
  also go inline
- tool permissions go through the permission_prompt MCP tool and wait
  for the owner in the chat
- bot state comes from the stream: busy, needs_approval, rate_limited
  and auth_error
- the PTY runtime, hooks and the named-pipe inbox are removed
attachments.read returns a file the owner sent, as it is now in the
bot's folder, so the app can show images after a reload. A file the bot
removed is not_found; one that grew past attachment_max_mb is refused.
A link followed inside the webview would replace the app. open_url hands
http and https links to the default browser and refuses anything else,
since other schemes can start programs.
Each bot opens on its conversation, laid out like a messaging app:
- the owner's messages on the right with delivery and read receipts,
  other bots' and Botloft's on the left
- replies in markdown, streamed live while the bot writes them
- tool calls as compact lines that open to show input and output
- approval cards with Allow, Deny and a note for the bot
- a composer where Enter sends, Shift+Enter breaks the line, and files
  can be picked, pasted or dropped; images show as thumbnails and are
  read back with attachments.read

The sidebar lists bots as conversations with their state and last
activity, and bot details moved to a side panel. xterm.js and the
terminal feature are gone.
Claude Code loads our MCP tools on demand, so a bot calls ToolSearch
before its first send_message. The tool line now reads "load
send_message" instead of an empty summary.
Headless bots never show Claude Code's folder-trust dialog, so the
welcome screen now explains approvals instead. The empty chat shows the
bot's role on its own line rather than running it into the next
sentence.
Tested with Claude Code 2.1.284 through the daemon and the app: approvals
answered after 95 s, denials with a note, resume after restarting the
bot and the daemon, rules in -p, Windows-path deny rules in -p, deferred
MCP tools, and where Claude Code keeps inline images.
@httpsphl
httpsphl enabled auto-merge September 28, 2026 22:08
@httpsphl
httpsphl merged commit 7be6789 into main Sep 28, 2026
3 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