fix(tui): optimize Mermaid terminal rendering (#42595)

This commit is contained in:
Kit Langton 2026-08-14 11:10:32 -04:00 committed by GitHub
parent f7d21babb6
commit 1cf97fac5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 60 additions and 2 deletions

View file

@ -44,6 +44,7 @@ interface PreparedDiagram {
}
export interface MermaidMarkdownRendererOptions {
/** Use terminal-optimized diagram spacing. Defaults to true. */
compact?: boolean
/** Fold horizontal flowcharts that exceed this width. Defaults to 120 columns. */
layoutMaxWidth?: number
@ -118,10 +119,11 @@ function prepareDiagram(
layoutMaxWidth: number,
): PreparedDiagram {
const colors = options.colors ?? {}
const compact = options.compact ?? true
switch (kind) {
case "flowchart": {
const grid = drawFlowchartDiagramGrid(parseMermaidFlowchartDiagram(source), {
compact: options.compact,
compact,
layoutMaxWidth,
})
const size = grid.getTextSize({ trimTop: true, trimBottom: true })
@ -161,7 +163,7 @@ function prepareDiagram(
}
}
case "sequence": {
const grid = drawSequenceDiagramGrid(parseMermaidSequenceDiagram(source), { compact: options.compact })
const grid = drawSequenceDiagramGrid(parseMermaidSequenceDiagram(source), { compact })
const size = grid.getTextSize()
return {
kind,

View file

@ -243,6 +243,28 @@ describe("createStateTransitionRenderPlans", () => {
).toBe(false)
}
})
test("keeps routes to offset end markers continuous", () => {
const diagram = prepareVisibleStateDiagram(
parseMermaidStateDiagram(`stateDiagram-v2
[*] --> Pending
Pending --> Running
Running --> Idle
Running --> Interrupted
Interrupted --> [*]`),
)
const layout = createStateDiagramLayout(diagram, { minStateGap: 12 })
const plan = createStateTransitionRenderPlans(diagram, layout.bounds, 30).find(
(plan) => plan.route.transition.to === "__end",
)!
expect(
plan.path.slice(1).every(([x, y], index) => {
const previous = plan.path[index]!
return Math.abs(x - previous[0]) + Math.abs(y - previous[1]) === 1
}),
).toBe(true)
})
})
describe("createStateTransitionJunctionPlans", () => {

View file

@ -216,6 +216,11 @@ function verticalCorridorCrossesUnrelatedState(
})
}
function hasVerticalCorridor(from: BoxBounds, to: BoxBounds): boolean {
if (from.centerY < to.centerY) return from.top + from.height <= to.top - 1
return from.top - 1 >= to.top + to.height
}
function horizontalCorridorCrossesUnrelatedState(
diagram: StateVisibleDiagram,
transition: StateVisibleTransition,
@ -388,6 +393,9 @@ export function createStateTransitionRoutePlans(
}
return [{ ...base, kind: "horizontal-forward", leftToRight: from.centerX <= to.centerX }]
}
if (!hasVerticalCorridor(from, to)) {
return [{ ...base, kind: "side-parallel", railX: allocateSideRail(transition.label) }]
}
if (from.centerX !== to.centerX) {
return [{ ...base, kind: "vertical-elbow", hasReverse: false, offsetConnector: false }]
}
@ -395,6 +403,9 @@ export function createStateTransitionRoutePlans(
}
if (from.centerY !== to.centerY) {
if (!hasVerticalCorridor(from, to)) {
return [{ ...base, kind: "side-parallel", railX: allocateSideRail(transition.label) }]
}
if (from.centerY > to.centerY && feedback)
return [
{

View file

@ -78,6 +78,29 @@ flowchart LR
expect(markdown.getChildren()[0]?.marginTop).toBe(1)
})
test("uses compact terminal spacing for Mermaid diagrams by default", async () => {
const testRenderer = await createTestRenderer({ width: 80, height: 40 })
renderer = testRenderer.renderer
const markdown = new MarkdownRenderable(renderer, {
id: "markdown-compact-mermaid",
content: `\`\`\`mermaid
flowchart TD
repo[Organization profile repo] --> build[Build]
build --> worker[Isolated organization Worker]
worker --> sessions[SessionDOs]
sessions --> opencode[OpenCode + native plugins]
opencode --> modal[Modal workspaces]
\`\`\``,
syntaxStyle,
renderNode: createMermaidMarkdownRenderer(renderer),
})
renderer.root.add(markdown)
await renderMarkdown(markdown, testRenderer.renderOnce)
expect(markdown.getChildren()[0]?.height).toBe(28)
})
test("recognizes normalized Mermaid fence info strings", async () => {
const testRenderer = await createTestRenderer({ width: 80, height: 14 })
renderer = testRenderer.renderer