diff --git a/src/runtime/local-opentui-bridge.test.ts b/src/runtime/local-opentui-bridge.test.ts new file mode 100644 index 00000000..eebdbc45 --- /dev/null +++ b/src/runtime/local-opentui-bridge.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { LocalOpenTuiTranscriptBridge } from "./local-opentui-bridge.js"; +import type { ChatMessage } from "@step-cli/protocol"; + +function createBridge(): LocalOpenTuiTranscriptBridge { + return new LocalOpenTuiTranscriptBridge(); +} + +describe("LocalOpenTuiTranscriptBridge", () => { + describe("reconcileWithSessionMessages with reasoning", () => { + it("splits assistant messages with reasoning into two entries", () => { + const bridge = createBridge(); + const messages: ChatMessage[] = [ + { + role: "assistant", + content: "Final answer.", + reasoning: "First I thought about X.\nThen I considered Y.", + }, + ]; + + bridge.reconcileWithSessionMessages(messages); + const entries = bridge.getEntries(); + + expect(entries).toHaveLength(2); + expect(entries[0]?.role).toBe("reasoning"); + expect(entries[0]?.content).toBe( + "First I thought about X.\nThen I considered Y.", + ); + expect(entries[1]?.role).toBe("assistant"); + expect(entries[1]?.content).toBe("Final answer."); + }); + + it("keeps a single assistant entry when there is no reasoning", () => { + const bridge = createBridge(); + const messages: ChatMessage[] = [ + { + role: "assistant", + content: "Just the answer.", + }, + ]; + + bridge.reconcileWithSessionMessages(messages); + const entries = bridge.getEntries(); + + expect(entries).toHaveLength(1); + expect(entries[0]?.role).toBe("assistant"); + expect(entries[0]?.content).toBe("Just the answer."); + }); + + it("uses reasoning_content over reasoning when both are present", () => { + const bridge = createBridge(); + const messages: ChatMessage[] = [ + { + role: "assistant", + content: "Answer.", + reasoning: "Old reasoning.", + reasoning_content: "New reasoning.\nMore details.", + }, + ]; + + bridge.reconcileWithSessionMessages(messages); + const entries = bridge.getEntries(); + + expect(entries).toHaveLength(2); + expect(entries[0]?.role).toBe("reasoning"); + expect(entries[0]?.content).toBe("New reasoning.\nMore details."); + }); + + it("ignores empty reasoning fields", () => { + const bridge = createBridge(); + const messages: ChatMessage[] = [ + { + role: "assistant", + content: "Answer.", + reasoning: " ", + }, + ]; + + bridge.reconcileWithSessionMessages(messages); + const entries = bridge.getEntries(); + + expect(entries).toHaveLength(1); + expect(entries[0]?.role).toBe("assistant"); + }); + + it("returns only the reasoning entry when assistant content is empty", () => { + const bridge = createBridge(); + const messages: ChatMessage[] = [ + { + role: "assistant", + content: "", + reasoning: "Planning the next tool call.", + }, + ]; + + bridge.reconcileWithSessionMessages(messages); + const entries = bridge.getEntries(); + + expect(entries).toHaveLength(1); + expect(entries[0]?.role).toBe("reasoning"); + expect(entries[0]?.content).toBe("Planning the next tool call."); + }); + }); +}); diff --git a/src/runtime/local-opentui-bridge.ts b/src/runtime/local-opentui-bridge.ts index 6374be43..47a9d3f4 100644 --- a/src/runtime/local-opentui-bridge.ts +++ b/src/runtime/local-opentui-bridge.ts @@ -159,7 +159,7 @@ export class LocalOpenTuiTranscriptBridge implements StepCliTuiTranscriptControl messages: ChatMessage[], settledTurnId?: string, ): void { - const nextSessionEntries = messages.map((message) => + const nextSessionEntries = messages.flatMap((message) => mapChatMessageToTranscriptEntry(message), ); const appendedSessionEntries = nextSessionEntries.slice( @@ -379,7 +379,7 @@ export class LocalOpenTuiTranscriptBridge implements StepCliTuiTranscriptControl this.sessionEntries = [ ...this.sessionEntries, - ...(messages as ChatMessage[]).map((message) => + ...(messages as ChatMessage[]).flatMap((message) => mapChatMessageToTranscriptEntry(message), ), ]; @@ -868,58 +868,80 @@ function matchCoveredOptimisticUserTurnIds( function mapChatMessageToTranscriptEntry( message: ChatMessage, -): StepCliTuiTranscriptEntry { +): StepCliTuiTranscriptEntry[] { switch (message.role) { - case "assistant": - return { + case "assistant": { + const reasoning = extractAssistantReasoning(message); + const assistantEntry: StepCliTuiTranscriptEntry = { id: randomUUID(), role: "assistant", caption: null, - content: formatAssistantContent(message), + content: message.content.trim(), }; - case "user": - return { + if (!reasoning) { + return [assistantEntry]; + } + const reasoningEntry: StepCliTuiTranscriptEntry = { id: randomUUID(), - role: "user", + role: "reasoning", caption: null, - content: formatUserTurnContent({ - content: message.content, - attachments: message.attachments, - }), + content: reasoning, }; + // Reasoning-only messages (e.g. intermediate tool-call steps with an + // empty content field) must not produce an empty assistant entry, which + // would render as a bare ASSISTANT badge row. + if (assistantEntry.content.length === 0) { + return [reasoningEntry]; + } + return [reasoningEntry, assistantEntry]; + } + case "user": + return [ + { + id: randomUUID(), + role: "user", + caption: null, + content: formatUserTurnContent({ + content: message.content, + attachments: message.attachments, + }), + }, + ]; case "tool": - return { - id: randomUUID(), - role: "tool", - caption: message.name, - content: formatStoredToolMessageContent(message), - }; + return [ + { + id: randomUUID(), + role: "tool", + caption: message.name, + content: formatStoredToolMessageContent(message), + }, + ]; case "system": - return { - id: randomUUID(), - role: "system", - caption: null, - content: message.content, - hidden: message.hidden, - }; + return [ + { + id: randomUUID(), + role: "system", + caption: null, + content: message.content, + hidden: message.hidden, + }, + ]; } } -function formatAssistantContent( +function extractAssistantReasoning( message: Extract, -): string { +): string | null { const reasoning = message.reasoning_content ?? message.reasoning ?? message.thinking ?? message.analysis ?? message.redacted_thinking; - const reasoningBlock = - typeof reasoning === "string" && reasoning.trim().length > 0 - ? `\n[reasoning] ${reasoning}` - : ""; - - return `${message.content}${reasoningBlock}`.trim(); + if (typeof reasoning !== "string" || reasoning.trim().length === 0) { + return null; + } + return reasoning.trim(); } function formatLocalHookEntry( diff --git a/src/tui/app.tsx b/src/tui/app.tsx index 82880d8f..a811536e 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -45,6 +45,8 @@ import { } from "./theme.js"; import { compactToolTranscriptContent } from "./transcript-preview.js"; import { buildTranscriptClipboardText } from "./transcript-export.js"; +import { buildToolTranscriptLines } from "./tool-call-collapsible.js"; +import { buildReasoningTranscriptLines } from "./reasoning-collapsible.js"; import type { StepCliTuiComposerState, StepCliTuiComposerHistoryState, @@ -147,6 +149,7 @@ export function StepCliTuiScreen(props: StepCliTuiScreenProps) { const [activeRunCount, setActiveRunCount] = useState(0); const [spinnerFrameIndex, setSpinnerFrameIndex] = useState(0); const [slashSelectionIndex, setSlashSelectionIndex] = useState(0); + const [toolOutputExpanded, setToolOutputExpanded] = useState(false); const submitting = activeRunCount > 0; // Voice mode state. The host passes a loadVoiceRuntime factory; we cache @@ -604,6 +607,12 @@ export function StepCliTuiScreen(props: StepCliTuiScreenProps) { return; } + if (key.ctrl && key.name === "o") { + key.preventDefault(); + setToolOutputExpanded((current) => !current); + return; + } + if (key.name === "up") { if (slashPaletteState.visible && slashPaletteState.matches.length > 0) { key.preventDefault(); @@ -770,8 +779,14 @@ export function StepCliTuiScreen(props: StepCliTuiScreenProps) { [props.scrollConfig, terminal.height], ); const transcriptItems = useMemo( - () => buildTranscriptItems(transcriptEntries, transcriptWidth, theme), - [theme, transcriptEntries, transcriptWidth], + () => + buildTranscriptItems( + transcriptEntries, + transcriptWidth, + theme, + toolOutputExpanded, + ), + [theme, transcriptEntries, transcriptWidth, toolOutputExpanded], ); return ( @@ -1522,6 +1537,7 @@ const TranscriptEntry = React.memo(function TranscriptEntry(input: { const [firstLine = "", ...restLines] = item.lines.length > 0 ? item.lines : [""]; const badgeStyle = resolveTranscriptBadgeStyle(item.tone, input.theme); + const isCollapsed = item.collapsible && !item.expanded; const body = ( <> @@ -1529,17 +1545,23 @@ const TranscriptEntry = React.memo(function TranscriptEntry(input: { {" "} {item.badge}{" "} - {item.caption ? ( + {item.caption && !isCollapsed ? ( {item.caption} ) : null} {firstLine.length > 0 ? {firstLine} : null} - {restLines.map((line, index) => ( - - - {line.length > 0 ? line : " "} - - ))} + {isCollapsed + ? restLines.map((line, index) => ( + + {line.length > 0 ? line : " "} + + )) + : restLines.map((line, index) => ( + + + {line.length > 0 ? line : " "} + + ))} {item.truncated ? ( … @@ -1911,6 +1933,7 @@ function buildTranscriptItems( entries: StepCliTuiTranscriptEntry[], width: number, theme: StepCliTuiThemeColors, + toolOutputExpanded: boolean, ): TranscriptItem[] { return [ buildWelcomeTranscriptItem(width), @@ -1918,8 +1941,19 @@ function buildTranscriptItems( .filter((entry) => !entry.hidden) .map((entry, index) => { const identity = resolveTranscriptIdentity(entry); - const body = compactToolTranscriptContent(entry); - const lines = wrapMultiline(body, Math.max(12, width - 4)); + const isTool = entry.role === "tool"; + const isReasoning = entry.role === "reasoning"; + const collapsible = isTool || isReasoning; + const contentWidth = Math.max(12, width - 4); + // Collapsible lines must go through wrapMultiline like every other + // transcript line: OpenTUI does not wrap by itself and scroll + // heights are computed from wrapped line counts. + const lines = collapsible + ? (isTool + ? buildToolTranscriptLines(entry, toolOutputExpanded) + : buildReasoningTranscriptLines(entry, toolOutputExpanded) + ).flatMap((line) => wrapMultiline(line, contentWidth)) + : wrapMultiline(compactToolTranscriptContent(entry), contentWidth); return { id: entry.id || @@ -1929,6 +1963,8 @@ function buildTranscriptItems( border: false, lines, truncated: false, + collapsible, + expanded: collapsible ? toolOutputExpanded : undefined, }; }), ]; @@ -1938,7 +1974,7 @@ function buildWelcomeTranscriptItem(width: number): TranscriptItem { const welcomeLines = [ "Welcome to STEP.", "Start with a prompt, or use /attach to queue an image.", - "Enter send · Shift+Enter newline · Ctrl+Y or /copy copy selection/full transcript · Esc quit", + "Enter send · Shift+Enter newline · Ctrl+Y or /copy copy selection/full transcript · Ctrl+O expand/collapse tool & reasoning output · Esc quit", "/goal /attach /copy /detach /status /refresh /theme [name] /resume /exit", ].flatMap((line) => wrapMultiline(line, Math.max(12, width - 4))); @@ -2091,6 +2127,12 @@ function resolveTranscriptIdentity( caption: entry.caption, tone: "success", }; + case "reasoning": + return { + badge: "THINK", + caption: entry.caption, + tone: "muted", + }; case "system": return { badge: "SYSTEM", @@ -2319,6 +2361,10 @@ interface TranscriptItem { border: boolean; lines: string[]; truncated: boolean; + /** When true, the entry can be expanded/collapsed with Ctrl+O. */ + collapsible?: boolean; + /** Current expansion state (only meaningful when collapsible is true). */ + expanded?: boolean; } interface SlashPaletteState { diff --git a/src/tui/reasoning-collapsible.test.ts b/src/tui/reasoning-collapsible.test.ts new file mode 100644 index 00000000..cb7bf4c6 --- /dev/null +++ b/src/tui/reasoning-collapsible.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { + buildCollapsedReasoningSummary, + buildReasoningTranscriptLines, +} from "./reasoning-collapsible.js"; +import type { StepCliTuiTranscriptEntry } from "./types.js"; + +function makeReasoningEntry(content: string): StepCliTuiTranscriptEntry { + return { + id: "test-reasoning-id", + role: "reasoning", + caption: null, + content, + }; +} + +describe("buildCollapsedReasoningSummary", () => { + it("shows all lines when content is within preview limit", () => { + const entry = makeReasoningEntry("line 1\nline 2"); + const summary = buildCollapsedReasoningSummary(entry); + expect(summary.previewLines).toEqual(["line 1", "line 2"]); + expect(summary.hiddenLineCount).toBe(0); + }); + + it("reports hidden line count when content exceeds preview", () => { + const entry = makeReasoningEntry("line 1\nline 2\nline 3\nline 4"); + const summary = buildCollapsedReasoningSummary(entry); + expect(summary.previewLines).toEqual(["line 1", "line 2"]); + expect(summary.hiddenLineCount).toBe(2); + expect(summary.expandHint).toBe("... (2 more lines, ctrl-o to expand)"); + }); + + it("uses singular hint when exactly one line is hidden", () => { + const entry = makeReasoningEntry("line 1\nline 2\nline 3"); + const summary = buildCollapsedReasoningSummary(entry); + expect(summary.hiddenLineCount).toBe(1); + expect(summary.expandHint).toBe("... (1 more line, ctrl-o to expand)"); + }); + + it("ignores empty lines when counting hidden lines", () => { + const entry = makeReasoningEntry("line 1\nline 2\n\n\nline 3"); + const summary = buildCollapsedReasoningSummary(entry); + expect(summary.previewLines).toEqual(["line 1", "line 2"]); + expect(summary.hiddenLineCount).toBe(1); + }); +}); + +describe("buildReasoningTranscriptLines", () => { + it("collapses reasoning entries by default with preview lines and hint", () => { + const entry = makeReasoningEntry("line 1\nline 2\nline 3\nline 4"); + const lines = buildReasoningTranscriptLines(entry, false); + expect(lines).toEqual([ + "line 1", + "line 2", + "... (2 more lines, ctrl-o to expand)", + ]); + }); + + it("expands reasoning entries when requested", () => { + const entry = makeReasoningEntry("line 1\nline 2\nline 3"); + const lines = buildReasoningTranscriptLines(entry, true); + expect(lines).toEqual(["line 1", "line 2", "line 3"]); + }); + + it("normalizes CRLF line endings when expanded", () => { + const entry = makeReasoningEntry("line 1\r\nline 2\r\nline 3"); + const lines = buildReasoningTranscriptLines(entry, true); + expect(lines).toEqual(["line 1", "line 2", "line 3"]); + }); + + it("does not show expand hint when everything fits in preview", () => { + const entry = makeReasoningEntry("line 1\nline 2"); + const lines = buildReasoningTranscriptLines(entry, false); + expect(lines).toEqual(["line 1", "line 2"]); + }); +}); diff --git a/src/tui/reasoning-collapsible.ts b/src/tui/reasoning-collapsible.ts new file mode 100644 index 00000000..125a4628 --- /dev/null +++ b/src/tui/reasoning-collapsible.ts @@ -0,0 +1,53 @@ +import type { StepCliTuiTranscriptEntry } from "./types.js"; + +export const REASONING_PREVIEW_LINES = 2; + +// Reasoning content can carry CRLF/CR line endings; normalize before +// splitting so no stray \r reaches the renderer. +function splitContentLines(content: string): string[] { + return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); +} + +export interface CollapsedReasoningSummary { + previewLines: string[]; + hiddenLineCount: number; + expandHint: string; +} + +export function buildCollapsedReasoningSummary( + entry: StepCliTuiTranscriptEntry, +): CollapsedReasoningSummary { + const lines = splitContentLines(entry.content) + .map((line) => line.trimEnd()) + .filter((line) => line.trim().length > 0); + + const previewLines = lines.slice(0, REASONING_PREVIEW_LINES); + const hiddenLineCount = Math.max(0, lines.length - REASONING_PREVIEW_LINES); + + const expandHint = + hiddenLineCount === 1 + ? "... (1 more line, ctrl-o to expand)" + : `... (${hiddenLineCount} more lines, ctrl-o to expand)`; + + return { + previewLines, + hiddenLineCount, + expandHint, + }; +} + +export function buildReasoningTranscriptLines( + entry: StepCliTuiTranscriptEntry, + expanded: boolean, +): string[] { + if (expanded) { + return splitContentLines(entry.content); + } + + const summary = buildCollapsedReasoningSummary(entry); + const collapsedLines = [...summary.previewLines]; + if (summary.hiddenLineCount > 0) { + collapsedLines.push(summary.expandHint); + } + return collapsedLines; +} diff --git a/src/tui/tool-call-collapsible.test.ts b/src/tui/tool-call-collapsible.test.ts new file mode 100644 index 00000000..2a82bd3e --- /dev/null +++ b/src/tui/tool-call-collapsible.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vitest"; +import { + buildCollapsedToolSummary, + buildToolTranscriptLines, +} from "./tool-call-collapsible.js"; +import type { StepCliTuiTranscriptEntry } from "./types.js"; + +function makeToolEntry( + content: string, + caption: string | null = "Bash", +): StepCliTuiTranscriptEntry { + return { + id: "test-tool-id", + role: "tool", + caption, + content, + }; +} + +describe("buildCollapsedToolSummary", () => { + it("returns status and first detail in headline", () => { + const entry = makeToolEntry( + "[completed] completed\nargs ls -la\nresult line 1\nresult line 2", + ); + const summary = buildCollapsedToolSummary(entry); + expect(summary.headline).toBe("Bash · [completed] args ls -la"); + expect(summary.previewLines).toEqual(["result line 1", "result line 2"]); + expect(summary.hiddenLineCount).toBe(0); + }); + + it("reports hidden line count when content exceeds preview", () => { + const entry = makeToolEntry( + "[completed] completed\nargs ls -la\nline 1\nline 2\nline 3\nline 4", + ); + const summary = buildCollapsedToolSummary(entry); + expect(summary.previewLines).toEqual(["line 1", "line 2"]); + expect(summary.hiddenLineCount).toBe(2); + expect(summary.expandHint).toBe("... (2 more lines, ctrl-o to expand)"); + }); + + it("truncates very long first detail lines", () => { + const longCommand = "x".repeat(120); + const entry = makeToolEntry(`[completed] completed\n${longCommand}`); + const summary = buildCollapsedToolSummary(entry); + expect(summary.headline).toBe(`Bash · [completed] ${"x".repeat(77)}...`); + expect(summary.previewLines).toEqual([]); + expect(summary.hiddenLineCount).toBe(0); + }); + + it("does not show expand hint for a single-line result", () => { + const entry = makeToolEntry("[completed] completed"); + const summary = buildCollapsedToolSummary(entry); + expect(summary.headline).toBe("Bash · [completed]"); + expect(summary.previewLines).toEqual([]); + expect(summary.hiddenLineCount).toBe(0); + }); + + it("falls back to 'completed' when no status tag is present", () => { + const entry = makeToolEntry("plain output\nmore output\nthird line"); + const summary = buildCollapsedToolSummary(entry); + expect(summary.headline).toBe("Bash · [completed] plain output"); + expect(summary.previewLines).toEqual(["more output", "third line"]); + expect(summary.hiddenLineCount).toBe(0); + }); + + it("normalizes CRLF line endings so previews carry no stray \\r", () => { + const entry = makeToolEntry( + "[completed] completed\r\nargs dir\r\nline 1\r\nline 2", + ); + const summary = buildCollapsedToolSummary(entry); + expect(summary.headline).toBe("Bash · [completed] args dir"); + expect(summary.previewLines).toEqual(["line 1", "line 2"]); + }); +}); + +describe("buildToolTranscriptLines", () => { + it("collapses tool entries by default with preview lines and hint", () => { + const entry = makeToolEntry( + "[completed] completed\nargs ls -la\nline 1\nline 2\nline 3", + ); + const lines = buildToolTranscriptLines(entry, false); + expect(lines).toEqual([ + "Bash · [completed] args ls -la", + "line 1", + "line 2", + "... (1 more line, ctrl-o to expand)", + ]); + }); + + it("expands tool entries when requested", () => { + const entry = makeToolEntry("[completed] completed\nline 1\nline 2"); + const lines = buildToolTranscriptLines(entry, true); + expect(lines.join("\n")).toContain("line 1"); + expect(lines.join("\n")).toContain("line 2"); + }); + + it("normalizes CRLF line endings when expanded", () => { + const entry = makeToolEntry("[completed] completed\r\nline 1\r\nline 2"); + const lines = buildToolTranscriptLines(entry, true); + expect(lines).toEqual(["[completed] completed", "line 1", "line 2"]); + }); + + it("does not show expand hint when everything fits in preview", () => { + const entry = makeToolEntry("[completed] completed\nargs ls -la\nline 1"); + const lines = buildToolTranscriptLines(entry, false); + expect(lines).toEqual(["Bash · [completed] args ls -la", "line 1"]); + }); +}); diff --git a/src/tui/tool-call-collapsible.ts b/src/tui/tool-call-collapsible.ts new file mode 100644 index 00000000..c0130bef --- /dev/null +++ b/src/tui/tool-call-collapsible.ts @@ -0,0 +1,80 @@ +import type { StepCliTuiTranscriptEntry } from "./types.js"; + +const COLLAPSED_TOOL_DETAIL_MAX_LENGTH = 80; +const TOOL_PREVIEW_LINES = 2; +const STATUS_LINE_RE = /^\[(\w+)\]\s*(.*)$/; + +// Tool output on Windows can carry CRLF/CR line endings; normalize before +// splitting so no stray \r reaches the renderer. +function splitContentLines(content: string): string[] { + return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); +} + +export interface CollapsedToolSummary { + headline: string; + previewLines: string[]; + hiddenLineCount: number; + expandHint: string; +} + +export function buildCollapsedToolSummary( + entry: StepCliTuiTranscriptEntry, +): CollapsedToolSummary { + const caption = entry.caption ?? "tool"; + const lines = splitContentLines(entry.content); + const firstLine = lines[0]?.trim() ?? ""; + const statusMatch = STATUS_LINE_RE.exec(firstLine); + const status = statusMatch?.[1] ?? "completed"; + + // Detail lines are everything after the status line, skipping empty lines. + const detailStartIndex = statusMatch ? 1 : 0; + const detailLines = lines + .slice(detailStartIndex) + .map((line) => line.trimEnd()) + .filter((line) => line.trim().length > 0); + + const firstDetail = detailLines[0]?.trim() ?? ""; + const trimmedFirstDetail = + firstDetail.length > COLLAPSED_TOOL_DETAIL_MAX_LENGTH + ? `${firstDetail.slice(0, COLLAPSED_TOOL_DETAIL_MAX_LENGTH - 3)}...` + : firstDetail; + + const headline = trimmedFirstDetail + ? `${caption} · [${status}] ${trimmedFirstDetail}` + : `${caption} · [${status}]`; + + // Show the next N detail lines after the one already used in the headline. + const previewLines = detailLines.slice(1, 1 + TOOL_PREVIEW_LINES); + const hiddenLineCount = Math.max( + 0, + detailLines.length - 1 - previewLines.length, + ); + + const expandHint = + hiddenLineCount === 1 + ? "... (1 more line, ctrl-o to expand)" + : `... (${hiddenLineCount} more lines, ctrl-o to expand)`; + + return { + headline, + previewLines, + hiddenLineCount, + expandHint, + }; +} + +export function buildToolTranscriptLines( + entry: StepCliTuiTranscriptEntry, + expanded: boolean, +): string[] { + if (expanded) { + return splitContentLines(entry.content); + } + + const summary = buildCollapsedToolSummary(entry); + const collapsedLines = [summary.headline, ...summary.previewLines]; + if (summary.hiddenLineCount > 0) { + collapsedLines.push(summary.expandHint); + } + return collapsedLines; +} diff --git a/src/tui/types.ts b/src/tui/types.ts index adf838f9..37eeed7e 100644 --- a/src/tui/types.ts +++ b/src/tui/types.ts @@ -84,7 +84,7 @@ export interface StepCliTuiSessionData { export interface StepCliTuiTranscriptEntry { id: string; - role: "assistant" | "user" | "tool" | "system"; + role: "assistant" | "user" | "tool" | "system" | "reasoning"; content: string; caption: string | null; /** Internal message that should not be rendered in the transcript. */