fix(tui): preserve interrupted Mermaid diagrams (#45102)

This commit is contained in:
Kit Langton 2026-08-25 20:25:00 -04:00 committed by GitHub
parent 690ad8e8bd
commit 73d7b1d4c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 7 deletions

View file

@ -288,10 +288,30 @@ export function createMermaidCodeBlockRenderer(
} catch (error) {
if (error instanceof MermaidSyntaxError) {
const previous = key ? lastGood.get(key) : undefined
if (!previous || previous.kind !== kind) return undefined
const diagram = new StaticDiagramRenderable(ctx, previous)
claimLastGood(key!, previous, diagram, lastGood)
return diagram
if (previous?.kind === kind) {
const diagram = new StaticDiagramRenderable(ctx, previous)
claimLastGood(key!, previous, diagram, lastGood)
return diagram
}
const lines = token.text.split("\n")
if (error.lineNumber <= 2 || lines.slice(error.lineNumber).some((line) => line.trim())) return undefined
try {
const prepared = prepareDiagram(
kind,
lines.slice(0, error.lineNumber - 1).join("\n"),
options,
layoutMaxWidth,
)
if (!prepared.height) return undefined
const diagram = new StaticDiagramRenderable(ctx, prepared)
if (key) claimLastGood(key, prepared, diagram, lastGood)
return diagram
} catch (error) {
if (error instanceof MermaidSyntaxError || error instanceof DiagramCanvasSizeError) return undefined
throw error
}
}
if (error instanceof DiagramCanvasSizeError) return undefined
throw error

View file

@ -211,6 +211,32 @@ flowchart LR
expect(testRenderer.captureCharFrame()).toContain("Current")
})
test("renders the valid prefix of an interrupted Mermaid fence", async () => {
const testRenderer = await createTestRenderer({ width: 100, height: 18 })
renderer = testRenderer.renderer
const markdown = new MarkdownRenderable(renderer, {
id: "markdown-interrupted-mermaid",
content: `\`\`\`mermaid
flowchart TD
A[Resolve Project] --> B[Current directory]
B --> C[Search ancestors]
C --> D[Marker found]
C --> E[No marker found]
E --> G[Project root is`,
syntaxStyle,
internalBlockMode: "top-level",
renderNode: createMermaidMarkdownRenderer(renderer),
})
renderer.root.add(markdown)
await renderMarkdown(markdown, testRenderer.renderOnce)
const frame = testRenderer.captureCharFrame()
expect(frame).toContain("Resolve Project")
expect(frame).toContain("No marker found")
expect(frame).not.toContain("flowchart TD")
})
test("renders a Mermaid sequence fence inside MarkdownRenderable", async () => {
const testRenderer = await createTestRenderer({ width: 80, height: 14 })
renderer = testRenderer.renderer

View file

@ -1666,7 +1666,11 @@ function SessionPartView(props: { partRef: PartRef; message: (messageID: string)
{(item) => (
<Switch>
<Match when={item().type === "text"}>
<TextPart part={item() as SessionMessageAssistantText} last={false} />
<TextPart
part={item() as SessionMessageAssistantText}
message={message() as SessionMessageAssistant}
last={false}
/>
</Match>
<Match when={item().type === "reasoning"}>
<ReasoningPart
@ -2483,7 +2487,7 @@ function ReasoningHeader(props: {
)
}
function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
function TextPart(props: { last: boolean; part: SessionMessageAssistantText; message: SessionMessageAssistant }) {
const ctx = use()
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
@ -2493,7 +2497,7 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
<box paddingLeft={3} flexShrink={0}>
<markdown
syntaxStyle={syntax()}
streaming={true}
streaming={props.message.time.completed === undefined}
internalBlockMode="top-level"
content={props.part.text.trim()}
tableOptions={{ style: "grid", cellPaddingX: 1 }}