diff --git a/examples/chat/web/app/page.tsx b/examples/chat/web/app/page.tsx index ff05284a..a1db90a0 100644 --- a/examples/chat/web/app/page.tsx +++ b/examples/chat/web/app/page.tsx @@ -18,7 +18,7 @@ import { TodosPanel } from "./components/TodosPanel" // - `labels` is `Partial`, whose header title field is `modalHeaderTitle`. export default function Home() { return ( - +
diff --git a/examples/research/web/app/components/ToolCallCard.tsx b/examples/research/web/app/components/ToolCallCard.tsx new file mode 100644 index 00000000..3d23fe48 --- /dev/null +++ b/examples/research/web/app/components/ToolCallCard.tsx @@ -0,0 +1,140 @@ +"use client" +import { useRenderTool } from "@copilotkit/react-core/v2" + +// Notes (verified against installed @copilotkit/react-core@1.62.3 types — +// examples/research/web/node_modules/@copilotkit/react-core/dist/copilotkit-Bp6BD8xe.d.mts): +// +// - The registration hook is `useRenderTool` (NOT `useRenderToolCall` — that +// one takes no args and returns a `({toolCall, toolMessage}) => ReactElement` +// render *function* used internally by CopilotKit's own message view; it is +// not a registration API). `useRenderTool` is called under ``, +// the same way `useInterrupt` is used in PermissionInterrupt.tsx. +// - Wildcard registration: pass `{ name: "*", render, agentId? }` — the "*" +// overload is documented as "used as a fallback when no exact name-matched +// renderer is registered for a tool call" (src/v2/hooks/use-render-tool.d.ts). +// `WildcardToolCallRender`/`defineToolCallRenderer` are the analogous +// *prop*-based API (for ``), not needed +// here since the hook form matches the rest of this app's components. +// - Render prop field names for `useRenderTool`'s wildcard overload are +// `{ name, toolCallId, parameters, status, result }` — note the args field +// is called `parameters` here (not `args`; `args` is only the field name on +// the sibling `ReactToolCallRenderer`/`defineToolCallRenderer` types used by +// the prop-based API). +// - `status` is the string union `"inProgress" | "executing" | "complete"` +// (plain string literals) for `useRenderTool` — NOT the `ToolCallStatus` +// enum (`InProgress`/`Executing`/`Complete`), which belongs to the +// prop-based `ReactToolCallRenderer` render props instead. +// - `result` is `string | undefined`, populated only once `status === "complete"`. +// +// With no agentId, this binds to CopilotKit's default agent id ("default"), +// which the runtime route registers as our Dawn /research agent — same as +// PermissionInterrupt.tsx and the other panels. +/** + * Dawn delivers tool args as a JSON *string* under `input` (that's how the + * agent-adapter serializes them), so `parameters` arrives as + * `{ input: '{"path":"corpus/x.md"}' }`. Unwrap it so the card can show the + * real argument instead of double-encoded JSON. + */ +function parseArgs(parameters: unknown): Record { + const p = (parameters ?? {}) as Record + if (typeof p.input === "string") { + try { + const inner = JSON.parse(p.input) + if (inner && typeof inner === "object") return inner as Record + } catch { + // Not JSON — fall through and show the raw string. + } + } + return p +} + +/** + * Tool results arrive as a serialized LangChain `ToolMessage` + * (`{ lc, type, id: [...], kwargs: { content } }`). Pull out the content so the + * card shows the actual output rather than LangChain internals. + */ +function parseResult(result: string | undefined): string | undefined { + if (!result) return undefined + try { + const parsed = JSON.parse(result) as { kwargs?: { content?: unknown } } + const content = parsed?.kwargs?.content + if (typeof content === "string") return content + if (content != null) return JSON.stringify(content, null, 2) + } catch { + // Not JSON — show it as-is. + } + return result +} + +function summarizeArgs(name: string, parameters: unknown): string { + const p = parseArgs(parameters) + switch (name) { + case "searchCorpus": + return typeof p.query === "string" ? p.query : JSON.stringify(p) + case "readDoc": + return typeof p.path === "string" ? p.path : JSON.stringify(p) + case "runBash": + return typeof p.command === "string" ? p.command : JSON.stringify(p) + case "task": + return typeof p.subagent === "string" ? `→ ${p.subagent}` : JSON.stringify(p) + default: + return JSON.stringify(p) + } +} + +export function ToolCallCard() { + useRenderTool( + { + name: "*", + render: ({ name, status, parameters, result }) => ( +
+ + {name} + + + {status === "complete" ? "done" : status === "executing" ? "running…" : "preparing…"} + +
+ {summarizeArgs(name, parameters)} +
+ {status === "complete" && + (() => { + const content = parseResult(result) + if (!content) return null + return ( +
+                  {content.slice(0, 400)}
+                
+ ) + })()} +
+ ), + }, + [], + ) + return null +} diff --git a/examples/research/web/app/page.tsx b/examples/research/web/app/page.tsx index 72d2223c..ecdc16dc 100644 --- a/examples/research/web/app/page.tsx +++ b/examples/research/web/app/page.tsx @@ -4,6 +4,7 @@ import { MemoryCandidates } from "./components/MemoryCandidates" import { PermissionInterrupt } from "./components/PermissionInterrupt" import { PlanPanel } from "./components/PlanPanel" import { SubagentActivity } from "./components/SubagentActivity" +import { ToolCallCard } from "./components/ToolCallCard" // Notes (verified against installed @copilotkit/react-core@1.62.3 types — see // examples/chat/web/app/page.tsx for the original investigation): @@ -18,10 +19,16 @@ import { SubagentActivity } from "./components/SubagentActivity" // under "default", so the sidebar, useAgent, and useInterrupt all bind to it with // no per-component agentId. // - `labels` is `Partial`, whose header title field is `modalHeaderTitle`. +// - `defaultThrottleMs` coalesces the useAgent re-renders that every panel and the +// sidebar transcript get from OnMessagesChanged/OnStateChanged. It defaults to +// UNTHROTTLED, and a full research run streams hundreds of events (coordinator +// tokens + ~200 subagent events), which pegs the renderer. 100ms keeps the UI +// live-feeling while capping re-renders at ~10/sec. export default function Home() { return ( - + +