From 9a8e2e8368a10611bd6e27dd07c40b6dc5736b4b Mon Sep 17 00:00:00 2001 From: David McKay Date: Thu, 11 Jun 2026 13:00:37 -0500 Subject: [PATCH 001/456] fix(runtime): emit activity snapshot before any delta in open generative UI middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generateSandboxedUi args parser only emitted the activity-creating ACTIVITY_SNAPSHOT when it parsed the initialHeight param. The LLM controls the key order of the streamed tool-call JSON, so whenever initialHeight was not the first key (or was omitted), every css/html/ jsFunctions ACTIVITY_DELTA was emitted before the activity message existed and was silently dropped by the client — the chat showed an empty gray box while the assistant claimed success. Emit the snapshot lazily before the first delta of any kind, and deliver initialHeight as a regular delta when it arrives after the snapshot. Fixes CPK-7634 --- .../open-generative-ui-middleware.e2e.test.ts | 99 ++++++++++++++++++- .../runtime/open-generative-ui-middleware.ts | 19 +++- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/v2/runtime/__tests__/open-generative-ui-middleware.e2e.test.ts b/packages/runtime/src/v2/runtime/__tests__/open-generative-ui-middleware.e2e.test.ts index 1652b8e46e3..31871c6a7fd 100644 --- a/packages/runtime/src/v2/runtime/__tests__/open-generative-ui-middleware.e2e.test.ts +++ b/packages/runtime/src/v2/runtime/__tests__/open-generative-ui-middleware.e2e.test.ts @@ -1,12 +1,11 @@ import { describe, it, expect } from "vitest"; -import { - AbstractAgent, +import type { BaseEvent, - EventType, RunAgentInput, ActivitySnapshotEvent, ActivityDeltaEvent, } from "@ag-ui/client"; +import { AbstractAgent, EventType } from "@ag-ui/client"; import { Observable, firstValueFrom } from "rxjs"; import { toArray } from "rxjs/operators"; import { @@ -330,6 +329,100 @@ describe("OpenGenerativeUIMiddleware e2e", () => { expect(completeDelta).toBeDefined(); }); + it("emits ACTIVITY_SNAPSHOT before any delta when css precedes initialHeight", () => { + const emitted: BaseEvent[] = []; + const parser = new ArgsParser("tc-1", (e) => emitted.push(e)); + + parser.write( + '{"css":"body{margin:0}","html":"
","initialHeight":300}', + ); + + // The snapshot must be the very first activity event — deltas emitted + // before it are silently dropped by the client (no activity message + // exists yet to patch). + expect(emitted[0].type).toBe(EventType.ACTIVITY_SNAPSHOT); + + // initialHeight arrived after the snapshot, so it must be delivered + // as a delta instead. + const heightDelta = emitted.find( + (e) => + e.type === EventType.ACTIVITY_DELTA && + (e as ActivityDeltaEvent).patch.some( + (p) => p.path === "/initialHeight" && p.value === 300, + ), + ); + expect(heightDelta).toBeDefined(); + + const snapshots = emitted.filter( + (e) => e.type === EventType.ACTIVITY_SNAPSHOT, + ); + expect(snapshots).toHaveLength(1); + }); + + it("emits ACTIVITY_SNAPSHOT before any delta when initialHeight is omitted", () => { + const emitted: BaseEvent[] = []; + const parser = new ArgsParser("tc-1", (e) => emitted.push(e)); + + parser.write('{"css":"body{margin:0}","html":"

hi

"}'); + + expect(emitted.length).toBeGreaterThan(0); + expect(emitted[0].type).toBe(EventType.ACTIVITY_SNAPSHOT); + const snapshot = emitted[0] as ActivitySnapshotEvent; + expect(snapshot.content).toEqual({ + initialHeight: undefined, + generating: true, + }); + }); + + it("builds complete content when params stream in real-world failure order (css, html, jsFunctions, initialHeight, jsExpressions, placeholderMessages)", () => { + // Regression for CPK-7634: the LLM controls the key order of the + // streamed tool-call JSON. When initialHeight was not first, every + // delta before it targeted a not-yet-existing activity message and + // was dropped, leaving an empty gray box in the chat. + const emitted: BaseEvent[] = []; + const parser = new ArgsParser("tc-1", (e) => emitted.push(e)); + + parser.write('{"css":"body{margin:0}",'); + parser.write('"html":"
calc
",'); + parser.write('"jsFunctions":"function f(){}",'); + parser.write('"initialHeight":760,'); + parser.write('"jsExpressions":["f()"],'); + parser.write('"placeholderMessages":["Building…"]}'); + + expect(emitted[0].type).toBe(EventType.ACTIVITY_SNAPSHOT); + + // Reconstruct content by applying snapshot + deltas in order + let content: Record = {}; + for (const event of emitted) { + if (event.type === EventType.ACTIVITY_SNAPSHOT) { + content = { ...(event as ActivitySnapshotEvent).content } as Record< + string, + unknown + >; + } else if (event.type === EventType.ACTIVITY_DELTA) { + for (const op of (event as ActivityDeltaEvent).patch) { + if (op.op === "add") { + if (op.path.endsWith("/-")) { + const arrayKey = op.path.slice(1, -2); + (content[arrayKey] as unknown[]).push(op.value); + } else { + content[op.path.slice(1)] = op.value; + } + } + } + } + } + + expect((content.html as string[]).join("")).toBe("
calc
"); + expect(content.htmlComplete).toBe(true); + expect(content.css).toBe("body{margin:0}"); + expect(content.cssComplete).toBe(true); + expect(content.jsFunctions).toBe("function f(){}"); + expect(content.initialHeight).toBe(760); + expect(content.jsExpressions).toEqual(["f()"]); + expect(content.placeholderMessages).toEqual(["Building…"]); + }); + it("emits snapshot only once even with multiple params", () => { const emitted: BaseEvent[] = []; const parser = new ArgsParser("tc-1", (e) => emitted.push(e)); diff --git a/packages/runtime/src/v2/runtime/open-generative-ui-middleware.ts b/packages/runtime/src/v2/runtime/open-generative-ui-middleware.ts index 424282ea110..e6440a15754 100644 --- a/packages/runtime/src/v2/runtime/open-generative-ui-middleware.ts +++ b/packages/runtime/src/v2/runtime/open-generative-ui-middleware.ts @@ -1,14 +1,13 @@ -import { - Middleware, +import type { RunAgentInput, AbstractAgent, BaseEvent, - EventType, ToolCallStartEvent, ToolCallArgsEvent, ActivitySnapshotEvent, ActivityDeltaEvent, } from "@ag-ui/client"; +import { Middleware, EventType } from "@ag-ui/client"; import { Observable } from "rxjs"; import clarinet from "clarinet"; @@ -183,7 +182,13 @@ export class ArgsParser { case "initialHeight": this.params.initialHeight = typeof value === "number" ? value : undefined; - this.emitSnapshot(); + if (this.snapshotEmitted) { + // Snapshot already went out (another param parsed first) — deliver + // the height as a delta instead. + this.emitParamDelta("initialHeight", this.params.initialHeight); + } else { + this.emitSnapshot(); + } break; case "css": this.params.css = value != null ? String(value) : undefined; @@ -212,6 +217,11 @@ export class ArgsParser { } private emitParamDelta(key: string, value: unknown): void { + // The activity message must exist before any delta can be applied — + // the client silently drops ACTIVITY_DELTA events whose messageId has + // no prior ACTIVITY_SNAPSHOT. The LLM controls the key order of the + // streamed args, so the snapshot cannot wait for initialHeight. + this.emitSnapshot(); const event: ActivityDeltaEvent = { type: EventType.ACTIVITY_DELTA, messageId: this.messageId, @@ -222,6 +232,7 @@ export class ArgsParser { } private emitArrayItemDelta(arrayKey: string, value: string): void { + this.emitSnapshot(); const event: ActivityDeltaEvent = { type: EventType.ACTIVITY_DELTA, messageId: this.messageId, From 314e2c99cec37e5f01313a4a33237b38e0137565 Mon Sep 17 00:00:00 2001 From: SeoyeonKim Date: Sat, 13 Jun 2026 10:51:55 +0900 Subject: [PATCH 002/456] fix(react-core): preserve mobile chat input caret --- .../v2/components/chat/CopilotChatInput.tsx | 1 - .../chat/__tests__/CopilotChatInput.test.tsx | 74 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/packages/react-core/src/v2/components/chat/CopilotChatInput.tsx b/packages/react-core/src/v2/components/chat/CopilotChatInput.tsx index 27dc7336fbc..1d7caa2967f 100644 --- a/packages/react-core/src/v2/components/chat/CopilotChatInput.tsx +++ b/packages/react-core/src/v2/components/chat/CopilotChatInput.tsx @@ -770,7 +770,6 @@ export function CopilotChatInput({ ) { const isMobileViewport = window.matchMedia("(max-width: 767px)").matches; if (isMobileViewport) { - ensureMeasurements(); adjustTextareaHeight(); updateLayout("expanded"); return; diff --git a/packages/react-core/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx b/packages/react-core/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx index 5ddd75ddcb0..6bd1ca71277 100644 --- a/packages/react-core/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +++ b/packages/react-core/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx @@ -1076,6 +1076,7 @@ describe("CopilotChatInput", () => { describe("Container dimension cache", () => { const OriginalResizeObserver = globalThis.ResizeObserver; + const OriginalMatchMedia = window.matchMedia; class MockResizeObserver { static instances: MockResizeObserver[] = []; @@ -1144,8 +1145,34 @@ describe("CopilotChatInput", () => { afterEach(() => { vi.restoreAllMocks(); globalThis.ResizeObserver = OriginalResizeObserver; + if (OriginalMatchMedia) { + Object.defineProperty(window, "matchMedia", { + configurable: true, + writable: true, + value: OriginalMatchMedia, + }); + } else { + Reflect.deleteProperty(window, "matchMedia"); + } }); + const mockMobileViewport = () => { + Object.defineProperty(window, "matchMedia", { + configurable: true, + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: query === "(max-width: 767px)", + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); + }; + /** * Extends mockLayoutMetrics with getComputedStyle mocks so that * updateContainerCache can compute real compactWidth and font values, @@ -1536,6 +1563,53 @@ describe("CopilotChatInput", () => { expect(addRectSpy).toHaveBeenCalled(); }); + it("does not re-measure textarea value on mobile after measurements are warm", async () => { + mockMobileViewport(); + + const valueDescriptor = Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + "value", + ); + const valueSetterSpy = vi + .spyOn(HTMLTextAreaElement.prototype, "value", "set") + .mockImplementation(function ( + this: HTMLTextAreaElement, + nextValue: string, + ) { + valueDescriptor?.set?.call(this, nextValue); + }); + + const { container } = renderWithProvider( + , + ); + setupMocksAndInvalidateCache(container, DEFAULT_LAYOUT_OPTIONS); + + const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; + fireEvent.change(textarea, { target: { value: "ABCD" } }); + + await waitFor(() => { + expect(getLayoutGrid(textarea).getAttribute("data-layout")).toBe( + "expanded", + ); + }); + + valueSetterSpy.mockClear(); + textarea.setSelectionRange(1, 1); + + fireEvent.change(textarea, { + target: { value: "AEBCD", selectionStart: 2, selectionEnd: 2 }, + }); + triggerResizeForTargets(textarea); + + await waitFor(() => { + expect(getLayoutGrid(textarea).getAttribute("data-layout")).toBe( + "expanded", + ); + }); + + expect(valueSetterSpy).not.toHaveBeenCalledWith(""); + }); + it("keeps cache warm during layout toggle (ignoreResizeRef path)", async () => { const { textarea, grid } = await renderAndWarmCache(); From 522402886f71bc9306fcb2924011b41952bf31b8 Mon Sep 17 00:00:00 2001 From: SeoyeonKim Date: Sat, 13 Jun 2026 11:00:52 +0900 Subject: [PATCH 003/456] fix(core): preserve proxied runtime credentials --- .../core/src/__tests__/proxied-runtime-transport.test.ts | 4 ++++ packages/core/src/agent.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/core/src/__tests__/proxied-runtime-transport.test.ts b/packages/core/src/__tests__/proxied-runtime-transport.test.ts index 88c7333be96..29804bd8e57 100644 --- a/packages/core/src/__tests__/proxied-runtime-transport.test.ts +++ b/packages/core/src/__tests__/proxied-runtime-transport.test.ts @@ -58,6 +58,7 @@ describe("ProxiedCopilotRuntimeAgent transport integration", () => { runtimeUrl, agentId, headers: { Authorization: "Bearer test-token" }, + credentials: "include", transport, }); @@ -89,6 +90,7 @@ describe("ProxiedCopilotRuntimeAgent transport integration", () => { } expect(init.method).toBe("POST"); + expect(init.credentials).toBe("include"); const headers = new Headers(init.headers as HeadersInit); expect(headers.get("content-type")).toBe("application/json"); expect(headers.get("accept")).toBe("text/event-stream"); @@ -100,6 +102,7 @@ describe("ProxiedCopilotRuntimeAgent transport integration", () => { runtimeUrl, agentId, headers: { Authorization: "Bearer test-token" }, + credentials: "include", transport, }); @@ -126,6 +129,7 @@ describe("ProxiedCopilotRuntimeAgent transport integration", () => { }); } expect(init.method).toBe("POST"); + expect(init.credentials).toBe("include"); const headers = new Headers(init.headers as HeadersInit); expect(headers.get("accept")).toBe("text/event-stream"); }); diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 0544483c513..3231e15979f 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -173,6 +173,14 @@ export class ProxiedCopilotRuntimeAgent extends HttpAgent { return this._capabilities; } + override requestInit(input: RunAgentInput): RequestInit { + const baseInit = super.requestInit(input); + return { + ...baseInit, + ...(this.credentials ? { credentials: this.credentials } : {}), + }; + } + async getCapabilities(): Promise { return this._capabilities ?? {}; } From 1c6d7cb6fcebdd389764172c92b71b5c0a8bec50 Mon Sep 17 00:00:00 2001 From: godququ5-code Date: Fri, 3 Jul 2026 00:37:00 +0300 Subject: [PATCH 004/456] fix react-ui sidebar css specificity --- packages/react-ui/src/css/header.css | 2 +- packages/react-ui/src/css/input.css | 2 +- .../src/css/sidebar-specificity.test.ts | 31 +++++++++++++++++++ packages/react-ui/src/css/window.css | 6 ++-- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 packages/react-ui/src/css/sidebar-specificity.test.ts diff --git a/packages/react-ui/src/css/header.css b/packages/react-ui/src/css/header.css index 5f48ddac15e..f85a62b6700 100644 --- a/packages/react-ui/src/css/header.css +++ b/packages/react-ui/src/css/header.css @@ -14,7 +14,7 @@ z-index: 2; } -.copilotKitSidebar .copilotKitHeader { +:where(.copilotKitSidebar) .copilotKitHeader { border-radius: 0; } diff --git a/packages/react-ui/src/css/input.css b/packages/react-ui/src/css/input.css index 9502fcf144d..4f5664beb19 100644 --- a/packages/react-ui/src/css/input.css +++ b/packages/react-ui/src/css/input.css @@ -19,7 +19,7 @@ border-bottom-right-radius: 0.75rem; } -.copilotKitSidebar .copilotKitInputContainer { +:where(.copilotKitSidebar) .copilotKitInputContainer { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } diff --git a/packages/react-ui/src/css/sidebar-specificity.test.ts b/packages/react-ui/src/css/sidebar-specificity.test.ts new file mode 100644 index 00000000000..7218da51ff3 --- /dev/null +++ b/packages/react-ui/src/css/sidebar-specificity.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +const cssRoot = path.resolve(__dirname); + +const sidebarScopedSelectors = [ + { + file: "window.css", + selector: ".copilotKitWindow", + }, + { + file: "header.css", + selector: ".copilotKitHeader", + }, + { + file: "input.css", + selector: ".copilotKitInputContainer", + }, +]; + +describe("sidebar CSS specificity", () => { + it("keeps sidebar variant selectors easy to override", () => { + for (const { file, selector } of sidebarScopedSelectors) { + const css = fs.readFileSync(path.join(cssRoot, file), "utf-8"); + + expect(css).not.toContain(`.copilotKitSidebar ${selector}`); + expect(css).toContain(`:where(.copilotKitSidebar) ${selector}`); + } + }); +}); diff --git a/packages/react-ui/src/css/window.css b/packages/react-ui/src/css/window.css index e6446fdf5a2..969d9d3c949 100644 --- a/packages/react-ui/src/css/window.css +++ b/packages/react-ui/src/css/window.css @@ -16,7 +16,7 @@ pointer-events: none; } -.copilotKitSidebar .copilotKitWindow { +:where(.copilotKitSidebar) .copilotKitWindow { border-radius: 0; opacity: 1; transform: translateX(100%); @@ -28,7 +28,7 @@ pointer-events: auto; } -.copilotKitSidebar .copilotKitWindow.open { +:where(.copilotKitSidebar) .copilotKitWindow.open { transform: translateX(0); } @@ -54,7 +54,7 @@ max-height: calc(100% - 6rem); } - .copilotKitSidebar .copilotKitWindow { + :where(.copilotKitSidebar) .copilotKitWindow { bottom: 0; right: 0; top: auto; From bd21ce8365fda04f0c28be79ed158529cb1478c4 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:00:01 -0700 Subject: [PATCH 005/456] =?UTF-8?q?chore:=20WIP=20preserve=20(round=202,?= =?UTF-8?q?=20UNVERIFIED=20=E2=80=94=20verify+squash=20pending)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/core-capability-toggle.test.ts | 32 +++++++++ .../run-handler-capability-toggle.test.ts | 69 +++++++++++++++++++ packages/core/src/core/core.ts | 15 ++++ packages/core/src/core/run-handler.ts | 36 +++++++++- 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/core/__tests__/core-capability-toggle.test.ts create mode 100644 packages/core/src/core/__tests__/run-handler-capability-toggle.test.ts diff --git a/packages/core/src/core/__tests__/core-capability-toggle.test.ts b/packages/core/src/core/__tests__/core-capability-toggle.test.ts new file mode 100644 index 00000000000..49394648e60 --- /dev/null +++ b/packages/core/src/core/__tests__/core-capability-toggle.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { CopilotKitCore } from "../core"; + +function createCore(): CopilotKitCore { + return new CopilotKitCore({ + tools: [ + { name: "chart", description: "renders a chart", parameters: z.object({}) }, + { name: "map", description: "renders a map", parameters: z.object({}) }, + ], + }); +} + +describe("CopilotKitCore capability toggle delegation", () => { + it("disables a tool through the public core API", () => { + const core = createCore(); + expect(core.isToolEnabled("map")).toBe(true); + + core.setToolEnabled("map", false); + + expect(core.isToolEnabled("map")).toBe(false); + // core.tools still lists the disabled tool (registry is unchanged)... + expect(core.tools.map((t) => t.name)).toContain("map"); + }); + + it("re-enables a tool through the public core API", () => { + const core = createCore(); + core.setToolEnabled("map", false); + core.setToolEnabled("map", true); + expect(core.isToolEnabled("map")).toBe(true); + }); +}); diff --git a/packages/core/src/core/__tests__/run-handler-capability-toggle.test.ts b/packages/core/src/core/__tests__/run-handler-capability-toggle.test.ts new file mode 100644 index 00000000000..ed87978ec92 --- /dev/null +++ b/packages/core/src/core/__tests__/run-handler-capability-toggle.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { RunHandler } from "../run-handler"; +import type { CopilotKitCore } from "../core"; + +function createRunHandler(): RunHandler { + return new RunHandler({} as CopilotKitCore); +} + +describe("RunHandler capability toggle", () => { + it("omits a tool from buildFrontendTools once disabled via setToolEnabled", () => { + const runHandler = createRunHandler(); + runHandler.initialize([ + { name: "chart", description: "renders a chart", parameters: z.object({}) }, + { name: "map", description: "renders a map", parameters: z.object({}) }, + ]); + + expect(runHandler.buildFrontendTools().map((t) => t.name)).toEqual(["chart", "map"]); + + runHandler.setToolEnabled("map", false); + + expect(runHandler.buildFrontendTools().map((t) => t.name)).toEqual(["chart"]); + expect(runHandler.isToolEnabled("map")).toBe(false); + expect(runHandler.isToolEnabled("chart")).toBe(true); + }); + + it("re-enables a tool via setToolEnabled(true)", () => { + const runHandler = createRunHandler(); + runHandler.initialize([{ name: "chart", description: "c" }]); + runHandler.setToolEnabled("chart", false); + expect(runHandler.buildFrontendTools()).toHaveLength(0); + + runHandler.setToolEnabled("chart", true); + expect(runHandler.buildFrontendTools().map((t) => t.name)).toEqual(["chart"]); + }); + + it("override survives re-registration (setTools) — keyed by name+agentId, not object identity", () => { + const runHandler = createRunHandler(); + runHandler.initialize([{ name: "chart", description: "c" }]); + runHandler.setToolEnabled("chart", false); + + // Simulate a hook re-registering the tool with a fresh object (available resets to default). + runHandler.setTools([{ name: "chart", description: "c (re-registered)" }]); + + expect(runHandler.isToolEnabled("chart")).toBe(false); + expect(runHandler.buildFrontendTools()).toHaveLength(0); + }); + + it("distinguishes a global tool from an agent-scoped tool of the same name", () => { + const runHandler = createRunHandler(); + runHandler.initialize([ + { name: "dup", description: "global" }, + { name: "dup", description: "scoped", agentId: "agentA" }, + ]); + + runHandler.setToolEnabled("dup", false, "agentA"); + + const names = runHandler.buildFrontendTools("agentA").map((t) => t.name); + // The global "dup" (no agentId) is still enabled; the agentA-scoped one is off. + expect(names).toEqual(["dup"]); + expect(runHandler.isToolEnabled("dup")).toBe(true); + expect(runHandler.isToolEnabled("dup", "agentA")).toBe(false); + }); + + it("defaults to enabled for an unknown tool name", () => { + const runHandler = createRunHandler(); + expect(runHandler.isToolEnabled("never-registered")).toBe(true); + }); +}); diff --git a/packages/core/src/core/core.ts b/packages/core/src/core/core.ts index 9adc441cb56..1acf5dbb6b5 100644 --- a/packages/core/src/core/core.ts +++ b/packages/core/src/core/core.ts @@ -996,6 +996,21 @@ export class CopilotKitCore { this.runHandler.setTools(tools); } + /** + * Enable/disable a registered frontend tool at runtime without unregistering + * it (Inspector "Capabilities" tool). A disabled tool is omitted from the + * tool list sent to the agent on the next run. The override is keyed by name + * (+ optional agentId) and survives the tool being re-registered. + */ + setToolEnabled(name: string, enabled: boolean, agentId?: string): void { + this.runHandler.setToolEnabled(name, enabled, agentId); + } + + /** Whether a registered tool is currently enabled (defaults true). */ + isToolEnabled(name: string, agentId?: string): boolean { + return this.runHandler.isToolEnabled(name, agentId); + } + /** * Subscription lifecycle */ diff --git a/packages/core/src/core/run-handler.ts b/packages/core/src/core/run-handler.ts index bfe7a156b21..d18285e91ff 100644 --- a/packages/core/src/core/run-handler.ts +++ b/packages/core/src/core/run-handler.ts @@ -86,6 +86,14 @@ export class RunHandler { // eslint-disable-next-line @typescript-eslint/no-explicit-any private _tools: FrontendTool[] = []; + /** + * Keys of frontend tools explicitly disabled at runtime via the Inspector's + * Capabilities tool (`setToolEnabled`). Kept independently of each tool's own + * `available` flag so a hook re-registering the tool (which resets + * `available`) does not clobber the override. Key = `capabilityKey(name, agentId)`. + */ + private _disabledToolKeys = new Set(); + /** * Tracks whether the current run (including in-flight tool execution) * has been aborted via `stopAgent()` or `agent.abortRun()`. Created @@ -954,6 +962,31 @@ export class RunHandler { }; } + /** Stable identity for a tool override: agent-scope + name (NUL-separated). */ + private capabilityKey(name: string, agentId?: string): string { + return `${agentId ?? ""}${name}`; + } + + /** + * Enable/disable a registered frontend tool at runtime without unregistering + * it. A disabled tool is omitted from {@link buildFrontendTools}, so the agent + * never receives it. Unlike the per-tool `available` flag, this override + * survives the tool being re-registered. + */ + setToolEnabled(name: string, enabled: boolean, agentId?: string): void { + const key = this.capabilityKey(name, agentId); + if (enabled) { + this._disabledToolKeys.delete(key); + } else { + this._disabledToolKeys.add(key); + } + } + + /** Whether a tool is currently enabled (not overridden off). Defaults true. */ + isToolEnabled(name: string, agentId?: string): boolean { + return !this._disabledToolKeys.has(this.capabilityKey(name, agentId)); + } + /** * Build frontend tools for an agent */ @@ -963,7 +996,8 @@ export class RunHandler { (tool) => tool.available !== false && (tool.available as boolean | string | undefined) !== "disabled" && - (!tool.agentId || tool.agentId === agentId), + (!tool.agentId || tool.agentId === agentId) && + this.isToolEnabled(tool.name, tool.agentId), ) .map((tool) => ({ name: tool.name, From 5db6ded719e715542ce92c034bc9f758517224c5 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:00:02 -0700 Subject: [PATCH 006/456] =?UTF-8?q?chore:=20WIP=20preserve=20(round=202,?= =?UTF-8?q?=20UNVERIFIED=20=E2=80=94=20verify+squash=20pending)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/filter-catalog.test.ts | 53 +++++++++++++++++++ .../src/react-renderer/filter-catalog.ts | 28 ++++++++++ .../a2ui-renderer/src/react-renderer/index.ts | 1 + 3 files changed, 82 insertions(+) create mode 100644 packages/a2ui-renderer/src/react-renderer/__tests__/filter-catalog.test.ts create mode 100644 packages/a2ui-renderer/src/react-renderer/filter-catalog.ts diff --git a/packages/a2ui-renderer/src/react-renderer/__tests__/filter-catalog.test.ts b/packages/a2ui-renderer/src/react-renderer/__tests__/filter-catalog.test.ts new file mode 100644 index 00000000000..dd47120a67f --- /dev/null +++ b/packages/a2ui-renderer/src/react-renderer/__tests__/filter-catalog.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { z } from "zod"; +import { Catalog } from "@a2ui/web_core/v0_9"; +import type { ComponentApi } from "@a2ui/web_core/v0_9"; +import { filterCatalog } from "../filter-catalog"; + +function makeCatalog(): Catalog { + const components: ComponentApi[] = [ + { name: "PieChart", schema: z.object({ innerRadius: z.number().optional() }) }, + { name: "FlightCard", schema: z.object({ airline: z.string() }) }, + { name: "Badge", schema: z.object({ text: z.string() }) }, + ]; + return new Catalog("copilotkit://custom-catalog", components, []); +} + +describe("filterCatalog", () => { + it("keeps only components whose name passes the predicate", () => { + const catalog = makeCatalog(); + const filtered = filterCatalog(catalog, (name) => name !== "FlightCard"); + expect(filtered.components.has("PieChart")).toBe(true); + expect(filtered.components.has("Badge")).toBe(true); + expect(filtered.components.has("FlightCard")).toBe(false); + }); + + it("preserves the catalog id and functions", () => { + const catalog = makeCatalog(); + const filtered = filterCatalog(catalog, () => true); + expect(filtered.id).toBe("copilotkit://custom-catalog"); + expect(filtered.components.size).toBe(3); + }); + + it("does not mutate the source catalog", () => { + const catalog = makeCatalog(); + filterCatalog(catalog, () => false); + expect(catalog.components.size).toBe(3); + }); + + it("returns an empty-component catalog when predicate rejects all", () => { + const catalog = makeCatalog(); + const filtered = filterCatalog(catalog, () => false); + expect(filtered.components.size).toBe(0); + expect(filtered.id).toBe("copilotkit://custom-catalog"); + }); +}); + +describe("filterCatalog package export", () => { + it("is exported from the package entry", async () => { + const mod = await import("@copilotkit/a2ui-renderer"); + expect(typeof (mod as { filterCatalog?: unknown }).filterCatalog).toBe( + "function", + ); + }); +}); diff --git a/packages/a2ui-renderer/src/react-renderer/filter-catalog.ts b/packages/a2ui-renderer/src/react-renderer/filter-catalog.ts new file mode 100644 index 00000000000..326bbf50501 --- /dev/null +++ b/packages/a2ui-renderer/src/react-renderer/filter-catalog.ts @@ -0,0 +1,28 @@ +import { Catalog } from "@a2ui/web_core/v0_9"; +import type { ComponentApi } from "@a2ui/web_core/v0_9"; + +/** + * Rebuild a Catalog keeping only components whose `name` passes `predicate`. + * + * Pure: does not mutate the source catalog. The returned catalog preserves the + * original `id` and all `functions`; only the component set is narrowed. Used by + * react-core to enforce per-component enable/disable on BOTH the advertisement + * path (context) and the render path. + * + * @typeParam T - The component implementation type carried by the catalog. + * @param catalog - The source catalog. + * @param predicate - Returns true to KEEP a component with the given name. + */ +export function filterCatalog( + catalog: Catalog, + predicate: (name: string) => boolean, +): Catalog { + const keptComponents: T[] = []; + for (const [name, component] of catalog.components) { + if (predicate(name)) { + keptComponents.push(component); + } + } + const functions = Array.from(catalog.functions.values()); + return new Catalog(catalog.id, keptComponents, functions); +} diff --git a/packages/a2ui-renderer/src/react-renderer/index.ts b/packages/a2ui-renderer/src/react-renderer/index.ts index f68b2ee0013..4f9d389eead 100644 --- a/packages/a2ui-renderer/src/react-renderer/index.ts +++ b/packages/a2ui-renderer/src/react-renderer/index.ts @@ -38,6 +38,7 @@ export type { InlineCatalogSchema } from "./catalog-utils"; // Catalog creation — new API (definitions + renderers) export { createCatalog, extractSchema } from "./create-catalog"; +export { filterCatalog } from "./filter-catalog"; export type { CatalogComponentDefinition, CatalogDefinitions, From f01bba737893dc9848d30f44930e21e6ade7433c Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:00:02 -0700 Subject: [PATCH 007/456] =?UTF-8?q?chore:=20WIP=20preserve=20(round=202,?= =?UTF-8?q?=20UNVERIFIED=20=E2=80=94=20verify+squash=20pending)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/__tests__/fetch-handler.test.ts | 119 +++++++++++- .../v2/runtime/__tests__/fetch-router.test.ts | 5 + .../runtime/__tests__/handle-memories.test.ts | 170 ++++++++++++++++++ .../src/v2/runtime/core/fetch-handler.ts | 36 ++++ .../src/v2/runtime/core/fetch-router.ts | 10 ++ packages/runtime/src/v2/runtime/core/hooks.ts | 1 + .../runtime/src/v2/runtime/core/runtime.ts | 31 ++++ .../v2/runtime/handlers/handle-memories.ts | 1 + .../runtime/handlers/intelligence/memories.ts | 77 ++++++++ .../__tests__/client.test.ts | 47 +++++ .../runtime/intelligence-platform/client.ts | 31 ++++ 11 files changed, 523 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/v2/runtime/__tests__/fetch-handler.test.ts b/packages/runtime/src/v2/runtime/__tests__/fetch-handler.test.ts index fa0abd63525..e49ef8e3732 100644 --- a/packages/runtime/src/v2/runtime/__tests__/fetch-handler.test.ts +++ b/packages/runtime/src/v2/runtime/__tests__/fetch-handler.test.ts @@ -207,8 +207,20 @@ describe("createCopilotRuntimeHandler — multi-route with basePath", () => { expect(response.status).not.toBe(405); }); + // Memory routes are opt-in (secure default off). These reachability tests use + // a runtime with `exposeMemoryRoutes: true`; the gated-off (404) behavior is + // covered in its own describe block below. + const memoryHandler = createCopilotRuntimeHandler({ + runtime: new CopilotRuntime({ + agents: { default: createMockAgent() }, + exposeMemoryRoutes: true, + }), + basePath: "/api/copilotkit", + mode: "multi-route", + }); + it("routes GET /memories (not 404/405)", async () => { - const response = await handler( + const response = await memoryHandler( get("http://localhost/api/copilotkit/memories"), ); // No intelligence configured here → 422, but the route + GET method match. @@ -217,7 +229,7 @@ describe("createCopilotRuntimeHandler — multi-route with basePath", () => { }); it("routes POST /memories (create) — not 404/405", async () => { - const response = await handler( + const response = await memoryHandler( post("http://localhost/api/copilotkit/memories", { content: "c", kind: "topical", @@ -228,8 +240,26 @@ describe("createCopilotRuntimeHandler — multi-route with basePath", () => { expect(response.status).not.toBe(405); }); + it("routes POST /memories/recall (not 404/405)", async () => { + const response = await memoryHandler( + post("http://localhost/api/copilotkit/memories/recall", { + query: "music", + }), + ); + // No intelligence configured → 422, but the route + POST method match. + expect(response.status).not.toBe(404); + expect(response.status).not.toBe(405); + }); + + it("returns 405 for GET /memories/recall (POST-only)", async () => { + const response = await memoryHandler( + get("http://localhost/api/copilotkit/memories/recall"), + ); + expect(response.status).toBe(405); + }); + it("routes PATCH /memories/:id (supersede) — not 404/405", async () => { - const response = await handler( + const response = await memoryHandler( new Request("http://localhost/api/copilotkit/memories/m-1", { method: "PATCH", headers: { "Content-Type": "application/json" }, @@ -241,7 +271,7 @@ describe("createCopilotRuntimeHandler — multi-route with basePath", () => { }); it("routes DELETE /memories/:id (retire) — not 404/405", async () => { - const response = await handler( + const response = await memoryHandler( new Request("http://localhost/api/copilotkit/memories/m-1", { method: "DELETE", }), @@ -251,7 +281,7 @@ describe("createCopilotRuntimeHandler — multi-route with basePath", () => { }); it("returns 405 for GET /memories/:id (PATCH/DELETE-only)", async () => { - const response = await handler( + const response = await memoryHandler( get("http://localhost/api/copilotkit/memories/m-1"), ); expect(response.status).toBe(405); @@ -289,6 +319,85 @@ describe("createCopilotRuntimeHandler — multi-route with basePath", () => { }); }); +/* ------------------------------------------------------------------------------------------------ + * Opt-in memory-proxy flag (exposeMemoryRoutes) + * --------------------------------------------------------------------------------------------- */ + +describe("createCopilotRuntimeHandler — exposeMemoryRoutes gate", () => { + const offHandler = createCopilotRuntimeHandler({ + // Default: exposeMemoryRoutes omitted → off. + runtime: new CopilotRuntime({ agents: { default: createMockAgent() } }), + basePath: "/api/copilotkit", + mode: "multi-route", + }); + const onHandler = createCopilotRuntimeHandler({ + runtime: new CopilotRuntime({ + agents: { default: createMockAgent() }, + exposeMemoryRoutes: true, + }), + basePath: "/api/copilotkit", + mode: "multi-route", + }); + + it("404s GET /memories when the flag is off (default)", async () => { + const response = await offHandler( + get("http://localhost/api/copilotkit/memories"), + ); + expect(response.status).toBe(404); + }); + + it("404s POST /memories/recall when the flag is off (default)", async () => { + const response = await offHandler( + post("http://localhost/api/copilotkit/memories/recall", { + query: "music", + }), + ); + expect(response.status).toBe(404); + }); + + it("404s POST /memories/subscribe when the flag is off (default)", async () => { + const response = await offHandler( + post("http://localhost/api/copilotkit/memories/subscribe"), + ); + expect(response.status).toBe(404); + }); + + it("404s PATCH /memories/:id when the flag is off (default)", async () => { + const response = await offHandler( + new Request("http://localhost/api/copilotkit/memories/m-1", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: "c", kind: "topical" }), + }), + ); + expect(response.status).toBe(404); + }); + + it("404s (not 405) for a wrong method on a hidden memory route — no route-existence leak", async () => { + // GET on the POST-only recall route would 405 if reachable; the gate must + // 404 before method validation so the route's existence is not disclosed. + const response = await offHandler( + get("http://localhost/api/copilotkit/memories/recall"), + ); + expect(response.status).toBe(404); + }); + + it("does not 404 memory routes when the flag is on", async () => { + // No intelligence configured → 422 (not 404): the route is now exposed. + const response = await onHandler( + get("http://localhost/api/copilotkit/memories"), + ); + expect(response.status).not.toBe(404); + }); + + it("leaves non-memory routes reachable when the flag is off", async () => { + const response = await offHandler( + get("http://localhost/api/copilotkit/info"), + ); + expect(response.status).toBe(200); + }); +}); + /* ------------------------------------------------------------------------------------------------ * Multi-route without basePath (suffix matching) * --------------------------------------------------------------------------------------------- */ diff --git a/packages/runtime/src/v2/runtime/__tests__/fetch-router.test.ts b/packages/runtime/src/v2/runtime/__tests__/fetch-router.test.ts index 9f9064a9fcd..3e5c27710eb 100644 --- a/packages/runtime/src/v2/runtime/__tests__/fetch-router.test.ts +++ b/packages/runtime/src/v2/runtime/__tests__/fetch-router.test.ts @@ -81,6 +81,11 @@ describe("fetch-router", () => { expect(result).toEqual({ method: "memories/subscribe" }); }); + it("matches POST /memories/recall to memories/recall (not memories/mutate)", () => { + const result = matchRoute("/api/copilotkit/memories/recall", basePath); + expect(result).toEqual({ method: "memories/recall" }); + }); + it("matches POST /annotate", () => { const result = matchRoute("/api/copilotkit/annotate", basePath); expect(result).toEqual({ method: "annotate" }); diff --git a/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts b/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts index c5ceef6f597..5b8e6777d9b 100644 --- a/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts +++ b/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { handleListMemories, + handleRecallMemories, handleSubscribeToMemories, handleCreateMemory, handleUpdateMemory, @@ -498,4 +499,173 @@ describe("memory handlers", () => { expect(response.status).toBe(422); }); + + it("recalls memories via identifyUser and returns the scored envelope", async () => { + const intelligence = { + recallMemories: vi.fn().mockResolvedValue({ + memories: [ + { + id: "m-1", + kind: "topical", + scope: "user", + content: "User likes jazz.", + sourceThreadIds: [], + invalidatedAt: null, + score: 0.91, + }, + ], + }), + }; + const identifyUser = createIdentifyUser(); + const runtime = createIntelligenceRuntime({ intelligence, identifyUser }); + const request = jsonRequest("/memories/recall", "POST", { + query: "music taste", + limit: 3, + scope: "user", + }); + + const response = await handleRecallMemories({ runtime, request }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + memories: [ + { + id: "m-1", + kind: "topical", + scope: "user", + content: "User likes jazz.", + sourceThreadIds: [], + invalidatedAt: null, + score: 0.91, + }, + ], + }); + expect(identifyUser).toHaveBeenCalledWith(request); + expect(intelligence.recallMemories).toHaveBeenCalledWith({ + userId: "user-1", + query: "music taste", + limit: 3, + scope: "user", + }); + }); + + it("omits limit/scope when the recall body has none", async () => { + const intelligence = { + recallMemories: vi.fn().mockResolvedValue({ memories: [] }), + }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { query: "hi" }), + }); + + expect(response.status).toBe(200); + expect(intelligence.recallMemories).toHaveBeenCalledWith({ + userId: "user-1", + query: "hi", + }); + }); + + it("returns 400 when recall query is missing", async () => { + const intelligence = { recallMemories: vi.fn() }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { limit: 3 }), + }); + + expect(response.status).toBe(400); + expect(intelligence.recallMemories).not.toHaveBeenCalled(); + }); + + it("returns 400 for an out-of-vocabulary recall scope", async () => { + const intelligence = { recallMemories: vi.fn() }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { + query: "hi", + scope: "global", + }), + }); + + expect(response.status).toBe(400); + expect(intelligence.recallMemories).not.toHaveBeenCalled(); + }); + + it("returns 400 for a non-number recall limit", async () => { + const intelligence = { recallMemories: vi.fn() }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { + query: "hi", + limit: "3", + }), + }); + + expect(response.status).toBe(400); + expect(intelligence.recallMemories).not.toHaveBeenCalled(); + }); + + it("returns 422 for recall when intelligence is not configured", async () => { + const runtime = new CopilotRuntime({ agents: {} }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { query: "hi" }), + }); + + expect(response.status).toBe(422); + }); + + it("forwards a PlatformRequestError 4xx on recall verbatim", async () => { + const intelligence = { + recallMemories: vi + .fn() + .mockRejectedValue(new PlatformRequestError("bad", 422)), + }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { query: "hi" }), + }); + + expect(response.status).toBe(422); + }); + + it("maps a platform 5xx to 502 on recall", async () => { + const intelligence = { + recallMemories: vi + .fn() + .mockRejectedValue(new PlatformRequestError("boom", 503)), + }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { query: "hi" }), + }); + + expect(response.status).toBe(502); + }); + + it("returns 502 when the recall response has no memories array", async () => { + const intelligence = { + recallMemories: vi.fn().mockResolvedValue({ items: [] }), + }; + const runtime = createIntelligenceRuntime({ intelligence }); + + const response = await handleRecallMemories({ + runtime, + request: jsonRequest("/memories/recall", "POST", { query: "hi" }), + }); + + expect(response.status).toBe(502); + }); }); diff --git a/packages/runtime/src/v2/runtime/core/fetch-handler.ts b/packages/runtime/src/v2/runtime/core/fetch-handler.ts index 2ee9f4e1fed..3da5d42c6b2 100644 --- a/packages/runtime/src/v2/runtime/core/fetch-handler.ts +++ b/packages/runtime/src/v2/runtime/core/fetch-handler.ts @@ -61,6 +61,7 @@ import { } from "../handlers/handle-threads"; import { handleListMemories, + handleRecallMemories, handleSubscribeToMemories, handleCreateMemory, handleUpdateMemory, @@ -205,6 +206,19 @@ export function createCopilotRuntimeHandler( throw jsonResponse({ error: "Not found" }, 404); } + // Opt-in gate for the client-facing memory proxy routes (secure + // default: off). Runs BEFORE method validation so a hidden route 404s + // uniformly regardless of HTTP method — a 405 here would otherwise leak + // that the route exists. `dispatchRoute` re-applies the same gate as + // defense-in-depth (and to cover the single-route path). + if ( + matched.method.startsWith("memories/") && + runtime.exposeMemoryRoutes !== true + ) { + route = matched; + throw jsonResponse({ error: "Not found" }, 404); + } + // Validate HTTP method const methodError = validateHttpMethod(request.method, matched); if (methodError) { @@ -312,6 +326,19 @@ function dispatchRoute( route: RouteInfo, options: { threadEndpointsEnabled: boolean }, ): Promise { + // Opt-in gate for the client-facing memory proxy routes (secure default: + // off). When not explicitly enabled, every `/memories/*` route 404s as if it + // did not exist — this MUST run before the per-handler `isIntelligenceRuntime` + // check so an un-opted-in deployment reveals nothing about memory (not even + // whether Intelligence is configured). Coalesce a missing flag (external + // `CopilotRuntimeLike` implementor) to `false`. + if ( + route.method.startsWith("memories/") && + runtime.exposeMemoryRoutes !== true + ) { + throw jsonResponse({ error: "Not found" }, 404); + } + switch (route.method) { case "agent/run": return handleRunAgent({ @@ -354,6 +381,8 @@ function dispatchRoute( return request.method.toUpperCase() === "POST" ? handleCreateMemory({ runtime, request }) : handleListMemories({ runtime, request }); + case "memories/recall": + return handleRecallMemories({ runtime, request }); case "memories/subscribe": return handleSubscribeToMemories({ runtime, request }); case "memories/mutate": @@ -519,6 +548,13 @@ function validateHttpMethod( Allow: "PATCH, DELETE", }); + case "memories/recall": + // POST-only: semantic recall carries its query in the body. + if (method === "POST") return null; + return jsonResponse({ error: "Method not allowed" }, 405, { + Allow: "POST", + }); + case "threads/update": if (method === "PATCH" || method === "DELETE") return null; return jsonResponse({ error: "Method not allowed" }, 405, { diff --git a/packages/runtime/src/v2/runtime/core/fetch-router.ts b/packages/runtime/src/v2/runtime/core/fetch-router.ts index f9f69de7645..0c01ca2f344 100644 --- a/packages/runtime/src/v2/runtime/core/fetch-router.ts +++ b/packages/runtime/src/v2/runtime/core/fetch-router.ts @@ -210,6 +210,16 @@ function matchSegments(path: string): RouteInfo | null { return { method: "threads/list" }; } + // /memories/recall (2 segments) — semantic recall (POST). Must precede the + // /memories/:id mutate rule below, which would otherwise capture "recall". + if ( + len >= 2 && + segments[len - 2] === "memories" && + segments[len - 1] === "recall" + ) { + return { method: "memories/recall" }; + } + // /memories/subscribe (2 segments) — mint memory-realtime join credentials. if ( len >= 2 && diff --git a/packages/runtime/src/v2/runtime/core/hooks.ts b/packages/runtime/src/v2/runtime/core/hooks.ts index 5ecb0e9d906..40def821441 100644 --- a/packages/runtime/src/v2/runtime/core/hooks.ts +++ b/packages/runtime/src/v2/runtime/core/hooks.ts @@ -49,6 +49,7 @@ export type RouteInfo = | { method: "threads/state"; threadId: string } | { method: "threads/clear" } | { method: "memories/list" } + | { method: "memories/recall" } | { method: "memories/subscribe" } | { method: "memories/mutate"; memoryId: string } | { method: "annotate" } diff --git a/packages/runtime/src/v2/runtime/core/runtime.ts b/packages/runtime/src/v2/runtime/core/runtime.ts index 658e9bba023..0f0b15d3042 100644 --- a/packages/runtime/src/v2/runtime/core/runtime.ts +++ b/packages/runtime/src/v2/runtime/core/runtime.ts @@ -165,6 +165,21 @@ interface BaseCopilotRuntimeOptions extends CopilotRuntimeMiddlewares { * `{ useDefaultDenylist: false }` to restore the previous wide-open behavior. */ forwardHeaders?: ForwardHeadersConfig; + /** + * Opt-in flag exposing the client-facing memory proxy routes + * (`/memories`, `/memories/recall`, `/memories/subscribe`, `/memories/:id`). + * + * Defaults to `false` — a **secure default**. When off, every `/memories/*` + * request 404s as if the route did not exist, so an un-opted-in deployment + * reveals nothing about memory even when Intelligence is configured. This does + * NOT affect the agent's own server-side memory tooling (`recall_memory` runs + * via the Intelligence MCP path, separate from this client REST proxy). + * + * Flip to `true` to power a client memory inspector (e.g. the dev console's + * Memory tab). Existing Intelligence deployments relying on the previously + * always-on Learning tab must set this to restore it. + */ + exposeMemoryRoutes?: boolean; } export interface CopilotRuntimeUser { @@ -241,6 +256,14 @@ export interface CopilotRuntimeLike { * (`resolveForwardHeadersPolicy(undefined)` — default-on denylist). */ forwardHeadersPolicy?: ResolvedForwardHeadersPolicy; + /** + * Resolved opt-in flag for the client-facing memory proxy routes. Optional on + * the published interface so an external `CopilotRuntimeLike` implementor + * predating this field stays source-compatible; the dispatcher coalesces a + * missing value to `false` (secure default — routes hidden). Concrete runtimes + * (`BaseCopilotRuntime`) always resolve and set it. + */ + exposeMemoryRoutes?: boolean; } export interface CopilotSseRuntimeLike extends CopilotRuntimeLike { @@ -273,6 +296,7 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike { public debug: ResolvedDebugConfig; public debugLogger?: CopilotRuntimeLogger; public readonly forwardHeadersPolicy: ResolvedForwardHeadersPolicy; + public readonly exposeMemoryRoutes: boolean; /** * License token resolved once with the env fallback, so telemetry @@ -332,6 +356,9 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike { this.forwardHeadersPolicy = resolveForwardHeadersPolicy( options.forwardHeaders, ); + // Secure default: the client-facing memory proxy routes stay hidden (404) + // unless a deployment explicitly opts in. + this.exposeMemoryRoutes = options.exposeMemoryRoutes ?? false; this.debug = resolveDebugConfig(options.debug); if (this.debug.enabled) { this.debugLogger = createLogger({ @@ -562,4 +589,8 @@ export class CopilotRuntime implements CopilotRuntimeLike { get forwardHeadersPolicy(): ResolvedForwardHeadersPolicy { return this.delegate.forwardHeadersPolicy; } + + get exposeMemoryRoutes(): boolean | undefined { + return this.delegate.exposeMemoryRoutes; + } } diff --git a/packages/runtime/src/v2/runtime/handlers/handle-memories.ts b/packages/runtime/src/v2/runtime/handlers/handle-memories.ts index 47df524b9ec..1a1e2dbddc1 100644 --- a/packages/runtime/src/v2/runtime/handlers/handle-memories.ts +++ b/packages/runtime/src/v2/runtime/handlers/handle-memories.ts @@ -1,5 +1,6 @@ export { handleListMemories, + handleRecallMemories, handleSubscribeToMemories, handleCreateMemory, handleUpdateMemory, diff --git a/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts b/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts index 45fa06ff4da..4d3b0cf8000 100644 --- a/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts +++ b/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts @@ -116,6 +116,33 @@ function parseMemoryBody(body: Record): }; } +/** + * Validates the recall body: `query` required non-empty string; `limit` optional + * number; `scope` optional and in the known scopes. Returns a 400 Response on invalid input. + */ +function parseRecallBody(body: Record): + | { query: string; limit?: number; scope?: string } + | Response { + const { query, limit, scope } = body; + if (typeof query !== "string" || query.length === 0) { + return errorResponse("Recall requires a non-empty string `query`", 400); + } + if (limit !== undefined && typeof limit !== "number") { + return errorResponse("Recall `limit` must be a number when provided", 400); + } + if (scope !== undefined && typeof scope !== "string") { + return errorResponse("Recall `scope` must be a string when provided", 400); + } + if (typeof scope === "string" && !MEMORY_SCOPES.has(scope)) { + return errorResponse("Recall `scope` must be one of: user, project", 400); + } + return { + query, + ...(typeof limit === "number" ? { limit } : {}), + ...(typeof scope === "string" ? { scope } : {}), + }; +} + /** * Lists the resolved user's long-term memories via the Intelligence platform. * @@ -175,6 +202,56 @@ export async function handleListMemories({ return errorResponse(MISSING_INTELLIGENCE_MESSAGE, 422); } +/** + * Semantically recalls the resolved user's memories via the platform (`POST + * /api/memories/recall`, hybrid RAG). Mirrors {@link handleListMemories}: + * requires a `CopilotKitIntelligence` runtime, resolves the user with + * `identifyUser` (never a client-supplied id), proxies with the project API + * key + resolved user. Body `{ query, limit?, scope? }`; response `{ memories }`, + * each optionally carrying `score`. + */ +export async function handleRecallMemories({ + runtime, + request, +}: MemoriesHandlerParams): Promise { + if (!isIntelligenceRuntime(runtime)) { + return errorResponse(MISSING_INTELLIGENCE_MESSAGE, 422); + } + try { + const body = await parseJsonBody(request); + if (isHandlerResponse(body)) return body; + const fields = parseRecallBody(body); + if (isHandlerResponse(fields)) return fields; + + const user = await resolveIntelligenceUser({ runtime, request }); + if (isHandlerResponse(user)) return user; + + const data = await runtime.intelligence.recallMemories({ + userId: user.id, + ...fields, + }); + + if ( + data == null || + typeof data !== "object" || + !Array.isArray((data as { memories?: unknown }).memories) + ) { + logger.error( + { data }, + "recallMemories: platform returned a response without a `memories` array", + ); + return errorResponse( + "Memory platform returned an invalid recall response", + 502, + ); + } + return Response.json(data); + } catch (error) { + logger.error({ err: error }, "Error recalling memories"); + return memoryErrorResponse(error, "Failed to recall memories"); + } +} + /** * Mints memory-realtime join credentials (platform `POST * /api/memories/subscribe`). Mirrors {@link handleSubscribeToThreads}: requires diff --git a/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts b/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts index 3df76d28518..49f5f988d93 100644 --- a/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +++ b/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts @@ -923,4 +923,51 @@ describe("CopilotKitIntelligence", () => { }); }); }); + + describe("recallMemories", () => { + it("POSTs to /api/memories/recall with the user header and returns the envelope", async () => { + fetchMock.mockReturnValue( + jsonResponse({ + memories: [ + { + id: "m1", + kind: "topical", + scope: "user", + content: "User likes jazz.", + sourceThreadIds: [], + invalidatedAt: null, + score: 0.87, + }, + ], + }), + ); + + const result = await client.recallMemories({ + userId: "user-1", + query: "music taste", + limit: 5, + scope: "user", + }); + + expect(result.memories[0]).toMatchObject({ id: "m1", score: 0.87 }); + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("https://api.example.com/api/memories/recall"); + expect(init.method).toBe("POST"); + expect(init.headers["x-cpki-user-id"]).toBe("user-1"); + expect(JSON.parse(init.body)).toEqual({ + query: "music taste", + limit: 5, + scope: "user", + }); + }); + + it("omits limit and scope from the body when not provided", async () => { + fetchMock.mockReturnValue(jsonResponse({ memories: [] })); + + await client.recallMemories({ userId: "user-1", query: "hi" }); + + const [, init] = fetchMock.mock.calls[0]; + expect(JSON.parse(init.body)).toEqual({ query: "hi" }); + }); + }); }); diff --git a/packages/runtime/src/v2/runtime/intelligence-platform/client.ts b/packages/runtime/src/v2/runtime/intelligence-platform/client.ts index 53f70da1d6b..0669f20f914 100644 --- a/packages/runtime/src/v2/runtime/intelligence-platform/client.ts +++ b/packages/runtime/src/v2/runtime/intelligence-platform/client.ts @@ -165,6 +165,8 @@ export interface MemorySummary { sourceThreadIds: string[]; /** ISO-8601 timestamp when the memory was retired, or `null` if live. */ invalidatedAt: string | null; + /** Relevance score from a `recall` (hybrid RAG) query. Present only on recall responses. */ + score?: number; } /** Response from {@link CopilotKitIntelligence.listMemories}. */ @@ -172,6 +174,11 @@ export interface ListMemoriesResponse { memories: MemorySummary[]; } +/** Response from {@link CopilotKitIntelligence.recallMemories}. */ +export interface RecallMemoriesResponse { + memories: MemorySummary[]; +} + /** * Response from a create ({@link CopilotKitIntelligence.createMemory}) or * supersede ({@link CopilotKitIntelligence.updateMemory}) call: the stored @@ -692,6 +699,30 @@ export class CopilotKitIntelligence { ); } + /** + * Semantically recall the given user's memories (platform `POST + * /api/memories/recall`, hybrid RAG). Each returned memory carries a + * relevance `score`. `scope` narrows to `"user"`/`"project"`; omitted → platform default. + * @throws {@link PlatformRequestError} on non-2xx responses. + */ + async recallMemories(params: { + userId: string; + query: string; + limit?: number; + scope?: string; + }): Promise { + return this.#request( + "POST", + `/api/memories/recall`, + { + query: params.query, + ...(params.limit !== undefined ? { limit: params.limit } : {}), + ...(params.scope !== undefined ? { scope: params.scope } : {}), + }, + { [INTELLIGENCE_USER_ID_HEADER]: params.userId }, + ); + } + async ɵsubscribeToThreads( params: SubscribeToThreadsRequest, ): Promise { From d62df4b3d18c04047c8b040f55ed029efcb5db21 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:51:47 -0700 Subject: [PATCH 008/456] feat(runtime): thread project join creds through /memories/subscribe proxy Extend the memory subscribe passthrough so the response carries the optional project-scoped realtime credentials { projectJoinToken?, projectJoinCode? } alongside the existing user { joinToken, joinCode }. - Widen SubscribeToMemoriesResponse with optional projectJoinToken / projectJoinCode; the platform client passes them through verbatim. - handleSubscribeToMemories forwards both fields only when present and omits them when absent (silent-degrade contract; client then opens only the user channel). Field names match Intelligence B0a verbatim. - Tests: handler forwards project creds when present and omits when absent (no present-with-undefined keys); client passes them through. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../runtime/__tests__/handle-memories.test.ts | 44 +++++++++++++++++++ .../runtime/handlers/intelligence/memories.ts | 14 ++++++ .../__tests__/client.test.ts | 20 +++++++++ .../runtime/intelligence-platform/client.ts | 14 +++++- 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts b/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts index 5b8e6777d9b..f82f33d4a84 100644 --- a/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts +++ b/packages/runtime/src/v2/runtime/__tests__/handle-memories.test.ts @@ -489,6 +489,50 @@ describe("memory handlers", () => { }); }); + it("forwards project credentials when the platform mints them", async () => { + const intelligence = { + ɵsubscribeToMemories: vi.fn().mockResolvedValue({ + joinToken: "jt-1", + joinCode: "jc-1", + projectJoinToken: "pjt-1", + projectJoinCode: "pjc-1", + }), + }; + const identifyUser = createIdentifyUser(); + const runtime = createIntelligenceRuntime({ intelligence, identifyUser }); + const request = jsonRequest("/memories/subscribe", "POST"); + + const response = await handleSubscribeToMemories({ runtime, request }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + joinToken: "jt-1", + joinCode: "jc-1", + projectJoinToken: "pjt-1", + projectJoinCode: "pjc-1", + }); + }); + + it("omits project credentials when the platform does not mint them", async () => { + const intelligence = { + ɵsubscribeToMemories: vi + .fn() + .mockResolvedValue({ joinToken: "jt-1", joinCode: "jc-1" }), + }; + const identifyUser = createIdentifyUser(); + const runtime = createIntelligenceRuntime({ intelligence, identifyUser }); + const request = jsonRequest("/memories/subscribe", "POST"); + + const response = await handleSubscribeToMemories({ runtime, request }); + + expect(response.status).toBe(200); + const body = (await response.json()) as Record; + // Silent-degrade: the keys are absent, not present-with-undefined. + expect(body).toEqual({ joinToken: "jt-1", joinCode: "jc-1" }); + expect(body).not.toHaveProperty("projectJoinToken"); + expect(body).not.toHaveProperty("projectJoinCode"); + }); + it("returns 422 for subscribe when intelligence is not configured", async () => { const runtime = new CopilotRuntime({ agents: {} }); diff --git a/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts b/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts index 4d3b0cf8000..fadb870d0e9 100644 --- a/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts +++ b/packages/runtime/src/v2/runtime/handlers/intelligence/memories.ts @@ -260,6 +260,12 @@ export async function handleRecallMemories({ * the `joinCode` here (unlike threads, where it rides the thread-list response) * because the client builds the `user_meta:memories:` channel topic * from it. + * + * When the platform also resolves a project scope, the response additionally + * carries `projectJoinToken` / `projectJoinCode`, which the client uses to open + * a second `project_meta:memories:` channel. These are + * optional: absent project scope → both fields are omitted (silent-degrade + * contract; the client opens only the user channel). */ export async function handleSubscribeToMemories({ runtime, @@ -277,6 +283,14 @@ export async function handleSubscribeToMemories({ return Response.json({ joinToken: credentials.joinToken, joinCode: credentials.joinCode, + // Project-scoped credentials ride along only when the platform minted + // them; omit both when absent (silent-degrade contract). + ...(credentials.projectJoinToken !== undefined + ? { projectJoinToken: credentials.projectJoinToken } + : {}), + ...(credentials.projectJoinCode !== undefined + ? { projectJoinCode: credentials.projectJoinCode } + : {}), }); } catch (error) { logger.error({ err: error }, "Error subscribing to memories"); diff --git a/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts b/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts index 49f5f988d93..01dc6e2c625 100644 --- a/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +++ b/packages/runtime/src/v2/runtime/intelligence-platform/__tests__/client.test.ts @@ -290,6 +290,26 @@ describe("CopilotKitIntelligence", () => { expect(opts.headers["x-cpki-user-id"]).toBe("user-1"); expect(opts.body).toBeUndefined(); }); + + it("passes through optional project credentials when the platform returns them", async () => { + fetchMock.mockReturnValue( + jsonResponse({ + joinToken: "jt-mem", + joinCode: "jc-mem", + projectJoinToken: "pjt-mem", + projectJoinCode: "pjc-mem", + }), + ); + + const result = await client.ɵsubscribeToMemories({ userId: "user-1" }); + + expect(result).toEqual({ + joinToken: "jt-mem", + joinCode: "jc-mem", + projectJoinToken: "pjt-mem", + projectJoinCode: "pjc-mem", + }); + }); }); describe("updateThread", () => { diff --git a/packages/runtime/src/v2/runtime/intelligence-platform/client.ts b/packages/runtime/src/v2/runtime/intelligence-platform/client.ts index 0669f20f914..06a32c340bf 100644 --- a/packages/runtime/src/v2/runtime/intelligence-platform/client.ts +++ b/packages/runtime/src/v2/runtime/intelligence-platform/client.ts @@ -248,6 +248,15 @@ export interface SubscribeToMemoriesRequest { export interface SubscribeToMemoriesResponse { joinToken: string; joinCode: string; + /** + * Project-scoped realtime credentials, minted by the platform only when the + * caller's API key resolves to a project scope. Absent when project scope is + * unavailable — a silent-degrade contract: the client then opens only the + * user channel. When present, the client builds the second + * `project_meta:memories:` channel topic from them. + */ + projectJoinToken?: string; + projectJoinCode?: string; } export type ConnectThreadResponse = ThreadConnectionResponse | null; @@ -739,7 +748,10 @@ export class CopilotKitIntelligence { * Mint memory-realtime join credentials (platform `POST * /api/memories/subscribe`). Returns both the single-use `joinToken` and the * per-user `joinCode` the client needs to build the - * `user_meta:memories:` channel topic. + * `user_meta:memories:` channel topic. When the platform also + * resolves a project scope it returns optional `projectJoinToken` / + * `projectJoinCode`; both are passed through verbatim (omitted when absent, + * the silent-degrade contract). * * The user is supplied via the `x-cpki-user-id` header — the same way every * other memory endpoint (`listMemories`/`createMemory`/…) identifies the app From 491675037bfcc30f7710eeb055ce5726af4600f7 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:52:30 -0700 Subject: [PATCH 009/456] feat(core): add recall() to the memory store with score projection Adds recall(query, opts?) to the MemoryStore (POST /memories/recall, hybrid RAG), a score? field on Memory, the recallResponseToMemory projector, MEMORIES_RECALL_PATH, and the MEMORY_RECALL_FAILED error code. The user-scope list filter is intentionally retained (its removal is relocated to a later slot co-landing with the project realtime channel). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/__tests__/memory.test.ts | 102 +++++++++++++++++++++ packages/core/src/memory-errors.ts | 8 ++ packages/core/src/memory.ts | 80 ++++++++++++++++ 3 files changed, 190 insertions(+) diff --git a/packages/core/src/__tests__/memory.test.ts b/packages/core/src/__tests__/memory.test.ts index 8406089a859..c86821291d5 100644 --- a/packages/core/src/__tests__/memory.test.ts +++ b/packages/core/src/__tests__/memory.test.ts @@ -1669,3 +1669,105 @@ test("getServerState returns the empty initial state and is stable", () => { // Stable reference across calls so React's useSyncExternalStore does not loop. expect(store.getServerState()).toBe(serverState); }); + +describe("memory store recall", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("POSTs to /memories/recall and resolves to scored memories", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ ok: true, json: async () => ({ memories: [] }) }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ joinToken: "jt-1", joinCode: "jc-1" }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + memories: [ + { + id: "m1", + kind: "topical", + scope: "user", + content: "User likes jazz.", + sourceThreadIds: [], + invalidatedAt: null, + score: 0.91, + }, + { + id: "p1", + kind: "operational", + scope: "project", + content: "Team ships on Fridays.", + sourceThreadIds: [], + invalidatedAt: null, + score: 0.42, + }, + ], + }), + }); + vi.stubGlobal("fetch", fetchMock); + const store = createMemoryStore(memoryEnvironment(fetchMock)); + store.start(); + store.setContext(sampleContext); + await flushEffects(); + const results = await store.recall("music", { limit: 5, scope: "user" }); + expect(results.map((m) => m.id)).toEqual(["m1", "p1"]); + expect(results.map((m) => m.score)).toEqual([0.91, 0.42]); + expect(results.map((m) => m.scope)).toEqual(["user", "project"]); + expect(fetchMock).toHaveBeenLastCalledWith( + "https://runtime.example.com/memories/recall", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ query: "music", limit: 5, scope: "user" }), + }), + ); + expect(store.getState().memories).toEqual([]); // recall does NOT mutate the snapshot + }); + + it("omits limit/scope from the body when not provided", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ ok: true, json: async () => ({ memories: [] }) }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ joinToken: "jt-1", joinCode: "jc-1" }), + }) + .mockResolvedValueOnce({ ok: true, json: async () => ({ memories: [] }) }); + vi.stubGlobal("fetch", fetchMock); + const store = createMemoryStore(memoryEnvironment(fetchMock)); + store.start(); + store.setContext(sampleContext); + await flushEffects(); + await store.recall("hi"); + expect(fetchMock).toHaveBeenLastCalledWith( + "https://runtime.example.com/memories/recall", + expect.objectContaining({ body: JSON.stringify({ query: "hi" }) }), + ); + }); + + it("rejects when no context is set", async () => { + const store = createMemoryStore(memoryEnvironment(vi.fn())); + store.start(); + await expect(store.recall("hi")).rejects.toThrow(); + }); + + it("rejects with a MemoryError on a non-ok recall response", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ ok: true, json: async () => ({ memories: [] }) }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ joinToken: "jt-1", joinCode: "jc-1" }), + }) + .mockResolvedValueOnce({ ok: false, status: 500 }); + vi.stubGlobal("fetch", fetchMock); + const store = createMemoryStore(memoryEnvironment(fetchMock)); + store.start(); + store.setContext(sampleContext); + await flushEffects(); + await expect(store.recall("hi")).rejects.toBeInstanceOf(MemoryError); + }); +}); diff --git a/packages/core/src/memory-errors.ts b/packages/core/src/memory-errors.ts index 7678e647441..85b38b50291 100644 --- a/packages/core/src/memory-errors.ts +++ b/packages/core/src/memory-errors.ts @@ -48,6 +48,7 @@ export type MemoryErrorCategory = /** Stable codes for the memory store's surfaced errors. */ export type MemoryErrorCode = | "MEMORY_LIST_FAILED" + | "MEMORY_RECALL_FAILED" | "MEMORY_CREDENTIALS_FAILED" | "MEMORY_MUTATION_FAILED" | "MEMORY_REQUEST_TIMEOUT"; @@ -83,6 +84,13 @@ export const MEMORY_ERROR_REGISTRY: Readonly< message: "Failed to fetch memories", docsPath: "docs/errors/memory.md#memory_list_failed", }, + MEMORY_RECALL_FAILED: { + code: "MEMORY_RECALL_FAILED", + category: "dependency", + retryable: true, + message: "Failed to recall memories", + docsPath: "docs/errors/memory.md#memory_recall_failed", + }, MEMORY_CREDENTIALS_FAILED: { code: "MEMORY_CREDENTIALS_FAILED", category: "dependency", diff --git a/packages/core/src/memory.ts b/packages/core/src/memory.ts index 739f974483b..fa6952fc239 100644 --- a/packages/core/src/memory.ts +++ b/packages/core/src/memory.ts @@ -53,6 +53,7 @@ import type { ɵPhoenixChannelSession } from "./utils/phoenix-observable"; // `/api/memories` the same way it maps `/threads` -> `/api/threads`. const MEMORIES_PATH = "/memories"; const MEMORIES_SUBSCRIBE_PATH = "/memories/subscribe"; +const MEMORIES_RECALL_PATH = "/memories/recall"; const REQUEST_TIMEOUT_MS = 15_000; /** Consecutive socket errors tolerated before the realtime stream gives up. */ const MAX_SOCKET_RETRIES = 5; @@ -105,6 +106,8 @@ export interface Memory { content: string; sourceThreadIds: readonly string[]; invalidatedAt: string | null; + /** Relevance score from a `recall()` (hybrid RAG) query; `undefined` for list/realtime/mutation memories. */ + score?: number; } /** Input for creating a memory; `scope` defaults to `"user"` (v1 is user-scoped). */ @@ -550,6 +553,13 @@ interface MemoryStore { * immediately when no context is set. */ refresh(): Promise; + /** + * Semantically recalls memories via `POST {runtimeUrl}/memories/recall` + * (hybrid RAG). Returns memories ordered by relevance, each carrying a + * `score`. Does NOT mutate the snapshot — a one-shot query. Rejects when no + * context is set or the request fails. + */ + recall(query: string, opts?: { limit?: number; scope?: MemoryScope }): Promise; /** Creates a memory; resolves to the stored memory (server-authoritative). */ addMemory(input: NewMemory): Promise; /** Supersedes a memory; resolves to the new memory (its id changes). */ @@ -756,6 +766,30 @@ function responseToMemory(data: { }; } +/** + * Projects a recall REST response memory (which carries a relevance `score`) + * to the public {@link Memory} shape. `score` is copied through when supplied. + */ +function recallResponseToMemory(data: { + id: string; + kind: MemoryKind; + scope: MemoryScope; + content: string; + sourceThreadIds: readonly string[]; + invalidatedAt: string | null; + score?: number; +}): Memory { + return { + id: data.id, + kind: data.kind, + scope: data.scope, + content: data.content, + sourceThreadIds: data.sourceThreadIds, + invalidatedAt: data.invalidatedAt, + ...(typeof data.score === "number" ? { score: data.score } : {}), + }; +} + const MEMORY_KINDS: ReadonlySet = new Set([ "topical", "episodic", @@ -1374,6 +1408,52 @@ function createMemoryStore(environment: MemoryEnvironment): MemoryStore { store.dispatch(memoryRestEvents.listRequested({ sessionId })); return done; }, + recall( + query: string, + opts?: { limit?: number; scope?: MemoryScope }, + ): Promise { + const { context } = store.getState(); + if (!context?.runtimeUrl) { + return Promise.reject(new Error("Runtime URL is not configured")); + } + const body: Record = { query }; + if (opts?.limit !== undefined) body.limit = opts.limit; + if (opts?.scope !== undefined) body.scope = opts.scope; + + const recall$ = memoryFromFetch( + `${context.runtimeUrl}${MEMORIES_RECALL_PATH}`, + { + selector: async (response) => { + if (!response.ok) { + throw new MemoryError("MEMORY_RECALL_FAILED", { + message: `Failed to recall memories: ${response.status}`, + retryable: isRetryableStatus(response.status), + }); + } + return (await response.json()) as { memories: unknown[] }; + }, + fetch: environment.fetch, + method: "POST", + headers: { ...context.headers, "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, + ).pipe( + timeout({ + first: REQUEST_TIMEOUT_MS, + with: () => { + throw new MemoryError("MEMORY_REQUEST_TIMEOUT"); + }, + }), + map((data) => + (data.memories ?? []).map((m) => + recallResponseToMemory( + m as Parameters[0], + ), + ), + ), + ); + return firstValueFrom(recall$); + }, addMemory(input: NewMemory): Promise { return trackMutation( memoryAdapterEvents.addRequested({ From 227059ad4861b8be6914caa4dc8871692fc05986 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:55:43 -0700 Subject: [PATCH 010/456] feat(core): add catalog-component state + methods to RunHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/__tests__/catalog-components.test.ts | 59 +++++++++++++++++++ packages/core/src/core/run-handler.ts | 59 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 packages/core/src/core/__tests__/catalog-components.test.ts diff --git a/packages/core/src/core/__tests__/catalog-components.test.ts b/packages/core/src/core/__tests__/catalog-components.test.ts new file mode 100644 index 00000000000..bf0567fb225 --- /dev/null +++ b/packages/core/src/core/__tests__/catalog-components.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi } from "vitest"; +import { CopilotKitCore } from "../core"; + +describe("CopilotKitCore catalog components", () => { + it("registers a component list readable via catalogComponents", () => { + const core = new CopilotKitCore({}); + core.setCatalogComponents([ + { name: "PieChart", schema: { type: "object" } }, + { name: "FlightCard", description: "flight", schema: { type: "object" } }, + ]); + expect(core.catalogComponents.map((c) => c.name)).toEqual([ + "PieChart", + "FlightCard", + ]); + expect(core.catalogComponents[1]!.description).toBe("flight"); + }); + + it("treats every component as enabled by default", () => { + const core = new CopilotKitCore({}); + core.setCatalogComponents([{ name: "PieChart", schema: {} }]); + expect(core.isCatalogComponentEnabled("PieChart")).toBe(true); + expect(core.isCatalogComponentEnabled("Unknown")).toBe(true); + }); + + it("disables and re-enables a component", () => { + const core = new CopilotKitCore({}); + core.setCatalogComponents([{ name: "PieChart", schema: {} }]); + core.setCatalogComponentEnabled("PieChart", false); + expect(core.isCatalogComponentEnabled("PieChart")).toBe(false); + core.setCatalogComponentEnabled("PieChart", true); + expect(core.isCatalogComponentEnabled("PieChart")).toBe(true); + }); + + it("preserves disabled state across setCatalogComponents re-registration", () => { + const core = new CopilotKitCore({}); + core.setCatalogComponents([{ name: "PieChart", schema: {} }]); + core.setCatalogComponentEnabled("PieChart", false); + core.setCatalogComponents([ + { name: "PieChart", schema: {} }, + { name: "Badge", schema: {} }, + ]); + expect(core.isCatalogComponentEnabled("PieChart")).toBe(false); + expect(core.isCatalogComponentEnabled("Badge")).toBe(true); + }); + + it("notifies subscribers via onCatalogComponentsChanged on register and toggle", () => { + const core = new CopilotKitCore({}); + const onCatalogComponentsChanged = vi.fn(); + core.subscribe({ onCatalogComponentsChanged }); + core.setCatalogComponents([{ name: "PieChart", schema: {} }]); + core.setCatalogComponentEnabled("PieChart", false); + expect(onCatalogComponentsChanged).toHaveBeenCalledTimes(2); + const last = onCatalogComponentsChanged.mock.calls.at(-1)![0]; + expect(last.copilotkit).toBe(core); + expect(last.catalogComponents.map((c: { name: string }) => c.name)).toEqual([ + "PieChart", + ]); + }); +}); diff --git a/packages/core/src/core/run-handler.ts b/packages/core/src/core/run-handler.ts index d18285e91ff..408fb0ffb13 100644 --- a/packages/core/src/core/run-handler.ts +++ b/packages/core/src/core/run-handler.ts @@ -78,6 +78,19 @@ interface ExecuteToolHandlerResult { isArgumentError: boolean; } +/** + * A registered A2UI catalog component, as exposed to the inspector via + * `CopilotKitCore.catalogComponents`. `schema` is an opaque JSON-schema-ish + * value (the built catalog's Zod schema or a serialized form); the inspector + * treats it as unknown. `description` is optional because the built + * `ComponentApi` does not carry descriptions. + */ +export interface CopilotKitCoreCatalogComponent { + name: string; + description?: string; + schema: unknown; +} + /** * Handles agent execution, tool calling, and agent connectivity for CopilotKitCore. * Manages the complete lifecycle of agent runs including tool execution and follow-ups. @@ -86,6 +99,19 @@ export class RunHandler { // eslint-disable-next-line @typescript-eslint/no-explicit-any private _tools: FrontendTool[] = []; + /** + * The full list of A2UI catalog components registered by the provider. + * Order is preserved for display in the inspector. + */ + private _catalogComponents: CopilotKitCoreCatalogComponent[] = []; + + /** + * Names of catalog components the caller has explicitly disabled. A name + * absent from this set is enabled (default). Survives re-registration so a + * catalog identity change does not silently re-enable a disabled component. + */ + private _disabledCatalogComponents: Set = new Set(); + /** * Keys of frontend tools explicitly disabled at runtime via the Inspector's * Capabilities tool (`setToolEnabled`). Kept independently of each tool's own @@ -247,6 +273,39 @@ export class RunHandler { this._tools = [...tools]; } + /** + * Return the registered A2UI catalog components (readonly). + */ + get catalogComponents(): ReadonlyArray { + return this._catalogComponents; + } + + /** + * Replace the registered catalog component list. Preserves the disabled set + * (by name) so re-registration does not re-enable disabled components. + */ + setCatalogComponents(components: CopilotKitCoreCatalogComponent[]): void { + this._catalogComponents = [...components]; + } + + /** + * Enable or disable a catalog component by name. + */ + setCatalogComponentEnabled(name: string, enabled: boolean): void { + if (enabled) { + this._disabledCatalogComponents.delete(name); + } else { + this._disabledCatalogComponents.add(name); + } + } + + /** + * Whether a catalog component is enabled. Unknown names default to enabled. + */ + isCatalogComponentEnabled(name: string): boolean { + return !this._disabledCatalogComponents.has(name); + } + /** * Connect an agent (establish initial connection) */ From 9084f29cd5ccb642100b8479fa45ce0538ac554f Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 13:55:43 -0700 Subject: [PATCH 011/456] feat(core): expose catalogComponents + enable/disable via RunHandler delegation Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/core/core.ts | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/core/src/core/core.ts b/packages/core/src/core/core.ts index 1acf5dbb6b5..61a231f3eb5 100644 --- a/packages/core/src/core/core.ts +++ b/packages/core/src/core/core.ts @@ -26,6 +26,7 @@ import type { CopilotKitCoreGetToolParams, CopilotKitCoreRunToolParams, CopilotKitCoreRunToolResult, + CopilotKitCoreCatalogComponent, } from "./run-handler"; import { RunHandler } from "./run-handler"; import type { DebugConfig } from "@copilotkit/shared"; @@ -81,6 +82,7 @@ export type { CopilotKitCoreGetToolParams, CopilotKitCoreRunToolParams, CopilotKitCoreRunToolResult, + CopilotKitCoreCatalogComponent, }; export interface CopilotKitCoreStopAgentParams { @@ -166,6 +168,15 @@ export interface CopilotKitCoreSubscriber { copilotkit: CopilotKitCore; context: Readonly>; }) => void | Promise; + /** + * Fired when the A2UI catalog component list changes (registration) or when + * a component is enabled/disabled. Consumers (the provider, the inspector) + * re-derive the filtered catalog from this event. + */ + onCatalogComponentsChanged?: (event: { + copilotkit: CopilotKitCore; + catalogComponents: ReadonlyArray; + }) => void | Promise; onSuggestionsConfigChanged?: (event: { copilotkit: CopilotKitCore; suggestionsConfig: Readonly>; @@ -595,6 +606,10 @@ export class CopilotKitCore { return this.runHandler.tools; } + get catalogComponents(): ReadonlyArray { + return this.runHandler.catalogComponents; + } + get runtimeUrl(): string | undefined { return this.agentRegistry.runtimeUrl; } @@ -1011,6 +1026,38 @@ export class CopilotKitCore { return this.runHandler.isToolEnabled(name, agentId); } + /** + * A2UI catalog component management (delegated to RunHandler). + * Registers the full component list and controls per-component enablement. + */ + setCatalogComponents(components: CopilotKitCoreCatalogComponent[]): void { + this.runHandler.setCatalogComponents(components); + void this.notifySubscribers( + (subscriber) => + subscriber.onCatalogComponentsChanged?.({ + copilotkit: this, + catalogComponents: this.runHandler.catalogComponents, + }), + "Subscriber onCatalogComponentsChanged error:", + ); + } + + setCatalogComponentEnabled(name: string, enabled: boolean): void { + this.runHandler.setCatalogComponentEnabled(name, enabled); + void this.notifySubscribers( + (subscriber) => + subscriber.onCatalogComponentsChanged?.({ + copilotkit: this, + catalogComponents: this.runHandler.catalogComponents, + }), + "Subscriber onCatalogComponentsChanged error:", + ); + } + + isCatalogComponentEnabled(name: string): boolean { + return this.runHandler.isCatalogComponentEnabled(name); + } + /** * Subscription lifecycle */ From 6ac2a9e032df56d6a1ad40bac3127fb0432e1c62 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:09:01 -0700 Subject: [PATCH 012/456] feat(web-inspector): add Capabilities tab label + MenuKey (A3 task 1) --- packages/web-inspector/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 79f8888dfe3..e351a10bab1 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -94,6 +94,7 @@ type MenuKey = | "ag-ui-events" | "agents" | "frontend-tools" + | "capabilities" | "agent-context" | "threads" | "memories" @@ -122,6 +123,11 @@ const INTELLIGENCE_SIGNUP_URL = "https://go.copilotkit.ai/intelligence-signup"; const THREADS_INTELLIGENCE_SIGNIN_URL = "https://dashboard.operations.copilotkit.ai/sign-in"; const TALK_TO_ENGINEER_URL = "https://www.copilotkit.ai/talk-to-an-engineer"; +// Label for the Capabilities tab (client-authoritative dev experimentation +// surface: toggle frontend tools + A2UI catalog components on/off, enforced +// immediately via core.setToolEnabled / core.setCatalogComponentEnabled). +// Renameable — keep the display string in this one place. +const CAPABILITIES_TAB_LABEL = "Capabilities"; const THREADS_DOCS_URL = "https://docs.copilotkit.ai/threads"; const SELF_HOSTED_INTELLIGENCE_URL = "https://docs.copilotkit.ai/premium/self-hosting"; From 201d4477c0b098ddf635b0abde0c59e45195ea1c Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:10:57 -0700 Subject: [PATCH 013/456] feat(web-inspector): add capability row model + pure helper with tests (A3 task 2) --- .../src/__tests__/web-inspector.spec.ts | 56 +++++++++++++++ packages/web-inspector/src/index.ts | 68 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/web-inspector/src/__tests__/web-inspector.spec.ts b/packages/web-inspector/src/__tests__/web-inspector.spec.ts index e960f176a05..259653cb179 100644 --- a/packages/web-inspector/src/__tests__/web-inspector.spec.ts +++ b/packages/web-inspector/src/__tests__/web-inspector.spec.ts @@ -1,6 +1,7 @@ import { CpkThreadInspector, WebInspectorElement, + ɵbuildCapabilityRows, ɵCpkThreadDetails, } from "../index.js"; import type { ThreadDebuggerProvider } from "../index.js"; @@ -3316,3 +3317,58 @@ describe("WebInspectorElement memories — tab telemetry + detach reset", () => expect(state._memoriesAvailable).toBe(true); }); }); + +describe("ɵbuildCapabilityRows", () => { + it("maps core.tools to rows, reflects isToolEnabled, and sorts by agentId then name", () => { + const enabled = new Set(["b-tool"]); + const core = { + tools: [ + { name: "z-tool", agentId: "agent-2", description: "zed" }, + { name: "a-tool", agentId: "agent-1" }, + { name: "b-tool" }, + ], + isToolEnabled: (name: string) => enabled.has(name), + }; + const rows = ɵbuildCapabilityRows(core); + expect(rows.map((r) => r.name)).toEqual(["b-tool", "a-tool", "z-tool"]); + expect(rows[0]).toMatchObject({ + key: ":b-tool", + name: "b-tool", + agentId: undefined, + enabled: true, + fired: false, + }); + expect(rows.find((r) => r.name === "a-tool")).toMatchObject({ + key: "agent-1:a-tool", + enabled: false, + }); + expect(rows.find((r) => r.name === "z-tool")).toMatchObject({ + description: "zed", + enabled: false, + }); + }); + + it("passes isToolEnabled the tool's agentId (per-agent enablement)", () => { + const calls: Array<[string, string | undefined]> = []; + const core = { + tools: [{ name: "t", agentId: "agent-x" }], + isToolEnabled: (name: string, agentId?: string) => { + calls.push([name, agentId]); + return true; + }, + }; + ɵbuildCapabilityRows(core); + expect(calls).toEqual([["t", "agent-x"]]); + }); + + it("marks rows as fired when their key is in the fired set", () => { + const core = { tools: [{ name: "t", agentId: "a" }], isToolEnabled: () => true }; + const rows = ɵbuildCapabilityRows(core, new Set(["a:t"])); + expect(rows[0]?.fired).toBe(true); + }); + + it("returns an empty array when there are no tools", () => { + expect(ɵbuildCapabilityRows({ tools: [], isToolEnabled: () => false })).toEqual([]); + expect(ɵbuildCapabilityRows({ isToolEnabled: () => false })).toEqual([]); + }); +}); diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index e351a10bab1..11f1ec01ab2 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -84,6 +84,8 @@ import type { } from "./lib/telemetry.js"; export type { Anchor } from "./lib/types.js"; +export { buildCapabilityRows as ɵbuildCapabilityRows }; +export type { CapabilityToolRow as ɵCapabilityToolRow }; export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const; export const THREAD_INSPECTOR_TAG = "cpk-thread-inspector" as const; @@ -226,6 +228,61 @@ type InspectorToolDefinition = { type: "handler" | "renderer"; }; +// ─── Capabilities tab view-models ──────────────────────────────────────────── +// A single toggle row. `key` is the stable identity used for the "fired" set +// and as a Lit list key; for tools it is `${agentId}:${name}` (agentId "" for +// global tools), for catalog components it is the component name. +type CapabilityToolRow = { + key: string; + name: string; + description?: string; + agentId?: string; + enabled: boolean; + fired: boolean; +}; + +// Minimal structural view of CopilotKitCore that the pure helper needs, so +// buildCapabilityRows is trivially unit-testable with a plain object. Method +// names MUST match the A1 contract exactly. +type CapabilityToolSource = { + tools?: ReadonlyArray<{ + name: string; + description?: string; + agentId?: string; + }>; + isToolEnabled: (name: string, agentId?: string) => boolean; +}; + +/** + * Map core.tools (the registry INCLUDING disabled tools) into Capabilities-tab + * frontend-tool rows. Pure: no DOM, no `this`. Reads current on/off state from + * core.isToolEnabled(name, agentId?) per the A1 contract. `fired` is passed in + * from the caller's session set (keyed identically to `key`). + */ +function buildCapabilityRows( + core: CapabilityToolSource, + firedKeys: ReadonlySet = new Set(), +): CapabilityToolRow[] { + const rows: CapabilityToolRow[] = []; + for (const tool of core.tools ?? []) { + const agentId = tool.agentId ?? ""; + const key = `${agentId}:${tool.name}`; + rows.push({ + key, + name: tool.name, + description: tool.description, + agentId: tool.agentId, + enabled: core.isToolEnabled(tool.name, tool.agentId), + fired: firedKeys.has(key), + }); + } + return rows.sort((a, b) => { + const agentCompare = (a.agentId ?? "").localeCompare(b.agentId ?? ""); + if (agentCompare !== 0) return agentCompare; + return a.name.localeCompare(b.name); + }); +} + type InspectorEvent = { id: string; agentId: string; @@ -4082,6 +4139,7 @@ export class WebInspectorElement extends LitElement { static properties = { core: { attribute: false }, autoAttachCore: { type: Boolean, attribute: "auto-attach-core" }, + _capabilitiesVersion: { state: true }, } as const; private _core: CopilotKitCore | null = null; @@ -4165,6 +4223,16 @@ export class WebInspectorElement extends LitElement { private attemptedAutoAttach = false; private cachedTools: InspectorToolDefinition[] = []; private toolSignature = ""; + // Bumped after every core.setToolEnabled / core.setCatalogComponentEnabled + // call so the Capabilities tab re-paints from the fresh isToolEnabled / + // isCatalogComponentEnabled getters. There is no core subscriber for + // enablement changes — the inspector itself drives the toggle, so we force + // the re-render locally. + private _capabilitiesVersion = 0; + // Names of capabilities (tool key `${agentId}:${name}` or catalog component + // `name`) that have FIRED at least once this session. Drives the optional + // "active" dot. Populated in the agent tool-call subscriber (Task 7). + private firedCapabilities: Set = new Set(); private eventFilterText = ""; private eventTypeFilter: InspectorAgentEventType | "all" = "all"; // Column widths for the AG-UI events table (agent, time, event-type; last col is auto) From ad7c052af2f3223a9442c5b0854f192b83cd65ba Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:11:40 -0700 Subject: [PATCH 014/456] feat(web-inspector): add Capabilities nav item gated on tools/catalog (A3 task 3) --- packages/web-inspector/src/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 11f1ec01ab2..2e515cbd41b 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -4338,6 +4338,8 @@ export class WebInspectorElement extends LitElement { private get menuItems(): MenuItem[] { const hasFrontendTools = (this._core?.tools?.length ?? 0) > 0; + const hasCatalog = (this._core?.catalogComponents?.length ?? 0) > 0; + const hasCapabilities = hasFrontendTools || hasCatalog; return [ { key: "ag-ui-events", @@ -4354,6 +4356,15 @@ export class WebInspectorElement extends LitElement { }, ] : []), + ...(hasCapabilities + ? [ + { + key: "capabilities" as const, + label: CAPABILITIES_TAB_LABEL, + icon: "SlidersHorizontal" as LucideIconName, + }, + ] + : []), { key: "agent-context", label: "Context", From 1300afd067bed00afc67a9bca1ab3db82e7e9c4a Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:12:37 -0700 Subject: [PATCH 015/456] feat(web-inspector): dispatch Capabilities view from renderMainContent (A3 task 4) --- packages/web-inspector/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 2e515cbd41b..de05e2cc692 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -7868,6 +7868,10 @@ ${argsString} Date: Tue, 14 Jul 2026 14:13:32 -0700 Subject: [PATCH 016/456] feat(web-inspector): implement Capabilities view + toggles (A3 task 5) --- packages/web-inspector/src/index.ts | 149 +++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index de05e2cc692..762ae428a2b 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -9990,7 +9990,154 @@ ${prettyEvent} + No core instance available +
+ `; + } + + const toolRows = buildCapabilityRows( + this._core as unknown as CapabilityToolSource, + this.firedCapabilities, + ); + const catalog = this._core.catalogComponents ?? []; + const hasCatalog = catalog.length > 0; + + if (toolRows.length === 0 && !hasCatalog) { + return html` +
+
+
+ ${this.renderIcon("SlidersHorizontal")} +
+

No capabilities registered

+

+ Frontend tools and A2UI catalog components will appear here once + they are registered on the CopilotKit core. +

+
+
+ `; + } + + return html` +
+
+
+

+ Toggle a capability off to omit it from what the agent sees. This + is a client-side experimentation surface and takes effect + immediately. +

+
+ + ${ + toolRows.length > 0 + ? html` +
+

Frontend tools

+
+ ${toolRows.map((row) => this.renderCapabilityRow(row))} +
+
+ ` + : nothing + } + + ${ + hasCatalog + ? html` +
+

A2UI catalog components

+
+ ${catalog.map((component) => + this.renderCapabilityRow({ + key: component.name, + name: component.name, + description: component.description, + enabled: this._core!.isCatalogComponentEnabled( + component.name, + ), + fired: this.firedCapabilities.has(component.name), + }), + )} +
+
+ ` + : nothing + } +
+
+ `; + } + + private renderCapabilityRow(row: CapabilityToolRow) { + // Frontend-tool keys are always `${agentId}:${name}` (agentId may be ""), + // so they contain a ":"; catalog keys are the bare component name. + const isTool = row.key.includes(":"); + return html` +
+
+
+ ${ + row.fired + ? html`` + : nothing + } + ${row.name} + ${ + row.agentId + ? html` + ${this.renderIcon("Bot")}${row.agentId} + ` + : nothing + } +
+ ${row.description ? html`

${row.description}

` : nothing} +
+ ${this.renderCapabilitySwitch(row.enabled, () => + isTool + ? this.handleToggleTool(row) + : this.handleToggleCatalogComponent(row.name), + )} +
+ `; + } + + private renderCapabilitySwitch(enabled: boolean, onToggle: () => void) { + const track = enabled ? "bg-emerald-500" : "bg-gray-300"; + const knob = enabled ? "translate-x-4" : "translate-x-0.5"; + return html` + + `; + } + + private handleToggleTool(row: CapabilityToolRow): void { + if (!this._core) return; + const next = !row.enabled; + // A1 contract: setToolEnabled(name, enabled, agentId?). Pass agentId only + // when the tool is agent-scoped so global tools toggle globally. + this._core.setToolEnabled(row.name, next, row.agentId); + this._capabilitiesVersion += 1; + this.requestUpdate(); + } + + private handleToggleCatalogComponent(name: string): void { + if (!this._core) return; + const next = !this._core.isCatalogComponentEnabled(name); + this._core.setCatalogComponentEnabled(name, next); + this._capabilitiesVersion += 1; + this.requestUpdate(); } private renderToolsView() { From eb31768a00ae7248450400607b237c3b899b571f Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:13:50 -0700 Subject: [PATCH 017/456] feat(core): open project_meta realtime channel and surface project memories Parse optional projectJoinToken/projectJoinCode from /memories/subscribe. When present, open a second project_meta:memories: realtime channel alongside the user channel, feeding the same id-keyed reducer. Both channels share the same session stamp so the reducer's superseded-context guard drops stale deltas from both identically (the D4 landmine). Absent project creds -> user-only, silent degrade, realtimeStatus unaffected. Remove the user-scope snapshot filter now that project rows stay in sync via the project channel, and invert the snapshot test to expect project rows. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/__tests__/memory.test.ts | 180 ++++++++++++++++++++- packages/core/src/memory.ts | 115 +++++++++++-- 2 files changed, 284 insertions(+), 11 deletions(-) diff --git a/packages/core/src/__tests__/memory.test.ts b/packages/core/src/__tests__/memory.test.ts index c86821291d5..60d2f759382 100644 --- a/packages/core/src/__tests__/memory.test.ts +++ b/packages/core/src/__tests__/memory.test.ts @@ -324,6 +324,59 @@ describe("memory store realtime", () => { return store; } + /** + * Boots a store whose `/memories/subscribe` ALSO returns project credentials, + * so the store opens a SECOND `project_meta:memories:` channel alongside + * the user channel. Returns the connected store. Socket ordering: + * `phoenix.sockets[0]` is the user channel (subscribed first in the merge), + * `phoenix.sockets[1]` is the project channel. + */ + async function connectedProjectRealtimeStore() { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ ok: true, json: async () => ({ memories: [] }) }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + joinToken: "jt-1", + joinCode: "jc-1", + projectJoinToken: "pjt-1", + projectJoinCode: "pjc-1", + }), + }); + vi.stubGlobal("fetch", fetchMock); + + const store = createMemoryStore(memoryEnvironment(fetchMock)); + store.start(); + store.setContext(realtimeContext); + await flushEffects(); + return store; + } + + /** A `project`-scoped `created` event as broadcast on the project channel. */ + function projectCreatedEvent( + id: string, + content = `content-${id}`, + ): MemoryMetadataEvent { + return { + operation: "created", + memoryId: id, + organizationId: "org-1", + projectId: "proj-1", + occurredAt: "2026-01-01T00:00:00Z", + memory: { + id, + organizationId: "org-1", + projectId: "proj-1", + scope: "project", + kind: "topical", + content, + sourceThreadIds: [], + invalidatedAt: null, + }, + }; + } + afterEach(() => { phoenix.sockets.splice(0); vi.unstubAllGlobals(); @@ -518,6 +571,122 @@ describe("memory store realtime", () => { expect(store.getState().realtimeStatus).toBe("connecting"); }); + + it("opens ONLY the user channel when no project credentials are returned", async () => { + const store = await connectedRealtimeStore(); + + // A single socket/channel — the user channel — is opened. No project socket. + expect(phoenix.sockets).toHaveLength(1); + expect(memoryChannel().topic).toBe("user_meta:memories:jc-1"); + + store.stop(); + }); + + it("opens a SECOND project_meta channel when project credentials are present", async () => { + const store = await connectedProjectRealtimeStore(); + + // Two sockets: the user channel and the project channel, each with its own + // join token (the token is a socket-level param). + expect(phoenix.sockets).toHaveLength(2); + expect(phoenix.sockets[0]?.channels[0]?.topic).toBe( + "user_meta:memories:jc-1", + ); + expect(phoenix.sockets[0]?.opts.params).toMatchObject({ + join_token: "jt-1", + }); + expect(phoenix.sockets[1]?.channels[0]?.topic).toBe( + "project_meta:memories:pjc-1", + ); + expect(phoenix.sockets[1]?.opts.params).toMatchObject({ + join_token: "pjt-1", + }); + // Both channels are actually joined. + expect(phoenix.sockets[0]?.channels[0]?.joinCount).toBeGreaterThan(0); + expect(phoenix.sockets[1]?.channels[0]?.joinCount).toBeGreaterThan(0); + + store.stop(); + }); + + it("upserts and invalidates project memories delivered on the project channel", async () => { + const store = await connectedProjectRealtimeStore(); + const userChannel = phoenix.sockets[0]!.channels[0]!; + const projectChannel = phoenix.sockets[1]!.channels[0]!; + + // A user-scoped delta on the user channel and a project-scoped delta on the + // project channel both land in the SAME id-keyed list. + userChannel.serverPush("memory_metadata", createdEvent("m1")); + projectChannel.serverPush("memory_metadata", projectCreatedEvent("p1")); + await flushEffects(); + + expect(store.getState().memories.map((m) => m.id)).toEqual(["p1", "m1"]); + expect( + store.getState().memories.find((m) => m.id === "p1")?.scope, + ).toBe("project"); + + // Invalidating the project memory on the project channel removes it. + projectChannel.serverPush("memory_metadata", { + operation: "invalidated", + memoryId: "p1", + organizationId: "org-1", + projectId: "proj-1", + occurredAt: "2026-01-01T00:00:00Z", + invalidated: { id: "p1" }, + }); + await flushEffects(); + + expect(store.getState().memories.map((m) => m.id)).toEqual(["m1"]); + + store.stop(); + }); + + it("shares the session stamp so a superseded context drops BOTH channels' deltas", async () => { + const store = await connectedProjectRealtimeStore(); + const userChannel = phoenix.sockets[0]!.channels[0]!; + const projectChannel = phoenix.sockets[1]!.channels[0]!; + + // Supersede the context: `contextChanged` bumps sessionId and clears the + // list. The old channels are now stamped with the PREVIOUS session. + store.setContext({ + runtimeUrl: "https://runtime.example.com", + wsUrl: "wss://gw.example.com/client", + headers: { Authorization: "Bearer token", "X-Cpki-User-Id": "u2" }, + }); + await flushEffects(); + + // Late deltas arriving on BOTH old channels must be dropped identically by + // the reducer's session guard (the D4 landmine): the project channel shares + // the user channel's session stamp, so neither stale delta leaks in. + userChannel.serverPush("memory_metadata", createdEvent("m-stale")); + projectChannel.serverPush("memory_metadata", projectCreatedEvent("p-stale")); + await flushEffects(); + + expect(store.getState().memories).toEqual([]); + + store.stop(); + }); + + it("does not regress realtimeStatus when the project channel is present", async () => { + const store = await connectedProjectRealtimeStore(); + + // The project channel does not emit status deltas; realtimeStatus is driven + // solely by the user channel. Joining the user channel -> "connected". + phoenix.sockets[0]!.channels[0]!.triggerJoin("ok"); + await flushEffects(); + + expect(store.getState().realtimeStatus).toBe("connected"); + + // A project-channel join failure must NOT regress the user-facing status. + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + phoenix.sockets[1]!.channels[0]!.triggerJoin("error", { + reason: "unauthorized", + }); + await flushEffects(); + + expect(store.getState().realtimeStatus).toBe("connected"); + + warn.mockRestore(); + store.stop(); + }); }); const sampleContext = { @@ -531,7 +700,10 @@ describe("memory store REST snapshot", () => { vi.unstubAllGlobals(); }); - it("loads the snapshot on setContext, keeping only user-scoped memories", async () => { + it("loads the snapshot on setContext, keeping both user- and project-scoped memories", async () => { + // Project rows are no longer filtered out of the snapshot: the store opens a + // `project_meta:memories:` channel that keeps them live, so surfacing + // them in the list no longer risks going stale (B0 project-realtime slice). const fetchMock = vi.fn().mockResolvedValueOnce({ ok: true, json: async () => ({ @@ -566,7 +738,11 @@ describe("memory store REST snapshot", () => { "https://runtime.example.com/memories", expect.objectContaining({ method: "GET" }), ); - expect(store.getState().memories.map((m) => m.id)).toEqual(["m1"]); + expect(store.getState().memories.map((m) => m.id)).toEqual(["m1", "p1"]); + expect(store.getState().memories.map((m) => m.scope)).toEqual([ + "user", + "project", + ]); expect(store.getState().isLoading).toBe(false); store.stop(); diff --git a/packages/core/src/memory.ts b/packages/core/src/memory.ts index fa6952fc239..06e991e37e3 100644 --- a/packages/core/src/memory.ts +++ b/packages/core/src/memory.ts @@ -1,6 +1,7 @@ import { phoenixExponentialBackoff } from "@copilotkit/shared"; import type { Observable } from "rxjs"; import { + EMPTY, asapScheduler, defer, firstValueFrom, @@ -206,6 +207,13 @@ const memoryRestEvents = createActionGroup("Memory REST", { sessionId: number; joinToken: string; joinCode: string; + // Optional project-scoped realtime credentials. Present only when the + // caller's API key resolves a project scope (B1b `/memories/subscribe` + // returns them alongside the user credentials); omitted otherwise. When + // present, the socket effect opens a SECOND `project_meta:memories:` + // channel so project rows stay live. Absent -> user-only (silent degrade). + projectJoinToken?: string; + projectJoinCode?: string; }>(), // Credentials outcomes are a SILENT degrade: they feed only the realtime // socket effect and deliberately do NOT touch the reducer's `available`/ @@ -234,10 +242,12 @@ interface MemoryMetadataPayloadMemory extends Memory { } /** - * The realtime `memory_metadata` event broadcast on the `user_meta` channel: + * The realtime `memory_metadata` event broadcast on a memory channel: * `created`/`updated` carry the full memory, `invalidated` carries only its id. - * The gateway strips `userId` before broadcasting and only delivers - * user-scoped memories, so this is always the current user's stream. + * The same wire shape is delivered on both the user channel + * (`user_meta:memories:`, current user's memories) and the optional + * project channel (`project_meta:memories:`, memories shared across the + * project); the store reduces both into one id-keyed list. */ type MemoryMetadataEvent = | { @@ -592,10 +602,12 @@ function memoryFromFetch( } /** - * Fetches the memory snapshot and maps it to a success/failure action. Keeps - * only user-scoped memories (v1 surfaces user scope only; the realtime stream - * is user-scoped too), so project-scoped rows visible to the caller are - * dropped from the store. + * Fetches the memory snapshot and maps it to a success/failure action. Both + * user- and project-scoped rows are kept: the store now opens a second + * `project_meta:memories:` realtime channel (when project credentials are + * available) that keeps project rows in sync, so surfacing them in the snapshot + * no longer risks a stale list. Rows are keyed by id across both channels, so + * realtime deltas reconcile either scope idempotently. */ function createMemoryFetchObservable( environment: MemoryEnvironment, @@ -635,7 +647,7 @@ function createMemoryFetchObservable( map((data) => memoryRestEvents.listSucceeded({ sessionId, - memories: data.memories.filter((memory) => memory.scope === "user"), + memories: data.memories, }), ), catchError((error) => { @@ -684,6 +696,8 @@ function createMemoryCredentialsFetchObservable( return response.json() as Promise<{ joinToken: string; joinCode: string; + projectJoinToken?: string; + projectJoinCode?: string; }>; }, fetch: environment.fetch, @@ -705,10 +719,27 @@ function createMemoryCredentialsFetchObservable( throw new Error("missing joinCode"); } + // Project credentials are optional and only forwarded when BOTH are + // non-empty strings (the silent-degrade contract): a partial/malformed + // pair is treated as "no project scope", so the store falls back to the + // user channel only rather than erroring. A missing project scope must + // never fail credentials — the user channel is the baseline. + const hasProjectCreds = + typeof data.projectJoinToken === "string" && + data.projectJoinToken.length > 0 && + typeof data.projectJoinCode === "string" && + data.projectJoinCode.length > 0; + return memoryRestEvents.credentialsSucceeded({ sessionId, joinToken: data.joinToken, joinCode: data.joinCode, + ...(hasProjectCreds + ? { + projectJoinToken: data.projectJoinToken, + projectJoinCode: data.projectJoinCode, + } + : {}), }); }), catchError((error) => { @@ -1167,7 +1198,8 @@ function createMemoryStore(environment: MemoryEnvironment): MemoryStore { }), switchMap(([action, state]) => { const context = state.context as MemoryRuntimeContext; - const { joinToken, joinCode } = action; + const { joinToken, joinCode, projectJoinToken, projectJoinCode } = + action; const sessionId = action.sessionId; const shutdown$ = actions$.pipe( ofType( @@ -1176,6 +1208,57 @@ function createMemoryStore(environment: MemoryEnvironment): MemoryStore { ), ); + // Builds the live `memory_metadata` delta stream for one realtime + // channel (its own socket, keyed by that channel's join token). Both + // the user channel and the optional project channel feed the SAME + // reducer through this, and — critically — both stamp their deltas + // with the SAME `sessionId` (the D4 landmine): the reducer's + // superseded-context guard therefore drops stale deltas from BOTH + // channels identically the moment the context changes. The reducer's + // upsert/invalidate already key by memory id, so interleaving user + // and project rows is safe (no per-channel bookkeeping needed). + const channelMetadata$ = ( + channelJoinToken: string, + topic: string, + ): Observable< + | ReturnType + | ReturnType + > => { + const socket$ = ɵphoenixSocket$({ + url: context.wsUrl, + options: { + params: { join_token: channelJoinToken }, + reconnectAfterMs: phoenixExponentialBackoff(100, 10_000), + rejoinAfterMs: phoenixExponentialBackoff(1_000, 30_000), + }, + }).pipe(shareReplay({ bufferSize: 1, refCount: true })); + const channel$ = ɵphoenixChannel$({ socket$, topic }).pipe( + shareReplay({ bufferSize: 1, refCount: true }), + ); + const metadata$ = channel$.pipe( + switchMap(({ channel }: ɵPhoenixChannelSession) => + ɵobservePhoenixEvent$( + channel, + MEMORY_METADATA_EVENT, + ), + ), + map((event) => mapMemoryMetadataEvent(event, sessionId)), + ); + // Drive the join (see the user-channel note below) but swallow its + // outcome: the project channel deliberately does NOT emit realtime + // status deltas — `realtimeStatus` is owned by the user channel so + // a project-channel join failure never regresses the user-facing + // "live" indicator (silent degrade). On failure the metadata stream + // stays alive for Phoenix's automatic rejoin on reconnect. + const join$ = ɵjoinPhoenixChannel$(channel$).pipe( + catchError((error) => { + console.warn(`[memory] failed to join ${topic}`, error); + return EMPTY; + }), + ); + return merge(metadata$, join$); + }; + return defer(() => { const socket$ = ɵphoenixSocket$({ url: context.wsUrl, @@ -1258,6 +1341,19 @@ function createMemoryStore(environment: MemoryEnvironment): MemoryStore { }), ); + // Open the project channel alongside the user channel when project + // credentials are present. It feeds the SAME reducer with the SAME + // session stamp (see `channelMetadata$`), so project rows stay live + // without going stale. Absent project creds -> `EMPTY`: user-only, + // no second socket, no status regression (silent degrade). + const projectMetadata$ = + projectJoinToken && projectJoinCode + ? channelMetadata$( + projectJoinToken, + `project_meta:memories:${projectJoinCode}`, + ) + : EMPTY; + return merge( // Surface "connecting" immediately when the realtime stream starts // (credentials succeeded -> socket subscribing/joining). Reset to @@ -1266,6 +1362,7 @@ function createMemoryStore(environment: MemoryEnvironment): MemoryStore { of(memoryDomainEvents.realtimeConnecting({ sessionId })), metadata$, join$, + projectMetadata$, fatalStatus$, ).pipe( takeUntil( From d785693062edca3900f0f4e93d735fd45c33160a Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:16:01 -0700 Subject: [PATCH 018/456] test(web-inspector): cover Capabilities view render + toggles (A3 task 6) --- .../src/__tests__/web-inspector.spec.ts | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/packages/web-inspector/src/__tests__/web-inspector.spec.ts b/packages/web-inspector/src/__tests__/web-inspector.spec.ts index 259653cb179..a760a26c59b 100644 --- a/packages/web-inspector/src/__tests__/web-inspector.spec.ts +++ b/packages/web-inspector/src/__tests__/web-inspector.spec.ts @@ -3372,3 +3372,143 @@ describe("ɵbuildCapabilityRows", () => { expect(ɵbuildCapabilityRows({ isToolEnabled: () => false })).toEqual([]); }); }); + +describe("WebInspectorElement Capabilities tab", () => { + beforeEach(() => { + document.body.innerHTML = ""; + const store: Record = {}; + vi.stubGlobal("localStorage", { + getItem: (k: string) => store[k] ?? null, + setItem: (k: string, v: string) => { + store[k] = v; + }, + removeItem: (k: string) => { + delete store[k]; + }, + clear: () => { + for (const k of Object.keys(store)) delete store[k]; + }, + get length() { + return Object.keys(store).length; + }, + key: (i: number) => Object.keys(store)[i] ?? null, + }); + }); + + function createCapabilitiesCore() { + const toolEnabled: Record = { greet: true, hide: true }; + const catalogEnabled: Record = { Chart: true }; + const setToolEnabled = vi.fn( + (name: string, enabled: boolean, _agentId?: string) => { + toolEnabled[name] = enabled; + }, + ); + const setCatalogComponentEnabled = vi.fn( + (name: string, enabled: boolean) => { + catalogEnabled[name] = enabled; + }, + ); + const core = { + agents: {}, + context: {}, + properties: {}, + runtimeConnectionStatus: + CopilotKitCoreRuntimeConnectionStatus.Connected, + subscribe: () => ({ unsubscribe: () => undefined }), + getThreadStores: () => ({}), + getThreadStore: () => undefined, + getMemoryStore: () => ({ + getState: () => ({ available: true }), + select: () => ({ + subscribe: (cb: (v: unknown) => void) => { + cb(undefined); + return { unsubscribe: () => undefined }; + }, + }), + }), + tools: [{ name: "greet", description: "Say hi" }, { name: "hide" }], + isToolEnabled: (name: string) => toolEnabled[name] ?? true, + setToolEnabled, + catalogComponents: [{ name: "Chart", schema: {} }], + isCatalogComponentEnabled: (name: string) => catalogEnabled[name] ?? true, + setCatalogComponentEnabled, + }; + return { core, setToolEnabled, setCatalogComponentEnabled }; + } + + it("shows the Capabilities tab and renders both sections", async () => { + const { core } = createCapabilitiesCore(); + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + (inspector as unknown as { isOpen: boolean }).isOpen = true; + ( + inspector as unknown as { handleMenuSelect: (k: string) => void } + ).handleMenuSelect("capabilities"); + await inspector.updateComplete; + const text = inspector.shadowRoot?.textContent ?? ""; + expect(text).toContain("Frontend tools"); + expect(text).toContain("A2UI catalog components"); + expect(text).toContain("greet"); + expect(text).toContain("Chart"); + }); + + it("calls setToolEnabled(false) when a tool switch is toggled off", async () => { + const { core, setToolEnabled } = createCapabilitiesCore(); + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + (inspector as unknown as { isOpen: boolean }).isOpen = true; + ( + inspector as unknown as { handleMenuSelect: (k: string) => void } + ).handleMenuSelect("capabilities"); + await inspector.updateComplete; + const switches = + inspector.shadowRoot?.querySelectorAll( + 'button[role="switch"]', + ) ?? []; + switches[0]?.click(); + await inspector.updateComplete; + expect(setToolEnabled).toHaveBeenCalledWith("greet", false, undefined); + const refreshed = + inspector.shadowRoot?.querySelectorAll( + 'button[role="switch"]', + ) ?? []; + expect(refreshed[0]?.getAttribute("aria-checked")).toBe("false"); + }); + + it("calls setCatalogComponentEnabled when a catalog switch is toggled", async () => { + const { core, setCatalogComponentEnabled } = createCapabilitiesCore(); + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + (inspector as unknown as { isOpen: boolean }).isOpen = true; + ( + inspector as unknown as { handleMenuSelect: (k: string) => void } + ).handleMenuSelect("capabilities"); + await inspector.updateComplete; + const switches = + inspector.shadowRoot?.querySelectorAll( + 'button[role="switch"]', + ) ?? []; + switches[switches.length - 1]?.click(); + await inspector.updateComplete; + expect(setCatalogComponentEnabled).toHaveBeenCalledWith("Chart", false); + }); + + it("hides the catalog section when catalogComponents is empty", async () => { + const { core } = createCapabilitiesCore(); + (core as { catalogComponents: unknown[] }).catalogComponents = []; + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + (inspector as unknown as { isOpen: boolean }).isOpen = true; + ( + inspector as unknown as { handleMenuSelect: (k: string) => void } + ).handleMenuSelect("capabilities"); + await inspector.updateComplete; + const text = inspector.shadowRoot?.textContent ?? ""; + expect(text).toContain("Frontend tools"); + expect(text).not.toContain("A2UI catalog components"); + }); +}); From 0b14899dbba8a1519d5b4fe3abc9f8e2ed9b0e6b Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:17:14 -0700 Subject: [PATCH 019/456] feat(web-inspector): mark fired capabilities with an active dot (A3 task 7, optional) --- .../src/__tests__/web-inspector.spec.ts | 18 ++++++++++++++++++ packages/web-inspector/src/index.ts | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/web-inspector/src/__tests__/web-inspector.spec.ts b/packages/web-inspector/src/__tests__/web-inspector.spec.ts index a760a26c59b..bd2321570e7 100644 --- a/packages/web-inspector/src/__tests__/web-inspector.spec.ts +++ b/packages/web-inspector/src/__tests__/web-inspector.spec.ts @@ -3511,4 +3511,22 @@ describe("WebInspectorElement Capabilities tab", () => { expect(text).toContain("Frontend tools"); expect(text).not.toContain("A2UI catalog components"); }); + + it("marks a tool row as fired after its tool-call event", async () => { + const { core } = createCapabilitiesCore(); + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + (inspector as unknown as { isOpen: boolean }).isOpen = true; + ( + inspector as unknown as { firedCapabilities: Set } + ).firedCapabilities.add(":greet"); + ( + inspector as unknown as { handleMenuSelect: (k: string) => void } + ).handleMenuSelect("capabilities"); + await inspector.updateComplete; + expect( + inspector.shadowRoot?.querySelector('[title="Fired this session"]'), + ).not.toBeNull(); + }); }); diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 762ae428a2b..099d4406282 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -4971,6 +4971,15 @@ export class WebInspectorElement extends LitElement { toolCallName, partialToolCallArgs, }); + if (typeof toolCallName === "string" && toolCallName.length > 0) { + const before = this.firedCapabilities.size; + this.firedCapabilities.add(`${agentId}:${toolCallName}`); + this.firedCapabilities.add(toolCallName); + if (this.firedCapabilities.size !== before) { + this._capabilitiesVersion += 1; + this.requestUpdate(); + } + } }, onToolCallEndEvent: ({ event, toolCallArgs, toolCallName }) => { this.recordAgentEvent(agentId, "TOOL_CALL_END", { From 23a56400bc1f3196fb12e3774dc40583b79c0f42 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:17:45 -0700 Subject: [PATCH 020/456] chore(web-inspector): regenerate Tailwind CSS for Capabilities tab utilities (A3) --- packages/web-inspector/src/styles/generated.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-inspector/src/styles/generated.css b/packages/web-inspector/src/styles/generated.css index 921d30af283..468549ca508 100644 --- a/packages/web-inspector/src/styles/generated.css +++ b/packages/web-inspector/src/styles/generated.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-green-100:oklch(96.2% .044 156.743);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-700:oklch(50% .134 242.749);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-700:oklch(49.1% .27 292.581);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-700:oklch(49.6% .265 301.924);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-700:oklch(51.4% .222 16.935);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--font-weight-medium:500;--font-weight-semibold:600;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:host{font-family:var(--font-sans);color:var(--color-slate-900);background-color:#0000;display:block}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.bottom-1{bottom:calc(var(--spacing)*1)}.left-0{left:calc(var(--spacing)*0)}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-0{margin:calc(var(--spacing)*0)}.mx-4{margin-inline:calc(var(--spacing)*4)}.my-1{margin-block:calc(var(--spacing)*1)}.my-3{margin-block:calc(var(--spacing)*3)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing)*64)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-40{width:calc(var(--spacing)*40)}.w-auto{width:auto}.w-full{width:100%}.max-w-\[240px\]{max-width:240px}.max-w-md{max-width:var(--container-md)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[160px\]{min-width:160px}.min-w-\[200px\]{min-width:200px}.flex-1{flex:1}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.border-collapse{border-collapse:collapse}.-translate-y-\[2px\]{--tw-translate-y:calc(2px*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-blue-200{border-color:var(--color-blue-200)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-fuchsia-200{border-color:var(--color-fuchsia-200)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-rose-200{border-color:var(--color-rose-200)}.border-sky-200{border-color:var(--color-sky-200)}.border-slate-200{border-color:var(--color-slate-200)}.border-violet-200{border-color:var(--color-violet-200)}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-fuchsia-50{background-color:var(--color-fuchsia-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-50\/50{background-color:#f9fafb80}@supports (color:color-mix(in lab, red, red)){.bg-gray-50\/50{background-color:color-mix(in oklab,var(--color-gray-50)50%,transparent)}}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-100{background-color:var(--color-green-100)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950\/95{background-color:#020618f2}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/95{background-color:color-mix(in oklab,var(--color-slate-950)95%,transparent)}}.bg-violet-50{background-color:var(--color-violet-50)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white)60%,transparent)}}.bg-white\/95{background-color:#fffffff2}@supports (color:color-mix(in lab, red, red)){.bg-white\/95{background-color:color-mix(in oklab,var(--color-white)95%,transparent)}}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-emerald-700{color:var(--color-emerald-700)}.text-fuchsia-700{color:var(--color-fuchsia-700)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-700{color:var(--color-purple-700)}.text-rose-700{color:var(--color-rose-700)}.text-sky-700{color:var(--color-sky-700)}.text-slate-500{color:var(--color-slate-500)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-violet-700{color:var(--color-violet-700)}.text-white{color:var(--color-white)}.italic{font-style:italic}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab,var(--color-black)5%,transparent)}}.ring-transparent{--tw-ring-color:transparent}.ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.ring-white\/10{--tw-ring-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-white\/30:hover{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/30:hover{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.hover\:bg-blue-50\/50:hover{background-color:#eff6ff80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-blue-50\/50:hover{background-color:color-mix(in oklab,var(--color-blue-50)50%,transparent)}}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-slate-900\/95:hover{background-color:#0f172bf2}@supports (color:color-mix(in lab, red, red)){.hover\:bg-slate-900\/95:hover{background-color:color-mix(in oklab,var(--color-slate-900)95%,transparent)}}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-slate-900:hover{color:var(--color-slate-900)}}.focus\:border-gray-300:focus{border-color:var(--color-gray-300)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-\[\#BEC2FF\]:focus-visible{outline-color:#bec2ff}.focus-visible\:outline-gray-300:focus-visible{outline-color:var(--color-gray-300)}.focus-visible\:outline-gray-400:focus-visible{outline-color:var(--color-gray-400)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:48rem){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\>svg\]\:\!h-8>svg{height:calc(var(--spacing)*8)!important}.\[\&\>svg\]\:\!w-8>svg{width:calc(var(--spacing)*8)!important}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-green-100:oklch(96.2% .044 156.743);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-700:oklch(50% .134 242.749);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-700:oklch(49.1% .27 292.581);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-700:oklch(49.6% .265 301.924);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-700:oklch(51.4% .222 16.935);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--font-weight-medium:500;--font-weight-semibold:600;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:host{font-family:var(--font-sans);color:var(--color-slate-900);background-color:#0000;display:block}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.bottom-1{bottom:calc(var(--spacing)*1)}.left-0{left:calc(var(--spacing)*0)}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-0{margin:calc(var(--spacing)*0)}.mx-4{margin-inline:calc(var(--spacing)*4)}.my-1{margin-block:calc(var(--spacing)*1)}.my-3{margin-block:calc(var(--spacing)*3)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2{height:calc(var(--spacing)*2)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing)*64)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-2{width:calc(var(--spacing)*2)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-40{width:calc(var(--spacing)*40)}.w-auto{width:auto}.w-full{width:100%}.max-w-\[240px\]{max-width:240px}.max-w-md{max-width:var(--container-md)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[160px\]{min-width:160px}.min-w-\[200px\]{min-width:200px}.flex-1{flex:1}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.border-collapse{border-collapse:collapse}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-\[2px\]{--tw-translate-y:calc(2px*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-blue-200{border-color:var(--color-blue-200)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-fuchsia-200{border-color:var(--color-fuchsia-200)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-rose-200{border-color:var(--color-rose-200)}.border-sky-200{border-color:var(--color-sky-200)}.border-slate-200{border-color:var(--color-slate-200)}.border-violet-200{border-color:var(--color-violet-200)}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-fuchsia-50{background-color:var(--color-fuchsia-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-50\/50{background-color:#f9fafb80}@supports (color:color-mix(in lab, red, red)){.bg-gray-50\/50{background-color:color-mix(in oklab,var(--color-gray-50)50%,transparent)}}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-100{background-color:var(--color-green-100)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950\/95{background-color:#020618f2}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/95{background-color:color-mix(in oklab,var(--color-slate-950)95%,transparent)}}.bg-violet-50{background-color:var(--color-violet-50)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white)60%,transparent)}}.bg-white\/95{background-color:#fffffff2}@supports (color:color-mix(in lab, red, red)){.bg-white\/95{background-color:color-mix(in oklab,var(--color-white)95%,transparent)}}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-emerald-700{color:var(--color-emerald-700)}.text-fuchsia-700{color:var(--color-fuchsia-700)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-700{color:var(--color-purple-700)}.text-rose-700{color:var(--color-rose-700)}.text-sky-700{color:var(--color-sky-700)}.text-slate-500{color:var(--color-slate-500)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-violet-700{color:var(--color-violet-700)}.text-white{color:var(--color-white)}.italic{font-style:italic}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab,var(--color-black)5%,transparent)}}.ring-transparent{--tw-ring-color:transparent}.ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.ring-white\/10{--tw-ring-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-white\/30:hover{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/30:hover{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.hover\:bg-blue-50\/50:hover{background-color:#eff6ff80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-blue-50\/50:hover{background-color:color-mix(in oklab,var(--color-blue-50)50%,transparent)}}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-slate-900\/95:hover{background-color:#0f172bf2}@supports (color:color-mix(in lab, red, red)){.hover\:bg-slate-900\/95:hover{background-color:color-mix(in oklab,var(--color-slate-900)95%,transparent)}}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-slate-900:hover{color:var(--color-slate-900)}}.focus\:border-gray-300:focus{border-color:var(--color-gray-300)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-\[\#BEC2FF\]:focus-visible{outline-color:#bec2ff}.focus-visible\:outline-gray-300:focus-visible{outline-color:var(--color-gray-300)}.focus-visible\:outline-gray-400:focus-visible{outline-color:var(--color-gray-400)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:48rem){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\>svg\]\:\!h-8>svg{height:calc(var(--spacing)*8)!important}.\[\&\>svg\]\:\!w-8>svg{width:calc(var(--spacing)*8)!important}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file From 895e2dd2d80326ee73b5357589e4cbff3a54d4f2 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:19:28 -0700 Subject: [PATCH 021/456] feat(react-core): enforce A2UI catalog component toggling on both context and render paths Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/v2/providers/CopilotKitProvider.tsx | 65 ++++++++- .../copilotkit-provider-catalog.test.tsx | 129 ++++++++++++++++++ 2 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 packages/react-core/src/v2/providers/__tests__/copilotkit-provider-catalog.test.tsx diff --git a/packages/react-core/src/v2/providers/CopilotKitProvider.tsx b/packages/react-core/src/v2/providers/CopilotKitProvider.tsx index b0ade0dddfd..e27f93f8efd 100644 --- a/packages/react-core/src/v2/providers/CopilotKitProvider.tsx +++ b/packages/react-core/src/v2/providers/CopilotKitProvider.tsx @@ -44,7 +44,7 @@ import { createA2UIMessageRenderer } from "../a2ui/A2UIMessageRenderer"; import type { A2UIRecoveryRendererOptions } from "../a2ui/A2UIRecoveryStates"; import { A2UIBuiltInToolCallRenderer } from "../a2ui/A2UIToolCallRenderer"; import { A2UICatalogContext } from "../a2ui/A2UICatalogContext"; -import { viewerTheme } from "@copilotkit/a2ui-renderer"; +import { viewerTheme, filterCatalog, Catalog } from "@copilotkit/a2ui-renderer"; import type { Theme as A2UITheme } from "@copilotkit/a2ui-renderer"; import { CopilotKitCoreReact } from "../lib/react-core"; import type { @@ -296,6 +296,9 @@ export const CopilotKitProvider: React.FC = ({ const [shouldRenderInspector, setShouldRenderInspector] = useState(false); const [runtimeA2UIEnabled, setRuntimeA2UIEnabled] = useState(false); const [runtimeOpenGenUIEnabled, setRuntimeOpenGenUIEnabled] = useState(false); + // Bumped by onCatalogComponentsChanged so the filtered catalog re-derives + // when a component is enabled/disabled after mount. + const [catalogToggleVersion, setCatalogToggleVersion] = useState(0); const openGenUIActive = runtimeOpenGenUIEnabled || !!openGenerativeUI; // A catalog passed to the provider is enough to turn A2UI on: render the // surfaces locally and forward the catalog signal so the runtime injects the @@ -357,6 +360,34 @@ export const CopilotKitProvider: React.FC = ({ ReactActivityMessageRenderer >(renderActivityMessages, "renderActivityMessages must be a stable array."); + // Stable core instance ref, declared here (before the activity-renderer memo) + // so the filtered-catalog memo can read enablement off the current instance. + // The instance itself is created in the ref-init block below; on the very + // first render `current` is still null (nothing is disabled yet), so the + // filtered catalog correctly equals the full catalog. + const copilotkitRef = useRef(null); + + // Raw catalog provided by the caller (typed loosely to match `a2ui.catalog?: any`). + const rawCatalog = a2ui?.catalog as Catalog | undefined; + + // Derive the FILTERED catalog: only components currently enabled on core. + // Re-derives when enablement changes (catalogToggleVersion bump from the + // onCatalogComponentsChanged subscription below). Passed to BOTH the render + // path (createA2UIMessageRenderer) and the advertisement path (A2UICatalogContext) + // so a disabled component vanishes from what the model sees AND what can paint. + const filteredCatalog = useMemo(() => { + if (!rawCatalog) return rawCatalog; + // `filterCatalog` rebuilds a real `Catalog` (reads `.functions`). Callers may + // also pass a minimal catalog-shaped object; only filter genuine instances. + if (!(rawCatalog instanceof Catalog)) return rawCatalog; + const core = copilotkitRef.current; + return filterCatalog(rawCatalog, (name) => + core ? core.isCatalogComponentEnabled(name) : true, + ); + // catalogToggleVersion forces re-derivation when enablement changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rawCatalog, catalogToggleVersion]); + // Built-in activity renderers that are always included const builtInActivityRenderers = useMemo< ReactActivityMessageRenderer[] @@ -385,7 +416,7 @@ export const CopilotKitProvider: React.FC = ({ renderers.unshift( createA2UIMessageRenderer({ theme: a2ui?.theme ?? viewerTheme, - catalog: a2ui?.catalog, + catalog: filteredCatalog, loadingComponent: a2ui?.loadingComponent, recovery: a2ui?.recovery, }), @@ -393,7 +424,7 @@ export const CopilotKitProvider: React.FC = ({ } return renderers; - }, [a2uiActive, openGenUIActive, a2ui]); + }, [a2uiActive, openGenUIActive, a2ui, filteredCatalog]); // Combine user-provided activity renderers with built-in ones // User-provided renderers take precedence (come first) so they can override built-ins @@ -569,9 +600,9 @@ export const CopilotKitProvider: React.FC = ({ processedHumanInTheLoopTools, ]); - // Stable instance: created once for the provider lifetime. + // Stable instance: created once for the provider lifetime (ref declared above + // so the filtered-catalog memo can read enablement off it). // Updates are applied via setter effects below rather than recreating the instance. - const copilotkitRef = useRef(null); if (copilotkitRef.current === null) { copilotkitRef.current = new CopilotKitCoreReact({ runtimeUrl: chatApiEndpoint, @@ -605,6 +636,28 @@ export const CopilotKitProvider: React.FC = ({ } const copilotkit = copilotkitRef.current; + // Register the full A2UI catalog component list onto core so the inspector can + // read `core.catalogComponents`, and re-derive the filtered catalog whenever + // enablement changes. Descriptions are undefined here because the built + // ComponentApi does not carry them (see plan A2 rationale). + useEffect(() => { + if (!rawCatalog) return; + const components = Array.from(rawCatalog.components.values()).map( + (comp: { name: string; schema: unknown }) => ({ + name: comp.name, + description: undefined as string | undefined, + schema: comp.schema, + }), + ); + copilotkit.setCatalogComponents(components); + const subscription = copilotkit.subscribe({ + onCatalogComponentsChanged: () => { + setCatalogToggleVersion((v) => v + 1); + }, + }); + return () => subscription.unsubscribe(); + }, [copilotkit, rawCatalog]); + // Sync runtime feature flags from the core once runtime info is fetched. // // The core kicks off its `/info` fetch synchronously from its constructor @@ -853,7 +906,7 @@ export const CopilotKitProvider: React.FC = ({ {a2uiActive && } {a2uiActive && ( )} diff --git a/packages/react-core/src/v2/providers/__tests__/copilotkit-provider-catalog.test.tsx b/packages/react-core/src/v2/providers/__tests__/copilotkit-provider-catalog.test.tsx new file mode 100644 index 00000000000..a21c736a14d --- /dev/null +++ b/packages/react-core/src/v2/providers/__tests__/copilotkit-provider-catalog.test.tsx @@ -0,0 +1,129 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, act } from "@testing-library/react"; +import { z } from "zod"; +import { Catalog } from "@copilotkit/a2ui-renderer"; + +// `@a2ui/web_core` is not a direct dependency of react-core, so we mirror the +// minimal `ComponentApi` shape the built catalog carries locally (schema is a +// Zod schema, matching the real `ComponentApi` constraint on `Catalog`). +type ComponentApi = { name: string; schema: z.ZodType }; + +// Capture the catalog handed to the render path. The provider imports +// `createA2UIMessageRenderer` from `../a2ui/A2UIMessageRenderer` (not from the +// package), so that is the module to stub. +const rendererCatalogs: Array | undefined> = []; +vi.mock("../../a2ui/A2UIMessageRenderer", () => ({ + createA2UIMessageRenderer: (opts: { catalog?: Catalog }) => { + rendererCatalogs.push(opts?.catalog); + return { + activityType: "a2ui-surface", + content: z.object({}), + render: () => null, + }; + }, +})); + +// Capture the catalog handed to the advertisement path. +const contextCatalogs: Array | undefined> = []; +vi.mock("../../a2ui/A2UICatalogContext", () => ({ + A2UICatalogContext: ({ catalog }: { catalog?: Catalog }) => { + contextCatalogs.push(catalog); + return null; + }, +})); + +import { CopilotKitProvider, useCopilotKit } from "../CopilotKitProvider"; +import type { CopilotKitCore } from "@copilotkit/core"; + +function makeCatalog(): Catalog { + const components: ComponentApi[] = [ + { name: "PieChart", schema: z.object({ innerRadius: z.number().optional() }) }, + { name: "FlightCard", schema: z.object({ airline: z.string() }) }, + { name: "Badge", schema: z.object({ text: z.string() }) }, + ]; + return new Catalog("copilotkit://custom-catalog", components, []); +} + +let capturedCore: CopilotKitCore | null = null; +function CoreCapture() { + const { copilotkit } = useCopilotKit(); + capturedCore = copilotkit; + return null; +} + +describe("CopilotKitProvider A2UI catalog toggling", () => { + beforeEach(() => { + rendererCatalogs.length = 0; + contextCatalogs.length = 0; + capturedCore = null; + }); + + it("registers all catalog components onto core", () => { + render( + + + , + ); + expect(capturedCore!.catalogComponents.map((c) => c.name).sort()).toEqual([ + "Badge", + "FlightCard", + "PieChart", + ]); + }); + + it("passes the FULL catalog to both paths when nothing is disabled", () => { + render( + + + , + ); + const lastRenderer = rendererCatalogs.at(-1)!; + const lastContext = contextCatalogs.at(-1)!; + expect(lastRenderer.components.has("FlightCard")).toBe(true); + expect(lastContext.components.has("FlightCard")).toBe(true); + }); + + it("removes a disabled component from BOTH the render and advertisement catalogs", () => { + render( + + + , + ); + act(() => { + capturedCore!.setCatalogComponentEnabled("FlightCard", false); + }); + const lastRenderer = rendererCatalogs.at(-1)!; + const lastContext = contextCatalogs.at(-1)!; + expect(lastRenderer.components.has("FlightCard")).toBe(false); + expect(lastRenderer.components.has("PieChart")).toBe(true); + expect(lastContext.components.has("FlightCard")).toBe(false); + expect(lastContext.components.has("PieChart")).toBe(true); + }); + + it("does not register components when no catalog is provided", () => { + render( + + + , + ); + expect(capturedCore!.catalogComponents).toHaveLength(0); + }); + + it("re-enabling a component restores it on both paths", () => { + render( + + + , + ); + act(() => { + capturedCore!.setCatalogComponentEnabled("FlightCard", false); + }); + act(() => { + capturedCore!.setCatalogComponentEnabled("FlightCard", true); + }); + const lastRenderer = rendererCatalogs.at(-1)!; + const lastContext = contextCatalogs.at(-1)!; + expect(lastRenderer.components.has("FlightCard")).toBe(true); + expect(lastContext.components.has("FlightCard")).toBe(true); + }); +}); From 7308f340555775690900af3f4d0429ee4b357b55 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:55:10 -0700 Subject: [PATCH 022/456] feat(web-inspector): rename Learning tab to Memory (OSS memory B3) Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web-inspector/src/index.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 099d4406282..caf5445dbe6 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -90,6 +90,13 @@ export type { CapabilityToolRow as ɵCapabilityToolRow }; export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const; export const THREAD_INSPECTOR_TAG = "cpk-thread-inspector" as const; +/** + * User-facing label for the memory surface (nav item + view header). The menu + * KEY stays "memories" for persistence/telemetry stability; only the label + * changed from "Learning" to "Memory". + */ +const MEMORY_VIEW_LABEL = "Memory"; + type LucideIconName = keyof typeof icons; type MenuKey = @@ -4375,7 +4382,11 @@ export class WebInspectorElement extends LitElement { label: "Threads", icon: "MessageSquare" as LucideIconName, }, - { key: "memories", label: "Learning", icon: "Brain" as LucideIconName }, + { + key: "memories", + label: MEMORY_VIEW_LABEL, + icon: "Brain" as LucideIconName, + }, ]; } @@ -9077,7 +9088,7 @@ ${argsString}
-

Learning

+

${MEMORY_VIEW_LABEL}

${this.renderMemoryRealtimeIndicator()} Date: Tue, 14 Jul 2026 14:57:14 -0700 Subject: [PATCH 023/456] feat(web-inspector): add pure recall relevance helpers + tests (OSS memory B3) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/__tests__/memory-recall.spec.ts | 72 +++++++++++++++++++ .../src/__tests__/web-inspector.spec.ts | 8 +-- packages/web-inspector/src/index.ts | 45 ++++++++++++ 3 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 packages/web-inspector/src/__tests__/memory-recall.spec.ts diff --git a/packages/web-inspector/src/__tests__/memory-recall.spec.ts b/packages/web-inspector/src/__tests__/memory-recall.spec.ts new file mode 100644 index 00000000000..c788341e3c8 --- /dev/null +++ b/packages/web-inspector/src/__tests__/memory-recall.spec.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from "vitest"; +import type { Memory } from "@copilotkit/core"; +import { + ɵnormalizeRelevance, + ɵmaxRecallScore, + ɵrelevanceBarWidth, +} from "../index.js"; + +function mem(id: string, score?: number): Memory { + return { + id, + kind: "topical", + scope: "user", + content: `content ${id}`, + sourceThreadIds: [], + invalidatedAt: null, + ...(score !== undefined ? { score } : {}), + } as Memory; +} + +describe("ɵmaxRecallScore", () => { + it("returns 0 for an empty set", () => { + expect(ɵmaxRecallScore([])).toBe(0); + }); + it("returns 0 when no memory carries a score", () => { + expect(ɵmaxRecallScore([mem("a"), mem("b")])).toBe(0); + }); + it("returns the largest finite score", () => { + expect(ɵmaxRecallScore([mem("a", 0.2), mem("b", 0.9), mem("c", 0.5)])).toBe( + 0.9, + ); + }); + it("ignores non-finite scores", () => { + expect( + ɵmaxRecallScore([mem("a", Number.NaN), mem("b", Infinity), mem("c", 0.3)]), + ).toBe(0.3); + }); +}); + +describe("ɵnormalizeRelevance", () => { + it("returns undefined when maxScore is non-positive", () => { + expect(ɵnormalizeRelevance(0.5, 0)).toBeUndefined(); + expect(ɵnormalizeRelevance(0.5, -1)).toBeUndefined(); + }); + it("returns undefined when the score is missing or non-finite", () => { + expect(ɵnormalizeRelevance(undefined, 1)).toBeUndefined(); + expect(ɵnormalizeRelevance(Number.NaN, 1)).toBeUndefined(); + }); + it("normalizes against the set max", () => { + expect(ɵnormalizeRelevance(0.45, 0.9)).toBeCloseTo(0.5, 5); + expect(ɵnormalizeRelevance(0.9, 0.9)).toBe(1); + }); + it("clamps into [0, 1]", () => { + expect(ɵnormalizeRelevance(2, 1)).toBe(1); + expect(ɵnormalizeRelevance(-0.3, 1)).toBe(0); + }); +}); + +describe("ɵrelevanceBarWidth", () => { + it("floors at 6 for weak-but-matched results", () => { + expect(ɵrelevanceBarWidth(0)).toBe(6); + expect(ɵrelevanceBarWidth(0.01)).toBe(6); + }); + it("rounds the percentage", () => { + expect(ɵrelevanceBarWidth(0.5)).toBe(50); + expect(ɵrelevanceBarWidth(0.734)).toBe(73); + }); + it("caps at 100", () => { + expect(ɵrelevanceBarWidth(1)).toBe(100); + expect(ɵrelevanceBarWidth(1.5)).toBe(100); + }); +}); diff --git a/packages/web-inspector/src/__tests__/web-inspector.spec.ts b/packages/web-inspector/src/__tests__/web-inspector.spec.ts index bd2321570e7..4031b804463 100644 --- a/packages/web-inspector/src/__tests__/web-inspector.spec.ts +++ b/packages/web-inspector/src/__tests__/web-inspector.spec.ts @@ -2296,7 +2296,7 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => { // // 6.1 Helpers: makeCoreWithMemory / makeCoreNoIntelligence / mountMemories // 6.2 Subscription: inspector._memories is seeded from store -// 6.3 Tab presence: "Learning" label appears in the rendered menu +// 6.3 Tab presence: "Memory" label appears in the rendered menu // 6.4 View states: locked teaser vs. enabled empty vs. enabled with cards // 6.5 cpk-memory-list: cards, kind filter, search filter, empty state // 6.6 Passive guard: inspector reads from core.getMemoryStore(), never creates its own @@ -2515,7 +2515,7 @@ describe("WebInspectorElement memories — tab presence", () => { vi.unstubAllGlobals(); }); - it("renders a Learning tab button in the inspector menu", async () => { + it("renders a Memory tab button in the inspector menu", async () => { const core = makeCoreWithMemory([]); const el = await mountMemories(core); @@ -2523,10 +2523,10 @@ describe("WebInspectorElement memories — tab presence", () => { el.shadowRoot?.querySelectorAll("button") ?? [], ); const memoriesButton = buttons.find((btn) => - btn.textContent?.trim().includes("Learning"), + btn.textContent?.trim().includes("Memory"), ); - expect(memoriesButton, "Learning tab button should render").toBeDefined(); + expect(memoriesButton, "Memory tab button should render").toBeDefined(); }); }); diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index caf5445dbe6..cb792ffe1d8 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -3762,6 +3762,51 @@ ${unsafeHTML(highlightedJson(stateValue))} 1 ? 1 : ratio; +} + +/** Largest finite `score` across a result set, or 0 when none present. */ +function maxRecallScore(memories: readonly Memory[]): number { + let max = 0; + for (const m of memories) { + const s = m.score; + if (typeof s === "number" && Number.isFinite(s) && s > max) max = s; + } + return max; +} + +/** + * Percent width for a relevance bar. Mirrors the banking reference + * (`max(6, round(rel*100))%`) so a matched-but-weak result still shows a sliver. + * Returns a whole number in [6, 100]. + */ +function relevanceBarWidth(relevance: number): number { + return Math.max(6, Math.min(100, Math.round(relevance * 100))); +} + +export { + normalizeRelevance as ɵnormalizeRelevance, + maxRecallScore as ɵmaxRecallScore, + relevanceBarWidth as ɵrelevanceBarWidth, +}; + // ─── cpk-memory-list ───────────────────────────────────────────────────────── /** Memory kind values including the "all" sentinel used by the filter UI. */ From a8727a49369a740af1db2b6ad1f2db5978f30467 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:58:45 -0700 Subject: [PATCH 024/456] feat(web-inspector): recall section, relevance bars, scope badges in CpkMemoryList (OSS memory B3) Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web-inspector/src/index.ts | 269 ++++++++++++++++++++++++++-- 1 file changed, 252 insertions(+), 17 deletions(-) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index cb792ffe1d8..5911d38ec52 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -3815,12 +3815,24 @@ type MemoryKindFilter = "all" | "topical" | "episodic" | "operational"; class CpkMemoryList extends LitElement { static properties = { memories: { attribute: false }, + recallResults: { attribute: false }, + recallLoading: { attribute: false }, + recallError: { attribute: false }, + recallQueryText: { attribute: false }, search: { state: true }, kind: { state: true }, }; /** Ordered (newest-first) list of memories supplied by the parent. */ memories: Memory[] = []; + /** Semantic-recall results. `null` = no recall run (section hidden); `[]` = ran, no matches. */ + recallResults: Memory[] | null = null; + /** True while a recall request is in flight. */ + recallLoading = false; + /** Error message from the most recent recall attempt, or null. */ + recallError: string | null = null; + /** The recall input text (owned by the parent). */ + recallQueryText = ""; private search = ""; private kind: MemoryKindFilter = "all"; @@ -4022,6 +4034,117 @@ class CpkMemoryList extends LitElement { .cpk-ml__empty-icon { color: #c0c0c8; } + + /* ── Recall ── */ + .cpk-ml__recall { + display: flex; + gap: 6px; + padding: 10px 12px; + border-bottom: 1px solid #dbdbe5; + flex-shrink: 0; + } + .cpk-ml__recall-input { + flex: 1; + box-sizing: border-box; + font-family: "Plus Jakarta Sans", sans-serif; + font-size: 12px; + padding: 7px 10px; + border-radius: 6px; + border: 1px solid #dbdbe5; + background: #fff; + color: #010507; + outline: none; + transition: border-color 0.15s; + } + .cpk-ml__recall-input:focus { + border-color: #bec2ff; + } + .cpk-ml__recall-btn { + font-family: "Plus Jakarta Sans", sans-serif; + font-size: 12px; + font-weight: 500; + padding: 7px 12px; + border-radius: 6px; + border: 1px solid #dbdbe5; + background: #fff; + color: #010507; + cursor: pointer; + transition: background 0.1s; + } + .cpk-ml__recall-btn:hover:not(:disabled) { + background: #f0f0f5; + } + .cpk-ml__recall-btn:disabled { + opacity: 0.4; + cursor: default; + } + .cpk-ml__recall-section { + flex-shrink: 0; + max-height: 45%; + overflow-y: auto; + padding: 8px 12px; + border-bottom: 1px solid #dbdbe5; + background: #fbfbfd; + display: flex; + flex-direction: column; + gap: 8px; + } + .cpk-ml__recall-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .cpk-ml__recall-title { + font-family: "Plus Jakarta Sans", sans-serif; + font-size: 12px; + font-weight: 600; + color: #010507; + } + .cpk-ml__recall-clear { + font-family: "Plus Jakarta Sans", sans-serif; + font-size: 10px; + color: #838389; + background: none; + border: none; + cursor: pointer; + padding: 0; + } + .cpk-ml__recall-clear:hover { + color: #010507; + } + .cpk-ml__recall-msg { + font-size: 11px; + color: #838389; + line-height: 1.45; + } + .cpk-ml__recall-msg--error { + color: #c0333a; + } + + /* ── Relevance bar ── */ + .cpk-ml__relevance { + height: 4px; + width: 100%; + overflow: hidden; + border-radius: 9999px; + background: #f0f0f5; + } + .cpk-ml__relevance-fill { + height: 100%; + border-radius: 9999px; + background: #6366f1; + } + + /* ── Scope badge variants ── */ + .cpk-ml__scope-badge--user { + background: #f0f0f5; + color: #838389; + } + .cpk-ml__scope-badge--project { + background: #fef3c7; + color: #92660c; + } `; /** Memories that pass the current text search (before kind filter). */ @@ -4066,6 +4189,131 @@ class CpkMemoryList extends LitElement { >`; } + private renderScopeBadge(scope: string): TemplateResult { + const variant = scope === "project" ? "project" : "user"; + return html`${scope}`; + } + + /** + * Renders one memory card. `relevance` (0..1) is supplied only for recall + * results — when present a relevance bar is drawn; the full list omits it. + */ + private renderCard(m: Memory, relevance?: number): TemplateResult { + const threads = m.sourceThreadIds.length; + return html` +
+
+ ${this.renderKindBadge(m.kind)}${this.renderScopeBadge(m.scope)} +
+
${m.content}
+ ${relevance !== undefined + ? html`
+
+
` + : nothing} + +
+ `; + } + + private onRecallInput = (event: Event): void => { + const value = (event.target as HTMLInputElement).value; + this.recallQueryText = value; + this.dispatchEvent( + new CustomEvent("recallQueryChanged", { + detail: value, + bubbles: true, + composed: true, + }), + ); + }; + + private onRecallSubmit = (event: Event): void => { + event.preventDefault(); + const query = this.recallQueryText.trim(); + if (query.length === 0 || this.recallLoading) return; + this.dispatchEvent( + new CustomEvent("recallSubmitted", { + detail: query, + bubbles: true, + composed: true, + }), + ); + }; + + private onRecallClear = (): void => { + this.dispatchEvent( + new CustomEvent("recallCleared", { bubbles: true, composed: true }), + ); + }; + + private renderRecallForm(): TemplateResult { + const disabled = + this.recallLoading || this.recallQueryText.trim().length === 0; + return html` +
+ + +
+ `; + } + + private renderRecallSection(): TemplateResult { + const results = this.recallResults; + if (results === null) return html``; + const max = maxRecallScore(results); + return html` +
+
+ Semantic recall (${results.length}) + +
+ ${this.recallError + ? html`

+ Recall failed: ${this.recallError} +

` + : results.length === 0 + ? html`

+ No memories matched that query. +

` + : results.map((m) => + this.renderCard(m, normalizeRelevance(m.score, max)), + )} +
+ `; + } + private renderEmpty(): TemplateResult { const q = this.search.trim(); if (this.memories.length === 0) { @@ -4112,6 +4360,9 @@ class CpkMemoryList extends LitElement { return html`
+ + ${this.renderRecallForm()} ${this.renderRecallSection()} + From 7f7e39b4b733b0698ad1905c9e2c7aca51fb066b Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 14:59:59 -0700 Subject: [PATCH 025/456] feat(web-inspector): wire memory-store recall into memory view recall UI (OSS memory B3) Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web-inspector/src/index.ts | 81 +++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 5911d38ec52..ee5b1d6ed7d 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -4451,6 +4451,15 @@ export class WebInspectorElement extends LitElement { // SDK). Distinct from `_memoriesAvailable` (memory not enabled on an // otherwise-current deployment) so the teaser can show upgrade-the-SDK copy. private _memoryStoreUnsupported = false; + // ── Semantic recall (B3) ────────────────────────────────────────────── + // `null` = no recall run yet (section hidden). `[]` = ran, no matches. + private _recallResults: Memory[] | null = null; + private _recallLoading = false; + private _recallError: string | null = null; + private _recallQuery = ""; + // Monotonic token so a slow recall resolving after a newer one / Clear / + // detach is ignored — last-write-wins without racing state. + private _recallSeq = 0; private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null; private coreProperties: Readonly> = {}; private lastCoreError: { @@ -5080,6 +5089,58 @@ export class WebInspectorElement extends LitElement { this.requestUpdate(); } + /** + * Runs a semantic recall via the memory store (`core.getMemoryStore().recall`, + * from B2) and stores ranked results. Guarded by a monotonic sequence token + * so a stale request cannot overwrite a newer result / Clear / detach. Only + * reachable from the Intelligence-gated memory view, so it inherits the gate. + */ + private runRecall(query: string): void { + const trimmed = query.trim(); + if (trimmed.length === 0) return; + const store = this._core?.getMemoryStore?.(); + if (!store || typeof store.recall !== "function") { + this._recallResults = []; + this._recallError = "Recall is not supported by this SDK version."; + this._recallLoading = false; + this.requestUpdate(); + return; + } + + const seq = ++this._recallSeq; + this._recallLoading = true; + this._recallError = null; + this.requestUpdate(); + + store + .recall(trimmed) + .then((results) => { + if (seq !== this._recallSeq) return; + this._recallResults = results; + this._recallError = null; + this._recallLoading = false; + this.requestUpdate(); + }) + .catch((error: unknown) => { + if (seq !== this._recallSeq) return; + this._recallResults = []; + this._recallError = + error instanceof Error ? error.message : "unknown error"; + this._recallLoading = false; + this.requestUpdate(); + }); + } + + /** Clears recall results/section and cancels any in-flight recall. */ + private clearRecall(): void { + this._recallSeq += 1; + this._recallResults = null; + this._recallError = null; + this._recallLoading = false; + this._recallQuery = ""; + this.requestUpdate(); + } + private detachFromCore(): void { if (this.coreUnsubscribe) { this.coreUnsubscribe(); @@ -5096,6 +5157,13 @@ export class WebInspectorElement extends LitElement { // activation re-subscribes (and re-evaluates SDK support) cleanly. this._memorySubscribed = false; this._memoryStoreUnsupported = false; + // Reset recall state and bump the sequence token so any in-flight recall + // resolving after detach is ignored. + this._recallSeq += 1; + this._recallResults = null; + this._recallLoading = false; + this._recallError = null; + this._recallQuery = ""; this.coreSubscriber = null; this.runtimeStatus = null; this.lastCoreError = null; @@ -9427,6 +9495,19 @@ ${argsString}) => { + this._recallQuery = e.detail; + }} + @recallSubmitted=${(e: CustomEvent) => { + this.runRecall(e.detail); + }} + @recallCleared=${() => { + this.clearRecall(); + }} >
From fc085e2a3ab3e5cffd405d12934a3b2e0ef156f0 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 15:04:49 -0700 Subject: [PATCH 026/456] feat(banking): enable product web-inspector via showDevConsole Set showDevConsole={true} on the CopilotKitProvider to surface the product web-inspector on every host, and set exposeMemoryRoutes: true on the Intelligence-mode CopilotRuntime so the inspector's Memory tab can list and recall memories in the demo. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../banking/src/app/api/copilotkit/[[...slug]]/route.ts | 5 +++++ examples/showcases/banking/src/app/wrapper.tsx | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/showcases/banking/src/app/api/copilotkit/[[...slug]]/route.ts b/examples/showcases/banking/src/app/api/copilotkit/[[...slug]]/route.ts index ed8bdccc445..015f029117c 100644 --- a/examples/showcases/banking/src/app/api/copilotkit/[[...slug]]/route.ts +++ b/examples/showcases/banking/src/app/api/copilotkit/[[...slug]]/route.ts @@ -332,6 +332,11 @@ function createRuntime(): CopilotRuntime { agents: { default: bankingAgent }, intelligence, identifyUser, + // Opt in to the client-facing /memories/* proxy routes (default off) so the + // product web-inspector's Memory tab can list + recall memories in this + // demo. Only meaningful in Intelligence mode; does not affect the agent's + // own server-side recall_memory (that runs via the MCP path). + exposeMemoryRoutes: true, licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN, lockTtlSeconds: 30, lockKeyPrefix: "northwind-lock", diff --git a/examples/showcases/banking/src/app/wrapper.tsx b/examples/showcases/banking/src/app/wrapper.tsx index 2155bfc0bea..d2c6ddcc430 100644 --- a/examples/showcases/banking/src/app/wrapper.tsx +++ b/examples/showcases/banking/src/app/wrapper.tsx @@ -260,7 +260,10 @@ export function CopilotKitWrapper({ // chat appears "stuck". CopilotKitProvider is the lean stack the working // e-commerce reference uses; our inbox's `useThreads` (from /v2) reads // CopilotKitProvider's own context, so the inbox keeps working. - showDevConsole={false} + // Surface the product web-inspector () on every host — + // this reference demo showcases it. `true` (not "auto") so deployed demo + // hosts show it too; the provider mounts it automatically when this is set. + showDevConsole={true} > {/* Anchor the whole chat surface to the actively-selected thread. The From 2614060266a9b1fc25c60ebc246fe7b2e17899fd Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 15:07:46 -0700 Subject: [PATCH 027/456] refactor(banking): drop bespoke inspector providers from wrapper Remove the GlassEngineProvider / InspectorStoreProvider wrappers and the mount from the client tree, and drop the glassAvailable prop (and its server-side threading in layout.tsx). The product web-inspector enabled in the prior commit replaces the bespoke pane. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/showcases/banking/src/app/layout.tsx | 10 ++--- .../showcases/banking/src/app/wrapper.tsx | 45 +++++++------------ 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/examples/showcases/banking/src/app/layout.tsx b/examples/showcases/banking/src/app/layout.tsx index 474a755bae5..162f6fdc85d 100644 --- a/examples/showcases/banking/src/app/layout.tsx +++ b/examples/showcases/banking/src/app/layout.tsx @@ -6,7 +6,6 @@ import "./globals.css"; import { AuthContextProvider } from "@/components/auth-context"; import { CopilotKitWrapper } from "./wrapper"; import { IDENTITY } from "@/lib/identity"; -import { glassEngineAvailable } from "@/lib/glass-engine"; import { presenterResetEnabled } from "@/lib/presenter"; const geistSans = localFont({ @@ -45,12 +44,9 @@ export default function RootLayout({ className={`${inter.variable} ${geistSans.variable} ${geistMono.variable} antialiased`} > - {/* Read the deployment gate server-side (non-NEXT_PUBLIC_ env) and - thread it to the client as a prop — one image, per-deploy env. */} - + {/* Read the presenter-reset deployment gate server-side (non-NEXT_PUBLIC_ + env) and thread it to the client as a prop — one image, per-deploy env. */} + {children} diff --git a/examples/showcases/banking/src/app/wrapper.tsx b/examples/showcases/banking/src/app/wrapper.tsx index d2c6ddcc430..684a225451d 100644 --- a/examples/showcases/banking/src/app/wrapper.tsx +++ b/examples/showcases/banking/src/app/wrapper.tsx @@ -18,9 +18,6 @@ import { RecordingProvider } from "@/components/recording-context"; import { RecordingVignette } from "@/components/recording-vignette"; import { ProactiveNotice } from "@/components/wow/proactive-notice"; import { ReportCopilotTools } from "@/components/wow/report-tool"; -import { GlassEngineProvider } from "@/components/glass-engine-context"; -import { InspectorStoreProvider } from "@/lib/inspector/store"; -import { InspectorPane } from "@/components/inspector/inspector-pane"; import { sandboxFunctions } from "@/opengen/sandbox-functions"; import { SandboxDataSync } from "@/opengen/sandbox-data-sync"; @@ -217,11 +214,9 @@ function BankingSuggestions() { export function CopilotKitWrapper({ children, - glassAvailable = false, resetEnabled = false, }: { children: React.ReactNode; - glassAvailable?: boolean; resetEnabled?: boolean; }) { const { currentUser } = useAuthContext(); @@ -299,30 +294,22 @@ export function CopilotKitWrapper({ call site (the transactions list approve/deny, the inline policy exception card) is inside it. */} - - - - {/* - CanvasProvider derives whether a report surface is active from - the agent message stream (+ a local dismiss for the "← Back" - control). It must be an ancestor of LayoutComponent, which calls - useCanvas() to render in place of the page body. - */} - - - {children} - - - - - - {/* Mount the pane (and its AG-UI subscription) ONLY where the - deployment opted in. Public hosts never subscribe. */} - {glassAvailable && } - - - - + + {/* + CanvasProvider derives whether a report surface is active from the + agent message stream (+ a local dismiss). It must be an ancestor of + LayoutComponent, which calls useCanvas() to render . + */} + + + {children} + + + + + + + From 9f40ebe2a22198feba99861decbaa38e92704c27 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 14 Jul 2026 15:07:46 -0700 Subject: [PATCH 028/456] refactor(banking): remove Glass Engine deployment gate from layout Drop useGlassEngine(), the Telescope toggle button, and the glassActive-driven body padding from the app layout. The product inspector launcher replaces the telescope toggle. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../banking/src/components/layout.tsx | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/examples/showcases/banking/src/components/layout.tsx b/examples/showcases/banking/src/components/layout.tsx index 2b5a51c555e..b1a0d123943 100644 --- a/examples/showcases/banking/src/components/layout.tsx +++ b/examples/showcases/banking/src/components/layout.tsx @@ -7,7 +7,6 @@ import { HelpCircle, LayoutDashboard, RotateCcw, - Telescope, Users, } from "lucide-react"; @@ -28,7 +27,6 @@ import { import type { Member } from "@/app/api/v1/data"; import { MemberRole } from "@/app/api/v1/data"; import { useAuthContext } from "@/components/auth-context"; -import { useGlassEngine } from "@/components/glass-engine-context"; import { useRecording } from "@/components/recording-context"; import { ThemeToggle } from "@/components/ui/theme-toggle"; import { useAgentContext } from "@copilotkit/react-core/v2"; @@ -143,11 +141,6 @@ export function LayoutComponent({ resetEnabled = false, }: LayoutProps) { const { users, currentUser, setCurrentUser } = useAuthContext(); - const { - available: glassAvailable, - active: glassActive, - toggle: toggleGlass, - } = useGlassEngine(); const pathname = usePathname(); useAgentContext({ description: "The current page where the user is", @@ -186,12 +179,7 @@ export function LayoutComponent({ }, [pathname]); return ( -
+
{/* Floating icon rail. */}