---
url: 'https://www.botholomew.com/tui.md'
---
# The TUI (`botholomew chat`)

![Tour of every tab in the chat TUI](./assets/full-tour.gif)

Botholomew ships with an interactive terminal UI for talking to the
agent, inspecting its work, and managing the local database. It's built
on [Ink 6](https://github.com/vadimdemedes/ink) + React 19 — a real
React tree, just rendered to ANSI characters instead of DOM nodes.

The TUI is not a thin wrapper around the CLI. It's an 8-tab dashboard
that runs against the same DuckDB workers use, so you can watch tasks
claim and complete, browse the agent's memory, edit schedules, and
monitor workers in real time.

***

## Launching

```bash
botholomew chat                      # new thread
botholomew chat --thread-id <id>     # resume a previous thread
botholomew chat -p "summarize inbox" # one-shot: send prompt, then chat
```

The TUI does not auto-spawn workers — dispatch them explicitly via the
CLI (`botholomew worker start --persist`) or have the chat agent call
the `spawn_worker` tool. Exiting the TUI prints the thread ID and the
exact command to resume it.

Thread titles are auto-generated by the LLM from the first user
message and updated in the status bar every 5s.

***

## The eight tabs

The TUI is organized as eight sibling panels. Only one is visible at a
time. All panels stay mounted — switching tabs hides them with CSS
(`display="none"`) rather than unmounting, so scroll position and
filter state survive a round trip.

| # | Tab | What it's for |
|---|---|---|
| 1 | **Chat** | Talk to the agent. Streamed responses, tool-call boxes, slash commands, message queue. |
| 2 | **Tools** | Scrollable log of every tool call in the current session, with full input/output. |
| 3 | **Context** | Browse the agent's "virtual filesystem" (DuckDB-backed). Preview, search, delete. |
| 4 | **Tasks** | Task queue with status + priority filters. View details, payloads, and predecessor outputs. |
| 5 | **Threads** | Browse past chat and worker threads. View interactions, delete with confirmation. |
| 6 | **Schedules** | Recurring work. Toggle enabled/disabled, delete, inspect last run. |
| 7 | **Workers** | Live view of registered workers (running / stopped / dead), pid, mode, heartbeat age. `f` cycles the status filter. |
| 8 | **Help** | System info, worker status, keyboard reference. |

### 1. Chat

The default view. User and assistant messages render as bubbles,
tool calls render as compact boxes beneath the message that triggered
them, and the input bar sits at the bottom.

Completed messages are printed via Ink's `<Static>` component so they
live in real terminal scrollback — you can select and copy them with
your terminal's native tools, and they survive tab switches without
re-layout.

While the agent is streaming, text flushes to the screen on a ~50 ms
timer (~20 fps) to keep the terminal from flickering. A spinner marks
in-flight tool calls. While the model is assembling a tool-call input
(streaming a large JSON args block), a `Preparing tool call: <name>...`
spinner is shown so the UI doesn't appear frozen.

### 2. Tools

Every tool call the agent has made in the current session, in order.
`↑`/`↓` selects a row; the detail pane shows the full input JSON and
the full (untruncated) output. `Shift+↑`/`↓` or `j`/`k` scroll the
detail pane.

Tool calls from **MCP** (`mcp_exec`) are displayed as
`<server> / <tool>` — e.g. `Linear / CreateIssue` — with the `server`
and `tool` fields extracted from the input JSON so the name stays
readable.

See [tools.md](tools.md) for the underlying `ToolDefinition` pattern
and [mcpx.md](mcpx.md) for how MCP tools are merged into the agent's
toolset.

### 3. Context

Interactive browser for context items (the agent's "virtual
filesystem" — see [virtual-filesystem.md](virtual-filesystem.md) and
[context-and-search.md](context-and-search.md)).

* `↑`/`↓` navigate.
* `Enter` picks a drive (at the top level), expands a directory, or
  previews a file.
* `Backspace` goes up one directory; at the root of a drive, it returns
  to the drive picker.
* `/` opens a hybrid (keyword + vector) search across all drives.
* `d` deletes the selected item.

Markdown files (detected by `mime_type === "text/markdown"` or a `.md`
extension on the path) are rendered through `Bun.markdown.ansi` so
headers, emphasis, lists, and fenced code blocks show with terminal
formatting. Other file types render as plain text.

### 4. Tasks

The task queue, with filters for status (pending / in\_progress /
completed / failed) and priority. Select a row to see the full task
body, its payload, predecessor outputs (for DAG tasks), and the log of
attempts. `r` refreshes. See [tasks-and-schedules.md](tasks-and-schedules.md).

### 5. Threads

Every thread ever persisted to the project DB, with a type filter
(chat vs. worker). Threads store the full interaction history
(messages, tool calls, tool results) — the same data the agent uses to
reconstruct context on resume.

`d` deletes a thread, with a yes/no confirmation. You can't delete the
thread you're currently attached to.

### 6. Schedules

Recurring tasks. Toggle `e`nabled, `d`elete, or `r`efresh. Schedules
are evaluated by an LLM pass during the tick loop against natural-language
rules like "every weekday at 9am" — see
[tasks-and-schedules.md](tasks-and-schedules.md).

### 7. Workers

Live view of every worker registered against this project (status
filter cycles with `f`: all → running → stopped → dead → all). Each row
shows status, short id, mode, and heartbeat age. The detail pane has
full id, pid, hostname, started time, heartbeat time, stopped time (if
any), pinned task id (if any), and the per-worker log path.

Press `l` to swap the detail pane into a **log view** that tails the
selected worker's log file (`.botholomew/logs/<id>.log`). The log
auto-refreshes every ~1.5 s and follows the bottom by default — scroll
up with `Shift+↑`, `k`, or `K` to pause following; `G` (or scrolling
back to the bottom) resumes it. Press `l` again to return to the
detail view. Foreground workers (`worker run`) have no log file, so
the log view shows an empty-state message instead.

The panel polls the DB every ~3s. Workers heartbeat every
`worker_heartbeat_interval_seconds` (default 15s); ones older than
`worker_dead_after_seconds` (default 60s) get flagged `dead` by a
peer's reaper. Start new workers from the CLI (`botholomew worker
start --persist`) or have the chat agent call `spawn_worker`.

### 8. Help

Project directory, active thread ID, worker status summary, and the
full keyboard reference.

***

## The input bar

The bar at the bottom of the Chat tab is a custom multi-line input
(not `ink-text-input`). It supports:

* **Multi-line editing** — `⌥+Enter` (Alt+Enter) inserts a newline;
  plain `Enter` submits.
* **History** — `↑`/`↓` walks through previously submitted messages.
  Works across skills and plain messages.
* **Slash autocomplete** — type `/` at the start of the line to open a
  popup (see below).
* **Blinking cursor** — visual cue for focus.
* **Stable input handlers** — keypresses are handled via `useRef`-stable
  callbacks, so Ink's `useInput` doesn't re-register stdin listeners on
  every render. Historically this was the difference between a smooth
  typing experience and one that pegged a CPU core under fast input.

### Slash-command popup

Typing `/` with nothing before it opens the autocomplete popup.

| Key | Action |
|---|---|
| `↑` / `↓` | Move the highlight |
| `Return` | Submit the highlighted command if it takes no arguments; otherwise insert `/<name> ` so you can type args |
| `Tab` | Insert the highlighted completion as `/<name> ` without submitting (lets you edit before sending) |
| `Esc` | Close the popup (keeps what you typed) |

Built-in commands are `/help`, `/skills`, `/clear`, and `/exit`.
`/clear` ends the current chat thread (persisted, still resumable via
`botholomew chat --thread-id <id>`) and starts a fresh one on the same
session, so you can reset context without losing the conversation.
Every file in `.botholomew/skills/` is also surfaced in the popup with
its description. See [skills.md](skills.md) for the file format and how
skills are invoked with positional arguments.

Skills that reference `$1` / `$ARGUMENTS` (or declare `arguments` in
frontmatter) are treated as argument-taking: `Return` inserts
`/<name> ` and waits for your input. Skills without placeholders, like
the built-ins, submit in a single `Return`.

***

## The message queue

You can keep typing while the agent is working. Each submitted message
is appended to a queue that drains sequentially — when the current
turn finishes, the next queued message is sent automatically.

On the Chat tab, when at least one message is queued:

| Key | Action |
|---|---|
| `Ctrl+J` | Select the next queued message |
| `Ctrl+K` | Select the previous queued message |
| `Ctrl+E` | Edit the selected message (moves it back into the input bar) |
| `Ctrl+X` | Delete the selected message from the queue |

The queue is ephemeral (in-memory, not persisted) — it's a way to
batch follow-ups without interrupting a tool loop mid-flight.

***

## Tool-call visualization

Every tool call the agent makes renders as a small box under the
assistant message that triggered it:

```
  ⟳ Linear / CreateIssue (exec) ({"team":"..."})
  ✔ Linear / CreateIssue (exec) ({"team":"..."})
    → {"id":"...","url":"https://..."}
  ✘ Linear / CreateIssue (exec) ({"team":"..."})
    → {"is_error":true,"error":"Team not found..."}
```

| Marker | State |
|---|---|
| `⟳` | Running |
| `✔` | Succeeded |
| `✘` | Errored (`is_error: true` in the tool result) |

The input preview is truncated to 60 chars and the output preview to
120 — head over to the **Tools** tab for the full payload.

When a tool returns more than `MAX_INLINE_CHARS` (see
`src/worker/large-results.ts`), Botholomew routes the full payload
through the large-results cache and shows a stub instead:

```
  ✔ context_read ({"path":"big.md"})
    ⚡ Paginated for LLM [42K, 8pg]
```

The agent sees paged access to the result via dedicated tools; the TUI
just shows the summary to keep the chat view compact.

***

## Keyboard reference (consolidated)

### Global (any tab)

| Key | Action |
|---|---|
| `Tab` | Cycle to the next tab |
| `1`–`8` | Jump to tab N (not on Chat — the Chat input consumes digits) |
| `Esc` | Return to Chat from any other tab |
| `Ctrl+C` | Exit the TUI |

### Chat tab

| Key | Action |
|---|---|
| `Enter` | Send / queue message |
| `⌥+Enter` | Insert newline |
| `↑` / `↓` | Browse input history |
| `/` | Open slash-command popup |
| `Return` | Run highlighted command (popup open, no-arg) / insert `/<name> ` if args needed |
| `Tab` | Insert highlighted command as `/<name> ` without submitting |
| `Esc` | Close popup |
| `Ctrl+J` / `Ctrl+K` | Select queued message |
| `Ctrl+E` | Edit queued message |
| `Ctrl+X` | Delete queued message |

### List panels (Tools / Tasks / Threads / Schedules)

| Key | Action |
|---|---|
| `↑` / `↓` | Move selection |
| `Shift+↑` / `Shift+↓` | Scroll detail pane |
| `j` / `k` | Scroll detail pane (alternate) |
| `f` | Cycle filter (status, type, enabled — per panel) |
| `p` | Cycle priority filter (Tasks only) |
| `r` | Refresh from DB |
| `d` | Delete with confirmation (Threads, Schedules, Context) |
| `e` | Toggle enable/disable (Schedules only) |

### Workers tab

| Key | Action |
|---|---|
| `↑` / `↓` | Select worker |
| `f` | Cycle status filter (all → running → stopped → dead) |
| `l` | Toggle between detail and log-tail view |
| `Shift+↑` / `Shift+↓` | Scroll log up/down (log view) |
| `j` / `k` | Scroll log down/up by one line (log view) |
| `J` / `K` | Page scroll the log (log view) |
| `g` / `G` | Jump to top / bottom of log (log view, `G` resumes follow) |

### Context tab

| Key | Action |
|---|---|
| `↑` / `↓` | Navigate |
| `Enter` | Expand directory / preview file |
| `Backspace` | Go up one directory |
| `/` | Search |
| `d` | Delete selected item |

***

## Theming

The TUI detects whether your terminal has a dark or light background
and picks colors accordingly. Detection order:

1. `COLORFGBG` environment variable (set by Terminal.app, iTerm2, xterm).
2. On macOS, `defaults read -g AppleInterfaceStyle`.
3. Fallback: dark.

All colors and ANSI codes live in `src/tui/theme.ts`, so one import is
the single source of truth for hue choices across every panel.

***

## Architecture notes

A few choices worth knowing if you're reading or modifying the TUI:

* **Streaming is throttled.** `App.tsx` flushes `streamingText` every
  50 ms max during a response, not per-token. Per-token flushing
  caused visible flicker and ~30× the React commits.
* **Scroll state lives at the root.** Each list panel (`TaskPanel`,
  `ThreadPanel`, etc.) keeps its scroll offset lifted up so that
  switching tabs doesn't reset your position.
* **All tabs stay mounted.** Inactive panels are hidden with
  `display="none"` instead of being unmounted. Remounting the
  Context/Tasks panels would re-query DuckDB and lose filter state.
* **Completed messages render via `<Static>`.** They're written to
  terminal scrollback once and never re-rendered — essential for
  performance in long sessions, and it means the chat history survives
  in your terminal buffer after you exit.
* **Input handlers are ref-stable.** `InputBar` and `App` both install
  a single `useInput` handler wrapped in `useCallback` with
  `useRef`-backed state reads. A prior bug caused 100 % CPU under fast
  typing because every keystroke re-registered the stdin listener.
* **Kitty keyboard protocol.** The TUI enables Kitty's
  `disambiguateEscapeCodes` flag when starting Ink so modifiers
  (`Shift+↑`, `⌥+Enter`, etc.) are distinguishable from plain arrow /
  Enter presses on supporting terminals.

***

## Troubleshooting

* **Colors look off / no colors at all.** Your terminal may not be
  reporting `COLORFGBG`. Export it manually (`export COLORFGBG="15;0"`
  for light, `"15;16"` for dark) or rely on the macOS fallback.
* **No workers running.** The TUI does not auto-spawn workers — check
  `botholomew worker list --status running`. If nothing is alive, start
  one with `botholomew worker start --persist` or have the chat agent
  call `spawn_worker`.
* **Weird layout in tmux / split panes.** Ink needs a stable terminal
  width; if the pane resizes mid-render, large tool-call boxes can
  wrap oddly. A fresh `Ctrl+L` usually sorts it.
* **`⌥+Enter` inserts the literal character `¬` or similar.** Your
  terminal is sending Option as Meta. Enable "Use Option as Meta key"
  in your terminal profile (Terminal.app, iTerm2, Ghostty all support
  this).

***

## Related docs

* [Skills (slash commands)](skills.md) — the `/<name>` commands the
  popup surfaces.
* [Architecture](architecture.md) — how the TUI, workers, and CLI
  share one DuckDB.
* [Tasks & schedules](tasks-and-schedules.md) — what the Tasks and
  Schedules tabs are actually managing.
* [Context & hybrid search](context-and-search.md) — backs the Context
  tab's search.
* [Virtual filesystem](virtual-filesystem.md) — the `context_*` tools
  visible in the Tools tab.
* [MCPX](mcpx.md) — how `mcp_exec` calls get routed to external
  servers.
