diff --git a/packages/codeplane/src/tui/component/spinner.tsx b/packages/codeplane/src/tui/component/spinner.tsx index 0c236aaa5..60880eeb2 100644 --- a/packages/codeplane/src/tui/component/spinner.tsx +++ b/packages/codeplane/src/tui/component/spinner.tsx @@ -1,22 +1,23 @@ import { Show } from "solid-js" import { useTheme } from "../context/theme" import { useKV } from "../context/kv" -import type { JSX } from "@opentui/solid" import type { RGBA } from "@opentui/core" import "opentui-spinner/solid" +import { textValue } from "@/tui/util/text-value" const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] -export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { +export function Spinner(props: { children?: unknown; color?: RGBA }) { const { theme } = useTheme() const kv = useKV() const color = () => props.color ?? theme.textMuted + const label = () => textValue(props.children) return ( - ⋯ {props.children}}> + ⋯ {label()}}> - - {props.children} + + {label()} @@ -32,8 +33,8 @@ export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { * in favour of this plain, legible indicator. `seed` is accepted for call-site * compatibility but no longer used. */ -export function PendingAnimation(props: { label: JSX.Element; seed?: string; color?: RGBA }) { +export function PendingAnimation(props: { label: unknown; seed?: string; color?: RGBA }) { const { theme } = useTheme() const color = () => props.color ?? theme.textMuted - return ⋯ {props.label} + return ⋯ {textValue(props.label)} } diff --git a/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx b/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx index ea517255a..f196f16aa 100644 --- a/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx +++ b/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx @@ -9,6 +9,7 @@ import { useLocal } from "@/tui/context/local" import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { RGBA, SyntaxStyle } from "@opentui/core" import { Locale } from "@/tui/_compat/locale" +import { textValue } from "@/tui/util/text-value" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import path from "path" import stripAnsi from "strip-ansi" @@ -518,7 +519,7 @@ function InlineTool(props: { complete: unknown pending: string spinner?: boolean - children: JSX.Element + children: string part: SessionMessageAssistantTool agentColor?: RGBA }) { @@ -647,17 +648,17 @@ function Bash(props: ToolProps) { } function Glob(props: ToolProps) { + const label = createMemo(() => { + const path = stringValue(props.input.path) + return `Glob "${valueText(props.input.pattern, pendingInput(props.part))}"${path ? ` in ${normalizePath(path)} ` : ""}${countText( + props.metadata.count, + "match", + "matches", + )}` + }) return ( - Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "} - in {normalizePath(stringValue(props.input.path))} - - {(count) => ( - <> - ({count()} {count() === 1 ? "match" : "matches"}) - - )} - + {label()} ) } @@ -677,8 +678,7 @@ function Read(props: ToolProps) { part={props.part} agentColor={props.agentColor} > - Read {normalizePath(stringValue(props.input.filePath) ?? pendingInput(props.part))}{" "} - {input(props.input, ["filePath"])} + {`Read ${pathText(props.input.filePath, pendingInput(props.part))} ${input(props.input, ["filePath"])}`.trimEnd()} {(filepath) => ( @@ -694,17 +694,17 @@ function Read(props: ToolProps) { } function Grep(props: ToolProps) { + const label = createMemo(() => { + const path = stringValue(props.input.path) + return `Grep "${valueText(props.input.pattern, pendingInput(props.part))}"${path ? ` in ${normalizePath(path)} ` : ""}${countText( + props.metadata.matches, + "match", + "matches", + )}` + }) return ( - Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "} - in {normalizePath(stringValue(props.input.path))} - - {(matches) => ( - <> - ({matches()} {matches() === 1 ? "match" : "matches"}) - - )} - + {label()} ) } @@ -712,25 +712,39 @@ function Grep(props: ToolProps) { function WebFetch(props: ToolProps) { return ( - WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)} + {`WebFetch ${valueText(props.input.url, pendingInput(props.part))}`} ) } function CodeSearch(props: ToolProps) { + const label = createMemo( + () => + `Exa Code Search "${valueText(props.input.query, pendingInput(props.part))}"${countText( + props.metadata.results, + "result", + "results", + )}`, + ) return ( - Exa Code Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "} - {(results) => <>({results()} results)} + {label()} ) } function WebSearch(props: ToolProps) { + const label = createMemo( + () => + `Exa Web Search "${valueText(props.input.query, pendingInput(props.part))}"${countText( + props.metadata.numResults, + "result", + "results", + )}`, + ) return ( - Exa Web Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "} - {(results) => <>({results()} results)} + {label()} ) } @@ -757,7 +771,7 @@ function Write(props: ToolProps) { - Write {normalizePath(filePath())} + {`Write ${normalizePath(filePath())}`} @@ -801,7 +815,7 @@ function Edit(props: ToolProps) { - Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })} + {`Edit ${normalizePath(filePath())} ${input({ replaceAll: props.input.replaceAll })}`.trimEnd()} @@ -922,7 +936,7 @@ function Question(props: ToolProps) { - Asked {questions().length} question{questions().length === 1 ? "" : "s"} + {`Asked ${questions().length} question${questions().length === 1 ? "" : "s"}`} @@ -932,7 +946,7 @@ function Question(props: ToolProps) { function Skill(props: ToolProps) { return ( - Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}" + {`Skill "${valueText(props.input.name, pendingInput(props.part))}"`} ) } @@ -1003,6 +1017,22 @@ function toolComplete(part: SessionMessageAssistantTool) { return part.state.status === "completed" || part.state.status === "error" || part.state.status === "running" } +function valueText(value: unknown, fallback = "") { + return textValue(value) || fallback +} + +function pathText(value: unknown, fallback = "") { + const path = stringValue(value) + if (path) return normalizePath(path) + return valueText(value, fallback) +} + +function countText(value: unknown, singular: string, plural: string) { + const count = numberValue(value) + if (!count) return "" + return ` (${count} ${count === 1 ? singular : plural})` +} + function stringValue(value: unknown) { return typeof value === "string" ? value : undefined } diff --git a/packages/codeplane/src/tui/routes/session/index.tsx b/packages/codeplane/src/tui/routes/session/index.tsx index 09dbba681..2a5d88d3a 100644 --- a/packages/codeplane/src/tui/routes/session/index.tsx +++ b/packages/codeplane/src/tui/routes/session/index.tsx @@ -1998,7 +1998,7 @@ function InlineTool(props: { complete: any pending: string spinner?: boolean - children: JSX.Element + children: string part: ToolPart agentColor?: RGBA onClick?: () => void @@ -2219,7 +2219,7 @@ function Write(props: ToolProps) { - Write {normalizePath(props.input.filePath!)} + {`Write ${normalizePath(props.input.filePath)}`} @@ -2227,12 +2227,14 @@ function Write(props: ToolProps) { } function Glob(props: ToolProps) { + const label = createMemo(() => { + const path = props.input.path ? ` in ${normalizePath(props.input.path)} ` : "" + const count = props.metadata.count ? ` (${props.metadata.count} ${props.metadata.count === 1 ? "match" : "matches"})` : "" + return `Glob "${textValue(props.input.pattern)}"${path}${count}` + }) return ( - Glob "{textValue(props.input.pattern)}" in {normalizePath(props.input.path)} - - ({props.metadata.count} {props.metadata.count === 1 ? "match" : "matches"}) - + {label()} ) } @@ -2256,7 +2258,7 @@ function Read(props: ToolProps) { spinner={isRunning()} part={props.part} > - Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])} + {`Read ${normalizePath(props.input.filePath)} ${input(props.input, ["filePath"])}`.trimEnd()} {(filepath) => ( @@ -2272,12 +2274,16 @@ function Read(props: ToolProps) { } function Grep(props: ToolProps) { + const label = createMemo(() => { + const path = props.input.path ? ` in ${normalizePath(props.input.path)} ` : "" + const matches = props.metadata.matches + ? ` (${props.metadata.matches} ${props.metadata.matches === 1 ? "match" : "matches"})` + : "" + return `Grep "${textValue(props.input.pattern)}"${path}${matches}` + }) return ( - Grep "{textValue(props.input.pattern)}" in {normalizePath(props.input.path)} - - ({props.metadata.matches} {props.metadata.matches === 1 ? "match" : "matches"}) - + {label()} ) } @@ -2285,16 +2291,20 @@ function Grep(props: ToolProps) { function WebFetch(props: ToolProps) { return ( - WebFetch {textValue(props.input.url)} + {`WebFetch ${textValue(props.input.url)}`} ) } function WebSearch(props: ToolProps) { const metadata = props.metadata as { numResults?: number } + const label = createMemo(() => { + const results = metadata.numResults ? ` (${metadata.numResults} results)` : "" + return `Exa Web Search "${textValue(props.input.query)}"${results}` + }) return ( - Exa Web Search "{textValue(props.input.query)}" ({metadata.numResults} results) + {label()} ) } @@ -2414,7 +2424,7 @@ function Edit(props: ToolProps) { - Edit {normalizePath(props.input.filePath!)} {input({ replaceAll: props.input.replaceAll })} + {`Edit ${normalizePath(props.input.filePath)} ${input({ replaceAll: props.input.replaceAll })}`.trimEnd()} @@ -2544,7 +2554,7 @@ function Question(props: ToolProps) { - Asked {count()} question{count() !== 1 ? "s" : ""} + {`Asked ${count()} question${count() !== 1 ? "s" : ""}`} @@ -2554,7 +2564,7 @@ function Question(props: ToolProps) { function Skill(props: ToolProps) { return ( - Skill "{textValue(props.input.name)}" + {`Skill "${textValue(props.input.name)}"`} ) }