Skip to main content

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:

PrioritySegmentExample
1Model name (bold, up to 24 chars)gpt-4.1
2Running indicator...
3Cumulative cost$0.0042
4Permission mode (hidden when default)[plan]
5Git branch(main)
6Workspace path (up to 20 chars)~/projects/myapp
7MCP failure count2 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.

KeyAction
EnterSubmit message or confirm selection
Shift+Enter / Ctrl+JInsert a newline in the input
Up / Ctrl+PInput history up, or scroll viewport
Down / Ctrl+NInput history down, or scroll viewport
PgUpScroll viewport up half a screen
PgDnScroll viewport down half a screen
/Open slash-command autocomplete dropdown
@Insert @ to begin a file-path attachment
TabComplete a slash command or file path
Ctrl+OExpand/collapse active tool card, or toggle plan mode when idle
Ctrl+EOpen $EDITOR for multi-line prompt editing
Ctrl+SCopy last assistant response to clipboard
Ctrl+GSteer the active run: inject the input-box text into the running turn (see below)
Ctrl+UClear the input (when no overlay is open)
Ctrl+CTwo-stage interrupt (see below)
EscMulti-priority close: dropdown → overlay → cancel run → clear input
? / Ctrl+HOpen help dialog

Two-stage interrupt

Accidentally pressing Ctrl+C mid-run would be frustrating, so the TUI requires confirmation:

  1. First Ctrl+C

    An interrupt banner appears: "⚠ Press Ctrl+C again to stop, or Esc to continue". The run keeps going.

  2. Second Ctrl+C

    The run is cancelled. The status bar shows "Interrupted".

  3. 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+G is a no-op with a status hint.
  • The same path is available outside the TUI as harnesscli steer <run-id> <prompt>.
  • Ctrl+S (copy) and Esc (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.

CommandDescription
/modelOpen the model picker
/profilesView and select a capability profile
/sessionsBrowse 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
/newStart a fresh conversation (resets conversation ID)
/forkFork 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
/exportExport the conversation to a Markdown file
/statsShow cumulative cost and token statistics
/contextShow context window usage
/keysManage provider API keys
/runsList 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
/subagentsView active subagent processes
/permissionsView the current session's tool permissions
/attachAttach file context with @path tokens
/doctorShow local harness diagnostic commands
/helpShow the help dialog
/clearClear the conversation history and viewport
/quitQuit 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-dir lists the attached directories; /add-dir remove <path> detaches one. (remove is a subcommand only when followed by a path, so a directory literally named remove can still be added.)
  • The list is session-scoped — it is not persisted across restarts.
  • Current limits: the bash tool's command sandbox and glob still 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 (unstamped until 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 stored api_keys value 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.log and harnessd.stderr.log tails, redacted and capped at 256 KiB each; otherwise a NOT_PRESENT.txt marker.
  • rollouts/<date>/<run>.jsonl — the newest five rollout files, each redacted and capped at 1 MiB, when HARNESS_ROLLOUT_DIR is set; otherwise a rollouts/NOT_PRESENT.txt marker explains the absence.
  • attachments/screenshot.png or .jpg plus screenshot.json for 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

ProviderModels
OpenAIgpt-4.1, gpt-4.1-mini
Anthropicclaude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5-20251001
Googlegemini-2.5-flash, gemini-2.0-flash
DeepSeekdeepseek-chat, deepseek-reasoner
xAIgrok-3-mini, grok-4-1-fast-reasoning
Groqllama-3.3-70b-versatile, qwen-qwq-32b
Qwenqwen-plus, qwen-turbo
Kimikimi-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.

CommandWhat it does
/sessionsOpens a picker of saved sessions. Press D to delete one.
/newResets the conversation ID and clears the viewport, starting a fresh session.
/forkDuplicates 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.json with a forked 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), /fork shows 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 idle

Toggling 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:

EventWhat you see
assistant.message.deltaMessage bubble builds up character by character (Markdown rendered)
assistant.thinking.deltaThinking bar shows "Thinking: <text>..."
tool.call.startedA tool card appears with status running
tool.output.deltaTool card result area grows
tool.call.completedTool card status updates to completed or error
usage.deltaCost counter in status bar updates
run.waiting_for_userAn interactive overlay appears for AskUserQuestion approvals
run.resumedThe AskUserQuestion overlay is dismissed
run.completedRun is marked inactive; assistant transcript is saved
run.failedRun 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 with S in the picker
  • gateway"" for Direct, "openrouter" for OpenRouter
  • api_keys — keys stored here are sent to the server at startup via PUT /v1/providers/{provider}/key
  • history_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:

VariablePurpose
OPENAI_API_KEYDetected at startup to show OpenAI as available in the picker
ANTHROPIC_API_KEYSame, for Anthropic
OPENROUTER_API_KEYSame, for OpenRouter; also sent with OpenRouter model-list requests
EDITORExternal 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

FeatureOne-shot (--prompt)TUI (--tui)
Multi-turn conversationNoYes
Live tool-call cardsNo (raw events to stdout)Yes
Model / profile / session switchingNoYes
Interrupt mid-runNo (kill process)Yes (two-stage Ctrl+C)
Transcript exportNoYes (/export)
Slash commandsNoYes (20+ commands)
Requires a TTYNoYes
Machine-parseable outputYes (JSON lines)No
Piping / scriptingYesNo

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