Skip to content

Using the TUI

Once the TUI is up you have an input box at the bottom of the screen. What you type there is interpreted by its first character:

  • A plain message is sent to the model as your prompt. The assistant's reply, its thinking, and any tool calls stream into the transcript above the input box. While a turn is running you can keep typing: pressing Enter steers the model with the new text, and Esc interrupts the turn.
  • !command runs command in a shell in the current directory. Its output is committed to the transcript and buffered, so it is included with your next message to the model (handy for showing the assistant a build error or a git diff).
  • !!command runs command the same way but keeps the output display-only: it is shown to you but not fed to the model.
  • /command runs a slash command (see below), recognised only when / is the very first character of the first row.

Slash commands

Slash commands drive the session itself rather than the model. Type / on its own (or /help) to see the full list rendered in the transcript, and /<command> --help for one command's own help. The available commands are:

Command Action
/quit (/q) Quit Wallah; with background jobs still running, reports them and stays. /quit --force cancels them and quits anyway
/jobs List the background sub-agents (launched by config slash commands) still running
/kill <id> Cancel a running background job by its id (from /jobs), or every job with --all
/model [ref] Switch the active model as [provider/]model[@effort]; with no argument, open the model picker
/personality [ref] Switch the active personality as personality[/mode]; with no argument, open the personality picker
/new [name] Start a new session, keeping the current provider, model, and effort
/name [name] Rename the current session; with no argument, clear the name
/resume Resume a previous session in this project
/fork Fork the current session at an earlier turn
/compact Compact the conversation now, replacing the oldest turns with a summary while keeping a recent tail
/reset-provider-session Re-seed the provider from the session history, discarding any continuable server-side session
/recall <id> Show the full original content of an item by its recall id (the number in a #12 tag)
/ollama Inspect and tune the native Ollama provider's context window
/reload Re-read config.lua and rebuild the runtime environment for the current session
/keys Show the current key bindings
/usage Report token usage across stored sessions for this month, week, and day, split by provider and model and priced from the model catalog. --project <name> scopes it to one project. --by-role adds a second breakdown by personality and mode.
/codex-login Sign in with a ChatGPT subscription for Codex models, renewing an expired sign-in without leaving the session
/codex-logout Remove the stored ChatGPT sign-in
/help Print the slash-command help

Your config can add its own commands, too, either expanding into a canned prompt or launching a background sub-agent. See defining your own slash commands, and personalities, modes, and sub-agents for the personalities /personality switches between.

Key assignments

The TUI recognises the following keys. The composing hotkeys are configurable; see Rebinding keys below.

While composing a prompt:

Key Action Config name
Enter Send the message (or run a / command or ! shell command) submit
Shift+Enter Insert a newline insert_newline
Tab Open the completion popup (slash commands, file paths) complete
Shift+Tab Cycle the active personality's mode cycle_mode
Up / Down Move through input history (at the first / last row) (fixed)
Ctrl-o Toggle full-output expansion for tool and shell blocks expand_output
Ctrl-g Compose the prompt in $VISUAL / $EDITOR edit_externally
Escape / Ctrl-c Interrupt a running shell command interrupt
Ctrl-d Quit (when the input is empty) quit

While the completion popup is open:

Key Action
Tab / Down Select the next candidate
Shift+Tab / Up Select the previous candidate
Enter Accept the selected candidate
Esc Close the popup

While a turn (or a compaction pass) is running:

Key Action
Esc / Ctrl-c Interrupt the turn
Enter Steer the running turn with the typed text
Shift+Enter Insert a newline
Shift+Tab Cycle the active personality's mode
Ctrl-o Toggle full-output expansion

In a picker (/model, /resume, fork selection):

Key Action
Up / Down Move the selection
Ctrl-p / Ctrl-n Move the selection
Enter Accept the highlighted item
Esc / Ctrl-c Cancel the picker
Backspace / printable keys Edit the fuzzy filter

Rebinding keys

The composing hotkeys can be reassigned from your config with agent.configure_keys. Each field names an action (the config names in the table above) and the press, or list of presses, that triggers it:

agent.configure_keys {
  submit = 'ctrl-enter',            -- a single press, or
  quit = { 'ctrl-d', 'ctrl-q' },   -- several presses, or
  complete = {},                   -- an empty list to unbind
}

A press is written as optional ctrl-, alt-, shift-, and super- prefixes followed by a key: a single character (a, /), space, a function key (f1 through f24), or a named key. A named key is spelled as its termwiz key name in snake_case, such as enter, tab, escape, backspace, delete, insert, home, end, page_up, page_down, up_arrow, down_arrow, left_arrow, and right_arrow. An omitted action keeps its default; an empty list unbinds it. A press bound to two actions, or a key name that does not parse, is reported against the line that set it.

Actions read differently in each context: interrupt cancels a running turn, a compaction pass, or a shell command depending on what is active, and does nothing when nothing is running. Set disable_default_keymap = true to start from an empty map, so only the presses you configure take effect. The completion popup and picker keys are fixed and not yet configurable.

Transcript display

When a turn settles, the various tool and other UI blocks commit to scrollback. Tool and shell blocks commit as a short summary by default. Ctrl-o toggles them to commit their full output instead.

Thinking blocks commit their full body by default. Your config can change that with agent.configure_display:

agent.configure_display {
  expand_thinking = false,   -- commit thinking to a one-line summary
}

If a block was collapsed and you find that you want to see into it, look for the /recall note that shows you the record number to use to recall that output from the block: /recall <id>.

A harness watchdog nudge shows as a dim chrome line, » watchdog: <text>, distinct from an ordinary user message. The nudge message itself is part of the conversation history. The line is display-only.

The status bar

Below the input box sits the status bar. By default it is two dim lines: a path line on top, and a status line below with your usage on the left and your session on the right.

/repos/wallah  (main)
↑1.2k ↓3.4k R5.0k CH60.0% 50.0%/140 42 tok/s 210 ch/s                  claude-code · sonnet • medium

The path line shows the working directory and the git branch. The status line's left shows token counts, the cache-hit rate, the context gauge, and live throughput; its right shows the session name, the personality and mode, the model, and the effort.

Your config can throw all of that out and draw the bar itself, by handling the format_status event:

local status = require('status')

agent.on('format_status', function(ctx)
  return {
    status.flat { status.span { text = 'hello from my config' } },
  }
end)

The handler receives a ctx argument and returns a list of lines. One handler may be registered. Without one the built-in bar shows. It runs on a roughly once-a-second tick and again whenever the state it reads changes, off the main loop, so a slow handler never stalls your typing or the model's output. If the handler raises an error, the bar shows the traceback rather than taking down the session.

Laying out a line

Each line is one of two shapes. Require the status module to build them.

A status.flat line lays its pieces out left to right. A status.fill between two pieces eats the leftover width, pushing what follows to the right edge:

status.flat {
  status.span { text = 'sonnet' },
  status.fill { glyph = ' ' },
  status.span { text = 'main' },
}
sonnet                                                              main

Give the fill a visible glyph and it draws a rule the width of the bar:

status.flat { status.fill { glyph = '-' } }
-----------------------------------------------------------------------

A status.zones line aligns independent groups: left at the left edge, right at the right edge, and center in the true middle of the whole width.

status.zones {
  left = { status.span { text = 'sonnet' } },
  center = { status.span { text = 'plan mode' } },
  right = { status.span { text = 'main' } },
}
sonnet                            plan mode                            main

When the groups would collide, the center drops out first. If that is still not enough room, the right is trimmed from its own leading edge, keeping the left and the tail end of the right (the part closest to the edge) intact.

The pieces of text are status.span values. A span is { text = '...' } plus any styling: fg, bg, bold, italic, underline, dim, reverse, and strikethrough. Each color is a "#rrggbb" hex string, a color name, a CSS color spec, or a palette index from 0 to 255:

status.span { text = 'BUILD', fg = '#ff0000', bold = true }

Return as many lines as you like. The bar grows to fit, and when the terminal is too short for everything, the transcript above gives up its rows first and the bar is trimmed last.

Reusing the built-in pieces

ctx (a StatusCtx) contains the raw values behind the bar, to render them however you like:

  • Model and session: model, provider, effort, thinking, personality, mode, session_name
  • Context and usage: context_window, context_used, context_percent, input_tokens, output_tokens, cache_read, cache_write, cache_hit_rate, tokens_per_second, chars_per_second
  • Environment: cwd, git_branch, turn_active, width

ctx.default (a StatusCtxDefault) returns the built-in bar's own pieces, already styled, each a list of spans you can drop straight into a line. A piece with nothing to show is an empty list.

Piece What it renders
tokens Up, down, and cache token counts
cache_hit The cache-hit rate
context The context-occupancy gauge
throughput Tokens and characters per second, only mid-turn
name The session name
scope The personality and mode
model The provider and model
effort The effort level and thinking keyword
cwd The working directory
branch The git branch
left The whole stock left group (tokens, cache_hit, context, throughput)
right The whole stock right group (name, scope, model, effort)
path The whole stock path line (cwd, branch)

So you can keep most of the stock bar and change one part. Here the left side is left unchanged, the right is replaced with just the model, and the model turns red while a turn is running:

local status = require('status')

agent.on('format_status', function(ctx)
  local model_fg = ctx.turn_active and '#ff0000' or '#888888'
  return {
    status.zones {
      left = ctx.default.left,
      right = { status.span { text = ctx.model, fg = model_fg } },
    },
  }
end)