diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 4d983ee99b72..f36ddd9daba6 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -91,6 +91,8 @@ const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_don const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"]) +export const alwaysSeparate = new WeakSet() + type RetryAction = Extract["action"] function goUpsellKeys(action: RetryAction) { @@ -160,7 +162,6 @@ const context = createContext<{ showTimestamps: () => boolean showDetails: () => boolean showGenericToolOutput: () => boolean - userMessageIDs: () => ReadonlySet diffWrapMode: () => "word" | "none" providers: () => ReadonlyMap sync: ReturnType @@ -218,14 +219,6 @@ export function Session() { ) : [], ) - const userMessageIDs = createMemo( - () => - new Set( - messages() - .filter((message) => message.role === "user") - .map((message) => message.id), - ), - ) const permissions = createMemo(() => { if (session()?.parentID) return [] return children().flatMap((x) => sync.data.permission[x.id] ?? []) @@ -1158,7 +1151,6 @@ export function Session() { showTimestamps, showDetails, showGenericToolOutput, - userMessageIDs, diffWrapMode, providers, sync, @@ -1395,6 +1387,7 @@ function UserMessage(props: { alwaysSeparate.add(el)} border={["left"]} borderColor={color()} customBorderChars={SplitBorder.customBorderChars} @@ -1532,7 +1525,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las alwaysSeparate.add(el)} border={["left"]} paddingTop={1} paddingBottom={1} @@ -1547,7 +1540,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las - + alwaysSeparate.add(el)} paddingLeft={3}> alwaysSeparate.add(el)} paddingLeft={3} marginTop={1} flexDirection="column" @@ -1695,7 +1688,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess const { theme, syntax } = useTheme() return ( - + alwaysSeparate.add(el)} paddingLeft={3} marginTop={1} flexShrink={0}> void @@ -1886,7 +1878,6 @@ function InlineTool(props: { return ( id !== undefined && ctx.userMessageIDs().has(id)} onMouseOver={() => clickable() && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { @@ -1918,7 +1907,6 @@ function InlineTool(props: { } export function InlineToolRow(props: { - id?: string icon: string iconColor?: RGBA color?: RGBA @@ -1931,32 +1919,20 @@ export function InlineToolRow(props: { pending: string failure?: string spinner?: boolean - subagent?: boolean children: JSX.Element - separateAfter?: (id: string | undefined) => boolean onMouseOver?: () => void onMouseOut?: () => void onMouseUp?: () => void }) { return ( { setPreLayoutSiblingMargin(el, (previous) => { - const previousInline = previous?.id.startsWith("tool-inline-") ?? false - const previousSubagent = previous?.id.startsWith("tool-inline-subagent-") ?? false - return previous?.id.startsWith("text-") || - previous?.id.startsWith("tool-block-") || - previous?.id.startsWith("assistant-error-") || - previous?.id.startsWith("assistant-summary-") || - (previousInline && previousSubagent !== Boolean(props.subagent)) || - props.separateAfter?.(previous?.id) - ? 1 - : 0 + return previous instanceof BoxRenderable && (previous.height > 1 || alwaysSeparate.has(previous)) ? 1 : 0 }) }} > @@ -2018,7 +1994,7 @@ function BlockTool(props: { const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined)) return ( alwaysSeparate.add(el)} border={["left"]} paddingTop={1} paddingBottom={1} @@ -2184,8 +2160,8 @@ function Read(props: ToolProps) { Read {pathFormatter.format(stringValue(props.input.filePath))} {input(props.input, ["filePath"])} - {(filepath, index) => ( - + {(filepath) => ( + ↳ Loaded {pathFormatter.format(filepath)} @@ -2305,7 +2281,6 @@ function Task(props: ToolProps) { return ( + alwaysSeparate.add(el)} + marginTop={1} + paddingTop={1} + paddingBottom={1} + paddingLeft={2} + gap={1} + > # List files $ ls @@ -65,7 +73,7 @@ function ShellOutput() { function UserMessage() { return ( - + alwaysSeparate.add(el)}> Check whether the next tool remains separated. @@ -88,7 +96,6 @@ function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" }) failed={Boolean(item.error)} error={item.error} errorExpanded={props.errorExpanded} - separateAfter={(id) => id === "message-user"} > {item.label} @@ -99,61 +106,67 @@ function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" }) ) } -function SubagentGroupFixture() { +function TaskRowsFixture() { return ( - + Grep "Task" (2 matches) - + Explore Task — Inspect active task spacing - + {"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"} - + Read src/cli/cmd/tui/routes/session/index.tsx ) } -function LoadedReadBeforeSubagentFixture() { +function LoadedReadBeforeTaskFixture() { return ( - + Read src/cli/cmd/tui/routes/session/index.tsx - + ↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx - + {"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"} ) } -function AssistantSummaryBeforeSubagentFixture() { +function AssistantSummaryBeforeInlineFixture() { return ( - + alwaysSeparate.add(el)} paddingLeft={3}> ▣ Build · Little Frank · 53.1s - + {"Build Task — Review changes\n↳ 48 toolcalls · 1m 40s"} ) } -function AssistantErrorBeforeSubagentFixture() { +function AssistantErrorBeforeInlineFixture() { return ( - + alwaysSeparate.add(el)} + border={["left"]} + paddingTop={1} + paddingBottom={1} + paddingLeft={2} + > Managed inference requires an active Member plan - + {"Build Task — Review changes\n↳ 48 toolcalls · 1m 40s"} @@ -170,7 +183,7 @@ function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: Scrol Second row - + alwaysSeparate.add(el)}> Assistant text @@ -200,6 +213,7 @@ function FailedCompleteToolFixture() { async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) { testSetup = await testRender(component, options) await testSetup.renderOnce() + await testSetup.renderOnce() return testSetup .captureCharFrame() @@ -294,22 +308,20 @@ describe("TUI inline tool wrapping", () => { expect(await renderFrame(() => , { width: 72, height: 14 })).toMatchSnapshot() }) - test("separates a contiguous subagent group from inline tools", async () => { - expect(await renderFrame(() => , { width: 72, height: 10 })).toMatchSnapshot() + test("separates after a multi-line task row", async () => { + expect(await renderFrame(() => , { width: 72, height: 10 })).toMatchSnapshot() }) - test("separates a subagent group after an expanded read", async () => { - expect(await renderFrame(() => , { width: 72, height: 8 })).toMatchSnapshot() + test("does not treat task rows differently from other inline rows", async () => { + expect(await renderFrame(() => , { width: 72, height: 8 })).toMatchSnapshot() }) - test("separates a subagent from the previous assistant summary", async () => { - expect( - await renderFrame(() => , { width: 72, height: 5 }), - ).toMatchSnapshot() + test("separates an inline row from the previous assistant summary", async () => { + expect(await renderFrame(() => , { width: 72, height: 5 })).toMatchSnapshot() }) - test("separates a subagent from the previous assistant error", async () => { - expect(await renderFrame(() => , { width: 72, height: 7 })).toMatchSnapshot() + test("separates an inline row from the previous assistant error", async () => { + expect(await renderFrame(() => , { width: 72, height: 7 })).toMatchSnapshot() }) test("updates sticky-bottom geometry when a text separator mounts and unmounts", async () => {