Skip to content

The TUI (botholomew chat)

Tour of every tab in the chat TUI

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 + 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.

#TabWhat it's for
1ChatTalk to the agent. Streamed responses, tool-call boxes, slash commands, message queue.
2ToolsScrollable log of every tool call in the current session, with full input/output.
3ContextBrowse the agent's "virtual filesystem" (DuckDB-backed). Preview, search, delete.
4TasksTask queue with status + priority filters. View details, payloads, and predecessor outputs.
5ThreadsBrowse past chat and worker threads. View interactions, delete with confirmation.
6SchedulesRecurring work. Toggle enabled/disabled, delete, inspect last run.
7WorkersLive view of registered workers (running / stopped / dead), pid, mode, heartbeat age. f cycles the status filter.
8HelpSystem 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 for the underlying ToolDefinition pattern and 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 and 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.

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 enabled, delete, or refresh. 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.

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.

KeyAction
/ Move the highlight
ReturnSubmit the highlighted command if it takes no arguments; otherwise insert /<name> so you can type args
TabInsert the highlighted completion as /<name> without submitting (lets you edit before sending)
EscClose 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 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:

KeyAction
Ctrl+JSelect the next queued message
Ctrl+KSelect the previous queued message
Ctrl+EEdit the selected message (moves it back into the input bar)
Ctrl+XDelete 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..."}
MarkerState
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)

KeyAction
TabCycle to the next tab
18Jump to tab N (not on Chat — the Chat input consumes digits)
EscReturn to Chat from any other tab
Ctrl+CExit the TUI

Chat tab

KeyAction
EnterSend / queue message
⌥+EnterInsert newline
/ Browse input history
/Open slash-command popup
ReturnRun highlighted command (popup open, no-arg) / insert /<name> if args needed
TabInsert highlighted command as /<name> without submitting
EscClose popup
Ctrl+J / Ctrl+KSelect queued message
Ctrl+EEdit queued message
Ctrl+XDelete queued message

List panels (Tools / Tasks / Threads / Schedules)

KeyAction
/ Move selection
Shift+↑ / Shift+↓Scroll detail pane
j / kScroll detail pane (alternate)
fCycle filter (status, type, enabled — per panel)
pCycle priority filter (Tasks only)
rRefresh from DB
dDelete with confirmation (Threads, Schedules, Context)
eToggle enable/disable (Schedules only)

Workers tab

KeyAction
/ Select worker
fCycle status filter (all → running → stopped → dead)
lToggle between detail and log-tail view
Shift+↑ / Shift+↓Scroll log up/down (log view)
j / kScroll log down/up by one line (log view)
J / KPage scroll the log (log view)
g / GJump to top / bottom of log (log view, G resumes follow)

Context tab

KeyAction
/ Navigate
EnterExpand directory / preview file
BackspaceGo up one directory
/Search
dDelete 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).

Released under the MIT License.