Skip to content
Open
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
2 changes: 1 addition & 1 deletion examples/chat/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { TodosPanel } from "./components/TodosPanel"
// - `labels` is `Partial<CopilotChatLabels>`, whose header title field is `modalHeaderTitle`.
export default function Home() {
return (
<CopilotKit runtimeUrl="/api/copilotkit">
<CopilotKit runtimeUrl="/api/copilotkit" defaultThrottleMs={100}>
<PermissionInterrupt />
<div style={{ display: "flex", height: "100vh" }}>
<TodosPanel />
Expand Down
140 changes: 140 additions & 0 deletions examples/research/web/app/components/ToolCallCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client"
import { useRenderTool } from "@copilotkit/react-core/v2"

// Notes (verified against installed @copilotkit/[email protected] 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 `<CopilotKit>`,
// 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 `<CopilotKit renderToolCalls={[...]}>`), 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<T>` 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<string, unknown> {
const p = (parameters ?? {}) as Record<string, unknown>
if (typeof p.input === "string") {
try {
const inner = JSON.parse(p.input)
if (inner && typeof inner === "object") return inner as Record<string, unknown>
} 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 }) => (
<div
style={{
border: "1px solid #e5e5e5",
borderRadius: 8,
padding: "8px 10px",
margin: "6px 0",
fontSize: 13,
}}
>
<span
style={{
display: "inline-block",
fontWeight: 600,
background: "#f2f2f2",
borderRadius: 4,
padding: "1px 6px",
}}
>
{name}
</span>
<span style={{ color: "#888", marginLeft: 6 }}>
{status === "complete" ? "done" : status === "executing" ? "running…" : "preparing…"}
</span>
<div style={{ color: "#555", marginTop: 4, wordBreak: "break-word" }}>
{summarizeArgs(name, parameters)}
</div>
{status === "complete" &&
(() => {
const content = parseResult(result)
if (!content) return null
return (
<pre
style={{
margin: "6px 0 0",
whiteSpace: "pre-wrap",
color: "#444",
maxHeight: 120,
overflow: "auto",
}}
>
{content.slice(0, 400)}
</pre>
)
})()}
</div>
),
},
[],
)
return null
}
9 changes: 8 additions & 1 deletion examples/research/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected] types — see
// examples/chat/web/app/page.tsx for the original investigation):
Expand All @@ -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<CopilotChatLabels>`, 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 (
<CopilotKit runtimeUrl="/api/copilotkit">
<CopilotKit runtimeUrl="/api/copilotkit" defaultThrottleMs={100}>
<PermissionInterrupt />
<ToolCallCard />
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ display: "flex", flexDirection: "column" }}>
<PlanPanel />
Expand Down