From d2b1e61c9fbc0116041b9cba23a1bd7491a346c0 Mon Sep 17 00:00:00 2001
From: Lukas Moschitz
Date: Wed, 2 Sep 2026 16:39:04 +0200
Subject: [PATCH 01/57] fix(showcase): redirect the legacy premium inspector
and guides URLs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two legacy docs URLs have been dead ends since cc8c945893 ("refactor(docs):
optimize structure, content and navigability", 2026-02-23). That commit had no
redirect config to update — seo-redirects.ts did not exist yet — and nobody
back-filled the entries when it was built.
`/premium/inspector` 404s. The commit renamed `(root)/premium/inspector.mdx` to
`(root)/inspector.mdx` with identical content, so the page still exists; only
the URL moved. Points at /inspector now, expanded over the framework slugs the
way the observability entries are, since /langgraph-python/inspector and its
siblings are served too.
`/direct-to-llm/guides/premium/*` loses the page. All four of them — overview,
headless-ui, observability, inspector — were deleted in the same commit, and
the R16 `/direct-to-llm/:path*` wildcard drops the remainder on the docs home.
That reads as a working link while serving the wrong page, which is why it went
unnoticed longer than a 404 would have. Each gets an exact entry.
Both need entries on the docs host and the shell host: P7 renames `/premium/*`
to a docs-host `/intelligence/*` on the way across, so without the shell entry
the legacy shell URL would 301 into a docs-host 404. The harness driver is the
intentional copy of the shell list and moves with it.
Exact sources beat every wildcard — middleware checks exactMap before the
wildcard list — so these win over INTEL-rename-wild, which would otherwise
rewrite them to a nonexistent /intelligence/inspector.
The decommission report fixtures move with the catalogue: 390 to 395 entries
defined, 377 to 382 zero-hit candidates, plus the five new ids. Regenerated
through the CLI with --events-json so the byte-for-byte cross-check against the
core module keeps its meaning.
Verified all 24 distinct destinations against staging. 22 return 200.
/langroid/inspector and /spring-ai/inspector 404, because those two framework
surfaces are not served at all — their bare roots and quickstarts 404 too. The
existing observability entries carry the same property, so no reachable URL
changes behavior.
---
.../src/probes/drivers/seo-redirects.ts | 38 ++++++++++
.../cli-stdout-human.txt | 9 ++-
.../redirect-decommission/cli-stdout.txt | 9 ++-
.../src/lib/__tests__/seo-redirects.test.ts | 70 +++++++++++++++++++
showcase/shell-docs/src/lib/seo-redirects.ts | 45 ++++++++++++
showcase/shell/src/lib/seo-redirects.ts | 38 ++++++++++
6 files changed, 205 insertions(+), 4 deletions(-)
diff --git a/showcase/harness/src/probes/drivers/seo-redirects.ts b/showcase/harness/src/probes/drivers/seo-redirects.ts
index b8b7e6c180..24ff17e43b 100644
--- a/showcase/harness/src/probes/drivers/seo-redirects.ts
+++ b/showcase/harness/src/probes/drivers/seo-redirects.ts
@@ -378,6 +378,35 @@ const SPECIFIC_FRAMEWORK: RedirectEntry[] = [
source: "/direct-to-llm/guides/mcp",
destination: "/built-in-agent/coding-agents",
},
+ // `/direct-to-llm/guides/premium/*` pages were deleted in cc8c945893
+ // ("refactor(docs): optimize structure, content and navigability",
+ // 2026-02-23) without redirects. The R16 `/direct-to-llm/:path*` wildcard
+ // strips the prefix and the remainder falls through to the docs home, so
+ // the page is lost rather than 404'd — quieter and harder to notice.
+ // Exact entries land each one on its current equivalent in one hop.
+ {
+ id: "INTEL-d2l-guides-overview",
+ source: "/direct-to-llm/guides/premium/overview",
+ destination: "/intelligence/overview",
+ },
+ {
+ id: "INTEL-d2l-guides-headless-ui",
+ source: "/direct-to-llm/guides/premium/headless-ui",
+ destination: "/intelligence/headless-ui",
+ },
+ {
+ // The observability page is retired; the overview is its standing
+ // destination everywhere else (INTEL-observability-*).
+ id: "INTEL-d2l-guides-observability",
+ source: "/direct-to-llm/guides/premium/observability",
+ destination: "/intelligence/overview",
+ },
+ {
+ // Inspector moved out of the Intelligence folder rather than retiring.
+ id: "INTEL-d2l-guides-inspector",
+ source: "/direct-to-llm/guides/premium/inspector",
+ destination: "/inspector",
+ },
];
// ---------------------------------------------------------------------------
@@ -707,6 +736,15 @@ const RETIRED_INTELLIGENCE_REDIRECTS: RedirectEntry[] = [
source: "/premium/observability",
destination: "/intelligence/overview",
},
+ // Same reason, different cause: the inspector page moved out of the folder
+ // in cc8c945893 instead of retiring, and never got a redirect. Without this
+ // entry P7 renames the legacy shell URL into a docs-host
+ // `/intelligence/inspector` that does not exist.
+ {
+ id: "INTEL-inspector-root",
+ source: "/premium/inspector",
+ destination: "/inspector",
+ },
];
// ---------------------------------------------------------------------------
diff --git a/showcase/harness/test/fixtures/redirect-decommission/cli-stdout-human.txt b/showcase/harness/test/fixtures/redirect-decommission/cli-stdout-human.txt
index 525690624c..861d948169 100644
--- a/showcase/harness/test/fixtures/redirect-decommission/cli-stdout-human.txt
+++ b/showcase/harness/test/fixtures/redirect-decommission/cli-stdout-human.txt
@@ -1,8 +1,8 @@
=== SEO Redirect Decommission Report (last 30 days) ===
-Total redirects defined: 390
+Total redirects defined: 395
Total hits: 12,537
Redirects with hits: 13
-Zero-hit candidates: 377
+Zero-hit candidates: 382
Top 10 most-hit redirects:
D1 5,432 hits
@@ -97,6 +97,11 @@ Decommission candidates (zero hits):
FI-premium /premium → /intelligence/overview
FI-reference /reference → /reference/v2
FI-troubleshooting /troubleshooting → /troubleshooting/common-issues
+ INTEL-d2l-guides-headless-ui /direct-to-llm/guides/premium/headless-ui → /intelligence/headless-ui
+ INTEL-d2l-guides-inspector /direct-to-llm/guides/premium/inspector → /inspector
+ INTEL-d2l-guides-observability /direct-to-llm/guides/premium/observability → /intelligence/overview
+ INTEL-d2l-guides-overview /direct-to-llm/guides/premium/overview → /intelligence/overview
+ INTEL-inspector-root /premium/inspector → /inspector
INTEL-observability-root /premium/observability → /intelligence/overview
L10 /coagents/tutorials → /langgraph-python/tutorials
L11 /coagents/videos → /langgraph-python/videos
diff --git a/showcase/harness/test/fixtures/redirect-decommission/cli-stdout.txt b/showcase/harness/test/fixtures/redirect-decommission/cli-stdout.txt
index b009764b70..6b231ae7b3 100644
--- a/showcase/harness/test/fixtures/redirect-decommission/cli-stdout.txt
+++ b/showcase/harness/test/fixtures/redirect-decommission/cli-stdout.txt
@@ -1,8 +1,8 @@
:bar_chart: *SEO Redirect Decommission Report* (last 30 days)
-Total redirects defined: 390
+Total redirects defined: 395
Total hits: 12,537
-:warning: *377 redirect(s) with zero hits — decommission candidates:*
+:warning: *382 redirect(s) with zero hits — decommission candidates:*
• B1
• B2
• B3
@@ -59,6 +59,11 @@ Total hits: 12,537
• FI-premium
• FI-reference
• FI-troubleshooting
+ • INTEL-d2l-guides-headless-ui
+ • INTEL-d2l-guides-inspector
+ • INTEL-d2l-guides-observability
+ • INTEL-d2l-guides-overview
+ • INTEL-inspector-root
• INTEL-observability-root
• L10
• L11
diff --git a/showcase/shell-docs/src/lib/__tests__/seo-redirects.test.ts b/showcase/shell-docs/src/lib/__tests__/seo-redirects.test.ts
index 5518ea21ef..90bf44727b 100644
--- a/showcase/shell-docs/src/lib/__tests__/seo-redirects.test.ts
+++ b/showcase/shell-docs/src/lib/__tests__/seo-redirects.test.ts
@@ -113,6 +113,76 @@ describe("seoRedirects", () => {
);
});
+ it("redirects the moved Intelligence inspector page instead of 404ing", () => {
+ // cc8c945893 renamed `(root)/premium/inspector.mdx` to
+ // `(root)/inspector.mdx` and added no redirect, so `/premium/inspector`
+ // 404'd from 2026-02-23 on. The page still exists, so these forward to it
+ // rather than falling back to the overview.
+ expect(seoRedirects).toEqual(
+ expect.arrayContaining([
+ {
+ id: "INTEL-inspector-root",
+ source: "/premium/inspector",
+ destination: "/inspector",
+ },
+ {
+ id: "INTEL-inspector×langgraph-python",
+ source: "/langgraph-python/premium/inspector",
+ destination: "/langgraph-python/inspector",
+ },
+ {
+ // Built-in Agent is served at the root, so its destination carries
+ // no framework prefix.
+ id: "INTEL-inspector×built-in-agent",
+ source: "/built-in-agent/premium/inspector",
+ destination: "/inspector",
+ },
+ ]),
+ );
+ });
+
+ it("redirects the deleted /direct-to-llm/guides/premium pages to their current equivalents", () => {
+ // Deleted in cc8c945893 without redirects. The R16
+ // `/direct-to-llm/:path*` wildcard drops them on the docs home, which
+ // reads as a working link while serving the wrong page.
+ expect(seoRedirects).toEqual(
+ expect.arrayContaining([
+ {
+ id: "INTEL-d2l-guides-overview",
+ source: "/direct-to-llm/guides/premium/overview",
+ destination: "/intelligence/overview",
+ },
+ {
+ id: "INTEL-d2l-guides-headless-ui",
+ source: "/direct-to-llm/guides/premium/headless-ui",
+ destination: "/intelligence/headless-ui",
+ },
+ {
+ id: "INTEL-d2l-guides-observability",
+ source: "/direct-to-llm/guides/premium/observability",
+ destination: "/intelligence/overview",
+ },
+ {
+ id: "INTEL-d2l-guides-inspector",
+ source: "/direct-to-llm/guides/premium/inspector",
+ destination: "/inspector",
+ },
+ ]),
+ );
+ });
+
+ it("matches the new exact sources before the premium rename wildcard", () => {
+ // The whole point of exact entries here: INTEL-rename-wild would rewrite
+ // these to `/intelligence/inspector`, which does not exist.
+ expect(matchesSeoRedirectSource("/premium/inspector")).toBe(true);
+ expect(
+ matchesSeoRedirectSource("/langgraph-python/premium/inspector"),
+ ).toBe(true);
+ expect(
+ matchesSeoRedirectSource("/direct-to-llm/guides/premium/headless-ui"),
+ ).toBe(true);
+ });
+
it("serves the Built-in Agent docs at the root: no redirect may capture a bare BIA page URL", () => {
// These bare URLs render BIA-authored pages directly now. A
// middleware entry whose source matches one of them would either
diff --git a/showcase/shell-docs/src/lib/seo-redirects.ts b/showcase/shell-docs/src/lib/seo-redirects.ts
index 09df72d5c8..36066ed364 100644
--- a/showcase/shell-docs/src/lib/seo-redirects.ts
+++ b/showcase/shell-docs/src/lib/seo-redirects.ts
@@ -465,6 +465,35 @@ const SPECIFIC_FRAMEWORK: RedirectEntry[] = [
source: "/direct-to-llm/guides/mcp",
destination: "/build-with-agents",
},
+ // `/direct-to-llm/guides/premium/*` pages were deleted in cc8c945893
+ // ("refactor(docs): optimize structure, content and navigability",
+ // 2026-02-23) without redirects. The R16 `/direct-to-llm/:path*` wildcard
+ // strips the prefix and the remainder falls through to the docs home, so
+ // the page is lost rather than 404'd — quieter and harder to notice.
+ // Exact entries land each one on its current equivalent in one hop.
+ {
+ id: "INTEL-d2l-guides-overview",
+ source: "/direct-to-llm/guides/premium/overview",
+ destination: "/intelligence/overview",
+ },
+ {
+ id: "INTEL-d2l-guides-headless-ui",
+ source: "/direct-to-llm/guides/premium/headless-ui",
+ destination: "/intelligence/headless-ui",
+ },
+ {
+ // The observability page is retired; the overview is its standing
+ // destination everywhere else (INTEL-observability-*).
+ id: "INTEL-d2l-guides-observability",
+ source: "/direct-to-llm/guides/premium/observability",
+ destination: "/intelligence/overview",
+ },
+ {
+ // Inspector moved out of the Intelligence folder rather than retiring.
+ id: "INTEL-d2l-guides-inspector",
+ source: "/direct-to-llm/guides/premium/inspector",
+ destination: "/inspector",
+ },
];
// ---------------------------------------------------------------------------
@@ -1024,6 +1053,22 @@ const RETIRED_INTELLIGENCE_REDIRECTS: RedirectEntry[] = [
"intelligence/overview",
),
})),
+ // The inspector page MOVED out of the Intelligence folder — it was not
+ // retired. cc8c945893 renamed `(root)/premium/inspector.mdx` to
+ // `(root)/inspector.mdx` (R100, identical content) and added no redirect,
+ // so `/premium/inspector` has 404'd ever since. It needs an exact entry for
+ // the same reason observability does: INTEL-rename-wild would rewrite it to
+ // a nonexistent `/intelligence/inspector`.
+ {
+ id: "INTEL-inspector-root",
+ source: "/premium/inspector",
+ destination: "/inspector",
+ },
+ ...PREMIUM_URL_FRAMEWORKS.map((framework) => ({
+ id: `INTEL-inspector×${framework}`,
+ source: `/${framework}/premium/inspector`,
+ destination: destinationPath(canonicalSlug(framework), "inspector"),
+ })),
];
// ---------------------------------------------------------------------------
diff --git a/showcase/shell/src/lib/seo-redirects.ts b/showcase/shell/src/lib/seo-redirects.ts
index 8406e63be2..1cc87d02f9 100644
--- a/showcase/shell/src/lib/seo-redirects.ts
+++ b/showcase/shell/src/lib/seo-redirects.ts
@@ -414,6 +414,35 @@ const SPECIFIC_FRAMEWORK: RedirectEntry[] = [
source: "/direct-to-llm/guides/mcp",
destination: "/built-in-agent/coding-agents",
},
+ // `/direct-to-llm/guides/premium/*` pages were deleted in cc8c945893
+ // ("refactor(docs): optimize structure, content and navigability",
+ // 2026-02-23) without redirects. The R16 `/direct-to-llm/:path*` wildcard
+ // strips the prefix and the remainder falls through to the docs home, so
+ // the page is lost rather than 404'd — quieter and harder to notice.
+ // Exact entries land each one on its current equivalent in one hop.
+ {
+ id: "INTEL-d2l-guides-overview",
+ source: "/direct-to-llm/guides/premium/overview",
+ destination: "/intelligence/overview",
+ },
+ {
+ id: "INTEL-d2l-guides-headless-ui",
+ source: "/direct-to-llm/guides/premium/headless-ui",
+ destination: "/intelligence/headless-ui",
+ },
+ {
+ // The observability page is retired; the overview is its standing
+ // destination everywhere else (INTEL-observability-*).
+ id: "INTEL-d2l-guides-observability",
+ source: "/direct-to-llm/guides/premium/observability",
+ destination: "/intelligence/overview",
+ },
+ {
+ // Inspector moved out of the Intelligence folder rather than retiring.
+ id: "INTEL-d2l-guides-inspector",
+ source: "/direct-to-llm/guides/premium/inspector",
+ destination: "/inspector",
+ },
];
// ---------------------------------------------------------------------------
@@ -709,6 +738,15 @@ const RETIRED_INTELLIGENCE_REDIRECTS: RedirectEntry[] = [
source: "/premium/observability",
destination: "/intelligence/overview",
},
+ // Same reason, different cause: the inspector page moved out of the folder
+ // in cc8c945893 instead of retiring, and never got a redirect. Without this
+ // entry P7 renames the legacy shell URL into a docs-host
+ // `/intelligence/inspector` that does not exist.
+ {
+ id: "INTEL-inspector-root",
+ source: "/premium/inspector",
+ destination: "/inspector",
+ },
];
// ---------------------------------------------------------------------------
From 452356b5e8c73fbf356ea6b2ae2b6379d1eb5fe2 Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Wed, 2 Sep 2026 15:57:16 -0700
Subject: [PATCH 02/57] feat(web-inspector): unify locked feature pages
---
.../dev/threads-state-lab.spec.ts | 67 ++-
.../src/__tests__/inspector-metadata.spec.ts | 34 +-
.../src/__tests__/inspector-pop-out.spec.ts | 61 +-
.../src/__tests__/metadata-telemetry.spec.ts | 50 +-
.../__tests__/threads-example-video.spec.ts | 32 +-
.../src/__tests__/threads-states.spec.ts | 142 ++---
.../src/__tests__/threads-telemetry.spec.ts | 67 ++-
.../src/__tests__/web-inspector.spec.ts | 150 ++---
packages/web-inspector/src/index.ts | 506 +++++++---------
.../web-inspector/src/styles/generated.css | 2 +-
.../web-inspector/src/styles/tailwind.css | 552 ++++++++----------
11 files changed, 792 insertions(+), 871 deletions(-)
diff --git a/packages/web-inspector/dev/threads-state-lab.spec.ts b/packages/web-inspector/dev/threads-state-lab.spec.ts
index cf8e733d39..bf458724b6 100644
--- a/packages/web-inspector/dev/threads-state-lab.spec.ts
+++ b/packages/web-inspector/dev/threads-state-lab.spec.ts
@@ -347,8 +347,12 @@ async function requestCounters(
/** Returns the expected action label for trusted fixture metadata. */
function expectedActionLabel(scenario: ThreadsStateScenario): string | null {
+ if (scenario.capability !== "enabled") return null;
const kind = scenario.inspectorMetadata?.action?.kind;
- if (kind === "manage_plan") {
+ if (
+ scenario.inspectorMetadata?.license?.state === "valid" &&
+ kind === "manage_plan"
+ ) {
const usage = scenario.inspectorMetadata?.usage;
if (usage?.limit.kind === "finite") {
const warningThreshold =
@@ -357,8 +361,6 @@ function expectedActionLabel(scenario: ThreadsStateScenario): string | null {
}
return "Manage Your Plan";
}
- if (kind === "enable_intelligence") return "Enable Intelligence";
- if (kind === "renew") return "Renew";
return null;
}
@@ -382,9 +384,10 @@ function expectedOverviewCopy(
if (scenario.data === "error") return null;
if (scenario.runtimeInfo.licenseStatus === "none") {
return {
- heading: "Enable Intelligence to inspect Threads.",
+ heading:
+ "Production-grade chat threads without the complexity. Self hostable.",
description:
- "Persist conversations and inspect saved thread history from the Inspector.",
+ "Chat threads that go beyond text with generative UI and multimodal inputs, built to replay missed events and stay in sync across tabs, sessions, and devices.",
};
}
if (scenario.runtimeInfo.licenseStatus === "expired") {
@@ -1589,6 +1592,37 @@ test("drives the real Core, Inspector, stores, surfaces, and ledger for all 37 r
`${key}: setup prompt label`,
).toBe("Copy setup prompt");
}
+ const lockedVideo = collectDeep(
+ root,
+ '[data-inspector-feature-video="threads"]',
+ );
+ const lockedEngineerAction = collectDeep(
+ root,
+ '[data-inspector-locked-feature-talk="threads"]',
+ );
+ expect(lockedVideo, `${key}: locked video presence`).toHaveLength(
+ expectsSetup ? 1 : 0,
+ );
+ expect(
+ lockedEngineerAction,
+ `${key}: locked engineer CTA presence`,
+ ).toHaveLength(expectsSetup ? 1 : 0);
+ if (expectsSetup) {
+ expect(
+ lockedVideo[0]?.getAttribute("src"),
+ `${key}: locked video URL`,
+ ).toBe(
+ "https://www.loom.com/embed/79817778d29e490c97225127d2f17b3a?hide_owner=true&hide_share=true&hide_title=true&hideEmbedTopBar=true&hide_speed=true",
+ );
+ expect(
+ lockedEngineerAction[0]?.textContent?.trim(),
+ `${key}: locked engineer CTA label`,
+ ).toBe("Talk to an Engineer");
+ }
+ expect(
+ collectDeep(root, "cpk-thread-list"),
+ `${key}: thread list presence`,
+ ).toHaveLength(expectsSetup ? 0 : 1);
const usage = scenario.inspectorMetadata?.usage;
const threadCount = collectDeep(
@@ -1596,9 +1630,9 @@ test("drives the real Core, Inspector, stores, surfaces, and ledger for all 37 r
"[data-inspector-thread-count]",
);
expect(threadCount, `${key}: usage presence`).toHaveLength(
- usage ? 1 : 0,
+ usage && !expectsSetup ? 1 : 0,
);
- if (usage) {
+ if (usage && !expectsSetup) {
const used = String(usage.used);
expect(text, `${key}: used count`).toContain(used);
const progress = collectDeep(root, "progress");
@@ -1715,7 +1749,7 @@ test("drives the real Core, Inspector, stores, surfaces, and ledger for all 37 r
if (scenario.data === "error") {
expect(examples, `${key}: list-error examples`).toHaveLength(0);
} else if (
- scenario.capability !== "enabled" ||
+ scenario.capability === "enabled" &&
scenario.data === "zero"
) {
expect(examples, `${key}: local examples`).toHaveLength(3);
@@ -1747,7 +1781,7 @@ test("drives the real Core, Inspector, stores, surfaces, and ledger for all 37 r
expect(await requestCounters(lab.origin, scenario), key).toEqual(
scenario.expectedRequests,
);
- } else {
+ } else if (scenario.capability === "enabled") {
expect(examples, `${key}: no local examples`).toHaveLength(0);
for (const thread of scenario.threads) {
expect(
@@ -1829,6 +1863,21 @@ test("drives the real Core, Inspector, stores, surfaces, and ledger for all 37 r
},
{ timeout: 5_000, interval: 20 },
);
+ } else {
+ expect(examples, `${key}: locked examples`).toHaveLength(0);
+ expect(
+ collectDeep(root, "cpk-thread-details"),
+ `${key}: locked thread details`,
+ ).toHaveLength(0);
+ for (const thread of scenario.threads) {
+ expect(
+ inspectorText(inspector),
+ `${key}: hidden locked thread ${thread.id}`,
+ ).not.toContain(thread.name);
+ }
+ expect(await requestCounters(lab.origin, scenario), key).toEqual(
+ scenario.expectedRequests,
+ );
}
} finally {
stopThreadsStateLabClient(core, inspector);
diff --git a/packages/web-inspector/src/__tests__/inspector-metadata.spec.ts b/packages/web-inspector/src/__tests__/inspector-metadata.spec.ts
index 55a97bfd1c..90cc97ef75 100644
--- a/packages/web-inspector/src/__tests__/inspector-metadata.spec.ts
+++ b/packages/web-inspector/src/__tests__/inspector-metadata.spec.ts
@@ -295,18 +295,17 @@ test.each([
);
test.each([
- ["valid", "manage_plan", "Finish setting up Rich Threads", undefined],
+ ["valid", "manage_plan", "Finish setting up Rich Threads"],
[
"none",
"enable_intelligence",
- "Enable Intelligence to inspect Threads.",
- "Enable Intelligence",
+ "Production-grade chat threads without the complexity. Self hostable.",
],
- ["expired", "renew", "Renew Intelligence to inspect Threads.", "Renew"],
- ["unknown", "manage_plan", "Threads are unavailable.", undefined],
+ ["expired", "renew", "Renew Intelligence to inspect Threads."],
+ ["unknown", "manage_plan", "Threads are unavailable."],
] as const)(
- "locked Threads use %s license copy and action placement",
- async (licenseState, actionKind, heading, actionLabel) => {
+ "locked Threads use %s license copy and the unified actions",
+ async (licenseState, actionKind, heading) => {
const context = await setup({
metadata: fullMetadata(licenseState, actionKind),
runtimeLicense: licenseState,
@@ -320,13 +319,15 @@ test.each([
const action = root.querySelector(
'[data-inspector-action-placement="locked"]',
);
+ const talk = root.querySelector(
+ '[data-inspector-locked-feature-talk="threads"]',
+ );
expect(root.textContent).toContain(heading);
- expect(action?.textContent?.trim()).toBe(actionLabel);
- if (actionLabel !== undefined) {
- expect(action?.href).toBe(
- `https://cloud.copilotkit.ai/actions/${actionKind}`,
- );
- }
+ expect(action).toBeNull();
+ expect(talk?.textContent?.trim()).toBe("Talk to an Engineer");
+ expect(
+ root.querySelector('[data-inspector-feature-setup-prompt="threads"]'),
+ ).not.toBeNull();
} finally {
context.teardown();
}
@@ -522,12 +523,9 @@ test("metadata usage stays independent from Threads capability and debug navigat
Object.prototype.hasOwnProperty.call(usage ?? {}, "expiringSoonCount"),
).toBe(true);
expect(root.textContent).toContain(
- "Enable Intelligence to inspect Threads.",
- );
- expect(lockedAction?.textContent?.trim()).toBe("Enable Intelligence");
- expect(lockedAction?.href).toBe(
- "https://cloud.copilotkit.ai/actions/enable_intelligence",
+ "Production-grade chat threads without the complexity. Self hostable.",
);
+ expect(lockedAction).toBeNull();
expect(
context.requests.filter((url) => url.includes("/threads?")),
).toHaveLength(0);
diff --git a/packages/web-inspector/src/__tests__/inspector-pop-out.spec.ts b/packages/web-inspector/src/__tests__/inspector-pop-out.spec.ts
index bd47e701de..771d8477c9 100644
--- a/packages/web-inspector/src/__tests__/inspector-pop-out.spec.ts
+++ b/packages/web-inspector/src/__tests__/inspector-pop-out.spec.ts
@@ -588,7 +588,7 @@ describe("Inspector pop-out", () => {
}
}, 15_000);
- it("renders the threads list in the pop-out document", async () => {
+ it("renders the locked Threads page in the pop-out document", async () => {
const context = await setup();
try {
await context.open();
@@ -596,10 +596,17 @@ describe("Inspector pop-out", () => {
await context.clickDetach();
await waitFor(
- () => context.popDoc.querySelector("cpk-thread-list") !== null,
- "the threads list in the pop-out document",
+ () =>
+ context.popDoc.querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ ) !== null,
+ "the locked Threads page in the pop-out document",
);
- expect(context.popDoc.querySelector("cpk-thread-list")).not.toBeNull();
+ expect(
+ context.popDoc.querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ )?.textContent,
+ ).toContain("Rich Threads");
} finally {
context.teardown();
}
@@ -611,41 +618,41 @@ describe("Inspector pop-out", () => {
await context.open();
await context.selectLeaf("threads");
- const inPageThreadList = requireElement(
- requireShadow(context.inspector).querySelector("cpk-thread-list"),
- "the in-page threads list",
+ const inPageLockedFeature = requireElement(
+ requireShadow(context.inspector).querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ ),
+ "the in-page locked Threads page",
);
- const portableThreadListStyle = requireElement(
- inPageThreadList.shadowRoot?.querySelector("style"),
- "the portable threads-list stylesheet",
- );
- expect(portableThreadListStyle.textContent).toContain(".cpk-tl");
await context.clickDetach();
await waitFor(
- () => context.popDoc.querySelector("cpk-thread-list") !== null,
- "the threads list to move into the pop-out",
+ () =>
+ context.popDoc.querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ ) !== null,
+ "the locked Threads page to move into the pop-out",
);
- expect(context.popDoc.querySelector("cpk-thread-list")).toBe(
- inPageThreadList,
- );
- expect(inPageThreadList.shadowRoot?.querySelector("style")).toBe(
- portableThreadListStyle,
- );
- expect(portableThreadListStyle.ownerDocument).toBe(context.popDoc);
+ expect(
+ context.popDoc.querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ ),
+ ).toBe(inPageLockedFeature);
context.firePageHide();
await context.inspector.updateComplete;
await waitFor(
() =>
- requireShadow(context.inspector).querySelector("cpk-thread-list") !==
- null,
- "the threads list to return to the app page",
+ requireShadow(context.inspector).querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ ) !== null,
+ "the locked Threads page to return to the app page",
);
expect(
- requireShadow(context.inspector).querySelector("cpk-thread-list"),
- ).toBe(inPageThreadList);
- expect(portableThreadListStyle.ownerDocument).toBe(document);
+ requireShadow(context.inspector).querySelector(
+ '[data-inspector-locked-feature="threads"]',
+ ),
+ ).toBe(inPageLockedFeature);
} finally {
context.teardown();
}
diff --git a/packages/web-inspector/src/__tests__/metadata-telemetry.spec.ts b/packages/web-inspector/src/__tests__/metadata-telemetry.spec.ts
index c2859a2b91..d452ba9b92 100644
--- a/packages/web-inspector/src/__tests__/metadata-telemetry.spec.ts
+++ b/packages/web-inspector/src/__tests__/metadata-telemetry.spec.ts
@@ -402,7 +402,7 @@ test("Threads footer action emits one impression per visible transition and one
}
});
-test("a valid manage action emits one footer impression when Threads endpoints are locked", async () => {
+test("locked Threads suppress the footer action and its impression", async () => {
const context = await setup({
metadataResponses: [fullMetadata()],
threadsAvailable: false,
@@ -414,13 +414,12 @@ test("a valid manage action emits one footer impression when Threads endpoints a
const root = requireShadowRoot(context.inspector);
const footer = root.querySelectorAll("[data-inspector-threads-footer]");
const action = root.querySelectorAll("[data-inspector-threads-footer] a");
- expect(footer).toHaveLength(1);
- expect(action).toHaveLength(1);
+ expect(footer).toHaveLength(0);
+ expect(action).toHaveLength(0);
const actionViews = metadataBodies(context).filter(
({ properties }) => properties.module === "action",
);
- expect(actionViews).toHaveLength(1);
- expect(actionViews[0]?.properties.action_placement).toBe("threads_footer");
+ expect(actionViews).toHaveLength(0);
expect(
context.telemetryBodies.filter(
({ event }) => event === TELEMETRY_EVENTS.metadataActionClicked,
@@ -439,16 +438,6 @@ const metadataActionCases = [
label: "Manage plan",
expectedKind: "manage_plan",
},
- {
- name: "renew",
- metadata: fullMetadata({
- licenseState: "expired",
- actionKind: "renew",
- }),
- threadsAvailable: false,
- label: "Renew",
- expectedKind: "renew",
- },
] satisfies ReadonlyArray<{
name: string;
metadata: InspectorMetadataV1;
@@ -503,7 +492,7 @@ test.each(metadataActionCases)(
},
);
-test("enable Intelligence stays on its existing event without a generic double count", async () => {
+test("locked metadata actions yield to the unified engineer CTA", async () => {
const context = await setup({
metadataResponses: [
fullMetadata({ licenseState: "none", actionKind: "enable_intelligence" }),
@@ -515,13 +504,7 @@ test("enable Intelligence stays on its existing event without a generic double c
await context.selectTab("Threads");
expect(
metadataBodies(context).map(({ properties }) => properties.module),
- ).toEqual(["identity", "plan", "identity", "plan", "action"]);
-
- expect(metadataBodies(context).at(-1)?.properties).toMatchObject({
- module: "action",
- action_kind: "enable_intelligence",
- license_bucket: "none",
- });
+ ).toEqual(["identity", "plan", "identity", "plan"]);
const toggleSettings = async (): Promise => {
const settings = context.inspector.shadowRoot?.querySelector(
@@ -537,7 +520,7 @@ test("enable Intelligence stays on its existing event without a generic double c
metadataBodies(context).filter(
({ properties }) => properties.module === "action",
),
- ).toHaveLength(2);
+ ).toHaveLength(0);
await context.selectTab("Agent");
await context.selectTab("AG-UI Events");
@@ -546,24 +529,23 @@ test("enable Intelligence stays on its existing event without a generic double c
metadataBodies(context).filter(
({ properties }) => properties.module === "action",
),
- ).toHaveLength(3);
+ ).toHaveLength(0);
const action =
context.inspector.shadowRoot?.querySelector(
- '[data-inspector-action-placement="locked"]',
+ '[data-inspector-locked-feature-talk="threads"]',
);
- if (!action) throw new Error("Enable Intelligence action was not rendered");
+ if (!action) throw new Error("Talk to an Engineer action was not rendered");
action.dispatchEvent(new Event("click"));
action.dispatchEvent(new Event("click"));
await Promise.resolve();
- const enableEvents = context.telemetryBodies.filter(
- ({ event }) =>
- event === "oss.inspector.threads_intelligence_signup_clicked",
+ const talkEvents = context.telemetryBodies.filter(
+ ({ event }) => event === TELEMETRY_EVENTS.threadsTalkToEngineerClicked,
);
- expect(enableEvents).toHaveLength(2);
- expect(enableEvents.map(({ event }) => event)).toEqual([
- "oss.inspector.threads_intelligence_signup_clicked",
- "oss.inspector.threads_intelligence_signup_clicked",
+ expect(talkEvents).toHaveLength(2);
+ expect(talkEvents.map(({ event }) => event)).toEqual([
+ TELEMETRY_EVENTS.threadsTalkToEngineerClicked,
+ TELEMETRY_EVENTS.threadsTalkToEngineerClicked,
]);
expect(
context.telemetryBodies.filter(
diff --git a/packages/web-inspector/src/__tests__/threads-example-video.spec.ts b/packages/web-inspector/src/__tests__/threads-example-video.spec.ts
index 42a33d5cab..84c2ddae5c 100644
--- a/packages/web-inspector/src/__tests__/threads-example-video.spec.ts
+++ b/packages/web-inspector/src/__tests__/threads-example-video.spec.ts
@@ -515,7 +515,7 @@ test("loaded data fades in and resolved guarded playback enters playing", async
}
});
-test("locked reduced motion defers then loads the same asset without autoplay", async () => {
+test("locked Threads use the Rich Threads Loom embed without starting the native demo lifecycle", async () => {
const harness = await setupFixture({
mode: "locked",
reducedMotion: true,
@@ -524,23 +524,23 @@ test("locked reduced motion defers then loads the same asset without autoplay",
});
try {
const root = harness.inspector.shadowRoot!;
- const video = requireVideo(root);
- const routesBeforeGate = harness.routes();
+ const video = root.querySelector(
+ '[data-inspector-feature-video="threads"]',
+ );
- expect(video.hasAttribute("src")).toBe(false);
- expect(requireDemoControl(root).textContent?.trim()).toBe("Play demo");
- expectAllExamples(root);
-
- await harness.fireGate();
-
- const control = requireDemoControl(root);
- expect(video.getAttribute("src")).toBe(VIDEO_URL);
- expect(video.autoplay).toBe(false);
- expect(video.hasAttribute("autoplay")).toBe(false);
+ expect(video?.src).toBe(
+ "https://www.loom.com/embed/79817778d29e490c97225127d2f17b3a?hide_owner=true&hide_share=true&hide_title=true&hideEmbedTopBar=true&hide_speed=true",
+ );
+ expect(video?.title).toBe("Rich Threads overview");
+ expect(root.querySelector(".cpk-threads-overview-video")).toBeNull();
+ expect(
+ root.querySelector(".cpk-threads-overview-video-control"),
+ ).toBeNull();
+ expect(exampleRows(root)).toHaveLength(0);
+ expect(root.querySelector("cpk-thread-list")).toBeNull();
+ expect(root.textContent).toContain("Rich Threads");
expect(harness.play).not.toHaveBeenCalled();
- expect(control.textContent?.trim()).toBe("Play demo");
- expect(control.getAttribute("aria-pressed")).toBe("true");
- expect(harness.routes()).toEqual(routesBeforeGate);
+ expect(harness.routes()).toEqual(ZERO_ROUTES);
} finally {
await harness.teardown();
}
diff --git a/packages/web-inspector/src/__tests__/threads-states.spec.ts b/packages/web-inspector/src/__tests__/threads-states.spec.ts
index c373086fd0..e91f9cde14 100644
--- a/packages/web-inspector/src/__tests__/threads-states.spec.ts
+++ b/packages/web-inspector/src/__tests__/threads-states.spec.ts
@@ -985,7 +985,7 @@ const lockedCapabilityCases = [
}>;
test.each(lockedCapabilityCases)(
- "locked Threads reuse the local demo frame for $name without real routes",
+ "locked Threads render the full-page Rich Threads gate for $name without real routes",
async (case_) => {
const harness = await setupSettledState({
endpoints: case_.endpoints,
@@ -1004,40 +1004,44 @@ test.each(lockedCapabilityCases)(
});
try {
const root = harness.inspector.shadowRoot!;
- const rows = harness.rows();
-
- expect(rows.map((row) => row.textContent)).toEqual([
- expect.stringContaining("Realtime thread sync"),
- expect.stringContaining("Manage saved conversations"),
- expect.stringContaining("Inspect durable run history"),
- ]);
- expect(harness.threadList().threads.map((thread) => thread.id)).toEqual([
- "example-realtime-sync",
- "example-manage-history",
- "example-inspect-runs",
- ]);
- expect(harness.threadList().shadowRoot?.textContent).not.toContain(
- "Persisted support thread",
- );
+ expect(root.querySelector("cpk-thread-list")).toBeNull();
+ expect(root.textContent).toContain("Rich Threads");
expect(root.textContent).toContain(
- "Enable Intelligence to inspect Threads.",
+ "Production-grade chat threads without the complexity. Self hostable.",
+ );
+ const video = root.querySelector(
+ '[data-inspector-feature-video="threads"]',
+ );
+ expect(video?.src).toBe(
+ "https://www.loom.com/embed/79817778d29e490c97225127d2f17b3a?hide_owner=true&hide_share=true&hide_title=true&hideEmbedTopBar=true&hide_speed=true",
+ );
+ const outline = root.querySelector(
+ '[data-inspector-feature-outline="threads"]',
+ );
+ expect(outline?.getAttribute("aria-label")).toBe(
+ "Rich Threads capabilities",
+ );
+ expect(outline?.textContent).toContain(
+ "The whole conversation comes back",
+ );
+ expect(outline?.textContent).toContain(
+ "Generated UI stays in the thread",
+ );
+ expect(outline?.textContent).toContain("Return without starting over");
+ expect(outline?.textContent).toContain(
+ "Infrastructure you do not have to rebuild",
);
- expect(
- root.querySelector(".cpk-threads-overview-video-frame"),
- ).not.toBeNull();
expect(root.textContent).not.toContain("Learn how Threads work");
expect(root.textContent).not.toContain(
"Explore self-hosted Intelligence",
);
const action = root.querySelector(
- '[data-inspector-action-placement="locked"]',
+ '[data-inspector-locked-feature-talk="threads"]',
);
- expect(action?.textContent?.trim()).toBe("Enable Intelligence");
- expect(action?.href).toBe("https://cloud.copilotkit.ai/actions/enable");
- expect(
- root.querySelector("[data-inspector-threads-footer]"),
- ).not.toBeNull();
- expect(root.textContent).toContain("0 / 200 Threads");
+ expect(action?.textContent?.trim()).toBe("Talk to an Engineer");
+ expect(new URL(action!.href).pathname).toBe("/talk-to-an-engineer");
+ expect(root.querySelector("[data-inspector-threads-footer]")).toBeNull();
+ expect(root.textContent).not.toContain("0 / 200 Threads");
expect(harness.routes()).toEqual(ZERO_ROUTES);
const lockedEvents = telemetryFor(
harness.telemetryBodies,
@@ -1059,23 +1063,13 @@ test.each(lockedCapabilityCases)(
runtime_url_type: "remote",
telemetry_disabled: false,
});
- expect(exampleEvents).toHaveLength(3);
- expect(
- exampleEvents.map(({ properties }) => properties.example_kind),
- ).toEqual(["realtime_sync", "manage_history", "inspect_runs"]);
+ expect(exampleEvents).toEqual([]);
expect(
telemetryFor(
harness.telemetryBodies,
TELEMETRY_EVENTS.threadsEmptyEnabledViewed,
),
).toEqual([]);
-
- await exerciseLocalExamples(harness, ZERO_ROUTES, "Locked");
-
- await harness.selectRow("Inspect durable run history");
- expect(root.textContent).toContain(
- "Enable Intelligence to inspect Threads.",
- );
expect(harness.routes()).toEqual(ZERO_ROUTES);
} finally {
await harness.teardown();
@@ -1091,8 +1085,6 @@ type LockedActionCase = Readonly<{
actionUrl?: string;
heading: string;
description?: string;
- bodyLabel?: string;
- footerLabel?: string;
}>;
const lockedActionCases: ReadonlyArray = [
@@ -1104,7 +1096,6 @@ const lockedActionCases: ReadonlyArray = [
actionUrl: "https://cloud.copilotkit.ai/actions/manage",
heading: "Finish setting up Rich Threads",
description: "Copy this prompt into your coding agent to finish the setup.",
- footerLabel: "Manage Your Plan",
},
{
name: "none enable action",
@@ -1112,8 +1103,10 @@ const lockedActionCases: ReadonlyArray = [
runtimeLicense: "none",
actionKind: "enable_intelligence",
actionUrl: "https://cloud.copilotkit.ai/actions/enable",
- heading: "Enable Intelligence to inspect Threads.",
- bodyLabel: "Enable Intelligence",
+ heading:
+ "Production-grade chat threads without the complexity. Self hostable.",
+ description:
+ "Chat threads that go beyond text with generative UI and multimodal inputs, built to replay missed events and stay in sync across tabs, sessions, and devices.",
},
{
name: "expired renew action",
@@ -1122,7 +1115,6 @@ const lockedActionCases: ReadonlyArray = [
actionKind: "renew",
actionUrl: "https://cloud.copilotkit.ai/actions/renew",
heading: "Renew Intelligence to inspect Threads.",
- bodyLabel: "Renew",
},
{
name: "expired manage action",
@@ -1131,7 +1123,6 @@ const lockedActionCases: ReadonlyArray = [
actionKind: "manage_plan",
actionUrl: "https://cloud.copilotkit.ai/actions/manage-expired",
heading: "Renew Intelligence to inspect Threads.",
- bodyLabel: "Manage Your Plan",
},
{
name: "unknown action",
@@ -1145,7 +1136,8 @@ const lockedActionCases: ReadonlyArray = [
name: "missing action",
metadataState: "none",
runtimeLicense: "none",
- heading: "Enable Intelligence to inspect Threads.",
+ heading:
+ "Production-grade chat threads without the complexity. Self hostable.",
},
{
name: "unsafe matched action",
@@ -1153,7 +1145,8 @@ const lockedActionCases: ReadonlyArray = [
runtimeLicense: "none",
actionKind: "enable_intelligence",
actionUrl: "javascript:alert(1)",
- heading: "Enable Intelligence to inspect Threads.",
+ heading:
+ "Production-grade chat threads without the complexity. Self hostable.",
},
{
name: "known metadata Runtime conflict",
@@ -1166,7 +1159,7 @@ const lockedActionCases: ReadonlyArray = [
];
test.each(lockedActionCases)(
- "locked action matrix keeps only the trusted $name",
+ "locked action matrix keeps the unified CTAs for $name",
async (case_) => {
const harness = await setupSettledState({
endpoints: DISABLED_ENDPOINTS,
@@ -1181,11 +1174,11 @@ test.each(lockedActionCases)(
});
try {
const root = harness.inspector.shadowRoot!;
- const bodyAction = root.querySelector(
+ const metadataBodyAction = root.querySelector(
'[data-inspector-action-placement="locked"]',
);
- const footerAction = root.querySelector(
- '[data-inspector-action-placement="threads-footer"]',
+ const talkAction = root.querySelector(
+ '[data-inspector-locked-feature-talk="threads"]',
);
const promptAction = root.querySelector(
"[data-inspector-threads-setup-prompt]",
@@ -1195,27 +1188,32 @@ test.each(lockedActionCases)(
if (case_.description) {
expect(root.textContent).toContain(case_.description);
}
- expect(harness.rows()).toHaveLength(3);
+ expect(root.querySelector("cpk-thread-list")).toBeNull();
+ expect(root.textContent).toContain("Rich Threads");
expect(promptAction?.textContent?.trim()).toBe("Copy setup prompt");
- expect(bodyAction?.textContent?.trim()).toBe(case_.bodyLabel);
- expect(footerAction?.textContent?.trim()).toBe(case_.footerLabel);
- if (case_.bodyLabel) {
- expect(bodyAction?.href).toBe(case_.actionUrl);
- expect(bodyAction?.target).toBe("_blank");
- expect(bodyAction?.rel.split(/\s+/)).toContain("noopener");
- } else {
- expect(bodyAction).toBeNull();
- }
+ expect(metadataBodyAction).toBeNull();
+ expect(talkAction?.textContent?.trim()).toBe("Talk to an Engineer");
+ expect(new URL(talkAction!.href).pathname).toBe("/talk-to-an-engineer");
+ expect(talkAction?.target).toBe("_blank");
+ expect(talkAction?.rel.split(/\s+/)).toContain("noopener");
expect(promptAction?.type).toBe("button");
+ expect(promptAction?.classList.contains("inspector-account-cta")).toBe(
+ true,
+ );
+ expect(
+ promptAction?.classList.contains("cpk-threads-overview-action-primary"),
+ ).toBe(false);
expect(promptAction?.getAttribute("aria-label")).toBe(
"Copy setup prompt for Threads",
);
- if (case_.footerLabel) {
- expect(footerAction?.href).toBe(case_.actionUrl);
- } else {
- expect(footerAction).toBeNull();
- }
- await exerciseLocalExamples(harness, ZERO_ROUTES, case_.name);
+ expect(
+ root.querySelector('.cpk-locked-feature-icon svg[viewBox="0 0 15 15"]'),
+ ).not.toBeNull();
+ expect(
+ root.querySelector(
+ '[data-inspector-action-placement="threads-footer"]',
+ ),
+ ).toBeNull();
expect(harness.routes()).toEqual(ZERO_ROUTES);
} finally {
await harness.teardown();
@@ -1363,7 +1361,7 @@ const footerCases = footerBodyStates.flatMap((state) =>
);
test.each(footerCases)(
- "the $module.name footer stays last in the $state body",
+ "the $state body handles the $module.name footer placement",
async ({ state, module }) => {
const metadata = inspectorMetadata(
"valid",
@@ -1377,6 +1375,11 @@ test.each(footerCases)(
const footers = root.querySelectorAll(
"footer[data-inspector-threads-footer]",
);
+ if (state === "locked") {
+ expect(footers).toHaveLength(0);
+ expect(root.textContent).toContain("Finish setting up Rich Threads");
+ return;
+ }
const footer = footers[0];
expect(footers).toHaveLength(1);
expect(footer?.parentElement?.lastElementChild).toBe(footer);
@@ -1397,9 +1400,6 @@ test.each(footerCases)(
expect(action).toBeNull();
}
- if (state === "locked") {
- expect(root.textContent).toContain("Finish setting up Rich Threads");
- }
if (state === "loading") {
expect(root.querySelector('[role="status"]')).not.toBeNull();
}
diff --git a/packages/web-inspector/src/__tests__/threads-telemetry.spec.ts b/packages/web-inspector/src/__tests__/threads-telemetry.spec.ts
index 1faa779b09..7a2410685b 100644
--- a/packages/web-inspector/src/__tests__/threads-telemetry.spec.ts
+++ b/packages/web-inspector/src/__tests__/threads-telemetry.spec.ts
@@ -846,14 +846,6 @@ const placementCases = [
renderedPlacement: "threads-footer",
telemetryPlacement: "threads_footer",
},
- {
- name: "locked body",
- endpoints: LOCKED_ENDPOINTS,
- actionKind: "renew",
- licenseState: "expired",
- renderedPlacement: "locked",
- telemetryPlacement: "threads_locked",
- },
] satisfies readonly PlacementCase[];
test.each(placementCases)(
@@ -924,6 +916,59 @@ test.each(placementCases)(
},
);
+test("the locked feature engineer CTA serializes its coarse placement", async () => {
+ const lockedCase = {
+ name: "locked body",
+ endpoints: LOCKED_ENDPOINTS,
+ actionKind: "renew",
+ licenseState: "expired",
+ renderedPlacement: "locked",
+ telemetryPlacement: "threads_locked",
+ } as const;
+ const harness = await setup({
+ endpoints: lockedCase.endpoints,
+ metadataResponses: [metadataWithAction(lockedCase)],
+ threadsByAgent: { alpha: [] },
+ });
+ try {
+ await harness.open();
+
+ const root = requireElement(
+ harness.inspector.shadowRoot,
+ "Web Inspector shadow root was not rendered",
+ );
+ const action = requireElement(
+ root.querySelector(
+ '[data-inspector-locked-feature-talk="threads"]',
+ ),
+ "Locked feature engineer action was not rendered",
+ );
+
+ action.dispatchEvent(new Event("click"));
+ await harness.flush();
+
+ const clicked = harness.telemetryFor(
+ TELEMETRY_EVENTS.threadsTalkToEngineerClicked,
+ );
+ expect(clicked).toHaveLength(1);
+ expectExactProperties(
+ requireElement(clicked[0], "Engineer click was not captured"),
+ threadCommon({
+ intelligence_status: "intelligence_not_enabled",
+ thread_service_status: "unavailable",
+ license_status: "expired",
+ usage_bucket: "within_limit",
+ expiry_bucket: "positive",
+ cta: "talk_to_engineer",
+ cta_surface: "threads_locked",
+ posthog_distinct_id: expect.any(String),
+ }),
+ );
+ } finally {
+ await harness.teardown();
+ }
+});
+
test("all examples and the complete tour serialize only closed kinds and step pairs", async () => {
const harness = await setup();
try {
@@ -1242,12 +1287,10 @@ test("Settings overlay keeps Learning memories keys for changed metadata", async
test("runtime telemetry opt-out stops every rendered Thread telemetry side effect", async () => {
const disclosureLog = vi.spyOn(console, "info").mockImplementation(() => {});
const harness = await setup({
- endpoints: LOCKED_ENDPOINTS,
+ endpoints: ENABLED_ENDPOINTS,
initialMenu: "ag-ui-events",
telemetryDisabled: true,
- threadsByAgent: {
- alpha: [realThread("alpha", "opted-out-seeded-row")],
- },
+ threadsByAgent: { alpha: [] },
});
try {
await harness.open();
diff --git a/packages/web-inspector/src/__tests__/web-inspector.spec.ts b/packages/web-inspector/src/__tests__/web-inspector.spec.ts
index 701c3e939f..3702c48702 100644
--- a/packages/web-inspector/src/__tests__/web-inspector.spec.ts
+++ b/packages/web-inspector/src/__tests__/web-inspector.spec.ts
@@ -2245,7 +2245,6 @@ function setupRuntimeDiagnostics() {
test.each([
{
diagnostic: "ready entitlement",
- status: "ready",
legacyStatus: "expired",
runtimeEntitlements: {
status: "ready",
@@ -2258,15 +2257,10 @@ test.each([
entitlementSource: "clerk_subscription",
},
},
- errorMessage: undefined,
- errorCode: undefined,
- requestId: undefined,
- traceId: undefined,
lockedHeading: "Renew Intelligence to inspect Threads.",
},
{
diagnostic: "expired self-hosted entitlement",
- status: "degraded",
legacyStatus: "valid",
runtimeEntitlements: {
status: "degraded",
@@ -2278,15 +2272,10 @@ test.each([
traceId: "trace-expired",
},
},
- errorMessage: "Self-hosted license has expired.",
- errorCode: "RUNTIME_ENTITLEMENTS_SELF_HOSTED_EXPIRED",
- requestId: "req-expired",
- traceId: "trace-expired",
lockedHeading: "Finish setting up Rich Threads",
},
{
diagnostic: "misconfigured self-hosted entitlement",
- status: "misconfigured",
legacyStatus: "valid",
runtimeEntitlements: {
status: "misconfigured",
@@ -2296,15 +2285,10 @@ test.each([
retryable: false,
},
},
- errorMessage: "Self-hosted license configuration is missing or invalid.",
- errorCode: "RUNTIME_ENTITLEMENTS_SELF_HOSTED_MISCONFIGURED",
- requestId: undefined,
- traceId: undefined,
lockedHeading: "Finish setting up Rich Threads",
},
{
diagnostic: "unavailable managed entitlement",
- status: "unavailable",
legacyStatus: "valid",
runtimeEntitlements: {
status: "unavailable",
@@ -2314,15 +2298,10 @@ test.each([
retryable: true,
},
},
- errorMessage: "Managed entitlement resolution is temporarily unavailable.",
- errorCode: "RUNTIME_ENTITLEMENTS_MANAGED_UNAVAILABLE",
- requestId: undefined,
- traceId: undefined,
lockedHeading: "Finish setting up Rich Threads",
},
{
diagnostic: "SDK fail-soft entitlement lookup",
- status: "unavailable",
legacyStatus: "valid",
runtimeEntitlements: {
status: "unavailable",
@@ -2332,24 +2311,11 @@ test.each([
retryable: true,
},
},
- errorMessage: "Runtime entitlement lookup failed",
- errorCode: "runtime_entitlements_unavailable",
- requestId: undefined,
- traceId: undefined,
lockedHeading: "Finish setting up Rich Threads",
},
] as const)(
- "renders structured Runtime entitlement diagnostics for $diagnostic",
- async ({
- status,
- legacyStatus,
- runtimeEntitlements,
- errorMessage,
- errorCode,
- requestId,
- traceId,
- lockedHeading,
- }) => {
+ "keeps the unified locked splash for $diagnostic",
+ async ({ legacyStatus, runtimeEntitlements, lockedHeading }) => {
const fixture = setupRuntimeDiagnostics();
try {
@@ -2361,24 +2327,12 @@ test.each([
const diagnostics = inspector.shadowRoot?.querySelectorAll(
"[data-runtime-entitlement-status]",
);
- const diagnostic = inspector.shadowRoot?.querySelector(
- `[data-runtime-entitlement-status="${status}"]`,
- );
-
- expect(diagnostics).toHaveLength(1);
- expect(diagnostic).not.toBeNull();
- if (errorMessage) {
- expect(diagnostic?.textContent).toContain(errorMessage);
- }
- if (errorCode) {
- expect(diagnostic?.textContent).toContain(errorCode);
- }
- if (requestId) {
- expect(diagnostic?.textContent).toContain(requestId);
- }
- if (traceId) {
- expect(diagnostic?.textContent).toContain(traceId);
- }
+ expect(diagnostics).toHaveLength(0);
+ expect(
+ inspector.shadowRoot?.querySelector(
+ '[data-inspector-feature-video="threads"]',
+ ),
+ ).not.toBeNull();
expect(inspector.shadowRoot?.textContent ?? "").toContain(lockedHeading);
expect(
fixture.fetchMock.mock.calls.some((call) =>
@@ -2391,7 +2345,7 @@ test.each([
},
);
-test("falls back to expired legacy license diagnostics when structured entitlements are omitted", async () => {
+test("keeps the unified locked splash for an expired legacy license", async () => {
const fixture = setupRuntimeDiagnostics();
try {
@@ -2402,12 +2356,7 @@ test("falls back to expired legacy license diagnostics when structured entitleme
const diagnostics = inspector.shadowRoot?.querySelectorAll(
"[data-runtime-entitlement-status]",
);
- const degraded = inspector.shadowRoot?.querySelector(
- '[data-runtime-entitlement-status="degraded"]',
- );
-
- expect(diagnostics).toHaveLength(1);
- expect(degraded).not.toBeNull();
+ expect(diagnostics).toHaveLength(0);
expect(inspector.shadowRoot?.textContent ?? "").toContain(
"Renew Intelligence to inspect Threads.",
);
@@ -2766,11 +2715,13 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
).map((anchor) => anchor.textContent?.trim());
expect(
ctaLabels.filter((label) => label === "Talk to an Engineer"),
- ).toEqual(["Talk to an Engineer"]);
+ ).toEqual(["Talk to an Engineer", "Talk to an Engineer"]);
const engineer = inspector.shadowRoot?.querySelector(
- 'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
+ '[data-inspector-locked-feature-talk="threads"]',
);
- expect(engineer?.closest("#cpk-main-scroll")).toBeNull();
+ expect(engineer?.closest("#cpk-main-scroll")).not.toBeNull();
+ expect(inspector.shadowRoot?.querySelector("cpk-thread-list")).toBeNull();
+ expect(text).toContain("Rich Threads");
expect(text).not.toContain("No threads yet");
expect(
fetchMock.mock.calls.some((call) => String(call[0]).includes("/threads")),
@@ -3502,15 +3453,37 @@ describe("WebInspectorElement memories — view states", () => {
const text = el.shadowRoot?.textContent ?? "";
expect(text).toContain("Learning");
expect(text).toContain(
- "Learning turns durable information from agent interactions into reusable context. It isn't enabled on this deployment.",
+ "Learning captures durable information from agent interactions and brings it back when it matters, so your product gets more useful over time.",
);
- expect(el.shadowRoot?.querySelector(".cpk-memory-locked")).not.toBeNull();
expect(
- el.shadowRoot?.querySelector(".cpk-memory-locked-scrim"),
+ el.shadowRoot?.querySelector('[data-inspector-locked-feature="memory"]'),
).not.toBeNull();
+ const video = el.shadowRoot?.querySelector(
+ '[data-inspector-feature-video="memory"]',
+ );
+ expect(video?.src).toBe(
+ "https://www.loom.com/embed/2978fbfe42324e509057ac5fd46b7a70?hide_owner=true&hide_share=true&hide_title=true&hideEmbedTopBar=true&hide_speed=true",
+ );
+ const outline = el.shadowRoot?.querySelector(
+ '[data-inspector-feature-outline="memory"]',
+ );
+ expect(outline?.getAttribute("aria-label")).toBe("Learning capabilities");
+ expect(outline?.textContent).toContain("Memory across sessions");
+ expect(outline?.textContent).toContain("Recall by meaning");
+ expect(outline?.textContent).toContain("Built-in structure");
+ expect(outline?.textContent).toContain("Full visibility");
+ const setupPrompt = el.shadowRoot?.querySelector(
+ '[data-inspector-feature-setup-prompt="memory"]',
+ );
+ expect(setupPrompt?.classList.contains("inspector-account-cta")).toBe(true);
expect(
- el.shadowRoot?.querySelector(".cpk-memory-locked-action-secondary"),
+ el.shadowRoot?.querySelector(
+ '.cpk-locked-feature-icon svg[viewBox="0 0 24 24"]',
+ ),
).not.toBeNull();
+ expect(el.shadowRoot?.textContent).not.toContain(
+ "Sign up for Intelligence",
+ );
const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
expect(
memoryList,
@@ -3522,7 +3495,9 @@ describe("WebInspectorElement memories — view states", () => {
const core = makeCoreWithMemory([], { licenseStatus: "none" });
const el = await mountMemories(core);
- expect(el.shadowRoot?.querySelector(".cpk-memory-locked")).not.toBeNull();
+ expect(
+ el.shadowRoot?.querySelector('[data-inspector-locked-feature="memory"]'),
+ ).not.toBeNull();
expect(
el.shadowRoot?.querySelector(
'[data-inspector-feature-setup-prompt="memory"]',
@@ -3534,24 +3509,25 @@ describe("WebInspectorElement memories — view states", () => {
it("does not use Threads onboarding UTM attribution for locked memory CTAs", async () => {
const core = makeCoreNoIntelligence();
const el = await mountMemories(core);
+ const lockedOverview = el.shadowRoot?.querySelector(
+ '[data-inspector-locked-feature="memory"]',
+ );
- const talkToEngineer = el.shadowRoot?.querySelector(
+ const talkToEngineer = lockedOverview?.querySelector(
'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
);
- const signup = el.shadowRoot?.querySelector(
- 'a[href^="https://intelligence.copilotkit.ai/?ref="]',
- );
-
expect(talkToEngineer).not.toBeNull();
- expect(signup).not.toBeNull();
+ expect(
+ lockedOverview?.querySelector(
+ 'a[href^="https://intelligence.copilotkit.ai/?ref="]',
+ ),
+ ).toBeNull();
- for (const href of [talkToEngineer!.href, signup!.href]) {
- const url = new URL(href);
- expect(url.searchParams.get("ref")).toBeTruthy();
- expect(url.searchParams.has("utm_source")).toBe(false);
- expect(url.searchParams.has("utm_medium")).toBe(false);
- expect(url.searchParams.has("utm_campaign")).toBe(false);
- }
+ const url = new URL(talkToEngineer!.href);
+ expect(url.searchParams.get("ref")).toBeTruthy();
+ expect(url.searchParams.has("utm_source")).toBe(false);
+ expect(url.searchParams.has("utm_medium")).toBe(false);
+ expect(url.searchParams.has("utm_campaign")).toBe(false);
});
it("renders the locked teaser when memories are unavailable", async () => {
@@ -4123,11 +4099,11 @@ describe("WebInspectorElement memories — older-core compat (no getMemoryStore)
await el.updateComplete;
const text = el.shadowRoot?.textContent ?? "";
- expect(text).toContain("@copilotkit SDK");
- expect(text).toContain("Upgrade");
+ expect(text).toContain("Upgrade to enable Learning");
+ expect(text).toContain("@copilotkit/core and @copilotkit/react");
// Must NOT show the deployment-not-enabled copy in this case.
expect(text).not.toContain(
- "Learning turns durable information from agent interactions into reusable context. It isn't enabled on this deployment.",
+ "Learning captures durable information from agent interactions and brings it back when it matters, so your product gets more useful over time.",
);
});
@@ -4139,9 +4115,9 @@ describe("WebInspectorElement memories — older-core compat (no getMemoryStore)
const text = el.shadowRoot?.textContent ?? "";
expect(text).toContain(
- "Learning turns durable information from agent interactions into reusable context. It isn't enabled on this deployment.",
+ "Learning captures durable information from agent interactions and brings it back when it matters, so your product gets more useful over time.",
);
- expect(text).not.toContain("@copilotkit SDK");
+ expect(text).not.toContain("Learning requires a newer version");
});
});
diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts
index 97d3f9edf6..c3b66febc4 100644
--- a/packages/web-inspector/src/index.ts
+++ b/packages/web-inspector/src/index.ts
@@ -193,17 +193,6 @@ const WHATS_NEW_VIEW_LABEL = "What's new";
/** Menu key of the What's new leaf — the news signal's destination. */
const WHATS_NEW_MENU_KEY = "whats-new";
-interface RuntimeEntitlementDisplayDiagnostic {
- status: "ready" | "degraded" | "misconfigured" | "unavailable";
- error?: {
- code: string;
- message: string;
- retryable: boolean;
- requestId?: string;
- traceId?: string;
- };
-}
-
type LucideIconName = keyof typeof icons;
type MenuItem = {
@@ -823,6 +812,67 @@ const THREADS_EXAMPLE_OVERVIEW_VIDEO_URL =
"https://cdn.copilotkit.ai/corp-site/videos/copilotkit-generative-ui-agentic-frontend-demo.webm";
const THREADS_EXAMPLE_OVERVIEW_VIDEO_FALLBACK =
"The demo video is unavailable. Use the example threads to explore Messages, AG-UI Events, and State.";
+const THREADS_LOCKED_VIDEO_URL =
+ "https://www.loom.com/embed/79817778d29e490c97225127d2f17b3a?hide_owner=true&hide_share=true&hide_title=true&hideEmbedTopBar=true&hide_speed=true";
+const LEARNING_LOCKED_VIDEO_URL =
+ "https://www.loom.com/embed/2978fbfe42324e509057ac5fd46b7a70?hide_owner=true&hide_share=true&hide_title=true&hideEmbedTopBar=true&hide_speed=true";
+type LockedFeatureOutlineItem = Readonly<{
+ icon: LucideIconName;
+ title: string;
+ description: string;
+}>;
+const THREADS_LOCKED_FEATURE_OUTLINE = [
+ {
+ icon: "MessagesSquare",
+ title: "The whole conversation comes back",
+ description:
+ "Rich Threads restores the complete interaction, not just a transcript. Messages, tool calls, shared state, generated interfaces, and supported files return together when a user reopens the thread.",
+ },
+ {
+ icon: "LayoutGrid",
+ title: "Generated UI stays in the thread",
+ description:
+ "Cards, charts, A2UI surfaces, MCP Apps, and tool renderers remain part of the conversation. The demo above shows a generated spending chart returning after a reload.",
+ },
+ {
+ icon: "RefreshCw",
+ title: "Return without starting over",
+ description:
+ "Users can move across sessions and devices while thread lists stay synchronized across open tabs. Rich Threads replays missed events and reconnects to work already in progress.",
+ },
+ {
+ icon: "Server",
+ title: "Infrastructure you do not have to rebuild",
+ description:
+ "CopilotKit handles durable event storage, replay, synchronization, lifecycle APIs, and thread locks on one portable AG-UI history model. Use CopilotKit Cloud or deploy Intelligence in your own Kubernetes environment.",
+ },
+] as const satisfies ReadonlyArray;
+const LEARNING_LOCKED_FEATURE_OUTLINE = [
+ {
+ icon: "History",
+ title: "Memory across sessions",
+ description:
+ "Keep useful facts, preferences, and decisions after the thread closes. Learning gives future conversations the context they need without asking users to repeat themselves.",
+ },
+ {
+ icon: "Search",
+ title: "Recall by meaning",
+ description:
+ "Surface relevant memories by what they mean, not by an exact phrase. Your agent can bring the right context into a new interaction even when the user asks in a completely different way.",
+ },
+ {
+ icon: "Layers",
+ title: "Built-in structure",
+ description:
+ "Turn raw interactions into organized topical, episodic, and operational knowledge. Learning separates enduring facts from individual experiences and useful instructions automatically.",
+ },
+ {
+ icon: "Eye",
+ title: "Full visibility",
+ description:
+ "Inspect exactly what your agent learned and trace each memory back to the threads that shaped it. Review the stored context instead of treating memory like a black box.",
+ },
+] as const satisfies ReadonlyArray;
const THREADS_EXAMPLE_TOUR_STORAGE_KEY =
"cpk:inspector:threads-example-tour:v1";
const THREADS_EXAMPLE_AGENT_ID = "threads-feature";
@@ -10501,6 +10551,11 @@ export class WebInspectorElement extends LitElement {
height: 100%;
object-fit: cover;
}
+ .cpk-threads-overview-video-embed {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ }
/* ── Header controls on the branded account strip ──────────── */
.drag-handle > div[data-inspector-account-strip] button {
@@ -16489,23 +16544,15 @@ export class WebInspectorElement extends LitElement {
placement: "threads-footer" | "locked";
}>
| undefined {
- const { threadsFooterAction, lockedAction } =
- this.inspectorMetadataProjection;
+ const { threadsFooterAction } = this.inspectorMetadataProjection;
if (
threadsFooterAction &&
!this.settingsOpen &&
- this.selectedMenu === "threads"
+ this.selectedMenu === "threads" &&
+ this.areThreadEndpointsAvailable()
) {
return { action: threadsFooterAction, placement: "threads-footer" };
}
- if (
- lockedAction &&
- !this.settingsOpen &&
- this.selectedMenu === "threads" &&
- !this.areThreadEndpointsAvailable()
- ) {
- return { action: lockedAction, placement: "locked" };
- }
return undefined;
}
@@ -16744,14 +16791,12 @@ export class WebInspectorElement extends LitElement {
}
private shouldRenderExampleThreads(
- locked: boolean,
displayThreads: ɵThread[],
threadsErrorMessage: string | null,
threadsLoading: boolean,
): boolean {
return (
- locked ||
- (!threadsErrorMessage && !threadsLoading && displayThreads.length === 0)
+ !threadsErrorMessage && !threadsLoading && displayThreads.length === 0
);
}
@@ -17496,72 +17541,140 @@ export class WebInspectorElement extends LitElement {
`;
}
- private renderThreadsExampleOverview(locked: boolean) {
- const lockedCopy = locked ? this.getThreadsLockedCopy() : undefined;
- const { lockedAction } = this.inspectorMetadataProjection;
+ private renderThreadsExampleOverview() {
const onboardingAction = this.getThreadsEmptyOnboardingAction();
return html`
- ${
- lockedCopy?.heading ??
- "Threads are persistent, inspectable conversations"
- }
+ Threads are persistent, inspectable conversations
${this.renderThreadsExampleOverviewVideo()}
- ${
- lockedCopy?.description ??
- "Take a tour with the example threads in the sidebar. Then, start chatting in your app to create the first real thread."
- }
+ Take a tour with the example threads in the sidebar. Then, start
+ chatting in your app to create the first real thread.
- `;
- }
-
/**
* Renders the realtime-connection indicator in the memory-store header.
* Only `"connected"` shows the live (green-dot) state; `"connecting"` shows a
@@ -17873,51 +17837,19 @@ export class WebInspectorElement extends LitElement {
// as Home and the launcher so an unavailable feature always lands on its
// setup path instead of an enabled-looking empty state.
if (!learningEnabled) {
- return html`
-
- ${
- this._memoryStoreUnsupported
- ? "Learning is unavailable in this version of the @copilotkit SDK. Upgrade @copilotkit/core (and @copilotkit/react) to a version that supports long-term memory."
- : "Learning turns durable information from agent interactions into reusable context. It isn't enabled on this deployment."
- }
-
+ It inspects your app first, then reuses an existing CopilotKit
+ tool or wraps the smallest suitable browser action. WebMCP calls
+ do not run through a backend agent.
+
+
+
+
+
+
+
+
+ {copyState === "copied"
+ ? "Prompt copied"
+ : copyState === "error"
+ ? "Prompt copy failed. Try again."
+ : ""}
+
+
+ );
+}
diff --git a/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx b/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
new file mode 100644
index 0000000000..44376c97ec
--- /dev/null
+++ b/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
@@ -0,0 +1,82 @@
+---
+title: WebMCP
+icon: "lucide/Globe2"
+description: Expose Angular application actions as structured tools for compatible browser agents.
+---
+
+import { WebMCPOnboardingPrompt } from "@/components/webmcp-onboarding-prompt";
+
+WebMCP lets your website declare typed JavaScript tools that compatible browser agents can discover and call. CopilotKit can publish the same frontend tool handler to your configured agent and to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/).
+
+
+
+
+ WebMCP is a draft Community Group report. Chrome offers it through an origin
+ trial beginning in Chrome 149, or through the
+ `chrome://flags/#enable-webmcp-testing` flag for local development. Check the
+ [current Chrome setup instructions](https://developer.chrome.com/docs/ai/webmcp)
+ before testing.
+
+
+## Do I need an agent?
+
+You do not need a CopilotKit backend agent to handle a WebMCP call. A compatible browser agent calls the frontend tool handler directly on the page, so the handler context has no `agent` in that path.
+
+If your application already registers the action as a CopilotKit frontend tool, add `webmcp`. Otherwise, wrap the smallest suitable browser-side action in a frontend tool. Keep any existing backend agent, but do not create one only for WebMCP.
+
+## Add WebMCP manually
+
+Call `registerFrontendTool` in an injection context and opt the tool into WebMCP:
+
+```ts title="src/app/order-search.component.ts"
+import { Component } from "@angular/core";
+import { registerFrontendTool } from "@copilotkit/angular";
+import { z } from "zod";
+
+@Component({
+ selector: "app-order-search",
+ standalone: true,
+ template: "",
+})
+export class OrderSearchComponent {
+ constructor() {
+ registerFrontendTool({
+ name: "searchOrders",
+ description: "Search the signed-in user's orders by status",
+ parameters: z.object({
+ status: z.enum(["open", "shipped", "delivered"]),
+ }),
+ handler: async ({ status }) => {
+ const orders = await searchOrders(status);
+ return JSON.stringify(orders);
+ },
+ webmcp: {
+ annotations: {
+ readOnlyHint: true,
+ },
+ },
+ });
+ }
+}
+```
+
+The tool remains registered while the owning injector is alive. CopilotKit removes it from `document.modelContext` when that injector is destroyed or when the tool becomes unavailable.
+
+See the [`registerFrontendTool` reference](/reference/angular/functions/registerFrontendTool) for the full API.
+
+## Secure and test the tool
+
+- Give the tool and each parameter a specific description.
+- Use `readOnlyHint` only for actions that cannot change state.
+- Use `untrustedContentHint` when results may contain user-generated or external content.
+- Enforce authentication, authorization, input validation, and required user confirmation in the handler or the API it calls. Annotations are hints, not policy.
+- Remember that `agentId` does not restrict WebMCP access; WebMCP registrations are page-level.
+
+Open the application in Chrome 149 or newer with the origin trial or local flag enabled. The page must be origin-isolated, and cross-origin iframes need `allow="tools"`. Use Chrome's [Model Context Tool Inspector](https://developer.chrome.com/docs/ai/webmcp#imitate-agent-chat-with-the-inspector-extension) to inspect the schema, call the tool, and verify the complete user task.
+
+## Related
+
+- [Frontend tools and generative UI](guides/frontend-tools-generative-ui)
+- [`registerFrontendTool` reference](/reference/angular/functions/registerFrontendTool)
+- [WebMCP specification](https://webmachinelearning.github.io/webmcp/)
+- [WebMCP tool security](https://developer.chrome.com/docs/ai/webmcp/secure-tools)
diff --git a/showcase/shell-docs/src/content/docs/meta.json b/showcase/shell-docs/src/content/docs/meta.json
index 91ff7709ee..21ea0aa071 100644
--- a/showcase/shell-docs/src/content/docs/meta.json
+++ b/showcase/shell-docs/src/content/docs/meta.json
@@ -32,6 +32,7 @@
"...generative-ui",
"---Add Agent Powers---",
"frontend-tools",
+ "webmcp",
{
"title": "Shared State",
"pages": [
diff --git a/showcase/shell-docs/src/content/docs/webmcp.mdx b/showcase/shell-docs/src/content/docs/webmcp.mdx
new file mode 100644
index 0000000000..83e17a43b4
--- /dev/null
+++ b/showcase/shell-docs/src/content/docs/webmcp.mdx
@@ -0,0 +1,149 @@
+---
+title: WebMCP
+icon: "lucide/Globe2"
+description: Expose browser-side actions as structured tools that compatible agents can discover and call.
+---
+
+import { WebMCPOnboardingPrompt } from "@/components/webmcp-onboarding-prompt";
+
+WebMCP lets your website tell compatible browser agents what they can do. Instead of guessing which buttons to click, an agent discovers a named tool with a description, a typed input schema, and a JavaScript handler.
+
+CopilotKit can publish an existing frontend tool to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/). The same handler then works for your CopilotKit agent and for WebMCP-aware browser agents.
+
+
+
+
+ WebMCP is a draft Community Group report, not a W3C Standard. Chrome offers
+ it through an origin trial beginning in Chrome 149, or through the
+ `chrome://flags/#enable-webmcp-testing` flag for local development. CopilotKit
+ safely does nothing when `document.modelContext` is unavailable. Check the
+ [current Chrome setup instructions](https://developer.chrome.com/docs/ai/webmcp)
+ before testing.
+
+
+## Do I need an agent?
+
+You do not need a CopilotKit backend agent to handle a WebMCP call. A compatible browser agent calls the frontend tool handler directly on the page; in that path, the handler context has no `agent`.
+
+Choose the smallest path that fits your app:
+
+| What your app has today | What to do |
+| --- | --- |
+| A CopilotKit frontend tool for the action | Add `webmcp: true`, or add annotations with `webmcp: { annotations: ... }`. |
+| CopilotKit, but no frontend tool for the action | Wrap the smallest suitable browser-side action in a frontend tool, then opt it into WebMCP. |
+| A backend agent | Keep it. The tool can remain available to that agent, but WebMCP calls do not pass through it. |
+| No backend agent | Use `CopilotKitCore` directly in browser code, or let the setup prompt add the appropriate frontend integration. Do not create an agent only for WebMCP. |
+
+React Native does not expose `document.modelContext`, so WebMCP registration is a no-op there. CopilotKit currently supports this WebMCP option in its browser integrations for React, Vue, Angular, and direct core usage.
+
+## How CopilotKit connects WebMCP
+
+1. Your app registers a CopilotKit frontend tool with `webmcp` enabled.
+2. CopilotKit mirrors the tool's name, description, JSON Schema, annotations, and handler onto `document.modelContext`.
+3. A compatible browser agent discovers the tool and calls the same JavaScript handler while the page is open.
+4. CopilotKit unregisters the WebMCP tool when the frontend tool is removed or becomes unavailable.
+
+The WebMCP call stays in the browser unless your handler deliberately calls an API. CopilotKit does not route that call through a Runtime or backend agent.
+
+## Add WebMCP manually
+
+### Existing React frontend tool
+
+Add `webmcp` to the tool you already register. This code must run inside your existing `CopilotKitProvider`.
+
+```tsx title="OrderSearch.tsx"
+"use client";
+
+import { useFrontendTool } from "@copilotkit/react-core/v2";
+import { z } from "zod";
+
+export function OrderSearch() {
+ useFrontendTool({
+ name: "searchOrders",
+ description: "Search the signed-in user's orders by status",
+ parameters: z.object({
+ status: z.enum(["open", "shipped", "delivered"]),
+ }),
+ handler: async ({ status }) => {
+ const orders = await searchOrders(status);
+ return JSON.stringify(orders);
+ },
+ webmcp: {
+ annotations: {
+ readOnlyHint: true,
+ },
+ },
+ });
+
+ return null;
+}
+```
+
+Use the equivalent option with [`useFrontendTool` for Vue](/reference/vue/hooks/useFrontendTool) or [`registerFrontendTool` for Angular](/reference/angular/functions/registerFrontendTool). The React API is documented in [`useFrontendTool`](/reference/hooks/useFrontendTool).
+
+### WebMCP only, with no agent
+
+For browser code that does not use a framework provider, register the frontend tool directly with the core. Keep the core instance alive for as long as the tool should remain available.
+
+```ts title="webmcp.ts"
+import { CopilotKitCore } from "@copilotkit/core";
+import { z } from "zod";
+
+export const copilotkit = new CopilotKitCore({
+ tools: [
+ {
+ name: "searchOrders",
+ description: "Search the signed-in user's orders by status",
+ parameters: z.object({
+ status: z.enum(["open", "shipped", "delivered"]),
+ }),
+ handler: async ({ status }) => {
+ const orders = await searchOrders(status);
+ return JSON.stringify(orders);
+ },
+ webmcp: {
+ annotations: {
+ readOnlyHint: true,
+ },
+ },
+ },
+ ],
+});
+```
+
+This path does not configure a Runtime or an agent. It only registers browser-side tools through CopilotKit's core lifecycle.
+
+## Design tools agents can use reliably
+
+- Give every WebMCP tool a non-empty, specific `description`. CopilotKit skips WebMCP registration when it is missing.
+- Keep each tool focused on one action and give every parameter a useful description.
+- Use `readOnlyHint: true` only when the handler cannot change state.
+- Use `untrustedContentHint: true` when results may contain user-generated or external content.
+- Return concise, structured results that tell the caller what happened.
+- Set the frontend tool to unavailable when the action cannot currently run; CopilotKit removes it from WebMCP until it is available again.
+
+Annotations are hints for browser agents, not security controls. Enforce authentication, authorization, validation, rate limits, and required user confirmation inside the handler or the API it calls. See Chrome's [WebMCP tool security guidance](https://developer.chrome.com/docs/ai/webmcp/secure-tools).
+
+
+ `agentId` limits which CopilotKit agent receives a frontend tool. WebMCP tools
+ are page-level, so `agentId` does not restrict browser-agent access. If
+ multiple opted-in tools share a name across agent IDs, CopilotKit exposes the
+ first one and logs a warning.
+
+
+## Test the complete path
+
+1. Open Chrome 149 or newer and enable the WebMCP origin trial or local testing flag.
+2. Load the app in an [origin-isolated document](https://developer.chrome.com/docs/ai/webmcp#origin-isolation). The `tools` permissions policy defaults to `self`; cross-origin iframes also need `allow="tools"`.
+3. Confirm that `document.modelContext` exists.
+4. Use Chrome's [Model Context Tool Inspector](https://developer.chrome.com/docs/ai/webmcp#imitate-agent-chat-with-the-inspector-extension) to confirm the tool name and schema, call it with representative inputs, and inspect its result.
+5. Ask the inspector's agent to complete the user task in natural language. Verify that it chooses the right tool and that authentication and confirmation boundaries still hold.
+
+If the tool does not appear, check the browser setup first, then verify that the tool has a description, `webmcp` is enabled, and the frontend tool is currently available.
+
+## Related
+
+- [Frontend tools](/frontend-tools)
+- [React `useFrontendTool` reference](/reference/hooks/useFrontendTool)
+- [WebMCP specification](https://webmachinelearning.github.io/webmcp/)
+- [Chrome WebMCP best practices](https://developer.chrome.com/docs/ai/webmcp/best-practices)
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
new file mode 100644
index 0000000000..332102a484
--- /dev/null
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
@@ -0,0 +1,44 @@
+import { expect, test } from "vitest";
+import { loadDoc } from "../docs-render";
+import { renderPageToLlmText } from "../llm-text";
+
+test("leads the WebMCP guide with repository-aware onboarding", () => {
+ const source = loadDoc("webmcp")?.source ?? "";
+ const prompt = source.indexOf("");
+ const manualSetup = source.indexOf("## Add WebMCP manually");
+
+ expect(prompt).toBeGreaterThan(-1);
+ expect(manualSetup).toBeGreaterThan(prompt);
+ expect(source).toContain("You do not need a CopilotKit backend agent");
+ expect(source).toContain("React Native does not expose `document.modelContext`");
+ expect(source).toContain("Chrome 149");
+});
+
+test("expands the WebMCP onboarding CTA for Markdown and LLM readers", () => {
+ const doc = loadDoc("webmcp");
+ if (!doc) throw new Error("WebMCP guide is missing");
+
+ const output = renderPageToLlmText({
+ url: "webmcp",
+ title: doc.fm.title,
+ description: doc.fm.description,
+ filePath: doc.filePath,
+ loadSlug: "webmcp",
+ });
+
+ expect(output).toContain(
+ "npx --yes copilotkit@latest onboard start --coding-agent ",
+ );
+ expect(output).toContain(
+ "The goal of this onboarding run is to get WebMCP working",
+ );
+ expect(output).not.toContain("");
+});
+
+test("provides an Angular-native WebMCP page for the frontend selector", () => {
+ const source = loadDoc("frontends/angular/webmcp")?.source ?? "";
+
+ expect(source).toContain("registerFrontendTool");
+ expect(source).toContain("webmcp:");
+ expect(source).not.toMatch(/\bReact\b/);
+});
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts
new file mode 100644
index 0000000000..bf212c4e51
--- /dev/null
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts
@@ -0,0 +1,12 @@
+import { expect, test } from "vitest";
+import { createWebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
+
+test("keeps the Markdown prompt usable when no telemetry run id is available", () => {
+ const prompt = createWebMCPOnboardingPrompt();
+
+ expect(prompt).toContain(
+ "npx --yes copilotkit@latest onboard start --coding-agent ",
+ );
+ expect(prompt).not.toContain("--run undefined");
+ expect(prompt).not.toContain("");
+});
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
new file mode 100644
index 0000000000..5d91384763
--- /dev/null
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
@@ -0,0 +1,14 @@
+import { expect, test } from "vitest";
+import { createWebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
+
+test("keeps WebMCP as the goal of the CLI onboarding run", () => {
+ const prompt = createWebMCPOnboardingPrompt("0123456789ab");
+
+ expect(prompt).toBe(`Identify which coding-agent product you are, using a short slug such as \`codex\` or \`claude-code\`. From the root of the project where you want CopilotKit, run \`npx --yes copilotkit@latest onboard start --run 0123456789ab --coding-agent \`. Follow the Markdown instructions it prints until onboarding is complete.
+
+The goal of this onboarding run is to get WebMCP working in this project. Use https://docs.copilotkit.ai/webmcp as the official guide. Let the onboarding workflow inspect the repository and determine whether the app already has CopilotKit, a backend agent, CopilotKit frontend tools, or only browser-side actions. Ask only about choices the repository cannot answer.
+
+A WebMCP call does not require a CopilotKit backend agent. If suitable CopilotKit frontend tools already exist, expose only the appropriate ones through WebMCP. Otherwise, wrap the smallest suitable browser-side action in a supported CopilotKit frontend tool. Do not create or replace a backend agent solely for WebMCP.
+
+Preserve the existing framework, agent behavior, authentication, authorization, and user-confirmation boundaries. Follow the guide for schemas, annotations, browser requirements, and verification. Finish only after a compatible browser can discover and call the intended tool; if the selected surface cannot provide \`document.modelContext\`, explain that limitation instead of adding a fake integration.`);
+});
diff --git a/showcase/shell-docs/src/lib/llm-text.ts b/showcase/shell-docs/src/lib/llm-text.ts
index c379491e96..c54754cb3b 100644
--- a/showcase/shell-docs/src/lib/llm-text.ts
+++ b/showcase/shell-docs/src/lib/llm-text.ts
@@ -75,6 +75,7 @@ import { resolveDocsHref } from "./docs-link-rewrite";
import { resolveBundledSetupConcept } from "./setup-content";
import type { SetupContentBundle } from "./setup-content";
import { RICH_THREADS_SETUP_PROMPT } from "./rich-threads-setup-prompt";
+import { createWebMCPOnboardingPrompt } from "./webmcp-onboarding-prompt";
interface Region {
file: string;
@@ -766,6 +767,17 @@ function expandRichThreadsSetupPrompts(body: string): string {
);
}
+/** Expand the interactive WebMCP prompt for raw Markdown consumers. */
+function expandWebMCPOnboardingPrompts(body: string): string {
+ return body.replace(
+ //g,
+ `### Copy this prompt into your coding agent\n\n${fenceFor(
+ "text",
+ createWebMCPOnboardingPrompt(),
+ )}`,
+ );
+}
+
/**
* Drop `` tags — these mount live iframes in the
* browser; in plain markdown they're noise. Leave a short note so the
@@ -899,6 +911,7 @@ export function renderPageToLlmText(
// Interactive prompt buttons cannot run in raw Markdown or LLM feeds.
body = expandRichThreadsSetupPrompts(body);
+ body = expandWebMCPOnboardingPrompts(body);
// 1) Inline `` shared snippets (``, etc.). Uses
// the SNIPPET_MAP / SUBPATH_TO_COMPONENT logic — same as the page
diff --git a/showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts b/showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts
new file mode 100644
index 0000000000..7abd57ab1a
--- /dev/null
+++ b/showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts
@@ -0,0 +1,21 @@
+import {
+ createIntelligenceOnboardingPrompt,
+ INTELLIGENCE_ONBOARDING_PROMPT,
+} from "./intelligence-onboarding-prompt";
+
+export const WEBMCP_DOCS_URL = "https://docs.copilotkit.ai/webmcp";
+
+/** Build the WebMCP goal that a docs reader hands to the CLI onboarding flow. */
+export function createWebMCPOnboardingPrompt(runId?: string): string {
+ const onboardingPrompt = runId
+ ? createIntelligenceOnboardingPrompt(runId)
+ : INTELLIGENCE_ONBOARDING_PROMPT.replace(" --run ", "");
+
+ return `${onboardingPrompt}
+
+The goal of this onboarding run is to get WebMCP working in this project. Use ${WEBMCP_DOCS_URL} as the official guide. Let the onboarding workflow inspect the repository and determine whether the app already has CopilotKit, a backend agent, CopilotKit frontend tools, or only browser-side actions. Ask only about choices the repository cannot answer.
+
+A WebMCP call does not require a CopilotKit backend agent. If suitable CopilotKit frontend tools already exist, expose only the appropriate ones through WebMCP. Otherwise, wrap the smallest suitable browser-side action in a supported CopilotKit frontend tool. Do not create or replace a backend agent solely for WebMCP.
+
+Preserve the existing framework, agent behavior, authentication, authorization, and user-confirmation boundaries. Follow the guide for schemas, annotations, browser requirements, and verification. Finish only after a compatible browser can discover and call the intended tool; if the selected surface cannot provide \`document.modelContext\`, explain that limitation instead of adding a fake integration.`;
+}
From 7ce4ab260d4a04c757d1d820f437152f48af4916 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:51:22 +0000
Subject: [PATCH 05/57] style: auto-fix formatting
---
.../__tests__/webmcp-onboarding-prompt.test.tsx | 16 +++++++++++++---
.../src/components/webmcp-onboarding-prompt.tsx | 8 ++++----
.../src/lib/__tests__/webmcp-docs.test.ts | 4 +++-
.../__tests__/webmcp-onboarding-prompt.test.ts | 3 ++-
4 files changed, 22 insertions(+), 9 deletions(-)
diff --git a/showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx b/showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx
index b2f8feb885..9086bc3235 100644
--- a/showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx
+++ b/showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx
@@ -1,7 +1,13 @@
// @vitest-environment jsdom
import React from "react";
-import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { WebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
import { INTELLIGENCE_ONBOARDING_EVENTS } from "@/lib/intelligence-onboarding-prompt";
@@ -39,8 +45,12 @@ test("copies a run-bound WebMCP goal into the CLI onboarding path", async () =>
)?.[1];
expect(runId).toBeTruthy();
- expect(prompt).toContain("The goal of this onboarding run is to get WebMCP working");
- expect(prompt).toContain("A WebMCP call does not require a CopilotKit backend agent");
+ expect(prompt).toContain(
+ "The goal of this onboarding run is to get WebMCP working",
+ );
+ expect(prompt).toContain(
+ "A WebMCP call does not require a CopilotKit backend agent",
+ );
expect(prompt).toContain("https://docs.copilotkit.ai/webmcp");
expect(screen.getByRole("button", { name: "Copied" })).toBeTruthy();
expect(screen.getByText("Prompt copied")).toBeTruthy();
diff --git a/showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx b/showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx
index 72172697b5..5218f45956 100644
--- a/showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx
+++ b/showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx
@@ -111,10 +111,10 @@ export function WebMCPOnboardingPrompt(): React.JSX.Element {
{isCopying
? "Copying…"
: copyState === "copied"
- ? "Copied"
- : copyState === "error"
- ? "Copy blocked"
- : "Copy setup prompt"}
+ ? "Copied"
+ : copyState === "error"
+ ? "Copy blocked"
+ : "Copy setup prompt"}
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
index 332102a484..41ba67aafe 100644
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
@@ -10,7 +10,9 @@ test("leads the WebMCP guide with repository-aware onboarding", () => {
expect(prompt).toBeGreaterThan(-1);
expect(manualSetup).toBeGreaterThan(prompt);
expect(source).toContain("You do not need a CopilotKit backend agent");
- expect(source).toContain("React Native does not expose `document.modelContext`");
+ expect(source).toContain(
+ "React Native does not expose `document.modelContext`",
+ );
expect(source).toContain("Chrome 149");
});
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
index 5d91384763..e2c1fa3997 100644
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
@@ -4,7 +4,8 @@ import { createWebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
test("keeps WebMCP as the goal of the CLI onboarding run", () => {
const prompt = createWebMCPOnboardingPrompt("0123456789ab");
- expect(prompt).toBe(`Identify which coding-agent product you are, using a short slug such as \`codex\` or \`claude-code\`. From the root of the project where you want CopilotKit, run \`npx --yes copilotkit@latest onboard start --run 0123456789ab --coding-agent \`. Follow the Markdown instructions it prints until onboarding is complete.
+ expect(prompt)
+ .toBe(`Identify which coding-agent product you are, using a short slug such as \`codex\` or \`claude-code\`. From the root of the project where you want CopilotKit, run \`npx --yes copilotkit@latest onboard start --run 0123456789ab --coding-agent \`. Follow the Markdown instructions it prints until onboarding is complete.
The goal of this onboarding run is to get WebMCP working in this project. Use https://docs.copilotkit.ai/webmcp as the official guide. Let the onboarding workflow inspect the repository and determine whether the app already has CopilotKit, a backend agent, CopilotKit frontend tools, or only browser-side actions. Ask only about choices the repository cannot answer.
From 45e1e6ed7e576133c96693fea22826a383854f9a Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 10:05:00 -0700
Subject: [PATCH 06/57] docs(shell-docs): simplify WebMCP setup prompt
---
....test.tsx => webmcp-setup-prompt.test.tsx} | 32 +++++++------------
.../src/components/docs-page-view.tsx | 4 +--
...ing-prompt.tsx => webmcp-setup-prompt.tsx} | 24 ++++++--------
.../content/docs/frontends/angular/webmcp.mdx | 4 +--
.../shell-docs/src/content/docs/webmcp.mdx | 4 +--
.../src/lib/__tests__/webmcp-docs.test.ts | 17 ++++------
.../webmcp-onboarding-prompt-markdown.test.ts | 12 -------
.../webmcp-onboarding-prompt.test.ts | 15 ---------
.../lib/__tests__/webmcp-setup-prompt.test.ts | 9 ++++++
showcase/shell-docs/src/lib/llm-text.ts | 10 +++---
.../src/lib/webmcp-onboarding-prompt.ts | 21 ------------
.../shell-docs/src/lib/webmcp-setup-prompt.ts | 7 ++++
12 files changed, 56 insertions(+), 103 deletions(-)
rename showcase/shell-docs/src/components/__tests__/{webmcp-onboarding-prompt.test.tsx => webmcp-setup-prompt.test.tsx} (68%)
rename showcase/shell-docs/src/components/{webmcp-onboarding-prompt.tsx => webmcp-setup-prompt.tsx} (82%)
delete mode 100644 showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts
delete mode 100644 showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
create mode 100644 showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts
delete mode 100644 showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts
create mode 100644 showcase/shell-docs/src/lib/webmcp-setup-prompt.ts
diff --git a/showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx b/showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx
similarity index 68%
rename from showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx
rename to showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx
index 9086bc3235..2bd4a676a2 100644
--- a/showcase/shell-docs/src/components/__tests__/webmcp-onboarding-prompt.test.tsx
+++ b/showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx
@@ -9,8 +9,11 @@ import {
waitFor,
} from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
-import { WebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
-import { INTELLIGENCE_ONBOARDING_EVENTS } from "@/lib/intelligence-onboarding-prompt";
+import { WebMCPSetupPrompt } from "../webmcp-setup-prompt";
+import {
+ WEBMCP_SETUP_EVENTS,
+ WEBMCP_SETUP_PROMPT,
+} from "@/lib/webmcp-setup-prompt";
const analytics = vi.hoisted(() => ({ capture: vi.fn() }));
@@ -27,39 +30,28 @@ afterEach(() => {
vi.clearAllMocks();
});
-test("copies a run-bound WebMCP goal into the CLI onboarding path", async () => {
+test("copies the standalone WebMCP setup prompt", async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
- render();
+ render();
fireEvent.click(screen.getByRole("button", { name: "Copy setup prompt" }));
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
const prompt = writeText.mock.calls[0]?.[0] as string;
- const runId = /onboard start --run ([0-9a-f]{12}) --coding-agent/.exec(
- prompt,
- )?.[1];
-
- expect(runId).toBeTruthy();
- expect(prompt).toContain(
- "The goal of this onboarding run is to get WebMCP working",
- );
- expect(prompt).toContain(
- "A WebMCP call does not require a CopilotKit backend agent",
- );
- expect(prompt).toContain("https://docs.copilotkit.ai/webmcp");
+ expect(prompt).toBe(WEBMCP_SETUP_PROMPT);
+ expect(prompt).not.toContain("onboard start");
expect(screen.getByRole("button", { name: "Copied" })).toBeTruthy();
expect(screen.getByText("Prompt copied")).toBeTruthy();
expect(analytics.capture).toHaveBeenCalledWith(
- INTELLIGENCE_ONBOARDING_EVENTS.promptCopied,
+ WEBMCP_SETUP_EVENTS.promptCopied,
{
from_path: "/webmcp",
- onboarding_run_id: runId,
- surface: "docs_webmcp_onboarding_prompt",
+ surface: "docs_webmcp_setup_prompt",
},
);
});
@@ -77,7 +69,7 @@ test("disables the CTA while the clipboard write is pending", async () => {
value: { writeText },
});
- render();
+ render();
const button = screen.getByRole("button", { name: "Copy setup prompt" });
fireEvent.click(button);
diff --git a/showcase/shell-docs/src/components/docs-page-view.tsx b/showcase/shell-docs/src/components/docs-page-view.tsx
index 2ddb156207..7ed9b3fa71 100644
--- a/showcase/shell-docs/src/components/docs-page-view.tsx
+++ b/showcase/shell-docs/src/components/docs-page-view.tsx
@@ -40,7 +40,7 @@ import type { OpsPlatformCTAProps } from "@/components/react/ops-platform-cta";
import { ChannelsStartPrompt } from "@/components/channels-start-prompt";
import type { ChannelsStartPromptProps } from "@/components/channels-start-prompt";
import { RichThreadsSetupPrompt } from "@/components/rich-threads-setup-prompt";
-import { WebMCPOnboardingPrompt } from "@/components/webmcp-onboarding-prompt";
+import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
import { IntelligenceOnboardingPrompt } from "@/components/intelligence-onboarding-prompt";
import type { IntelligenceOnboardingPromptProps } from "@/components/intelligence-onboarding-prompt";
import { SignupLink } from "@/components/react/signup-link";
@@ -365,7 +365,7 @@ export async function DocsPageView({
/>
),
RichThreadsSetupPrompt,
- WebMCPOnboardingPrompt,
+ WebMCPSetupPrompt,
IntelligenceOnboardingPrompt:
IntelligenceOnboardingPromptMdx,
OpsPlatformCTA: (props: OpsPlatformCTAProps) => (
diff --git a/showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
similarity index 82%
rename from showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx
rename to showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
index 5218f45956..782c94dc08 100644
--- a/showcase/shell-docs/src/components/webmcp-onboarding-prompt.tsx
+++ b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
@@ -5,17 +5,16 @@ import { Copy, SquareTerminal } from "lucide-react";
import { usePathname } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import {
- createOnboardingRunId,
- INTELLIGENCE_ONBOARDING_EVENTS,
-} from "@/lib/intelligence-onboarding-prompt";
-import { createWebMCPOnboardingPrompt } from "@/lib/webmcp-onboarding-prompt";
+ WEBMCP_SETUP_EVENTS,
+ WEBMCP_SETUP_PROMPT,
+} from "@/lib/webmcp-setup-prompt";
-const COPY_SURFACE = "docs_webmcp_onboarding_prompt";
+const COPY_SURFACE = "docs_webmcp_setup_prompt";
type CopyState = "idle" | "copied" | "error";
-/** Copyable WebMCP goal that enters the repository-aware CLI onboarding flow. */
-export function WebMCPOnboardingPrompt(): React.JSX.Element {
+/** Copyable, standalone WebMCP setup prompt. */
+export function WebMCPSetupPrompt(): React.JSX.Element {
const titleId = React.useId();
const pathname = usePathname();
const posthog = usePostHog();
@@ -47,10 +46,9 @@ export function WebMCPOnboardingPrompt(): React.JSX.Element {
if (copyInFlightRef.current) return;
copyInFlightRef.current = true;
setIsCopying(true);
- const runId = createOnboardingRunId();
try {
- await navigator.clipboard.writeText(createWebMCPOnboardingPrompt(runId));
+ await navigator.clipboard.writeText(WEBMCP_SETUP_PROMPT);
} catch {
if (mountedRef.current) {
setCopyState("error");
@@ -67,9 +65,8 @@ export function WebMCPOnboardingPrompt(): React.JSX.Element {
scheduleReset(1800);
try {
- posthog?.capture(INTELLIGENCE_ONBOARDING_EVENTS.promptCopied, {
+ posthog?.capture(WEBMCP_SETUP_EVENTS.promptCopied, {
from_path: pathname,
- onboarding_run_id: runId,
surface: COPY_SURFACE,
});
} catch {
@@ -94,9 +91,8 @@ export function WebMCPOnboardingPrompt(): React.JSX.Element {
Add WebMCP with your coding agent
- It inspects your app first, then reuses an existing CopilotKit
- tool or wraps the smallest suitable browser action. WebMCP calls
- do not run through a backend agent.
+ Copies a short prompt that points your coding agent to this guide
+ and asks it to verify the result.
diff --git a/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx b/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
index 44376c97ec..799f804933 100644
--- a/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
+++ b/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
@@ -4,11 +4,11 @@ icon: "lucide/Globe2"
description: Expose Angular application actions as structured tools for compatible browser agents.
---
-import { WebMCPOnboardingPrompt } from "@/components/webmcp-onboarding-prompt";
+import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
WebMCP lets your website declare typed JavaScript tools that compatible browser agents can discover and call. CopilotKit can publish the same frontend tool handler to your configured agent and to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/).
-
+
WebMCP is a draft Community Group report. Chrome offers it through an origin
diff --git a/showcase/shell-docs/src/content/docs/webmcp.mdx b/showcase/shell-docs/src/content/docs/webmcp.mdx
index 83e17a43b4..1d2391e8b3 100644
--- a/showcase/shell-docs/src/content/docs/webmcp.mdx
+++ b/showcase/shell-docs/src/content/docs/webmcp.mdx
@@ -4,13 +4,13 @@ icon: "lucide/Globe2"
description: Expose browser-side actions as structured tools that compatible agents can discover and call.
---
-import { WebMCPOnboardingPrompt } from "@/components/webmcp-onboarding-prompt";
+import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
WebMCP lets your website tell compatible browser agents what they can do. Instead of guessing which buttons to click, an agent discovers a named tool with a description, a typed input schema, and a JavaScript handler.
CopilotKit can publish an existing frontend tool to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/). The same handler then works for your CopilotKit agent and for WebMCP-aware browser agents.
-
+
WebMCP is a draft Community Group report, not a W3C Standard. Chrome offers
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
index 41ba67aafe..9dad995319 100644
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
@@ -1,10 +1,11 @@
import { expect, test } from "vitest";
import { loadDoc } from "../docs-render";
import { renderPageToLlmText } from "../llm-text";
+import { WEBMCP_SETUP_PROMPT } from "../webmcp-setup-prompt";
-test("leads the WebMCP guide with repository-aware onboarding", () => {
+test("leads the WebMCP guide with the standalone setup prompt", () => {
const source = loadDoc("webmcp")?.source ?? "";
- const prompt = source.indexOf("");
+ const prompt = source.indexOf("");
const manualSetup = source.indexOf("## Add WebMCP manually");
expect(prompt).toBeGreaterThan(-1);
@@ -16,7 +17,7 @@ test("leads the WebMCP guide with repository-aware onboarding", () => {
expect(source).toContain("Chrome 149");
});
-test("expands the WebMCP onboarding CTA for Markdown and LLM readers", () => {
+test("expands the WebMCP setup CTA for Markdown and LLM readers", () => {
const doc = loadDoc("webmcp");
if (!doc) throw new Error("WebMCP guide is missing");
@@ -28,13 +29,9 @@ test("expands the WebMCP onboarding CTA for Markdown and LLM readers", () => {
loadSlug: "webmcp",
});
- expect(output).toContain(
- "npx --yes copilotkit@latest onboard start --coding-agent ",
- );
- expect(output).toContain(
- "The goal of this onboarding run is to get WebMCP working",
- );
- expect(output).not.toContain("");
+ expect(output).toContain(WEBMCP_SETUP_PROMPT);
+ expect(output).not.toContain("onboard start");
+ expect(output).not.toContain("");
});
test("provides an Angular-native WebMCP page for the frontend selector", () => {
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts
deleted file mode 100644
index bf212c4e51..0000000000
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt-markdown.test.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { expect, test } from "vitest";
-import { createWebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
-
-test("keeps the Markdown prompt usable when no telemetry run id is available", () => {
- const prompt = createWebMCPOnboardingPrompt();
-
- expect(prompt).toContain(
- "npx --yes copilotkit@latest onboard start --coding-agent ",
- );
- expect(prompt).not.toContain("--run undefined");
- expect(prompt).not.toContain("");
-});
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
deleted file mode 100644
index e2c1fa3997..0000000000
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-onboarding-prompt.test.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { expect, test } from "vitest";
-import { createWebMCPOnboardingPrompt } from "../webmcp-onboarding-prompt";
-
-test("keeps WebMCP as the goal of the CLI onboarding run", () => {
- const prompt = createWebMCPOnboardingPrompt("0123456789ab");
-
- expect(prompt)
- .toBe(`Identify which coding-agent product you are, using a short slug such as \`codex\` or \`claude-code\`. From the root of the project where you want CopilotKit, run \`npx --yes copilotkit@latest onboard start --run 0123456789ab --coding-agent \`. Follow the Markdown instructions it prints until onboarding is complete.
-
-The goal of this onboarding run is to get WebMCP working in this project. Use https://docs.copilotkit.ai/webmcp as the official guide. Let the onboarding workflow inspect the repository and determine whether the app already has CopilotKit, a backend agent, CopilotKit frontend tools, or only browser-side actions. Ask only about choices the repository cannot answer.
-
-A WebMCP call does not require a CopilotKit backend agent. If suitable CopilotKit frontend tools already exist, expose only the appropriate ones through WebMCP. Otherwise, wrap the smallest suitable browser-side action in a supported CopilotKit frontend tool. Do not create or replace a backend agent solely for WebMCP.
-
-Preserve the existing framework, agent behavior, authentication, authorization, and user-confirmation boundaries. Follow the guide for schemas, annotations, browser requirements, and verification. Finish only after a compatible browser can discover and call the intended tool; if the selected surface cannot provide \`document.modelContext\`, explain that limitation instead of adding a fake integration.`);
-});
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts
new file mode 100644
index 0000000000..838ff1cef6
--- /dev/null
+++ b/showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts
@@ -0,0 +1,9 @@
+import { expect, test } from "vitest";
+import { WEBMCP_SETUP_PROMPT } from "../webmcp-setup-prompt";
+
+test("keeps the standalone WebMCP setup prompt small and outcome-focused", () => {
+ expect(WEBMCP_SETUP_PROMPT).toBe(
+ "Set up WebMCP in this project using https://docs.copilotkit.ai/webmcp. Follow the guide and verify that a compatible browser can discover and call the tool.",
+ );
+ expect(WEBMCP_SETUP_PROMPT).not.toContain("onboard");
+});
diff --git a/showcase/shell-docs/src/lib/llm-text.ts b/showcase/shell-docs/src/lib/llm-text.ts
index c54754cb3b..6175a89d99 100644
--- a/showcase/shell-docs/src/lib/llm-text.ts
+++ b/showcase/shell-docs/src/lib/llm-text.ts
@@ -75,7 +75,7 @@ import { resolveDocsHref } from "./docs-link-rewrite";
import { resolveBundledSetupConcept } from "./setup-content";
import type { SetupContentBundle } from "./setup-content";
import { RICH_THREADS_SETUP_PROMPT } from "./rich-threads-setup-prompt";
-import { createWebMCPOnboardingPrompt } from "./webmcp-onboarding-prompt";
+import { WEBMCP_SETUP_PROMPT } from "./webmcp-setup-prompt";
interface Region {
file: string;
@@ -768,12 +768,12 @@ function expandRichThreadsSetupPrompts(body: string): string {
}
/** Expand the interactive WebMCP prompt for raw Markdown consumers. */
-function expandWebMCPOnboardingPrompts(body: string): string {
+function expandWebMCPSetupPrompts(body: string): string {
return body.replace(
- //g,
+ //g,
`### Copy this prompt into your coding agent\n\n${fenceFor(
"text",
- createWebMCPOnboardingPrompt(),
+ WEBMCP_SETUP_PROMPT,
)}`,
);
}
@@ -911,7 +911,7 @@ export function renderPageToLlmText(
// Interactive prompt buttons cannot run in raw Markdown or LLM feeds.
body = expandRichThreadsSetupPrompts(body);
- body = expandWebMCPOnboardingPrompts(body);
+ body = expandWebMCPSetupPrompts(body);
// 1) Inline `` shared snippets (``, etc.). Uses
// the SNIPPET_MAP / SUBPATH_TO_COMPONENT logic — same as the page
diff --git a/showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts b/showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts
deleted file mode 100644
index 7abd57ab1a..0000000000
--- a/showcase/shell-docs/src/lib/webmcp-onboarding-prompt.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {
- createIntelligenceOnboardingPrompt,
- INTELLIGENCE_ONBOARDING_PROMPT,
-} from "./intelligence-onboarding-prompt";
-
-export const WEBMCP_DOCS_URL = "https://docs.copilotkit.ai/webmcp";
-
-/** Build the WebMCP goal that a docs reader hands to the CLI onboarding flow. */
-export function createWebMCPOnboardingPrompt(runId?: string): string {
- const onboardingPrompt = runId
- ? createIntelligenceOnboardingPrompt(runId)
- : INTELLIGENCE_ONBOARDING_PROMPT.replace(" --run ", "");
-
- return `${onboardingPrompt}
-
-The goal of this onboarding run is to get WebMCP working in this project. Use ${WEBMCP_DOCS_URL} as the official guide. Let the onboarding workflow inspect the repository and determine whether the app already has CopilotKit, a backend agent, CopilotKit frontend tools, or only browser-side actions. Ask only about choices the repository cannot answer.
-
-A WebMCP call does not require a CopilotKit backend agent. If suitable CopilotKit frontend tools already exist, expose only the appropriate ones through WebMCP. Otherwise, wrap the smallest suitable browser-side action in a supported CopilotKit frontend tool. Do not create or replace a backend agent solely for WebMCP.
-
-Preserve the existing framework, agent behavior, authentication, authorization, and user-confirmation boundaries. Follow the guide for schemas, annotations, browser requirements, and verification. Finish only after a compatible browser can discover and call the intended tool; if the selected surface cannot provide \`document.modelContext\`, explain that limitation instead of adding a fake integration.`;
-}
diff --git a/showcase/shell-docs/src/lib/webmcp-setup-prompt.ts b/showcase/shell-docs/src/lib/webmcp-setup-prompt.ts
new file mode 100644
index 0000000000..338554c30e
--- /dev/null
+++ b/showcase/shell-docs/src/lib/webmcp-setup-prompt.ts
@@ -0,0 +1,7 @@
+export const WEBMCP_DOCS_URL = "https://docs.copilotkit.ai/webmcp";
+
+export const WEBMCP_SETUP_EVENTS = {
+ promptCopied: "docs.webmcp_setup_prompt_copied",
+} as const;
+
+export const WEBMCP_SETUP_PROMPT = `Set up WebMCP in this project using ${WEBMCP_DOCS_URL}. Follow the guide and verify that a compatible browser can discover and call the tool.`;
From 91aeed423cfa2beebc524d6ac5e0d8cfe34223b3 Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 10:09:25 -0700
Subject: [PATCH 07/57] docs(shell-docs): make WebMCP guide universal
---
.../__tests__/webmcp-setup-prompt.test.tsx | 88 ------------
.../src/components/docs-page-view.tsx | 2 -
.../src/components/webmcp-setup-prompt.tsx | 126 ------------------
.../content/docs/frontends/angular/webmcp.mdx | 82 ------------
.../shell-docs/src/content/docs/webmcp.mdx | 7 +-
.../src/lib/__tests__/webmcp-docs.test.ts | 43 ------
.../lib/__tests__/webmcp-setup-prompt.test.ts | 9 --
showcase/shell-docs/src/lib/llm-text.ts | 13 --
.../shell-docs/src/lib/webmcp-setup-prompt.ts | 7 -
9 files changed, 2 insertions(+), 375 deletions(-)
delete mode 100644 showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx
delete mode 100644 showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
delete mode 100644 showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
delete mode 100644 showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
delete mode 100644 showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts
delete mode 100644 showcase/shell-docs/src/lib/webmcp-setup-prompt.ts
diff --git a/showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx b/showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx
deleted file mode 100644
index 2bd4a676a2..0000000000
--- a/showcase/shell-docs/src/components/__tests__/webmcp-setup-prompt.test.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-// @vitest-environment jsdom
-
-import React from "react";
-import {
- cleanup,
- fireEvent,
- render,
- screen,
- waitFor,
-} from "@testing-library/react";
-import { afterEach, expect, test, vi } from "vitest";
-import { WebMCPSetupPrompt } from "../webmcp-setup-prompt";
-import {
- WEBMCP_SETUP_EVENTS,
- WEBMCP_SETUP_PROMPT,
-} from "@/lib/webmcp-setup-prompt";
-
-const analytics = vi.hoisted(() => ({ capture: vi.fn() }));
-
-vi.mock("next/navigation", () => ({
- usePathname: () => "/webmcp",
-}));
-
-vi.mock("posthog-js/react", () => ({
- usePostHog: () => analytics,
-}));
-
-afterEach(() => {
- cleanup();
- vi.clearAllMocks();
-});
-
-test("copies the standalone WebMCP setup prompt", async () => {
- const writeText = vi.fn().mockResolvedValue(undefined);
- Object.defineProperty(navigator, "clipboard", {
- configurable: true,
- value: { writeText },
- });
-
- render();
- fireEvent.click(screen.getByRole("button", { name: "Copy setup prompt" }));
-
- await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
-
- const prompt = writeText.mock.calls[0]?.[0] as string;
- expect(prompt).toBe(WEBMCP_SETUP_PROMPT);
- expect(prompt).not.toContain("onboard start");
- expect(screen.getByRole("button", { name: "Copied" })).toBeTruthy();
- expect(screen.getByText("Prompt copied")).toBeTruthy();
- expect(analytics.capture).toHaveBeenCalledWith(
- WEBMCP_SETUP_EVENTS.promptCopied,
- {
- from_path: "/webmcp",
- surface: "docs_webmcp_setup_prompt",
- },
- );
-});
-
-test("disables the CTA while the clipboard write is pending", async () => {
- let finishCopy: (() => void) | undefined;
- const writeText = vi.fn(
- () =>
- new Promise((resolve) => {
- finishCopy = resolve;
- }),
- );
- Object.defineProperty(navigator, "clipboard", {
- configurable: true,
- value: { writeText },
- });
-
- render();
- const button = screen.getByRole("button", { name: "Copy setup prompt" });
-
- fireEvent.click(button);
-
- await waitFor(() => expect(button).toHaveProperty("disabled", true));
- fireEvent.click(button);
- expect(writeText).toHaveBeenCalledTimes(1);
-
- finishCopy?.();
- await waitFor(() =>
- expect(screen.getByRole("button", { name: "Copied" })).toHaveProperty(
- "disabled",
- false,
- ),
- );
-});
diff --git a/showcase/shell-docs/src/components/docs-page-view.tsx b/showcase/shell-docs/src/components/docs-page-view.tsx
index 7ed9b3fa71..bb06a3c455 100644
--- a/showcase/shell-docs/src/components/docs-page-view.tsx
+++ b/showcase/shell-docs/src/components/docs-page-view.tsx
@@ -40,7 +40,6 @@ import type { OpsPlatformCTAProps } from "@/components/react/ops-platform-cta";
import { ChannelsStartPrompt } from "@/components/channels-start-prompt";
import type { ChannelsStartPromptProps } from "@/components/channels-start-prompt";
import { RichThreadsSetupPrompt } from "@/components/rich-threads-setup-prompt";
-import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
import { IntelligenceOnboardingPrompt } from "@/components/intelligence-onboarding-prompt";
import type { IntelligenceOnboardingPromptProps } from "@/components/intelligence-onboarding-prompt";
import { SignupLink } from "@/components/react/signup-link";
@@ -365,7 +364,6 @@ export async function DocsPageView({
/>
),
RichThreadsSetupPrompt,
- WebMCPSetupPrompt,
IntelligenceOnboardingPrompt:
IntelligenceOnboardingPromptMdx,
OpsPlatformCTA: (props: OpsPlatformCTAProps) => (
diff --git a/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
deleted file mode 100644
index 782c94dc08..0000000000
--- a/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-"use client";
-
-import React from "react";
-import { Copy, SquareTerminal } from "lucide-react";
-import { usePathname } from "next/navigation";
-import { usePostHog } from "posthog-js/react";
-import {
- WEBMCP_SETUP_EVENTS,
- WEBMCP_SETUP_PROMPT,
-} from "@/lib/webmcp-setup-prompt";
-
-const COPY_SURFACE = "docs_webmcp_setup_prompt";
-
-type CopyState = "idle" | "copied" | "error";
-
-/** Copyable, standalone WebMCP setup prompt. */
-export function WebMCPSetupPrompt(): React.JSX.Element {
- const titleId = React.useId();
- const pathname = usePathname();
- const posthog = usePostHog();
- const [copyState, setCopyState] = React.useState("idle");
- const [isCopying, setIsCopying] = React.useState(false);
- const copyInFlightRef = React.useRef(false);
- const resetTimerRef = React.useRef | null>(
- null,
- );
- const mountedRef = React.useRef(true);
-
- React.useEffect(
- () => () => {
- mountedRef.current = false;
- if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
- },
- [],
- );
-
- function scheduleReset(delayMs: number): void {
- if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
- resetTimerRef.current = setTimeout(() => {
- if (mountedRef.current) setCopyState("idle");
- resetTimerRef.current = null;
- }, delayMs);
- }
-
- async function copyPrompt(): Promise {
- if (copyInFlightRef.current) return;
- copyInFlightRef.current = true;
- setIsCopying(true);
-
- try {
- await navigator.clipboard.writeText(WEBMCP_SETUP_PROMPT);
- } catch {
- if (mountedRef.current) {
- setCopyState("error");
- scheduleReset(2600);
- }
- return;
- } finally {
- copyInFlightRef.current = false;
- if (mountedRef.current) setIsCopying(false);
- }
-
- if (!mountedRef.current) return;
- setCopyState("copied");
- scheduleReset(1800);
-
- try {
- posthog?.capture(WEBMCP_SETUP_EVENTS.promptCopied, {
- from_path: pathname,
- surface: COPY_SURFACE,
- });
- } catch {
- // Analytics must never change the result of a successful clipboard write.
- }
- }
-
- return (
-
-
-
-
-
-
- Add WebMCP with your coding agent
-
-
- Copies a short prompt that points your coding agent to this guide
- and asks it to verify the result.
-
-
-
-
-
-
-
-
- {copyState === "copied"
- ? "Prompt copied"
- : copyState === "error"
- ? "Prompt copy failed. Try again."
- : ""}
-
-
- );
-}
diff --git a/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx b/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
deleted file mode 100644
index 799f804933..0000000000
--- a/showcase/shell-docs/src/content/docs/frontends/angular/webmcp.mdx
+++ /dev/null
@@ -1,82 +0,0 @@
----
-title: WebMCP
-icon: "lucide/Globe2"
-description: Expose Angular application actions as structured tools for compatible browser agents.
----
-
-import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
-
-WebMCP lets your website declare typed JavaScript tools that compatible browser agents can discover and call. CopilotKit can publish the same frontend tool handler to your configured agent and to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/).
-
-
-
-
- WebMCP is a draft Community Group report. Chrome offers it through an origin
- trial beginning in Chrome 149, or through the
- `chrome://flags/#enable-webmcp-testing` flag for local development. Check the
- [current Chrome setup instructions](https://developer.chrome.com/docs/ai/webmcp)
- before testing.
-
-
-## Do I need an agent?
-
-You do not need a CopilotKit backend agent to handle a WebMCP call. A compatible browser agent calls the frontend tool handler directly on the page, so the handler context has no `agent` in that path.
-
-If your application already registers the action as a CopilotKit frontend tool, add `webmcp`. Otherwise, wrap the smallest suitable browser-side action in a frontend tool. Keep any existing backend agent, but do not create one only for WebMCP.
-
-## Add WebMCP manually
-
-Call `registerFrontendTool` in an injection context and opt the tool into WebMCP:
-
-```ts title="src/app/order-search.component.ts"
-import { Component } from "@angular/core";
-import { registerFrontendTool } from "@copilotkit/angular";
-import { z } from "zod";
-
-@Component({
- selector: "app-order-search",
- standalone: true,
- template: "",
-})
-export class OrderSearchComponent {
- constructor() {
- registerFrontendTool({
- name: "searchOrders",
- description: "Search the signed-in user's orders by status",
- parameters: z.object({
- status: z.enum(["open", "shipped", "delivered"]),
- }),
- handler: async ({ status }) => {
- const orders = await searchOrders(status);
- return JSON.stringify(orders);
- },
- webmcp: {
- annotations: {
- readOnlyHint: true,
- },
- },
- });
- }
-}
-```
-
-The tool remains registered while the owning injector is alive. CopilotKit removes it from `document.modelContext` when that injector is destroyed or when the tool becomes unavailable.
-
-See the [`registerFrontendTool` reference](/reference/angular/functions/registerFrontendTool) for the full API.
-
-## Secure and test the tool
-
-- Give the tool and each parameter a specific description.
-- Use `readOnlyHint` only for actions that cannot change state.
-- Use `untrustedContentHint` when results may contain user-generated or external content.
-- Enforce authentication, authorization, input validation, and required user confirmation in the handler or the API it calls. Annotations are hints, not policy.
-- Remember that `agentId` does not restrict WebMCP access; WebMCP registrations are page-level.
-
-Open the application in Chrome 149 or newer with the origin trial or local flag enabled. The page must be origin-isolated, and cross-origin iframes need `allow="tools"`. Use Chrome's [Model Context Tool Inspector](https://developer.chrome.com/docs/ai/webmcp#imitate-agent-chat-with-the-inspector-extension) to inspect the schema, call the tool, and verify the complete user task.
-
-## Related
-
-- [Frontend tools and generative UI](guides/frontend-tools-generative-ui)
-- [`registerFrontendTool` reference](/reference/angular/functions/registerFrontendTool)
-- [WebMCP specification](https://webmachinelearning.github.io/webmcp/)
-- [WebMCP tool security](https://developer.chrome.com/docs/ai/webmcp/secure-tools)
diff --git a/showcase/shell-docs/src/content/docs/webmcp.mdx b/showcase/shell-docs/src/content/docs/webmcp.mdx
index 1d2391e8b3..c06d5b28d0 100644
--- a/showcase/shell-docs/src/content/docs/webmcp.mdx
+++ b/showcase/shell-docs/src/content/docs/webmcp.mdx
@@ -2,16 +2,13 @@
title: WebMCP
icon: "lucide/Globe2"
description: Expose browser-side actions as structured tools that compatible agents can discover and call.
+frontend: universal
---
-import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
-
WebMCP lets your website tell compatible browser agents what they can do. Instead of guessing which buttons to click, an agent discovers a named tool with a description, a typed input schema, and a JavaScript handler.
CopilotKit can publish an existing frontend tool to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/). The same handler then works for your CopilotKit agent and for WebMCP-aware browser agents.
-
-
WebMCP is a draft Community Group report, not a W3C Standard. Chrome offers
it through an origin trial beginning in Chrome 149, or through the
@@ -32,7 +29,7 @@ Choose the smallest path that fits your app:
| A CopilotKit frontend tool for the action | Add `webmcp: true`, or add annotations with `webmcp: { annotations: ... }`. |
| CopilotKit, but no frontend tool for the action | Wrap the smallest suitable browser-side action in a frontend tool, then opt it into WebMCP. |
| A backend agent | Keep it. The tool can remain available to that agent, but WebMCP calls do not pass through it. |
-| No backend agent | Use `CopilotKitCore` directly in browser code, or let the setup prompt add the appropriate frontend integration. Do not create an agent only for WebMCP. |
+| No backend agent | Use `CopilotKitCore` directly in browser code, or add the appropriate frontend integration. Do not create an agent only for WebMCP. |
React Native does not expose `document.modelContext`, so WebMCP registration is a no-op there. CopilotKit currently supports this WebMCP option in its browser integrations for React, Vue, Angular, and direct core usage.
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
deleted file mode 100644
index 9dad995319..0000000000
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-docs.test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { expect, test } from "vitest";
-import { loadDoc } from "../docs-render";
-import { renderPageToLlmText } from "../llm-text";
-import { WEBMCP_SETUP_PROMPT } from "../webmcp-setup-prompt";
-
-test("leads the WebMCP guide with the standalone setup prompt", () => {
- const source = loadDoc("webmcp")?.source ?? "";
- const prompt = source.indexOf("");
- const manualSetup = source.indexOf("## Add WebMCP manually");
-
- expect(prompt).toBeGreaterThan(-1);
- expect(manualSetup).toBeGreaterThan(prompt);
- expect(source).toContain("You do not need a CopilotKit backend agent");
- expect(source).toContain(
- "React Native does not expose `document.modelContext`",
- );
- expect(source).toContain("Chrome 149");
-});
-
-test("expands the WebMCP setup CTA for Markdown and LLM readers", () => {
- const doc = loadDoc("webmcp");
- if (!doc) throw new Error("WebMCP guide is missing");
-
- const output = renderPageToLlmText({
- url: "webmcp",
- title: doc.fm.title,
- description: doc.fm.description,
- filePath: doc.filePath,
- loadSlug: "webmcp",
- });
-
- expect(output).toContain(WEBMCP_SETUP_PROMPT);
- expect(output).not.toContain("onboard start");
- expect(output).not.toContain("");
-});
-
-test("provides an Angular-native WebMCP page for the frontend selector", () => {
- const source = loadDoc("frontends/angular/webmcp")?.source ?? "";
-
- expect(source).toContain("registerFrontendTool");
- expect(source).toContain("webmcp:");
- expect(source).not.toMatch(/\bReact\b/);
-});
diff --git a/showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts b/showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts
deleted file mode 100644
index 838ff1cef6..0000000000
--- a/showcase/shell-docs/src/lib/__tests__/webmcp-setup-prompt.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { expect, test } from "vitest";
-import { WEBMCP_SETUP_PROMPT } from "../webmcp-setup-prompt";
-
-test("keeps the standalone WebMCP setup prompt small and outcome-focused", () => {
- expect(WEBMCP_SETUP_PROMPT).toBe(
- "Set up WebMCP in this project using https://docs.copilotkit.ai/webmcp. Follow the guide and verify that a compatible browser can discover and call the tool.",
- );
- expect(WEBMCP_SETUP_PROMPT).not.toContain("onboard");
-});
diff --git a/showcase/shell-docs/src/lib/llm-text.ts b/showcase/shell-docs/src/lib/llm-text.ts
index 6175a89d99..c379491e96 100644
--- a/showcase/shell-docs/src/lib/llm-text.ts
+++ b/showcase/shell-docs/src/lib/llm-text.ts
@@ -75,7 +75,6 @@ import { resolveDocsHref } from "./docs-link-rewrite";
import { resolveBundledSetupConcept } from "./setup-content";
import type { SetupContentBundle } from "./setup-content";
import { RICH_THREADS_SETUP_PROMPT } from "./rich-threads-setup-prompt";
-import { WEBMCP_SETUP_PROMPT } from "./webmcp-setup-prompt";
interface Region {
file: string;
@@ -767,17 +766,6 @@ function expandRichThreadsSetupPrompts(body: string): string {
);
}
-/** Expand the interactive WebMCP prompt for raw Markdown consumers. */
-function expandWebMCPSetupPrompts(body: string): string {
- return body.replace(
- //g,
- `### Copy this prompt into your coding agent\n\n${fenceFor(
- "text",
- WEBMCP_SETUP_PROMPT,
- )}`,
- );
-}
-
/**
* Drop `` tags — these mount live iframes in the
* browser; in plain markdown they're noise. Leave a short note so the
@@ -911,7 +899,6 @@ export function renderPageToLlmText(
// Interactive prompt buttons cannot run in raw Markdown or LLM feeds.
body = expandRichThreadsSetupPrompts(body);
- body = expandWebMCPSetupPrompts(body);
// 1) Inline `` shared snippets (``, etc.). Uses
// the SNIPPET_MAP / SUBPATH_TO_COMPONENT logic — same as the page
diff --git a/showcase/shell-docs/src/lib/webmcp-setup-prompt.ts b/showcase/shell-docs/src/lib/webmcp-setup-prompt.ts
deleted file mode 100644
index 338554c30e..0000000000
--- a/showcase/shell-docs/src/lib/webmcp-setup-prompt.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const WEBMCP_DOCS_URL = "https://docs.copilotkit.ai/webmcp";
-
-export const WEBMCP_SETUP_EVENTS = {
- promptCopied: "docs.webmcp_setup_prompt_copied",
-} as const;
-
-export const WEBMCP_SETUP_PROMPT = `Set up WebMCP in this project using ${WEBMCP_DOCS_URL}. Follow the guide and verify that a compatible browser can discover and call the tool.`;
From 46225f7c96ce55a122178bc9b135bed899ce40b5 Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 10:34:41 -0700
Subject: [PATCH 08/57] docs(shell-docs): add WebMCP prompt CTA
---
.../src/components/docs-page-tools.tsx | 25 +++++++++++--------
.../src/components/docs-page-view.tsx | 1 +
.../shell-docs/src/content/docs/webmcp.mdx | 16 ++++++++++++
3 files changed, 32 insertions(+), 10 deletions(-)
diff --git a/showcase/shell-docs/src/components/docs-page-tools.tsx b/showcase/shell-docs/src/components/docs-page-tools.tsx
index 2d1c215d1f..2e54bf904a 100644
--- a/showcase/shell-docs/src/components/docs-page-tools.tsx
+++ b/showcase/shell-docs/src/components/docs-page-tools.tsx
@@ -7,11 +7,11 @@
// whole nav tree, so asserting the row's contents through it would mean
// standing up most of the docs pipeline.
//
-// Every page that renders this row gets the onboarding button. `DocsPageView`
-// is only reached by docs surfaces, and the button's offer — set CopilotKit up
-// in this project — holds on all of them. Earlier revisions gated it on the
-// caller naming a framework, which made identical pages behave differently
-// depending on the URL the reader arrived through.
+// Pages get the onboarding button by default. A page with its own focused
+// prompt CTA can hide that one action while keeping the Markdown and LLM
+// tools. Earlier revisions gated it on the caller naming a framework, which
+// made identical pages behave differently depending on the URL the reader
+// arrived through.
import React from "react";
import {
@@ -41,6 +41,8 @@ export interface DocsPageToolsProps {
* framework, so the CLI's graph has to ask for neither selection.
*/
onboardingFrontend?: { id: string; name: string };
+ /** Hide the generic onboarding prompt when the page provides its own CTA. */
+ hideOnboardingPrompt?: boolean;
}
/**
@@ -66,15 +68,18 @@ export function DocsPageTools({
githubUrl,
onboardingFramework,
onboardingFrontend,
+ hideOnboardingPrompt = false,
}: DocsPageToolsProps): React.JSX.Element {
const markdownUrl = docsMarkdownUrl(slugHrefPrefix, slugPath);
return (
-
+ {!hideOnboardingPrompt && (
+
+ )}
diff --git a/showcase/shell-docs/src/components/docs-page-view.tsx b/showcase/shell-docs/src/components/docs-page-view.tsx
index bb06a3c455..ab59411f0a 100644
--- a/showcase/shell-docs/src/components/docs-page-view.tsx
+++ b/showcase/shell-docs/src/components/docs-page-view.tsx
@@ -320,6 +320,7 @@ export async function DocsPageView({
githubUrl={buildGitHubUrl(doc.filePath)}
onboardingFramework={onboardingFramework}
onboardingFrontend={onboardingFrontend}
+ hideOnboardingPrompt={slugPath === "webmcp"}
/>
{/* Thin divider between the page-actions row and the page body
diff --git a/showcase/shell-docs/src/content/docs/webmcp.mdx b/showcase/shell-docs/src/content/docs/webmcp.mdx
index c06d5b28d0..a7c6057264 100644
--- a/showcase/shell-docs/src/content/docs/webmcp.mdx
+++ b/showcase/shell-docs/src/content/docs/webmcp.mdx
@@ -5,6 +5,22 @@ description: Expose browser-side actions as structured tools that compatible age
frontend: universal
---
+
+
+
+
+```text
+Set up WebMCP in this project using https://docs.copilotkit.ai/webmcp. First inspect the app and extend its existing CopilotKit setup if present; do not add a backend agent solely for WebMCP. If I haven’t specified a tool, ask what I want to expose. If I don’t have one in mind, add a small, read-only demo tool that fits the app. Finish by verifying that a compatible browser can discover and call it.
+```
+
+
+
+
+
WebMCP lets your website tell compatible browser agents what they can do. Instead of guessing which buttons to click, an agent discovers a named tool with a description, a typed input schema, and a JavaScript handler.
CopilotKit can publish an existing frontend tool to [`document.modelContext`](https://webmachinelearning.github.io/webmcp/). The same handler then works for your CopilotKit agent and for WebMCP-aware browser agents.
From fdd4e33a6c716bfb62c33c39b1913f7d13bed8ff Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 10:45:16 -0700
Subject: [PATCH 09/57] docs(shell-docs): refine WebMCP prompt CTA
---
.../src/components/webmcp-setup-prompt.tsx | 73 +++++++++++++++++++
.../shell-docs/src/content/docs/webmcp.mdx | 16 +---
showcase/shell-docs/src/lib/mdx-registry.tsx | 2 +
3 files changed, 79 insertions(+), 12 deletions(-)
create mode 100644 showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
diff --git a/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
new file mode 100644
index 0000000000..13e8257cce
--- /dev/null
+++ b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import React from "react";
+import { ChevronRight } from "lucide-react";
+
+export const WEBMCP_SETUP_PROMPT =
+ "Set up WebMCP in this project using https://docs.copilotkit.ai/webmcp. First inspect the app and extend its existing CopilotKit setup if present; do not add a backend agent solely for WebMCP. If I haven’t specified a tool, ask what I want to expose. If I don’t have one in mind, add a small, read-only demo tool that fits the app. Finish by verifying that a compatible browser can discover and call it.";
+
+type CopyState = "idle" | "copied" | "error";
+
+export function WebMCPSetupPrompt(): React.JSX.Element {
+ const [copyState, setCopyState] = React.useState("idle");
+ const resetTimerRef = React.useRef | null>(
+ null,
+ );
+
+ React.useEffect(
+ () => () => {
+ if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
+ },
+ [],
+ );
+
+ async function copyPrompt(): Promise {
+ if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
+
+ try {
+ await navigator.clipboard.writeText(WEBMCP_SETUP_PROMPT);
+ setCopyState("copied");
+ resetTimerRef.current = setTimeout(() => setCopyState("idle"), 1800);
+ } catch {
+ setCopyState("error");
+ resetTimerRef.current = setTimeout(() => setCopyState("idle"), 2600);
+ }
+ }
+
+ return (
+
+
+
+
+ Use this pre-built prompt to get WebMCP running faster.
+
+ );
+}
diff --git a/showcase/shell-docs/src/content/docs/webmcp.mdx b/showcase/shell-docs/src/content/docs/webmcp.mdx
index a7c6057264..dc8b1c38b7 100644
--- a/showcase/shell-docs/src/content/docs/webmcp.mdx
+++ b/showcase/shell-docs/src/content/docs/webmcp.mdx
@@ -5,21 +5,13 @@ description: Expose browser-side actions as structured tools that compatible age
frontend: universal
---
-
+import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
-
+## Setup with a coding agent
-```text
-Set up WebMCP in this project using https://docs.copilotkit.ai/webmcp. First inspect the app and extend its existing CopilotKit setup if present; do not add a backend agent solely for WebMCP. If I haven’t specified a tool, ask what I want to expose. If I don’t have one in mind, add a small, read-only demo tool that fits the app. Finish by verifying that a compatible browser can discover and call it.
-```
+Use this pre-built prompt to add WebMCP to your project. Your coding agent will adapt the implementation to your app and verify that a compatible browser can discover and call the tool.
-
-
-
+
WebMCP lets your website tell compatible browser agents what they can do. Instead of guessing which buttons to click, an agent discovers a named tool with a description, a typed input schema, and a JavaScript handler.
diff --git a/showcase/shell-docs/src/lib/mdx-registry.tsx b/showcase/shell-docs/src/lib/mdx-registry.tsx
index 0ea906de8c..c0f12af39a 100644
--- a/showcase/shell-docs/src/lib/mdx-registry.tsx
+++ b/showcase/shell-docs/src/lib/mdx-registry.tsx
@@ -33,6 +33,7 @@ import { DocsLandingNext } from "@/components/docs-landing-next";
import { WhenFrameworkHas } from "@/components/when-framework-has";
import { WhenAngularBackend } from "@/components/when-angular-backend";
import { AgentCoreCommandTabs } from "@/components/agentcore-command-tabs";
+import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
import { DemoSource } from "@/components/demo-source";
import { AngularFeatureCatalog } from "@/components/angular-feature-catalog";
import { AngularSnippet } from "@/components/angular-snippet";
@@ -270,6 +271,7 @@ export const docsComponents = {
SignupLink,
DocsTrackedCopy,
DocsTrackedLink,
+ WebMCPSetupPrompt,
FeatureIntegrations: ({ feature }: { feature?: string }) => {
if (!feature) {
warnSilentNull("FeatureIntegrations", "no `feature` prop provided");
From fe33b816f8a08ab3b793c992bae5d00c53b7fd7d Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 10:46:17 -0700
Subject: [PATCH 10/57] docs(shell-docs): reorder WebMCP overview
---
showcase/shell-docs/src/content/docs/webmcp.mdx | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/showcase/shell-docs/src/content/docs/webmcp.mdx b/showcase/shell-docs/src/content/docs/webmcp.mdx
index dc8b1c38b7..13088ee0da 100644
--- a/showcase/shell-docs/src/content/docs/webmcp.mdx
+++ b/showcase/shell-docs/src/content/docs/webmcp.mdx
@@ -7,11 +7,7 @@ frontend: universal
import { WebMCPSetupPrompt } from "@/components/webmcp-setup-prompt";
-## Setup with a coding agent
-
-Use this pre-built prompt to add WebMCP to your project. Your coding agent will adapt the implementation to your app and verify that a compatible browser can discover and call the tool.
-
-
+## Overview
WebMCP lets your website tell compatible browser agents what they can do. Instead of guessing which buttons to click, an agent discovers a named tool with a description, a typed input schema, and a JavaScript handler.
@@ -26,6 +22,12 @@ CopilotKit can publish an existing frontend tool to [`document.modelContext`](ht
before testing.
+## Setup with a coding agent
+
+Use this pre-built prompt to add WebMCP to your project. Your coding agent will adapt the implementation to your app and verify that a compatible browser can discover and call the tool.
+
+
+
## Do I need an agent?
You do not need a CopilotKit backend agent to handle a WebMCP call. A compatible browser agent calls the frontend tool handler directly on the page; in that path, the handler context has no `agent`.
From af5fbf4065ed94b25cb4700cf1a442992e356d1f Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 10:53:04 -0700
Subject: [PATCH 11/57] docs(shell-docs): polish WebMCP prompt CTA
---
.../src/components/webmcp-setup-prompt.tsx | 63 +++++++++++++------
1 file changed, 44 insertions(+), 19 deletions(-)
diff --git a/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
index 13e8257cce..ef00f93b89 100644
--- a/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
+++ b/showcase/shell-docs/src/components/webmcp-setup-prompt.tsx
@@ -9,6 +9,8 @@ export const WEBMCP_SETUP_PROMPT =
type CopyState = "idle" | "copied" | "error";
export function WebMCPSetupPrompt(): React.JSX.Element {
+ const promptId = React.useId();
+ const [isExpanded, setIsExpanded] = React.useState(false);
const [copyState, setCopyState] = React.useState("idle");
const resetTimerRef = React.useRef | null>(
null,
@@ -36,30 +38,53 @@ export function WebMCPSetupPrompt(): React.JSX.Element {
return (
-
-
-
+
+
+
+
Use this pre-built prompt to get WebMCP running faster.
-
+
+
+
-
+ {isExpanded && (
+
+
+ {WEBMCP_SETUP_PROMPT}
+
+
+ )}
{copyState === "copied"
From 887e5a69c40370dc3bba06e3c311fb0045ea1ed5 Mon Sep 17 00:00:00 2001
From: Alem Tuzlak
Date: Thu, 3 Sep 2026 10:18:52 +0200
Subject: [PATCH 12/57] feat(docs): add Intelligence landing hero with sizzle
video
---
.../__tests__/hero-start-commands.test.tsx | 16 ++
.../__tests__/intelligence-overview.test.tsx | 105 +++++++++++
.../landing-pages/intelligence-overview.tsx | 171 ++++++++++++++++++
.../src/components/hero-start-commands.tsx | 4 +-
4 files changed, 295 insertions(+), 1 deletion(-)
create mode 100644 showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx
create mode 100644 showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx
diff --git a/showcase/shell-docs/src/components/__tests__/hero-start-commands.test.tsx b/showcase/shell-docs/src/components/__tests__/hero-start-commands.test.tsx
index 3b41a77982..b32a662d22 100644
--- a/showcase/shell-docs/src/components/__tests__/hero-start-commands.test.tsx
+++ b/showcase/shell-docs/src/components/__tests__/hero-start-commands.test.tsx
@@ -89,6 +89,22 @@ describe("HeroStartActions", () => {
});
describe("QuickstartLinkButton", () => {
+ it("renders a custom label when one is passed", () => {
+ render(
+ ,
+ );
+
+ const link = screen.getByRole("link", { name: /connect an app/i });
+
+ expect(link.getAttribute("href")).toBe(
+ "/intelligence/connect-your-runtime",
+ );
+ expect(screen.queryByRole("link", { name: /^quickstart$/i })).toBeNull();
+ });
+
it("renders the accent primary treatment by default", () => {
render();
diff --git a/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx b/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx
new file mode 100644
index 0000000000..05bbd81583
--- /dev/null
+++ b/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx
@@ -0,0 +1,105 @@
+// @vitest-environment jsdom
+
+import React from "react";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import {
+ INTELLIGENCE_SIZZLE_VIDEO_URL,
+ IntelligenceOverview,
+} from "../intelligence-overview";
+
+vi.mock("next/link", () => ({
+ default: ({ children, href, ...props }: React.ComponentProps<"a">) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("next/navigation", () => ({
+ usePathname: () => "/intelligence/overview",
+}));
+
+vi.mock("posthog-js/react", () => ({
+ usePostHog: () => ({ capture: vi.fn() }),
+}));
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("IntelligenceOverview", () => {
+ it("renders the hero headline, copy prompt, and connect action", () => {
+ render();
+
+ expect(
+ screen.getByRole("heading", {
+ level: 1,
+ name: "Ship durable agent experiences",
+ }),
+ ).toBeTruthy();
+ expect(
+ screen.getByRole("button", { name: /copy onboarding prompt/i }),
+ ).toBeTruthy();
+
+ const connect = screen.getByRole("link", { name: /connect an app/i });
+ expect(connect.getAttribute("href")).toBe(
+ "/intelligence/connect-your-runtime",
+ );
+ });
+
+ it("renders the sizzle video with a pause control", () => {
+ render();
+
+ const video = screen.getByLabelText("CopilotKit Intelligence product demo");
+ if (!(video instanceof HTMLVideoElement)) {
+ throw new Error("expected a video element");
+ }
+ expect(video.getAttribute("src")).toBe(INTELLIGENCE_SIZZLE_VIDEO_URL);
+ expect(video.controls).toBe(true);
+ expect(video.muted).toBe(true);
+ expect(video.loop).toBe(true);
+ });
+
+ it("links each feature card to its guide", () => {
+ render();
+
+ expect(
+ screen
+ .getByRole("link", { name: "Open the Rich Threads guide" })
+ .getAttribute("href"),
+ ).toBe("/threads");
+ expect(
+ screen
+ .getByRole("link", { name: "See Analytics on the product page" })
+ .getAttribute("href"),
+ ).toBe(
+ "https://www.copilotkit.ai/copilotkit-intelligence#analytics-insights",
+ );
+ expect(
+ screen
+ .getByRole("link", {
+ name: "See Automatic Learning on the product page",
+ })
+ .getAttribute("href"),
+ ).toBe(
+ "https://www.copilotkit.ai/copilotkit-intelligence#self-improvement",
+ );
+ expect(
+ screen
+ .getByRole("link", { name: "Open the self-hosting guide" })
+ .getAttribute("href"),
+ ).toBe("/intelligence/self-hosting");
+ });
+
+ it("links pricing out to the public pricing page", () => {
+ render();
+
+ expect(
+ screen
+ .getByRole("link", { name: "See CopilotKit Intelligence pricing" })
+ .getAttribute("href"),
+ ).toBe("https://www.copilotkit.ai/pricing");
+ });
+});
diff --git a/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx b/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx
new file mode 100644
index 0000000000..4a8702ed72
--- /dev/null
+++ b/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import Link from "next/link";
+import { ArrowRight } from "lucide-react";
+import { useEffect, useRef } from "react";
+
+import { HeroOnboardingPromptButton } from "@/components/hero-onboarding-prompt-button";
+import {
+ HeroStartActions,
+ QuickstartLinkButton,
+} from "@/components/hero-start-commands";
+
+export const INTELLIGENCE_SIZZLE_VIDEO_URL =
+ "https://github.com/user-attachments/assets/72b7b4f3-b6e7-460c-a932-5746fe3c8db3";
+
+const CONNECT_HREF = "/intelligence/connect-your-runtime";
+const PRICING_HREF = "https://www.copilotkit.ai/pricing";
+
+const FEATURES = [
+ {
+ title: "Rich Threads",
+ body: "Keep messages, generative UI, and tool activity across reloads and devices.",
+ href: "/threads",
+ cta: "Open the Rich Threads guide",
+ },
+ {
+ title: "Analytics",
+ body: "See what your agents do and where users get value, from the same interaction data.",
+ href: "https://www.copilotkit.ai/copilotkit-intelligence#analytics-insights",
+ cta: "See Analytics on the product page",
+ },
+ {
+ title: "Automatic Learning",
+ body: "Agents improve from real usage. No fine-tuning pipeline required.",
+ href: "https://www.copilotkit.ai/copilotkit-intelligence#self-improvement",
+ cta: "See Automatic Learning on the product page",
+ },
+ {
+ title: "Self-hosting",
+ body: "Run the same platform in your own cluster, VPC, or data boundary.",
+ href: "/intelligence/self-hosting",
+ cta: "Open the self-hosting guide",
+ },
+] as const;
+
+function SizzleVideo() {
+ const videoRef = useRef(null);
+
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video) return;
+ if (typeof window.matchMedia !== "function") return;
+
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)");
+
+ function applyReducedMotion(reduce: boolean) {
+ if (!video) return;
+ if (reduce) {
+ video.pause();
+ return;
+ }
+ void video.play();
+ }
+
+ applyReducedMotion(media.matches);
+
+ function onChange(event: MediaQueryListEvent) {
+ applyReducedMotion(event.matches);
+ }
+
+ media.addEventListener("change", onChange);
+ return () => media.removeEventListener("change", onChange);
+ }, []);
+
+ return (
+
+ );
+}
+
+export function IntelligenceOverview() {
+ return (
+
+
+
+
+ Ship durable agent experiences
+
+
+ CopilotKit Intelligence adds persistent threads, hosted inspection,
+ and production operations next to the runtime you already run.
+
+
+
+ }
+ quickstart={
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ What you can add next
+
+
+ {FEATURES.map((feature) => (
+
+
+ {feature.title}
+
+
+ {feature.body}
+
+
+ {feature.cta}
+
+
+
+ ))}
+
+
+
+
+ Plans start on the free Developer tier.{" "}
+
+ See CopilotKit Intelligence pricing
+
+ .
+
+
+ );
+}
diff --git a/showcase/shell-docs/src/components/hero-start-commands.tsx b/showcase/shell-docs/src/components/hero-start-commands.tsx
index b890495a5b..0881db9885 100644
--- a/showcase/shell-docs/src/components/hero-start-commands.tsx
+++ b/showcase/shell-docs/src/components/hero-start-commands.tsx
@@ -45,12 +45,14 @@ export function QuickstartLinkButton({
backend,
fromPath,
variant = "primary",
+ label = "Quickstart",
}: {
href: string;
frontend?: string;
backend?: string;
fromPath?: string;
variant?: QuickstartVariant;
+ label?: string;
}) {
const posthog = usePostHog();
@@ -74,7 +76,7 @@ export function QuickstartLinkButton({
onClick={handleClick}
className={`${QUICKSTART_BASE_CLASS} ${QUICKSTART_VARIANT_CLASS[variant]}`}
>
- Quickstart
+ {label}
);
From 93fd02f0510d72e2b93ef5d86eacf05027ee71ff Mon Sep 17 00:00:00 2001
From: Alem Tuzlak
Date: Thu, 3 Sep 2026 10:21:21 +0200
Subject: [PATCH 13/57] feat(docs): wire the Intelligence landing into the
overview page
---
.../src/components/docs-page-view.tsx | 18 ++++----
.../content/docs/intelligence/overview.mdx | 1 +
.../snippets/shared/intelligence/overview.mdx | 2 +
.../__tests__/intelligence-landing.test.ts | 41 +++++++++++++++++++
showcase/shell-docs/src/lib/docs-render.tsx | 3 ++
showcase/shell-docs/src/lib/mdx-registry.tsx | 2 +
6 files changed, 60 insertions(+), 7 deletions(-)
create mode 100644 showcase/shell-docs/src/lib/__tests__/intelligence-landing.test.ts
diff --git a/showcase/shell-docs/src/components/docs-page-view.tsx b/showcase/shell-docs/src/components/docs-page-view.tsx
index ab59411f0a..90aa2cf197 100644
--- a/showcase/shell-docs/src/components/docs-page-view.tsx
+++ b/showcase/shell-docs/src/components/docs-page-view.tsx
@@ -298,13 +298,17 @@ export async function DocsPageView({
})}
-
- {doc.fm.title}
-
- {doc.fm.description && (
-
- {doc.fm.description}
-
+ {!doc.fm.hideHeader && (
+ <>
+
+ {doc.fm.title}
+
+ {doc.fm.description && (
+
+ {doc.fm.description}
+
+ )}
+ >
)}
{/* Page actions (Copy agent prompt / Copy Markdown / Open in
diff --git a/showcase/shell-docs/src/content/docs/intelligence/overview.mdx b/showcase/shell-docs/src/content/docs/intelligence/overview.mdx
index 7a6409aca4..52415e9fbc 100644
--- a/showcase/shell-docs/src/content/docs/intelligence/overview.mdx
+++ b/showcase/shell-docs/src/content/docs/intelligence/overview.mdx
@@ -3,6 +3,7 @@ title: CopilotKit Intelligence
icon: "lucide/Star"
description: CopilotKit Intelligence overview for CopilotKit — features, cloud-hosted and self-hosted deployment options, threads, hosted inspection, and production operations.
doc_type: explanation
+hideHeader: true
---
import Overview from "@/snippets/shared/intelligence/overview.mdx";
diff --git a/showcase/shell-docs/src/content/snippets/shared/intelligence/overview.mdx b/showcase/shell-docs/src/content/snippets/shared/intelligence/overview.mdx
index 8e9b52003d..84ee941086 100644
--- a/showcase/shell-docs/src/content/snippets/shared/intelligence/overview.mdx
+++ b/showcase/shell-docs/src/content/snippets/shared/intelligence/overview.mdx
@@ -1,5 +1,7 @@
import InspectorPaneLearning from "@/snippets/shared/inspector/open-inspector-pane-learning.mdx";
+
+
## What is CopilotKit Intelligence?
CopilotKit Intelligence is CopilotKit's production layer for durable threads, persistence, hosted inspection, and operational visibility. It sits beside your CopilotKit runtime and gives production agentic applications shared infrastructure without changing the frontend SDK, AG-UI protocol, or agent framework you use.
diff --git a/showcase/shell-docs/src/lib/__tests__/intelligence-landing.test.ts b/showcase/shell-docs/src/lib/__tests__/intelligence-landing.test.ts
new file mode 100644
index 0000000000..dd684ff4c3
--- /dev/null
+++ b/showcase/shell-docs/src/lib/__tests__/intelligence-landing.test.ts
@@ -0,0 +1,41 @@
+import { readFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import matter from "gray-matter";
+import { expect, test } from "vitest";
+
+const here = dirname(fileURLToPath(import.meta.url));
+
+function read(relativeFromSrc: string) {
+ return readFileSync(resolve(here, "../..", relativeFromSrc), "utf8");
+}
+
+test("the Intelligence overview hides the default docs header", () => {
+ const page = matter(read("content/docs/intelligence/overview.mdx"));
+ const parser = read("lib/docs-render.tsx");
+
+ expect(page.data.hideHeader).toBe(true);
+ expect(parser).toContain("const hideHeader = data.hideHeader === true");
+});
+
+test("the shared Intelligence overview mounts the landing then keeps platform copy", () => {
+ const snippet = read("content/snippets/shared/intelligence/overview.mdx");
+
+ expect(snippet).toContain(" {
+ const registry = read("lib/mdx-registry.tsx");
+ const pageView = read("components/docs-page-view.tsx");
+
+ expect(registry).toContain(
+ 'from "@/components/content/landing-pages/intelligence-overview"',
+ );
+ expect(registry).toContain("IntelligenceOverview,");
+ expect(pageView).toContain("!doc.fm.hideHeader");
+});
diff --git a/showcase/shell-docs/src/lib/docs-render.tsx b/showcase/shell-docs/src/lib/docs-render.tsx
index 11d21f86db..7c0abb169a 100644
--- a/showcase/shell-docs/src/lib/docs-render.tsx
+++ b/showcase/shell-docs/src/lib/docs-render.tsx
@@ -1681,6 +1681,7 @@ export interface DocFrontmatter {
defaultFramework?: string;
defaultCell?: string;
hideTOC?: boolean;
+ hideHeader?: boolean;
frontend?: unknown;
/**
* Early-access gate id (see `src/lib/early-access.ts`). When set,
@@ -1830,6 +1831,7 @@ export function loadDoc(
const defaultCell =
typeof data.snippet_cell === "string" ? data.snippet_cell : undefined;
const hideTOC = data.hideTOC === true;
+ const hideHeader = data.hideHeader === true;
const frontend = data.frontend;
const earlyAccess =
typeof data.earlyAccess === "string" ? data.earlyAccess : undefined;
@@ -1843,6 +1845,7 @@ export function loadDoc(
defaultFramework,
defaultCell,
hideTOC,
+ hideHeader,
frontend,
earlyAccess,
},
diff --git a/showcase/shell-docs/src/lib/mdx-registry.tsx b/showcase/shell-docs/src/lib/mdx-registry.tsx
index c0f12af39a..e185aacc48 100644
--- a/showcase/shell-docs/src/lib/mdx-registry.tsx
+++ b/showcase/shell-docs/src/lib/mdx-registry.tsx
@@ -41,6 +41,7 @@ import { UnsupportedBox } from "@/components/snippet";
import { getRegistry } from "@/lib/registry";
import { PartialLoader } from "@/lib/mdx-registry-loader";
import { MdxFrameworkOverview } from "@/components/content/landing-pages/mdx-framework-overview";
+import { IntelligenceOverview } from "@/components/content/landing-pages/intelligence-overview";
import { FrameworkSetup } from "@/lib/setup-concept";
import {
AdkIcon,
@@ -590,6 +591,7 @@ export const docsComponents = {
// features grid, architecture image, live demos) instead of being
// dropped on the floor as a children-passthrough used to do.
FrameworkOverview: MdxFrameworkOverview,
+ IntelligenceOverview,
// Per-render override in DocsPageView binds `currentFramework` from
// the URL — same closure pattern as MdxFrameworkOverview. The base
// registration renders null when invoked without a framework slug
From 9236d514f328325a5640e4242b61f7174f0817c4 Mon Sep 17 00:00:00 2001
From: Alem Tuzlak
Date: Thu, 3 Sep 2026 12:08:11 +0200
Subject: [PATCH 14/57] feat(docs): polish the Intelligence landing layout and
feature cards
---
.../__tests__/intelligence-overview.test.tsx | 32 ++--
.../landing-pages/intelligence-overview.tsx | 155 +++++++++---------
.../snippets/shared/intelligence/overview.mdx | 8 +
.../__tests__/intelligence-landing.test.ts | 17 ++
showcase/shell-docs/src/lib/mdx-registry.tsx | 6 +-
5 files changed, 130 insertions(+), 88 deletions(-)
diff --git a/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx b/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx
index 05bbd81583..f4870911b9 100644
--- a/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx
+++ b/showcase/shell-docs/src/components/content/landing-pages/__tests__/intelligence-overview.test.tsx
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import {
INTELLIGENCE_SIZZLE_VIDEO_URL,
+ IntelligenceFeatureCards,
IntelligenceOverview,
} from "../intelligence-overview";
@@ -36,7 +37,7 @@ describe("IntelligenceOverview", () => {
expect(
screen.getByRole("heading", {
level: 1,
- name: "Ship durable agent experiences",
+ name: "Ship production grade agent experiences",
}),
).toBeTruthy();
expect(
@@ -60,10 +61,17 @@ describe("IntelligenceOverview", () => {
expect(video.controls).toBe(true);
expect(video.muted).toBe(true);
expect(video.loop).toBe(true);
+
+ const prompt = screen.getByRole("button", {
+ name: /copy onboarding prompt/i,
+ });
+ expect(
+ video.compareDocumentPosition(prompt) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
});
it("links each feature card to its guide", () => {
- render();
+ render();
expect(
screen
@@ -91,15 +99,17 @@ describe("IntelligenceOverview", () => {
.getByRole("link", { name: "Open the self-hosting guide" })
.getAttribute("href"),
).toBe("/intelligence/self-hosting");
- });
- it("links pricing out to the public pricing page", () => {
- render();
-
- expect(
- screen
- .getByRole("link", { name: "See CopilotKit Intelligence pricing" })
- .getAttribute("href"),
- ).toBe("https://www.copilotkit.ai/pricing");
+ for (const title of [
+ "Rich Threads",
+ "Analytics",
+ "Automatic Learning",
+ "Self-hosting",
+ ]) {
+ const card = screen
+ .getByRole("heading", { name: title })
+ .closest("article");
+ expect(card?.querySelector("svg")).toBeTruthy();
+ }
});
});
diff --git a/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx b/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx
index 4a8702ed72..c8a473fb0a 100644
--- a/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx
+++ b/showcase/shell-docs/src/components/content/landing-pages/intelligence-overview.tsx
@@ -1,7 +1,13 @@
"use client";
import Link from "next/link";
-import { ArrowRight } from "lucide-react";
+import {
+ ArrowRight,
+ BarChart3,
+ MessagesSquare,
+ Server,
+ Sparkles,
+} from "lucide-react";
import { useEffect, useRef } from "react";
import { HeroOnboardingPromptButton } from "@/components/hero-onboarding-prompt-button";
@@ -14,7 +20,6 @@ export const INTELLIGENCE_SIZZLE_VIDEO_URL =
"https://github.com/user-attachments/assets/72b7b4f3-b6e7-460c-a932-5746fe3c8db3";
const CONNECT_HREF = "/intelligence/connect-your-runtime";
-const PRICING_HREF = "https://www.copilotkit.ai/pricing";
const FEATURES = [
{
@@ -22,24 +27,28 @@ const FEATURES = [
body: "Keep messages, generative UI, and tool activity across reloads and devices.",
href: "/threads",
cta: "Open the Rich Threads guide",
+ icon: MessagesSquare,
},
{
title: "Analytics",
body: "See what your agents do and where users get value, from the same interaction data.",
href: "https://www.copilotkit.ai/copilotkit-intelligence#analytics-insights",
cta: "See Analytics on the product page",
+ icon: BarChart3,
},
{
title: "Automatic Learning",
body: "Agents improve from real usage. No fine-tuning pipeline required.",
href: "https://www.copilotkit.ai/copilotkit-intelligence#self-improvement",
cta: "See Automatic Learning on the product page",
+ icon: Sparkles,
},
{
title: "Self-hosting",
body: "Run the same platform in your own cluster, VPC, or data boundary.",
href: "/intelligence/self-hosting",
cta: "Open the self-hosting guide",
+ icon: Server,
},
] as const;
@@ -89,83 +98,77 @@ function SizzleVideo() {
export function IntelligenceOverview() {
return (
-
-
-
-
- Ship durable agent experiences
-
-
- CopilotKit Intelligence adds persistent threads, hosted inspection,
- and production operations next to the runtime you already run.
-
-
-
- }
- quickstart={
-
- }
- />
-
+
+
+
+ Ship production grade agent experiences
+
+
+ CopilotKit Intelligence adds persistent threads, analytics, automatic
+ learning, and production operations on top of the runtime you already
+ run.
+
+
+
-
-
-
-
+
+
+ }
+ quickstart={
+
+ }
+ />
-
-
-
- What you can add next
-
-
- {FEATURES.map((feature) => (
-
-
- {feature.title}
-
-
- {feature.body}
-
-
- {feature.cta}
-
-
-
- ))}
-
-
-
-
- Plans start on the free Developer tier.{" "}
-
- See CopilotKit Intelligence pricing
-
- .
-
- {/* Breadcrumb styling tracks canonical fumadocs PageBreadcrumb,
- * but tighter: this should read as quiet page chrome, not a
- * second title row above the H1. */}
-
+
+
+ {/* Keep breadcrumbs as quiet navigation and omit the current
+ * page, whose title immediately follows. This preserves useful
+ * hierarchy without repeating the H1 as decorative chrome. */}
+ {breadcrumbs.length > 1 && (
+
+ )}
-
- {doc.fm.title}
-
- {doc.fm.description && (
-
- {doc.fm.description}
-
- )}
+
+
+ {doc.fm.title}
+
- {/* Page actions (Copy agent prompt / Copy Markdown / Open in
- ) — fumadocs's upstream LLM page-actions feature. The
- markdown URL resolves through the `/:path*.mdx` rewrite to the
- route handler at `app/llms-mdx/[[...slug]]/route.ts`, which
- serves the raw MDX via the same `loadDoc()` the page uses. The
- GitHub URL is computed from `doc.filePath` (absolute fs path)
- by slicing from the `/showcase/` segment. */}
-
+ {/* Page actions (Copy prompt / Copy page / Open in
+ * ) stay beside the title when space allows and wrap as
+ * a single group on narrower pages. This keeps the article's
+ * utility chrome available without inserting a separate band
+ * between the description and the body. */}
+
+
- {/* Thin divider between the page-actions row and the page body
- (banner / content). Visually separates the page metadata
- chrome (title + page actions) from the page content
- underneath. Uses the project's `--border` token so it tracks
- the rest of the page chrome in light and dark modes. */}
-
+ {doc.fm.description && (
+
+ {doc.fm.description}
+
+ )}
+
{bannerSlot}
From 53c22a126bae62918ea5eeb613b8e97f19926913 Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 12:49:56 -0700
Subject: [PATCH 19/57] fix(docs): keep tables aligned with prose
---
.../shell-docs/src/app/__tests__/globals-css.test.ts | 9 +++++++++
showcase/shell-docs/src/app/globals.css | 11 ++++++-----
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/showcase/shell-docs/src/app/__tests__/globals-css.test.ts b/showcase/shell-docs/src/app/__tests__/globals-css.test.ts
index 19244946fa..36d2cfbc58 100644
--- a/showcase/shell-docs/src/app/__tests__/globals-css.test.ts
+++ b/showcase/shell-docs/src/app/__tests__/globals-css.test.ts
@@ -59,6 +59,15 @@ describe("globals.css docs headings", () => {
});
});
+describe("globals.css docs media breakouts", () => {
+ it("keeps standard tables aligned to the prose measure", () => {
+ expect(globalsCss).toContain("> :is(img, figure.shiki)");
+ expect(globalsCss).not.toMatch(
+ /\.reference-content\s*>\s*:is\([^)]*table/,
+ );
+ });
+});
+
describe("globals.css cookbook sidebar", () => {
it("removes the empty cookbook sidebar banner and aligns the recipe list", () => {
const normalizedGlobalsCss = normalizeWhitespace(globalsCss);
diff --git a/showcase/shell-docs/src/app/globals.css b/showcase/shell-docs/src/app/globals.css
index 9975c24a65..f4269fc94d 100644
--- a/showcase/shell-docs/src/app/globals.css
+++ b/showcase/shell-docs/src/app/globals.css
@@ -1543,13 +1543,14 @@ samp {
margin: 1.5rem 0;
}
-/* Dense media benefits from a wider canvas than prose. Direct diagrams,
- * tables, and standalone code blocks first fill the 760px outer content lane,
- * then gain one further step when both navigation rails leave enough room. */
+/* Diagrams and standalone code blocks benefit from a wider canvas than prose.
+ * Standard tables stay on the reading measure so their section heading and
+ * content share one clear axis; genuinely wide tables should opt into their
+ * own overflow treatment rather than inheriting a viewport breakpoint. */
@media (min-width: 1280px) {
.docs-article-content
.reference-content
- > :is(img, table, figure.shiki) {
+ > :is(img, figure.shiki) {
width: min(var(--docs-breakout-width), calc(100% + 5rem));
max-width: min(var(--docs-breakout-width), calc(100% + 5rem));
margin-right: 0;
@@ -1561,7 +1562,7 @@ samp {
@media (min-width: 1440px) {
.docs-article-content
.reference-content
- > :is(img, table, figure.shiki) {
+ > :is(img, figure.shiki) {
width: min(var(--docs-breakout-width), calc(100% + 8.75rem));
max-width: min(var(--docs-breakout-width), calc(100% + 8.75rem));
}
From 9eeb144add2c271f52725ed550b758752a2ca6cb Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 12:56:07 -0700
Subject: [PATCH 20/57] fix(docs): align code blocks with prose
---
.../src/app/__tests__/globals-css.test.ts | 8 +++++---
showcase/shell-docs/src/app/globals.css | 15 +++++----------
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/showcase/shell-docs/src/app/__tests__/globals-css.test.ts b/showcase/shell-docs/src/app/__tests__/globals-css.test.ts
index 36d2cfbc58..6494835acf 100644
--- a/showcase/shell-docs/src/app/__tests__/globals-css.test.ts
+++ b/showcase/shell-docs/src/app/__tests__/globals-css.test.ts
@@ -60,10 +60,12 @@ describe("globals.css docs headings", () => {
});
describe("globals.css docs media breakouts", () => {
- it("keeps standard tables aligned to the prose measure", () => {
- expect(globalsCss).toContain("> :is(img, figure.shiki)");
+ it("keeps standard tables and code blocks aligned to the prose measure", () => {
+ expect(globalsCss).toContain(
+ ".docs-article-content .reference-content > img",
+ );
expect(globalsCss).not.toMatch(
- /\.reference-content\s*>\s*:is\([^)]*table/,
+ /\.reference-content\s*>\s*:is\([^)]*(?:table|figure\.shiki)/,
);
});
});
diff --git a/showcase/shell-docs/src/app/globals.css b/showcase/shell-docs/src/app/globals.css
index f4269fc94d..8d1ef897ad 100644
--- a/showcase/shell-docs/src/app/globals.css
+++ b/showcase/shell-docs/src/app/globals.css
@@ -1543,14 +1543,11 @@ samp {
margin: 1.5rem 0;
}
-/* Diagrams and standalone code blocks benefit from a wider canvas than prose.
- * Standard tables stay on the reading measure so their section heading and
- * content share one clear axis; genuinely wide tables should opt into their
- * own overflow treatment rather than inheriting a viewport breakpoint. */
+/* Direct diagrams benefit from a wider canvas than prose. Standard tables and
+ * code blocks stay on the reading measure so their section heading and content
+ * share one clear axis; both already own their dense-content overflow. */
@media (min-width: 1280px) {
- .docs-article-content
- .reference-content
- > :is(img, figure.shiki) {
+ .docs-article-content .reference-content > img {
width: min(var(--docs-breakout-width), calc(100% + 5rem));
max-width: min(var(--docs-breakout-width), calc(100% + 5rem));
margin-right: 0;
@@ -1560,9 +1557,7 @@ samp {
}
@media (min-width: 1440px) {
- .docs-article-content
- .reference-content
- > :is(img, figure.shiki) {
+ .docs-article-content .reference-content > img {
width: min(var(--docs-breakout-width), calc(100% + 8.75rem));
max-width: min(var(--docs-breakout-width), calc(100% + 8.75rem));
}
From 8cc7f9f4521c543d16f7046aef41e2efa09ed5cc Mon Sep 17 00:00:00 2001
From: Tyler Slaton
Date: Thu, 3 Sep 2026 13:03:00 -0700
Subject: [PATCH 21/57] feat(docs): condense page actions
---
showcase/shell-docs/src/app/globals.css | 22 ++++++-
.../__tests__/docs-page-tools.test.tsx | 20 +++++--
.../src/components/ai/page-actions.tsx | 57 ++++++++++++++++---
.../src/components/docs-page-tools.tsx | 34 +++++++----
4 files changed, 108 insertions(+), 25 deletions(-)
diff --git a/showcase/shell-docs/src/app/globals.css b/showcase/shell-docs/src/app/globals.css
index 8d1ef897ad..2099c1650f 100644
--- a/showcase/shell-docs/src/app/globals.css
+++ b/showcase/shell-docs/src/app/globals.css
@@ -1055,7 +1055,7 @@ figure.shiki > div:first-child[class*="border-b"] {
.docs-page-tools {
flex: 0 0 auto;
flex-wrap: nowrap;
- gap: 0.375rem;
+ gap: 0;
margin: 0;
}
@@ -1064,6 +1064,26 @@ figure.shiki > div:first-child[class*="border-b"] {
white-space: nowrap;
}
+.docs-page-actions-primary {
+ border-radius: var(--shell-docs-radius-control) 0 0
+ var(--shell-docs-radius-control) !important;
+}
+
+.docs-page-actions-trigger {
+ align-self: stretch;
+ min-width: 2rem;
+ margin-left: -1px;
+ padding-inline: 0.5rem !important;
+ border-radius: 0 var(--shell-docs-radius-control)
+ var(--shell-docs-radius-control) 0 !important;
+}
+
+.docs-page-actions-primary:focus-visible,
+.docs-page-actions-trigger:focus-visible {
+ position: relative;
+ z-index: 1;
+}
+
.docs-page-description {
max-width: var(--docs-reading-width);
margin-top: 0.75rem;
diff --git a/showcase/shell-docs/src/components/__tests__/docs-page-tools.test.tsx b/showcase/shell-docs/src/components/__tests__/docs-page-tools.test.tsx
index fb7c497872..a5e89f92d4 100644
--- a/showcase/shell-docs/src/components/__tests__/docs-page-tools.test.tsx
+++ b/showcase/shell-docs/src/components/__tests__/docs-page-tools.test.tsx
@@ -55,7 +55,7 @@ function renderRow(onboardingFramework?: { slug: string; name: string }): void {
);
}
-it("still renders the onboarding button when no framework is passed", () => {
+it("renders one split CTA with copy prompt as its root action", async () => {
// The surfaces that omit the prop are `a2a` and `agent-spec`: documented
// like frameworks, but absent from the registry, so there is no display
// name to put in the prompt. They are docs pages all the same, and the
@@ -67,9 +67,21 @@ it("still renders the onboarding button when no framework is passed", () => {
expect(
screen.getByRole("button", { name: /copy prompt/i }),
).toBeTruthy();
- // The rest of the row is untouched.
- expect(screen.getByRole("button", { name: /copy page/i })).toBeTruthy();
- expect(screen.getByRole("button", { name: /^open$/i })).toBeTruthy();
+ expect(screen.queryByRole("button", { name: /copy page/i })).toBeNull();
+ expect(screen.queryByRole("button", { name: /^open$/i })).toBeNull();
+
+ fireEvent.click(
+ screen.getByRole("button", { name: /more page actions/i }),
+ );
+
+ expect(
+ await screen.findByRole("button", { name: /copy page/i }),
+ ).toBeTruthy();
+ expect(screen.getByRole("separator")).toBeTruthy();
+ expect(
+ screen.getByRole("link", { name: /open in github/i }),
+ ).toBeTruthy();
+ expect(screen.queryByRole("link", { name: /view as markdown/i })).toBeNull();
});
it("renders the onboarding button when a framework is passed", () => {
diff --git a/showcase/shell-docs/src/components/ai/page-actions.tsx b/showcase/shell-docs/src/components/ai/page-actions.tsx
index e858a7feff..156a01b479 100644
--- a/showcase/shell-docs/src/components/ai/page-actions.tsx
+++ b/showcase/shell-docs/src/components/ai/page-actions.tsx
@@ -84,12 +84,15 @@ async function fetchMarkdown(url: string): Promise {
*/
export function MarkdownCopyButton({
markdownUrl,
+ appearance = "button",
...props
}: ComponentProps<"button"> & {
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl: string;
+ /** Render as a full-width popover action instead of standalone chrome. */
+ appearance?: "button" | "menu-item";
}) {
const [isLoading, setLoading] = useState(false);
const pathname = usePathname();
@@ -148,11 +151,14 @@ export function MarkdownCopyButton({
disabled={isLoading}
onClick={onClick}
className={cn(
- buttonVariants({
- color: "secondary",
- size: "sm",
- className: "gap-2 [&_svg]:size-3.5 [&_svg]:text-[var(--text-muted)]",
- }),
+ appearance === "menu-item"
+ ? "shell-docs-radius-control inline-flex w-full items-center gap-2 p-2 text-left text-sm font-normal text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-elevated)] hover:text-[var(--text)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:text-[var(--text-muted)]"
+ : buttonVariants({
+ color: "secondary",
+ size: "sm",
+ className:
+ "gap-2 [&_svg]:size-3.5 [&_svg]:text-[var(--text-muted)]",
+ }),
props.className,
)}
>
@@ -439,6 +445,8 @@ export function OnboardingPromptCopyButton({
export function ViewOptionsPopover({
markdownUrl,
githubUrl,
+ condensed = false,
+ includeCopyPage = false,
...props
}: ComponentProps & {
/**
@@ -450,6 +458,12 @@ export function ViewOptionsPopover({
* Source file URL on GitHub
*/
githubUrl?: string;
+
+ /** Use an icon-only trigger designed to join a split primary action. */
+ condensed?: boolean;
+
+ /** Put the Markdown copy action at the top of the condensed menu. */
+ includeCopyPage?: boolean;
}) {
const pathname = usePathname();
const posthog = usePostHog();
@@ -477,7 +491,7 @@ export function ViewOptionsPopover({
),
},
- markdownUrl && {
+ !condensed && markdownUrl && {
title: "View as Markdown",
target: "view-as-markdown",
href: markdownUrl,
@@ -553,25 +567,50 @@ export function ViewOptionsPopover({
})}`,
},
].filter((v) => !!v);
- }, [githubUrl, markdownUrl, pathname]);
+ }, [condensed, githubUrl, markdownUrl, pathname]);
return (
- {props.children ?? "Open"}
+ {!condensed && (props.children ?? "Open")}
-
+
+ {includeCopyPage && markdownUrl && (
+ <>
+
+ Copy page
+
+
+ >
+ )}
{items.map((item) => (
" popover.
-// Fumadocs's upstream LLM page-actions feature.
+// DocsPageTools — the compact split action that sits beside a docs page title.
+// "Copy prompt" is the default action; its chevron progressively discloses
+// "Copy page" and the existing "Open in " destinations.
//
// Extracted from `docs-page-view.tsx` so the row sits in a component small
// enough to unit-test. `DocsPageView` itself loads MDX off disk and builds the
@@ -72,23 +72,35 @@ export function DocsPageTools({
}: DocsPageToolsProps): React.JSX.Element {
const markdownUrl = docsMarkdownUrl(slugHrefPrefix, slugPath);
return (
-