Skip to content

CLI: improve message formatting to match TUI and align with Claude Code/Codex - #1

Merged
afternoon merged 8 commits into
mainfrom
cursor/improve-cli-message-formatting-55ea
Jul 13, 2026
Merged

CLI: improve message formatting to match TUI and align with Claude Code/Codex#1
afternoon merged 8 commits into
mainfrom
cursor/improve-cli-message-formatting-55ea

Conversation

@afternoon

@afternoon afternoon commented Jul 13, 2026

Copy link
Copy Markdown
Owner

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 in goose-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:
    • A Role enum (Primary, Secondary, Muted, Accent, Success, Error) and role_style/apply to 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; Secondary differs from Muted/Primary by weight (bold), not hue, to keep the palette mostly grey.
    • Shared indentation constants (GUTTER, PARAM_INDENT) and a USER_PROMPT_GLYPH ().
    • ToolStatus + status_glyph/status_role/tool_result_status for 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.
    • 32 unit tests asserting the new spec (indentation, glyphs, role colors, tool header/footer shape, streaming line-boundary edge cases).
  • crates/goose-cli/src/session/output.rs — wired the new helpers in:
    • User messages are now echoed with the accent-colored glyph and gutter-aligned continuation lines (matching the live prompt), instead of plain unstyled text.
    • Normal assistant/model replies (and rendered markdown tables) are now indented under the same shared GUTTER, instead of sitting flush-left, so they line up visually with the user-message prompt glyph. print_markdown_raw now captures bat's rendered output via print_with_writer and indents it with formatting::indent_block before printing. An at_line_start flag 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.
    • Tool call headers are now a single indented line with a status glyph, replacing the heavy horizontal rule; tool responses get a ● done / ✗ error status footer derived from the tool result's actual is_error field (not just whether the call completed at the protocol level), suppressed for requests whose header was never shown (e.g. the internal load tool).
    • Parameters, tool output, and error lines use the shared indentation constants and Secondary/Muted/Error roles instead of hardcoded strings/colors.
  • crates/goose-cli/src/session/input.rs — the live rustyline prompt now uses USER_PROMPT_GLYPH instead of a literal string.
  • crates/goose-cli/src/session/completion.rs — implemented Highlighter::highlight_prompt so the live prompt is styled with the same Accent role 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:

  • Suppressed the tool-call status footer for requests whose header is never shown, avoiding an orphaned ● done line.
  • Routed failed tool responses through the same error-line formatter used elsewhere for a consistent shape.
  • Fixed format_user_message_plain to always emit the prompt glyph, even for an empty message.
  • Made Role::Secondary colorless (bold-only) instead of reusing Accent's cyan at a different weight, so Accent stays the palette's only splash of color.
  • Routed the remaining ad hoc style(...).dim() tool-graph step lines through the shared Role/indentation helpers.
  • Fixed a test that mutated console's global color-enabled state without restoring it.
  • Added exact-string and edge-case tests for the new footer/empty-message/suppression behavior.

As a follow-up, normal model replies were also brought under the shared gutter (previously the one thing still flush-left), with formatting::indent_block as a pure, independently tested helper.

Review round 2 (adversarial)

A second, adversarial subagent review specifically targeted the gutter-indent follow-up (the print_with_writer capture, streaming interaction, and table seams). It found two real regressions, which are fixed here:

  • Tool failures reported as success: render_tool_response only checked whether the outer Result was Ok, but a tool can complete the call successfully at the protocol level while still reporting failure via CallToolResult::is_error (e.g. a shell command that exits non-zero). The footer now uses the new formatting::tool_result_status(result.is_error) and shows ✗ error in that case.
  • Streamed replies got a spurious mid-sentence gutter: because each streaming chunk was rendered (and indented) independently, a chunk that only continued the previous chunk's still-open line (the common case for token-by-token streaming, since chunks rarely align to line boundaries) got its own leading gutter, visibly breaking sentences in two. Fixed by threading an explicit at_line_start flag through render_message_streaming/print_markdown/print_markdown_raw/print_table/flush_markdown_buffer, the same way thinking_header_shown is 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_response now ensures a fresh line before the footer.

Two other findings from that review were assessed and intentionally not changed: (1) bat's NoWrapping(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-space PARAM_INDENT tool-output indentation; (2) the thread-local set tracking suppressed tool headers is session-scoped and bounded by the number of internal load calls in a session, so a cancelled call leaving a stale entry isn't a practically meaningful leak.

docs/cli-formatting-plan.md was also updated to reconcile a stale non-goal note and an outdated Secondary color 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 the session::formatting unit 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-cli session formatting and the two docs files. The TUI (ui/desktop) and desktop app are untouched.

Open in Web Open in Cursor 

cursoragent and others added 3 commits July 13, 2026 16:01
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
afternoon marked this pull request as ready for review July 13, 2026 16:13
cursoragent and others added 5 commits July 13, 2026 16:17
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>
@afternoon
afternoon merged commit 1fa690b into main Jul 13, 2026
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.

2 participants