A chat CLI
A chat CLI in the shape of Claude Code on the native API: an inline screen whose transcript flows into scrollback, a streamed markdown reply, slash commands, a permission prompt, Esc to interrupt — and one prompt per stdin line in a pipe.
examples/chat-cli is a
chat CLI on controlroom's native API. It runs inline: the transcript flows into the
terminal's own scrollback, and a live region under it shows the reply as it streams, a status
line, and the input line.
npx degit ofri-peretz/burgee/examples/chat-cli my-chat
cd my-chat && npm install && node chat.mjs| Key | What it does |
|---|---|
Enter | send; Meta+Enter or Ctrl+J for a new line |
↑ ↓ | history |
Tab | complete a /command |
Esc | interrupt the reply |
Ctrl+C | quit, and hand the terminal back as it was |
A reply that wants to run a tool asks first: type y or n. The model is scripted, so the
boilerplate runs with no key and no network.
How it is built
An inline screen with four parts, each sized to fit what it shows, so the live region is only as tall as the reply being streamed:
const LAYOUT = {
direction: 'column',
parts: [
{ size: 'fit', content: 'reply' },
{ size: 'fit', content: 'status' },
{ size: 'fit', content: 'prompt' },
{ size: 1, content: 'hints' },
],
};The transcript is committed. Each prompt and each finished reply goes through
screen.commit(), which writes it above the live region, once, into scrollback: ink's
<Static>. The reply pane, the spinner status line and the hint line are liveOnly, so a pipe
and an agent get the transcript itself.
The input line is caique's editor in the prompt pane, with slash-command completion
through its complete option (The input line). Esc is not a key
the editor uses while no menu is open, so it falls through to the keymap, where it is bound to
interrupt.
A reply streams word by word on the runtime's clock, updating the reply pane and a status line with the elapsed time and a token count. On a terminal the committed reply is the markdown as it was drawn; off a terminal it is the markdown text.
Every caller
In a pipe, each stdin line is one prompt, and the program ends when stdin does:
> hello
You said: *hello*.
This reply is **scripted**. Replace `MODEL` in `chat.mjs` with a real client.
> /help
/help show the commands
/clear start a new conversation
/exit leave
> list the files
Allow ls? Type y or n.
⏺ ran ls
There are **three** files:
- `chat.mjs`
- `README.md`
- `package.json`Under --json, each transcript entry is one commit event on stderr:
{"event":"commit","state":"> hello"}
{"event":"commit","state":"You said: *hello*.\n\nThis reply is **scripted**. Replace `MODEL` in `chat.mjs` with a real client."}The example's own npm test checks both, and on a simulated terminal that a reply streams under
a status line, Esc interrupts it, Tab completes a slash command and Ctrl+C quits.
What to change first
MODEL: replacereply(prompt)with your client, and stream its tokens intostream()instead of the scripted words.COMMANDS: the slash commands, and whatsubmit()does for each.complete: completion for the input line.@filecompletion goes here.
Coming from an Ink chat app instead?
examples/chat-cli-ink
is the same boilerplate written for Ink, running on controlroom/ink
(Coming from ink).
A full-screen dashboard
The reference dashboard: tabs, a checklist with progress, a log tail and a status line that collapses, in the alternate screen — and clean lines in a pipe and NDJSON under --json, from the same program.
controlroom
Every export of controlroom, with its signature and doc comment: status, distribute, layout, hints, initial, reduce and 8 more, plus 17 types.