diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx
index 6c53d03f32..75a2d925fa 100644
--- a/packages/tui/src/routes/session/index.tsx
+++ b/packages/tui/src/routes/session/index.tsx
@@ -116,7 +116,7 @@ addDefaultParsers(parsers.parsers)
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_CODE_LANE_WIDTH = 88
+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
@@ -1181,22 +1181,30 @@ function SessionRowView(props: SessionRowViewProps) {
)}
-
+
+
+
{(row) => }
- {(row) => }
+ {(row) => (
+
+
+
+ )}
{(row) => (
-
+
+
+
)}
@@ -1214,7 +1222,13 @@ function SessionRowView(props: SessionRowViewProps) {
{(row) => (
-
+
+
+
)}
@@ -1222,19 +1236,21 @@ function SessionRowView(props: SessionRowViewProps) {
)
}
-function SessionContentLane(props: { children: JSX.Element; width: "readable" | "code" }) {
+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_CODE_LANE_WIDTH)
+ return Math.max(readable, SESSION_TECHNICAL_LANE_WIDTH)
})
return (
-
-
- {props.children}
+
+
+
+ {props.children}
+
-
+
)
}
@@ -1400,20 +1416,35 @@ function SessionMessageView(props: { message: SessionMessageInfo }) {
- } />
+
+ } />
+
-
+
+
+
- }>
- } />
+
+
+
+ }
+ >
+
+ } />
+
- } />
+
+ } />
+
)
@@ -1434,11 +1465,13 @@ function SessionPartView(props: { partRef: PartRef; message: (messageID: string)
-
+
+
+
@@ -2273,9 +2306,9 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
return (
- {content}
-
- {content}
+ {content}
+
+ {content}
{content}
@@ -2295,6 +2328,7 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
function ToolPart(props: { part: SessionMessageAssistantTool; images?: boolean }) {
const display = createMemo(() => toolDisplay(props.part.name))
+ const width = createMemo(() => toolLane(props.part.name))
const toolprops = {
get metadata() {
@@ -2364,10 +2398,12 @@ function ToolPart(props: { part: SessionMessageAssistantTool; images?: boolean }
)
return [
- content,
-
-
- ,
+ {content},
+
+
+
+
+ ,
]
}
@@ -3201,7 +3237,7 @@ function Edit(props: ToolProps) {
const diffView = ctx.config.diffs?.view
if (diffView === "unified") return "unified"
if (diffView === "split") return "split"
- // Default to "auto" behavior
+ if ((ctx.config.session?.max_width ?? "auto") !== "auto") return "unified"
return ctx.width > 120 ? "split" : "unified"
})
@@ -3279,6 +3315,7 @@ function ApplyPatch(props: ToolProps) {
const view = createMemo(() => {
if (ctx.config.diffs?.view === "unified") return "unified"
if (ctx.config.diffs?.view === "split") return "split"
+ if ((ctx.config.session?.max_width ?? "auto") !== "auto") return "unified"
return ctx.width > 120 ? "split" : "unified"
})
@@ -3464,11 +3501,17 @@ const toolDisplays = new Set([
"skill",
])
+const technicalToolDisplays = new Set(["shell", "write", "edit", "execute", "patch", "generic"])
+
export function toolDisplay(tool: string) {
const normalized = canonicalToolName(tool)
return toolDisplays.has(normalized) ? normalized : "generic"
}
+export function toolLane(tool: string): "readable" | "technical" {
+ return technicalToolDisplays.has(toolDisplay(tool)) ? "technical" : "readable"
+}
+
function recordValue(value: unknown): Record | undefined {
if (typeof value !== "object" || value === null || Array.isArray(value)) return
return value as Record
diff --git a/packages/tui/src/routes/session/markdown-lanes.ts b/packages/tui/src/routes/session/markdown-lanes.ts
index 402baaa552..9102d5777b 100644
--- a/packages/tui/src/routes/session/markdown-lanes.ts
+++ b/packages/tui/src/routes/session/markdown-lanes.ts
@@ -1,29 +1,38 @@
export type MarkdownLane = {
content: string
- width: "readable" | "code" | "wide"
+ width: "readable" | "technical" | "full"
}
export function markdownLanes(content: string): MarkdownLane[] {
const result: MarkdownLane[] = []
let fence: { marker: "`" | "~"; length: number } | undefined
+ let table = false
+ const lines = content.match(/[^\n]*(?:\n|$)/g)?.filter(Boolean) ?? []
- for (const line of content.match(/[^\n]*(?:\n|$)/g)?.filter(Boolean) ?? []) {
+ for (const [index, line] of lines.entries()) {
const opening = fence ? undefined : line.match(/^ {0,3}(`{3,}|~{3,})([^\n]*)/)
const marker = opening?.[1]
+ const tableOpening = !opening && !fence && isTableRow(line) && isTableDelimiter(lines[index + 1])
if (marker) fence = { marker: marker.startsWith("`") ? "`" : "~", length: marker.length }
+ if (tableOpening) table = true
const width = opening
? opening[2]?.trim().split(/\s/, 1)[0]?.toLowerCase() === "mermaid"
- ? "wide"
- : "code"
+ ? "full"
+ : "technical"
: fence
- ? (result.at(-1)?.width ?? "code")
- : "readable"
+ ? (result.at(-1)?.width ?? "technical")
+ : table
+ ? "technical"
+ : "readable"
const previous = result.at(-1)
if (previous?.width === width) previous.content += line
else result.push({ content: line, width })
- if (!fence) continue
+ if (!fence) {
+ if (table && !isTableRow(lines[index + 1])) table = false
+ continue
+ }
const currentFence = fence
const trimmed = line.trim()
if (
@@ -39,7 +48,18 @@ export function markdownLanes(content: string): MarkdownLane[] {
return result
}
+function isTableRow(line: string | undefined) {
+ return Boolean(line?.trim() && line.includes("|"))
+}
+
+function isTableDelimiter(line: string | undefined) {
+ if (!line) return false
+ const value = line.trim().replace(/^\||\|$/g, "")
+ const cells = value.split("|")
+ return cells.length > 1 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))
+}
+
export function markdownLaneMarginTop(index: number, width: MarkdownLane["width"]) {
- if (index === 0 || width === "wide") return 0
+ if (index === 0 || width === "full") return 0
return 1
}
diff --git a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx
index e6958334c9..3055db8294 100644
--- a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx
+++ b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx
@@ -10,6 +10,7 @@ import {
parseQuestionAnswers,
parseQuestions,
toolDisplay,
+ toolLane,
} from "../../../src/routes/session"
let testSetup: Awaited> | undefined
@@ -131,6 +132,11 @@ describe("TUI inline tool wrapping", () => {
expect(toolDisplay("apply_patch")).toBe("patch")
expect(toolDisplay("patch")).toBe("patch")
expect(toolDisplay("plugin_tool")).toBe("generic")
+ expect(toolLane("glob")).toBe("readable")
+ expect(toolLane("webfetch")).toBe("readable")
+ expect(toolLane("shell")).toBe("technical")
+ expect(toolLane("apply_patch")).toBe("technical")
+ expect(toolLane("plugin_tool")).toBe("technical")
})
test("replaces pending copy when a tool fails before completion", async () => {
diff --git a/packages/tui/test/cli/tui/markdown-lanes.test.ts b/packages/tui/test/cli/tui/markdown-lanes.test.ts
index 0cf06e3e43..3e6613de1a 100644
--- a/packages/tui/test/cli/tui/markdown-lanes.test.ts
+++ b/packages/tui/test/cli/tui/markdown-lanes.test.ts
@@ -5,7 +5,7 @@ test("keeps prose in the readable lane", () => {
expect(markdownLanes("Before\n\nAfter")).toEqual([{ content: "Before\n\nAfter", width: "readable" }])
})
-test("moves fenced blocks into the wide lane", () => {
+test("moves Mermaid fences into the full lane", () => {
expect(
markdownLanes(`Before
@@ -17,40 +17,52 @@ flowchart LR
After`),
).toEqual([
{ content: "Before\n\n", width: "readable" },
- { content: "```mermaid\nflowchart LR\n A --> B\n```\n", width: "wide" },
+ { content: "```mermaid\nflowchart LR\n A --> B\n```\n", width: "full" },
{ content: "\nAfter", width: "readable" },
])
})
-test("keeps an incomplete streaming fence wide", () => {
+test("keeps an incomplete streaming Mermaid fence full width", () => {
expect(markdownLanes("Before\n```mermaid\nflowchart LR\n A -->")).toEqual([
{ content: "Before\n", width: "readable" },
- { content: "```mermaid\nflowchart LR\n A -->", width: "wide" },
+ { content: "```mermaid\nflowchart LR\n A -->", width: "full" },
])
})
test("supports tilde fences and longer closing fences", () => {
expect(markdownLanes("~~~ts\nconst value = 1\n~~~~\nAfter")).toEqual([
- { content: "~~~ts\nconst value = 1\n~~~~\n", width: "code" },
+ { content: "~~~ts\nconst value = 1\n~~~~\n", width: "technical" },
{ content: "After", width: "readable" },
])
})
test("does not close a fence indented as code", () => {
expect(markdownLanes("```ts\n ```\nstill code")).toEqual([
- { content: "```ts\n ```\nstill code", width: "code" },
+ { content: "```ts\n ```\nstill code", width: "technical" },
])
})
test("gives ordinary fenced code an intermediate lane", () => {
expect(markdownLanes("```ts\nexport const value = true\n```")).toEqual([
- { content: "```ts\nexport const value = true\n```", width: "code" },
+ { content: "```ts\nexport const value = true\n```", width: "technical" },
])
})
+test("gives Markdown tables the technical lane", () => {
+ expect(markdownLanes("Before\n\n| Name | Value |\n| --- | ---: |\n| Width | 88 |\n\nAfter")).toEqual([
+ { content: "Before\n\n", width: "readable" },
+ { content: "| Name | Value |\n| --- | ---: |\n| Width | 88 |\n", width: "technical" },
+ { content: "\nAfter", width: "readable" },
+ ])
+})
+
+test("does not treat ordinary pipe characters as a table", () => {
+ expect(markdownLanes("Use foo | bar in prose.")).toEqual([{ content: "Use foo | bar in prose.", width: "readable" }])
+})
+
test("restores spacing between separately rendered blocks", () => {
expect(markdownLaneMarginTop(0, "readable")).toBe(0)
- expect(markdownLaneMarginTop(1, "code")).toBe(1)
+ expect(markdownLaneMarginTop(1, "technical")).toBe(1)
expect(markdownLaneMarginTop(2, "readable")).toBe(1)
- expect(markdownLaneMarginTop(1, "wide")).toBe(0)
+ expect(markdownLaneMarginTop(1, "full")).toBe(0)
})