Skip to content

feat(repl): a full-screen REPL with anchor's TUI experience - #143

Merged
fuseraft merged 6 commits into
mainfrom
feat/tui
Oct 15, 2026
Merged

fuseraft merged 6 commits into
mainfrom
feat/tui

Conversation

@fuseraft

@fuseraft fuseraft commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Gives fuseraft repl the full-screen experience anchor users have: on a terminal it now runs as a TUI (Terminal.Gui, ported from anchor), with the conversation above a prompt editor and a status line. --plain, piped stdin or stdout, and the --vscode bridge keep the line-based REPL.

What's in it

  • Streaming replies styled as Markdown, code blocks syntax-highlighted, tables fitted to the screen; each reply starts with a ◆ mark (in --plain too, replacing "fuseraft agent:")
  • The turn as it happens: ↳ tool args per call, a failing call's first line, subagent calls nested and tagged, ✎ path +n -m per changed file, thinking under ∴ folded when done
  • Ctrl+O opens any of the last 100 tool outputs, approval diffs or thoughts; Ctrl+F finds text; Ctrl+R searches past messages
  • Typing mid-turn: a message joins the running turn after its next tool results; commands wait; Ctrl+C drops what was sent
  • @path attachments, read through the session's own read_file / list_files (sandbox and deny rules apply), with path suggestions that skip git-ignored and deny-ruled files
  • Panel questions: approvals (Allow? [y]es [n]o [a]lways), ask_user, /mcp add, /provider setup
  • "Always" approvals, as in anchor: commands by program (saved for the directory in ~/.fuseraft/approvals.json, interpreters for the session only), file writes and FileSystem actions in the workspace for the session, Git and Http tools by name (saved). Never offered for sandbox-escape grants. /approvals lists them, /approvals clear forgets them; in --plain too
  • /copy [code], /theme, /dock top|bottom, and settings repl.theme, repl.colors, repl.dock, repl.bell; anchor's sample themes are in themes/
  • Status line colors the context % by how near auto-compaction is; the bell rings when a turn ends or a question waits while you're away
  • The transcript is printed to the terminal on exit, so it stays in the scrollback

How it fits in

  • AnsiConsole.Console is redirected into a Transcript that reads the ANSI back, so the existing ~500 Spectre call sites render unchanged. Console.Out stays with Terminal.Gui.
  • Prompts that read the console now go through ReplInput, which asks in the TUI's panel or on the plain terminal as before.
  • Mid-turn messages are added by ReplInterjectingChatClient, directly under the function-invocation loop, only after tool results, so a call is never separated from its result. It yields the message as a user-role update, so the turn's history records it in order.

Also fixed

A paragraph break armed by a tool round before any reply text was held until the reply's second chunk, splitting the reply ("Fixed\n\n.") in the history the plain REPL rendered and in the event log. There's a regression test for it.

Not included

  • In the TUI, !cmd gets no input, so interactive programs need --plain. The repl-tmux-driver skill now launches with --plain.

Testing

  • dotnet test: 3,179 passing. That includes anchor's Markdown, theme, transcript, diff and picker tests, ported, plus new ones for the renderer, mentions, file index, completion, interjection, "always" approvals and the paragraph-break fix.
  • Driven live in tmux against grok-4.5: streaming, cancelling, approvals, ✎ lines, Ctrl+O, a mid-turn message joining the turn, @ attachments, /theme, /dock, /copy. Also checked --plain and piped input, and a single-file self-contained publish.
  • Not tried on Windows. Under Git Bash/mintty stdin usually looks redirected, so it should fall back to the line-based REPL.

On a terminal, `fuseraft repl` now runs full screen, ported from anchor's
Terminal.Gui views: the conversation above a prompt editor and a status line.
--plain, piped stdin or stdout, and the --vscode bridge keep the line-based REPL.

- Spectre's output is redirected into a Transcript that reads the ANSI back,
  so every existing command looks as it did; questions go through ReplInput,
  which asks in the TUI's panel or on the plain terminal
- Replies stream in styled as Markdown, with code blocks highlighted by the
  TextMate grammars Terminal.Gui brings, and tables fitted to the screen
