The Interactive TUI
The TUI (Terminal User Interface) is the full-screen, interactive mode of harnesscli. Instead of firing a single prompt and exiting, you enter a persistent chat session that keeps a live conversation history, renders tool calls as they execute, and lets you switch models, apply profiles, and manage sessions — all without leaving your terminal.
Think of it as the difference between a shell one-liner and an IDE: the one-shot --prompt mode is great for scripting; the TUI is great for thinking out loud with an agent over many turns.
Launching the TUI
Pass the --tui flag to harnesscli. The TUI requires a real terminal on stdout — it will not start inside a pipe.
# Simplest launch — connects to harnessd at http://localhost:8080
harnesscli --tui
# Explicit server address
harnesscli --tui --base-url http://127.0.0.1:8080
# Start with a specific model pre-selected
harnesscli --tui --base-url http://127.0.0.1:8080 --model gpt-4.1
If you use go-code (the shell wrapper), running go-code with no arguments launches the TUI automatically after starting harnessd if needed. See The go-code Command for details.
If stdout is not a TTY — for example, when you pipe output or run inside CI — the program exits immediately with:
--tui requires a terminal; pipe output or use without --tui for streaming mode
The TUI always uses the alternate screen buffer (tea.WithAltScreen) and truecolor rendering. These are hardcoded when launched from harnesscli and are not user-configurable.
The TUIConfig.Theme field exists in the config struct but is not read by any code in the current release. Theme-based styling has no effect.
Screen layout
The TUI divides your terminal into fixed regions stacked top-to-bottom:
┌─────────────────────────────────────────┐
│ Viewport (message bubbles, tool cards) │ ← fills remaining height
├─────────────────────────────────────────┤
│ Thinking bar (while LLM is working) │ ← optional
│ Interrupt banner (Ctrl+C confirm) │ ← optional
│ Slash-complete dropdown │ ← optional
│ Input area │ ← min 3 lines, max 8
├─────────────────────────────────────────┤
│ Status bar │ ← 1 line
└─────────────────────────────────────────┘
Six lines are reserved at all times (status bar + two separators + minimum input height). The viewport receives whatever is left, with a floor of three lines. The minimum terminal width is 20 columns.
Status bar
The status bar (bottom line) shows several segments. When the terminal is narrow, lower-priority segments are dropped first:
| Priority | Segment | Example |
|---|---|---|
| 1 | Model name (bold, up to 24 chars) | gpt-4.1 |
| 2 | Running indicator | ... |
| 3 | Cumulative cost | $0.0042 |
| 4 | Permission mode (hidden when default) | [plan] |
| 5 | Git branch | (main) |
| 6 | Workspace path (up to 20 chars) | ~/projects/myapp |
| 7 | MCP failure count | 2 MCP fail |
Transient messages (command confirmations, errors) replace the status bar text for three seconds.
Keybindings
Press ? or Ctrl+H at any time to open the built-in help dialog with the full keybinding list.
| Key | Action |
|---|---|
Enter | Submit message or confirm selection |
Shift+Enter / Ctrl+J | Insert a newline in the input |
Up / Ctrl+P | Input history up, or scroll viewport |
Down / Ctrl+N | Input history down, or scroll viewport |
PgUp | Scroll viewport up half a screen |
PgDn | Scroll viewport down half a screen |
/ | Open slash-command autocomplete dropdown |
@ | Insert @ to begin a file-path attachment |
Tab | Complete a slash command or file path |
Ctrl+O | Expand/collapse active tool card, or toggle plan mode when idle |
Ctrl+E | Open $EDITOR for multi-line prompt editing |
Ctrl+S | Copy last assistant response to clipboard |
Ctrl+G | Steer the active run: inject the input-box text into the running turn (see below) |
Ctrl+U | Clear the input (when no overlay is open) |
Ctrl+C | Two-stage interrupt (see below) |
Esc | Multi-priority close: dropdown → overlay → cancel run → clear input |
? / Ctrl+H | Open help dialog |
Two-stage interrupt
Accidentally pressing Ctrl+C mid-run would be frustrating, so the TUI requires confirmation:
First Ctrl+C
An interrupt banner appears: "⚠ Press Ctrl+C again to stop, or Esc to continue". The run keeps going.
Second Ctrl+C
The run is cancelled. The status bar shows "Interrupted".
Esc (while banner is showing)
The banner is dismissed. The run continues. Status shows "Interrupt cancelled".
Pressing Esc when no banner is showing cancels the run directly (without the two-step confirmation).
When idle (no run active), Ctrl+C quits the TUI immediately.
Mid-turn steering
While a run is in flight, type corrective input and press Ctrl+G to inject it into the running turn — the run keeps going (it is not cancelled or restarted). The steered text is delivered to the agent as a user message at the next step boundary, not instantaneously, so the current tool call or model response finishes first.
- The input box clears on send and the status bar confirms with "Steering sent".
- The transcript shows the steered message with a
steered ⟂marker once the server confirms it, distinguishing it from a typed prompt. - Steering is queued server-side with a buffer of 10 pending messages per run; if the buffer is full or the run has already finished, the status bar says so and nothing is dropped silently.
- With no active run or an empty input box,
Ctrl+Gis a no-op with a status hint. - The same path is available outside the TUI as
harnesscli steer <run-id> <prompt>. Ctrl+S(copy) andEsc(cancel) are unchanged.
Slash commands
Type / to open the autocomplete dropdown. Tab completes to the common prefix; Enter selects a command. Commands are case-insensitive.
| Command | Description |
|---|---|
/model | Open the model picker |
/profiles | View and select a capability profile |
/sessions | Browse and resume past sessions |
/title [text] | Set or show the current session's title (/title clear removes it). Shown in the status bar and the /sessions picker, persisted across restarts |
/init [confirm] | Generate an AGENTS.md for the current workspace via a harness run. If AGENTS.md already exists, run /init confirm to overwrite it |
/add-dir [path] | Attach an extra directory to the session so runs can read/work in it. Bare /add-dir lists attached directories; /add-dir remove <path> detaches one. See Extra directories |
/feedback [--local] [request] | Publish a request, attached images, and current diagnostics without interrupting the active run. See Feedback bundles |
/new | Start a fresh conversation (resets conversation ID) |
/fork | Fork the current session into a new conversation with the full history, and switch into the copy (see Forking a session) |
/search <query> | Search the current session transcript |
/history <query> | Search across stored session metadata |
/export | Export the conversation to a Markdown file |
/stats | Show cumulative cost and token statistics |
/context | Show context window usage |
/keys | Manage provider API keys |
/runs | List recent harness runs |
/cancel [run-id] | Cancel the active run (or a specific run by ID) |
/replay <run-id-or-path> | Replay a recorded run |
/resume <run-id> <prompt> | Continue a completed run with a new prompt |
/subagents | View active subagent processes |
/permissions | View the current session's tool permissions |
/attach | Attach file context with @path tokens |
/doctor | Show local harness diagnostic commands |
/help | Show the help dialog |
/clear | Clear the conversation history and viewport |
/quit | Quit the TUI |
Extra directories (/add-dir)
/add-dir <path> attaches an additional directory to the session. Attached directories are sent on every run as extra_dirs (POST /v1/runs), and the server's file-tool confinement (read, write, edit, ls, grep, …) permits paths under them in addition to the workspace root. Paths outside the workspace root and all attached directories stay denied.
- Relative paths resolve against the session workspace.
- Bare
/add-dirlists the attached directories;/add-dir remove <path>detaches one. (removeis a subcommand only when followed by a path, so a directory literally namedremovecan still be added.) - The list is session-scoped — it is not persisted across restarts.
- Current limits: the
bashtool's command sandbox andglobstill confine to the primary workspace root only; the server validates every entry (absolute path to an existing directory) and rejects the run with HTTP 400 otherwise.
Feedback bundles (/feedback)
Use /feedback whenever something feels wrong, including while a run is active:
/feedback The export button stopped responding after the run completed
To include a screenshot, paste or attach the image so its [image #1] chip is visible beside the input, then type /feedback <request> and press Enter. Multiple image chips are accepted in attachment order. You do not need to copy a filesystem path or manually attach anything in a browser.
The command snapshots evidence without cancelling, steering, or otherwise changing the active run. It writes a uniquely timestamped zip under ~/.config/harnesscli/feedback/, copies attached images beside it, uploads those artifacts to the dedicated go-code-feedback-assets GitHub prerelease, creates an issue directly in dennisonbertram/go-code, and reports the issue URL. The images render inline in the issue and the zip is linked for download.
Use /feedback --local <request> when you explicitly want the same local evidence capture without any GitHub upload. The older --issue and --screenshot <path> arguments remain accepted for compatibility, but the normal path is attach image + plain /feedback.
The bundle contains:
version.json— harnesscli version (unstampeduntil version stamping lands), Go version, GOOS/GOARCH, server URL, model, and caveats (e.g. rollouts not configured).config.json— the CLI config with secrets scrubbed: every storedapi_keysvalue is replaced exactly, then the forensics redaction patterns run over the whole file (also covering keys pasted into command history).request.md— the explicit fix or feedback request, redacted.context.json— current workspace, run and conversation IDs, active state, last SSE event ID, server, model, and capture time.transcript.json— up to the newest 200 transcript entries, with per-entry size bounds and truncation provenance.logs/— available~/.harness/logs/harnessd.stdout.logandharnessd.stderr.logtails, redacted and capped at 256 KiB each; otherwise aNOT_PRESENT.txtmarker.rollouts/<date>/<run>.jsonl— the newest five rollout files, each redacted and capped at 1 MiB, whenHARNESS_ROLLOUT_DIRis set; otherwise arollouts/NOT_PRESENT.txtmarker explains the absence.attachments/screenshot.pngor.jpgplusscreenshot.jsonfor one image, or numbered members for multiple images — attached, validated PNG/JPEG evidence of at most 10 MiB per image with original filename, media type, byte size, and SHA-256 checksum.
An adjacent *-issue.md file remains as a recoverable, redacted copy of the submitted issue body. GitHub publication uses the authenticated gh CLI and always targets dennisonbertram/go-code, even when the TUI workspace is another repository. The first publication creates the dedicated asset prerelease; later publications reuse it with unique asset names.
Text bundle members and the generated issue body pass through secret redaction. Attached screenshot pixels are uploaded to GitHub as-is.
Model picker (/model)
The model picker is a two-level browser.
Level 0 — provider list: Anthropic, DeepSeek, Google, Groq, Kimi, OpenAI, Qwen, xAI
Level 1 — model list for the selected provider.
Navigation works with Up/Down (or K/J). Typing characters filters the list in place. Press S to star or unstar a model — starred models persist in ~/.config/harnesscli/config.json across sessions.
Pressing Enter at Level 1 opens a config panel where you can choose the gateway (Direct or OpenRouter), enter an API key, and set reasoning effort for models that support it.
Gateway options
Reasoning effort
For models that support extended thinking (deepseek-reasoner, grok-4-1-fast-reasoning, qwen-qwq-32b), the config panel offers effort levels: blank (provider default), low, medium, or high.
Default model list
Models in the built-in picker
| Provider | Models |
|---|---|
| OpenAI | gpt-4.1, gpt-4.1-mini |
| Anthropic | claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5-20251001 |
gemini-2.5-flash, gemini-2.0-flash | |
| DeepSeek | deepseek-chat, deepseek-reasoner |
| xAI | grok-3-mini, grok-4-1-fast-reasoning |
| Groq | llama-3.3-70b-versatile, qwen-qwq-32b |
| Qwen | qwen-plus, qwen-turbo |
| Kimi | kimi-k2.5 |
Profile picker (/profiles)
Profiles are named configurations that bundle model settings, tool allowlists, and permission policies. The TUI fetches the profile list from GET /v1/profiles and shows them in a picker. Selecting a profile applies it to the next run — the current run is not affected.
The profile name is sent in the profile JSON field of the run request, which maps to RunRequest.ProfileName. Sending a capability profile name in the prompt_profile field will cause the server to reject the request with HTTP 400.
Built-in profiles include full (all tools, default), researcher, reviewer, file-writer, bash-runner, and github. See Subagents and profiles for the full schema.
Sessions
The TUI tracks conversations by passing a conversation_id in every run request. When you send your first message in a new session, the server auto-assigns the ID. All subsequent messages in that session carry the same ID, so the server links them into a coherent conversation history.
Sessions are persisted to ~/.config/harnesscli/sessions.json.
| Command | What it does |
|---|---|
/sessions | Opens a picker of saved sessions. Press D to delete one. |
/new | Resets the conversation ID and clears the viewport, starting a fresh session. |
/fork | Duplicates the current session under a new conversation ID and switches into the copy. |
Forking a session (/fork)
/fork duplicates the live conversation — full message history included — into a new, server-minted conversation ID, then switches you into the copy. Use it to explore an alternative direction mid-task (a different prompt, a different plan) without abandoning or polluting the original session.
Semantics:
- Snapshot at fork time. The fork contains the full message history as it is at the moment you run
/fork; when the server's in-memory view is newer than the last persisted state, the fork captures the newer view. From then on the two conversations diverge independently — turns in one never appear in the other. - You land in the fork. The status bar confirms with
Forked <src> → <new>; you are now in the fork. The transcript stays in place because the fork holds identical history. The original session is untouched and remains resumable via/sessions. - Session store. The fork is registered in
~/.config/harnesscli/sessions.jsonwith aforked from <src-id>hint, so it survives a TUI restart. - Workspace is shared. Only messages are copied — the fork keeps the same workspace path. File state, rewind points, and the pinned flag are not carried over, and token/cost counters start at zero for the fork.
- Errors keep you put. When the fork fails (unknown conversation, server without conversation persistence, network error), the status bar shows the server error and you stay in the current conversation.
- With no active conversation (before the first message),
/forkshows a "send a message first" hint instead of calling the server.
/fork always copies the entire current history. To branch a recorded run from a specific mid-history step, use harnesscli replay -mode fork -fork-step N instead — that is a different feature with its own semantics.
Under the hood /fork calls POST /v1/conversations/{id}/fork — see the HTTP route reference.
Plan mode
Ctrl+O when idleToggling plan mode (when no run is active and no tool card is selected) tells the agent to produce a plan before taking action. The status bar shows [plan] when plan mode is on.
The plan-approval overlay — which would display a proposed plan and let you approve (Y) or reject (N) it before execution — is wired in the TUI but requires the server to emit plan.proposed SSE events. The server does not currently emit these events, so the overlay remains inactive. This is forward-looking UI.
Live event rendering
Every run streams SSE events from GET /v1/runs/{id}/events. The TUI translates each event type into a visual element:
| Event | What you see |
|---|---|
assistant.message.delta | Message bubble builds up character by character (Markdown rendered) |
assistant.thinking.delta | Thinking bar shows "Thinking: <text>..." |
tool.call.started | A tool card appears with status running |
tool.output.delta | Tool card result area grows |
tool.call.completed | Tool card status updates to completed or error |
usage.delta | Cost counter in status bar updates |
run.waiting_for_user | An interactive overlay appears for AskUserQuestion approvals |
run.resumed | The AskUserQuestion overlay is dismissed |
run.completed | Run is marked inactive; assistant transcript is saved |
run.failed | Run is marked inactive; error is rendered in the viewport |
Tool cards
Tool cards show a collapsed summary by default:
- Bash/shell tools: the command (first line, up to 60 characters)
- Write/edit tools:
path (N lines) - Read/view tools: just the path
Press Ctrl+O while a tool card is active to expand it and see the full parameters and output.
File attachments (@path)
Type @ followed by a file path to attach file content to your message. Tab completion works for file paths after @. Before the message is sent to the server, the TUI expands all @ tokens by reading the referenced files and embedding their content in the prompt.
Example:
Explain the bug in @src/api/handler.go
External editor (Ctrl+E)
Press Ctrl+E when no overlay is open to launch your $EDITOR. The current input is written to a temporary file. When you save and quit the editor, the content is loaded back into the input field.
If $EDITOR is not set, the status bar shows `$EDITOR not set`.
The editor integration uses tea.ExecProcess, which suspends the TUI while the editor is running. GUI editors configured as $EDITOR that fork to a window may not work correctly, because the TUI will resume as soon as the terminal process exits, before you have finished editing.
Custom slash-command plugins
You can add your own slash commands by placing .json files in ~/.config/harnesscli/plugins/. The TUI loads them at startup.
Two handler types are supported:
Plugin name rules: must match ^[a-z][a-z0-9-]*$. Plugin load errors appear as a transient status message at startup — they do not prevent the TUI from opening.
Persistent configuration
~/.config/harnesscli/config.json
{
"starred_models": ["gpt-4.1"],
"gateway": "openrouter",
"api_keys": {"openai": "sk-...", "openrouter": "sk-..."},
"history_entries": ["last command", "second-to-last command"]
}
starred_models— models marked withSin the pickergateway—""for Direct,"openrouter"for OpenRouterapi_keys— keys stored here are sent to the server at startup viaPUT /v1/providers/{provider}/keyhistory_entries— command history (newest first, max 100 entries)
Sessions are stored separately in ~/.config/harnesscli/sessions.json.
Environment variables
The TUI reads four environment variables:
| Variable | Purpose |
|---|---|
OPENAI_API_KEY | Detected at startup to show OpenAI as available in the picker |
ANTHROPIC_API_KEY | Same, for Anthropic |
OPENROUTER_API_KEY | Same, for OpenRouter; also sent with OpenRouter model-list requests |
EDITOR | External editor launched by Ctrl+E |
API keys detected from the environment are used only to mark a provider as "available" in the model picker. They are not forwarded to harnessd. The server reads its own environment variables independently. Only keys stored in ~/.config/harnesscli/config.json are actively pushed to the server via PUT /v1/providers/{provider}/key on startup.
TUI vs one-shot CLI
| Feature | One-shot (--prompt) | TUI (--tui) |
|---|---|---|
| Multi-turn conversation | No | Yes |
| Live tool-call cards | No (raw events to stdout) | Yes |
| Model / profile / session switching | No | Yes |
| Interrupt mid-run | No (kill process) | Yes (two-stage Ctrl+C) |
| Transcript export | No | Yes (/export) |
| Slash commands | No | Yes (20+ commands) |
| Requires a TTY | No | Yes |
| Machine-parseable output | Yes (JSON lines) | No |
| Piping / scripting | Yes | No |
Next steps
- Profiles — learn how to restrict tools and set cost limits for a run: Subagents and profiles
- Events — understand the full SSE event schema the TUI renders: Events
- harnesscli flags — the complete flag reference for one-shot mode and all subcommands: harnesscli reference