Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions packages/codeplane/src/tui/component/spinner.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={color()}>⋯ {props.children}</text>}>
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={color()}>⋯ {label()}</text>}>
Comment on lines 13 to +16
<box flexDirection="row" gap={1}>
<spinner frames={frames} interval={80} color={color()} />
<Show when={props.children}>
<text fg={color()}>{props.children}</text>
<Show when={label()}>
<text fg={color()}>{label()}</text>
</Show>
</box>
</Show>
Expand All @@ -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 <text fg={color()}>⋯ {props.label}</text>
return <text fg={color()}>⋯ {textValue(props.label)}</text>
}
90 changes: 60 additions & 30 deletions packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -518,7 +519,7 @@ function InlineTool(props: {
complete: unknown
pending: string
spinner?: boolean
children: JSX.Element
children: string
part: SessionMessageAssistantTool
agentColor?: RGBA
}) {
Expand Down Expand Up @@ -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 (
<InlineTool icon="✱" pending="Finding files..." complete={toolComplete(props.part)} part={props.part} agentColor={props.agentColor}>
Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
<Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
<Show when={numberValue(props.metadata.count)}>
{(count) => (
<>
({count()} {count() === 1 ? "match" : "matches"})
</>
)}
</Show>
{label()}
</InlineTool>
)
}
Expand All @@ -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()}
</InlineTool>
<For each={loaded()}>
{(filepath) => (
Expand All @@ -694,43 +694,57 @@ 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 (
<InlineTool icon="✱" pending="Searching content..." complete={toolComplete(props.part)} part={props.part} agentColor={props.agentColor}>
Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
<Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
<Show when={numberValue(props.metadata.matches)}>
{(matches) => (
<>
({matches()} {matches() === 1 ? "match" : "matches"})
</>
)}
</Show>
{label()}
</InlineTool>
)
}

function WebFetch(props: ToolProps) {
return (
<InlineTool icon="%" pending="Fetching from the web..." complete={toolComplete(props.part)} part={props.part} agentColor={props.agentColor}>
WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)}
{`WebFetch ${valueText(props.input.url, pendingInput(props.part))}`}
</InlineTool>
)
}

function CodeSearch(props: ToolProps) {
const label = createMemo(
() =>
`Exa Code Search "${valueText(props.input.query, pendingInput(props.part))}"${countText(
props.metadata.results,
"result",
"results",
)}`,
)
return (
<InlineTool icon="◇" pending="Searching code..." complete={toolComplete(props.part)} part={props.part} agentColor={props.agentColor}>
Exa Code Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
<Show when={numberValue(props.metadata.results)}>{(results) => <>({results()} results)</>}</Show>
{label()}
</InlineTool>
)
}

