mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
f13f8865a8
Add a zodState() helper that attaches a lazy `~standard.jsonSchema.input` to a zod schema so LangGraph's StateSchema.getJsonSchema() emits the field into the graph's output_schema. Without it, zod v4 fields carry `~standard.validate` + `vendor` only, `isStandardJSONSchema` returns false, and the field is silently dropped from output_schema — which in turn causes the AG-UI LangGraphAgent proxy to filter the value out of STATE_SNAPSHOT events on the wire, so the frontend never sees it even though the underlying thread state has the data. Apply zodState to the middleware's own `copilotkit` state field so it surfaces in output_schema and export it for demos to use on custom state fields (todos, documents, etc.). Uses `z.toJSONSchema` when available (zod v4 subpath) and falls back to an empty object, which is sufficient to make langgraph-api include the key in output_schema.
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import { Wrench, Check, ChevronDown } from "lucide-react";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
|
|
interface ToolReasoningProps {
|
|
name: string;
|
|
args?: object | unknown;
|
|
status: string;
|
|
}
|
|
|
|
function formatValue(value: unknown): string {
|
|
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
if (typeof value === "object" && value !== null)
|
|
return `{${Object.keys(value).length} keys}`;
|
|
if (typeof value === "string") return `"${value}"`;
|
|
return String(value);
|
|
}
|
|
|
|
export function ToolReasoning({ name, args, status }: ToolReasoningProps) {
|
|
const entries = args ? Object.entries(args) : [];
|
|
const detailsRef = useRef<HTMLDetailsElement>(null);
|
|
const isRunning = status === "executing" || status === "inProgress";
|
|
|
|
// Auto-open while executing, auto-close when complete
|
|
useEffect(() => {
|
|
if (!detailsRef.current) return;
|
|
detailsRef.current.open = isRunning;
|
|
}, [isRunning]);
|
|
|
|
const statusIcon = isRunning ? (
|
|
<Spinner size="sm" className="h-3 w-3" />
|
|
) : (
|
|
<Check className="h-3 w-3 text-emerald-500" />
|
|
);
|
|
|
|
return (
|
|
<div className="my-1.5">
|
|
{entries.length > 0 ? (
|
|
<details ref={detailsRef} open className="group">
|
|
<summary className="flex items-center gap-2 cursor-pointer list-none text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors">
|
|
{statusIcon}
|
|
<Wrench className="h-3 w-3" />
|
|
<span
|
|
className="font-medium"
|
|
style={{ fontFamily: "var(--font-code)" }}
|
|
>
|
|
{name}
|
|
</span>
|
|
<ChevronDown className="h-3 w-3 ml-auto transition-transform group-open:rotate-180" />
|
|
</summary>
|
|
<div className="ml-5 mt-1.5 rounded-md bg-[var(--secondary)] px-3 py-2 space-y-1">
|
|
{entries.map(([key, value]) => (
|
|
<div
|
|
key={key}
|
|
className="flex gap-2 min-w-0 text-xs"
|
|
style={{ fontFamily: "var(--font-code)" }}
|
|
>
|
|
<span className="text-[var(--muted-foreground)] shrink-0">
|
|
{key}:
|
|
</span>
|
|
<span className="text-[var(--foreground)] truncate">
|
|
{formatValue(value)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</details>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-sm text-[var(--muted-foreground)]">
|
|
{statusIcon}
|
|
<Wrench className="h-3 w-3" />
|
|
<span
|
|
className="font-medium"
|
|
style={{ fontFamily: "var(--font-code)" }}
|
|
>
|
|
{name}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|