Skip to content

Commit 009f379

Browse files
authored
refactor(tui): simplify inline tool spacing (#33097)
1 parent 95237a9 commit 009f379

3 files changed

Lines changed: 62 additions & 72 deletions

File tree

packages/tui/src/routes/session/index.tsx

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_don
9191
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
9292
const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"])
9393

94+
export const alwaysSeparate = new WeakSet<BoxRenderable>()
95+
9496
type RetryAction = Extract<SessionStatus, { type: "retry" }>["action"]
9597

9698
function goUpsellKeys(action: RetryAction) {
@@ -160,7 +162,6 @@ const context = createContext<{
160162
showTimestamps: () => boolean
161163
showDetails: () => boolean
162164
showGenericToolOutput: () => boolean
163-
userMessageIDs: () => ReadonlySet<string>
164165
diffWrapMode: () => "word" | "none"
165166
providers: () => ReadonlyMap<string, Provider>
166167
sync: ReturnType<typeof useSync>
@@ -218,14 +219,6 @@ export function Session() {
218219
)
219220
: [],
220221
)
221-
const userMessageIDs = createMemo(
222-
() =>
223-
new Set(
224-
messages()
225-
.filter((message) => message.role === "user")
226-
.map((message) => message.id),
227-
),
228-
)
229222
const permissions = createMemo(() => {
230223
if (session()?.parentID) return []
231224
return children().flatMap((x) => sync.data.permission[x.id] ?? [])
@@ -1158,7 +1151,6 @@ export function Session() {
11581151
showTimestamps,
11591152
showDetails,
11601153
showGenericToolOutput,
1161-
userMessageIDs,
11621154
diffWrapMode,
11631155
providers,
11641156
sync,
@@ -1395,6 +1387,7 @@ function UserMessage(props: {
13951387
<Show when={text()}>
13961388
<box
13971389
id={props.message.id}
1390+
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
13981391
border={["left"]}
13991392
borderColor={color()}
14001393
customBorderChars={SplitBorder.customBorderChars}
@@ -1532,7 +1525,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
15321525
</Show>
15331526
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
15341527
<box
1535-
id={`assistant-error-${props.message.id}`}
1528+
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
15361529
border={["left"]}
15371530
paddingTop={1}
15381531
paddingBottom={1}
@@ -1547,7 +1540,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
15471540
</Show>
15481541
<Switch>
15491542
<Match when={props.last || final() || props.message.error?.name === "MessageAbortedError"}>
1550-
<box id={`assistant-summary-${props.message.id}`} paddingLeft={3}>
1543+
<box ref={(el: BoxRenderable) => alwaysSeparate.add(el)} paddingLeft={3}>
15511544
<text marginTop={1}>
15521545
<span
15531546
style={{
@@ -1613,7 +1606,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
16131606
return (
16141607
<Show when={content()}>
16151608
<box
1616-
id={`text-${props.part.messageID}-${props.part.id}`}
1609+
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
16171610
paddingLeft={3}
16181611
marginTop={1}
16191612
flexDirection="column"
@@ -1695,7 +1688,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
16951688
const { theme, syntax } = useTheme()
16961689
return (
16971690
<Show when={props.part.text.trim()}>
1698-
<box id={`text-${props.part.messageID}-${props.part.id}`} paddingLeft={3} marginTop={1} flexShrink={0}>
1691+
<box ref={(el: BoxRenderable) => alwaysSeparate.add(el)} paddingLeft={3} marginTop={1} flexShrink={0}>
16991692
<markdown
17001693
syntaxStyle={syntax()}
17011694
streaming={true}
@@ -1845,7 +1838,6 @@ function InlineTool(props: {
18451838
pending: string
18461839
failure?: string
18471840
spinner?: boolean
1848-
subagent?: boolean
18491841
children: JSX.Element
18501842
part: ToolPart
18511843
onClick?: () => void
@@ -1886,7 +1878,6 @@ function InlineTool(props: {
18861878

18871879
return (
18881880
<InlineToolRow
1889-
id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.messageID}-${props.part.id}`}
18901881
icon={props.icon}
18911882
iconColor={props.iconColor}
18921883
color={fg()}
@@ -1899,8 +1890,6 @@ function InlineTool(props: {
18991890
pending={props.pending}
19001891
failure={props.failure}
19011892
spinner={props.spinner}
1902-
subagent={props.subagent}
1903-
separateAfter={(id) => id !== undefined && ctx.userMessageIDs().has(id)}
19041893
onMouseOver={() => clickable() && setHover(true)}
19051894
onMouseOut={() => setHover(false)}
19061895
onMouseUp={() => {
@@ -1918,7 +1907,6 @@ function InlineTool(props: {
19181907
}
19191908

19201909
export function InlineToolRow(props: {
1921-
id?: string
19221910
icon: string
19231911
iconColor?: RGBA
19241912
color?: RGBA
@@ -1931,32 +1919,20 @@ export function InlineToolRow(props: {
19311919
pending: string
19321920
failure?: string
19331921
spinner?: boolean
1934-
subagent?: boolean
19351922
children: JSX.Element
1936-
separateAfter?: (id: string | undefined) => boolean
19371923
onMouseOver?: () => void
19381924
onMouseOut?: () => void
19391925
onMouseUp?: () => void
19401926
}) {
19411927
return (
19421928
<box
1943-
id={props.id}
19441929
paddingLeft={3}
19451930
onMouseOver={props.onMouseOver}
19461931
onMouseOut={props.onMouseOut}
19471932
onMouseUp={props.onMouseUp}
19481933
ref={(el: BoxRenderable) => {
19491934
setPreLayoutSiblingMargin(el, (previous) => {
1950-
const previousInline = previous?.id.startsWith("tool-inline-") ?? false
1951-
const previousSubagent = previous?.id.startsWith("tool-inline-subagent-") ?? false
1952-
return previous?.id.startsWith("text-") ||
1953-
previous?.id.startsWith("tool-block-") ||
1954-
previous?.id.startsWith("assistant-error-") ||
1955-
previous?.id.startsWith("assistant-summary-") ||
1956-
(previousInline && previousSubagent !== Boolean(props.subagent)) ||
1957-
props.separateAfter?.(previous?.id)
1958-
? 1
1959-
: 0
1935+
return previous instanceof BoxRenderable && (previous.height > 1 || alwaysSeparate.has(previous)) ? 1 : 0
19601936
})
19611937
}}
19621938
>
@@ -2018,7 +1994,7 @@ function BlockTool(props: {
20181994
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
20191995
return (
20201996
<box
2021-
id={props.part ? `tool-block-${props.part.messageID}-${props.part.id}` : undefined}
1997+
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
20221998
border={["left"]}
20231999
paddingTop={1}
20242000
paddingBottom={1}
@@ -2184,8 +2160,8 @@ function Read(props: ToolProps) {
21842160
Read {pathFormatter.format(stringValue(props.input.filePath))} {input(props.input, ["filePath"])}
21852161
</InlineTool>
21862162
<For each={loaded()}>
2187-
{(filepath, index) => (
2188-
<box id={`tool-inline-loaded-${props.part.messageID}-${props.part.id}-${index()}`} paddingLeft={3}>
2163+
{(filepath) => (
2164+
<box paddingLeft={3}>
21892165
<text paddingLeft={3} fg={theme.textMuted}>
21902166
↳ Loaded {pathFormatter.format(filepath)}
21912167
</text>
@@ -2305,7 +2281,6 @@ function Task(props: ToolProps) {
23052281
return (
23062282
<InlineTool
23072283
icon={props.part.state.status === "completed" ? "✓" : "│"}
2308-
subagent={true}
23092284
color={retry() ? theme.error : undefined}
23102285
spinner={isRunning()}
23112286
complete={stringValue(props.input.description)}

packages/tui/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read rows at a narrow width 1`] = `
44
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
55
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
6+
67
✱ Glob "**/*db*" in packages/opencode (6 matches)
78
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
89
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
@@ -13,10 +14,12 @@ exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read row
1314
exports[`TUI inline tool wrapping snapshots expanded tool errors under the tool text 1`] = `
1415
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
1516
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
17+
1618
✱ Glob "**/*db*" in packages/opencode (6 matches)
1719
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
1820
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
1921
No LSP server available for this file type.
22+
2023
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
2124
Path\\.data|data =" in packages/opencode/src (115 matches)"
2225
`;
@@ -32,6 +35,7 @@ exports[`TUI inline tool wrapping keeps separation after a shell output block 1`
3235
3336
✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
3437
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
38+
3539
✱ Glob "**/*db*" in packages/opencode (6 matches)
3640
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
3741
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
@@ -46,39 +50,38 @@ exports[`TUI inline tool wrapping keeps separation after a padded user message 1
4650
4751
✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
4852
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
53+
4954
✱ Glob "**/*db*" in packages/opencode (6 matches)
5055
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
5156
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
5257
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
5358
Path\\.data|data =" in packages/opencode/src (115 matches)"
5459
`;
5560

56-
exports[`TUI inline tool wrapping separates a contiguous subagent group from inline tools 1`] = `
61+
exports[`TUI inline tool wrapping separates after a multi-line task row 1`] = `
5762
" ✱ Grep "Task" (2 matches)
58-
5963
⠙ Explore Task — Inspect active task spacing
6064
✓ General Task — Confirm completed task spacing
6165
↳ 1 toolcall · 501ms
6266
6367
→ Read src/cli/cmd/tui/routes/session/index.tsx"
6468
`;
6569

66-
exports[`TUI inline tool wrapping separates a subagent group after an expanded read 1`] = `
70+
exports[`TUI inline tool wrapping does not treat task rows differently from other inline rows 1`] = `
6771
" → Read src/cli/cmd/tui/routes/session/index.tsx
6872
↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx
69-
7073
✓ Explore Task — Inspect active task spacing
7174
↳ 1 toolcall · 501ms"
7275
`;
7376

74-
exports[`TUI inline tool wrapping separates a subagent from the previous assistant summary 1`] = `
77+
exports[`TUI inline tool wrapping separates an inline row from the previous assistant summary 1`] = `
7578
" ▣ Build · Little Frank · 53.1s
7679
7780
✓ Build Task — Review changes
7881
↳ 48 toolcalls · 1m 40s"
7982
`;
8083

81-
exports[`TUI inline tool wrapping separates a subagent from the previous assistant error 1`] = `
84+
exports[`TUI inline tool wrapping separates an inline row from the previous assistant error 1`] = `
8285
"│
8386
│ Managed inference requires an active Member plan
8487

0 commit comments

Comments
 (0)