Skip to content

Commit 2ee3e61

Browse files
BenTaylorDevclaude
andcommitted
feat(integrations/langgraph-python): scaffold Threads frontend (locked without license)
Restore the threads-drawer components + locked-state gate, page.tsx wiring (threadId state + CopilotChatConfigurationProvider + two-column shell), and the next.config build-time derivation of NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED from the license token. Without a license the gate renders the locked panel; the drawer is client-mounted (SSR-safe). This is the canonical north-star frontend that parity:sync distributes to instances per batch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b00d36b commit 2ee3e61

7 files changed

Lines changed: 1410 additions & 10 deletions

File tree

examples/integrations/langgraph-python/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
AGENT_URL=http://localhost:8123
22
OPENAI_API_KEY=
33

4-
# --- CopilotKit Intelligence (optional; set COPILOTKIT_LICENSE_TOKEN to enable) ---
4+
# --- CopilotKit Intelligence (optional; set COPILOTKIT_LICENSE_TOKEN to enable Threads — server + UI) ---
55
# COPILOTKIT_LICENSE_TOKEN=
66
# INTELLIGENCE_API_URL=http://localhost:4201
77
# INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:4401

examples/integrations/langgraph-python/next.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import type { NextConfig } from "next";
33
const nextConfig: NextConfig = {
44
output: "standalone",
55
serverExternalPackages: ["@copilotkit/runtime"],
6+
env: {
7+
// The public Threads UI flag is DERIVED from the server-side license token.
8+
// Set COPILOTKIT_LICENSE_TOKEN (only) to enable Threads — do not set this flag
9+
// directly. NOTE: NEXT_PUBLIC_* resolves at BUILD time while the runtime reads
10+
// the token per-request, so the UI gate and runtime agree only when the token is
11+
// present at build time (the standard `next dev` / host-build flow). For a
12+
// standalone/Docker image built without the token and injected at runtime, set
13+
// COPILOTKIT_LICENSE_TOKEN at build time too (or gate the UI at runtime) so the
14+
// baked flag reflects it.
15+
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.COPILOTKIT_LICENSE_TOKEN
16+
? "true"
17+
: "false",
18+
},
619
typescript: {
720
// Docker route override uses HttpAgent which has a type mismatch with CopilotRuntime
821
ignoreBuildErrors: true,
Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,57 @@
11
"use client";
22

3+
import { useState } from "react";
4+
35
import { ExampleLayout } from "@/components/example-layout";
46
import { ExampleCanvas } from "@/components/example-canvas";
7+
import { ThreadsDrawer } from "@/components/threads-drawer";
8+
import { ThreadsPanelGate } from "@/components/threads-drawer/locked-state";
59
import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks";
610

7-
import { CopilotChat } from "@copilotkit/react-core/v2";
11+
import {
12+
CopilotChat,
13+
CopilotChatConfigurationProvider,
14+
} from "@copilotkit/react-core/v2";
15+
16+
import styles from "@/components/threads-drawer/threads-drawer.module.css";
817

918
export default function HomePage() {
1019
useGenerativeUIExamples();
1120
useExampleSuggestions();
1221

22+
const [threadId, setThreadId] = useState<string | undefined>(undefined);
23+
1324
return (
14-
<ExampleLayout
15-
chatContent={
16-
<CopilotChat
17-
attachments={{ enabled: true }}
18-
input={{ disclaimer: () => null, className: "pb-6" }}
25+
<div className={styles.layout}>
26+
<ThreadsPanelGate>
27+
<ThreadsDrawer
28+
agentId="default"
29+
threadId={threadId}
30+
onThreadChange={setThreadId}
1931
/>
20-
}
21-
appContent={<ExampleCanvas />}
22-
/>
32+
</ThreadsPanelGate>
33+
<div className={styles.mainPanel}>
34+
{/*
35+
Wrap both the chat and the canvas in one CopilotChatConfigurationProvider
36+
so they share the active threadId. `useAgent()` falls back to the
37+
provider's threadId when called without an explicit one, which makes
38+
the canvas read from the same per-thread agent clone that the chat's
39+
/connect replay populates. Without this wrapper, the canvas resolves
40+
to the registry agent and never receives STATE_SNAPSHOT events on
41+
thread resume.
42+
*/}
43+
<CopilotChatConfigurationProvider agentId="default" threadId={threadId}>
44+
<ExampleLayout
45+
chatContent={
46+
<CopilotChat
47+
attachments={{ enabled: true }}
48+
input={{ disclaimer: () => null, className: "pb-6" }}
49+
/>
50+
}
51+
appContent={<ExampleCanvas />}
52+
/>
53+
</CopilotChatConfigurationProvider>
54+
</div>
55+
</div>
2356
);
2457
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"use client";
2+
3+
export { default as ThreadsDrawer } from "./threads-drawer";
4+
export type { ThreadsDrawerProps } from "./threads-drawer";
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardFooter,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card";
12+
import { Button } from "@/components/ui/button";
13+
14+
export function ThreadsPanelGate({ children }: { children: React.ReactNode }) {
15+
// The Threads drawer reads a client-only external store (useThreads /
16+
// useSyncExternalStore) with no server snapshot, so it must not render during
17+
// SSR/prerender — Next would fail to prerender "/". Defer to client mount.
18+
const [mounted, setMounted] = React.useState(false);
19+
React.useEffect(() => setMounted(true), []);
20+
21+
if (process.env.NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED === "true") {
22+
if (!mounted) {
23+
// SSR / first-paint placeholder: reserve the drawer's width, nothing interactive.
24+
return <div className="w-72 shrink-0" aria-hidden />;
25+
}
26+
return <>{children}</>;
27+
}
28+
29+
return (
30+
<div className="flex w-72 shrink-0 flex-col items-center justify-center p-4">
31+
<Card className="w-full">
32+
<CardHeader>
33+
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--secondary)]">
34+
<svg
35+
xmlns="http://www.w3.org/2000/svg"
36+
width="20"
37+
height="20"
38+
viewBox="0 0 24 24"
39+
fill="none"
40+
stroke="currentColor"
41+
strokeWidth="2"
42+
strokeLinecap="round"
43+
strokeLinejoin="round"
44+
className="text-[var(--muted-foreground)]"
45+
aria-hidden="true"
46+
>
47+
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
48+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
49+
</svg>
50+
</div>
51+
<CardTitle>Threads</CardTitle>
52+
<CardDescription>
53+
Threads is a licensed CopilotKit Intelligence feature. Unlock
54+
persistent conversation history, multi-session context, and thread
55+
management across your application.
56+
</CardDescription>
57+
</CardHeader>
58+
<CardContent>
59+
<p className="text-sm text-[var(--muted-foreground)]">
60+
To enable Threads, add a CopilotKit Intelligence license to your
61+
project.
62+
</p>
63+
</CardContent>
64+
<CardFooter className="flex-col items-start gap-3">
65+
<div className="w-full rounded-[var(--radius)] border border-[var(--border)] bg-[var(--secondary)] px-3 py-2">
66+
<code className="text-xs text-[var(--secondary-foreground)]">
67+
copilotkit add-intelligence
68+
</code>
69+
</div>
70+
<Button
71+
variant="default"
72+
size="sm"
73+
className="w-full"
74+
onClick={() =>
75+
window.open(
76+
"https://docs.copilotkit.ai/intelligence",
77+
"_blank",
78+
"noopener,noreferrer",
79+
)
80+
}
81+
>
82+
Learn more
83+
</Button>
84+
</CardFooter>
85+
</Card>
86+
</div>
87+
);
88+
}

0 commit comments

Comments
 (0)