fix(tui): align content lanes to shared spine

This commit is contained in:
Kit Langton
2026-08-11 14:30:59 -04:00
parent edfd0bdb0b
commit 1c53c90d4e
5 changed files with 56 additions and 17 deletions
+1 -1
View File
@@ -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,
})
+2 -2
View File
@@ -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 () => {
+30 -13
View File
@@ -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 (
<Show when={maxWidth() !== "auto"} fallback={props.children}>
<box width="100%" alignItems="center" flexShrink={0}>
<box width="100%" maxWidth={maxWidth() === "auto" ? undefined : maxWidth()} flexShrink={0}>
{props.children}
<Show when={layout()} fallback={props.children}>
{(value) => (
<box width="100%" paddingLeft={value().inset} flexShrink={0}>
<box width={value()[props.width]} flexShrink={0}>
{props.children}
</box>
</box>
)}
</Show>
)
}
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 (
<Show when={inset() !== undefined} fallback={props.children}>
<box width="100%" paddingLeft={inset()} flexShrink={0}>
{props.children}
</box>
</Show>
)
@@ -2306,7 +2321,9 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
return (
<box width="100%" marginTop={markdownLaneMarginTop(index, segment().width)} flexShrink={0}>
<Switch>
<Match when={segment().width === "full"}>{content}</Match>
<Match when={segment().width === "full"}>
<SessionBreakoutLane>{content}</SessionBreakoutLane>
</Match>
<Match when={segment().width === "technical"}>
<SessionContentLane width="technical">{content}</SessionContentLane>
</Match>
+10
View File
@@ -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,
}
}
+13 -1
View File
@@ -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 })
})