You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
description: "Read messages from stdin, write responses to stdout"
3
+
description: "A full chat TUI for developing and testing agents locally"
4
4
---
5
5
6
6
```ts
7
7
import { terminal } from "spectrum-ts/providers/terminal";
8
8
```
9
9
10
-
The terminal provider reads from `stdin` and writes to `stdout`. Each line of input becomes a message; text content sent into the terminal space is printed to the console.
10
+
The terminal provider gives your agent a real chat interface in the terminal — multiple conversations in a sidebar, typing indicators, reactions, threaded replies, file attachments, and inline image rendering — all wired through the same Spectrum APIs you'd use against iMessage or WhatsApp Business.
11
11
12
-
```ts
13
-
terminal.config();
14
-
```
12
+
<Frame>
13
+
<img src="https://framerusercontent.com/images/AMLYOPHgKunMxQPxUzQUFUTkvHM.png?width=2400&height=1260" alt="Terminal UI showing a chat sidebar with multiple conversations, message thread, and typing indicator" />
14
+
</Frame>
15
+
16
+
It's a drop-in test harness: write your agent against the unified `app.messages` stream, and develop everything end-to-end without provisioning a phone number or pairing a device.
15
17
16
-
No credentials, no config. Useful for local development, integration tests, and REPL-style bots.
18
+
## How it works
17
19
18
-
## Example
20
+
`terminal.config()` spawns the standalone [tuichat](https://github.com/photon-hq/tuichat) binary as a subprocess and drives it over JSON-RPC. The binary auto-downloads from GitHub Releases the first time you run it. In a TTY it boots the rich UI; in a non-TTY context (CI, piped input) it falls back to a synchronous readline loop, so the same agent code works for scripted integration tests.
19
21
20
22
```ts
21
23
import { Spectrum } from "spectrum-ts";
@@ -27,25 +29,86 @@ const app = await Spectrum({
27
29
28
30
for await (const [space, message] of app.messages) {
| `terminal(app).space(...)` | Always resolves to a single space with `id: "terminal"`. |
45
+
| Multiple chats | `Ctrl+N` opens a new chat, `Ctrl+J` / `Ctrl+K` switch between them. Each chat is its own Spectrum space. |
46
+
| Reactions | Press `r` on a message to react. Arrives in your code as a `reaction` content message. |
47
+
| Replies | Press `e` to reply inline. Arrives with a `replyTo: { messageId }` extra on the message. |
48
+
| File attachments | Drag-and-drop into the terminal — messages arrive with name, MIME type, and buffer. |
49
+
| Inline images | Rendered with the Kitty graphics protocol when supported, with a half-block fallback. |
50
+
| Typing indicators | `space.startTyping()` / `space.stopTyping()` show a live indicator. |
51
+
| Console capture | `console.log` / `info` / `warn` / `error` / `debug` from your agent are forwarded into a pinned `__system__` chat instead of garbling the UI. |
| `commands` | `{ name: string; description?: string }[]` | `[]` | Slash commands surfaced in the TUI's command picker. Names must match `/^\/[A-Za-z0-9_-]+$/`. |
67
+
68
+
Slash commands arrive as regular text messages with the command string as the content — handle them in your `for await` loop the same way you'd handle any text.
69
+
70
+
## Working with multiple spaces
71
+
72
+
By default the TUI starts on `chat-1`; new chats opened with `Ctrl+N` get `chat-2`, `chat-3`, and so on. To open a named space programmatically, pass an `id`:
73
+
74
+
```ts
75
+
import { terminal } from "spectrum-ts/providers/terminal";
76
+
77
+
const t = terminal(app);
78
+
const debug = await t.space({ id: "debug" });
79
+
await debug.send("agent online");
80
+
```
81
+
82
+
Calling `space()` ensures the chat exists in the sidebar — useful for kicking off a conversation before any user input.
83
+
84
+
## Reactions and replies
85
+
86
+
Reactions ride the same `app.messages` stream as text — they arrive as a `reaction` content message:
87
+
88
+
```ts
89
+
for await (const [space, message] of app.messages) {
0 commit comments