Audience: anyone using frontier-cli to debug a UserTalk script.
Last updated: 2026-06-25
C.6 change (2026-06-25): the boxen REPL is now the default for
frontier-cli(no flag).--debug-tuiis preserved as a deprecated no-op alias and emits a one-line warning at startup. The legacy linenoise REPL is reachable via--plain. Examples in this guide use the no-flag form; substitute--debug-tuiif you have older scripts wired against it (they still work).Phase C.0 change (2026-06-08, retained for context):
--debug-tuioriginally opted into the boxen-native Frontier REPL (vs. the standalone debugger TUI). The REPL is a full scrollback + input-bar interface; you type UserTalk expressions or slash commands directly. The debugger surfaces are reachable via the/debugslash command.
The TUI debugger is a full-screen terminal interface for debugging UserTalk scripts. It runs inside frontier-cli and uses the same NDJSON debugger protocol that drives --protocol mode.
For the protocol-level interface (used by IDEs and external tools), see docs/CLI_USAGE_GUIDE.md. For TUI internals (transport wiring, lazy-attach contract, teardown order), see docs/TUI_DEBUGGER_ARCHITECTURE.md.
./frontier-cli/frontier-cliThe boxen REPL opens with a scrollback output pane and a single-line input bar. Type UserTalk expressions (e.g. 1 + 1) and press Enter to evaluate. Type /help and press Enter to see available slash commands. Press Ctrl-C to quit.
./frontier-cli/frontier-cli --debug-tui @workspace.myVerbThe REPL opens and automatically dispatches /debug @workspace.myVerb, starting a debug session on that script. Equivalent to opening the REPL and typing /debug @workspace.myVerb manually.
Previously (Phase B), this opened the standalone debugger TUI directly on the script. The REPL-first launch is the new default; the standalone debugger is reachable via /debug inside the REPL.
The Phase B "attach-only" form (--debug-tui with no script argument) is now the REPL's default state. Evaluate expressions or dispatch /debug <path> to start a debug session. Any thread.callScript thread that hits a breakpoint will lazy-attach through the existing transport infrastructure.
./frontier-cli/frontier-cli --debug-tui @workspace.myVerbThe TUI starts, compiles the source body of workspace.myVerb, and spawns a debug thread that suspends at the first statement before executing anything. The source pane loads immediately; the user is dropped into the entry suspension with all F-key controls live.
The @path form is a "bare ODB address": it must start with @ and the rest must be a dotted identifier using only [A-Za-z0-9_.]. If the path has whitespace, parens, or operators, it is treated as a free-form expression (see below).
Running the script body is the right semantic for debugging: most Frontier.root scripts have a bundle pattern at the bottom that calls the script's entry point with test arguments. That bundle is part of the script body and runs when you F5 from the entry suspension.
./frontier-cli/frontier-cli --debug-tui -e "local (t); new (tableType, @t); myScript (@t)"Multi-statement expressions and any expression that is not a bare ODB address are passed through to the compiler verbatim. The expression is compiled and the resulting thread starts suspended at its first statement.
./frontier-cli/frontier-cli --debug-tui path/to/script.utThe file is read and its contents are dispatched as an inline expression. Useful for developing and testing UserTalk scripts that live outside the system root.
All three launch forms start the debug thread suspended at the first statement (start_suspended=true). This is the entry-suspension behavior: you are halted before any code runs, regardless of whether breakpoints are already saved in the script. Press F5 to run until the next saved or in-session breakpoint.
Requirements:
- A real TTY. The TUI opens
/dev/ttydirectly viatermbox2and cannot be piped or remapped. If you redirect stdin/stdout, initialization fails with a logged error and the binary exits non-zero. --debug-tuiis mutually exclusive with--protocol. Combining them is a usage error caught at argument parsing.
On launch you'll see three regions:
+-- Script ---------------------+-- Stack / Locals ------+
| 1: on doSomething(x) | #0 doSomething |
|> 2: local total = 0 | #1 caller |
| 3: loop (i = 1; i < x) | |
| 4: total = total + i | -- Locals -- |
| 5: return total | total 0 |
| | i (unset) |
+-------------------------------+------------------------+
| F5:continue F9:bp S-F9:cond F10:step-over F11:step-in S-F11:step-out q:quit |
+-----------------------------------------------------------------------------+
The footer always shows the active keybind set. When the eval pane (:) opens, the footer text is temporarily replaced with the prompt; pressing Escape or Enter restores the keybind line.
The TUI tracks one of two debugger states:
| State | Meaning | What's actionable |
|---|---|---|
| RUNNING | The UserTalk thread is executing. No frame is suspended. | F9 (set breakpoint at future line) accepts input only when a current line is known. Step/continue keys are ignored. |
| SUSPENDED | A thread hit a breakpoint, step, or watchpoint and is paused. | All keybinds active. Stack and locals reflect the suspended frame. |
State transitions happen automatically as runtime messages arrive over the attached transport. You don't toggle them manually.
| Key | Action |
|---|---|
| F5 | Continue execution until next breakpoint. |
| F10 | Step over (next statement in current frame). |
| F11 | Step into (descend into call). |
| Shift-F11 | Step out (run until current frame returns). |
Pressed while RUNNING, these are silently ignored — the runtime can't accept another step until it suspends again.
| Key | Action |
|---|---|
| F9 | Toggle a plain breakpoint at the current line. Requires a non-empty script path and a known current line. |
| Shift-F9 | Open the conditional breakpoint modal at the current line. Type a UserTalk expression (e.g. x > 100); Enter installs, Escape cancels and preserves the modal text for next time. |
Watchpoints fire when a target address changes value. Trigger the watchpoint modal by Cmd-double-clicking a local identifier in the script pane (see "Mouse" below), or via the protocol from an external tool.
In the modal:
- The suggested path (e.g.
@user.foo.bar) is pre-filled. - Edit if needed; Enter installs, Escape cancels.
| Key | State | Action |
|---|---|---|
: |
RUNNING (no attached thread) | Activates the eval pane for a new debug-run launch. |
: |
SUSPENDED | Activates the eval pane to evaluate in the current frame. |
| typing | (pane active) | Builds the expression. |
| Enter | RUNNING pane | Dispatches a debug/run; the new thread starts suspended at the first statement. Bare ODB addresses (@some.path) are resolved to source text first; all other expressions pass through verbatim. |
| Enter | SUSPENDED pane | Dispatches script/eval against the suspended frame; result appears in the history above the input row. |
| Escape | (pane active) | Dismisses the pane (the partial expression is preserved for re-activation). |
: is a no-op in IDLE state (no transport thread is active).
While the eval pane is active, q and : are typed into the expression — they don't trigger quit or re-activate the pane. The eval pane is suppressed while a modal (condition / watchpoint) is open.
Breakpoints set via F9 and Shift-F9 are stored in the scriptType external for the relevant script and persist when the .root saves (which happens automatically at exit). This means you can install breakpoints in a prior TUI session (or via --protocol), save, and then relaunch with --debug-tui @that.script in a fresh session. You will suspend at the first statement as usual; press F5 to run until the first saved breakpoint fires.
The canonical workflow for debugging an existing Frontier.root script:
- Open the script in any tool (REPL, prior TUI session, or
--protocolfrom an IDE). - Set breakpoints with F9 (toggle plain) or Shift-F9 (conditional).
- Exit so the
.rootsaves the breakpoints. ./frontier-cli/frontier-cli --debug-tui @workspace.myScript- TUI opens suspended at the first statement. Press F5 to run until the first saved breakpoint.
History rendering caps the visible expression at 80 characters per entry and keeps a small ring of recent results.
Cmd-double-click (Meta + left button, double-click) on a word in the script pane while SUSPENDED resolves it as an identifier:
- If the word is in the current frame's locals, its value flashes in the stack/locals pane.
- If it's a watchpoint candidate, the watchpoint modal opens pre-filled.
In the current release this is locals-only: full ODB navigation (descend into tables, follow addresses) is Phase C work. The gutter column is not clickable — only the source-text portion of the script pane.
| Key | Action |
|---|---|
| q | Clean exit. Drains pending lazy-attach threads, tears down boxen, restores stdin/stdout. |
| Escape (no modal, no eval pane) | Same as q. |
| Ctrl-C | Same as q if delivered as a key event; not a SIGINT handler. |
The drain is mandatory: it waits for any thread that hit a breakpoint via the lazy-attach path to release its transport reference before the transport itself is freed. See TUI_DEBUGGER_ARCHITECTURE.md.
The TUI is the supported way to debug menu-triggered UserTalk code that runs on thread.callScript threads (invisible to LLDB).
- Launch the TUI:
./frontier-cli/frontier-cli --debug-tui
- Wait for the source pane to show the system root (idle state — no script loaded yet).
- From another terminal (or via your normal automation), trigger the menu action. The TUI receives the breakpoint hit via the lazy-attach transport, suspends, loads source, and highlights the current line.
- Step or continue. Set additional breakpoints with F9. When the thread completes, the TUI returns to RUNNING.
- Press
qto exit.
To stop at the very first line of a known verb (e.g. commands.open), use the launch-from-address form:
./frontier-cli/frontier-cli --debug-tui @commands.openThe TUI launches and immediately suspends at the first statement of commands.open. No need to set a breakpoint first; the entry suspension is unconditional. See "Setting persistent breakpoints" above if you want subsequent breakpoints to fire on F5.
- Step or continue until you're suspended at the line you want to gate.
- Press Shift-F9. The condition modal opens.
- Type a UserTalk expression that evaluates to true/false in the current scope (e.g.
i = 42,x > threshold,name = "target"). - Enter installs. Continue (F5). The breakpoint now fires only when the condition evaluates truthy at that line.
- Plain F9 at the same line removes the conditional breakpoint (toggle semantics).
The expression runs in the suspended frame's scope — locals are in scope, and the same identifier resolution rules apply as script/eval.
- Suspended at any line.
- Cmd-double-click the local identifier in the script pane.
- The watchpoint modal opens with the resolved address (e.g.
@local.total) pre-filled. - Enter installs. Continue.
- The TUI suspends again the next time that address changes value, with the stack at the writing frame.
These are deliberate scope cuts, not bugs:
- No WebSocket-attached debugging. The TUI uses the in-process lazy-attach transport. Remote/WS attach is permanently deferred — the use-after-free surface across the WS boundary is not safe to ship. Use
--protocolover stdio for any external-tool integration. - Locals-only Cmd-double-click. Full ODB navigation (descend into tables, follow
@adrreferences, open address values in a viewer) lands in Phase C. - Single-row eval history footer. The eval pane shows one recent result in the footer area. A scrollable history pane is Phase C work.
- No breakpoint listing UI inside the TUI. Set/clear works (F9, Shift-F9). To see all currently-installed breakpoints, use the protocol-level
debug/listBreakpointsop. - No source-pane scroll keys. The pane scrolls automatically to keep the suspended line visible. Manual scrolling will be added in Phase C alongside the REPL pane work.
| Symptom | Cause | Fix |
|---|---|---|
| "boxen_init failed" then immediate exit | Stdin/stdout not a TTY (piped or redirected). | Run from a real terminal. The TUI cannot operate without /dev/tty. |
| "--debug-tui and --protocol are mutually exclusive" | Both flags on the command line. | Pick one. |
| Step/continue keys ignored | State is RUNNING — no frame is suspended. | Wait for a breakpoint to fire, or press : to launch a new debug-run from a script address or expression. |
Eval pane : does nothing |
State is IDLE (no transport), or a modal is open. | Close any open modal with Escape. In IDLE state there is no active thread; launch with --debug-tui @path or : is available once the TUI is in RUNNING or SUSPENDED. |
| Cmd-double-click does nothing | Click landed in the gutter column, or the identifier isn't in current locals, or state is RUNNING. | Click on the source text portion (right of the line number), while suspended, on a local identifier. |
| Quit hangs briefly | Drain step waiting for a lazy-attached thread to release the transport. | Normal; should complete in milliseconds. If it hangs >5s, investigate runtime thread state via --protocol from a separate session. |
If something in the TUI doesn't behave as this guide describes, file against the frontier-cli component with reproduction steps. For Phase C scope items (the limitations above), see planning/phase_c/OVERVIEW.md — those are tracked but won't be addressed as TUI bugs.