@choonkeat/agent-chat
v0.12.0
Published
A "less intimidating" chat UI for any agent
Readme
Agent Chat
An MCP server that gives your AI agent a rich chat interface. Instead of reading raw tool calls in a terminal, your users get a rendered conversation with markdown, code blocks, quick reply buttons, and voice support.
This is the MCP that powers the Agent Chat tab in swe-swe.
Same conversation, two views. The terminal shows MCP tool calls and code diffs. Agent Chat renders the same content as a rich, interactive chat.
Features
- Rich markdown — messages render with full markdown, syntax-highlighted code blocks, and blockquotes
- File drag & drop — drop files into the chat to share them with the agent
- Images in messages — agents can include screenshots and images inline
- Voice conversation — speak to your agent and hear responses via text-to-speech
- Quick replies — agents can offer clickable response buttons for common actions
- Permission prompts in chat — when Claude Code is launched with
--dangerously-load-development-channels server:swe-swe-agent-chat, tool-use permission prompts are intercepted from stdin and surfaced as Allow/Deny quick replies in the chat UI (and spoken aloud in voice mode), instead of blocking on a TUI prompt
How it works
Agent Chat runs as an MCP server alongside your AI agent. The agent calls tools like send_message, send_progress, and check_messages to communicate with the user through a browser-based chat UI.
Agent (Claude, etc.)
│
├─ send_message("Here's what I found...") → Chat UI shows rich message
├─ send_progress("Working on it...") → Chat UI shows progress indicator
└─ check_messages() ← Chat UI returns user's replyMCP Tools
| Tool | Description |
|------|-------------|
| send_message | Send a message and wait for user response. Supports quick reply buttons. |
| send_verbal_reply | Send a spoken reply in voice mode (text-to-speech). |
| send_progress | Send a non-blocking progress update. |
| send_verbal_progress | Send a non-blocking spoken progress update. |
| check_messages | Non-blocking check for queued user messages. |
| set_chat_title | Name the streaming chat-log export (see below): renames the auto-written …-untitled.md to …-{slugified-title}.md and rewrites its header. Call again anytime to rename; also re-enables the export after chatlog_optout. |
| chatlog_close | Close out the streaming chat-log export for a clean git commit: freezes this session's .md (kept, unlike chatlog_optout), regenerates index.html, and returns the exact paths to git add. Requires a title while the file is still untitled; never renames an already-titled file. set_chat_title re-opens with a full-history backfill. |
| chatlog_optout | Stop the streaming chat-log export for this session and delete its .md (assets are left — content-sha names may be shared; index.html regenerated). |
| export_chat_md | Manually export the current chat as a markdown file (script-style **USER** / **AGENT** markers that render as iMessage-style left/right bubbles via a sibling index.html and as a normal markdown doc on GitHub/GitLab). Writes ./agent-chats/YYYY-MM-DD-NN-{title}.md, copies attachments to ./agent-chats/assets/, refreshes viewer.css / viewer.js, and regenerates the chat-archive index.html. The manual escape hatch when the streaming export (below) is enabled. |
Chat commands
Typed by the user in the chat input, handled by the browser — these never reach the agent as messages. They need agent-chat to be running inside a host that owns the agent's terminal (swe-swe); standalone, they report that and do nothing.
| Command | Effect |
|---------|--------|
| stop (also wait, cancel, hold on, abort, halt, pause) | Interrupts the agent's current turn and nudges it to check_messages. |
| /clear-and-then <instruction> | Resets the agent's context, then hands it <instruction>. The message is recorded as typed — command and all — in the chat and the streaming chat log, so a log read later shows where the agent was reset; the agent itself is handed only <instruction>, from the message queue. It comes back with a line naming the log file as its context, so the conversation survives the reset even though the agent's own memory does not. A bare /clear resets with no follow-up instruction, and is recorded too. Requires the streaming export (below); without it the reset still happens but the earlier conversation is gone. See docs/adr/2026-08-02-clear-prefix-context-reset.md. |
| /compact-and-then <instruction> | Same sequence as /clear-and-then, but the agent summarises its context instead of losing it. Recorded the same way; the agent is woken with the ordinary nudge once the summary has had time to finish — there is no chat-log resume line, because the summary is the context. A bare /compact summarises with no follow-up instruction. |
| / at the start of a message | Offers the four commands above, ahead of whatever the / autocomplete provider (if any) returns. Elsewhere in a message / means whatever the provider says, or nothing. |
| clear context → yes | The older confirm-then-reset flow. Resets only; nothing is handed back. |
Streaming chat-log export
Set AGENT_CHAT_EXPORT_DIR (e.g. agent-chats, resolved relative to the
working directory — it cannot escape it) and the markdown archive writes
itself, no export_chat_md call needed:
Every chat bubble is appended to
{date}-{NN}-untitled.mdthe moment it happens ({date}-{NN}-untitled-{SESSION_UUID}.mdwhen aSESSION_UUIDenv var identifies the host session), and its attachments are copied into theassets/directory beside it at that same moment (content-sha filenames), while the upload files still exist.Month directories are understood everywhere, and written only on request. A chat can live flat in the archive root, or one directory per month:
agent-chats/ index.html ← landing page, always at the root assets/viewer.css, viewer.js ← one copy for the whole archive 2026-08-15-01-some-title.md ← flat (the default) assets/2026-08-15-01-1-{sha}.png 2026-08/ ← month layout 15-01-another-title.md assets/2026-08-15-01-2-{sha}.pngListing, session resume and daily
NNnumbering all read both shapes, so a mixed archive is correct. New exports go flat unless you pass-chatlog-layout=month(or setAGENT_CHAT_CHATLOG_LAYOUT=month) — the default flips tomonthin a later release, once installed copies have caught up: an older copy regeneratingindex.htmldrops every file it cannot see, and it cannot see month directories.agent-chat migrate-chatlogsfiles an existing flat archive by month — it prints thegit mvlines, and-applyruns them and regeneratesindex.html. Attachments keep their full-date basenames and are routed by what the markdown links to, so migrating rewrites no file content and a chat's./assets/…links resolve identically on GitHub and in the viewer.The agent names the file via
set_chat_title(renames + header rewrite; callable again to rename).chatlog_optoutstops the export for the session and deletes its.md.To commit the archive without it going dirty on the next reply, the agent calls
chatlog_close(optionally titling in the same call), which freezes the.mdand returns the exact paths togit add. Freezing loses nothing: the JSONL event log keeps recording, andset_chat_titlere-opens the export with a full-history rewrite that backfills anything that arrived while frozen.index.html— the archive landing page — is regenerated from the.mdfiles on disk, never patched incrementally. That makes it merge-friendly: on a git conflict inindex.html, accept either side (or delete the file); the next export regenerates it correctly. Duplicate dailyNNs from parallel branches are fine — the regenerated index lists both, and content-sha asset names never clobber.index.htmlis a tracked file, so it is only rewritten when the export set changes in a committable way:chatlog_close,chatlog_optout,export_chat_md, or aset_chat_titlethat renames an export already in the manifest. Streaming appends never touch it, and still-untitledexports are never listed — otherwise every live session would leave the working tree dirty with links to untracked files thatset_chat_titleis about to rename.The header comment carries a
session:line, so a restarted process (sameAGENT_CHAT_EVENT_LOG) resumes appending to its own file instead of minting a new one.Nothing is ever auto-committed — the export sits in the working tree.
Installation
Add as an MCP server to Claude Code:
claude mcp add agent-chat -- npx -y @choonkeat/agent-chatOr run standalone (HTTP-only mode):
npx -y @choonkeat/agent-chat --no-stdio-mcpThe chat UI opens automatically in your browser.
Environment variables
| Variable | Description |
|----------|-------------|
| AGENT_CHAT_PORT | Fixed port for the HTTP server (default: random) |
| AGENT_CHAT_EVENT_LOG | Path to a JSONL file for event persistence across restarts |
| AGENT_CHAT_EXPORT_DIR | Directory (relative to cwd) for the streaming markdown chat-log export; unset = disabled |
| AGENT_CHAT_DISABLE | Set to any value to disable tools and HTTP server |
License
MIT