- Each tool call is one line (↳ read_file calc.py), a failing one adds its
  first line, subagent calls nest under it, and changed files show as
  ✎ path +added -removed; thinking dims under ∴ and folds when done
- Ctrl+O shows the last 100 tool outputs, approval diffs and thoughts in full;
  Ctrl+F finds text, Ctrl+R searches past messages, PgUp/PgDn scroll
- A message sent mid-turn joins the turn after its next tool results
  (ReplInterjectingChatClient, under the function-invocation loop); one the
  turn didn't read starts the next turn, and Ctrl+C drops it
- @path attaches files and directories through the session's own read_file
  and list_files tools, so the sandbox and deny rules apply; the prompt
  suggests paths, skipping git-ignored and deny-ruled files
- Approvals read Allow? [y]es [n]o and ignore keys typed as they appear;
  /mcp add, /provider setup and ask_user are asked in the panel too
- /copy [code], /theme (anchor's themes and theme files, samples in themes/)
  and /dock top|bottom; settings repl.theme, repl.colors, repl.dock, repl.bell
- The status line colors the context % by how near auto-compaction is, and
  the bell rings when a turn ends or a question waits while you're away
- Replies start with a ◆ mark in both REPLs instead of "fuseraft agent:"
- !cmd output goes to the transcript in the TUI; /paste points to pasting
  straight into the prompt; the transcript is printed on exit for scrollback

Fix: a paragraph break armed by a tool round before any reply text was held
until the reply's second chunk, splitting it ("Fixed\n\n.") in the history
the plain REPL rendered and the event log recorded.
Approvals offer [a]lways, as anchor's do, when they can say what it covers:

- A shell command: commands using the same programs, saved for the directory
  in ~/.fuseraft/approvals.json; a command using an interpreter or shell
  (python3, bash, eval, xargs, npx, ...) only for the session. Not offered
  when the command can't be named with confidence: an environment
  assignment first, a program run by path, a substitution in quotes
- write_file/patch_file inside the launch directory: all file writes there,
  this session only
- FileSystem actions inside the launch directory: that action, this session
- Git and Http actions: that tool, saved for the directory
- Never offered for, nor applied to, a sandbox-escape grant
  (IncludedRootsState.EscapeMarker); hard denials and the sandbox still apply

IHumanApprovalService gains always-aware overloads with yes/no defaults, so
the VS Code bridge, serve and non-interactive services are unchanged.
/approvals lists the answers and /approvals clear forgets them. The theme
directory now follows FUSERAFT_HOME too.
repl.maxTurnInputTokens summed each call's full InputTokenCount. Every call
resends the whole conversation, so on an OpenAI-compatible provider the
budget really capped a turn at budget / context-size calls (about 16 at
60k context) however little the cached prefix cost, and the cap shrank as
the session grew. The budget now counts only uncached input, normalizing
the two provider conventions: OpenAI-compatible input includes cache
reads, Anthropic's already excludes them and reports cache writes apart.
Every earlier tool call's large arguments were swapped for the "[ELIDED —
N chars, NOT the real value ...]" note, including the call just made. A
model rewriting the same file saw only placeholder-content write_file
calls in its history and copied one, which the write guard then blocked.
- The two newest tool-calling assistant messages are now left whole, so
  the latest write is a real example to follow
- The blocked-write error says the earlier calls' content is a display
  artifact and asks for the full content
- A superseded shell_run result says its output was dropped for the later
  identical run, rather than a bare "succeeded" the model read as a
  cached result
`fuseraft --resume` loaded a session saved in one directory while taking
the sandbox root, approvals, project instructions and memories from
wherever it was launched, so resuming from the wrong directory got the
model's file and shell calls on the session's own paths blocked.
- --resume now loads the snapshot first and moves into the session's
  directory before anything reads the current one, with a note saying so;
  if that directory is gone it warns and stays put
- /switch can't move a running REPL's sandbox, so it refuses a session
  from another directory and prints the command that resumes it there
@fuseraft
fuseraft merged commit aebafaa into main Oct 15, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant