feat(tui): style background subagent indicator (#36751)
This commit is contained in:
parent
fbe7f26e71
commit
ce2e301e24
3 changed files with 89 additions and 28 deletions
|
|
@ -14,7 +14,10 @@ export function Spinner(props: { children?: JSX.Element; color?: RGBA }) {
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
const color = () => props.color ?? theme.textMuted
|
const color = () => props.color ?? theme.textMuted
|
||||||
return (
|
return (
|
||||||
<Show when={config.animations ?? true} fallback={<text fg={color()}>⋯ {props.children}</text>}>
|
<Show
|
||||||
|
when={config.animations ?? true}
|
||||||
|
fallback={<text fg={color()}>{props.children ? <>⋯ {props.children}</> : "⋯"}</text>}
|
||||||
|
>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
||||||
<Show when={props.children}>
|
<Show when={props.children}>
|
||||||
|
|
|
||||||
|
|
@ -1978,6 +1978,7 @@ function InlineTool(props: {
|
||||||
pending: string
|
pending: string
|
||||||
failure?: string
|
failure?: string
|
||||||
spinner?: boolean
|
spinner?: boolean
|
||||||
|
status?: JSX.Element
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
part: SessionMessageAssistantTool
|
part: SessionMessageAssistantTool
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
|
|
@ -2028,6 +2029,7 @@ function InlineTool(props: {
|
||||||
pending={props.pending}
|
pending={props.pending}
|
||||||
failure={props.failure}
|
failure={props.failure}
|
||||||
spinner={props.spinner}
|
spinner={props.spinner}
|
||||||
|
status={props.status}
|
||||||
onMouseOver={() => clickable() && setHover(true)}
|
onMouseOver={() => clickable() && setHover(true)}
|
||||||
onMouseOut={() => setHover(false)}
|
onMouseOut={() => setHover(false)}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
|
|
@ -2057,6 +2059,7 @@ export function InlineToolRow(props: {
|
||||||
pending: string
|
pending: string
|
||||||
failure?: string
|
failure?: string
|
||||||
spinner?: boolean
|
spinner?: boolean
|
||||||
|
status?: JSX.Element
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
onMouseOver?: () => void
|
onMouseOver?: () => void
|
||||||
onMouseOut?: () => void
|
onMouseOut?: () => void
|
||||||
|
|
@ -2066,7 +2069,16 @@ export function InlineToolRow(props: {
|
||||||
<box paddingLeft={3} onMouseOver={props.onMouseOver} onMouseOut={props.onMouseOut} onMouseUp={props.onMouseUp}>
|
<box paddingLeft={3} onMouseOver={props.onMouseOver} onMouseOut={props.onMouseOut} onMouseUp={props.onMouseUp}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.spinner}>
|
<Match when={props.spinner}>
|
||||||
<Spinner color={props.color} children={props.children} />
|
<Show when={props.status} fallback={<Spinner color={props.color} children={props.children} />}>
|
||||||
|
{(status) => (
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Spinner color={props.color} />
|
||||||
|
<InlineToolLabel color={props.color} status={status()}>
|
||||||
|
{props.children}
|
||||||
|
</InlineToolLabel>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Show fallback={<Spinner color={props.color}>{props.pending}</Spinner>} when={props.complete || props.failed}>
|
<Show fallback={<Spinner color={props.color}>{props.pending}</Spinner>} when={props.complete || props.failed}>
|
||||||
|
|
@ -2078,13 +2090,28 @@ export function InlineToolRow(props: {
|
||||||
>
|
>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
</text>
|
</text>
|
||||||
<text
|
<Show
|
||||||
flexGrow={1}
|
when={props.status}
|
||||||
fg={props.failed ? props.errorColor : props.color}
|
fallback={
|
||||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
<text
|
||||||
|
flexGrow={1}
|
||||||
|
fg={props.failed ? props.errorColor : props.color}
|
||||||
|
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||||
|
>
|
||||||
|
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||||
|
</text>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
{(status) => (
|
||||||
</text>
|
<InlineToolLabel
|
||||||
|
color={props.failed ? props.errorColor : props.color}
|
||||||
|
denied={props.denied}
|
||||||
|
status={status()}
|
||||||
|
>
|
||||||
|
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||||
|
</InlineToolLabel>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
@ -2098,6 +2125,32 @@ export function InlineToolRow(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.Element; children: JSX.Element }) {
|
||||||
|
return (
|
||||||
|
<box flexDirection="row" flexWrap="wrap" columnGap={1} flexGrow={1}>
|
||||||
|
<text
|
||||||
|
maxWidth="100%"
|
||||||
|
flexShrink={0}
|
||||||
|
fg={props.color}
|
||||||
|
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</text>
|
||||||
|
{props.status}
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge(props: { children: string }) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
return (
|
||||||
|
<text flexShrink={0} bg={theme.backgroundElement} fg={theme.textMuted}>
|
||||||
|
{" "}
|
||||||
|
{props.children}{" "}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function BlockTool(props: {
|
function BlockTool(props: {
|
||||||
title?: string
|
title?: string
|
||||||
path?: { label: string; value: string }
|
path?: { label: string; value: string }
|
||||||
|
|
@ -2241,9 +2294,7 @@ function Shell(props: ToolProps) {
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={shellID()}>
|
<Show when={shellID()}>
|
||||||
<text>
|
<StatusBadge>Background</StatusBadge>
|
||||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> Background </span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={collapsed().overflow}>
|
<Show when={collapsed().overflow}>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||||
|
|
@ -2386,20 +2437,17 @@ function Subagent(props: ToolProps) {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
if (id) navigate({ type: "session", sessionID: id })
|
if (id) navigate({ type: "session", sessionID: id })
|
||||||
}}
|
}}
|
||||||
|
status={
|
||||||
|
props.input.background === true || props.metadata.status === "running" ? (
|
||||||
|
<StatusBadge>Background</StatusBadge>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{formatSubagentTitle(
|
{`${Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General")} Subagent — ${description() ?? "Subagent"}`}
|
||||||
Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General"),
|
|
||||||
description() ?? "Subagent",
|
|
||||||
props.input.background === true || props.metadata.status === "running",
|
|
||||||
)}
|
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSubagentTitle(agent: string, description: string, background: boolean) {
|
|
||||||
return `${agent} Subagent — ${description}${background ? " [background]" : ""}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatSubagentRetry(attempt: number, message: string) {
|
export function formatSubagentRetry(attempt: number, message: string) {
|
||||||
return `Retrying (attempt ${attempt}) · ${message}`
|
return `Retrying (attempt ${attempt}) · ${message}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { For } from "solid-js"
|
||||||
import { testRender, type JSX } from "@opentui/solid"
|
import { testRender, type JSX } from "@opentui/solid"
|
||||||
import {
|
import {
|
||||||
formatSubagentRetry,
|
formatSubagentRetry,
|
||||||
formatSubagentTitle,
|
|
||||||
InlineToolRow,
|
InlineToolRow,
|
||||||
parseApplyPatchFiles,
|
parseApplyPatchFiles,
|
||||||
parseDiagnostics,
|
parseDiagnostics,
|
||||||
|
|
@ -99,7 +98,16 @@ function ReminderAlignmentFixture() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TrailingStatusFixture() {
|
||||||
|
return (
|
||||||
|
<InlineToolRow icon=":" complete={true} pending="" status={<text flexShrink={0}> Background </text>}>
|
||||||
|
Explore Subagent — Inspect renderer status styling
|
||||||
|
</InlineToolRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
|
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
|
||||||
|
testSetup?.renderer.destroy()
|
||||||
testSetup = await testRender(component, options)
|
testSetup = await testRender(component, options)
|
||||||
await testSetup.renderOnce()
|
await testSetup.renderOnce()
|
||||||
await testSetup.renderOnce()
|
await testSetup.renderOnce()
|
||||||
|
|
@ -142,6 +150,15 @@ describe("TUI inline tool wrapping", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("wraps a trailing status as one padded item", async () => {
|
||||||
|
expect(await renderFrame(() => <TrailingStatusFixture />, { width: 70, height: 2 })).toBe(
|
||||||
|
" : Explore Subagent — Inspect renderer status styling Background",
|
||||||
|
)
|
||||||
|
expect(await renderFrame(() => <TrailingStatusFixture />, { width: 62, height: 2 })).toBe(
|
||||||
|
" : Explore Subagent — Inspect renderer status styling\n Background",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("filters malformed nested tool wire data", () => {
|
test("filters malformed nested tool wire data", () => {
|
||||||
expect(
|
expect(
|
||||||
parseApplyPatchFiles([
|
parseApplyPatchFiles([
|
||||||
|
|
@ -180,13 +197,6 @@ describe("TUI inline tool wrapping", () => {
|
||||||
).toEqual([{ message: "valid", range: { start: { line: 2, character: 3 } } }])
|
).toEqual([{ message: "valid", range: { start: { line: 2, character: 3 } } }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps background state attached to the subagent identity", () => {
|
|
||||||
expect(formatSubagentTitle("Explore", "Inspect renderer", false)).toBe("Explore Subagent — Inspect renderer")
|
|
||||||
expect(formatSubagentTitle("Explore", "Inspect renderer", true)).toBe(
|
|
||||||
"Explore Subagent — Inspect renderer [background]",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps retry status ahead of wrapping messages", () => {
|
test("keeps retry status ahead of wrapping messages", () => {
|
||||||
expect(formatSubagentRetry(2, "Rate limited by provider")).toBe("Retrying (attempt 2) · Rate limited by provider")
|
expect(formatSubagentRetry(2, "Rate limited by provider")).toBe("Retrying (attempt 2) · Rate limited by provider")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue