From 1c53c90d4e459c6c30f89eac4ab135238c0d0fa9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 11 Aug 2026 14:30:59 -0400 Subject: [PATCH] fix(tui): align content lanes to shared spine --- packages/merman/src/markdown.ts | 2 +- packages/merman/src/test/markdown.test.ts | 4 +-- packages/tui/src/routes/session/index.tsx | 43 ++++++++++++++++------- packages/tui/src/ui/layout.ts | 10 ++++++ packages/tui/test/ui/layout.test.ts | 14 +++++++- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/merman/src/markdown.ts b/packages/merman/src/markdown.ts index e9e51e8c77..cf191366b7 100644 --- a/packages/merman/src/markdown.ts +++ b/packages/merman/src/markdown.ts @@ -61,7 +61,7 @@ class StaticDiagramRenderable extends BoxRenderable { constructor(ctx: RenderContext, prepared: PreparedDiagram) { super(ctx, { width: "100%", - alignItems: "center", + alignItems: "flex-start", flexShrink: 0, marginTop: 1, }) diff --git a/packages/merman/src/test/markdown.test.ts b/packages/merman/src/test/markdown.test.ts index b10a366614..282ff140fb 100644 --- a/packages/merman/src/test/markdown.test.ts +++ b/packages/merman/src/test/markdown.test.ts @@ -78,7 +78,7 @@ flowchart LR expect(markdown.getChildren()[0]?.marginTop).toBe(1) }) -test("centers a Mermaid diagram narrower than its canvas", async () => { +test("leaves Mermaid alignment to its containing layout", async () => { const testRenderer = await createTestRenderer({ width: 80, height: 14 }) renderer = testRenderer.renderer const markdown = new MarkdownRenderable(renderer, { @@ -100,7 +100,7 @@ flowchart LR .split("\n") .find((value) => value.includes("Start")) if (!line) throw new Error("Expected the rendered diagram") - expect(line.indexOf("Start")).toBeGreaterThan(10) + expect(line.indexOf("Start")).toBeLessThan(10) }) test("recognizes normalized Mermaid fence info strings", async () => { diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 75a2d925fa..7a58e536df 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -68,7 +68,7 @@ import { errorMessage } from "../../util/error" import { useToast } from "../../ui/toast" import stripAnsi from "strip-ansi" import { usePromptRef } from "../../context/prompt" -import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../ui/layout" +import { sessionLaneLayout, sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../ui/layout" import { projectedPromptInput } from "../../prompt/codec" import { deduplicateVisibleImages } from "../../prompt/attachment" import { useEpilogue } from "../../context/epilogue" @@ -115,9 +115,6 @@ addDefaultParsers(parsers.parsers) // Exclude temporary bottom space when measuring the real transcript height. const NAVIGATION_SLACK_ID = "session-navigation-slack" const BACKGROUND_TOOL_HINT_DELAY = 1_000 -// The assistant inset leaves 85 columns for code, matching common documentation guidance. -const SESSION_TECHNICAL_LANE_WIDTH = 88 - // Tail-first transcript mounting: rows mounted with the session, then backfill cadence. // The tail comfortably overfills a tall viewport; backfill drains a 200-message transcript // in a few hundred milliseconds without a perceptible pause. @@ -1238,17 +1235,35 @@ function SessionRowView(props: SessionRowViewProps) { function SessionContentLane(props: { children: JSX.Element; width: "readable" | "technical" }) { const ctx = use() - const maxWidth = createMemo(() => { - const readable = ctx.config.session?.max_width ?? "auto" - if (readable === "auto" || props.width === "readable") return readable - return Math.max(readable, SESSION_TECHNICAL_LANE_WIDTH) + const readable = () => ctx.config.session?.max_width ?? "auto" + const layout = createMemo(() => { + const width = readable() + return width === "auto" ? undefined : sessionLaneLayout(ctx.width, width) }) return ( - - - - {props.children} + + {(value) => ( + + + {props.children} + + )} + + ) +} + +function SessionBreakoutLane(props: { children: JSX.Element }) { + const ctx = use() + const readable = () => ctx.config.session?.max_width ?? "auto" + const inset = createMemo(() => { + const width = readable() + return width === "auto" ? undefined : sessionLaneLayout(ctx.width, width).inset + }) + return ( + + + {props.children} ) @@ -2306,7 +2321,9 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) { return ( - {content} + + {content} + {content} diff --git a/packages/tui/src/ui/layout.ts b/packages/tui/src/ui/layout.ts index 2fa311619f..d9c94830c3 100644 --- a/packages/tui/src/ui/layout.ts +++ b/packages/tui/src/ui/layout.ts @@ -1,6 +1,16 @@ export const SESSION_SIDEBAR_WIDTH = 42 +export const SESSION_TECHNICAL_LANE_WIDTH = 88 const SESSION_CONTENT_MIN_WIDTH = 44 export function sessionTabsFitVertically(total: number) { return total >= SESSION_SIDEBAR_WIDTH + SESSION_CONTENT_MIN_WIDTH } + +export function sessionLaneLayout(available: number, readable: number) { + const technical = Math.min(available, Math.max(readable, SESSION_TECHNICAL_LANE_WIDTH)) + return { + inset: Math.max(0, Math.floor((available - technical) / 2)), + readable: Math.min(readable, technical), + technical, + } +} diff --git a/packages/tui/test/ui/layout.test.ts b/packages/tui/test/ui/layout.test.ts index 53c9a82479..530975a6c0 100644 --- a/packages/tui/test/ui/layout.test.ts +++ b/packages/tui/test/ui/layout.test.ts @@ -1,8 +1,20 @@ import { expect, test } from "bun:test" -import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../src/ui/layout" +import { + sessionLaneLayout, + sessionTabsFitVertically, + SESSION_SIDEBAR_WIDTH, + SESSION_TECHNICAL_LANE_WIDTH, +} from "../../src/ui/layout" test("vertical tabs match the session sidebar and preserve compact content width", () => { expect(SESSION_SIDEBAR_WIDTH).toBe(42) expect(sessionTabsFitVertically(86)).toBe(true) expect(sessionTabsFitVertically(85)).toBe(false) }) + +test("session lanes share one leading edge", () => { + expect(SESSION_TECHNICAL_LANE_WIDTH).toBe(88) + expect(sessionLaneLayout(156, 66)).toEqual({ inset: 34, readable: 66, technical: 88 }) + expect(sessionLaneLayout(80, 66)).toEqual({ inset: 0, readable: 66, technical: 80 }) + expect(sessionLaneLayout(60, 66)).toEqual({ inset: 0, readable: 60, technical: 60 }) +})