fix(tui): align content lanes to shared spine
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user