Skip to content

fix(sequential-thinking): render box correctly with ANSI codes and multi-line thoughts#4005

Open
abhicris wants to merge 1 commit intomodelcontextprotocol:mainfrom
abhicris:fix/sequentialthinking-box-render-ansi
Open

fix(sequential-thinking): render box correctly with ANSI codes and multi-line thoughts#4005
abhicris wants to merge 1 commit intomodelcontextprotocol:mainfrom
abhicris:fix/sequentialthinking-box-render-ansi

Conversation

@abhicris
Copy link
Copy Markdown

Description

SequentialThinkingServer.formatThought has two rendering bugs that break the ASCII box written to stderr when DISABLE_THOUGHT_LOGGING is not set:

  1. ANSI escape sequences inflate the border width. The border is sized as Math.max(header.length, thought.length) + 4 where header is the chalk-colored string. On any terminal that supports colour (the default on a TTY), chalk wraps the prefix in CSI escape sequences that add ~10 characters to .length without adding any visible columns. The border is then drawn wider than the visible header, and the right-hand floats free of the frame.

  2. Multi-line thoughts shatter the frame. thought.padEnd(border.length - 2) is applied to the entire string, so a thought that contains \n produces one long padded row with the trailing pushed onto a new line.

Reproduction (on current main)

Running the compiled server locally with FORCE_COLOR=3:

=== short, single-line ===
┌────────────────────────────┐
│ 💭 Thought 1/3 │                 ← right │ floats away from the frame
├────────────────────────────┤
│ short                      │
└────────────────────────────┘

=== multi-line thought ===
┌──────────────────────────────────────────────────┐
│ 💭 Thought 2/3 │                                 ← mis-aligned
├──────────────────────────────────────────────────┤
│ line one
line two is quite a bit longer
line 3   │  ← whole box collapses
└──────────────────────────────────────────────────┘

After the patch, both cases render as clean rectangles (visible widths verified equal across every line of the frame).

Server Details

  • Server: sequentialthinking
  • Changes to: internal renderer + tests (no tool-surface change, no schema change)

Motivation and Context

The formatted box is the server's primary diagnostic output. Broken rendering makes it hard to skim the log of a thinking run, and since it only manifests when colour is on, it doesn't show up in the existing test suite (which mocks chalk to return plain strings).

How Has This Been Tested?

  • npx vitest run — all existing 14 tests plus 7 new tests in __tests__/format.test.ts pass (21/21).
  • Manual smoke test against the compiled dist/lib.js with FORCE_COLOR=3 across (a) a basic thought, (b) a multi-line thought, and (c) a branch with a long context string. Every case produces a frame whose ANSI-stripped line widths are all equal.
  • npm run build passes with no new TS errors.

The new tests assert rectangularity directly: for each rendered block, every line's ANSI-stripped length must be identical. This catches both the CSI-inflation bug and the multi-line bug, and will catch any future regression that desynchronises header width and border width.

Breaking Changes

None — this is an internal renderer change. No tool schema, no API, no environment variable, and no on-the-wire behavior changes.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly (no README-visible behavior change)
  • I have tested this with an LLM client (stderr capture + manual run)
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling (no new error paths introduced)
  • I have documented all environment variables and configuration options (none added)

Additional context

The fix adds a small visibleWidth helper that strips CSI sequences before measuring, and splits the thought on \n so each line becomes its own framed row sized to max(headerWidth, widestThoughtLine). The ANSI regex is scoped to CSI (\x1b\[...[A-Za-z]), which is what chalk emits; OSC/DCS sequences are not relevant here.

No new runtime dependencies. The existing chalk mock in __tests__/lib.test.ts is intentionally left untouched so the legacy tests remain deterministic; the new format.test.ts uses real chalk behavior and asserts rectangularity, which is what actually matters on end-user terminals.


Part of open-source MCP work from kcolbchain / Abhishek Krishna.

…codes and for multi-line thoughts

The formatThought box renderer used `string.length` on the chalk-colored
header to size the border. When the terminal supports colour (the default
on any TTY), chalk wraps the prefix in CSI escape sequences, inflating
the raw character count by ~10 code points without adding any visible
columns. The border is then drawn wider than the visible header and the
right-hand `│` floats free of the frame.

The same function also did not handle thoughts that contain newlines:
`thought.padEnd(border.length - 2)` is applied to the whole string, so
a multi-line thought produces one very long padded row with the trailing
`│` pushed onto a new line — the box is shattered into fragments.

This change:

- strips CSI escape sequences when measuring the header width so the
  border matches the visible content
- splits multi-line thoughts and renders each line as its own framed
  row, sizing the box to the widest line

Adds `__tests__/format.test.ts` which asserts that every line of the
rendered frame has the same visible width (ANSI-stripped length) for
regular, revision, branch, multi-line, and width-dominated cases.

All existing tests continue to pass.
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