Skip to content

fix: render Markdown in conversation viewer - #211

Closed
zeroknots wants to merge 1 commit into
tintinweb:masterfrom
zeroknots:fix/render-viewer-markdown
Closed

fix: render Markdown in conversation viewer#211
zeroknots wants to merge 1 commit into
tintinweb:masterfrom
zeroknots:fix/render-viewer-markdown

Conversation

@zeroknots

Copy link
Copy Markdown
Contributor

Summary

Render assistant messages and tool results in the FleetView conversation overlay with Pi's Markdown component. This fixes raw Markdown output from tools such as context-mode's ctx_execute and removes the fixed 500-character result truncation so the existing scrollable viewer can show the complete result.

Closes #210.

Changes

  • Add viewer-local Markdown styling based on the overlay theme.
  • Render assistant text and tool results through Pi's Markdown component.
  • Preserve width clamping and existing conversation chrome.
  • Add regression coverage for headings, lists, inline emphasis, and tool results beyond 500 characters.

Testing

  • npm run lint
  • npm run typecheck
  • npm run test — 47 files passed, 1 skipped; 822 tests passed, 5 skipped
  • npm run build

@tintinweb

Copy link
Copy Markdown
Owner

related #85 #173

@tintinweb tintinweb added <📍> feature New feature or request labels Aug 20, 2026
tintinweb added a commit that referenced this pull request Aug 24, 2026
…eable

The viewer wrapped every line with wrapTextWithAnsi, so assistant markdown
showed as raw fences and `#` markers, and tool results were cut at 500
characters (#210).

Assistant text now renders as markdown; tool results do not, by default. A
markdown pass over a tool result is lossy in ways that read as the tool
misbehaving: `# section` in a shell script loses its `#`, `3) 7) 9)` comes back
renumbered `3. 4. 5.`, a `---` line is swallowed as a setext heading, and
indented output is re-fenced. Assistant text is authored as markdown; a tool
result is arbitrary bytes.

viewerMarkdown (off | assistant | all) picks the scope, and `m` in the viewer
cycles it and persists the choice. That escape hatch is what makes rendering
safe to default on at all.

The 500-char cap is raised to 16k rather than removed, and now covers
bashExecution too. It bounds render cost, not just display: buildContentLines
runs on every render and on every scroll key, where an uncapped 200KB result
costs ~19ms per keystroke to re-parse against ~0.04ms for a capped wrap. One
Markdown per message in a WeakMap, so the component's own cache applies.

Code fences use pi's getMarkdownTheme() for syntax highlighting, probed rather
than try/caught around the call: its functions read the global theme lazily, so
an uninitialized theme throws inside render() and would take the overlay with
it. Falls back to a theme built from the viewer's own Theme.

Supersedes #211 — thanks @zeroknots.

Co-authored-by: zeroknots <zeroknots@protonmail.com>
tintinweb added a commit that referenced this pull request Aug 24, 2026
…eable (#259)

* fix(ui): render markdown in the conversation viewer, scoped and toggleable

The viewer wrapped every line with wrapTextWithAnsi, so assistant markdown
showed as raw fences and `#` markers, and tool results were cut at 500
characters (#210).

Assistant text now renders as markdown; tool results do not, by default. A
markdown pass over a tool result is lossy in ways that read as the tool
misbehaving: `# section` in a shell script loses its `#`, `3) 7) 9)` comes back
renumbered `3. 4. 5.`, a `---` line is swallowed as a setext heading, and
indented output is re-fenced. Assistant text is authored as markdown; a tool
result is arbitrary bytes.

viewerMarkdown (off | assistant | all) picks the scope, and `m` in the viewer
cycles it and persists the choice. That escape hatch is what makes rendering
safe to default on at all.

The 500-char cap is raised to 16k rather than removed, and now covers
bashExecution too. It bounds render cost, not just display: buildContentLines
runs on every render and on every scroll key, where an uncapped 200KB result
costs ~19ms per keystroke to re-parse against ~0.04ms for a capped wrap. One
Markdown per message in a WeakMap, so the component's own cache applies.

Code fences use pi's getMarkdownTheme() for syntax highlighting, probed rather
than try/caught around the call: its functions read the global theme lazily, so
an uninitialized theme throws inside render() and would take the overlay with
it. Falls back to a theme built from the viewer's own Theme.

Supersedes #211 — thanks @zeroknots.

Co-authored-by: zeroknots <zeroknots@protonmail.com>

* fix(ui): survive a Markdown render that throws, and warn when `m` can't persist

Fuzzing the viewer's Markdown path with hostile tool output — null bytes, lone
surrogates, control characters, unterminated fences and tables, mid-word ANSI,
ZWJ emoji, RTL marks — found one input that throws: ~54 nested blockquotes
overflow pi-tui's recursive renderer with a RangeError. buildContentLines runs
inside render() and again on every scroll key, so that took the overlay down for
content the literal path displays fine.

markdownLines now degrades to literal wrapping on any throw and remembers the
failure, which would otherwise repeat on every render and every keystroke; the
flag clears when the message's text changes. Catching rather than special-casing
blockquote depth, because fuzzing cannot prove that is the only such input.

The three literal-wrapping paths — `off` mode, non-`all` tool results and bash
output — collapse into the rawLines() helper the fallback returns.

Separately, persistSettings discarded saveAndEmitChanged's return, so a failed
write from the `m` key was silent: the mode applied for the session while
looking persisted. It now warns on failure and stays quiet on success, matching
every other settings path.

Tests: the fallback keeps content visible instead of throwing; results stay dim
on the literal path, untested before rawLines took ownership of it; and a tool
result growing past the cap keeps its held-back count moving while the cached
parse is correctly reused.

* update readme/doc

* fix fialing test

* fix(agents): read agent files that begin with a UTF-8 BOM

Editors across the Windows/CJK world write UTF-8 with a BOM, and pi's parser
did not look past one before 0.84.3: the fence missed, frontmatter came back
empty, and the whole file became the body. Such an agent lost every field —
and `tools: none` going missing left it holding bash, edit and write, a wider
grant than its author wrote. `/agents` then refused to toggle it, calling a
file with frontmatter frontmatter-less.

Normalised at the read boundary (parseAgentFrontmatter), the one place agent
files are read, rather than detected per pi version — one behaviour across the
whole supported peer range. The write side looks past the BOM too and leaves it
in the file, since it belongs to the encoding, not the block.

Tests drive the real loader over a real BOM'd Chinese agent file, including the
BOM+CRLF combination a Windows editor actually produces.

* fix test

---------

Co-authored-by: zeroknots <zeroknots@protonmail.com>
@tintinweb

Copy link
Copy Markdown
Owner

superseded by #259

@tintinweb tintinweb closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

<📍> feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Render Markdown in the conversation viewer

2 participants