Files
tai/taicpp/docs/wiring/tui.md
T
retoorandClaude Sonnet 5.5 bbdc927215 tui: grr terminal client (Grr* classes), control socket, entry points, spec test, parity docs
Port of the grr client of tai.py: client, SSE stream, session, store, router, shell, views,
chat, panes, file viewer and the unix control socket; GrrTui::run is the --tui entry.
Verified against tai.py --tui on a real grr server over a pty: 236 steps, screen text and
per-cell styles identical except clocks, spinner frames and server-made ids.
docs/tui.md holds the symbol table and gaps, docs/wiring/tui.md the integrator hooks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 06:15:05 +01:00

6.2 KiB

wiring: src/tui

What the integrator applies to main.cpp, the Makefile and the agent module. src/tui edits nothing outside itself. Include tui.h for every entry point below. The python reference is main() in tai.py (the block that starts at if args.demo:).

1. Order in main() (same order as python)

#include "tui.h"
using namespace detz::tui;

if (opts.demo)       return TuiDemo::demo();                    // --demo
if (opts.demo_shot)  return TuiDemo::demo_shot(std::cout);      // --demo-shot (30x100 frame as text)

std::string theme_name;
try { theme_name = TuiThemes::resolve_name(opts.theme); }       // --theme, then TAI_THEME, then the default
catch (const std::invalid_argument& e) { std::cerr << e.what() << "\n"; return 2; }

// ... --mplex and --plexer-wrap come here, as in python ...

if (opts.tui)        return GrrTui::run();                      // --tui: the grr client, needs no adapters

// ... profile check, boot(args), --env, eval, scheduler, telegram ...

if (opts.tui_local) {                                           // --tui-local
    auto agent = std::make_shared<YourChatAgent>(boot_result);  // section 2
    YourChatHost host;
    int code = TuiChatScene::run(host, agent, theme_name);
    boot_result.store.close();                                  // python: finally agent.store.close()
    return code;
}

Exit codes: GrrTui::run() returns 2 when the terminal is not interactive (tui unavailable: ... on stderr) and the app's own code otherwise. TuiDemo::demo() returns 2 with demo unavailable: ... in the same case. Both need a tty on stdin and stdout.

--demo-shot writes one composed frame, so diff <(./tai.py --demo-shot) <(detz --demo-shot) is the acceptance check (identical today at 30x100).

2. Adapters for --tui-local (the agent module is another port)

TuiChatScene depends on two interfaces in TuiChatAgent.h, never on the agent module:

interface python it replaces the integrator maps it to
TuiChatAgent Agent as tui_build_chat uses it (bot, profile, messages, pins, store.list_bots, load_session, save_session, switch_bot, run_turn(capture=True), checkpoint, quiet flag, handle_command, the on_phase, prompter and stream_factory hooks, VaultHistory.for_agent) detz::agent::Agent and detz::store::Store
TuiChatHost module functions tui_spawn_agent, running_agents, VERSION, short_error, start_scheduler free functions of the agent and cli modules

Contracts that matter:

  • run_turn is called on an offload thread and may throw TuiChatBackendError (shown as error: ...) or TuiChatInterrupted. Everything else it throws is shown with its what().
  • handle_command returns false to leave the app and fills output with what the command printed.
  • set_prompter hands the agent a function that may be called from the turn thread: it blocks until the user answers a dialog on the loop. set_stream_factory does the same for a command's live output (TuiStreamSink::feed, pause, close).
  • history_backend() returns std::make_shared<VaultHistory>(load_fn, add_fn, profile, bot_fn) with the three store calls injected: load (input_history rows of the profile, oldest first), add (profile, bot, text), and the current bot name.

3. Process-wide tables to fill once at boot (before TuiChatScene::run)

TuiComplete::sources() is empty until the integrator fills it; completion and /help read nothing else. Fields, with the python data each comes from:

field python
commands COMMANDS
help_text HELP_TEXT (also what TuiComplete::command_help() parses)
provider_labels PROVIDER_LABELS
mention_auto MENTION_AUTO
default_models sorted union of ZEN_FREE_MODELS, POLLINATIONS_MODELS, DEVPLACE_FREE_ROUTES and OPENROUTER_MODEL
complete_files the path completer (prefix, directory) used by the REPL
dir_key the key of the current directory in the vault

The grr client (--tui) needs none of this. It reads GRR_URL, TAI_HOME (for grr.json and the control socket tui.sock) and TAI_TUI_SOCK, like python.

4. Build

  • The top Makefile already finds src/tui/*.cpp and links -lcurl. GrrClient and GrrStream use libcurl; threads come from libc (no -pthread needed on glibc 2.34 or later; add it for the static link line if the linker asks).
  • src/tui/tui.mk lists nothing; the old stub tui.cpp is gone (nothing outside src/tui used detz::tui::render or poll_events).
  • Static build: libcurl needs no extra feature here (plain http to the grr server, https through the openssl already linked).
  • Tests (each is a stand-alone program, run from taicpp/; objects of src/tui plus -lutil -lcurl -pthread):
file needs checks
tests/tui_spec.cpp ../grrtui.md (GRRTUI_MD to move it) routes, keys, caps, shapes, events, flags against the doc; exit 1 on drift
tests/tui_shot.cpp nothing demo frame as text, sgr and raw bytes for the differential runs
tests/tui_script.cpp nothing scripted input scenarios on a headless app
tests/tui_rows.cpp nothing chat row, markdown and textwrap cases on stdin
tests/tui_chat.cpp nothing chat scene scenarios with a fake agent
tests/tui_grr.cpp a grr server the --tui entry; drive it through the control socket

make test does not run these yet; a rule per file is the same shape as $(BUILD)/store_test: $(CXX) $(CXXFLAGS) -Isrc/tui tests/tui_spec.cpp src/tui/*.cpp -lutil -lcurl -pthread -o $@.

5. The control socket (GrrControl)

GrrTui::run() starts it. One JSON line in, one out, mode 0600, at $TAI_TUI_SOCK or $TAI_HOME/tui.sock (default ~/.tai/tui.sock). Ops: screen (optional until, timeout), key (keys, at most 200), type (text), click (x, y), goto (route), result (id). Any op takes async and wait. GrrControl::call(payload) is the client side (python grr_control_call). If another TUI already answers on the path, start() returns false and the second TUI runs without a socket.

If the integrator adds a CLI front for it (python has none in this file), it is GrrControl::call(TuiValue::loads(line)) and print dumps().