e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { memo, useState } from "react";
|
|
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
import { ChevronRightIcon, DollarSignIcon, XCircleIcon } from "lucide-react";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/radix/collapsible";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
ToolStatusIcon,
|
|
isCancelledToolStatus,
|
|
truncate,
|
|
} from "@/components/tools/tool-ui-shared";
|
|
|
|
const parseResult = (result: unknown) => {
|
|
if (!result) return {};
|
|
if (typeof result === "string") {
|
|
try {
|
|
const parsed = JSON.parse(result);
|
|
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
} catch {
|
|
// plain text output
|
|
}
|
|
return { stdout: result };
|
|
}
|
|
if (typeof result === "object") return result as Record<string, unknown>;
|
|
return {};
|
|
};
|
|
|
|
export const BashTerminal: ToolCallMessagePartComponent = memo(
|
|
({ args, result, status }) => {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const command = typeof args?.command === "string" ? args.command : "";
|
|
const description =
|
|
typeof args?.description === "string" ? args.description : "";
|
|
const isRunning = status?.type === "running";
|
|
const isCancelled = isCancelledToolStatus(status);
|
|
|
|
const parsed = isRunning ? {} : parseResult(result);
|
|
const stdout =
|
|
typeof parsed.stdout === "string" ? parsed.stdout : undefined;
|
|
const stderr =
|
|
typeof parsed.stderr === "string" ? parsed.stderr : undefined;
|
|
const exitCode = typeof parsed.exitCode === "number" ? parsed.exitCode : 0;
|
|
const hasOutput = Boolean(stdout || stderr);
|
|
const isError = !isRunning && exitCode !== 0;
|
|
|
|
const summaryText = description || truncate(command);
|
|
|
|
return (
|
|
<Collapsible open={open} onOpenChange={setOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="group text-muted-foreground hover:text-foreground flex w-full items-center gap-2 py-0.5 text-sm transition-colors"
|
|
>
|
|
{isRunning ? (
|
|
<ToolStatusIcon status={status} />
|
|
) : isError ? (
|
|
<XCircleIcon className="text-destructive size-3 shrink-0" />
|
|
) : (
|
|
<ToolStatusIcon
|
|
status={status}
|
|
completeIcon={<DollarSignIcon className="size-3.5 shrink-0" />}
|
|
/>
|
|
)}
|
|
|
|
<span
|
|
className={cn(
|
|
"flex items-center gap-1.5 truncate",
|
|
isCancelled && "line-through opacity-50",
|
|
)}
|
|
>
|
|
<span className="font-medium">bash</span>
|
|
{summaryText && <span className="opacity-60">{summaryText}</span>}
|
|
</span>
|
|
|
|
{hasOutput && (
|
|
<>
|
|
<span className="border-muted-foreground/20 group-hover:border-muted-foreground/50 mt-0.5 min-w-4 flex-1 self-center border-b-[0.5px] transition-colors" />
|
|
<ChevronRightIcon
|
|
className={cn(
|
|
"stroke-muted-foreground/60 group-hover:stroke-foreground/60 mt-0.5 size-3.75 shrink-0 transition-[transform,stroke]",
|
|
open && "rotate-90",
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
|
|
{hasOutput && (
|
|
<CollapsibleContent className="data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down overflow-hidden data-[state=closed]:ease-out data-[state=open]:ease-in">
|
|
<div className="bg-muted/50 mt-1 ml-5 max-h-96 overflow-y-auto rounded-md border p-3 font-mono text-xs">
|
|
{command && (
|
|
<div className="text-muted-foreground mb-2">$ {command}</div>
|
|
)}
|
|
{stdout && (
|
|
<pre className="text-foreground wrap-break-word whitespace-pre-wrap">
|
|
{stdout}
|
|
</pre>
|
|
)}
|
|
{stderr && (
|
|
<pre
|
|
className={cn(
|
|
"text-destructive wrap-break-word whitespace-pre-wrap",
|
|
stdout && "mt-2",
|
|
)}
|
|
>
|
|
{stderr}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
)}
|
|
</Collapsible>
|
|
);
|
|
},
|
|
);
|
|
BashTerminal.displayName = "BashTerminal";
|