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>
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_turnis called on anoffloadthread and may throwTuiChatBackendError(shown aserror: ...) orTuiChatInterrupted. Everything else it throws is shown with itswhat().handle_commandreturns false to leave the app and fillsoutputwith what the command printed.set_prompterhands 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_factorydoes the same for a command's live output (TuiStreamSink::feed,pause,close).history_backend()returnsstd::make_shared<VaultHistory>(load_fn, add_fn, profile, bot_fn)with the three store calls injected: load (input_historyrows 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/*.cppand links-lcurl.GrrClientandGrrStreamuse libcurl; threads come from libc (no-pthreadneeded on glibc 2.34 or later; add it for the static link line if the linker asks). src/tui/tui.mklists nothing; the old stubtui.cppis gone (nothing outsidesrc/tuiuseddetz::tui::renderorpoll_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 ofsrc/tuiplus-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().