function WebSearch(props: ToolProps) {
const label = createMemo(
() =>
`Exa Web Search "${valueText(props.input.query, pendingInput(props.part))}"${countText(
props.metadata.numResults,
"result",
"results",
)}`,
)
return (
<InlineTool icon="◈" pending="Searching web..." complete={toolComplete(props.part)} part={props.part} agentColor={props.agentColor}>
Exa Web Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
<Show when={numberValue(props.metadata.numResults)}>{(results) => <>({results()} results)</>}</Show>
{label()}
</InlineTool>
)
}
Expand All @@ -757,7 +771,7 @@ function Write(props: ToolProps) {
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing write..." complete={filePath()} part={props.part} agentColor={props.agentColor}>
Write {normalizePath(filePath())}
{`Write ${normalizePath(filePath())}`}
</InlineTool>
</Match>
</Switch>
Expand Down Expand Up @@ -801,7 +815,7 @@ function Edit(props: ToolProps) {
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing edit..." complete={filePath()} part={props.part} agentColor={props.agentColor}>
Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })}
{`Edit ${normalizePath(filePath())} ${input({ replaceAll: props.input.replaceAll })}`.trimEnd()}
</InlineTool>
</Match>
</Switch>
Expand Down Expand Up @@ -922,7 +936,7 @@ function Question(props: ToolProps) {
</Match>
<Match when={true}>
<InlineTool icon="→" pending="Asking questions..." complete={questions().length} part={props.part} agentColor={props.agentColor}>
Asked {questions().length} question{questions().length === 1 ? "" : "s"}
{`Asked ${questions().length} question${questions().length === 1 ? "" : "s"}`}
</InlineTool>
</Match>
</Switch>
Expand All @@ -932,7 +946,7 @@ function Question(props: ToolProps) {
function Skill(props: ToolProps) {
return (
<InlineTool icon="→" pending="Loading skill..." complete={toolComplete(props.part)} part={props.part} agentColor={props.agentColor}>
Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}"
{`Skill "${valueText(props.input.name, pendingInput(props.part))}"`}
</InlineTool>
)
}
Expand Down Expand Up @@ -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
}
Comment on lines +1020 to +1022

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
}
Expand Down
42 changes: 26 additions & 16 deletions packages/codeplane/src/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1998,7 +1998,7 @@ function InlineTool(props: {
complete: any
pending: string
spinner?: boolean
children: JSX.Element
children: string
part: ToolPart
agentColor?: RGBA
onClick?: () => void
Expand Down Expand Up @@ -2219,20 +2219,22 @@ function Write(props: ToolProps<typeof WriteTool>) {
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing write..." complete={props.input.filePath} part={props.part}>
Write {normalizePath(props.input.filePath!)}
{`Write ${normalizePath(props.input.filePath)}`}
</InlineTool>
</Match>
</Switch>
)
}

function Glob(props: ToolProps<typeof GlobTool>) {
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 (
<InlineTool icon="✱" pending="Finding files..." complete={props.input.pattern} part={props.part}>
Glob "{textValue(props.input.pattern)}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
<Show when={props.metadata.count}>
({props.metadata.count} {props.metadata.count === 1 ? "match" : "matches"})
</Show>
{label()}
</InlineTool>
)
}
Expand All @@ -2256,7 +2258,7 @@ function Read(props: ToolProps<typeof ReadTool>) {
spinner={isRunning()}
part={props.part}
>
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
{`Read ${normalizePath(props.input.filePath)} ${input(props.input, ["filePath"])}`.trimEnd()}
</InlineTool>
<For each={loaded()}>
{(filepath) => (
Expand All @@ -2272,29 +2274,37 @@ function Read(props: ToolProps<typeof ReadTool>) {
}

function Grep(props: ToolProps<typeof GrepTool>) {
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 (
<InlineTool icon="✱" pending="Searching content..." complete={props.input.pattern} part={props.part}>
Grep "{textValue(props.input.pattern)}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
<Show when={props.metadata.matches}>
({props.metadata.matches} {props.metadata.matches === 1 ? "match" : "matches"})
</Show>
{label()}
</InlineTool>
)
}

function WebFetch(props: ToolProps<typeof WebFetchTool>) {
return (
<InlineTool icon="%" pending="Fetching from the web..." complete={props.input.url} part={props.part}>
WebFetch {textValue(props.input.url)}
{`WebFetch ${textValue(props.input.url)}`}
</InlineTool>
)
}

function WebSearch(props: ToolProps<typeof WebSearchTool>) {
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 (
<InlineTool icon="◈" pending="Searching web..." complete={props.input.query} part={props.part}>
Exa Web Search "{textValue(props.input.query)}" <Show when={metadata.numResults}>({metadata.numResults} results)</Show>
{label()}
</InlineTool>
)
}
Expand Down Expand Up @@ -2414,7 +2424,7 @@ function Edit(props: ToolProps<typeof EditTool>) {
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing edit..." complete={props.input.filePath} part={props.part}>
Edit {normalizePath(props.input.filePath!)} {input({ replaceAll: props.input.replaceAll })}
{`Edit ${normalizePath(props.input.filePath)} ${input({ replaceAll: props.input.replaceAll })}`.trimEnd()}
</InlineTool>
</Match>
</Switch>
Expand Down Expand Up @@ -2544,7 +2554,7 @@ function Question(props: ToolProps<typeof QuestionTool>) {
</Match>
<Match when={true}>
<InlineTool icon="→" pending="Asking questions..." complete={count()} part={props.part}>
Asked {count()} question{count() !== 1 ? "s" : ""}
{`Asked ${count()} question${count() !== 1 ? "s" : ""}`}
</InlineTool>
</Match>
</Switch>
Expand All @@ -2554,7 +2564,7 @@ function Question(props: ToolProps<typeof QuestionTool>) {
function Skill(props: ToolProps<typeof SkillTool>) {
return (
<InlineTool icon="→" pending="Loading skill..." complete={props.input.name} part={props.part}>
Skill "{textValue(props.input.name)}"
{`Skill "${textValue(props.input.name)}"`}
</InlineTool>
)
}
Expand Down
Loading