CLI: improve message formatting to match TUI and align with Claude Code/Codex - #1
Merged
Merged
Conversation
Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
Introduce crates/goose-cli/src/session/formatting.rs with pure, testable helpers for a consistent, mostly-grey semantic palette (Role::Primary/Secondary/Muted/Accent/Success/Error), shared indentation constants, and status glyphs modeled on the TUI and tools like Claude Code / Codex. Wire these helpers into output.rs, input.rs, and completion.rs: - User messages are echoed with an accent-colored prompt glyph and gutter-aligned continuation lines, matching the live input prompt. - Tool call headers use a single indented line with a status glyph instead of a heavy horizontal rule, and get a done/error status footer after the tool response. - Parameters, tool output, and error lines use consistent indentation and muted/secondary roles instead of ad-hoc styling. Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
afternoon
marked this pull request as ready for review
July 13, 2026 16:13
Fix issues found by an independent GPT-5 review of the formatting change: - Suppress the tool-call status footer for requests whose header was never shown (e.g. the internal `load` tool, or an unparseable tool call), so no orphaned '● done' line appears with nothing above it. - Route failed tool responses through the same error-line formatter used elsewhere, instead of a bespoke inline string. - format_user_message_plain now always emits the prompt glyph, even for an empty message, instead of silently rendering nothing. - Make Role::Secondary colorless (bold-only) instead of reusing Accent's cyan at a different weight, so Accent stays the only splash of color in an otherwise grey/dim palette. - Route the execute/subagent tool-graph step lines through formatting::apply(Role::Muted, ..)/PARAM_INDENT instead of ad hoc style(...).dim() calls, for one source of truth on tool-call styling. - Fix a test that mutated console's global color-enabled state without restoring it, which could leak across tests running in the same process. - Add tests: exact-string assertions for the tool status footer, an empty-message case, a Running-status no-footer case, a Secondary-is- colorless case, and coverage for the new footer-suppression bookkeeping. Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
Assistant text/markdown replies previously rendered flush-left via bat, while the user's own echoed messages and all the tool-call chrome sat under the shared 2-space GUTTER. Add formatting::indent_block, a pure helper that indents every non-empty line of an already-rendered block (preserving blank lines and trailing newlines exactly), and use it in print_markdown_raw by capturing bat's output via print_with_writer instead of letting it write straight to stdout. This makes normal model output line up with the user-message prompt glyph and with the rest of the transcript's left margin, instead of being the one thing still flush-left. Add unit tests locking in the indent_block spec (single/multi-line, blank-line handling, trailing newline, empty input), plus an ignored manual_visual_smoke_check test for eyeballing the combined user-message + assistant-reply rendering. Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
- tool_result_status(is_error) maps a CallToolResult's is_error field to a ToolStatus, so a protocol-level Ok(..) that still failed (e.g. a shell command with a non-zero exit code) isn't reported as success. - indent_block now takes an at_line_start flag, and a new ends_at_line_start helper reports whether rendered text leaves the cursor at a fresh line. Streamed replies are rendered in separate chunks that don't necessarily land on line boundaries, so a chunk that only continues the previous chunk's still-open line must not get its own leading gutter. Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
Addresses issues found in an adversarial review of the CLI formatting
changes:
- render_tool_response now derives the status footer from
formatting::tool_result_status(result.is_error) instead of always
showing '\u{25cf} done' for Ok(..), which previously mislabeled failed
tool calls (e.g. shell commands with a non-zero exit code) as
successful.
- render_message_streaming, print_markdown, print_markdown_raw,
print_table, and flush_markdown_buffer now thread an at_line_start
flag through every call, mirroring the existing thinking_header_shown
pattern. Without this, each independently-rendered streaming chunk
re-indented itself from the start of its own output regardless of
whether the cursor was already mid-line, inserting a spurious gutter
in the middle of a sentence whenever a chunk boundary didn't land on a
newline (the common case for token-by-token streaming).
- render_tool_response no longer glues the status footer onto
un-terminated tool output in non-terminal mode (e.g. piped/redirected
stdout), where tool output is printed verbatim without a guaranteed
trailing newline.
Added an ignored manual smoke test demonstrating the streaming fix.
Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
…ixes Documents the goal-4 follow-up (indenting normal model replies) and its implication for the 'no change to bat' non-goal, and fixes a stale note that still described Secondary as colored. Co-authored-by: Ben Godfrey <afternoon@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Improves the visual formatting of CLI session output so it's closer to the TUI and to tools like Claude Code and Codex: a consistent, mostly-grey semantic palette, clearer indentation, and less visual noise around tool calls.
This is a pure formatting/visual change. It does not rebuild CLI output, add a TUI, or touch any other interface (e.g. desktop).
What changed
docs/cli-formatting-research.md— report on how formatting currently works ingoose-cli(console/bat/comfy-table/cliclack) and in the TUI (ui/text,colors.tsx,marked-terminal), plus a summary of Claude Code/Codex conventions, with concrete recommendations.docs/cli-formatting-plan.md— the implementation plan derived from that research.crates/goose-cli/src/session/formatting.rs(new) — pure, unit-tested helpers with no I/O:Roleenum (Primary,Secondary,Muted,Accent,Success,Error) androle_style/applyto centralize color/dim/bold decisions instead of styling ad hoc at each call site.Accent(cyan) is the only role carrying a non-semantic color;Secondarydiffers fromMuted/Primaryby weight (bold), not hue, to keep the palette mostly grey.GUTTER,PARAM_INDENT) and aUSER_PROMPT_GLYPH(❯).ToolStatus+status_glyph/status_role/tool_result_statusfor consistent running/success/error glyphs.format_user_message_plain,format_tool_header_plain,format_tool_status_line_plain,format_error_line_plain,indent_block/ends_at_line_start— plain-text formatting logic that's fully covered by unit tests, separate from the styling/printing.crates/goose-cli/src/session/output.rs— wired the new helpers in:❯glyph and gutter-aligned continuation lines (matching the live prompt), instead of plain unstyled text.GUTTER, instead of sitting flush-left, so they line up visually with the user-message prompt glyph.print_markdown_rawnow capturesbat's rendered output viaprint_with_writerand indents it withformatting::indent_blockbefore printing. Anat_line_startflag is threaded through the whole streaming render path so a chunk that only continues the previous chunk's still-open line doesn't get re-indented mid-sentence.● done/✗ errorstatus footer derived from the tool result's actualis_errorfield (not just whether the call completed at the protocol level), suppressed for requests whose header was never shown (e.g. the internalloadtool).Secondary/Muted/Errorroles instead of hardcoded strings/colors.crates/goose-cli/src/session/input.rs— the live rustyline prompt now usesUSER_PROMPT_GLYPHinstead of a literal string.crates/goose-cli/src/session/completion.rs— implementedHighlighter::highlight_promptso the live prompt is styled with the sameAccentrole as the echoed prompt glyph, keeping live input and history visually consistent.Review round 1
Reviewed by a subagent on a different model (GPT-5.5) focused on correctness, palette fidelity to the TUI, scope, and test quality. Findings addressed:
● doneline.format_user_message_plainto always emit the prompt glyph, even for an empty message.Role::Secondarycolorless (bold-only) instead of reusingAccent's cyan at a different weight, soAccentstays the palette's only splash of color.style(...).dim()tool-graph step lines through the sharedRole/indentation helpers.console's global color-enabled state without restoring it.As a follow-up, normal model replies were also brought under the shared gutter (previously the one thing still flush-left), with
formatting::indent_blockas a pure, independently tested helper.Review round 2 (adversarial)
A second, adversarial subagent review specifically targeted the gutter-indent follow-up (the
print_with_writercapture, streaming interaction, and table seams). It found two real regressions, which are fixed here:render_tool_responseonly checked whether the outerResultwasOk, but a tool can complete the call successfully at the protocol level while still reporting failure viaCallToolResult::is_error(e.g. a shell command that exits non-zero). The footer now uses the newformatting::tool_result_status(result.is_error)and shows✗ errorin that case.at_line_startflag throughrender_message_streaming/print_markdown/print_markdown_raw/print_table/flush_markdown_buffer, the same waythinking_header_shownis already threaded.Also fixed as a smaller related issue found in the same pass: in non-terminal mode (piped/redirected stdout), tool output is printed verbatim without a guaranteed trailing newline, which could glue the status footer onto the same line as un-terminated output;
render_tool_responsenow ensures a fresh line before the footer.Two other findings from that review were assessed and intentionally not changed: (1)
bat'sNoWrapping(true)mode means it was never fitting content to the exact terminal width to begin with, so the extra 2-column gutter carries the same negligible overflow risk already accepted for the existing 4-spacePARAM_INDENTtool-output indentation; (2) the thread-local set tracking suppressed tool headers is session-scoped and bounded by the number of internalloadcalls in a session, so a cancelled call leaving a stale entry isn't a practically meaningful leak.docs/cli-formatting-plan.mdwas also updated to reconcile a stale non-goal note and an outdatedSecondarycolor description with the shipped gutter-indent follow-up.Testing
cargo test -p goose-cli— all tests pass (2 intentionally#[ignore]d manual visual smoke checks), including thesession::formattingunit tests.cargo clippy -p goose-cli --lib --tests -- -D warnings— clean.cargo fmt --check— clean.cargo build -p goose-cli— builds cleanly.Scope
No changes outside of
goose-clisession formatting and the two docs files. The TUI (ui/desktop) and desktop app are untouched.