fix(tui): restore content lane rhythm

This commit is contained in:
Kit Langton
2026-08-11 13:19:55 -04:00
parent 055eb78f06
commit 59642a436f
5 changed files with 73 additions and 20 deletions
+16 -4
View File
@@ -1,5 +1,6 @@
import {
TextRenderable,
BoxRenderable,
RenderableEvents,
createMarkdownCodeBlockRenderer,
parseColor,
@@ -33,6 +34,7 @@ interface PreparedDiagram {
readonly source: string
readonly text: StyledText
readonly height: number
readonly width: number
}
export interface MermaidMarkdownRendererOptions {
@@ -55,16 +57,23 @@ function color(value: ColorInput | undefined): RGBA | undefined {
return value === undefined ? undefined : parseColor(value)
}
class StaticDiagramRenderable extends TextRenderable {
class StaticDiagramRenderable extends BoxRenderable {
constructor(ctx: RenderContext, prepared: PreparedDiagram) {
super(ctx, {
content: prepared.text,
width: "100%",
alignItems: "center",
flexShrink: 0,
marginTop: 1,
})
const diagram = new TextRenderable(ctx, {
content: prepared.text,
width: prepared.width,
maxWidth: "100%",
height: prepared.height,
wrapMode: "none",
selectable: false,
marginTop: 1,
})
this.add(diagram)
let dragX: number | undefined
this.onMouseDown = (event: MouseEvent) => {
if (event.button !== 0) return
@@ -79,7 +88,7 @@ class StaticDiagramRenderable extends TextRenderable {
if (dragX === undefined) return
const dx = event.x - dragX
dragX = event.x
if (dx) this.scrollX -= dx
if (dx) diagram.scrollX -= dx
}
this.onMouseDragEnd = (event: MouseEvent) => {
dragX = undefined
@@ -121,6 +130,7 @@ function prepareDiagram(kind: DiagramKind, source: string, options: MermaidMarkd
}),
),
height: size.height,
width: size.width,
}
}
case "sequence": {
@@ -144,6 +154,7 @@ function prepareDiagram(kind: DiagramKind, source: string, options: MermaidMarkd
}),
),
height: size.height,
width: size.width,
}
}
case "state": {
@@ -168,6 +179,7 @@ function prepareDiagram(kind: DiagramKind, source: string, options: MermaidMarkd
}),
),
height: size.height,
width: size.width,
}
}
}
+31 -4
View File
@@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, expect, test } from "bun:test"
import { mkdir } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { CodeRenderable, MarkdownRenderable, RGBA, SyntaxStyle, TreeSitterClient } from "@opentui/core"
import { CodeRenderable, MarkdownRenderable, RGBA, SyntaxStyle, TextRenderable, TreeSitterClient } from "@opentui/core"
import { createTestRenderer } from "@opentui/core/testing"
import { createMermaidMarkdownRenderer } from "../markdown.js"
@@ -78,6 +78,31 @@ flowchart LR
expect(markdown.getChildren()[0]?.marginTop).toBe(1)
})
test("centers a Mermaid diagram narrower than its canvas", async () => {
const testRenderer = await createTestRenderer({ width: 80, height: 14 })
renderer = testRenderer.renderer
const markdown = new MarkdownRenderable(renderer, {
id: "markdown-centered-mermaid",
content: `\`\`\`mermaid
flowchart LR
A[Start] --> B[Done]
\`\`\``,
syntaxStyle,
treeSitterClient,
renderNode: createMermaidMarkdownRenderer(renderer),
})
renderer.root.add(markdown)
await renderMarkdown(markdown, testRenderer.renderOnce)
const line = testRenderer
.captureCharFrame()
.split("\n")
.find((value) => value.includes("Start"))
if (!line) throw new Error("Expected the rendered diagram")
expect(line.indexOf("Start")).toBeGreaterThan(10)
})
test("recognizes normalized Mermaid fence info strings", async () => {
const testRenderer = await createTestRenderer({ width: 80, height: 14 })
renderer = testRenderer.renderer
@@ -235,17 +260,19 @@ sequenceDiagram
renderer.root.add(markdown)
await renderMarkdown(markdown, testRenderer.renderOnce)
const diagram = markdown.getChildren()[0] as CodeRenderable
const wrapper = markdown.getChildren()[0]
if (!wrapper) throw new Error("Expected the rendered diagram wrapper")
const diagram = wrapper.getChildren()[0] as TextRenderable
expect(diagram.scrollWidth).toBeGreaterThan(diagram.width)
expect(diagram.scrollX).toBe(0)
await testRenderer.mockMouse.drag(diagram.x + 20, diagram.y + 2, diagram.x + 5, diagram.y + 2)
await testRenderer.mockMouse.drag(wrapper.x + 20, wrapper.y + 2, wrapper.x + 5, wrapper.y + 2)
await testRenderer.renderOnce()
expect(diagram.scrollX).toBeGreaterThan(0)
expect(diagram.hasSelection()).toBe(false)
diagram.scrollX = 0
await testRenderer.mockMouse.scroll(diagram.x + 20, diagram.y + 2, "right")
await testRenderer.mockMouse.scroll(wrapper.x + 20, wrapper.y + 2, "right")
await testRenderer.renderOnce()
expect(diagram.scrollX).toBeGreaterThan(0)
})
+13 -11
View File
@@ -108,7 +108,7 @@ import { createSingleFlight } from "../../util/single-flight"
import type { SessionPending } from "@opencode-ai/schema/session-pending"
import { generateThinkingSyntax } from "./thinking-syntax"
import { createDelayedPresence } from "../../util/delayed-presence"
import { markdownLanes } from "./markdown-lanes"
import { markdownLaneMarginTop, markdownLanes } from "./markdown-lanes"
addDefaultParsers(parsers.parsers)
@@ -2244,7 +2244,7 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
return (
<Show when={props.part.text.trim()}>
<Index each={segments()}>
{(segment) => {
{(segment, index) => {
const content = (
<box paddingLeft={3} flexShrink={0}>
<markdown
@@ -2261,15 +2261,17 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
</box>
)
return (
<Switch>
<Match when={segment().width === "wide"}>{content}</Match>
<Match when={segment().width === "code"}>
<SessionContentLane width="code">{content}</SessionContentLane>
</Match>
<Match when={segment().width === "readable"}>
<SessionContentLane width="readable">{content}</SessionContentLane>
</Match>
</Switch>
<box width="100%" marginTop={markdownLaneMarginTop(index, segment().width)} flexShrink={0}>
<Switch>
<Match when={segment().width === "wide"}>{content}</Match>
<Match when={segment().width === "code"}>
<SessionContentLane width="code">{content}</SessionContentLane>
</Match>
<Match when={segment().width === "readable"}>
<SessionContentLane width="readable">{content}</SessionContentLane>
</Match>
</Switch>
</box>
)
}}
</Index>
@@ -38,3 +38,8 @@ export function markdownLanes(content: string): MarkdownLane[] {
return result
}
export function markdownLaneMarginTop(index: number, width: MarkdownLane["width"]) {
if (index === 0 || width === "wide") return 0
return 1
}
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test"
import { markdownLanes } from "../../../src/routes/session/markdown-lanes"
import { markdownLaneMarginTop, markdownLanes } from "../../../src/routes/session/markdown-lanes"
test("keeps prose in the readable lane", () => {
expect(markdownLanes("Before\n\nAfter")).toEqual([{ content: "Before\n\nAfter", width: "readable" }])
@@ -47,3 +47,10 @@ test("gives ordinary fenced code an intermediate lane", () => {
{ content: "```ts\nexport const value = true\n```", width: "code" },
])
})
test("restores spacing between separately rendered blocks", () => {
expect(markdownLaneMarginTop(0, "readable")).toBe(0)
expect(markdownLaneMarginTop(1, "code")).toBe(1)
expect(markdownLaneMarginTop(2, "readable")).toBe(1)
expect(markdownLaneMarginTop(1, "wide")).toBe(0)
})