mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-20 05:54:01 +00:00
feat: bash tool UI component (#890)
This commit is contained in:
parent
57daef335d
commit
654386c644
1 changed files with 120 additions and 7 deletions
|
|
@ -4,17 +4,18 @@ import { useState } from "react"
|
|||
import type { UIMessage } from "@ai-sdk/react"
|
||||
import { Streamdown } from "streamdown"
|
||||
import {
|
||||
BookOpenIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
Loader2,
|
||||
SearchIcon,
|
||||
GlobeIcon,
|
||||
PlusIcon,
|
||||
BookOpenIcon,
|
||||
ClockIcon,
|
||||
GlobeIcon,
|
||||
ListIcon,
|
||||
XCircleIcon,
|
||||
Loader2,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
TerminalIcon,
|
||||
WrenchIcon,
|
||||
XCircleIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { isWebSearchToolName } from "@/lib/chat-web-search-tools"
|
||||
|
|
@ -22,9 +23,11 @@ import { RelatedMemories } from "./related-memories"
|
|||
import { MessageActions } from "./message-actions"
|
||||
|
||||
const TOOL_META: Record<string, { label: string; icon: typeof SearchIcon }> = {
|
||||
searchMemories: { label: "Search Memories", icon: SearchIcon },
|
||||
bash: { label: "Memory", icon: TerminalIcon },
|
||||
web_search: { label: "Web search", icon: GlobeIcon },
|
||||
google_search: { label: "Google search", icon: GlobeIcon },
|
||||
// legacy tool names kept for existing persisted messages
|
||||
searchMemories: { label: "Search Memories", icon: SearchIcon },
|
||||
addMemory: { label: "Add Memory", icon: PlusIcon },
|
||||
fetchMemory: { label: "Fetch Memory", icon: BookOpenIcon },
|
||||
scheduleTask: { label: "Schedule Task", icon: ClockIcon },
|
||||
|
|
@ -95,9 +98,119 @@ function WebSourcesGroup({ sources }: { sources: SourceUrlPart[] }) {
|
|||
)
|
||||
}
|
||||
|
||||
function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const isLoading =
|
||||
part.state === "input-streaming" || part.state === "input-available"
|
||||
const isDone = part.state === "output-available"
|
||||
const isError = part.state === "error" || part.state === "output-error"
|
||||
|
||||
const cmd =
|
||||
part.input && typeof part.input === "object" && "cmd" in part.input
|
||||
? String((part.input as { cmd: string }).cmd)
|
||||
: undefined
|
||||
|
||||
const output =
|
||||
isDone && part.output && typeof part.output === "object"
|
||||
? (part.output as { stdout?: string; stderr?: string; exitCode?: number })
|
||||
: undefined
|
||||
|
||||
const hasOutput =
|
||||
output &&
|
||||
((output.stdout && output.stdout.length > 0) ||
|
||||
(output.stderr && output.stderr.length > 0))
|
||||
const errorText = part.errorText
|
||||
const hasExpandable = hasOutput || (isError && errorText)
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[#1E2128] bg-[#0D121A] text-xs my-1 overflow-hidden font-mono">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-3 py-2 cursor-pointer hover:bg-[#141922] transition-colors",
|
||||
expanded && hasExpandable && "border-b border-[#1E2128]",
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="size-3 animate-spin text-blue-400 shrink-0" />
|
||||
) : (
|
||||
<TerminalIcon
|
||||
className={cn(
|
||||
"size-3 shrink-0",
|
||||
isDone
|
||||
? output?.exitCode === 0
|
||||
? "text-emerald-400"
|
||||
: "text-amber-400"
|
||||
: isError
|
||||
? "text-red-400"
|
||||
: "text-white/50",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className={cn("text-white/50", isLoading && "text-blue-400/60")}>
|
||||
$
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"flex-1 text-left truncate",
|
||||
isDone
|
||||
? output?.exitCode === 0
|
||||
? "text-emerald-300"
|
||||
: "text-amber-300"
|
||||
: isLoading
|
||||
? "text-blue-300"
|
||||
: isError
|
||||
? "text-red-300"
|
||||
: "text-white/70",
|
||||
)}
|
||||
>
|
||||
{cmd ?? "..."}
|
||||
</span>
|
||||
{isLoading && (
|
||||
<span className="text-white/30 shrink-0">running...</span>
|
||||
)}
|
||||
{isDone && !hasOutput && (
|
||||
<span className="text-white/30 shrink-0">done</span>
|
||||
)}
|
||||
{isError && <span className="text-red-400/60 shrink-0">error</span>}
|
||||
{hasExpandable &&
|
||||
(expanded ? (
|
||||
<ChevronDownIcon className="size-3 text-white/30 shrink-0" />
|
||||
) : (
|
||||
<ChevronRightIcon className="size-3 text-white/30 shrink-0" />
|
||||
))}
|
||||
</button>
|
||||
|
||||
{expanded && (hasOutput || (isError && errorText)) && (
|
||||
<div className="px-3 py-2 space-y-1">
|
||||
{output?.stdout && output.stdout.length > 0 && (
|
||||
<pre className="text-white/70 bg-[#080B10] rounded p-2 overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap break-all text-[11px]">
|
||||
{output.stdout}
|
||||
</pre>
|
||||
)}
|
||||
{output?.stderr && output.stderr.length > 0 && (
|
||||
<pre className="text-amber-300/70 bg-[#080B10] rounded p-2 overflow-x-auto max-h-24 overflow-y-auto whitespace-pre-wrap break-all text-[11px]">
|
||||
{output.stderr}
|
||||
</pre>
|
||||
)}
|
||||
{isError && errorText && (
|
||||
<pre className="text-red-300/90 bg-[#080B10] rounded p-2 overflow-x-auto max-h-24 overflow-y-auto whitespace-pre-wrap break-all text-[11px]">
|
||||
{errorText}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const toolName = part.type.replace("tool-", "")
|
||||
if (toolName === "bash") {
|
||||
return <BashToolDisplay part={part} />
|
||||
}
|
||||
const meta =
|
||||
TOOL_META[toolName] ??
|
||||
(isWebSearchToolName(toolName)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue