Skip to content

Commit e4afffd

Browse files
committed
feat(showcase): port hitl-in-chat-booking, mcp-apps, open-gen-ui to built-in-agent
- hitl-in-chat-booking: frontend useHumanInTheLoop with inline time-picker - mcp-apps: separate route with mcpApps middleware → Excalidraw MCP server - open-gen-ui: separate route with openGenerativeUI middleware + design skill
1 parent e18ee1d commit e4afffd

12 files changed

Lines changed: 485 additions & 0 deletions

File tree

showcase/integrations/built-in-agent/manifest.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ features:
3232
- headless-simple
3333
- headless-complete
3434
- hitl-in-chat
35+
- hitl-in-chat-booking
3536
- tool-rendering
3637
- gen-ui-tool-based
3738
- gen-ui-agent
3839
- shared-state-read-write
3940
- shared-state-streaming
4041
- subagents
42+
- mcp-apps
43+
- open-gen-ui
4144
demos:
4245
- id: cli-start
4346
name: CLI Start Command
@@ -156,6 +159,33 @@ demos:
156159
description: Multiple agents with visible task delegation
157160
tags: [multi-agent]
158161
route: /demos/subagents
162+
- id: hitl-in-chat-booking
163+
name: In-Chat HITL (Booking)
164+
description: Time-picker card rendered inline via useHumanInTheLoop for a booking flow
165+
tags: [interactivity]
166+
route: /demos/hitl-in-chat-booking
167+
highlight:
168+
- src/app/demos/hitl-in-chat-booking/page.tsx
169+
- src/app/demos/hitl-in-chat-booking/time-picker-card.tsx
170+
- src/lib/factory/tanstack-factory.ts
171+
- id: mcp-apps
172+
name: MCP Apps
173+
description: MCP server-driven UI via activity renderers
174+
tags: [generative-ui]
175+
route: /demos/mcp-apps
176+
highlight:
177+
- src/app/demos/mcp-apps/page.tsx
178+
- src/app/api/copilotkit-mcp-apps/route.ts
179+
- src/lib/factory/mcp-apps-factory.ts
180+
- id: open-gen-ui
181+
name: Fully Open-Ended Generative UI
182+
description: Agent generates UI from an arbitrary component library
183+
tags: [generative-ui]
184+
route: /demos/open-gen-ui
185+
highlight:
186+
- src/app/demos/open-gen-ui/page.tsx
187+
- src/app/api/copilotkit-ogui/route.ts
188+
- src/lib/factory/ogui-factory.ts
159189
# NOTE: starter omitted intentionally for v1 of this column. The
160190
# `showcase/scripts/generate-starters.ts` generator expects every package
161191
# to have a separate `src/agent/` backend; built-in-agent runs in-process
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
CopilotRuntime,
3+
createCopilotRuntimeHandler,
4+
InMemoryAgentRunner,
5+
} from "@copilotkit/runtime/v2";
6+
import { createMcpAppsAgent } from "@/lib/factory/mcp-apps-factory";
7+
8+
// Dedicated runtime for the MCP Apps demo.
9+
//
10+
// `mcpApps.servers` auto-applies the MCP Apps middleware to every registered
11+
// agent: the middleware exposes the remote MCP server's tools to the agent at
12+
// request time and emits the activity events that CopilotKit's built-in
13+
// `MCPAppsActivityRenderer` renders inline as a sandboxed iframe.
14+
const runtime = new CopilotRuntime({
15+
agents: { default: createMcpAppsAgent() },
16+
runner: new InMemoryAgentRunner(),
17+
mcpApps: {
18+
servers: [
19+
{
20+
type: "http",
21+
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
22+
// Always pin a stable serverId — without it CopilotKit hashes the URL
23+
// and a URL change silently breaks restoration of persisted MCP apps.
24+
serverId: "excalidraw",
25+
},
26+
],
27+
},
28+
});
29+
30+
const handler = createCopilotRuntimeHandler({
31+
runtime,
32+
basePath: "/api/copilotkit-mcp-apps",
33+
mode: "single-route",
34+
});
35+
36+
async function withProbeCompat(req: Request): Promise<Response> {
37+
const res = await handler(req);
38+
if (res.status === 404) {
39+
const body = await res.text();
40+
return new Response(body, { status: 400, headers: res.headers });
41+
}
42+
return res;
43+
}
44+
45+
export const GET = (req: Request) => handler(req);
46+
export const POST = (req: Request) => withProbeCompat(req);
47+
export const OPTIONS = (req: Request) => handler(req);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
CopilotRuntime,
3+
createCopilotRuntimeHandler,
4+
InMemoryAgentRunner,
5+
} from "@copilotkit/runtime/v2";
6+
import { createOguiAgent } from "@/lib/factory/ogui-factory";
7+
8+
// Dedicated runtime for the Open Generative UI demo.
9+
//
10+
// Isolated because the `openGenerativeUI` runtime flag advertises
11+
// `openGenerativeUIEnabled: true` on the probe, which causes the
12+
// CopilotKit provider's setTools effect to behave differently from the
13+
// default tools-only runtime. Keeping it on its own basePath avoids
14+
// cross-talk with other demos.
15+
const runtime = new CopilotRuntime({
16+
agents: { default: createOguiAgent() },
17+
runner: new InMemoryAgentRunner(),
18+
openGenerativeUI: {
19+
agents: ["default"],
20+
},
21+
});
22+
23+
const handler = createCopilotRuntimeHandler({
24+
runtime,
25+
basePath: "/api/copilotkit-ogui",
26+
mode: "single-route",
27+
});
28+
29+
async function withProbeCompat(req: Request): Promise<Response> {
30+
const res = await handler(req);
31+
if (res.status === 404) {
32+
const body = await res.text();
33+
return new Response(body, { status: 400, headers: res.headers });
34+
}
35+
return res;
36+
}
37+
38+
export const GET = (req: Request) => handler(req);
39+
export const POST = (req: Request) => withProbeCompat(req);
40+
export const OPTIONS = (req: Request) => handler(req);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# In-Chat Booking (HITL)
2+
3+
Frontend `useHumanInTheLoop` flow. The agent calls `book_call(topic, attendee)`,
4+
the chat renders an inline time-picker card, and the user's selection is
5+
piped back as the tool result.
6+
7+
- Frontend tool registration: `useHumanInTheLoop({ name: "book_call", ... })`
8+
- Picker UI: `time-picker-card.tsx` (slot grid + cancel)
9+
- Agent: built-in agent in `src/lib/factory/tanstack-factory.ts` — no
10+
backend definition needed; the frontend tool is auto-bridged.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
import {
4+
CopilotKitProvider,
5+
CopilotChat,
6+
useHumanInTheLoop,
7+
} from "@copilotkit/react-core/v2";
8+
import { z } from "zod";
9+
import { TimePickerCard, TimeSlot } from "./time-picker-card";
10+
11+
const DEFAULT_SLOTS: TimeSlot[] = [
12+
{ label: "Tomorrow 10:00 AM", iso: "2026-04-30T10:00:00-07:00" },
13+
{ label: "Tomorrow 2:00 PM", iso: "2026-04-30T14:00:00-07:00" },
14+
{ label: "Monday 9:00 AM", iso: "2026-05-04T09:00:00-07:00" },
15+
{ label: "Monday 3:30 PM", iso: "2026-05-04T15:30:00-07:00" },
16+
];
17+
18+
export default function HitlInChatBooking() {
19+
return (
20+
<CopilotKitProvider runtimeUrl="/api/copilotkit" useSingleEndpoint>
21+
<Demo />
22+
</CopilotKitProvider>
23+
);
24+
}
25+
26+
function Demo() {
27+
useHumanInTheLoop({
28+
name: "book_call",
29+
description:
30+
"Ask the user to pick a time slot for a call. The picker UI presents fixed candidate slots; the user's choice is returned to the agent.",
31+
parameters: z.object({
32+
topic: z
33+
.string()
34+
.describe("What the call is about (e.g. 'Intro with sales')"),
35+
attendee: z
36+
.string()
37+
.describe("Who the call is with (e.g. 'Alice from Sales')"),
38+
}),
39+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
40+
render: ({ args, status, respond }: any) => (
41+
<TimePickerCard
42+
topic={args?.topic ?? "a call"}
43+
attendee={args?.attendee}
44+
slots={DEFAULT_SLOTS}
45+
status={status}
46+
onSubmit={(result) => respond?.(result)}
47+
/>
48+
),
49+
});
50+
51+
return (
52+
<main className="p-8">
53+
<h1 className="text-2xl font-semibold mb-4">In-Chat Booking (HITL)</h1>
54+
<p className="text-sm opacity-70 mb-6">
55+
Try: &ldquo;Book an intro call with the sales team.&rdquo; The agent
56+
renders an inline time picker; pick a slot to confirm.
57+
</p>
58+
<CopilotChat />
59+
</main>
60+
);
61+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
5+
export interface TimeSlot {
6+
label: string;
7+
iso: string;
8+
}
9+
10+
export type TimePickerStatus = "InProgress" | "Executing" | "Complete";
11+
12+
export interface TimePickerCardProps {
13+
topic: string;
14+
attendee?: string;
15+
slots: TimeSlot[];
16+
status: TimePickerStatus;
17+
onSubmit: (
18+
result: { chosen_time: string; chosen_label: string } | { cancelled: true },
19+
) => void;
20+
}
21+
22+
/**
23+
* Renders a "Book a call" card with a small grid of time slots.
24+
* The user picks one slot (or cancels); that resolution is forwarded back
25+
* to the agent via the onSubmit callback wired up by `useHumanInTheLoop`.
26+
*/
27+
export function TimePickerCard({
28+
topic,
29+
attendee,
30+
slots,
31+
status,
32+
onSubmit,
33+
}: TimePickerCardProps) {
34+
const [picked, setPicked] = useState<TimeSlot | null>(null);
35+
const [cancelled, setCancelled] = useState(false);
36+
const disabled = status !== "Executing" || picked !== null || cancelled;
37+
38+
if (cancelled) {
39+
return (
40+
<div
41+
className="rounded-2xl border border-[#DBDBE5] bg-[#F7F7F9] p-4 text-sm text-[#57575B] max-w-md"
42+
data-testid="time-picker-cancelled"
43+
>
44+
Cancelled — no time picked.
45+
</div>
46+
);
47+
}
48+
49+
if (picked) {
50+
return (
51+
<div
52+
className="rounded-2xl border border-[#85ECCE4D] bg-[#85ECCE]/10 p-4 max-w-md"
53+
data-testid="time-picker-picked"
54+
>
55+
<p className="text-sm text-[#010507]">
56+
Booked for <span className="font-semibold">{picked.label}</span>
57+
</p>
58+
</div>
59+
);
60+
}
61+
62+
return (
63+
<div
64+
className="rounded-2xl border border-[#DBDBE5] bg-white p-5 shadow-sm max-w-md"
65+
data-testid="time-picker-card"
66+
>
67+
<p className="text-[10px] uppercase tracking-[0.14em] text-[#57575B] font-medium">
68+
Book a call
69+
</p>
70+
<h3 className="text-base font-semibold text-[#010507] mt-1.5">{topic}</h3>
71+
{attendee && (
72+
<p className="text-sm text-[#57575B] mt-0.5">With {attendee}</p>
73+
)}
74+
75+
<p className="text-sm text-[#57575B] mt-4 mb-2">Pick a time:</p>
76+
<div className="grid grid-cols-2 gap-2">
77+
{slots.map((s) => (
78+
<button
79+
key={s.iso}
80+
disabled={disabled}
81+
data-testid="time-picker-slot"
82+
onClick={() => {
83+
setPicked(s);
84+
onSubmit({ chosen_time: s.iso, chosen_label: s.label });
85+
}}
86+
className="rounded-xl border border-[#DBDBE5] bg-white px-3 py-2 text-sm font-medium text-[#010507] hover:border-[#BEC2FF] hover:bg-[#BEC2FF1A] disabled:opacity-50 transition-colors"
87+
>
88+
{s.label}
89+
</button>
90+
))}
91+
</div>
92+
<button
93+
disabled={disabled}
94+
onClick={() => {
95+
setCancelled(true);
96+
onSubmit({ cancelled: true });
97+
}}
98+
className="mt-3 w-full rounded-xl border border-[#E9E9EF] px-3 py-1.5 text-xs text-[#838389] hover:bg-[#FAFAFC] disabled:opacity-50 transition-colors"
99+
>
100+
None of these work
101+
</button>
102+
</div>
103+
);
104+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# MCP Apps
2+
3+
The runtime's `mcpApps.servers` config auto-applies the MCP Apps middleware:
4+
the agent's tool palette is augmented at request time with the remote MCP
5+
server's tools, and tool calls bound to UI resources stream back as
6+
sandboxed iframe activity events.
7+
8+
- Runtime: `src/app/api/copilotkit-mcp-apps/route.ts` (separate basePath)
9+
- Agent: `src/lib/factory/mcp-apps-factory.ts` (zero local tools)
10+
- Frontend: plain `<CopilotChat />``MCPAppsActivityRenderer` is
11+
auto-registered by `CopilotKitProvider`.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
/**
4+
* MCP Apps demo.
5+
*
6+
* MCP Apps are MCP servers that expose tools with associated UI resources.
7+
* The CopilotKit runtime is wired with `mcpApps: { servers: [...] }` (see
8+
* `src/app/api/copilotkit-mcp-apps/route.ts`), which auto-applies the MCP
9+
* Apps middleware. When the agent calls an MCP tool, the middleware
10+
* fetches the associated UI resource and emits an activity event; the
11+
* built-in `MCPAppsActivityRenderer` registered by `CopilotKitProvider`
12+
* renders the sandboxed iframe inline in the chat.
13+
*
14+
* This cell points at the public Excalidraw MCP app
15+
* (https://mcp.excalidraw.com).
16+
*/
17+
18+
import {
19+
CopilotKitProvider,
20+
CopilotChat,
21+
} from "@copilotkit/react-core/v2";
22+
23+
export default function MCPAppsDemo() {
24+
return (
25+
<CopilotKitProvider
26+
runtimeUrl="/api/copilotkit-mcp-apps"
27+
useSingleEndpoint
28+
>
29+
<main className="p-8">
30+
<h1 className="text-2xl font-semibold mb-4">MCP Apps</h1>
31+
<p className="text-sm opacity-70 mb-6">
32+
Try: &ldquo;Use Excalidraw to draw a simple flowchart with three
33+
steps.&rdquo; The agent invokes a remote MCP tool and the
34+
associated UI resource renders inline in chat.
35+
</p>
36+
<CopilotChat />
37+
</main>
38+
</CopilotKitProvider>
39+
);
40+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Open Generative UI
2+
3+
The agent authors HTML + CSS at request time; the runtime middleware
4+
streams it to the built-in `OpenGenerativeUIActivityRenderer`, which mounts
5+
it inside a sandboxed iframe.
6+
7+
- Runtime: `src/app/api/copilotkit-ogui/route.ts`
8+
(`openGenerativeUI: { agents: ["default"] }`)
9+
- Agent: `src/lib/factory/ogui-factory.ts` (no bespoke tools — middleware
10+
injects `generateSandboxedUi`)
11+
- Frontend: plain `<CopilotChat />` plus an inline `designSkill` that
12+
steers the LLM toward educational visualisations.

0 commit comments

Comments
 (0)