mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 06:16:48 +02:00
fix(tui): clarify inline subagent rows (#30051)
This commit is contained in:
@@ -1789,6 +1789,7 @@ function InlineTool(props: {
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
subagent?: boolean
|
||||
children: JSX.Element
|
||||
part: ToolPart
|
||||
onClick?: () => void
|
||||
@@ -1829,6 +1830,7 @@ function InlineTool(props: {
|
||||
|
||||
return (
|
||||
<InlineToolRow
|
||||
id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.id}`}
|
||||
icon={props.icon}
|
||||
iconColor={props.iconColor}
|
||||
color={fg()}
|
||||
@@ -1840,6 +1842,7 @@ function InlineTool(props: {
|
||||
complete={props.complete}
|
||||
pending={props.pending}
|
||||
spinner={props.spinner}
|
||||
subagent={props.subagent}
|
||||
separateAfter={(id) =>
|
||||
sync.data.message[ctx.sessionID]?.some((message) => message.role === "user" && message.id === id) ?? false
|
||||
}
|
||||
@@ -1860,6 +1863,7 @@ function InlineTool(props: {
|
||||
}
|
||||
|
||||
export function InlineToolRow(props: {
|
||||
id?: string
|
||||
icon: string
|
||||
iconColor?: RGBA
|
||||
color?: RGBA
|
||||
@@ -1871,6 +1875,7 @@ export function InlineToolRow(props: {
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
subagent?: boolean
|
||||
children: JSX.Element
|
||||
separateAfter?: (id: string | undefined) => boolean
|
||||
onMouseOver?: () => void
|
||||
@@ -1881,6 +1886,7 @@ export function InlineToolRow(props: {
|
||||
|
||||
return (
|
||||
<box
|
||||
id={props.id}
|
||||
marginTop={margin()}
|
||||
paddingLeft={3}
|
||||
onMouseOver={props.onMouseOver}
|
||||
@@ -1895,9 +1901,12 @@ export function InlineToolRow(props: {
|
||||
const children = parent.getChildren()
|
||||
const index = children.indexOf(el)
|
||||
const previous = children[index - 1]
|
||||
const previousInline = previous?.id.startsWith("tool-inline-") ?? false
|
||||
const previousSubagent = previous?.id.startsWith("tool-inline-subagent-") ?? false
|
||||
setMargin(
|
||||
previous?.id.startsWith("text-") ||
|
||||
previous?.id.startsWith("tool-block-") ||
|
||||
(previousInline && previousSubagent !== Boolean(props.subagent)) ||
|
||||
props.separateAfter?.(previous?.id)
|
||||
? 1
|
||||
: 0,
|
||||
@@ -2123,8 +2132,8 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
||||
Read {pathFormatter.format(props.input.filePath)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<box paddingLeft={3}>
|
||||
{(filepath, index) => (
|
||||
<box id={`tool-inline-loaded-${props.part.id}-${index()}`} paddingLeft={3}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
↳ Loaded {pathFormatter.format(filepath)}
|
||||
</text>
|
||||
@@ -2190,11 +2199,14 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
tools().findLast((x) => (x.state.status === "running" || x.state.status === "completed") && x.state.title),
|
||||
)
|
||||
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const status = createMemo(() => sync.data.session_status[props.metadata.sessionId ?? ""])
|
||||
const isRunning = createMemo(
|
||||
() => props.part.state.status === "running" || (props.metadata.background === true && status() !== undefined),
|
||||
)
|
||||
const retry = createMemo(() => {
|
||||
const status = sync.data.session_status[props.metadata.sessionId ?? ""]
|
||||
if (status?.type !== "retry") return
|
||||
return status
|
||||
const value = status()
|
||||
if (value?.type !== "retry") return
|
||||
return value
|
||||
})
|
||||
|
||||
const duration = createMemo(() => {
|
||||
@@ -2214,20 +2226,15 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
if (isRunning() && retrying) {
|
||||
content.push(`↳ ${Locale.truncate(retrying.message, 80)} [retrying attempt #${retrying.attempt}]`)
|
||||
} else if (isRunning() && tools().length > 0) {
|
||||
// content[0] += ` · ${tools().length} toolcalls`
|
||||
if (current()) {
|
||||
const state = current()!.state
|
||||
const title = state.status === "running" || state.status === "completed" ? state.title : undefined
|
||||
content.push(`↳ ${Locale.titlecase(current()!.tool)} ${title}`)
|
||||
} else content.push(`↳ ${tools().length} toolcalls`)
|
||||
} else content.push(`↳ ${formatSubagentToolcalls(tools().length)}`)
|
||||
}
|
||||
|
||||
if (props.part.state.status === "completed") {
|
||||
content.push(
|
||||
props.metadata.background === true
|
||||
? `└ ${tools().length} toolcalls`
|
||||
: `└ ${tools().length} toolcalls · ${Locale.duration(duration())}`,
|
||||
)
|
||||
if (!isRunning() && props.part.state.status === "completed") {
|
||||
content.push(`↳ ${formatCompletedSubagentDetail(tools().length, Locale.duration(duration()))}`)
|
||||
}
|
||||
|
||||
return content.join("\n")
|
||||
@@ -2235,7 +2242,8 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
|
||||
return (
|
||||
<InlineTool
|
||||
icon="│"
|
||||
icon={props.part.state.status === "completed" ? "✓" : "│"}
|
||||
subagent={true}
|
||||
color={retry() ? theme.error : undefined}
|
||||
spinner={isRunning()}
|
||||
complete={props.input.description}
|
||||
@@ -2254,6 +2262,15 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function formatSubagentToolcalls(count: number) {
|
||||
return `${count} toolcall${count === 1 ? "" : "s"}`
|
||||
}
|
||||
|
||||
export function formatCompletedSubagentDetail(toolcalls: number, duration: string) {
|
||||
if (toolcalls === 0) return duration
|
||||
return `${formatSubagentToolcalls(toolcalls)} · ${duration}`
|
||||
}
|
||||
|
||||
function Edit(props: ToolProps<typeof EditTool>) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
|
||||
@@ -52,3 +52,21 @@ exports[`TUI inline tool wrapping keeps separation after a padded user message 1
|
||||
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
|
||||
Path\\.data|data =" in packages/opencode/src (115 matches)"
|
||||
`;
|
||||
|
||||
exports[`TUI inline tool wrapping separates a contiguous subagent group from inline tools 1`] = `
|
||||
" ✱ Grep "Task" (2 matches)
|
||||
|
||||
⠙ Explore Task — Inspect active task spacing
|
||||
✓ General Task — Confirm completed task spacing
|
||||
↳ 1 toolcall · 501ms
|
||||
|
||||
→ Read src/cli/cmd/tui/routes/session/index.tsx"
|
||||
`;
|
||||
|
||||
exports[`TUI inline tool wrapping separates a subagent group after an expanded read 1`] = `
|
||||
" → Read src/cli/cmd/tui/routes/session/index.tsx
|
||||
↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx
|
||||
|
||||
✓ Explore Task — Inspect active task spacing
|
||||
↳ 1 toolcall · 501ms"
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { For } from "solid-js"
|
||||
import { testRender, type JSX } from "@opentui/solid"
|
||||
import { InlineToolRow } from "../../../src/cli/cmd/tui/routes/session/index"
|
||||
import {
|
||||
formatCompletedSubagentDetail,
|
||||
formatSubagentToolcalls,
|
||||
InlineToolRow,
|
||||
} from "../../../src/cli/cmd/tui/routes/session/index"
|
||||
|
||||
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined
|
||||
|
||||
@@ -86,6 +90,41 @@ function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" })
|
||||
)
|
||||
}
|
||||
|
||||
function SubagentGroupFixture() {
|
||||
return (
|
||||
<box flexDirection="column" width={72}>
|
||||
<InlineToolRow id="tool-inline-before" icon="✱" complete={true} pending="">
|
||||
Grep "Task" (2 matches)
|
||||
</InlineToolRow>
|
||||
<InlineToolRow id="tool-inline-subagent-one" icon="⠙" complete={true} pending="" subagent={true}>
|
||||
Explore Task — Inspect active task spacing
|
||||
</InlineToolRow>
|
||||
<InlineToolRow id="tool-inline-subagent-two" icon="✓" complete={true} pending="" subagent={true}>
|
||||
{"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"}
|
||||
</InlineToolRow>
|
||||
<InlineToolRow id="tool-inline-after" icon="→" complete={true} pending="">
|
||||
Read src/cli/cmd/tui/routes/session/index.tsx
|
||||
</InlineToolRow>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadedReadBeforeSubagentFixture() {
|
||||
return (
|
||||
<box flexDirection="column" width={72}>
|
||||
<InlineToolRow id="tool-inline-read" icon="→" complete={true} pending="">
|
||||
Read src/cli/cmd/tui/routes/session/index.tsx
|
||||
</InlineToolRow>
|
||||
<box id="tool-inline-loaded-read-child" paddingLeft={3}>
|
||||
<text paddingLeft={3}>↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx</text>
|
||||
</box>
|
||||
<InlineToolRow id="tool-inline-subagent-after-read" icon="✓" complete={true} pending="" subagent={true}>
|
||||
{"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"}
|
||||
</InlineToolRow>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
|
||||
testSetup = await testRender(component, options)
|
||||
await testSetup.renderOnce()
|
||||
@@ -101,6 +140,13 @@ async function renderFrame(component: () => JSX.Element, options: { width: numbe
|
||||
}
|
||||
|
||||
describe("TUI inline tool wrapping", () => {
|
||||
test("formats completed subagent toolcall details", () => {
|
||||
expect(formatCompletedSubagentDetail(0, "501ms")).toBe("501ms")
|
||||
expect(formatCompletedSubagentDetail(1, "501ms")).toBe("1 toolcall · 501ms")
|
||||
expect(formatCompletedSubagentDetail(2, "501ms")).toBe("2 toolcalls · 501ms")
|
||||
expect(formatSubagentToolcalls(0)).toBe("0 toolcalls")
|
||||
})
|
||||
|
||||
test("snapshots consecutive grep, glob, and read rows at a narrow width", async () => {
|
||||
expect(await renderFrame(() => <Fixture />, { width: 72, height: 12 })).toMatchSnapshot()
|
||||
})
|
||||
@@ -116,4 +162,12 @@ describe("TUI inline tool wrapping", () => {
|
||||
test("keeps separation after a padded user message", async () => {
|
||||
expect(await renderFrame(() => <Fixture before="user" />, { width: 72, height: 14 })).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("separates a contiguous subagent group from inline tools", async () => {
|
||||
expect(await renderFrame(() => <SubagentGroupFixture />, { width: 72, height: 10 })).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("separates a subagent group after an expanded read", async () => {
|
||||
expect(await renderFrame(() => <LoadedReadBeforeSubagentFixture />, { width: 72, height: 8 })).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user