From 3b4f602c1b81ad6c946f42d37eaaf857b8c06d8f Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Wed, 26 Nov 2025 08:31:42 -0800 Subject: [PATCH 1/2] feat(web-inspector): Redesign the inspector and connect it to state --- apps/angular/demo/package.json | 1 + .../headless/headless-chat.component.ts | 28 +- apps/angular/demo/tsconfig.json | 4 + packages/web-inspector/package.json | 6 +- .../src/__tests__/web-inspector.spec.ts | 171 +++ packages/web-inspector/src/index.ts | 1281 ++++++++++++----- packages/web-inspector/src/lib/persistence.ts | 58 +- packages/web-inspector/vitest.config.ts | 8 + pnpm-lock.yaml | 720 ++++----- 9 files changed, 1438 insertions(+), 839 deletions(-) create mode 100644 packages/web-inspector/src/__tests__/web-inspector.spec.ts create mode 100644 packages/web-inspector/vitest.config.ts diff --git a/apps/angular/demo/package.json b/apps/angular/demo/package.json index ad81164b..8e2e081c 100644 --- a/apps/angular/demo/package.json +++ b/apps/angular/demo/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@copilotkitnext/angular": "workspace:*", + "@copilotkitnext/web-inspector": "workspace:*", "rxjs": "^7.8.1", "tslib": "^2.8.1", "zod": "^3.25.75", diff --git a/apps/angular/demo/src/app/routes/headless/headless-chat.component.ts b/apps/angular/demo/src/app/routes/headless/headless-chat.component.ts index 89f0f6e4..54b360b0 100644 --- a/apps/angular/demo/src/app/routes/headless/headless-chat.component.ts +++ b/apps/angular/demo/src/app/routes/headless/headless-chat.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, computed, inject, input, signal } from "@angular/core"; +import { Component, ChangeDetectionStrategy, computed, inject, input, signal, OnDestroy, OnInit } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { @@ -10,6 +10,7 @@ import { registerHumanInTheLoop, } from "@copilotkitnext/angular"; import { RenderToolCalls } from "@copilotkitnext/angular"; +import { WEB_INSPECTOR_TAG, type WebInspectorElement } from "@copilotkitnext/web-inspector"; import { z } from "zod"; @Component({ @@ -76,7 +77,7 @@ export class RequireApprovalComponent implements HumanInTheLoopToolRenderer { `, }) -export class HeadlessChatComponent { +export class HeadlessChatComponent implements OnInit, OnDestroy { readonly agentStore = injectAgentStore("openai"); readonly agent = computed(() => this.agentStore()?.agent); readonly isRunning = computed(() => !!this.agentStore()?.isRunning()); @@ -84,6 +85,7 @@ export class HeadlessChatComponent { readonly copilotkit = inject(CopilotKit); inputValue = ""; + private inspectorElement: WebInspectorElement | null = null; constructor() { registerHumanInTheLoop({ @@ -104,6 +106,28 @@ export class HeadlessChatComponent { ); } + ngOnInit(): void { + if (typeof document === "undefined") return; + + const existing = document.querySelector(WEB_INSPECTOR_TAG); + const inspector = existing ?? (document.createElement(WEB_INSPECTOR_TAG) as WebInspectorElement); + inspector.core = this.copilotkit.core; + inspector.setAttribute("auto-attach-core", "false"); + + if (!existing) { + document.body.appendChild(inspector); + } + + this.inspectorElement = inspector; + } + + ngOnDestroy(): void { + if (this.inspectorElement && this.inspectorElement.isConnected) { + this.inspectorElement.remove(); + } + this.inspectorElement = null; + } + async send() { const content = this.inputValue.trim(); const agent = this.agent(); diff --git a/apps/angular/demo/tsconfig.json b/apps/angular/demo/tsconfig.json index 8b90a657..c1550f57 100644 --- a/apps/angular/demo/tsconfig.json +++ b/apps/angular/demo/tsconfig.json @@ -25,6 +25,10 @@ "../../packages/shared/dist/index.d.ts", "../../packages/shared/dist/index.mjs", "../../packages/shared/src/index.ts" + ], + "@copilotkitnext/web-inspector": [ + "../../packages/web-inspector/dist/index.d.ts", + "../../packages/web-inspector/src/index.ts" ] } }, diff --git a/packages/web-inspector/package.json b/packages/web-inspector/package.json index a78f44c9..7cd4f975 100644 --- a/packages/web-inspector/package.json +++ b/packages/web-inspector/package.json @@ -22,7 +22,8 @@ "prepublishOnly": "pnpm run build:css && pnpm run build", "lint": "eslint . --max-warnings 0", "check-types": "pnpm run build:css && tsc --noEmit", - "clean": "rm -rf dist src/styles/generated.css" + "clean": "rm -rf dist src/styles/generated.css", + "test": "pnpm run build:css && vitest run" }, "dependencies": { "@ag-ui/client": "0.0.42-alpha.1", @@ -39,7 +40,8 @@ "eslint": "^9.30.0", "tailwindcss": "^4.0.8", "tsup": "^8.5.0", - "typescript": "5.8.2" + "typescript": "5.8.2", + "vitest": "^3.0.5" }, "engines": { "node": ">=18" diff --git a/packages/web-inspector/src/__tests__/web-inspector.spec.ts b/packages/web-inspector/src/__tests__/web-inspector.spec.ts new file mode 100644 index 00000000..572baac0 --- /dev/null +++ b/packages/web-inspector/src/__tests__/web-inspector.spec.ts @@ -0,0 +1,171 @@ +import { WebInspectorElement } from "../index"; +import { + CopilotKitCore, + CopilotKitCoreRuntimeConnectionStatus, + type CopilotKitCoreSubscriber, +} from "@copilotkitnext/core"; +import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +type MockAgentController = { emit: (key: keyof AgentSubscriber, payload: unknown) => void }; + +type InspectorInternals = { + flattenedEvents: Array<{ type: string }>; + agentMessages: Map>; + agentStates: Map; + cachedTools: Array<{ name: string }>; +}; + +type InspectorContextInternals = { + contextStore: Record; + copyContextValue: (value: unknown, id: string) => Promise; + persistState: () => void; +}; + +type MockAgentExtras = Partial<{ + messages: unknown; + state: unknown; + toolHandlers: Record; + toolRenderers: Record; +}>; + +function createMockAgent( + agentId: string, + extras: MockAgentExtras = {}, +): { agent: AbstractAgent; controller: MockAgentController } { + const subscribers = new Set(); + + const agent = { + agentId, + ...extras, + subscribe(subscriber: AgentSubscriber) { + subscribers.add(subscriber); + return { + unsubscribe: () => subscribers.delete(subscriber), + }; + }, + }; + + const emit = (key: keyof AgentSubscriber, payload: unknown) => { + subscribers.forEach((subscriber) => { + const handler = subscriber[key]; + if (handler) { + (handler as (arg: unknown) => void)(payload); + } + }); + }; + + return { agent: agent as unknown as AbstractAgent, controller: { emit } }; +} + +type MockCore = { + agents: Record; + context: Record; + properties: Record; + runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus; + subscribe: (subscriber: CopilotKitCoreSubscriber) => { unsubscribe: () => void }; +}; + +function createMockCore(initialAgents: Record = {}) { + const subscribers = new Set(); + const core: MockCore = { + agents: initialAgents, + context: {}, + properties: {}, + runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected, + subscribe(subscriber: CopilotKitCoreSubscriber) { + subscribers.add(subscriber); + return { unsubscribe: () => subscribers.delete(subscriber) }; + }, + }; + + return { + core, + emitAgentsChanged(nextAgents = core.agents) { + core.agents = nextAgents; + subscribers.forEach((subscriber) => + subscriber.onAgentsChanged?.({ + copilotkit: core as unknown as CopilotKitCore, + agents: core.agents, + }), + ); + }, + emitContextChanged(nextContext: Record) { + core.context = nextContext; + subscribers.forEach((subscriber) => + subscriber.onContextChanged?.({ + copilotkit: core as unknown as CopilotKitCore, + context: core.context as unknown as Readonly>, + }), + ); + }, + }; +} + +describe("WebInspectorElement", () => { + beforeEach(() => { + document.body.innerHTML = ""; + localStorage.clear(); + const mockClipboard = { writeText: vi.fn().mockResolvedValue(undefined) }; + (navigator as unknown as { clipboard: typeof mockClipboard }).clipboard = mockClipboard; + }); + + it("records agent events and syncs state/messages/tools", async () => { + const { agent, controller } = createMockAgent("alpha", { + messages: [{ id: "m1", role: "user", content: "hi there" }], + state: { foo: "bar" }, + toolHandlers: { + greet: { description: "hello", parameters: { type: "object" } }, + }, + }); + const { core, emitAgentsChanged } = createMockCore({ alpha: agent }); + + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + + emitAgentsChanged(); + await inspector.updateComplete; + + controller.emit("onRunStartedEvent", { event: { id: "run-1" } }); + controller.emit("onMessagesSnapshotEvent", { event: { id: "msg-1" } }); + await inspector.updateComplete; + + const inspectorHandle = inspector as unknown as InspectorInternals; + + const flattened = inspectorHandle.flattenedEvents; + expect(flattened.some((evt) => evt.type === "RUN_STARTED")).toBe(true); + expect(flattened.some((evt) => evt.type === "MESSAGES_SNAPSHOT")).toBe(true); + expect(inspectorHandle.agentMessages.get("alpha")?.[0]?.contentText).toContain("hi there"); + expect(inspectorHandle.agentStates.get("alpha")).toBeDefined(); + expect(inspectorHandle.cachedTools.some((tool) => tool.name === "greet")).toBe(true); + }); + + it("normalizes context, persists state, and copies context values", async () => { + const { core, emitContextChanged } = createMockCore(); + const inspector = new WebInspectorElement(); + document.body.appendChild(inspector); + inspector.core = core as unknown as WebInspectorElement["core"]; + + emitContextChanged({ + ctxA: { value: { nested: true } }, + ctxB: { description: "Described", value: 5 }, + }); + await inspector.updateComplete; + + const inspectorHandle = inspector as unknown as InspectorContextInternals; + const contextStore = inspectorHandle.contextStore; + const ctxA = contextStore.ctxA!; + const ctxB = contextStore.ctxB!; + expect(ctxA.value).toMatchObject({ nested: true }); + expect(ctxB.description).toBe("Described"); + + await inspectorHandle.copyContextValue({ nested: true }, "ctxA"); + const clipboard = (navigator as unknown as { clipboard: { writeText: ReturnType } }).clipboard + .writeText as ReturnType; + expect(clipboard).toHaveBeenCalledTimes(1); + + inspectorHandle.persistState(); + expect(localStorage.getItem("copilotkit_inspector_state")).toBeTruthy(); + }); +}); diff --git a/packages/web-inspector/src/index.ts b/packages/web-inspector/src/index.ts index 943582ca..708180c9 100644 --- a/packages/web-inspector/src/index.ts +++ b/packages/web-inspector/src/index.ts @@ -4,7 +4,12 @@ import tailwindStyles from "./styles/generated.css"; import logoMarkUrl from "./assets/logo-mark.svg"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { icons } from "lucide"; -import type { CopilotKitCore, CopilotKitCoreSubscriber } from "@copilotkitnext/core"; +import { + CopilotKitCore, + CopilotKitCoreRuntimeConnectionStatus, + type CopilotKitCoreSubscriber, + type CopilotKitCoreErrorCode, +} from "@copilotkitnext/core"; import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client"; import type { Anchor, ContextKey, ContextState, DockMode, Position, Size } from "./lib/types"; import { @@ -43,34 +48,106 @@ const DRAG_THRESHOLD = 6; const MIN_WINDOW_WIDTH = 600; const MIN_WINDOW_WIDTH_DOCKED_LEFT = 420; const MIN_WINDOW_HEIGHT = 200; -const COOKIE_NAME = "copilotkit_inspector_state"; -const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days +const INSPECTOR_STORAGE_KEY = "copilotkit_inspector_state"; const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 }; const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 560 }; const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar const MAX_AGENT_EVENTS = 200; const MAX_TOTAL_EVENTS = 500; +type InspectorAgentEventType = + | "RUN_STARTED" + | "RUN_FINISHED" + | "RUN_ERROR" + | "TEXT_MESSAGE_START" + | "TEXT_MESSAGE_CONTENT" + | "TEXT_MESSAGE_END" + | "TOOL_CALL_START" + | "TOOL_CALL_ARGS" + | "TOOL_CALL_END" + | "TOOL_CALL_RESULT" + | "STATE_SNAPSHOT" + | "STATE_DELTA" + | "MESSAGES_SNAPSHOT" + | "RAW_EVENT" + | "CUSTOM_EVENT"; + +const AGENT_EVENT_TYPES: readonly InspectorAgentEventType[] = [ + "RUN_STARTED", + "RUN_FINISHED", + "RUN_ERROR", + "TEXT_MESSAGE_START", + "TEXT_MESSAGE_CONTENT", + "TEXT_MESSAGE_END", + "TOOL_CALL_START", + "TOOL_CALL_ARGS", + "TOOL_CALL_END", + "TOOL_CALL_RESULT", + "STATE_SNAPSHOT", + "STATE_DELTA", + "MESSAGES_SNAPSHOT", + "RAW_EVENT", + "CUSTOM_EVENT", +] as const; + +type SanitizedValue = + | string + | number + | boolean + | null + | SanitizedValue[] + | { [key: string]: SanitizedValue }; + +type InspectorToolCall = { + id?: string; + function?: { + name?: string; + arguments?: SanitizedValue | string; + }; + toolName?: string; + status?: string; +}; + +type InspectorMessage = { + id?: string; + role: string; + contentText: string; + contentRaw?: SanitizedValue; + toolCalls: InspectorToolCall[]; +}; + +type InspectorToolDefinition = { + agentId: string; + name: string; + description?: string; + parameters?: unknown; + type: "handler" | "renderer"; +}; + type InspectorEvent = { id: string; agentId: string; - type: string; + type: InspectorAgentEventType; timestamp: number; - payload: unknown; + payload: SanitizedValue; }; export class WebInspectorElement extends LitElement { static properties = { core: { attribute: false }, + autoAttachCore: { type: Boolean, attribute: "auto-attach-core" }, } as const; private _core: CopilotKitCore | null = null; private coreSubscriber: CopilotKitCoreSubscriber | null = null; private coreUnsubscribe: (() => void) | null = null; + private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null; + private coreProperties: Readonly> = {}; + private lastCoreError: { code: CopilotKitCoreErrorCode; message: string } | null = null; private agentSubscriptions: Map void> = new Map(); private agentEvents: Map = new Map(); - private agentMessages: Map = new Map(); - private agentStates: Map = new Map(); + private agentMessages: Map = new Map(); + private agentStates: Map = new Map(); private flattenedEvents: InspectorEvent[] = []; private eventCounter = 0; private contextStore: Record = {}; @@ -89,6 +166,12 @@ export class WebInspectorElement extends LitElement { private previousBodyMargins: { left: string; bottom: string } | null = null; private transitionTimeoutId: ReturnType | null = null; private pendingSelectedContext: string | null = null; + private autoAttachCore = true; + private attemptedAutoAttach = false; + private cachedTools: InspectorToolDefinition[] = []; + private toolSignature = ""; + private eventFilterText = ""; + private eventTypeFilter: InspectorAgentEventType | "all" = "all"; get core(): CopilotKitCore | null { return this._core; @@ -136,19 +219,35 @@ export class WebInspectorElement extends LitElement { private isResizing = false; private readonly menuItems: MenuItem[] = [ - { key: "ag-ui-events", label: "AG-UI Events", icon: "Zap" }, - { key: "agents", label: "Agents", icon: "Bot" }, + { key: "ag-ui-events", label: "Events", icon: "Zap" }, + { key: "agents", label: "Agent", icon: "Bot" }, { key: "frontend-tools", label: "Frontend Tools", icon: "Hammer" }, - { key: "agent-context", label: "Agent Context", icon: "FileText" }, + { key: "agent-context", label: "Context", icon: "FileText" }, ]; private attachToCore(core: CopilotKitCore): void { + this.runtimeStatus = core.runtimeConnectionStatus; + this.coreProperties = core.properties; + this.lastCoreError = null; + this.coreSubscriber = { + onRuntimeConnectionStatusChanged: ({ status }) => { + this.runtimeStatus = status; + this.requestUpdate(); + }, + onPropertiesChanged: ({ properties }) => { + this.coreProperties = properties; + this.requestUpdate(); + }, + onError: ({ code, error }) => { + this.lastCoreError = { code, message: error.message }; + this.requestUpdate(); + }, onAgentsChanged: ({ agents }) => { this.processAgentsChanged(agents); }, onContextChanged: ({ context }) => { - this.contextStore = { ...context }; + this.contextStore = this.normalizeContextStore(context); this.requestUpdate(); }, } satisfies CopilotKitCoreSubscriber; @@ -158,7 +257,7 @@ export class WebInspectorElement extends LitElement { // Initialize context from core if (core.context) { - this.contextStore = { ...core.context }; + this.contextStore = this.normalizeContextStore(core.context); } } @@ -168,6 +267,11 @@ export class WebInspectorElement extends LitElement { this.coreUnsubscribe = null; } this.coreSubscriber = null; + this.runtimeStatus = null; + this.lastCoreError = null; + this.coreProperties = {}; + this.cachedTools = []; + this.toolSignature = ""; this.teardownAgentSubscriptions(); } @@ -204,9 +308,62 @@ export class WebInspectorElement extends LitElement { } this.updateContextOptions(seenAgentIds); + this.refreshToolsSnapshot(); this.requestUpdate(); } + private refreshToolsSnapshot(): void { + if (!this._core) { + if (this.cachedTools.length > 0) { + this.cachedTools = []; + this.toolSignature = ""; + this.requestUpdate(); + } + return; + } + + const tools = this.extractToolsFromAgents(); + const signature = JSON.stringify( + tools.map((tool) => ({ + agentId: tool.agentId, + name: tool.name, + type: tool.type, + hasDescription: Boolean(tool.description), + hasParameters: Boolean(tool.parameters), + })), + ); + + if (signature !== this.toolSignature) { + this.toolSignature = signature; + this.cachedTools = tools; + this.requestUpdate(); + } + } + + private tryAutoAttachCore(): void { + if (this.attemptedAutoAttach || this._core || !this.autoAttachCore || typeof window === "undefined") { + return; + } + + this.attemptedAutoAttach = true; + + const globalWindow = window as unknown as Record; + const globalCandidates: Array = [ + // Common app-level globals used during development + globalWindow.__COPILOTKIT_CORE__, + (globalWindow.copilotkit as { core?: unknown } | undefined)?.core, + globalWindow.copilotkitCore, + ]; + + const foundCore = globalCandidates.find( + (candidate): candidate is CopilotKitCore => !!candidate && typeof candidate === "object", + ); + + if (foundCore) { + this.core = foundCore; + } + } + private subscribeToAgent(agent: AbstractAgent): void { if (!agent.agentId) { return; @@ -288,14 +445,15 @@ export class WebInspectorElement extends LitElement { } } - private recordAgentEvent(agentId: string, type: string, payload: unknown): void { + private recordAgentEvent(agentId: string, type: InspectorAgentEventType, payload: unknown): void { const eventId = `${agentId}:${++this.eventCounter}`; + const normalizedPayload = this.normalizeEventPayload(type, payload); const event: InspectorEvent = { id: eventId, agentId, type, timestamp: Date.now(), - payload, + payload: normalizedPayload, }; const currentAgentEvents = this.agentEvents.get(agentId) ?? []; @@ -303,6 +461,7 @@ export class WebInspectorElement extends LitElement { this.agentEvents.set(agentId, nextAgentEvents); this.flattenedEvents = [event, ...this.flattenedEvents].slice(0, MAX_TOTAL_EVENTS); + this.refreshToolsSnapshot(); this.requestUpdate(); } @@ -311,9 +470,8 @@ export class WebInspectorElement extends LitElement { return; } - const messages = (agent as { messages?: unknown }).messages; - - if (Array.isArray(messages)) { + const messages = this.normalizeAgentMessages((agent as { messages?: unknown }).messages); + if (messages) { this.agentMessages.set(agent.agentId, messages); } else { this.agentMessages.delete(agent.agentId); @@ -332,7 +490,7 @@ export class WebInspectorElement extends LitElement { if (state === undefined || state === null) { this.agentStates.delete(agent.agentId); } else { - this.agentStates.set(agent.agentId, state); + this.agentStates.set(agent.agentId, this.sanitizeForLogging(state)); } this.requestUpdate(); @@ -397,17 +555,42 @@ export class WebInspectorElement extends LitElement { return this.agentEvents.get(this.selectedContext) ?? []; } - private getLatestStateForAgent(agentId: string): unknown | null { + private filterEvents(events: InspectorEvent[]): InspectorEvent[] { + const query = this.eventFilterText.trim().toLowerCase(); + + return events.filter((event) => { + if (this.eventTypeFilter !== "all" && event.type !== this.eventTypeFilter) { + return false; + } + + if (!query) { + return true; + } + + const payloadText = this.stringifyPayload(event.payload, false).toLowerCase(); + return ( + event.type.toLowerCase().includes(query) || + event.agentId.toLowerCase().includes(query) || + payloadText.includes(query) + ); + }); + } + + private getLatestStateForAgent(agentId: string): SanitizedValue | null { if (this.agentStates.has(agentId)) { - return this.agentStates.get(agentId); + const value = this.agentStates.get(agentId); + return value === undefined ? null : value; } const events = this.agentEvents.get(agentId) ?? []; const stateEvent = events.find((e) => e.type === "STATE_SNAPSHOT"); - return stateEvent?.payload ?? null; + if (!stateEvent) { + return null; + } + return stateEvent.payload; } - private getLatestMessagesForAgent(agentId: string): unknown[] | null { + private getLatestMessagesForAgent(agentId: string): InspectorMessage[] | null { const messages = this.agentMessages.get(agentId); return messages ?? null; } @@ -445,22 +628,11 @@ export class WebInspectorElement extends LitElement { const messages = this.agentMessages.get(agentId); - const toolCallCount = Array.isArray(messages) - ? (messages as unknown[]).reduce((count, rawMessage) => { - if (!rawMessage || typeof rawMessage !== 'object') { - return count; - } - - const toolCalls = (rawMessage as { toolCalls?: unknown }).toolCalls; - if (!Array.isArray(toolCalls)) { - return count; - } - - return count + toolCalls.length; - }, 0) + const toolCallCount = messages + ? messages.reduce((count, message) => count + (message.toolCalls?.length ?? 0), 0) : events.filter((e) => e.type === "TOOL_CALL_END").length; - const messageCount = Array.isArray(messages) ? messages.length : 0; + const messageCount = messages?.length ?? 0; return { totalEvents: events.length, @@ -471,7 +643,7 @@ export class WebInspectorElement extends LitElement { }; } - private renderToolCallDetails(toolCalls: unknown[]) { + private renderToolCallDetails(toolCalls: InspectorToolCall[]) { if (!Array.isArray(toolCalls) || toolCalls.length === 0) { return nothing; } @@ -479,10 +651,9 @@ export class WebInspectorElement extends LitElement { return html`
${toolCalls.map((call, index) => { - const toolCall = call as any; - const functionName = typeof toolCall?.function?.name === 'string' ? toolCall.function.name : 'Unknown function'; - const callId = typeof toolCall?.id === 'string' ? toolCall.id : `tool-call-${index + 1}`; - const argsString = this.formatToolCallArguments(toolCall?.function?.arguments); + const functionName = call.function?.name ?? call.toolName ?? "Unknown function"; + const callId = typeof call?.id === "string" ? call.id : `tool-call-${index + 1}`; + const argsString = this.formatToolCallArguments(call.function?.arguments); return html`
@@ -508,7 +679,7 @@ export class WebInspectorElement extends LitElement { try { const parsed = JSON.parse(args); return JSON.stringify(parsed, null, 2); - } catch (error) { + } catch { return args; } } @@ -516,7 +687,7 @@ export class WebInspectorElement extends LitElement { if (typeof args === 'object') { try { return JSON.stringify(args, null, 2); - } catch (error) { + } catch { return String(args); } } @@ -622,7 +793,7 @@ export class WebInspectorElement extends LitElement { private extractEventFromPayload(payload: unknown): unknown { // If payload is an object with an 'event' field, extract it if (payload && typeof payload === "object" && "event" in payload) { - return (payload as any).event; + return (payload as Record).event; } // Otherwise, assume the payload itself is the event return payload; @@ -695,6 +866,35 @@ export class WebInspectorElement extends LitElement { z-index: 50; background: transparent; } + + .tooltip-target { + position: relative; + } + + .tooltip-target::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 6px); + left: 50%; + transform: translateX(-50%) translateY(-4px); + white-space: nowrap; + background: rgba(17, 24, 39, 0.95); + color: white; + padding: 4px 8px; + border-radius: 6px; + font-size: 10px; + line-height: 1.2; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease, transform 120ms ease; + z-index: 4000; + } + + .tooltip-target:hover::after { + opacity: 1; + transform: translateX(-50%) translateY(0); + } `, ]; @@ -705,7 +905,8 @@ export class WebInspectorElement extends LitElement { window.addEventListener("pointerdown", this.handleGlobalPointerDown as EventListener); // Load state early (before first render) so menu selection is correct - this.hydrateStateFromCookieEarly(); + this.hydrateStateFromStorageEarly(); + this.tryAutoAttachCore(); } } @@ -724,6 +925,10 @@ export class WebInspectorElement extends LitElement { return; } + if (!this._core) { + this.tryAutoAttachCore(); + } + this.measureContext("button"); this.measureContext("window"); @@ -733,7 +938,7 @@ export class WebInspectorElement extends LitElement { this.contextState.window.anchor = { horizontal: "right", vertical: "top" }; this.contextState.window.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN }; - this.hydrateStateFromCookie(); + this.hydrateStateFromStorage(); // Apply docking styles if open and docked (skip transition on initial load) if (this.isOpen && this.dockMode !== 'floating') { @@ -812,7 +1017,6 @@ export class WebInspectorElement extends LitElement { const windowState = this.contextState.window; const isDocked = this.dockMode !== 'floating'; const isTransitioning = this.hasAttribute('data-transitioning'); - const isCollapsed = this.dockMode === 'docked-left'; const windowStyles = isDocked ? this.getDockedWindowStyles() @@ -823,8 +1027,19 @@ export class WebInspectorElement extends LitElement { minHeight: `${MIN_WINDOW_HEIGHT}px`, }; - const contextDropdown = this.renderContextDropdown(); - const hasContextDropdown = contextDropdown !== nothing; + const hasContextDropdown = this.contextOptions.length > 0; + const contextDropdown = hasContextDropdown ? this.renderContextDropdown() : nothing; + const agentOptions = this.contextOptions.filter((opt) => opt.key !== "all-agents"); + const agentCountLabel = agentOptions.length === 1 ? "1 agent" : `${agentOptions.length} agents`; + const coreStatus = this.getCoreStatusSummary(); + const agentSelector = hasContextDropdown + ? contextDropdown + : html` +
+ ${this.renderIcon("Bot")} + No agents available +
+ `; return html`
` : nothing} -
- -
-
-
-
- - 🪁 - CopilotKit Inspector - - - - ${this.renderIcon(this.getSelectedMenu().icon)} - - ${this.getSelectedMenu().label} - ${hasContextDropdown - ? html` - -
${contextDropdown}
- ` - : nothing} -
+
+
+
+ ${this.renderCoreWarningBanner()} + ${this.renderMainContent()} +
-
- ${this.renderDockControls()} - + + ${this.renderIcon("Activity")} + + ${coreStatus.label} + ${coreStatus.description} +
-
- ${this.renderMainContent()} - -
-