Skip to content

Render ANSI escape codes instead of printing them as text - #108

Open
dchaudhari7177 wants to merge 1 commit into
simonw:mainfrom
dchaudhari7177:fix/render-ansi-escape-codes
Open

Render ANSI escape codes instead of printing them as text#108
dchaudhari7177 wants to merge 1 commit into
simonw:mainfrom
dchaudhari7177:fix/render-ansi-escape-codes

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Closes #95.

Before / after

The reporter's /context output arrives in the JSONL with its SGR sequences intact and was escaped as literal text:

[1mContext Usage[22m [38;2;136;136;136m⛁ [38;2;153;153;153m⛁ ⛁ [0m...

Now:

<span style="font-weight:bold">Context Usage</span> <span style="color:#888888"></span><span style="color:#999999">⛁ ⛁ </span>...

Colour is kept rather than stripped, because for /context specifically the colour is the information — the shaded blocks are the usage bar.

What's supported

render_ansi_text() handles bold, dim, italic, underline, strikethrough, their matching off-codes (22 turns off both bold and dim), the 16 named colours in both normal and bright form, foreground and background, the 256-entry xterm palette (38;5;N), and truecolor (38;2;R;G;B — the form /context emits).

Non-SGR sequences — cursor movement, erase-line, and the two-character ESC forms — are dropped, not rendered. They carry nothing to show, and leaving them would just be a different kind of noise.

On safety

The text is HTML-escaped first, always. The only markup trusted is what this function generates, and that is built from integers parsed out of the parameter list — a colour can only ever become #rrggbb. test_html_in_ansi_output_is_still_escaped pins that: \x1b[1m<script>alert(1)</script> still comes out as &lt;script&gt;.

This matters here because it is adjacent to #104 and my #107, which are about exactly this class of problem.

Scope

Applied to the four tool_result paths that previously escaped straight into a <pre> — including the two either side of a commit card, so output split by a commit card is handled consistently.

Text containing no \x1b takes an early return and is escaped exactly as before, so nothing that was not already broken changes. All 18 snapshots pass unmodified, which is the evidence for that.

Tests

Seven tests in TestAnsiEscapeCodes, written first and watched fail (5 of 7 failed before the change; the two that passed are the escaping and no-op controls, which is what they are for). They cover the literal-code regression from the issue, bold becoming markup rather than being discarded, truecolor, 256-colour, non-SGR removal, the escaping guarantee, and a plain-text control.

uv run pytest: 129 passed, 18 snapshots passed. uv run black . clean.

Also checked by hand against the exact byte sequence from the issue.

AI disclosure

Written with Claude Code (Claude Opus 5), following AGENTS.md: tests first, watched fail, then the implementation, then Black. I ran the suite and checked the rendered output against the issue's sample myself.

Terminal output reaches the transcript with its SGR sequences intact --
/context is the usual source -- and it was escaped as literal text, so the
page showed "\x1b[1mContext Usage\x1b[22m ...". The output most worth
reading was the least readable thing on it.

render_ansi_text() escapes the text first and then converts SGR sequences
into spans: bold, dim, italic, underline, strikethrough, the 16 named
colours, the 256-entry xterm palette, and truecolor. Only markup this
function builds itself is trusted, and it is built from parsed integers,
so HTML in the output is still escaped. Non-SGR sequences (cursor moves,
erase-line) carry nothing to show and are dropped rather than displayed.

Applied to the four tool_result paths that previously escaped straight
into a <pre>. Text with no escape sequences takes an early return and
renders byte for byte as before -- all 18 snapshots are unchanged.

Closes simonw#95
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.

Unparsed ANSI codes in transcript html

1 participant