Build your own frontend
Tau’s Textual app is one frontend, not the architecture. A custom UI plugs into the same primitives the built-in TUI uses:
CodingSession — owns the coding-agent environmentAgentEvent — describes assistant text, tool calls, results, errorsFrontend state — belongs to your UIThe reusable tau_agent package stays independent of terminal frameworks,
widgets, keybindings, config paths, and slash-command UX. Build against
tau_coding.session.CodingSession, not Textual widgets.
CodingSession provides the environment (provider/model, tools, persistence,
skills, prompt templates, project context, slash-command handling, compaction).
Your frontend provides the interface (prompt input, transcript rendering, command
entry, cancellation, pickers).
Minimal event loop
Section titled “Minimal event loop”async for event in session.prompt(user_text): render_event(event)The stream yields provider-neutral AgentEvent values from tau_agent.events
(see the agent loop for the list). Render from these, never
from provider-specific chunks. Treat AgentStartEvent/AgentEndEvent and
non-recoverable ErrorEvent as the source of truth for “is the agent working?”.
Steering and follow-ups
Section titled “Steering and follow-ups”If the user submits while a run is active, queue instead of starting a second run:
async for event in session.prompt(user_text, streaming_behavior="steer"): adapter.apply(event); redraw(state)Use streaming_behavior="follow_up" for a prompt that waits until the run would
otherwise stop. Overlapping session.prompt(...) calls without
streaming_behavior are rejected so two runs can’t mutate one transcript.
QueueUpdateEvent carries pending queued text for badges/status.
Slash commands
Section titled “Slash commands”Slash commands belong to tau_coding. Before treating input as a prompt:
result = session.handle_command(text)If result.handled, apply the requested effect (exit_requested,
clear_requested, new_session_requested, compact_summary, message) and
show reference/status output outside the durable conversation. If
result.compact_summary is not None, call await session.compact(result.compact_summary)
(an empty string means “use the built-in prompt as-is”).
/skill:<name> is intentionally not a command — pass it through to
session.prompt(...), which expands it before the run.
Restoring and switching sessions
Section titled “Restoring and switching sessions”Initialize the visible transcript from session.messages (the built-in
TuiState.load_messages() is a reference). ToolResultMessage preserves
structured metadata (e.g. edit patches), so you can render restored tool results
without reading JSONL directly.
For session switching, use tau_coding.session_manager.SessionManager —
list_sessions(session.cwd), then await session.resume(session_id) (or load a
fresh CodingSession with storage=jsonl_session_storage(record.path)), then
rebuild the transcript from session.messages.
Cancellation, pickers, keybindings
Section titled “Cancellation, pickers, keybindings”- Cancel with
session.cancel()— keep consuming events until the stream ends. - Read picker data directly from the session:
command_registry.list_commands(),skills,prompt_templates,available_model_choices,available_models,available_providers,thinking_level,available_thinking_levels,session_manager. For model changes from another provider, callset_provider(...)thenset_model(...). - Keybindings and themes are frontend policy. The built-in app reads
~/.tau/tui.jsonviatau_coding.tui.load_tui_settings(), but your UI can ignore it.
What not to depend on
Section titled “What not to depend on”Avoid coupling to private CodingSession attributes, provider-specific response
chunks, Textual internals, or the raw JSONL structure (use SessionManager /
CodingSession). Stick to the event, message, tool, harness, and session
primitives.