@copilotkit/web-inspector 0.0.0-main-20260402184302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/LICENSE +21 -0
  3. package/dist/assets/inspector-logo-icon.cjs +12 -0
  4. package/dist/assets/inspector-logo-icon.cjs.map +1 -0
  5. package/dist/assets/inspector-logo-icon.mjs +6 -0
  6. package/dist/assets/inspector-logo-icon.mjs.map +1 -0
  7. package/dist/assets/inspector-logo.cjs +12 -0
  8. package/dist/assets/inspector-logo.cjs.map +1 -0
  9. package/dist/assets/inspector-logo.mjs +6 -0
  10. package/dist/assets/inspector-logo.mjs.map +1 -0
  11. package/dist/index.cjs +2881 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +208 -0
  14. package/dist/index.d.cts.map +1 -0
  15. package/dist/index.d.mts +208 -0
  16. package/dist/index.d.mts.map +1 -0
  17. package/dist/index.mjs +2878 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/index.umd.js +3049 -0
  20. package/dist/index.umd.js.map +1 -0
  21. package/dist/lib/context-helpers.cjs +82 -0
  22. package/dist/lib/context-helpers.cjs.map +1 -0
  23. package/dist/lib/context-helpers.mjs +75 -0
  24. package/dist/lib/context-helpers.mjs.map +1 -0
  25. package/dist/lib/persistence.cjs +62 -0
  26. package/dist/lib/persistence.cjs.map +1 -0
  27. package/dist/lib/persistence.mjs +56 -0
  28. package/dist/lib/persistence.mjs.map +1 -0
  29. package/dist/styles/generated.cjs +12 -0
  30. package/dist/styles/generated.cjs.map +1 -0
  31. package/dist/styles/generated.mjs +6 -0
  32. package/dist/styles/generated.mjs.map +1 -0
  33. package/package.json +51 -0
  34. package/src/__tests__/web-inspector.spec.ts +191 -0
  35. package/src/assets/inspector-logo-icon.svg +8 -0
  36. package/src/assets/inspector-logo.svg +40 -0
  37. package/src/components.d.ts +20 -0
  38. package/src/index.ts +4364 -0
  39. package/src/lib/context-helpers.ts +166 -0
  40. package/src/lib/persistence.ts +109 -0
  41. package/src/lib/types.ts +19 -0
  42. package/src/styles/generated.css +2 -0
  43. package/src/styles/tailwind.css +23 -0
  44. package/src/types/css.d.ts +4 -0
  45. package/src/types/svg.d.ts +4 -0
  46. package/tsconfig.json +14 -0
  47. package/tsdown.config.ts +45 -0
  48. package/vitest.config.ts +10 -0
package/src/index.ts ADDED
@@ -0,0 +1,4364 @@
1
+ import { LitElement, css, html, nothing, unsafeCSS } from "lit";
2
+ import { styleMap } from "lit/directives/style-map.js";
3
+ import tailwindStyles from "./styles/generated.css";
4
+ import inspectorLogoUrl from "./assets/inspector-logo.svg";
5
+ import inspectorLogoIconUrl from "./assets/inspector-logo-icon.svg";
6
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
7
+ import { marked } from "marked";
8
+ import { icons } from "lucide";
9
+ import {
10
+ CopilotKitCore,
11
+ CopilotKitCoreRuntimeConnectionStatus,
12
+ type CopilotKitCoreSubscriber,
13
+ type CopilotKitCoreErrorCode,
14
+ } from "@copilotkit/core";
15
+ import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
16
+ import type {
17
+ Anchor,
18
+ ContextKey,
19
+ ContextState,
20
+ DockMode,
21
+ Position,
22
+ Size,
23
+ } from "./lib/types";
24
+ import {
25
+ applyAnchorPosition as applyAnchorPositionHelper,
26
+ centerContext as centerContextHelper,
27
+ constrainToViewport,
28
+ keepPositionWithinViewport,
29
+ updateAnchorFromPosition as updateAnchorFromPositionHelper,
30
+ updateSizeFromElement,
31
+ clampSize as clampSizeToViewport,
32
+ } from "./lib/context-helpers";
33
+ import {
34
+ loadInspectorState,
35
+ saveInspectorState,
36
+ type PersistedState,
37
+ isValidAnchor,
38
+ isValidPosition,
39
+ isValidSize,
40
+ isValidDockMode,
41
+ } from "./lib/persistence";
42
+
43
+ export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const;
44
+
45
+ type LucideIconName = keyof typeof icons;
46
+
47
+ type MenuKey = "ag-ui-events" | "agents" | "frontend-tools" | "agent-context";
48
+
49
+ type MenuItem = {
50
+ key: MenuKey;
51
+ label: string;
52
+ icon: LucideIconName;
53
+ };
54
+
55
+ const EDGE_MARGIN = 16;
56
+ const DRAG_THRESHOLD = 6;
57
+ const MIN_WINDOW_WIDTH = 600;
58
+ const MIN_WINDOW_WIDTH_DOCKED_LEFT = 420;
59
+ const MIN_WINDOW_HEIGHT = 200;
60
+ const INSPECTOR_STORAGE_KEY = "cpk:inspector:state";
61
+ const ANNOUNCEMENT_STORAGE_KEY = "cpk:inspector:announcements";
62
+ const ANNOUNCEMENT_URL = "https://cdn.copilotkit.ai/announcements.json";
63
+ const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 };
64
+ const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 560 };
65
+ const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar
66
+ const MAX_AGENT_EVENTS = 200;
67
+ const MAX_TOTAL_EVENTS = 500;
68
+
69
+ type InspectorAgentEventType =
70
+ | "RUN_STARTED"
71
+ | "RUN_FINISHED"
72
+ | "RUN_ERROR"
73
+ | "TEXT_MESSAGE_START"
74
+ | "TEXT_MESSAGE_CONTENT"
75
+ | "TEXT_MESSAGE_END"
76
+ | "TOOL_CALL_START"
77
+ | "TOOL_CALL_ARGS"
78
+ | "TOOL_CALL_END"
79
+ | "TOOL_CALL_RESULT"
80
+ | "STATE_SNAPSHOT"
81
+ | "STATE_DELTA"
82
+ | "MESSAGES_SNAPSHOT"
83
+ | "RAW_EVENT"
84
+ | "CUSTOM_EVENT"
85
+ | "REASONING_START"
86
+ | "REASONING_MESSAGE_START"
87
+ | "REASONING_MESSAGE_CONTENT"
88
+ | "REASONING_MESSAGE_END"
89
+ | "REASONING_END"
90
+ | "REASONING_ENCRYPTED_VALUE";
91
+
92
+ const AGENT_EVENT_TYPES: readonly InspectorAgentEventType[] = [
93
+ "RUN_STARTED",
94
+ "RUN_FINISHED",
95
+ "RUN_ERROR",
96
+ "TEXT_MESSAGE_START",
97
+ "TEXT_MESSAGE_CONTENT",
98
+ "TEXT_MESSAGE_END",
99
+ "TOOL_CALL_START",
100
+ "TOOL_CALL_ARGS",
101
+ "TOOL_CALL_END",
102
+ "TOOL_CALL_RESULT",
103
+ "STATE_SNAPSHOT",
104
+ "STATE_DELTA",
105
+ "MESSAGES_SNAPSHOT",
106
+ "RAW_EVENT",
107
+ "CUSTOM_EVENT",
108
+ "REASONING_START",
109
+ "REASONING_MESSAGE_START",
110
+ "REASONING_MESSAGE_CONTENT",
111
+ "REASONING_MESSAGE_END",
112
+ "REASONING_END",
113
+ "REASONING_ENCRYPTED_VALUE",
114
+ ] as const;
115
+
116
+ type SanitizedValue =
117
+ | string
118
+ | number
119
+ | boolean
120
+ | null
121
+ | SanitizedValue[]
122
+ | { [key: string]: SanitizedValue };
123
+
124
+ type InspectorToolCall = {
125
+ id?: string;
126
+ function?: {
127
+ name?: string;
128
+ arguments?: SanitizedValue | string;
129
+ };
130
+ toolName?: string;
131
+ status?: string;
132
+ };
133
+
134
+ type InspectorMessage = {
135
+ id?: string;
136
+ role: string;
137
+ contentText: string;
138
+ contentRaw?: SanitizedValue;
139
+ toolCalls: InspectorToolCall[];
140
+ };
141
+
142
+ type InspectorToolDefinition = {
143
+ agentId: string;
144
+ name: string;
145
+ description?: string;
146
+ parameters?: unknown;
147
+ type: "handler" | "renderer";
148
+ };
149
+
150
+ type InspectorEvent = {
151
+ id: string;
152
+ agentId: string;
153
+ type: InspectorAgentEventType;
154
+ timestamp: number;
155
+ payload: SanitizedValue;
156
+ };
157
+
158
+ export class WebInspectorElement extends LitElement {
159
+ static properties = {
160
+ core: { attribute: false },
161
+ autoAttachCore: { type: Boolean, attribute: "auto-attach-core" },
162
+ } as const;
163
+
164
+ private _core: CopilotKitCore | null = null;
165
+ private coreSubscriber: CopilotKitCoreSubscriber | null = null;
166
+ private coreUnsubscribe: (() => void) | null = null;
167
+ private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null;
168
+ private coreProperties: Readonly<Record<string, unknown>> = {};
169
+ private lastCoreError: {
170
+ code: CopilotKitCoreErrorCode;
171
+ message: string;
172
+ } | null = null;
173
+ private agentSubscriptions: Map<string, () => void> = new Map();
174
+ private agentEvents: Map<string, InspectorEvent[]> = new Map();
175
+ private agentMessages: Map<string, InspectorMessage[]> = new Map();
176
+ private agentStates: Map<string, SanitizedValue> = new Map();
177
+ private flattenedEvents: InspectorEvent[] = [];
178
+ private eventCounter = 0;
179
+ private contextStore: Record<
180
+ string,
181
+ { description?: string; value: unknown }
182
+ > = {};
183
+
184
+ private pointerId: number | null = null;
185
+ private dragStart: Position | null = null;
186
+ private dragOffset: Position = { x: 0, y: 0 };
187
+ private isDragging = false;
188
+ private pointerContext: ContextKey | null = null;
189
+ private isOpen = false;
190
+ private draggedDuringInteraction = false;
191
+ private ignoreNextButtonClick = false;
192
+ private selectedMenu: MenuKey = "ag-ui-events";
193
+ private contextMenuOpen = false;
194
+ private dockMode: DockMode = "floating";
195
+ private previousBodyMargins: { left: string; bottom: string } | null = null;
196
+ private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;
197
+ private bodyTransitionTimeoutIds: Set<ReturnType<typeof setTimeout>> =
198
+ new Set();
199
+ private pendingSelectedContext: string | null = null;
200
+ private autoAttachCore = true;
201
+ private attemptedAutoAttach = false;
202
+ private cachedTools: InspectorToolDefinition[] = [];
203
+ private toolSignature = "";
204
+ private eventFilterText = "";
205
+ private eventTypeFilter: InspectorAgentEventType | "all" = "all";
206
+
207
+ private announcementMarkdown: string | null = null;
208
+ private announcementHtml: string | null = null;
209
+ private announcementTimestamp: string | null = null;
210
+ private announcementPreviewText: string | null = null;
211
+ private hasUnseenAnnouncement = false;
212
+ private announcementLoaded = false;
213
+ private announcementLoadError: unknown = null;
214
+ private announcementPromise: Promise<void> | null = null;
215
+ private showAnnouncementPreview = true;
216
+
217
+ get core(): CopilotKitCore | null {
218
+ return this._core;
219
+ }
220
+
221
+ set core(value: CopilotKitCore | null) {
222
+ const oldValue = this._core;
223
+ if (oldValue === value) {
224
+ return;
225
+ }
226
+
227
+ this.detachFromCore();
228
+
229
+ this._core = value ?? null;
230
+ this.requestUpdate("core", oldValue);
231
+
232
+ if (this._core) {
233
+ this.attachToCore(this._core);
234
+ }
235
+ }
236
+
237
+ private readonly contextState: Record<ContextKey, ContextState> = {
238
+ button: {
239
+ position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
240
+ size: { ...DEFAULT_BUTTON_SIZE },
241
+ anchor: { horizontal: "right", vertical: "top" },
242
+ anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
243
+ },
244
+ window: {
245
+ position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
246
+ size: { ...DEFAULT_WINDOW_SIZE },
247
+ anchor: { horizontal: "right", vertical: "top" },
248
+ anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
249
+ },
250
+ };
251
+
252
+ private hasCustomPosition: Record<ContextKey, boolean> = {
253
+ button: false,
254
+ window: false,
255
+ };
256
+
257
+ private resizePointerId: number | null = null;
258
+ private resizeStart: Position | null = null;
259
+ private resizeInitialSize: { width: number; height: number } | null = null;
260
+ private isResizing = false;
261
+
262
+ private readonly menuItems: MenuItem[] = [
263
+ { key: "ag-ui-events", label: "AG-UI Events", icon: "Zap" },
264
+ { key: "agents", label: "Agent", icon: "Bot" },
265
+ { key: "frontend-tools", label: "Frontend Tools", icon: "Hammer" },
266
+ { key: "agent-context", label: "Context", icon: "FileText" },
267
+ ];
268
+
269
+ private attachToCore(core: CopilotKitCore): void {
270
+ this.runtimeStatus = core.runtimeConnectionStatus;
271
+ this.coreProperties = core.properties;
272
+ this.lastCoreError = null;
273
+
274
+ this.coreSubscriber = {
275
+ onRuntimeConnectionStatusChanged: ({ status }) => {
276
+ this.runtimeStatus = status;
277
+ this.requestUpdate();
278
+ },
279
+ onPropertiesChanged: ({ properties }) => {
280
+ this.coreProperties = properties;
281
+ this.requestUpdate();
282
+ },
283
+ onError: ({ code, error }) => {
284
+ this.lastCoreError = { code, message: error.message };
285
+ this.requestUpdate();
286
+ },
287
+ onAgentsChanged: ({ agents }) => {
288
+ this.processAgentsChanged(agents);
289
+ },
290
+ onContextChanged: ({ context }) => {
291
+ this.contextStore = this.normalizeContextStore(context);
292
+ this.requestUpdate();
293
+ },
294
+ } satisfies CopilotKitCoreSubscriber;
295
+
296
+ this.coreUnsubscribe = core.subscribe(this.coreSubscriber).unsubscribe;
297
+ this.processAgentsChanged(core.agents);
298
+
299
+ // Initialize context from core
300
+ if (core.context) {
301
+ this.contextStore = this.normalizeContextStore(core.context);
302
+ }
303
+ }
304
+
305
+ private detachFromCore(): void {
306
+ if (this.coreUnsubscribe) {
307
+ this.coreUnsubscribe();
308
+ this.coreUnsubscribe = null;
309
+ }
310
+ this.coreSubscriber = null;
311
+ this.runtimeStatus = null;
312
+ this.lastCoreError = null;
313
+ this.coreProperties = {};
314
+ this.cachedTools = [];
315
+ this.toolSignature = "";
316
+ this.teardownAgentSubscriptions();
317
+ }
318
+
319
+ private teardownAgentSubscriptions(): void {
320
+ for (const unsubscribe of this.agentSubscriptions.values()) {
321
+ unsubscribe();
322
+ }
323
+ this.agentSubscriptions.clear();
324
+ this.agentEvents.clear();
325
+ this.agentMessages.clear();
326
+ this.agentStates.clear();
327
+ this.flattenedEvents = [];
328
+ this.eventCounter = 0;
329
+ }
330
+
331
+ private processAgentsChanged(
332
+ agents: Readonly<Record<string, AbstractAgent>>,
333
+ ): void {
334
+ const seenAgentIds = new Set<string>();
335
+
336
+ for (const agent of Object.values(agents)) {
337
+ if (!agent?.agentId) {
338
+ continue;
339
+ }
340
+ seenAgentIds.add(agent.agentId);
341
+ this.subscribeToAgent(agent);
342
+ }
343
+
344
+ for (const agentId of Array.from(this.agentSubscriptions.keys())) {
345
+ if (!seenAgentIds.has(agentId)) {
346
+ this.unsubscribeFromAgent(agentId);
347
+ this.agentEvents.delete(agentId);
348
+ this.agentMessages.delete(agentId);
349
+ this.agentStates.delete(agentId);
350
+ }
351
+ }
352
+
353
+ this.updateContextOptions(seenAgentIds);
354
+ this.refreshToolsSnapshot();
355
+ this.requestUpdate();
356
+ }
357
+
358
+ private refreshToolsSnapshot(): void {
359
+ if (!this._core) {
360
+ if (this.cachedTools.length > 0) {
361
+ this.cachedTools = [];
362
+ this.toolSignature = "";
363
+ this.requestUpdate();
364
+ }
365
+ return;
366
+ }
367
+
368
+ const tools = this.extractToolsFromAgents();
369
+ const signature = JSON.stringify(
370
+ tools.map((tool) => ({
371
+ agentId: tool.agentId,
372
+ name: tool.name,
373
+ type: tool.type,
374
+ hasDescription: Boolean(tool.description),
375
+ hasParameters: Boolean(tool.parameters),
376
+ })),
377
+ );
378
+
379
+ if (signature !== this.toolSignature) {
380
+ this.toolSignature = signature;
381
+ this.cachedTools = tools;
382
+ this.requestUpdate();
383
+ }
384
+ }
385
+
386
+ private tryAutoAttachCore(): void {
387
+ if (
388
+ this.attemptedAutoAttach ||
389
+ this._core ||
390
+ !this.autoAttachCore ||
391
+ typeof window === "undefined"
392
+ ) {
393
+ return;
394
+ }
395
+
396
+ this.attemptedAutoAttach = true;
397
+
398
+ const globalWindow = window as unknown as Record<string, unknown>;
399
+ const globalCandidates: Array<unknown> = [
400
+ // Common app-level globals used during development
401
+ globalWindow.__COPILOTKIT_CORE__,
402
+ (globalWindow.copilotkit as { core?: unknown } | undefined)?.core,
403
+ globalWindow.copilotkitCore,
404
+ ];
405
+
406
+ const foundCore = globalCandidates.find(
407
+ (candidate): candidate is CopilotKitCore =>
408
+ !!candidate && typeof candidate === "object",
409
+ );
410
+
411
+ if (foundCore) {
412
+ this.core = foundCore;
413
+ }
414
+ }
415
+
416
+ private subscribeToAgent(agent: AbstractAgent): void {
417
+ if (!agent.agentId) {
418
+ return;
419
+ }
420
+
421
+ const agentId = agent.agentId;
422
+
423
+ this.unsubscribeFromAgent(agentId);
424
+
425
+ const subscriber: AgentSubscriber = {
426
+ onRunStartedEvent: ({ event }) => {
427
+ this.recordAgentEvent(agentId, "RUN_STARTED", event);
428
+ },
429
+ onRunFinishedEvent: ({ event, result }) => {
430
+ this.recordAgentEvent(agentId, "RUN_FINISHED", { event, result });
431
+ },
432
+ onRunErrorEvent: ({ event }) => {
433
+ this.recordAgentEvent(agentId, "RUN_ERROR", event);
434
+ },
435
+ onTextMessageStartEvent: ({ event }) => {
436
+ this.recordAgentEvent(agentId, "TEXT_MESSAGE_START", event);
437
+ },
438
+ onTextMessageContentEvent: ({ event, textMessageBuffer }) => {
439
+ this.recordAgentEvent(agentId, "TEXT_MESSAGE_CONTENT", {
440
+ event,
441
+ textMessageBuffer,
442
+ });
443
+ },
444
+ onTextMessageEndEvent: ({ event, textMessageBuffer }) => {
445
+ this.recordAgentEvent(agentId, "TEXT_MESSAGE_END", {
446
+ event,
447
+ textMessageBuffer,
448
+ });
449
+ },
450
+ onToolCallStartEvent: ({ event }) => {
451
+ this.recordAgentEvent(agentId, "TOOL_CALL_START", event);
452
+ },
453
+ onToolCallArgsEvent: ({
454
+ event,
455
+ toolCallBuffer,
456
+ toolCallName,
457
+ partialToolCallArgs,
458
+ }) => {
459
+ this.recordAgentEvent(agentId, "TOOL_CALL_ARGS", {
460
+ event,
461
+ toolCallBuffer,
462
+ toolCallName,
463
+ partialToolCallArgs,
464
+ });
465
+ },
466
+ onToolCallEndEvent: ({ event, toolCallArgs, toolCallName }) => {
467
+ this.recordAgentEvent(agentId, "TOOL_CALL_END", {
468
+ event,
469
+ toolCallArgs,
470
+ toolCallName,
471
+ });
472
+ },
473
+ onToolCallResultEvent: ({ event }) => {
474
+ this.recordAgentEvent(agentId, "TOOL_CALL_RESULT", event);
475
+ },
476
+ onStateSnapshotEvent: ({ event }) => {
477
+ this.recordAgentEvent(agentId, "STATE_SNAPSHOT", event);
478
+ this.syncAgentState(agent);
479
+ },
480
+ onStateDeltaEvent: ({ event }) => {
481
+ this.recordAgentEvent(agentId, "STATE_DELTA", event);
482
+ this.syncAgentState(agent);
483
+ },
484
+ onMessagesSnapshotEvent: ({ event }) => {
485
+ this.recordAgentEvent(agentId, "MESSAGES_SNAPSHOT", event);
486
+ this.syncAgentMessages(agent);
487
+ },
488
+ onMessagesChanged: () => {
489
+ this.syncAgentMessages(agent);
490
+ },
491
+ onRawEvent: ({ event }) => {
492
+ this.recordAgentEvent(agentId, "RAW_EVENT", event);
493
+ },
494
+ onCustomEvent: ({ event }) => {
495
+ this.recordAgentEvent(agentId, "CUSTOM_EVENT", event);
496
+ },
497
+ onReasoningStartEvent: ({ event }) => {
498
+ this.recordAgentEvent(agentId, "REASONING_START", event);
499
+ },
500
+ onReasoningMessageStartEvent: ({ event }) => {
501
+ this.recordAgentEvent(agentId, "REASONING_MESSAGE_START", event);
502
+ },
503
+ onReasoningMessageContentEvent: ({ event, reasoningMessageBuffer }) => {
504
+ this.recordAgentEvent(agentId, "REASONING_MESSAGE_CONTENT", {
505
+ event,
506
+ reasoningMessageBuffer,
507
+ });
508
+ },
509
+ onReasoningMessageEndEvent: ({ event, reasoningMessageBuffer }) => {
510
+ this.recordAgentEvent(agentId, "REASONING_MESSAGE_END", {
511
+ event,
512
+ reasoningMessageBuffer,
513
+ });
514
+ },
515
+ onReasoningEndEvent: ({ event }) => {
516
+ this.recordAgentEvent(agentId, "REASONING_END", event);
517
+ },
518
+ onReasoningEncryptedValueEvent: ({ event }) => {
519
+ this.recordAgentEvent(agentId, "REASONING_ENCRYPTED_VALUE", event);
520
+ },
521
+ };
522
+
523
+ const { unsubscribe } = agent.subscribe(subscriber);
524
+ this.agentSubscriptions.set(agentId, unsubscribe);
525
+ this.syncAgentMessages(agent);
526
+ this.syncAgentState(agent);
527
+
528
+ if (!this.agentEvents.has(agentId)) {
529
+ this.agentEvents.set(agentId, []);
530
+ }
531
+ }
532
+
533
+ private unsubscribeFromAgent(agentId: string): void {
534
+ const unsubscribe = this.agentSubscriptions.get(agentId);
535
+ if (unsubscribe) {
536
+ unsubscribe();
537
+ this.agentSubscriptions.delete(agentId);
538
+ }
539
+ }
540
+
541
+ private recordAgentEvent(
542
+ agentId: string,
543
+ type: InspectorAgentEventType,
544
+ payload: unknown,
545
+ ): void {
546
+ const eventId = `${agentId}:${++this.eventCounter}`;
547
+ const normalizedPayload = this.normalizeEventPayload(type, payload);
548
+ const event: InspectorEvent = {
549
+ id: eventId,
550
+ agentId,
551
+ type,
552
+ timestamp: Date.now(),
553
+ payload: normalizedPayload,
554
+ };
555
+
556
+ const currentAgentEvents = this.agentEvents.get(agentId) ?? [];
557
+ const nextAgentEvents = [event, ...currentAgentEvents].slice(
558
+ 0,
559
+ MAX_AGENT_EVENTS,
560
+ );
561
+ this.agentEvents.set(agentId, nextAgentEvents);
562
+
563
+ this.flattenedEvents = [event, ...this.flattenedEvents].slice(
564
+ 0,
565
+ MAX_TOTAL_EVENTS,
566
+ );
567
+ this.refreshToolsSnapshot();
568
+ this.requestUpdate();
569
+ }
570
+
571
+ private syncAgentMessages(agent: AbstractAgent): void {
572
+ if (!agent?.agentId) {
573
+ return;
574
+ }
575
+
576
+ const messages = this.normalizeAgentMessages(
577
+ (agent as { messages?: unknown }).messages,
578
+ );
579
+ if (messages) {
580
+ this.agentMessages.set(agent.agentId, messages);
581
+ } else {
582
+ this.agentMessages.delete(agent.agentId);
583
+ }
584
+
585
+ this.requestUpdate();
586
+ }
587
+
588
+ private syncAgentState(agent: AbstractAgent): void {
589
+ if (!agent?.agentId) {
590
+ return;
591
+ }
592
+
593
+ const state = (agent as { state?: unknown }).state;
594
+
595
+ if (state === undefined || state === null) {
596
+ this.agentStates.delete(agent.agentId);
597
+ } else {
598
+ this.agentStates.set(agent.agentId, this.sanitizeForLogging(state));
599
+ }
600
+
601
+ this.requestUpdate();
602
+ }
603
+
604
+ private updateContextOptions(agentIds: Set<string>): void {
605
+ const nextOptions: Array<{ key: string; label: string }> = [
606
+ { key: "all-agents", label: "All Agents" },
607
+ ...Array.from(agentIds)
608
+ .sort((a, b) => a.localeCompare(b))
609
+ .map((id) => ({ key: id, label: id })),
610
+ ];
611
+
612
+ const optionsChanged =
613
+ this.contextOptions.length !== nextOptions.length ||
614
+ this.contextOptions.some(
615
+ (option, index) => option.key !== nextOptions[index]?.key,
616
+ );
617
+
618
+ if (optionsChanged) {
619
+ this.contextOptions = nextOptions;
620
+ }
621
+
622
+ const pendingContext = this.pendingSelectedContext;
623
+ if (pendingContext) {
624
+ const isPendingAvailable =
625
+ pendingContext === "all-agents" || agentIds.has(pendingContext);
626
+ if (isPendingAvailable) {
627
+ if (this.selectedContext !== pendingContext) {
628
+ this.selectedContext = pendingContext;
629
+ this.expandedRows.clear();
630
+ }
631
+ this.pendingSelectedContext = null;
632
+ } else if (agentIds.size > 0) {
633
+ // Agents are loaded but the pending selection no longer exists
634
+ this.pendingSelectedContext = null;
635
+ }
636
+ }
637
+
638
+ const hasSelectedContext = nextOptions.some(
639
+ (option) => option.key === this.selectedContext,
640
+ );
641
+
642
+ if (!hasSelectedContext && this.pendingSelectedContext === null) {
643
+ // Auto-select "default" agent if it exists, otherwise first agent, otherwise "all-agents"
644
+ let nextSelected: string = "all-agents";
645
+
646
+ if (agentIds.has("default")) {
647
+ nextSelected = "default";
648
+ } else if (agentIds.size > 0) {
649
+ nextSelected = Array.from(agentIds).sort((a, b) =>
650
+ a.localeCompare(b),
651
+ )[0]!;
652
+ }
653
+
654
+ if (this.selectedContext !== nextSelected) {
655
+ this.selectedContext = nextSelected;
656
+ this.expandedRows.clear();
657
+ this.persistState();
658
+ }
659
+ }
660
+ }
661
+
662
+ private getEventsForSelectedContext(): InspectorEvent[] {
663
+ if (this.selectedContext === "all-agents") {
664
+ return this.flattenedEvents;
665
+ }
666
+
667
+ return this.agentEvents.get(this.selectedContext) ?? [];
668
+ }
669
+
670
+ private filterEvents(events: InspectorEvent[]): InspectorEvent[] {
671
+ const query = this.eventFilterText.trim().toLowerCase();
672
+
673
+ return events.filter((event) => {
674
+ if (
675
+ this.eventTypeFilter !== "all" &&
676
+ event.type !== this.eventTypeFilter
677
+ ) {
678
+ return false;
679
+ }
680
+
681
+ if (!query) {
682
+ return true;
683
+ }
684
+
685
+ const payloadText = this.stringifyPayload(
686
+ event.payload,
687
+ false,
688
+ ).toLowerCase();
689
+ return (
690
+ event.type.toLowerCase().includes(query) ||
691
+ event.agentId.toLowerCase().includes(query) ||
692
+ payloadText.includes(query)
693
+ );
694
+ });
695
+ }
696
+
697
+ private getLatestStateForAgent(agentId: string): SanitizedValue | null {
698
+ if (this.agentStates.has(agentId)) {
699
+ const value = this.agentStates.get(agentId);
700
+ return value === undefined ? null : value;
701
+ }
702
+
703
+ const events = this.agentEvents.get(agentId) ?? [];
704
+ const stateEvent = events.find((e) => e.type === "STATE_SNAPSHOT");
705
+ if (!stateEvent) {
706
+ return null;
707
+ }
708
+ return stateEvent.payload;
709
+ }
710
+
711
+ private getLatestMessagesForAgent(
712
+ agentId: string,
713
+ ): InspectorMessage[] | null {
714
+ const messages = this.agentMessages.get(agentId);
715
+ return messages ?? null;
716
+ }
717
+
718
+ private getAgentStatus(agentId: string): "running" | "idle" | "error" {
719
+ const events = this.agentEvents.get(agentId) ?? [];
720
+ if (events.length === 0) {
721
+ return "idle";
722
+ }
723
+
724
+ // Check most recent run-related event
725
+ const runEvent = events.find(
726
+ (e) =>
727
+ e.type === "RUN_STARTED" ||
728
+ e.type === "RUN_FINISHED" ||
729
+ e.type === "RUN_ERROR",
730
+ );
731
+
732
+ if (!runEvent) {
733
+ return "idle";
734
+ }
735
+
736
+ if (runEvent.type === "RUN_ERROR") {
737
+ return "error";
738
+ }
739
+
740
+ if (runEvent.type === "RUN_STARTED") {
741
+ // Check if there's a RUN_FINISHED after this
742
+ const finishedAfter = events.find(
743
+ (e) => e.type === "RUN_FINISHED" && e.timestamp > runEvent.timestamp,
744
+ );
745
+ return finishedAfter ? "idle" : "running";
746
+ }
747
+
748
+ return "idle";
749
+ }
750
+
751
+ private getAgentStats(agentId: string): {
752
+ totalEvents: number;
753
+ lastActivity: number | null;
754
+ messages: number;
755
+ toolCalls: number;
756
+ errors: number;
757
+ } {
758
+ const events = this.agentEvents.get(agentId) ?? [];
759
+
760
+ const messages = this.agentMessages.get(agentId);
761
+
762
+ const toolCallCount = messages
763
+ ? messages.reduce(
764
+ (count, message) => count + (message.toolCalls?.length ?? 0),
765
+ 0,
766
+ )
767
+ : events.filter((e) => e.type === "TOOL_CALL_END").length;
768
+
769
+ const messageCount = messages?.length ?? 0;
770
+
771
+ return {
772
+ totalEvents: events.length,
773
+ lastActivity: events[0]?.timestamp ?? null,
774
+ messages: messageCount,
775
+ toolCalls: toolCallCount,
776
+ errors: events.filter((e) => e.type === "RUN_ERROR").length,
777
+ };
778
+ }
779
+
780
+ private renderToolCallDetails(toolCalls: InspectorToolCall[]) {
781
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
782
+ return nothing;
783
+ }
784
+
785
+ return html`
786
+ <div class="mt-2 space-y-2">
787
+ ${toolCalls.map((call, index) => {
788
+ const functionName =
789
+ call.function?.name ?? call.toolName ?? "Unknown function";
790
+ const callId =
791
+ typeof call?.id === "string" ? call.id : `tool-call-${index + 1}`;
792
+ const argsString = this.formatToolCallArguments(
793
+ call.function?.arguments,
794
+ );
795
+ return html`
796
+ <div
797
+ class="rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-700"
798
+ >
799
+ <div
800
+ class="flex flex-wrap items-center justify-between gap-1 font-medium text-gray-900"
801
+ >
802
+ <span>${functionName}</span>
803
+ <span class="text-[10px] text-gray-500">ID: ${callId}</span>
804
+ </div>
805
+ ${
806
+ argsString
807
+ ? html`<pre
808
+ class="mt-2 overflow-auto rounded bg-white p-2 text-[11px] leading-relaxed text-gray-800"
809
+ >
810
+ ${argsString}</pre
811
+ >`
812
+ : nothing
813
+ }
814
+ </div>
815
+ `;
816
+ })}
817
+ </div>
818
+ `;
819
+ }
820
+
821
+ private formatToolCallArguments(args: unknown): string | null {
822
+ if (args === undefined || args === null || args === "") {
823
+ return null;
824
+ }
825
+
826
+ if (typeof args === "string") {
827
+ try {
828
+ const parsed = JSON.parse(args);
829
+ return JSON.stringify(parsed, null, 2);
830
+ } catch {
831
+ return args;
832
+ }
833
+ }
834
+
835
+ if (typeof args === "object") {
836
+ try {
837
+ return JSON.stringify(args, null, 2);
838
+ } catch {
839
+ return String(args);
840
+ }
841
+ }
842
+
843
+ return String(args);
844
+ }
845
+
846
+ private hasRenderableState(state: unknown): boolean {
847
+ if (state === null || state === undefined) {
848
+ return false;
849
+ }
850
+
851
+ if (Array.isArray(state)) {
852
+ return state.length > 0;
853
+ }
854
+
855
+ if (typeof state === "object") {
856
+ return Object.keys(state as Record<string, unknown>).length > 0;
857
+ }
858
+
859
+ if (typeof state === "string") {
860
+ const trimmed = state.trim();
861
+ return trimmed.length > 0 && trimmed !== "{}";
862
+ }
863
+
864
+ return true;
865
+ }
866
+
867
+ private formatStateForDisplay(state: unknown): string {
868
+ if (state === null || state === undefined) {
869
+ return "";
870
+ }
871
+
872
+ if (typeof state === "string") {
873
+ const trimmed = state.trim();
874
+ if (trimmed.length === 0) {
875
+ return "";
876
+ }
877
+ try {
878
+ const parsed = JSON.parse(trimmed);
879
+ return JSON.stringify(parsed, null, 2);
880
+ } catch {
881
+ return state;
882
+ }
883
+ }
884
+
885
+ if (typeof state === "object") {
886
+ try {
887
+ return JSON.stringify(state, null, 2);
888
+ } catch {
889
+ return String(state);
890
+ }
891
+ }
892
+
893
+ return String(state);
894
+ }
895
+
896
+ private getEventBadgeClasses(type: string): string {
897
+ const base =
898
+ "font-mono text-[10px] font-medium inline-flex items-center rounded-sm px-1.5 py-0.5 border";
899
+
900
+ if (type.startsWith("RUN_")) {
901
+ return `${base} bg-blue-50 text-blue-700 border-blue-200`;
902
+ }
903
+
904
+ if (type.startsWith("TEXT_MESSAGE")) {
905
+ return `${base} bg-emerald-50 text-emerald-700 border-emerald-200`;
906
+ }
907
+
908
+ if (type.startsWith("TOOL_CALL")) {
909
+ return `${base} bg-amber-50 text-amber-700 border-amber-200`;
910
+ }
911
+
912
+ if (type.startsWith("REASONING")) {
913
+ return `${base} bg-fuchsia-50 text-fuchsia-700 border-fuchsia-200`;
914
+ }
915
+
916
+ if (type.startsWith("STATE")) {
917
+ return `${base} bg-violet-50 text-violet-700 border-violet-200`;
918
+ }
919
+
920
+ if (type.startsWith("MESSAGES")) {
921
+ return `${base} bg-sky-50 text-sky-700 border-sky-200`;
922
+ }
923
+
924
+ if (type === "RUN_ERROR") {
925
+ return `${base} bg-rose-50 text-rose-700 border-rose-200`;
926
+ }
927
+
928
+ return `${base} bg-gray-100 text-gray-600 border-gray-200`;
929
+ }
930
+
931
+ private stringifyPayload(payload: unknown, pretty: boolean): string {
932
+ try {
933
+ if (payload === undefined) {
934
+ return pretty ? "undefined" : "undefined";
935
+ }
936
+ if (typeof payload === "string") {
937
+ return payload;
938
+ }
939
+ return JSON.stringify(payload, null, pretty ? 2 : 0) ?? "";
940
+ } catch (error) {
941
+ console.warn("Failed to stringify inspector payload", error);
942
+ return String(payload);
943
+ }
944
+ }
945
+
946
+ private extractEventFromPayload(payload: unknown): unknown {
947
+ // If payload is an object with an 'event' field, extract it
948
+ if (payload && typeof payload === "object" && "event" in payload) {
949
+ return (payload as Record<string, unknown>).event;
950
+ }
951
+ // Otherwise, assume the payload itself is the event
952
+ return payload;
953
+ }
954
+
955
+ private async copyToClipboard(text: string, eventId: string): Promise<void> {
956
+ try {
957
+ await navigator.clipboard.writeText(text);
958
+ this.copiedEvents.add(eventId);
959
+ this.requestUpdate();
960
+
961
+ // Clear the "copied" state after 2 seconds
962
+ setTimeout(() => {
963
+ this.copiedEvents.delete(eventId);
964
+ this.requestUpdate();
965
+ }, 2000);
966
+ } catch (err) {
967
+ console.error("Failed to copy to clipboard:", err);
968
+ }
969
+ }
970
+
971
+ static styles = [
972
+ unsafeCSS(tailwindStyles),
973
+ css`
974
+ :host {
975
+ position: fixed;
976
+ top: 0;
977
+ left: 0;
978
+ z-index: 2147483646;
979
+ display: block;
980
+ will-change: transform;
981
+ }
982
+
983
+ :host([data-transitioning="true"]) {
984
+ transition: transform 300ms ease;
985
+ }
986
+
987
+ .console-button {
988
+ transition:
989
+ transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
990
+ opacity 160ms ease;
991
+ }
992
+
993
+ .console-button[data-dragging="true"] {
994
+ transition: opacity 160ms ease;
995
+ }
996
+
997
+ .inspector-window[data-transitioning="true"] {
998
+ transition:
999
+ width 300ms ease,
1000
+ height 300ms ease;
1001
+ }
1002
+
1003
+ .inspector-window[data-docked="true"] {
1004
+ border-radius: 0 !important;
1005
+ box-shadow: none !important;
1006
+ }
1007
+
1008
+ .resize-handle {
1009
+ touch-action: none;
1010
+ user-select: none;
1011
+ }
1012
+
1013
+ .dock-resize-handle {
1014
+ position: absolute;
1015
+ top: 0;
1016
+ right: 0;
1017
+ width: 10px;
1018
+ height: 100%;
1019
+ cursor: ew-resize;
1020
+ touch-action: none;
1021
+ z-index: 50;
1022
+ background: transparent;
1023
+ }
1024
+
1025
+ .tooltip-target {
1026
+ position: relative;
1027
+ }
1028
+
1029
+ .tooltip-target::after {
1030
+ content: attr(data-tooltip);
1031
+ position: absolute;
1032
+ top: calc(100% + 6px);
1033
+ left: 50%;
1034
+ transform: translateX(-50%) translateY(-4px);
1035
+ white-space: nowrap;
1036
+ background: rgba(17, 24, 39, 0.95);
1037
+ color: white;
1038
+ padding: 4px 8px;
1039
+ border-radius: 6px;
1040
+ font-size: 10px;
1041
+ line-height: 1.2;
1042
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
1043
+ opacity: 0;
1044
+ pointer-events: none;
1045
+ transition:
1046
+ opacity 120ms ease,
1047
+ transform 120ms ease;
1048
+ z-index: 4000;
1049
+ }
1050
+
1051
+ .tooltip-target:hover::after {
1052
+ opacity: 1;
1053
+ transform: translateX(-50%) translateY(0);
1054
+ }
1055
+
1056
+ .announcement-preview {
1057
+ position: absolute;
1058
+ top: 50%;
1059
+ transform: translateY(-50%);
1060
+ min-width: 300px;
1061
+ max-width: 300px;
1062
+ background: white;
1063
+ color: #111827;
1064
+ font-size: 13px;
1065
+ line-height: 1.4;
1066
+ border-radius: 12px;
1067
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.22);
1068
+ padding: 10px 12px;
1069
+ display: inline-flex;
1070
+ align-items: flex-start;
1071
+ gap: 8px;
1072
+ z-index: 4500;
1073
+ animation: fade-slide-in 160ms ease;
1074
+ border: 1px solid rgba(148, 163, 184, 0.35);
1075
+ white-space: normal;
1076
+ word-break: break-word;
1077
+ text-align: left;
1078
+ }
1079
+
1080
+ .announcement-preview[data-side="left"] {
1081
+ right: 100%;
1082
+ margin-right: 10px;
1083
+ }
1084
+
1085
+ .announcement-preview[data-side="right"] {
1086
+ left: 100%;
1087
+ margin-left: 10px;
1088
+ }
1089
+
1090
+ .announcement-preview__arrow {
1091
+ position: absolute;
1092
+ width: 10px;
1093
+ height: 10px;
1094
+ background: white;
1095
+ border: 1px solid rgba(148, 163, 184, 0.35);
1096
+ transform: rotate(45deg);
1097
+ top: 50%;
1098
+ margin-top: -5px;
1099
+ z-index: -1;
1100
+ }
1101
+
1102
+ .announcement-preview[data-side="left"] .announcement-preview__arrow {
1103
+ right: -5px;
1104
+ box-shadow: 6px -6px 10px rgba(15, 23, 42, 0.12);
1105
+ }
1106
+
1107
+ .announcement-preview[data-side="right"] .announcement-preview__arrow {
1108
+ left: -5px;
1109
+ box-shadow: -6px 6px 10px rgba(15, 23, 42, 0.12);
1110
+ }
1111
+
1112
+ .announcement-dismiss {
1113
+ color: #6b7280;
1114
+ font-size: 12px;
1115
+ padding: 2px 8px;
1116
+ border-radius: 8px;
1117
+ border: 1px solid rgba(148, 163, 184, 0.5);
1118
+ background: rgba(248, 250, 252, 0.9);
1119
+ transition:
1120
+ background 120ms ease,
1121
+ color 120ms ease;
1122
+ }
1123
+
1124
+ .announcement-dismiss:hover {
1125
+ background: rgba(241, 245, 249, 1);
1126
+ color: #111827;
1127
+ }
1128
+
1129
+ .announcement-content {
1130
+ color: #111827;
1131
+ font-size: 14px;
1132
+ line-height: 1.6;
1133
+ }
1134
+
1135
+ .announcement-content h1,
1136
+ .announcement-content h2,
1137
+ .announcement-content h3 {
1138
+ font-weight: 700;
1139
+ margin: 0.4rem 0 0.2rem;
1140
+ }
1141
+
1142
+ .announcement-content h1 {
1143
+ font-size: 1.1rem;
1144
+ }
1145
+
1146
+ .announcement-content h2 {
1147
+ font-size: 1rem;
1148
+ }
1149
+
1150
+ .announcement-content h3 {
1151
+ font-size: 0.95rem;
1152
+ }
1153
+
1154
+ .announcement-content p {
1155
+ margin: 0.25rem 0;
1156
+ }
1157
+
1158
+ .announcement-content ul {
1159
+ list-style: disc;
1160
+ padding-left: 1.25rem;
1161
+ margin: 0.3rem 0;
1162
+ }
1163
+
1164
+ .announcement-content ol {
1165
+ list-style: decimal;
1166
+ padding-left: 1.25rem;
1167
+ margin: 0.3rem 0;
1168
+ }
1169
+
1170
+ .announcement-content a {
1171
+ color: #0f766e;
1172
+ text-decoration: underline;
1173
+ }
1174
+ `,
1175
+ ];
1176
+
1177
+ connectedCallback(): void {
1178
+ super.connectedCallback();
1179
+ if (typeof window !== "undefined") {
1180
+ window.addEventListener("resize", this.handleResize);
1181
+ window.addEventListener(
1182
+ "pointerdown",
1183
+ this.handleGlobalPointerDown as EventListener,
1184
+ );
1185
+
1186
+ // Load state early (before first render) so menu selection is correct
1187
+ this.hydrateStateFromStorageEarly();
1188
+ this.tryAutoAttachCore();
1189
+ this.ensureAnnouncementLoading();
1190
+ }
1191
+ }
1192
+
1193
+ disconnectedCallback(): void {
1194
+ super.disconnectedCallback();
1195
+ if (typeof window !== "undefined") {
1196
+ window.removeEventListener("resize", this.handleResize);
1197
+ window.removeEventListener(
1198
+ "pointerdown",
1199
+ this.handleGlobalPointerDown as EventListener,
1200
+ );
1201
+ }
1202
+ // Clear pending body-transition timers to prevent post-teardown errors
1203
+ for (const id of this.bodyTransitionTimeoutIds) {
1204
+ clearTimeout(id);
1205
+ }
1206
+ this.bodyTransitionTimeoutIds.clear();
1207
+ if (this.transitionTimeoutId !== null) {
1208
+ clearTimeout(this.transitionTimeoutId);
1209
+ this.transitionTimeoutId = null;
1210
+ }
1211
+ this.removeDockStyles(true); // Clean up any docking styles, skip transition
1212
+ this.detachFromCore();
1213
+ }
1214
+
1215
+ firstUpdated(): void {
1216
+ if (typeof window === "undefined") {
1217
+ return;
1218
+ }
1219
+
1220
+ if (!this._core) {
1221
+ this.tryAutoAttachCore();
1222
+ }
1223
+
1224
+ this.measureContext("button");
1225
+ this.measureContext("window");
1226
+
1227
+ this.contextState.button.anchor = { horizontal: "right", vertical: "top" };
1228
+ this.contextState.button.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
1229
+
1230
+ this.contextState.window.anchor = { horizontal: "right", vertical: "top" };
1231
+ this.contextState.window.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
1232
+
1233
+ this.hydrateStateFromStorage();
1234
+
1235
+ // Apply docking styles if open and docked (skip transition on initial load)
1236
+ if (this.isOpen && this.dockMode !== "floating") {
1237
+ this.applyDockStyles(true);
1238
+ }
1239
+
1240
+ this.applyAnchorPosition("button");
1241
+
1242
+ if (this.dockMode === "floating") {
1243
+ if (this.hasCustomPosition.window) {
1244
+ this.applyAnchorPosition("window");
1245
+ } else {
1246
+ this.centerContext("window");
1247
+ }
1248
+ }
1249
+
1250
+ this.ensureAnnouncementLoading();
1251
+
1252
+ this.updateHostTransform(this.isOpen ? "window" : "button");
1253
+ }
1254
+
1255
+ render() {
1256
+ return this.isOpen ? this.renderWindow() : this.renderButton();
1257
+ }
1258
+
1259
+ private renderButton() {
1260
+ const buttonClasses = [
1261
+ "console-button",
1262
+ "group",
1263
+ "relative",
1264
+ "pointer-events-auto",
1265
+ "inline-flex",
1266
+ "h-12",
1267
+ "w-12",
1268
+ "items-center",
1269
+ "justify-center",
1270
+ "rounded-full",
1271
+ "border",
1272
+ "border-white/20",
1273
+ "bg-slate-950/95",
1274
+ "text-xs",
1275
+ "font-medium",
1276
+ "text-white",
1277
+ "ring-1",
1278
+ "ring-white/10",
1279
+ "backdrop-blur-md",
1280
+ "transition",
1281
+ "hover:border-white/30",
1282
+ "hover:bg-slate-900/95",
1283
+ "hover:scale-105",
1284
+ "focus-visible:outline",
1285
+ "focus-visible:outline-2",
1286
+ "focus-visible:outline-offset-2",
1287
+ "focus-visible:outline-rose-500",
1288
+ "touch-none",
1289
+ "select-none",
1290
+ this.isDragging ? "cursor-grabbing" : "cursor-grab",
1291
+ ].join(" ");
1292
+
1293
+ return html`
1294
+ <button
1295
+ class=${buttonClasses}
1296
+ type="button"
1297
+ aria-label="Web Inspector"
1298
+ data-drag-context="button"
1299
+ data-dragging=${
1300
+ this.isDragging && this.pointerContext === "button" ? "true" : "false"
1301
+ }
1302
+ @pointerdown=${this.handlePointerDown}
1303
+ @pointermove=${this.handlePointerMove}
1304
+ @pointerup=${this.handlePointerUp}
1305
+ @pointercancel=${this.handlePointerCancel}
1306
+ @click=${this.handleButtonClick}
1307
+ >
1308
+ ${this.renderAnnouncementPreview()}
1309
+ <img
1310
+ src=${inspectorLogoIconUrl}
1311
+ alt="Inspector logo"
1312
+ class="h-5 w-auto"
1313
+ loading="lazy"
1314
+ />
1315
+ </button>
1316
+ `;
1317
+ }
1318
+
1319
+ private renderWindow() {
1320
+ const windowState = this.contextState.window;
1321
+ const isDocked = this.dockMode !== "floating";
1322
+ const isTransitioning = this.hasAttribute("data-transitioning");
1323
+
1324
+ const windowStyles = isDocked
1325
+ ? this.getDockedWindowStyles()
1326
+ : {
1327
+ width: `${Math.round(windowState.size.width)}px`,
1328
+ height: `${Math.round(windowState.size.height)}px`,
1329
+ minWidth: `${MIN_WINDOW_WIDTH}px`,
1330
+ minHeight: `${MIN_WINDOW_HEIGHT}px`,
1331
+ };
1332
+
1333
+ const hasContextDropdown = this.contextOptions.length > 0;
1334
+ const contextDropdown = hasContextDropdown
1335
+ ? this.renderContextDropdown()
1336
+ : nothing;
1337
+ const coreStatus = this.getCoreStatusSummary();
1338
+ const agentSelector = hasContextDropdown
1339
+ ? contextDropdown
1340
+ : html`
1341
+ <div
1342
+ class="flex items-center gap-2 rounded-md border border-dashed border-gray-200 px-2 py-1 text-xs text-gray-400"
1343
+ >
1344
+ <span>${this.renderIcon("Bot")}</span>
1345
+ <span class="truncate">No agents available</span>
1346
+ </div>
1347
+ `;
1348
+
1349
+ return html`
1350
+ <section
1351
+ class="inspector-window pointer-events-auto relative flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white text-gray-900 shadow-lg"
1352
+ style=${styleMap(windowStyles)}
1353
+ data-docked=${isDocked}
1354
+ data-transitioning=${isTransitioning}
1355
+ >
1356
+ ${
1357
+ isDocked
1358
+ ? html`
1359
+ <div
1360
+ class="dock-resize-handle pointer-events-auto"
1361
+ role="presentation"
1362
+ aria-hidden="true"
1363
+ @pointerdown=${this.handleResizePointerDown}
1364
+ @pointermove=${this.handleResizePointerMove}
1365
+ @pointerup=${this.handleResizePointerUp}
1366
+ @pointercancel=${this.handleResizePointerCancel}
1367
+ ></div>
1368
+ `
1369
+ : nothing
1370
+ }
1371
+ <div
1372
+ class="flex flex-1 flex-col overflow-hidden bg-white text-gray-800"
1373
+ >
1374
+ <div
1375
+ class="drag-handle relative z-30 flex flex-col border-b border-gray-200 bg-white/95 backdrop-blur-sm ${
1376
+ isDocked
1377
+ ? ""
1378
+ : this.isDragging && this.pointerContext === "window"
1379
+ ? "cursor-grabbing"
1380
+ : "cursor-grab"
1381
+ }"
1382
+ data-drag-context="window"
1383
+ @pointerdown=${isDocked ? undefined : this.handlePointerDown}
1384
+ @pointermove=${isDocked ? undefined : this.handlePointerMove}
1385
+ @pointerup=${isDocked ? undefined : this.handlePointerUp}
1386
+ @pointercancel=${isDocked ? undefined : this.handlePointerCancel}
1387
+ >
1388
+ <div class="flex flex-wrap items-center gap-3 px-4 py-3">
1389
+ <div class="flex items-center min-w-0">
1390
+ <img
1391
+ src=${inspectorLogoUrl}
1392
+ alt="Inspector logo"
1393
+ class="h-6 w-auto"
1394
+ loading="lazy"
1395
+ />
1396
+ </div>
1397
+ <div class="ml-auto flex min-w-0 items-center gap-2">
1398
+ <div class="min-w-[160px] max-w-xs">${agentSelector}</div>
1399
+ <div class="flex items-center gap-1">
1400
+ ${this.renderDockControls()}
1401
+ <button
1402
+ class="flex h-8 w-8 items-center justify-center rounded-md text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
1403
+ type="button"
1404
+ aria-label="Close Web Inspector"
1405
+ @pointerdown=${this.handleClosePointerDown}
1406
+ @click=${this.handleCloseClick}
1407
+ >
1408
+ ${this.renderIcon("X")}
1409
+ </button>
1410
+ </div>
1411
+ </div>
1412
+ </div>
1413
+ <div
1414
+ class="flex flex-wrap items-center gap-2 border-t border-gray-100 px-3 py-2 text-xs"
1415
+ >
1416
+ ${this.menuItems.map(({ key, label, icon }) => {
1417
+ const isSelected = this.selectedMenu === key;
1418
+ const tabClasses = [
1419
+ "inline-flex items-center gap-2 rounded-md px-3 py-2 transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300",
1420
+ isSelected
1421
+ ? "bg-gray-900 text-white shadow-sm"
1422
+ : "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
1423
+ ].join(" ");
1424
+
1425
+ return html`
1426
+ <button
1427
+ type="button"
1428
+ class=${tabClasses}
1429
+ aria-pressed=${isSelected}
1430
+ @click=${() => this.handleMenuSelect(key)}
1431
+ >
1432
+ <span
1433
+ class="text-gray-400 ${isSelected ? "text-white" : ""}"
1434
+ >
1435
+ ${this.renderIcon(icon)}
1436
+ </span>
1437
+ <span>${label}</span>
1438
+ </button>
1439
+ `;
1440
+ })}
1441
+ </div>
1442
+ </div>
1443
+ <div class="flex flex-1 flex-col overflow-hidden">
1444
+ <div class="flex-1 overflow-auto">
1445
+ ${this.renderAnnouncementPanel()}
1446
+ ${this.renderCoreWarningBanner()} ${this.renderMainContent()}
1447
+ <slot></slot>
1448
+ </div>
1449
+ <div class="border-t border-gray-200 bg-gray-50 px-4 py-2">
1450
+ <div
1451
+ class="flex items-center gap-2 rounded-md px-3 py-2 text-xs ${coreStatus.tone} w-full overflow-hidden my-1"
1452
+ title=${coreStatus.description}
1453
+ >
1454
+ <span
1455
+ class="flex h-6 w-6 items-center justify-center rounded bg-white/60"
1456
+ >
1457
+ ${this.renderIcon("Activity")}
1458
+ </span>
1459
+ <span class="font-medium">${coreStatus.label}</span>
1460
+ <span class="truncate text-[11px] opacity-80"
1461
+ >${coreStatus.description}</span
1462
+ >
1463
+ </div>
1464
+ </div>
1465
+ </div>
1466
+ </div>
1467
+ <div
1468
+ class="resize-handle pointer-events-auto absolute bottom-1 right-1 flex h-5 w-5 cursor-nwse-resize items-center justify-center text-gray-400 transition hover:text-gray-600"
1469
+ role="presentation"
1470
+ aria-hidden="true"
1471
+ @pointerdown=${this.handleResizePointerDown}
1472
+ @pointermove=${this.handleResizePointerMove}
1473
+ @pointerup=${this.handleResizePointerUp}
1474
+ @pointercancel=${this.handleResizePointerCancel}
1475
+ >
1476
+ <svg
1477
+ class="h-3 w-3"
1478
+ viewBox="0 0 16 16"
1479
+ fill="none"
1480
+ stroke="currentColor"
1481
+ stroke-linecap="round"
1482
+ stroke-width="1.5"
1483
+ >
1484
+ <path d="M5 15L15 5" />
1485
+ <path d="M9 15L15 9" />
1486
+ </svg>
1487
+ </div>
1488
+ </section>
1489
+ `;
1490
+ }
1491
+
1492
+ private hydrateStateFromStorageEarly(): void {
1493
+ if (typeof document === "undefined" || typeof window === "undefined") {
1494
+ return;
1495
+ }
1496
+
1497
+ const persisted = loadInspectorState(INSPECTOR_STORAGE_KEY);
1498
+ if (!persisted) {
1499
+ return;
1500
+ }
1501
+
1502
+ // Restore the open/closed state
1503
+ if (typeof persisted.isOpen === "boolean") {
1504
+ this.isOpen = persisted.isOpen;
1505
+ }
1506
+
1507
+ // Restore the dock mode
1508
+ if (isValidDockMode(persisted.dockMode)) {
1509
+ this.dockMode = persisted.dockMode;
1510
+ }
1511
+
1512
+ // Restore selected menu
1513
+ if (typeof persisted.selectedMenu === "string") {
1514
+ const validMenu = this.menuItems.find(
1515
+ (item) => item.key === persisted.selectedMenu,
1516
+ );
1517
+ if (validMenu) {
1518
+ this.selectedMenu = validMenu.key;
1519
+ }
1520
+ }
1521
+
1522
+ // Restore selected context (agent), will be validated later against available agents
1523
+ if (typeof persisted.selectedContext === "string") {
1524
+ this.selectedContext = persisted.selectedContext;
1525
+ this.pendingSelectedContext = persisted.selectedContext;
1526
+ }
1527
+ }
1528
+
1529
+ private hydrateStateFromStorage(): void {
1530
+ if (typeof document === "undefined" || typeof window === "undefined") {
1531
+ return;
1532
+ }
1533
+
1534
+ const persisted = loadInspectorState(INSPECTOR_STORAGE_KEY);
1535
+ if (!persisted) {
1536
+ return;
1537
+ }
1538
+
1539
+ const persistedButton = persisted.button;
1540
+ if (persistedButton) {
1541
+ if (isValidAnchor(persistedButton.anchor)) {
1542
+ this.contextState.button.anchor = persistedButton.anchor;
1543
+ }
1544
+
1545
+ if (isValidPosition(persistedButton.anchorOffset)) {
1546
+ this.contextState.button.anchorOffset = persistedButton.anchorOffset;
1547
+ }
1548
+
1549
+ if (typeof persistedButton.hasCustomPosition === "boolean") {
1550
+ this.hasCustomPosition.button = persistedButton.hasCustomPosition;
1551
+ }
1552
+ }
1553
+
1554
+ const persistedWindow = persisted.window;
1555
+ if (persistedWindow) {
1556
+ if (isValidAnchor(persistedWindow.anchor)) {
1557
+ this.contextState.window.anchor = persistedWindow.anchor;
1558
+ }
1559
+
1560
+ if (isValidPosition(persistedWindow.anchorOffset)) {
1561
+ this.contextState.window.anchorOffset = persistedWindow.anchorOffset;
1562
+ }
1563
+
1564
+ if (isValidSize(persistedWindow.size)) {
1565
+ // Now clampWindowSize will use the correct minimum based on dockMode
1566
+ this.contextState.window.size = this.clampWindowSize(
1567
+ persistedWindow.size,
1568
+ );
1569
+ }
1570
+
1571
+ if (typeof persistedWindow.hasCustomPosition === "boolean") {
1572
+ this.hasCustomPosition.window = persistedWindow.hasCustomPosition;
1573
+ }
1574
+ }
1575
+
1576
+ if (typeof persisted.selectedContext === "string") {
1577
+ this.selectedContext = persisted.selectedContext;
1578
+ this.pendingSelectedContext = persisted.selectedContext;
1579
+ }
1580
+ }
1581
+
1582
+ private get activeContext(): ContextKey {
1583
+ return this.isOpen ? "window" : "button";
1584
+ }
1585
+
1586
+ private handlePointerDown = (event: PointerEvent) => {
1587
+ // Don't allow dragging when docked
1588
+ if (this.dockMode !== "floating" && this.isOpen) {
1589
+ return;
1590
+ }
1591
+
1592
+ const target = event.currentTarget as HTMLElement | null;
1593
+ const contextAttr = target?.dataset.dragContext;
1594
+ const context: ContextKey = contextAttr === "window" ? "window" : "button";
1595
+
1596
+ const eventTarget = event.target as HTMLElement | null;
1597
+ if (context === "window" && eventTarget?.closest("button")) {
1598
+ return;
1599
+ }
1600
+
1601
+ this.pointerContext = context;
1602
+ this.measureContext(context);
1603
+
1604
+ event.preventDefault();
1605
+
1606
+ this.pointerId = event.pointerId;
1607
+ this.dragStart = { x: event.clientX, y: event.clientY };
1608
+ const state = this.contextState[context];
1609
+ this.dragOffset = {
1610
+ x: event.clientX - state.position.x,
1611
+ y: event.clientY - state.position.y,
1612
+ };
1613
+ this.isDragging = false;
1614
+ this.draggedDuringInteraction = false;
1615
+ this.ignoreNextButtonClick = false;
1616
+
1617
+ target?.setPointerCapture?.(this.pointerId);
1618
+ };
1619
+
1620
+ private handlePointerMove = (event: PointerEvent) => {
1621
+ if (
1622
+ this.pointerId !== event.pointerId ||
1623
+ !this.dragStart ||
1624
+ !this.pointerContext
1625
+ ) {
1626
+ return;
1627
+ }
1628
+
1629
+ const distance = Math.hypot(
1630
+ event.clientX - this.dragStart.x,
1631
+ event.clientY - this.dragStart.y,
1632
+ );
1633
+ if (!this.isDragging && distance < DRAG_THRESHOLD) {
1634
+ return;
1635
+ }
1636
+
1637
+ event.preventDefault();
1638
+ this.setDragging(true);
1639
+ this.draggedDuringInteraction = true;
1640
+
1641
+ const desired: Position = {
1642
+ x: event.clientX - this.dragOffset.x,
1643
+ y: event.clientY - this.dragOffset.y,
1644
+ };
1645
+
1646
+ const constrained = this.constrainToViewport(desired, this.pointerContext);
1647
+ this.contextState[this.pointerContext].position = constrained;
1648
+ this.updateHostTransform(this.pointerContext);
1649
+ };
1650
+
1651
+ private handlePointerUp = (event: PointerEvent) => {
1652
+ if (this.pointerId !== event.pointerId) {
1653
+ return;
1654
+ }
1655
+
1656
+ const target = event.currentTarget as HTMLElement | null;
1657
+ if (target?.hasPointerCapture(this.pointerId)) {
1658
+ target.releasePointerCapture(this.pointerId);
1659
+ }
1660
+
1661
+ const context = this.pointerContext ?? this.activeContext;
1662
+
1663
+ if (this.isDragging && this.pointerContext) {
1664
+ event.preventDefault();
1665
+ this.setDragging(false);
1666
+ if (this.pointerContext === "window") {
1667
+ this.updateAnchorFromPosition(this.pointerContext);
1668
+ this.hasCustomPosition.window = true;
1669
+ this.applyAnchorPosition(this.pointerContext);
1670
+ } else if (this.pointerContext === "button") {
1671
+ // Snap button to nearest corner
1672
+ this.snapButtonToCorner();
1673
+ this.hasCustomPosition.button = true;
1674
+ if (this.draggedDuringInteraction) {
1675
+ this.ignoreNextButtonClick = true;
1676
+ }
1677
+ }
1678
+ } else if (
1679
+ context === "button" &&
1680
+ !this.isOpen &&
1681
+ !this.draggedDuringInteraction
1682
+ ) {
1683
+ this.openInspector();
1684
+ }
1685
+
1686
+ this.resetPointerTracking();
1687
+ };
1688
+
1689
+ private handlePointerCancel = (event: PointerEvent) => {
1690
+ if (this.pointerId !== event.pointerId) {
1691
+ return;
1692
+ }
1693
+
1694
+ const target = event.currentTarget as HTMLElement | null;
1695
+ if (target?.hasPointerCapture(this.pointerId)) {
1696
+ target.releasePointerCapture(this.pointerId);
1697
+ }
1698
+
1699
+ this.resetPointerTracking();
1700
+ };
1701
+
1702
+ private handleButtonClick = (event: Event) => {
1703
+ if (this.isDragging) {
1704
+ event.preventDefault();
1705
+ return;
1706
+ }
1707
+
1708
+ if (this.ignoreNextButtonClick) {
1709
+ event.preventDefault();
1710
+ this.ignoreNextButtonClick = false;
1711
+ return;
1712
+ }
1713
+
1714
+ if (!this.isOpen) {
1715
+ event.preventDefault();
1716
+ this.openInspector();
1717
+ }
1718
+ };
1719
+
1720
+ private handleClosePointerDown = (event: PointerEvent) => {
1721
+ event.stopPropagation();
1722
+ event.preventDefault();
1723
+ };
1724
+
1725
+ private handleCloseClick = () => {
1726
+ this.closeInspector();
1727
+ };
1728
+
1729
+ private handleResizePointerDown = (event: PointerEvent) => {
1730
+ event.stopPropagation();
1731
+ event.preventDefault();
1732
+
1733
+ this.hasCustomPosition.window = true;
1734
+ this.isResizing = true;
1735
+ this.resizePointerId = event.pointerId;
1736
+ this.resizeStart = { x: event.clientX, y: event.clientY };
1737
+ this.resizeInitialSize = { ...this.contextState.window.size };
1738
+
1739
+ // Remove transition from body during resize to prevent lag
1740
+ if (document.body && this.dockMode !== "floating") {
1741
+ document.body.style.transition = "";
1742
+ }
1743
+
1744
+ const target = event.currentTarget as HTMLElement | null;
1745
+ target?.setPointerCapture?.(event.pointerId);
1746
+ };
1747
+
1748
+ private handleResizePointerMove = (event: PointerEvent) => {
1749
+ if (
1750
+ !this.isResizing ||
1751
+ this.resizePointerId !== event.pointerId ||
1752
+ !this.resizeStart ||
1753
+ !this.resizeInitialSize
1754
+ ) {
1755
+ return;
1756
+ }
1757
+
1758
+ event.preventDefault();
1759
+
1760
+ const deltaX = event.clientX - this.resizeStart.x;
1761
+ const deltaY = event.clientY - this.resizeStart.y;
1762
+ const state = this.contextState.window;
1763
+
1764
+ // For docked states, only resize in the appropriate dimension
1765
+ if (this.dockMode === "docked-left") {
1766
+ // Only resize width for left dock
1767
+ state.size = this.clampWindowSize({
1768
+ width: this.resizeInitialSize.width + deltaX,
1769
+ height: state.size.height,
1770
+ });
1771
+ // Update the body margin
1772
+ if (document.body) {
1773
+ document.body.style.marginLeft = `${state.size.width}px`;
1774
+ }
1775
+ } else {
1776
+ // Full resize for floating mode
1777
+ state.size = this.clampWindowSize({
1778
+ width: this.resizeInitialSize.width + deltaX,
1779
+ height: this.resizeInitialSize.height + deltaY,
1780
+ });
1781
+ this.keepPositionWithinViewport("window");
1782
+ this.updateAnchorFromPosition("window");
1783
+ }
1784
+
1785
+ this.requestUpdate();
1786
+ this.updateHostTransform("window");
1787
+ };
1788
+
1789
+ private handleResizePointerUp = (event: PointerEvent) => {
1790
+ if (this.resizePointerId !== event.pointerId) {
1791
+ return;
1792
+ }
1793
+
1794
+ const target = event.currentTarget as HTMLElement | null;
1795
+ if (target?.hasPointerCapture(this.resizePointerId)) {
1796
+ target.releasePointerCapture(this.resizePointerId);
1797
+ }
1798
+
1799
+ // Only update anchor position for floating mode
1800
+ if (this.dockMode === "floating") {
1801
+ this.updateAnchorFromPosition("window");
1802
+ this.applyAnchorPosition("window");
1803
+ }
1804
+
1805
+ // Persist the new size after resize completes
1806
+ this.persistState();
1807
+ this.resetResizeTracking();
1808
+ };
1809
+
1810
+ private handleResizePointerCancel = (event: PointerEvent) => {
1811
+ if (this.resizePointerId !== event.pointerId) {
1812
+ return;
1813
+ }
1814
+
1815
+ const target = event.currentTarget as HTMLElement | null;
1816
+ if (target?.hasPointerCapture(this.resizePointerId)) {
1817
+ target.releasePointerCapture(this.resizePointerId);
1818
+ }
1819
+
1820
+ // Only update anchor position for floating mode
1821
+ if (this.dockMode === "floating") {
1822
+ this.updateAnchorFromPosition("window");
1823
+ this.applyAnchorPosition("window");
1824
+ }
1825
+
1826
+ // Persist the new size after resize completes
1827
+ this.persistState();
1828
+ this.resetResizeTracking();
1829
+ };
1830
+
1831
+ private handleResize = () => {
1832
+ this.measureContext("button");
1833
+ this.applyAnchorPosition("button");
1834
+
1835
+ this.measureContext("window");
1836
+ if (this.hasCustomPosition.window) {
1837
+ this.applyAnchorPosition("window");
1838
+ } else {
1839
+ this.centerContext("window");
1840
+ }
1841
+
1842
+ this.updateHostTransform();
1843
+ };
1844
+
1845
+ private measureContext(context: ContextKey): void {
1846
+ const selector =
1847
+ context === "window" ? ".inspector-window" : ".console-button";
1848
+ const element = this.renderRoot?.querySelector(
1849
+ selector,
1850
+ ) as HTMLElement | null;
1851
+ if (!element) {
1852
+ return;
1853
+ }
1854
+ const fallback =
1855
+ context === "window" ? DEFAULT_WINDOW_SIZE : DEFAULT_BUTTON_SIZE;
1856
+ updateSizeFromElement(this.contextState[context], element, fallback);
1857
+ }
1858
+
1859
+ private centerContext(context: ContextKey): void {
1860
+ if (typeof window === "undefined") {
1861
+ return;
1862
+ }
1863
+
1864
+ const viewport = this.getViewportSize();
1865
+ centerContextHelper(this.contextState[context], viewport, EDGE_MARGIN);
1866
+
1867
+ if (context === this.activeContext) {
1868
+ this.updateHostTransform(context);
1869
+ }
1870
+
1871
+ this.hasCustomPosition[context] = false;
1872
+ this.persistState();
1873
+ }
1874
+
1875
+ private ensureWindowPlacement(): void {
1876
+ if (typeof window === "undefined") {
1877
+ return;
1878
+ }
1879
+
1880
+ if (!this.hasCustomPosition.window) {
1881
+ this.centerContext("window");
1882
+ return;
1883
+ }
1884
+
1885
+ const viewport = this.getViewportSize();
1886
+ keepPositionWithinViewport(this.contextState.window, viewport, EDGE_MARGIN);
1887
+ updateAnchorFromPositionHelper(
1888
+ this.contextState.window,
1889
+ viewport,
1890
+ EDGE_MARGIN,
1891
+ );
1892
+ this.updateHostTransform("window");
1893
+ this.persistState();
1894
+ }
1895
+
1896
+ private constrainToViewport(
1897
+ position: Position,
1898
+ context: ContextKey,
1899
+ ): Position {
1900
+ if (typeof window === "undefined") {
1901
+ return position;
1902
+ }
1903
+
1904
+ const viewport = this.getViewportSize();
1905
+ return constrainToViewport(
1906
+ this.contextState[context],
1907
+ position,
1908
+ viewport,
1909
+ EDGE_MARGIN,
1910
+ );
1911
+ }
1912
+
1913
+ private keepPositionWithinViewport(context: ContextKey): void {
1914
+ if (typeof window === "undefined") {
1915
+ return;
1916
+ }
1917
+
1918
+ const viewport = this.getViewportSize();
1919
+ keepPositionWithinViewport(
1920
+ this.contextState[context],
1921
+ viewport,
1922
+ EDGE_MARGIN,
1923
+ );
1924
+ }
1925
+
1926
+ private getViewportSize(): Size {
1927
+ if (typeof window === "undefined") {
1928
+ return { ...DEFAULT_WINDOW_SIZE };
1929
+ }
1930
+
1931
+ return { width: window.innerWidth, height: window.innerHeight };
1932
+ }
1933
+
1934
+ private persistState(): void {
1935
+ const state: PersistedState = {
1936
+ button: {
1937
+ anchor: this.contextState.button.anchor,
1938
+ anchorOffset: this.contextState.button.anchorOffset,
1939
+ hasCustomPosition: this.hasCustomPosition.button,
1940
+ },
1941
+ window: {
1942
+ anchor: this.contextState.window.anchor,
1943
+ anchorOffset: this.contextState.window.anchorOffset,
1944
+ size: {
1945
+ width: Math.round(this.contextState.window.size.width),
1946
+ height: Math.round(this.contextState.window.size.height),
1947
+ },
1948
+ hasCustomPosition: this.hasCustomPosition.window,
1949
+ },
1950
+ isOpen: this.isOpen,
1951
+ dockMode: this.dockMode,
1952
+ selectedMenu: this.selectedMenu,
1953
+ selectedContext: this.selectedContext,
1954
+ };
1955
+ saveInspectorState(INSPECTOR_STORAGE_KEY, state);
1956
+ this.pendingSelectedContext = state.selectedContext ?? null;
1957
+ }
1958
+
1959
+ private clampWindowSize(size: Size): Size {
1960
+ // Use smaller minimum width when docked left
1961
+ const minWidth =
1962
+ this.dockMode === "docked-left"
1963
+ ? MIN_WINDOW_WIDTH_DOCKED_LEFT
1964
+ : MIN_WINDOW_WIDTH;
1965
+
1966
+ if (typeof window === "undefined") {
1967
+ return {
1968
+ width: Math.max(minWidth, size.width),
1969
+ height: Math.max(MIN_WINDOW_HEIGHT, size.height),
1970
+ };
1971
+ }
1972
+
1973
+ const viewport = this.getViewportSize();
1974
+ return clampSizeToViewport(
1975
+ size,
1976
+ viewport,
1977
+ EDGE_MARGIN,
1978
+ minWidth,
1979
+ MIN_WINDOW_HEIGHT,
1980
+ );
1981
+ }
1982
+
1983
+ private setDockMode(mode: DockMode): void {
1984
+ if (this.dockMode === mode) {
1985
+ return;
1986
+ }
1987
+
1988
+ // Add transition class for smooth dock mode changes
1989
+ this.startHostTransition();
1990
+
1991
+ // Clean up previous dock state
1992
+ this.removeDockStyles();
1993
+
1994
+ this.dockMode = mode;
1995
+
1996
+ if (mode !== "floating") {
1997
+ // For docking, set the target size immediately so body margins are correct
1998
+ if (mode === "docked-left") {
1999
+ this.contextState.window.size.width = DOCKED_LEFT_WIDTH;
2000
+ }
2001
+
2002
+ // Then apply dock styles with correct sizes
2003
+ this.applyDockStyles();
2004
+ } else {
2005
+ // When floating, set size first then center
2006
+ this.contextState.window.size = { ...DEFAULT_WINDOW_SIZE };
2007
+ this.centerContext("window");
2008
+ }
2009
+
2010
+ this.persistState();
2011
+ this.requestUpdate();
2012
+ this.updateHostTransform("window");
2013
+ }
2014
+
2015
+ private startHostTransition(duration = 300): void {
2016
+ this.setAttribute("data-transitioning", "true");
2017
+
2018
+ if (this.transitionTimeoutId !== null) {
2019
+ clearTimeout(this.transitionTimeoutId);
2020
+ }
2021
+
2022
+ this.transitionTimeoutId = setTimeout(() => {
2023
+ this.removeAttribute("data-transitioning");
2024
+ this.transitionTimeoutId = null;
2025
+ }, duration);
2026
+ }
2027
+
2028
+ private applyDockStyles(skipTransition = false): void {
2029
+ if (typeof document === "undefined" || !document.body) {
2030
+ return;
2031
+ }
2032
+
2033
+ // Save original body margins
2034
+ const computedStyle = window.getComputedStyle(document.body);
2035
+ this.previousBodyMargins = {
2036
+ left: computedStyle.marginLeft,
2037
+ bottom: computedStyle.marginBottom,
2038
+ };
2039
+
2040
+ // Apply transition to body for smooth animation (only when docking, not during resize or initial load)
2041
+ if (!this.isResizing && !skipTransition) {
2042
+ document.body.style.transition = "margin 300ms ease";
2043
+ }
2044
+
2045
+ // Apply body margins with the actual window sizes
2046
+ if (this.dockMode === "docked-left") {
2047
+ document.body.style.marginLeft = `${this.contextState.window.size.width}px`;
2048
+ }
2049
+
2050
+ // Remove transition after animation completes
2051
+ if (!this.isResizing && !skipTransition) {
2052
+ const id = setTimeout(() => {
2053
+ this.bodyTransitionTimeoutIds.delete(id);
2054
+ if (typeof document !== "undefined" && document.body) {
2055
+ document.body.style.transition = "";
2056
+ }
2057
+ }, 300);
2058
+ this.bodyTransitionTimeoutIds.add(id);
2059
+ }
2060
+ }
2061
+
2062
+ private removeDockStyles(skipTransition = false): void {
2063
+ if (typeof document === "undefined" || !document.body) {
2064
+ return;
2065
+ }
2066
+
2067
+ // Only add transition if not resizing and not skipping
2068
+ if (!this.isResizing && !skipTransition) {
2069
+ document.body.style.transition = "margin 300ms ease";
2070
+ }
2071
+
2072
+ // Restore original margins if saved
2073
+ if (this.previousBodyMargins) {
2074
+ document.body.style.marginLeft = this.previousBodyMargins.left;
2075
+ document.body.style.marginBottom = this.previousBodyMargins.bottom;
2076
+ this.previousBodyMargins = null;
2077
+ } else {
2078
+ // Reset to default if no previous values
2079
+ document.body.style.marginLeft = "";
2080
+ document.body.style.marginBottom = "";
2081
+ }
2082
+
2083
+ // Clean up transition after animation completes
2084
+ if (!skipTransition) {
2085
+ const id = setTimeout(() => {
2086
+ this.bodyTransitionTimeoutIds.delete(id);
2087
+ if (typeof document !== "undefined" && document.body) {
2088
+ document.body.style.transition = "";
2089
+ }
2090
+ }, 300);
2091
+ this.bodyTransitionTimeoutIds.add(id);
2092
+ } else {
2093
+ document.body.style.transition = "";
2094
+ }
2095
+ }
2096
+
2097
+ private updateHostTransform(context: ContextKey = this.activeContext): void {
2098
+ if (context !== this.activeContext) {
2099
+ return;
2100
+ }
2101
+
2102
+ // For docked states, CSS handles positioning with fixed positioning
2103
+ if (this.isOpen && this.dockMode === "docked-left") {
2104
+ this.style.transform = `translate3d(0, 0, 0)`;
2105
+ } else {
2106
+ const { position } = this.contextState[context];
2107
+ this.style.transform = `translate3d(${position.x}px, ${position.y}px, 0)`;
2108
+ }
2109
+ }
2110
+
2111
+ private setDragging(value: boolean): void {
2112
+ if (this.isDragging !== value) {
2113
+ this.isDragging = value;
2114
+ this.requestUpdate();
2115
+ }
2116
+ }
2117
+
2118
+ private updateAnchorFromPosition(context: ContextKey): void {
2119
+ if (typeof window === "undefined") {
2120
+ return;
2121
+ }
2122
+ const viewport = this.getViewportSize();
2123
+ updateAnchorFromPositionHelper(
2124
+ this.contextState[context],
2125
+ viewport,
2126
+ EDGE_MARGIN,
2127
+ );
2128
+ }
2129
+
2130
+ private snapButtonToCorner(): void {
2131
+ if (typeof window === "undefined") {
2132
+ return;
2133
+ }
2134
+
2135
+ const viewport = this.getViewportSize();
2136
+ const state = this.contextState.button;
2137
+
2138
+ // Determine which corner is closest based on center of button
2139
+ const centerX = state.position.x + state.size.width / 2;
2140
+ const centerY = state.position.y + state.size.height / 2;
2141
+
2142
+ const horizontal: Anchor["horizontal"] =
2143
+ centerX < viewport.width / 2 ? "left" : "right";
2144
+ const vertical: Anchor["vertical"] =
2145
+ centerY < viewport.height / 2 ? "top" : "bottom";
2146
+
2147
+ // Set anchor to nearest corner
2148
+ state.anchor = { horizontal, vertical };
2149
+
2150
+ // Always use EDGE_MARGIN as offset (pinned to corner)
2151
+ state.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
2152
+
2153
+ // Apply the anchor position to snap to corner
2154
+ this.startHostTransition();
2155
+ this.applyAnchorPosition("button");
2156
+ }
2157
+
2158
+ private applyAnchorPosition(context: ContextKey): void {
2159
+ if (typeof window === "undefined") {
2160
+ return;
2161
+ }
2162
+ const viewport = this.getViewportSize();
2163
+ applyAnchorPositionHelper(
2164
+ this.contextState[context],
2165
+ viewport,
2166
+ EDGE_MARGIN,
2167
+ );
2168
+ this.updateHostTransform(context);
2169
+ this.persistState();
2170
+ }
2171
+
2172
+ private resetResizeTracking(): void {
2173
+ this.resizePointerId = null;
2174
+ this.resizeStart = null;
2175
+ this.resizeInitialSize = null;
2176
+ this.isResizing = false;
2177
+ }
2178
+
2179
+ private resetPointerTracking(): void {
2180
+ this.pointerId = null;
2181
+ this.dragStart = null;
2182
+ this.pointerContext = null;
2183
+ this.setDragging(false);
2184
+ this.draggedDuringInteraction = false;
2185
+ }
2186
+
2187
+ private openInspector(): void {
2188
+ if (this.isOpen) {
2189
+ return;
2190
+ }
2191
+
2192
+ this.showAnnouncementPreview = false; // hide the bubble once the inspector is opened
2193
+
2194
+ this.ensureAnnouncementLoading();
2195
+
2196
+ this.isOpen = true;
2197
+ this.persistState(); // Save the open state
2198
+
2199
+ // Apply docking styles if in docked mode
2200
+ if (this.dockMode !== "floating") {
2201
+ this.applyDockStyles();
2202
+ }
2203
+
2204
+ this.ensureWindowPlacement();
2205
+ this.requestUpdate();
2206
+ void this.updateComplete.then(() => {
2207
+ this.measureContext("window");
2208
+ if (this.dockMode === "floating") {
2209
+ if (this.hasCustomPosition.window) {
2210
+ this.applyAnchorPosition("window");
2211
+ } else {
2212
+ this.centerContext("window");
2213
+ }
2214
+ } else {
2215
+ // Update transform for docked position
2216
+ this.updateHostTransform("window");
2217
+ }
2218
+ });
2219
+ }
2220
+
2221
+ private closeInspector(): void {
2222
+ if (!this.isOpen) {
2223
+ return;
2224
+ }
2225
+
2226
+ this.isOpen = false;
2227
+
2228
+ // Remove docking styles when closing
2229
+ if (this.dockMode !== "floating") {
2230
+ this.removeDockStyles();
2231
+ }
2232
+
2233
+ this.persistState(); // Save the closed state
2234
+ this.updateHostTransform("button");
2235
+ this.requestUpdate();
2236
+ void this.updateComplete.then(() => {
2237
+ this.measureContext("button");
2238
+ this.applyAnchorPosition("button");
2239
+ });
2240
+ }
2241
+
2242
+ private renderIcon(name: LucideIconName) {
2243
+ const iconNode = icons[name];
2244
+ if (!iconNode) {
2245
+ return nothing;
2246
+ }
2247
+
2248
+ const svgAttrs: Record<string, string | number> = {
2249
+ xmlns: "http://www.w3.org/2000/svg",
2250
+ viewBox: "0 0 24 24",
2251
+ fill: "none",
2252
+ stroke: "currentColor",
2253
+ "stroke-width": "1.5",
2254
+ "stroke-linecap": "round",
2255
+ "stroke-linejoin": "round",
2256
+ class: "h-3.5 w-3.5",
2257
+ };
2258
+
2259
+ const svgMarkup = `<svg ${this.serializeAttributes(svgAttrs)}>${iconNode
2260
+ .map(([tag, attrs]) => `<${tag} ${this.serializeAttributes(attrs)} />`)
2261
+ .join("")}</svg>`;
2262
+
2263
+ return unsafeHTML(svgMarkup);
2264
+ }
2265
+
2266
+ private renderDockControls() {
2267
+ if (this.dockMode === "floating") {
2268
+ // Show dock left button
2269
+ return html`
2270
+ <button
2271
+ class="flex h-8 w-8 items-center justify-center rounded-md text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
2272
+ type="button"
2273
+ aria-label="Dock to left"
2274
+ title="Dock Left"
2275
+ @click=${() => this.handleDockClick("docked-left")}
2276
+ >
2277
+ ${this.renderIcon("PanelLeft")}
2278
+ </button>
2279
+ `;
2280
+ } else {
2281
+ // Show float button
2282
+ return html`
2283
+ <button
2284
+ class="flex h-8 w-8 items-center justify-center rounded-md text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
2285
+ type="button"
2286
+ aria-label="Float window"
2287
+ title="Float"
2288
+ @click=${() => this.handleDockClick("floating")}
2289
+ >
2290
+ ${this.renderIcon("Maximize2")}
2291
+ </button>
2292
+ `;
2293
+ }
2294
+ }
2295
+
2296
+ private getDockedWindowStyles(): Record<string, string> {
2297
+ if (this.dockMode === "docked-left") {
2298
+ return {
2299
+ position: "fixed",
2300
+ top: "0",
2301
+ left: "0",
2302
+ bottom: "0",
2303
+ width: `${Math.round(this.contextState.window.size.width)}px`,
2304
+ height: "100vh",
2305
+ minWidth: `${MIN_WINDOW_WIDTH_DOCKED_LEFT}px`,
2306
+ borderRadius: "0",
2307
+ };
2308
+ }
2309
+ // Default to floating styles
2310
+ return {
2311
+ width: `${Math.round(this.contextState.window.size.width)}px`,
2312
+ height: `${Math.round(this.contextState.window.size.height)}px`,
2313
+ minWidth: `${MIN_WINDOW_WIDTH}px`,
2314
+ minHeight: `${MIN_WINDOW_HEIGHT}px`,
2315
+ };
2316
+ }
2317
+
2318
+ private handleDockClick(mode: DockMode): void {
2319
+ this.setDockMode(mode);
2320
+ }
2321
+
2322
+ private serializeAttributes(
2323
+ attributes: Record<string, string | number | undefined>,
2324
+ ): string {
2325
+ return Object.entries(attributes)
2326
+ .filter(
2327
+ ([key, value]) =>
2328
+ key !== "key" &&
2329
+ value !== undefined &&
2330
+ value !== null &&
2331
+ value !== "",
2332
+ )
2333
+ .map(
2334
+ ([key, value]) => `${key}="${String(value).replace(/"/g, "&quot;")}"`,
2335
+ )
2336
+ .join(" ");
2337
+ }
2338
+
2339
+ private sanitizeForLogging(
2340
+ value: unknown,
2341
+ depth = 0,
2342
+ seen = new WeakSet<object>(),
2343
+ ): SanitizedValue {
2344
+ if (value === undefined) {
2345
+ return "[undefined]";
2346
+ }
2347
+
2348
+ if (
2349
+ value === null ||
2350
+ typeof value === "number" ||
2351
+ typeof value === "boolean"
2352
+ ) {
2353
+ return value;
2354
+ }
2355
+
2356
+ if (typeof value === "string") {
2357
+ return value;
2358
+ }
2359
+
2360
+ if (
2361
+ typeof value === "bigint" ||
2362
+ typeof value === "symbol" ||
2363
+ typeof value === "function"
2364
+ ) {
2365
+ return String(value);
2366
+ }
2367
+
2368
+ if (value instanceof Date) {
2369
+ return value.toISOString();
2370
+ }
2371
+
2372
+ if (Array.isArray(value)) {
2373
+ if (depth >= 4) {
2374
+ return "[Truncated depth]" as SanitizedValue;
2375
+ }
2376
+ return value.map((item) =>
2377
+ this.sanitizeForLogging(item, depth + 1, seen),
2378
+ );
2379
+ }
2380
+
2381
+ if (typeof value === "object") {
2382
+ if (seen.has(value as object)) {
2383
+ return "[Circular]" as SanitizedValue;
2384
+ }
2385
+ seen.add(value as object);
2386
+
2387
+ if (depth >= 4) {
2388
+ return "[Truncated depth]" as SanitizedValue;
2389
+ }
2390
+
2391
+ const result: Record<string, SanitizedValue> = {};
2392
+ for (const [key, entry] of Object.entries(
2393
+ value as Record<string, unknown>,
2394
+ )) {
2395
+ result[key] = this.sanitizeForLogging(entry, depth + 1, seen);
2396
+ }
2397
+ return result;
2398
+ }
2399
+
2400
+ return String(value);
2401
+ }
2402
+
2403
+ private normalizeEventPayload(
2404
+ _type: InspectorAgentEventType,
2405
+ payload: unknown,
2406
+ ): SanitizedValue {
2407
+ if (payload && typeof payload === "object" && "event" in payload) {
2408
+ const { event, ...rest } = payload as Record<string, unknown>;
2409
+ const cleaned =
2410
+ Object.keys(rest).length === 0 ? event : { event, ...rest };
2411
+ return this.sanitizeForLogging(cleaned);
2412
+ }
2413
+
2414
+ return this.sanitizeForLogging(payload);
2415
+ }
2416
+
2417
+ private normalizeMessageContent(content: unknown): string {
2418
+ if (typeof content === "string") {
2419
+ return content;
2420
+ }
2421
+
2422
+ if (
2423
+ content &&
2424
+ typeof content === "object" &&
2425
+ "text" in (content as Record<string, unknown>)
2426
+ ) {
2427
+ const maybeText = (content as Record<string, unknown>).text;
2428
+ if (typeof maybeText === "string") {
2429
+ return maybeText;
2430
+ }
2431
+ }
2432
+
2433
+ if (content === null || content === undefined) {
2434
+ return "";
2435
+ }
2436
+
2437
+ if (typeof content === "object") {
2438
+ try {
2439
+ return JSON.stringify(this.sanitizeForLogging(content));
2440
+ } catch {
2441
+ return "";
2442
+ }
2443
+ }
2444
+
2445
+ return String(content);
2446
+ }
2447
+
2448
+ private normalizeToolCalls(raw: unknown): InspectorToolCall[] {
2449
+ if (!Array.isArray(raw)) {
2450
+ return [];
2451
+ }
2452
+
2453
+ return raw
2454
+ .map((entry) => {
2455
+ if (!entry || typeof entry !== "object") {
2456
+ return null;
2457
+ }
2458
+ const call = entry as Record<string, unknown>;
2459
+ const fn = call.function as Record<string, unknown> | undefined;
2460
+ const functionName =
2461
+ typeof fn?.name === "string"
2462
+ ? fn.name
2463
+ : typeof call.toolName === "string"
2464
+ ? call.toolName
2465
+ : undefined;
2466
+ const args =
2467
+ fn && "arguments" in fn
2468
+ ? (fn as Record<string, unknown>).arguments
2469
+ : call.arguments;
2470
+
2471
+ const normalized: InspectorToolCall = {
2472
+ id: typeof call.id === "string" ? call.id : undefined,
2473
+ toolName:
2474
+ typeof call.toolName === "string" ? call.toolName : functionName,
2475
+ status: typeof call.status === "string" ? call.status : undefined,
2476
+ };
2477
+
2478
+ if (functionName) {
2479
+ normalized.function = {
2480
+ name: functionName,
2481
+ arguments: this.sanitizeForLogging(args),
2482
+ };
2483
+ }
2484
+
2485
+ return normalized;
2486
+ })
2487
+ .filter((call): call is InspectorToolCall => Boolean(call));
2488
+ }
2489
+
2490
+ private normalizeAgentMessage(message: unknown): InspectorMessage | null {
2491
+ if (!message || typeof message !== "object") {
2492
+ return null;
2493
+ }
2494
+
2495
+ const raw = message as Record<string, unknown>;
2496
+ const role = typeof raw.role === "string" ? raw.role : "unknown";
2497
+ const contentText = this.normalizeMessageContent(raw.content);
2498
+ const toolCalls = this.normalizeToolCalls(raw.toolCalls);
2499
+
2500
+ return {
2501
+ id: typeof raw.id === "string" ? raw.id : undefined,
2502
+ role,
2503
+ contentText,
2504
+ contentRaw:
2505
+ raw.content !== undefined
2506
+ ? this.sanitizeForLogging(raw.content)
2507
+ : undefined,
2508
+ toolCalls,
2509
+ };
2510
+ }
2511
+
2512
+ private normalizeAgentMessages(messages: unknown): InspectorMessage[] | null {
2513
+ if (!Array.isArray(messages)) {
2514
+ return null;
2515
+ }
2516
+
2517
+ const normalized = messages
2518
+ .map((message) => this.normalizeAgentMessage(message))
2519
+ .filter((msg): msg is InspectorMessage => msg !== null);
2520
+
2521
+ return normalized;
2522
+ }
2523
+
2524
+ private normalizeContextStore(
2525
+ context: Readonly<Record<string, unknown>> | null | undefined,
2526
+ ): Record<string, { description?: string; value: unknown }> {
2527
+ if (!context || typeof context !== "object") {
2528
+ return {};
2529
+ }
2530
+
2531
+ const normalized: Record<string, { description?: string; value: unknown }> =
2532
+ {};
2533
+ for (const [key, entry] of Object.entries(context)) {
2534
+ if (
2535
+ entry &&
2536
+ typeof entry === "object" &&
2537
+ "value" in (entry as Record<string, unknown>)
2538
+ ) {
2539
+ const candidate = entry as Record<string, unknown>;
2540
+ const description =
2541
+ typeof candidate.description === "string" &&
2542
+ candidate.description.trim().length > 0
2543
+ ? candidate.description
2544
+ : undefined;
2545
+ normalized[key] = { description, value: candidate.value };
2546
+ } else {
2547
+ normalized[key] = { value: entry };
2548
+ }
2549
+ }
2550
+
2551
+ return normalized;
2552
+ }
2553
+
2554
+ private contextOptions: Array<{ key: string; label: string }> = [
2555
+ { key: "all-agents", label: "All Agents" },
2556
+ ];
2557
+
2558
+ private selectedContext = "all-agents";
2559
+ private expandedRows: Set<string> = new Set();
2560
+ private copiedEvents: Set<string> = new Set();
2561
+ private expandedTools: Set<string> = new Set();
2562
+ private expandedContextItems: Set<string> = new Set();
2563
+ private copiedContextItems: Set<string> = new Set();
2564
+
2565
+ private getSelectedMenu(): MenuItem {
2566
+ const found = this.menuItems.find((item) => item.key === this.selectedMenu);
2567
+ return found ?? this.menuItems[0]!;
2568
+ }
2569
+
2570
+ private renderCoreWarningBanner() {
2571
+ if (this._core) {
2572
+ return nothing;
2573
+ }
2574
+
2575
+ return html`
2576
+ <div
2577
+ class="mx-4 my-3 flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800"
2578
+ >
2579
+ <span class="mt-0.5 shrink-0 text-amber-600"
2580
+ >${this.renderIcon("AlertTriangle")}</span
2581
+ >
2582
+ <div class="space-y-1">
2583
+ <div class="font-semibold text-amber-900">
2584
+ CopilotKit core not attached
2585
+ </div>
2586
+ <p class="text-[11px] leading-snug text-amber-800">
2587
+ Pass a live <code>CopilotKitCore</code> instance to
2588
+ <code>&lt;cpk-web-inspector&gt;</code> or expose it on
2589
+ <code>window.__COPILOTKIT_CORE__</code> for auto-attach.
2590
+ </p>
2591
+ </div>
2592
+ </div>
2593
+ `;
2594
+ }
2595
+
2596
+ private getCoreStatusSummary(): {
2597
+ label: string;
2598
+ tone: string;
2599
+ description: string;
2600
+ } {
2601
+ if (!this._core) {
2602
+ return {
2603
+ label: "Core not attached",
2604
+ tone: "border border-amber-200 bg-amber-50 text-amber-800",
2605
+ description:
2606
+ "Pass a CopilotKitCore instance to <cpk-web-inspector> or enable auto-attach.",
2607
+ };
2608
+ }
2609
+
2610
+ const status =
2611
+ this.runtimeStatus ?? CopilotKitCoreRuntimeConnectionStatus.Disconnected;
2612
+ const lastErrorMessage = this.lastCoreError?.message;
2613
+
2614
+ if (status === CopilotKitCoreRuntimeConnectionStatus.Error) {
2615
+ return {
2616
+ label: "Runtime error",
2617
+ tone: "border border-rose-200 bg-rose-50 text-rose-700",
2618
+ description:
2619
+ lastErrorMessage ?? "CopilotKit runtime reported an error.",
2620
+ };
2621
+ }
2622
+
2623
+ if (status === CopilotKitCoreRuntimeConnectionStatus.Connecting) {
2624
+ return {
2625
+ label: "Connecting",
2626
+ tone: "border border-amber-200 bg-amber-50 text-amber-800",
2627
+ description: "Waiting for CopilotKit runtime to finish connecting.",
2628
+ };
2629
+ }
2630
+
2631
+ if (status === CopilotKitCoreRuntimeConnectionStatus.Connected) {
2632
+ return {
2633
+ label: "Connected",
2634
+ tone: "border border-emerald-200 bg-emerald-50 text-emerald-700",
2635
+ description: "Live runtime connection established.",
2636
+ };
2637
+ }
2638
+
2639
+ return {
2640
+ label: "Disconnected",
2641
+ tone: "border border-gray-200 bg-gray-50 text-gray-700",
2642
+ description:
2643
+ lastErrorMessage ?? "Waiting for CopilotKit runtime to connect.",
2644
+ };
2645
+ }
2646
+
2647
+ private renderMainContent() {
2648
+ if (this.selectedMenu === "ag-ui-events") {
2649
+ return this.renderEventsTable();
2650
+ }
2651
+
2652
+ if (this.selectedMenu === "agents") {
2653
+ return this.renderAgentsView();
2654
+ }
2655
+
2656
+ if (this.selectedMenu === "frontend-tools") {
2657
+ return this.renderToolsView();
2658
+ }
2659
+
2660
+ if (this.selectedMenu === "agent-context") {
2661
+ return this.renderContextView();
2662
+ }
2663
+
2664
+ return nothing;
2665
+ }
2666
+
2667
+ private renderEventsTable() {
2668
+ const events = this.getEventsForSelectedContext();
2669
+ const filteredEvents = this.filterEvents(events);
2670
+ const selectedLabel =
2671
+ this.selectedContext === "all-agents"
2672
+ ? "all agents"
2673
+ : `agent ${this.selectedContext}`;
2674
+
2675
+ if (events.length === 0) {
2676
+ return html`
2677
+ <div
2678
+ class="flex h-full items-center justify-center px-4 py-8 text-center"
2679
+ >
2680
+ <div class="max-w-md">
2681
+ <div
2682
+ class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8"
2683
+ >
2684
+ ${this.renderIcon("Zap")}
2685
+ </div>
2686
+ <p class="text-sm text-gray-600">No events yet</p>
2687
+ <p class="mt-2 text-xs text-gray-500">
2688
+ Trigger an agent run to see live activity.
2689
+ </p>
2690
+ </div>
2691
+ </div>
2692
+ `;
2693
+ }
2694
+
2695
+ if (filteredEvents.length === 0) {
2696
+ return html`
2697
+ <div
2698
+ class="flex h-full items-center justify-center px-4 py-8 text-center"
2699
+ >
2700
+ <div class="max-w-md space-y-3">
2701
+ <div
2702
+ class="flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8"
2703
+ >
2704
+ ${this.renderIcon("Filter")}
2705
+ </div>
2706
+ <p class="text-sm text-gray-600">
2707
+ No events match the current filters.
2708
+ </p>
2709
+ <div>
2710
+ <button
2711
+ type="button"
2712
+ class="inline-flex items-center gap-1 rounded-md bg-gray-900 px-3 py-1.5 text-[11px] font-medium text-white transition hover:bg-gray-800"
2713
+ @click=${this.resetEventFilters}
2714
+ >
2715
+ ${this.renderIcon("RefreshCw")}
2716
+ <span>Reset filters</span>
2717
+ </button>
2718
+ </div>
2719
+ </div>
2720
+ </div>
2721
+ `;
2722
+ }
2723
+
2724
+ return html`
2725
+ <div class="flex h-full flex-col">
2726
+ <div
2727
+ class="flex flex-col gap-1.5 border-b border-gray-200 bg-white px-4 py-2.5"
2728
+ >
2729
+ <div class="flex flex-wrap items-center gap-2">
2730
+ <div class="relative min-w-[200px] flex-1">
2731
+ <input
2732
+ type="search"
2733
+ class="w-full rounded-md border border-gray-200 px-3 py-1.5 text-[11px] text-gray-700 shadow-sm outline-none ring-1 ring-transparent transition focus:border-gray-300 focus:ring-gray-200"
2734
+ placeholder="Search agent, type, payload"
2735
+ .value=${this.eventFilterText}
2736
+ @input=${this.handleEventFilterInput}
2737
+ />
2738
+ </div>
2739
+ <select
2740
+ class="w-40 rounded-md border border-gray-200 bg-white px-2 py-1.5 text-[11px] text-gray-700 shadow-sm outline-none transition focus:border-gray-300 focus:ring-2 focus:ring-gray-200"
2741
+ .value=${this.eventTypeFilter}
2742
+ @change=${this.handleEventTypeChange}
2743
+ >
2744
+ <option value="all">All event types</option>
2745
+ ${AGENT_EVENT_TYPES.map(
2746
+ (type) =>
2747
+ html`<option value=${type}>
2748
+ ${type.toLowerCase().replace(/_/g, " ")}
2749
+ </option>`,
2750
+ )}
2751
+ </select>
2752
+ <div class="flex items-center gap-1 text-[11px]">
2753
+ <button
2754
+ type="button"
2755
+ class="tooltip-target flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-600 transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
2756
+ title="Reset filters"
2757
+ data-tooltip="Reset filters"
2758
+ aria-label="Reset filters"
2759
+ @click=${this.resetEventFilters}
2760
+ ?disabled=${
2761
+ !this.eventFilterText && this.eventTypeFilter === "all"
2762
+ }
2763
+ >
2764
+ ${this.renderIcon("RotateCw")}
2765
+ </button>
2766
+ <button
2767
+ type="button"
2768
+ class="tooltip-target flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-600 transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
2769
+ title="Export JSON"
2770
+ data-tooltip="Export JSON"
2771
+ aria-label="Export JSON"
2772
+ @click=${() => this.exportEvents(filteredEvents)}
2773
+ ?disabled=${filteredEvents.length === 0}
2774
+ >
2775
+ ${this.renderIcon("Download")}
2776
+ </button>
2777
+ <button
2778
+ type="button"
2779
+ class="tooltip-target flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-600 transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
2780
+ title="Clear events"
2781
+ data-tooltip="Clear events"
2782
+ aria-label="Clear events"
2783
+ @click=${this.handleClearEvents}
2784
+ ?disabled=${events.length === 0}
2785
+ >
2786
+ ${this.renderIcon("Trash2")}
2787
+ </button>
2788
+ </div>
2789
+ </div>
2790
+ <div class="text-[11px] text-gray-500">
2791
+ Showing ${filteredEvents.length} of
2792
+ ${events.length}${
2793
+ this.selectedContext === "all-agents"
2794
+ ? ""
2795
+ : ` for ${selectedLabel}`
2796
+ }
2797
+ </div>
2798
+ </div>
2799
+ <div class="relative h-full w-full overflow-y-auto overflow-x-hidden">
2800
+ <table class="w-full table-fixed border-collapse text-xs box-border">
2801
+ <thead class="sticky top-0 z-10">
2802
+ <tr class="bg-white">
2803
+ <th
2804
+ class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2805
+ >
2806
+ Agent
2807
+ </th>
2808
+ <th
2809
+ class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2810
+ >
2811
+ Time
2812
+ </th>
2813
+ <th
2814
+ class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2815
+ >
2816
+ Event Type
2817
+ </th>
2818
+ <th
2819
+ class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2820
+ >
2821
+ AG-UI Event
2822
+ </th>
2823
+ </tr>
2824
+ </thead>
2825
+ <tbody>
2826
+ ${filteredEvents.map((event, index) => {
2827
+ const rowBg = index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
2828
+ const badgeClasses = this.getEventBadgeClasses(event.type);
2829
+ const extractedEvent = this.extractEventFromPayload(
2830
+ event.payload,
2831
+ );
2832
+ const inlineEvent =
2833
+ this.stringifyPayload(extractedEvent, false) || "—";
2834
+ const prettyEvent =
2835
+ this.stringifyPayload(extractedEvent, true) || inlineEvent;
2836
+ const isExpanded = this.expandedRows.has(event.id);
2837
+
2838
+ return html`
2839
+ <tr
2840
+ class="${rowBg} cursor-pointer transition hover:bg-blue-50/50"
2841
+ @click=${() => this.toggleRowExpansion(event.id)}
2842
+ >
2843
+ <td
2844
+ class="border-l border-r border-b border-gray-200 px-3 py-2"
2845
+ >
2846
+ <span class="font-mono text-[11px] text-gray-600"
2847
+ >${event.agentId}</span
2848
+ >
2849
+ </td>
2850
+ <td
2851
+ class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[11px] text-gray-600"
2852
+ >
2853
+ <span title=${new Date(event.timestamp).toLocaleString()}>
2854
+ ${new Date(event.timestamp).toLocaleTimeString()}
2855
+ </span>
2856
+ </td>
2857
+ <td class="border-r border-b border-gray-200 px-3 py-2">
2858
+ <span class=${badgeClasses}>${event.type}</span>
2859
+ </td>
2860
+ <td
2861
+ class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[10px] text-gray-600 ${
2862
+ isExpanded ? "" : "truncate max-w-xs"
2863
+ }"
2864
+ >
2865
+ ${
2866
+ isExpanded
2867
+ ? html`
2868
+ <div class="group relative">
2869
+ <pre
2870
+ class="m-0 whitespace-pre-wrap break-words text-[10px] font-mono text-gray-600"
2871
+ >
2872
+ ${prettyEvent}</pre
2873
+ >
2874
+ <button
2875
+ class="absolute right-0 top-0 cursor-pointer rounded px-2 py-1 text-[10px] opacity-0 transition group-hover:opacity-100 ${
2876
+ this.copiedEvents.has(event.id)
2877
+ ? "bg-green-100 text-green-700"
2878
+ : "bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900"
2879
+ }"
2880
+ @click=${(e: Event) => {
2881
+ e.stopPropagation();
2882
+ this.copyToClipboard(prettyEvent, event.id);
2883
+ }}
2884
+ >
2885
+ ${
2886
+ this.copiedEvents.has(event.id)
2887
+ ? html`
2888
+ <span>✓ Copied</span>
2889
+ `
2890
+ : html`
2891
+ <span>Copy</span>
2892
+ `
2893
+ }
2894
+ </button>
2895
+ </div>
2896
+ `
2897
+ : inlineEvent
2898
+ }
2899
+ </td>
2900
+ </tr>
2901
+ `;
2902
+ })}
2903
+ </tbody>
2904
+ </table>
2905
+ </div>
2906
+ </div>
2907
+ `;
2908
+ }
2909
+
2910
+ private handleEventFilterInput(event: Event): void {
2911
+ const target = event.target as HTMLInputElement | null;
2912
+ this.eventFilterText = target?.value ?? "";
2913
+ this.requestUpdate();
2914
+ }
2915
+
2916
+ private handleEventTypeChange(event: Event): void {
2917
+ const target = event.target as HTMLSelectElement | null;
2918
+ const value = target?.value as InspectorAgentEventType | "all" | undefined;
2919
+ if (!value) {
2920
+ return;
2921
+ }
2922
+ this.eventTypeFilter = value;
2923
+ this.requestUpdate();
2924
+ }
2925
+
2926
+ private resetEventFilters(): void {
2927
+ this.eventFilterText = "";
2928
+ this.eventTypeFilter = "all";
2929
+ this.requestUpdate();
2930
+ }
2931
+
2932
+ private handleClearEvents = (): void => {
2933
+ if (this.selectedContext === "all-agents") {
2934
+ this.agentEvents.clear();
2935
+ this.flattenedEvents = [];
2936
+ } else {
2937
+ this.agentEvents.delete(this.selectedContext);
2938
+ this.flattenedEvents = this.flattenedEvents.filter(
2939
+ (event) => event.agentId !== this.selectedContext,
2940
+ );
2941
+ }
2942
+
2943
+ this.expandedRows.clear();
2944
+ this.copiedEvents.clear();
2945
+ this.requestUpdate();
2946
+ };
2947
+
2948
+ private exportEvents(events: InspectorEvent[]): void {
2949
+ try {
2950
+ const payload = JSON.stringify(events, null, 2);
2951
+ const blob = new Blob([payload], { type: "application/json" });
2952
+ const url = URL.createObjectURL(blob);
2953
+ const anchor = document.createElement("a");
2954
+ anchor.href = url;
2955
+ anchor.download = `copilotkit-events-${Date.now()}.json`;
2956
+ anchor.click();
2957
+ URL.revokeObjectURL(url);
2958
+ } catch (error) {
2959
+ console.error("Failed to export events", error);
2960
+ }
2961
+ }
2962
+
2963
+ private renderAgentsView() {
2964
+ // Show message if "all-agents" is selected or no agents available
2965
+ if (this.selectedContext === "all-agents") {
2966
+ return html`
2967
+ <div
2968
+ class="flex h-full items-center justify-center px-4 py-8 text-center"
2969
+ >
2970
+ <div class="max-w-md">
2971
+ <div
2972
+ class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8"
2973
+ >
2974
+ ${this.renderIcon("Bot")}
2975
+ </div>
2976
+ <p class="text-sm text-gray-600">No agent selected</p>
2977
+ <p class="mt-2 text-xs text-gray-500">
2978
+ Select an agent from the dropdown above to view details.
2979
+ </p>
2980
+ </div>
2981
+ </div>
2982
+ `;
2983
+ }
2984
+
2985
+ const agentId = this.selectedContext;
2986
+ const status = this.getAgentStatus(agentId);
2987
+ const stats = this.getAgentStats(agentId);
2988
+ const state = this.getLatestStateForAgent(agentId);
2989
+ const messages = this.getLatestMessagesForAgent(agentId);
2990
+
2991
+ const statusColors = {
2992
+ running: "bg-emerald-50 text-emerald-700",
2993
+ idle: "bg-gray-100 text-gray-600",
2994
+ error: "bg-rose-50 text-rose-700",
2995
+ };
2996
+
2997
+ return html`
2998
+ <div class="flex flex-col gap-4 p-4 overflow-auto">
2999
+ <!-- Agent Overview Card -->
3000
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
3001
+ <div class="flex items-start justify-between mb-4">
3002
+ <div class="flex items-center gap-3">
3003
+ <div
3004
+ class="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600"
3005
+ >
3006
+ ${this.renderIcon("Bot")}
3007
+ </div>
3008
+ <div>
3009
+ <h3 class="font-semibold text-sm text-gray-900">${agentId}</h3>
3010
+ <span
3011
+ class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${
3012
+ statusColors[status]
3013
+ } relative -translate-y-[2px]"
3014
+ >
3015
+ <span
3016
+ class="h-1.5 w-1.5 rounded-full ${
3017
+ status === "running"
3018
+ ? "bg-emerald-500 animate-pulse"
3019
+ : status === "error"
3020
+ ? "bg-rose-500"
3021
+ : "bg-gray-400"
3022
+ }"
3023
+ ></span>
3024
+ ${status.charAt(0).toUpperCase() + status.slice(1)}
3025
+ </span>
3026
+ </div>
3027
+ </div>
3028
+ ${
3029
+ stats.lastActivity
3030
+ ? html`<span class="text-xs text-gray-500"
3031
+ >Last activity:
3032
+ ${new Date(stats.lastActivity).toLocaleTimeString()}</span
3033
+ >`
3034
+ : nothing
3035
+ }
3036
+ </div>
3037
+ <div class="grid grid-cols-2 gap-4 md:grid-cols-4">
3038
+ <button
3039
+ type="button"
3040
+ class="rounded-md bg-gray-50 px-3 py-2 text-left transition hover:bg-gray-100 cursor-pointer overflow-hidden"
3041
+ @click=${() => this.handleMenuSelect("ag-ui-events")}
3042
+ title="View all events in AG-UI Events"
3043
+ >
3044
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">
3045
+ Total Events
3046
+ </div>
3047
+ <div class="text-lg font-semibold text-gray-900">
3048
+ ${stats.totalEvents}
3049
+ </div>
3050
+ </button>
3051
+ <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
3052
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">
3053
+ Messages
3054
+ </div>
3055
+ <div class="text-lg font-semibold text-gray-900">
3056
+ ${stats.messages}
3057
+ </div>
3058
+ </div>
3059
+ <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
3060
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">
3061
+ Tool Calls
3062
+ </div>
3063
+ <div class="text-lg font-semibold text-gray-900">
3064
+ ${stats.toolCalls}
3065
+ </div>
3066
+ </div>
3067
+ <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
3068
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">
3069
+ Errors
3070
+ </div>
3071
+ <div class="text-lg font-semibold text-gray-900">
3072
+ ${stats.errors}
3073
+ </div>
3074
+ </div>
3075
+ </div>
3076
+ </div>
3077
+
3078
+ <!-- Current State Section -->
3079
+ <div class="rounded-lg border border-gray-200 bg-white">
3080
+ <div class="border-b border-gray-200 px-4 py-3">
3081
+ <h4 class="text-sm font-semibold text-gray-900">Current State</h4>
3082
+ </div>
3083
+ <div class="overflow-auto p-4">
3084
+ ${
3085
+ this.hasRenderableState(state)
3086
+ ? html`
3087
+ <pre
3088
+ class="overflow-auto rounded-md bg-gray-50 p-3 text-xs text-gray-800 max-h-64"
3089
+ ><code>${this.formatStateForDisplay(state)}</code></pre>
3090
+ `
3091
+ : html`
3092
+ <div
3093
+ class="flex h-40 items-center justify-center text-xs text-gray-500"
3094
+ >
3095
+ <div class="flex items-center gap-2 text-gray-500">
3096
+ <span class="text-lg text-gray-400"
3097
+ >${this.renderIcon("Database")}</span
3098
+ >
3099
+ <span>State is empty</span>
3100
+ </div>
3101
+ </div>
3102
+ `
3103
+ }
3104
+ </div>
3105
+ </div>
3106
+
3107
+ <!-- Current Messages Section -->
3108
+ <div class="rounded-lg border border-gray-200 bg-white">
3109
+ <div class="border-b border-gray-200 px-4 py-3">
3110
+ <h4 class="text-sm font-semibold text-gray-900">
3111
+ Current Messages
3112
+ </h4>
3113
+ </div>
3114
+ <div class="overflow-auto">
3115
+ ${
3116
+ messages && messages.length > 0
3117
+ ? html`
3118
+ <table class="w-full text-xs">
3119
+ <thead class="bg-gray-50">
3120
+ <tr>
3121
+ <th
3122
+ class="px-4 py-2 text-left font-medium text-gray-700"
3123
+ >
3124
+ Role
3125
+ </th>
3126
+ <th
3127
+ class="px-4 py-2 text-left font-medium text-gray-700"
3128
+ >
3129
+ Content
3130
+ </th>
3131
+ </tr>
3132
+ </thead>
3133
+ <tbody class="divide-y divide-gray-200">
3134
+ ${messages.map((msg) => {
3135
+ const role = msg.role || "unknown";
3136
+ const roleColors: Record<string, string> = {
3137
+ user: "bg-blue-100 text-blue-800",
3138
+ assistant: "bg-green-100 text-green-800",
3139
+ system: "bg-gray-100 text-gray-800",
3140
+ tool: "bg-amber-100 text-amber-800",
3141
+ unknown: "bg-gray-100 text-gray-600",
3142
+ };
3143
+
3144
+ const rawContent = msg.contentText ?? "";
3145
+ const toolCalls = msg.toolCalls ?? [];
3146
+ const hasContent = rawContent.trim().length > 0;
3147
+ const contentFallback =
3148
+ toolCalls.length > 0 ? "Invoked tool call" : "—";
3149
+
3150
+ return html`
3151
+ <tr>
3152
+ <td class="px-4 py-2 align-top">
3153
+ <span
3154
+ class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${
3155
+ roleColors[role] || roleColors.unknown
3156
+ }"
3157
+ >
3158
+ ${role}
3159
+ </span>
3160
+ </td>
3161
+ <td class="px-4 py-2">
3162
+ ${
3163
+ hasContent
3164
+ ? html`<div
3165
+ class="max-w-2xl whitespace-pre-wrap break-words text-gray-700"
3166
+ >
3167
+ ${rawContent}
3168
+ </div>`
3169
+ : html`<div
3170
+ class="text-xs italic text-gray-400"
3171
+ >
3172
+ ${contentFallback}
3173
+ </div>`
3174
+ }
3175
+ ${
3176
+ role === "assistant" && toolCalls.length > 0
3177
+ ? this.renderToolCallDetails(toolCalls)
3178
+ : nothing
3179
+ }
3180
+ </td>
3181
+ </tr>
3182
+ `;
3183
+ })}
3184
+ </tbody>
3185
+ </table>
3186
+ `
3187
+ : html`
3188
+ <div
3189
+ class="flex h-40 items-center justify-center text-xs text-gray-500"
3190
+ >
3191
+ <div class="flex items-center gap-2 text-gray-500">
3192
+ <span class="text-lg text-gray-400"
3193
+ >${this.renderIcon("MessageSquare")}</span
3194
+ >
3195
+ <span>No messages available</span>
3196
+ </div>
3197
+ </div>
3198
+ `
3199
+ }
3200
+ </div>
3201
+ </div>
3202
+ </div>
3203
+ `;
3204
+ }
3205
+
3206
+ private renderContextDropdown() {
3207
+ // Filter out "all-agents" when in agents view
3208
+ const filteredOptions =
3209
+ this.selectedMenu === "agents"
3210
+ ? this.contextOptions.filter((opt) => opt.key !== "all-agents")
3211
+ : this.contextOptions;
3212
+
3213
+ const selectedLabel =
3214
+ filteredOptions.find((opt) => opt.key === this.selectedContext)?.label ??
3215
+ "";
3216
+
3217
+ return html`
3218
+ <div
3219
+ class="relative z-40 min-w-0 flex-1"
3220
+ data-context-dropdown-root="true"
3221
+ >
3222
+ <button
3223
+ type="button"
3224
+ class="relative z-40 flex w-full min-w-0 max-w-[240px] items-center gap-1.5 rounded-md border border-gray-200 px-2 py-1 text-xs font-medium text-gray-700 transition hover:border-gray-300 hover:bg-gray-50"
3225
+ @pointerdown=${this.handleContextDropdownToggle}
3226
+ >
3227
+ <span class="truncate flex-1 text-left">${selectedLabel}</span>
3228
+ <span class="shrink-0 text-gray-400"
3229
+ >${this.renderIcon("ChevronDown")}</span
3230
+ >
3231
+ </button>
3232
+ ${
3233
+ this.contextMenuOpen
3234
+ ? html`
3235
+ <div
3236
+ class="absolute left-0 z-50 mt-1.5 w-40 rounded-md border border-gray-200 bg-white py-1 shadow-md ring-1 ring-black/5"
3237
+ data-context-dropdown-root="true"
3238
+ >
3239
+ ${filteredOptions.map(
3240
+ (option) => html`
3241
+ <button
3242
+ type="button"
3243
+ class="flex w-full items-center justify-between px-3 py-1.5 text-left text-xs transition hover:bg-gray-50 focus:bg-gray-50 focus:outline-none"
3244
+ data-context-dropdown-root="true"
3245
+ @click=${() => this.handleContextOptionSelect(option.key)}
3246
+ >
3247
+ <span
3248
+ class="truncate ${
3249
+ option.key === this.selectedContext
3250
+ ? "text-gray-900 font-medium"
3251
+ : "text-gray-600"
3252
+ }"
3253
+ >${option.label}</span
3254
+ >
3255
+ ${
3256
+ option.key === this.selectedContext
3257
+ ? html`<span class="text-gray-500"
3258
+ >${this.renderIcon("Check")}</span
3259
+ >`
3260
+ : nothing
3261
+ }
3262
+ </button>
3263
+ `,
3264
+ )}
3265
+ </div>
3266
+ `
3267
+ : nothing
3268
+ }
3269
+ </div>
3270
+ `;
3271
+ }
3272
+
3273
+ private handleMenuSelect(key: MenuKey): void {
3274
+ if (!this.menuItems.some((item) => item.key === key)) {
3275
+ return;
3276
+ }
3277
+
3278
+ this.selectedMenu = key;
3279
+
3280
+ // If switching to agents view and "all-agents" is selected, switch to default or first agent
3281
+ if (key === "agents" && this.selectedContext === "all-agents") {
3282
+ const agentOptions = this.contextOptions.filter(
3283
+ (opt) => opt.key !== "all-agents",
3284
+ );
3285
+ if (agentOptions.length > 0) {
3286
+ // Try to find "default" agent first
3287
+ const defaultAgent = agentOptions.find((opt) => opt.key === "default");
3288
+ this.selectedContext = defaultAgent
3289
+ ? defaultAgent.key
3290
+ : agentOptions[0]!.key;
3291
+ }
3292
+ }
3293
+
3294
+ this.contextMenuOpen = false;
3295
+ this.persistState();
3296
+ this.requestUpdate();
3297
+ }
3298
+
3299
+ private handleContextDropdownToggle(event: PointerEvent): void {
3300
+ event.preventDefault();
3301
+ event.stopPropagation();
3302
+ this.contextMenuOpen = !this.contextMenuOpen;
3303
+ this.requestUpdate();
3304
+ }
3305
+
3306
+ private handleContextOptionSelect(key: string): void {
3307
+ if (!this.contextOptions.some((option) => option.key === key)) {
3308
+ return;
3309
+ }
3310
+
3311
+ if (this.selectedContext !== key) {
3312
+ this.selectedContext = key;
3313
+ this.expandedRows.clear();
3314
+ }
3315
+
3316
+ this.contextMenuOpen = false;
3317
+ this.persistState();
3318
+ this.requestUpdate();
3319
+ }
3320
+
3321
+ private renderToolsView() {
3322
+ if (!this._core) {
3323
+ return html`
3324
+ <div
3325
+ class="flex h-full items-center justify-center px-4 py-8 text-xs text-gray-500"
3326
+ >
3327
+ No core instance available
3328
+ </div>
3329
+ `;
3330
+ }
3331
+
3332
+ this.refreshToolsSnapshot();
3333
+ const allTools = this.cachedTools;
3334
+
3335
+ if (allTools.length === 0) {
3336
+ return html`
3337
+ <div
3338
+ class="flex h-full items-center justify-center px-4 py-8 text-center"
3339
+ >
3340
+ <div class="max-w-md">
3341
+ <div
3342
+ class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8"
3343
+ >
3344
+ ${this.renderIcon("Hammer")}
3345
+ </div>
3346
+ <p class="text-sm text-gray-600">No tools available</p>
3347
+ <p class="mt-2 text-xs text-gray-500">
3348
+ Tools will appear here once agents are configured with tool
3349
+ handlers or renderers.
3350
+ </p>
3351
+ </div>
3352
+ </div>
3353
+ `;
3354
+ }
3355
+
3356
+ // Filter tools by selected agent
3357
+ const filteredTools =
3358
+ this.selectedContext === "all-agents"
3359
+ ? allTools
3360
+ : allTools.filter(
3361
+ (tool) => !tool.agentId || tool.agentId === this.selectedContext,
3362
+ );
3363
+
3364
+ return html`
3365
+ <div class="flex h-full flex-col overflow-hidden">
3366
+ <div class="overflow-auto p-4">
3367
+ <div class="space-y-3">
3368
+ ${filteredTools.map((tool) => this.renderToolCard(tool))}
3369
+ </div>
3370
+ </div>
3371
+ </div>
3372
+ `;
3373
+ }
3374
+
3375
+ private extractToolsFromAgents(): InspectorToolDefinition[] {
3376
+ if (!this._core) {
3377
+ return [];
3378
+ }
3379
+
3380
+ const tools: InspectorToolDefinition[] = [];
3381
+
3382
+ // Start with tools registered on the core (frontend tools / HIL)
3383
+ for (const coreTool of this._core.tools ?? []) {
3384
+ tools.push({
3385
+ agentId: coreTool.agentId ?? "",
3386
+ name: coreTool.name,
3387
+ description: coreTool.description,
3388
+ parameters: coreTool.parameters,
3389
+ type: "handler",
3390
+ });
3391
+ }
3392
+
3393
+ // Augment with agent-level tool handlers/renderers
3394
+ for (const [agentId, agent] of Object.entries(this._core.agents)) {
3395
+ if (!agent) continue;
3396
+
3397
+ // Try to extract tool handlers
3398
+ const handlers = (agent as { toolHandlers?: Record<string, unknown> })
3399
+ .toolHandlers;
3400
+ if (handlers && typeof handlers === "object") {
3401
+ for (const [toolName, handler] of Object.entries(handlers)) {
3402
+ if (handler && typeof handler === "object") {
3403
+ const handlerObj = handler as Record<string, unknown>;
3404
+ tools.push({
3405
+ agentId,
3406
+ name: toolName,
3407
+ description:
3408
+ (typeof handlerObj.description === "string" &&
3409
+ handlerObj.description) ||
3410
+ (handlerObj.tool as { description?: string } | undefined)
3411
+ ?.description,
3412
+ parameters:
3413
+ handlerObj.parameters ??
3414
+ (handlerObj.tool as { parameters?: unknown } | undefined)
3415
+ ?.parameters,
3416
+ type: "handler",
3417
+ });
3418
+ }
3419
+ }
3420
+ }
3421
+
3422
+ // Try to extract tool renderers
3423
+ const renderers = (agent as { toolRenderers?: Record<string, unknown> })
3424
+ .toolRenderers;
3425
+ if (renderers && typeof renderers === "object") {
3426
+ for (const [toolName, renderer] of Object.entries(renderers)) {
3427
+ // Don't duplicate if we already have it as a handler
3428
+ if (
3429
+ !tools.some((t) => t.agentId === agentId && t.name === toolName)
3430
+ ) {
3431
+ if (renderer && typeof renderer === "object") {
3432
+ const rendererObj = renderer as Record<string, unknown>;
3433
+ tools.push({
3434
+ agentId,
3435
+ name: toolName,
3436
+ description:
3437
+ (typeof rendererObj.description === "string" &&
3438
+ rendererObj.description) ||
3439
+ (rendererObj.tool as { description?: string } | undefined)
3440
+ ?.description,
3441
+ parameters:
3442
+ rendererObj.parameters ??
3443
+ (rendererObj.tool as { parameters?: unknown } | undefined)
3444
+ ?.parameters,
3445
+ type: "renderer",
3446
+ });
3447
+ }
3448
+ }
3449
+ }
3450
+ }
3451
+ }
3452
+
3453
+ return tools.sort((a, b) => {
3454
+ const agentCompare = a.agentId.localeCompare(b.agentId);
3455
+ if (agentCompare !== 0) return agentCompare;
3456
+ return a.name.localeCompare(b.name);
3457
+ });
3458
+ }
3459
+
3460
+ private renderToolCard(tool: InspectorToolDefinition) {
3461
+ const isExpanded = this.expandedTools.has(`${tool.agentId}:${tool.name}`);
3462
+ const schema = this.extractSchemaInfo(tool.parameters);
3463
+
3464
+ const typeColors = {
3465
+ handler: "bg-blue-50 text-blue-700 border-blue-200",
3466
+ renderer: "bg-purple-50 text-purple-700 border-purple-200",
3467
+ };
3468
+
3469
+ return html`
3470
+ <div class="rounded-lg border border-gray-200 bg-white overflow-hidden">
3471
+ <button
3472
+ type="button"
3473
+ class="w-full px-4 py-3 text-left transition hover:bg-gray-50"
3474
+ @click=${() =>
3475
+ this.toggleToolExpansion(`${tool.agentId}:${tool.name}`)}
3476
+ >
3477
+ <div class="flex items-start justify-between gap-3">
3478
+ <div class="flex-1 min-w-0">
3479
+ <div class="flex items-center gap-2 mb-1">
3480
+ <span class="font-mono text-sm font-semibold text-gray-900"
3481
+ >${tool.name}</span
3482
+ >
3483
+ <span
3484
+ class="inline-flex items-center rounded-sm border px-1.5 py-0.5 text-[10px] font-medium ${
3485
+ typeColors[tool.type]
3486
+ }"
3487
+ >
3488
+ ${tool.type}
3489
+ </span>
3490
+ </div>
3491
+ <div class="flex items-center gap-2 text-xs text-gray-500">
3492
+ <span class="flex items-center gap-1">
3493
+ ${this.renderIcon("Bot")}
3494
+ <span class="font-mono">${tool.agentId}</span>
3495
+ </span>
3496
+ ${
3497
+ schema.properties.length > 0
3498
+ ? html`
3499
+ <span class="text-gray-300">•</span>
3500
+ <span
3501
+ >${schema.properties.length}
3502
+ parameter${
3503
+ schema.properties.length !== 1 ? "s" : ""
3504
+ }</span
3505
+ >
3506
+ `
3507
+ : nothing
3508
+ }
3509
+ </div>
3510
+ ${
3511
+ tool.description
3512
+ ? html`<p class="mt-2 text-xs text-gray-600">
3513
+ ${tool.description}
3514
+ </p>`
3515
+ : nothing
3516
+ }
3517
+ </div>
3518
+ <span
3519
+ class="shrink-0 text-gray-400 transition ${
3520
+ isExpanded ? "rotate-180" : ""
3521
+ }"
3522
+ >
3523
+ ${this.renderIcon("ChevronDown")}
3524
+ </span>
3525
+ </div>
3526
+ </button>
3527
+
3528
+ ${
3529
+ isExpanded
3530
+ ? html`
3531
+ <div class="border-t border-gray-200 bg-gray-50/50 px-4 py-3">
3532
+ ${
3533
+ schema.properties.length > 0
3534
+ ? html`
3535
+ <h5 class="mb-3 text-xs font-semibold text-gray-700">
3536
+ Parameters
3537
+ </h5>
3538
+ <div class="space-y-3">
3539
+ ${schema.properties.map(
3540
+ (prop) => html`
3541
+ <div
3542
+ class="rounded-md border border-gray-200 bg-white p-3"
3543
+ >
3544
+ <div
3545
+ class="flex items-start justify-between gap-2 mb-1"
3546
+ >
3547
+ <span
3548
+ class="font-mono text-xs font-medium text-gray-900"
3549
+ >${prop.name}</span
3550
+ >
3551
+ <div class="flex items-center gap-1.5 shrink-0">
3552
+ ${
3553
+ prop.required
3554
+ ? html`
3555
+ <span
3556
+ class="text-[9px] rounded border border-rose-200 bg-rose-50 px-1 py-0.5 font-medium text-rose-700"
3557
+ >required</span
3558
+ >
3559
+ `
3560
+ : html`
3561
+ <span
3562
+ class="text-[9px] rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-medium text-gray-600"
3563
+ >optional</span
3564
+ >
3565
+ `
3566
+ }
3567
+ ${
3568
+ prop.type
3569
+ ? html`<span
3570
+ class="text-[9px] rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-mono text-gray-600"
3571
+ >${prop.type}</span
3572
+ >`
3573
+ : nothing
3574
+ }
3575
+ </div>
3576
+ </div>
3577
+ ${
3578
+ prop.description
3579
+ ? html`<p class="mt-1 text-xs text-gray-600">
3580
+ ${prop.description}
3581
+ </p>`
3582
+ : nothing
3583
+ }
3584
+ ${
3585
+ prop.defaultValue !== undefined
3586
+ ? html`
3587
+ <div
3588
+ class="mt-2 flex items-center gap-1.5 text-[10px] text-gray-500"
3589
+ >
3590
+ <span>Default:</span>
3591
+ <code
3592
+ class="rounded bg-gray-100 px-1 py-0.5 font-mono"
3593
+ >${JSON.stringify(
3594
+ prop.defaultValue,
3595
+ )}</code
3596
+ >
3597
+ </div>
3598
+ `
3599
+ : nothing
3600
+ }
3601
+ ${
3602
+ prop.enum && prop.enum.length > 0
3603
+ ? html`
3604
+ <div class="mt-2">
3605
+ <span class="text-[10px] text-gray-500"
3606
+ >Allowed values:</span
3607
+ >
3608
+ <div class="mt-1 flex flex-wrap gap-1">
3609
+ ${prop.enum.map(
3610
+ (val) => html`
3611
+ <code
3612
+ class="rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-mono text-gray-700"
3613
+ >${JSON.stringify(val)}</code
3614
+ >
3615
+ `,
3616
+ )}
3617
+ </div>
3618
+ </div>
3619
+ `
3620
+ : nothing
3621
+ }
3622
+ </div>
3623
+ `,
3624
+ )}
3625
+ </div>
3626
+ `
3627
+ : html`
3628
+ <div class="flex items-center justify-center py-4 text-xs text-gray-500">
3629
+ <span>No parameters defined</span>
3630
+ </div>
3631
+ `
3632
+ }
3633
+ </div>
3634
+ `
3635
+ : nothing
3636
+ }
3637
+ </div>
3638
+ `;
3639
+ }
3640
+
3641
+ private extractSchemaInfo(parameters: unknown): {
3642
+ properties: Array<{
3643
+ name: string;
3644
+ type?: string;
3645
+ description?: string;
3646
+ required: boolean;
3647
+ defaultValue?: unknown;
3648
+ enum?: unknown[];
3649
+ }>;
3650
+ } {
3651
+ const result: {
3652
+ properties: Array<{
3653
+ name: string;
3654
+ type?: string;
3655
+ description?: string;
3656
+ required: boolean;
3657
+ defaultValue?: unknown;
3658
+ enum?: unknown[];
3659
+ }>;
3660
+ } = { properties: [] };
3661
+
3662
+ if (!parameters || typeof parameters !== "object") {
3663
+ return result;
3664
+ }
3665
+
3666
+ // Try Zod schema introspection
3667
+ const zodDef = (parameters as { _def?: Record<string, unknown> })._def;
3668
+ if (zodDef && typeof zodDef === "object") {
3669
+ // Handle Zod object schema
3670
+ if (zodDef.typeName === "ZodObject") {
3671
+ const rawShape = zodDef.shape;
3672
+ const shape =
3673
+ typeof rawShape === "function"
3674
+ ? (rawShape as () => Record<string, unknown>)()
3675
+ : (rawShape as Record<string, unknown> | undefined);
3676
+
3677
+ if (!shape || typeof shape !== "object") {
3678
+ return result;
3679
+ }
3680
+ const requiredKeys = new Set<string>();
3681
+
3682
+ // Get required fields
3683
+ if (zodDef.unknownKeys === "strict" || !zodDef.catchall) {
3684
+ Object.keys(shape || {}).forEach((key) => {
3685
+ const candidate = (shape as Record<string, unknown>)[key];
3686
+ const fieldDef = (
3687
+ candidate as { _def?: Record<string, unknown> } | undefined
3688
+ )?._def;
3689
+ if (fieldDef && !this.isZodOptional(candidate)) {
3690
+ requiredKeys.add(key);
3691
+ }
3692
+ });
3693
+ }
3694
+
3695
+ // Extract properties
3696
+ for (const [key, value] of Object.entries(shape || {})) {
3697
+ const fieldInfo = this.extractZodFieldInfo(value);
3698
+ result.properties.push({
3699
+ name: key,
3700
+ type: fieldInfo.type,
3701
+ description: fieldInfo.description,
3702
+ required: requiredKeys.has(key),
3703
+ defaultValue: fieldInfo.defaultValue,
3704
+ enum: fieldInfo.enum,
3705
+ });
3706
+ }
3707
+ }
3708
+ } else if (
3709
+ (parameters as { type?: string; properties?: Record<string, unknown> })
3710
+ .type === "object" &&
3711
+ (parameters as { properties?: Record<string, unknown> }).properties
3712
+ ) {
3713
+ // Handle JSON Schema format
3714
+ const props = (parameters as { properties?: Record<string, unknown> })
3715
+ .properties;
3716
+ const required = new Set(
3717
+ Array.isArray((parameters as { required?: string[] }).required)
3718
+ ? (parameters as { required?: string[] }).required
3719
+ : [],
3720
+ );
3721
+
3722
+ for (const [key, value] of Object.entries(props ?? {})) {
3723
+ const prop = value as Record<string, unknown>;
3724
+ result.properties.push({
3725
+ name: key,
3726
+ type: prop.type as string | undefined,
3727
+ description:
3728
+ typeof prop.description === "string" ? prop.description : undefined,
3729
+ required: required.has(key),
3730
+ defaultValue: prop.default,
3731
+ enum: Array.isArray(prop.enum) ? prop.enum : undefined,
3732
+ });
3733
+ }
3734
+ }
3735
+
3736
+ return result;
3737
+ }
3738
+
3739
+ private isZodOptional(zodSchema: unknown): boolean {
3740
+ const schema = zodSchema as { _def?: Record<string, unknown> };
3741
+ if (!schema?._def) return false;
3742
+
3743
+ const def = schema._def;
3744
+
3745
+ // Check if it's explicitly optional or nullable
3746
+ if (def.typeName === "ZodOptional" || def.typeName === "ZodNullable") {
3747
+ return true;
3748
+ }
3749
+
3750
+ // Check if it has a default value
3751
+ if (def.defaultValue !== undefined) {
3752
+ return true;
3753
+ }
3754
+
3755
+ return false;
3756
+ }
3757
+
3758
+ private extractZodFieldInfo(zodSchema: unknown): {
3759
+ type?: string;
3760
+ description?: string;
3761
+ defaultValue?: unknown;
3762
+ enum?: unknown[];
3763
+ } {
3764
+ const info: {
3765
+ type?: string;
3766
+ description?: string;
3767
+ defaultValue?: unknown;
3768
+ enum?: unknown[];
3769
+ } = {};
3770
+
3771
+ const schema = zodSchema as { _def?: Record<string, unknown> };
3772
+ if (!schema?._def) return info;
3773
+
3774
+ let currentSchema = schema as { _def?: Record<string, unknown> };
3775
+ let def = currentSchema._def as Record<string, unknown>;
3776
+
3777
+ // Unwrap optional/nullable
3778
+ while (
3779
+ def.typeName === "ZodOptional" ||
3780
+ def.typeName === "ZodNullable" ||
3781
+ def.typeName === "ZodDefault"
3782
+ ) {
3783
+ if (def.typeName === "ZodDefault" && def.defaultValue !== undefined) {
3784
+ info.defaultValue =
3785
+ typeof def.defaultValue === "function"
3786
+ ? def.defaultValue()
3787
+ : def.defaultValue;
3788
+ }
3789
+ currentSchema =
3790
+ (def.innerType as { _def?: Record<string, unknown> }) ?? currentSchema;
3791
+ if (!currentSchema?._def) break;
3792
+ def = currentSchema._def as Record<string, unknown>;
3793
+ }
3794
+
3795
+ // Extract description
3796
+ info.description =
3797
+ typeof def.description === "string" ? def.description : undefined;
3798
+
3799
+ const typeName =
3800
+ typeof def.typeName === "string" ? def.typeName : undefined;
3801
+
3802
+ // Extract type
3803
+ const typeMap: Record<string, string> = {
3804
+ ZodString: "string",
3805
+ ZodNumber: "number",
3806
+ ZodBoolean: "boolean",
3807
+ ZodArray: "array",
3808
+ ZodObject: "object",
3809
+ ZodEnum: "enum",
3810
+ ZodLiteral: "literal",
3811
+ ZodUnion: "union",
3812
+ ZodAny: "any",
3813
+ ZodUnknown: "unknown",
3814
+ };
3815
+ info.type = typeName
3816
+ ? typeMap[typeName] || typeName.replace("Zod", "").toLowerCase()
3817
+ : undefined;
3818
+
3819
+ // Extract enum values
3820
+ if (typeName === "ZodEnum" && Array.isArray(def.values)) {
3821
+ info.enum = def.values as unknown[];
3822
+ } else if (typeName === "ZodLiteral" && def.value !== undefined) {
3823
+ info.enum = [def.value];
3824
+ }
3825
+
3826
+ return info;
3827
+ }
3828
+
3829
+ private toggleToolExpansion(toolId: string): void {
3830
+ if (this.expandedTools.has(toolId)) {
3831
+ this.expandedTools.delete(toolId);
3832
+ } else {
3833
+ this.expandedTools.add(toolId);
3834
+ }
3835
+ this.requestUpdate();
3836
+ }
3837
+
3838
+ private renderContextView() {
3839
+ const contextEntries = Object.entries(this.contextStore);
3840
+
3841
+ if (contextEntries.length === 0) {
3842
+ return html`
3843
+ <div
3844
+ class="flex h-full items-center justify-center px-4 py-8 text-center"
3845
+ >
3846
+ <div class="max-w-md">
3847
+ <div
3848
+ class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8"
3849
+ >
3850
+ ${this.renderIcon("FileText")}
3851
+ </div>
3852
+ <p class="text-sm text-gray-600">No context available</p>
3853
+ <p class="mt-2 text-xs text-gray-500">
3854
+ Context will appear here once added to CopilotKit.
3855
+ </p>
3856
+ </div>
3857
+ </div>
3858
+ `;
3859
+ }
3860
+
3861
+ return html`
3862
+ <div class="flex h-full flex-col overflow-hidden">
3863
+ <div class="overflow-auto p-4">
3864
+ <div class="space-y-3">
3865
+ ${contextEntries.map(([id, context]) =>
3866
+ this.renderContextCard(id, context),
3867
+ )}
3868
+ </div>
3869
+ </div>
3870
+ </div>
3871
+ `;
3872
+ }
3873
+
3874
+ private renderContextCard(
3875
+ id: string,
3876
+ context: { description?: string; value: unknown },
3877
+ ) {
3878
+ const isExpanded = this.expandedContextItems.has(id);
3879
+ const valuePreview = this.getContextValuePreview(context.value);
3880
+ const hasValue = context.value !== undefined && context.value !== null;
3881
+ const title = context.description?.trim() || id;
3882
+
3883
+ return html`
3884
+ <div class="rounded-lg border border-gray-200 bg-white overflow-hidden">
3885
+ <button
3886
+ type="button"
3887
+ class="w-full px-4 py-3 text-left transition hover:bg-gray-50"
3888
+ @click=${() => this.toggleContextExpansion(id)}
3889
+ >
3890
+ <div class="flex items-start justify-between gap-3">
3891
+ <div class="flex-1 min-w-0">
3892
+ <p class="text-sm font-medium text-gray-900 mb-1">${title}</p>
3893
+ <div class="flex items-center gap-2 text-xs text-gray-500">
3894
+ <span
3895
+ class="font-mono truncate inline-block align-middle"
3896
+ style="max-width: 180px;"
3897
+ >${id}</span
3898
+ >
3899
+ ${
3900
+ hasValue
3901
+ ? html`
3902
+ <span class="text-gray-300">•</span>
3903
+ <span class="truncate">${valuePreview}</span>
3904
+ `
3905
+ : nothing
3906
+ }
3907
+ </div>
3908
+ </div>
3909
+ <span
3910
+ class="shrink-0 text-gray-400 transition ${
3911
+ isExpanded ? "rotate-180" : ""
3912
+ }"
3913
+ >
3914
+ ${this.renderIcon("ChevronDown")}
3915
+ </span>
3916
+ </div>
3917
+ </button>
3918
+
3919
+ ${
3920
+ isExpanded
3921
+ ? html`
3922
+ <div class="border-t border-gray-200 bg-gray-50/50 px-4 py-3">
3923
+ <div class="mb-3">
3924
+ <h5 class="mb-1 text-xs font-semibold text-gray-700">ID</h5>
3925
+ <code
3926
+ class="block rounded bg-white border border-gray-200 px-2 py-1 text-[10px] font-mono text-gray-600"
3927
+ >${id}</code
3928
+ >
3929
+ </div>
3930
+ ${
3931
+ hasValue
3932
+ ? html`
3933
+ <div class="mb-2 flex items-center justify-between gap-2">
3934
+ <h5 class="text-xs font-semibold text-gray-700">
3935
+ Value
3936
+ </h5>
3937
+ <button
3938
+ class="flex items-center gap-1 rounded-md border border-gray-200 bg-white px-2 py-1 text-[10px] font-medium text-gray-700 transition hover:bg-gray-50"
3939
+ type="button"
3940
+ @click=${(e: Event) => {
3941
+ e.stopPropagation();
3942
+ void this.copyContextValue(context.value, id);
3943
+ }}
3944
+ >
3945
+ ${
3946
+ this.copiedContextItems.has(id)
3947
+ ? "Copied"
3948
+ : "Copy JSON"
3949
+ }
3950
+ </button>
3951
+ </div>
3952
+ <div
3953
+ class="rounded-md border border-gray-200 bg-white p-3"
3954
+ >
3955
+ <pre
3956
+ class="overflow-auto text-xs text-gray-800 max-h-96"
3957
+ ><code>${this.formatContextValue(
3958
+ context.value,
3959
+ )}</code></pre>
3960
+ </div>
3961
+ `
3962
+ : html`
3963
+ <div class="flex items-center justify-center py-4 text-xs text-gray-500">
3964
+ <span>No value available</span>
3965
+ </div>
3966
+ `
3967
+ }
3968
+ </div>
3969
+ `
3970
+ : nothing
3971
+ }
3972
+ </div>
3973
+ `;
3974
+ }
3975
+
3976
+ private getContextValuePreview(value: unknown): string {
3977
+ if (value === undefined || value === null) {
3978
+ return "—";
3979
+ }
3980
+
3981
+ if (typeof value === "string") {
3982
+ return value.length > 50 ? `${value.substring(0, 50)}...` : value;
3983
+ }
3984
+
3985
+ if (typeof value === "number" || typeof value === "boolean") {
3986
+ return String(value);
3987
+ }
3988
+
3989
+ if (Array.isArray(value)) {
3990
+ return `Array(${value.length})`;
3991
+ }
3992
+
3993
+ if (typeof value === "object") {
3994
+ const keys = Object.keys(value);
3995
+ return `Object with ${keys.length} key${keys.length !== 1 ? "s" : ""}`;
3996
+ }
3997
+
3998
+ if (typeof value === "function") {
3999
+ return "Function";
4000
+ }
4001
+
4002
+ return String(value);
4003
+ }
4004
+
4005
+ private formatContextValue(value: unknown): string {
4006
+ if (value === undefined) {
4007
+ return "undefined";
4008
+ }
4009
+
4010
+ if (value === null) {
4011
+ return "null";
4012
+ }
4013
+
4014
+ if (typeof value === "function") {
4015
+ return value.toString();
4016
+ }
4017
+
4018
+ try {
4019
+ return JSON.stringify(value, null, 2);
4020
+ } catch {
4021
+ return String(value);
4022
+ }
4023
+ }
4024
+
4025
+ private async copyContextValue(
4026
+ value: unknown,
4027
+ contextId: string,
4028
+ ): Promise<void> {
4029
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
4030
+ console.warn("Clipboard API is not available in this environment.");
4031
+ return;
4032
+ }
4033
+
4034
+ const serialized = this.formatContextValue(value);
4035
+ try {
4036
+ await navigator.clipboard.writeText(serialized);
4037
+ this.copiedContextItems.add(contextId);
4038
+ this.requestUpdate();
4039
+ setTimeout(() => {
4040
+ this.copiedContextItems.delete(contextId);
4041
+ this.requestUpdate();
4042
+ }, 1500);
4043
+ } catch (error) {
4044
+ console.error("Failed to copy context value:", error);
4045
+ }
4046
+ }
4047
+
4048
+ private toggleContextExpansion(contextId: string): void {
4049
+ if (this.expandedContextItems.has(contextId)) {
4050
+ this.expandedContextItems.delete(contextId);
4051
+ } else {
4052
+ this.expandedContextItems.add(contextId);
4053
+ }
4054
+ this.requestUpdate();
4055
+ }
4056
+
4057
+ private handleGlobalPointerDown = (event: PointerEvent): void => {
4058
+ if (!this.contextMenuOpen) {
4059
+ return;
4060
+ }
4061
+
4062
+ const clickedDropdown = event.composedPath().some((node) => {
4063
+ return (
4064
+ node instanceof HTMLElement &&
4065
+ node.dataset?.contextDropdownRoot === "true"
4066
+ );
4067
+ });
4068
+
4069
+ if (!clickedDropdown) {
4070
+ this.contextMenuOpen = false;
4071
+ this.requestUpdate();
4072
+ }
4073
+ };
4074
+
4075
+ private toggleRowExpansion(eventId: string): void {
4076
+ // Don't toggle if user is selecting text
4077
+ const selection = window.getSelection();
4078
+ if (selection && selection.toString().length > 0) {
4079
+ return;
4080
+ }
4081
+
4082
+ if (this.expandedRows.has(eventId)) {
4083
+ this.expandedRows.delete(eventId);
4084
+ } else {
4085
+ this.expandedRows.add(eventId);
4086
+ }
4087
+ this.requestUpdate();
4088
+ }
4089
+
4090
+ private renderAnnouncementPanel() {
4091
+ if (!this.isOpen) {
4092
+ return nothing;
4093
+ }
4094
+
4095
+ // Ensure loading is triggered even if we mounted in an already-open state
4096
+ this.ensureAnnouncementLoading();
4097
+
4098
+ if (!this.hasUnseenAnnouncement) {
4099
+ return nothing;
4100
+ }
4101
+
4102
+ if (!this.announcementLoaded && !this.announcementMarkdown) {
4103
+ return html`<div
4104
+ class="mx-4 my-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-800 shadow-[0_12px_30px_rgba(15,23,42,0.12)]"
4105
+ >
4106
+ <div class="flex items-center gap-2 font-semibold">
4107
+ <span
4108
+ class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm"
4109
+ >
4110
+ ${this.renderIcon("Megaphone")}
4111
+ </span>
4112
+ <span>Loading latest announcement…</span>
4113
+ </div>
4114
+ </div>`;
4115
+ }
4116
+
4117
+ if (this.announcementLoadError) {
4118
+ return html`<div
4119
+ class="mx-4 my-3 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-900 shadow-[0_12px_30px_rgba(15,23,42,0.12)]"
4120
+ >
4121
+ <div class="flex items-center gap-2 font-semibold">
4122
+ <span
4123
+ class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-rose-600 text-white shadow-sm"
4124
+ >
4125
+ ${this.renderIcon("Megaphone")}
4126
+ </span>
4127
+ <span>Announcement unavailable</span>
4128
+ </div>
4129
+ <p class="mt-2 text-xs text-rose-800">
4130
+ We couldn’t load the latest notice. Please try opening the inspector
4131
+ again.
4132
+ </p>
4133
+ </div>`;
4134
+ }
4135
+
4136
+ if (!this.announcementMarkdown) {
4137
+ return nothing;
4138
+ }
4139
+
4140
+ const content = this.announcementHtml
4141
+ ? unsafeHTML(this.announcementHtml)
4142
+ : html`<pre class="whitespace-pre-wrap text-sm text-gray-900">
4143
+ ${this.announcementMarkdown}</pre
4144
+ >`;
4145
+
4146
+ return html`<div
4147
+ class="mx-4 my-3 rounded-xl border border-slate-200 bg-white px-4 py-4 shadow-[0_12px_30px_rgba(15,23,42,0.12)]"
4148
+ >
4149
+ <div
4150
+ class="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-900"
4151
+ >
4152
+ <span
4153
+ class="inline-flex h-7 w-7 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm"
4154
+ >
4155
+ ${this.renderIcon("Megaphone")}
4156
+ </span>
4157
+ <span>Announcement</span>
4158
+ <button
4159
+ class="announcement-dismiss ml-auto"
4160
+ type="button"
4161
+ @click=${this.handleDismissAnnouncement}
4162
+ aria-label="Dismiss announcement"
4163
+ >
4164
+ Dismiss
4165
+ </button>
4166
+ </div>
4167
+ <div class="announcement-content text-sm leading-relaxed text-gray-900">
4168
+ ${content}
4169
+ </div>
4170
+ </div>`;
4171
+ }
4172
+
4173
+ private ensureAnnouncementLoading(): void {
4174
+ if (
4175
+ this.announcementPromise ||
4176
+ typeof window === "undefined" ||
4177
+ typeof fetch === "undefined"
4178
+ ) {
4179
+ return;
4180
+ }
4181
+ this.announcementPromise = this.fetchAnnouncement();
4182
+ }
4183
+
4184
+ private renderAnnouncementPreview() {
4185
+ if (
4186
+ !this.hasUnseenAnnouncement ||
4187
+ !this.showAnnouncementPreview ||
4188
+ !this.announcementPreviewText
4189
+ ) {
4190
+ return nothing;
4191
+ }
4192
+
4193
+ const side =
4194
+ this.contextState.button.anchor.horizontal === "left" ? "right" : "left";
4195
+
4196
+ return html`<div
4197
+ class="announcement-preview"
4198
+ data-side=${side}
4199
+ role="note"
4200
+ @click=${() => this.handleAnnouncementPreviewClick()}
4201
+ >
4202
+ <span>${this.announcementPreviewText}</span>
4203
+ <span class="announcement-preview__arrow"></span>
4204
+ </div>`;
4205
+ }
4206
+
4207
+ private handleAnnouncementPreviewClick(): void {
4208
+ this.showAnnouncementPreview = false;
4209
+ this.openInspector();
4210
+ }
4211
+
4212
+ private handleDismissAnnouncement = (): void => {
4213
+ this.markAnnouncementSeen();
4214
+ };
4215
+
4216
+ private async fetchAnnouncement(): Promise<void> {
4217
+ try {
4218
+ const response = await fetch(ANNOUNCEMENT_URL, { cache: "no-cache" });
4219
+ if (!response.ok) {
4220
+ throw new Error(`Failed to load announcement (${response.status})`);
4221
+ }
4222
+
4223
+ const data = (await response.json()) as {
4224
+ timestamp?: unknown;
4225
+ previewText?: unknown;
4226
+ announcement?: unknown;
4227
+ };
4228
+
4229
+ const timestamp =
4230
+ typeof data?.timestamp === "string" ? data.timestamp : null;
4231
+ const previewText =
4232
+ typeof data?.previewText === "string" ? data.previewText : null;
4233
+ const markdown =
4234
+ typeof data?.announcement === "string" ? data.announcement : null;
4235
+
4236
+ if (!timestamp || !markdown) {
4237
+ throw new Error("Malformed announcement payload");
4238
+ }
4239
+
4240
+ const storedTimestamp = this.loadStoredAnnouncementTimestamp();
4241
+
4242
+ this.announcementTimestamp = timestamp;
4243
+ this.announcementPreviewText = previewText ?? "";
4244
+ this.announcementMarkdown = markdown;
4245
+ this.hasUnseenAnnouncement =
4246
+ (!storedTimestamp || storedTimestamp !== timestamp) &&
4247
+ !!this.announcementPreviewText;
4248
+ this.showAnnouncementPreview = this.hasUnseenAnnouncement;
4249
+ this.announcementHtml = await this.convertMarkdownToHtml(markdown);
4250
+ this.announcementLoaded = true;
4251
+
4252
+ this.requestUpdate();
4253
+ } catch (error) {
4254
+ this.announcementLoadError = error;
4255
+ this.announcementLoaded = true;
4256
+ this.requestUpdate();
4257
+ }
4258
+ }
4259
+
4260
+ private async convertMarkdownToHtml(
4261
+ markdown: string,
4262
+ ): Promise<string | null> {
4263
+ const renderer = new marked.Renderer();
4264
+ renderer.link = (href, title, text) => {
4265
+ const safeHref = this.escapeHtmlAttr(this.appendRefParam(href ?? ""));
4266
+ const titleAttr = title ? ` title="${this.escapeHtmlAttr(title)}"` : "";
4267
+ return `<a href="${safeHref}" target="_blank" rel="noopener"${titleAttr}>${text}</a>`;
4268
+ };
4269
+ return marked.parse(markdown, { renderer });
4270
+ }
4271
+
4272
+ private appendRefParam(href: string): string {
4273
+ try {
4274
+ const url = new URL(
4275
+ href,
4276
+ typeof window !== "undefined"
4277
+ ? window.location.href
4278
+ : "https://copilotkit.ai",
4279
+ );
4280
+ if (!url.searchParams.has("ref")) {
4281
+ url.searchParams.append("ref", "cpk-inspector");
4282
+ }
4283
+ return url.toString();
4284
+ } catch {
4285
+ return href;
4286
+ }
4287
+ }
4288
+
4289
+ private escapeHtmlAttr(value: string): string {
4290
+ return value
4291
+ .replace(/&/g, "&amp;")
4292
+ .replace(/</g, "&lt;")
4293
+ .replace(/>/g, "&gt;")
4294
+ .replace(/\"/g, "&quot;")
4295
+ .replace(/'/g, "&#39;");
4296
+ }
4297
+
4298
+ private loadStoredAnnouncementTimestamp(): string | null {
4299
+ if (typeof window === "undefined" || !window.localStorage) {
4300
+ return null;
4301
+ }
4302
+ try {
4303
+ const raw = window.localStorage.getItem(ANNOUNCEMENT_STORAGE_KEY);
4304
+ if (!raw) {
4305
+ return null;
4306
+ }
4307
+ const parsed = JSON.parse(raw);
4308
+ if (parsed && typeof parsed.timestamp === "string") {
4309
+ return parsed.timestamp;
4310
+ }
4311
+ // Backward compatibility: previous shape { hash }
4312
+ return null;
4313
+ } catch {
4314
+ // ignore malformed storage
4315
+ }
4316
+ return null;
4317
+ }
4318
+
4319
+ private persistAnnouncementTimestamp(timestamp: string): void {
4320
+ if (typeof window === "undefined" || !window.localStorage) {
4321
+ return;
4322
+ }
4323
+ try {
4324
+ const payload = JSON.stringify({ timestamp });
4325
+ window.localStorage.setItem(ANNOUNCEMENT_STORAGE_KEY, payload);
4326
+ } catch {
4327
+ // Non-fatal if storage is unavailable
4328
+ }
4329
+ }
4330
+
4331
+ private markAnnouncementSeen(): void {
4332
+ // Clear badge only when explicitly dismissed
4333
+ this.hasUnseenAnnouncement = false;
4334
+ this.showAnnouncementPreview = false;
4335
+
4336
+ if (!this.announcementTimestamp) {
4337
+ // If still loading, attempt once more after promise resolves; avoid infinite requeues
4338
+ if (this.announcementPromise && !this.announcementLoaded) {
4339
+ void this.announcementPromise
4340
+ .then(() => this.markAnnouncementSeen())
4341
+ .catch(() => undefined);
4342
+ }
4343
+ this.requestUpdate();
4344
+ return;
4345
+ }
4346
+
4347
+ this.persistAnnouncementTimestamp(this.announcementTimestamp);
4348
+ this.requestUpdate();
4349
+ }
4350
+ }
4351
+
4352
+ export function defineWebInspector(): void {
4353
+ if (!customElements.get(WEB_INSPECTOR_TAG)) {
4354
+ customElements.define(WEB_INSPECTOR_TAG, WebInspectorElement);
4355
+ }
4356
+ }
4357
+
4358
+ defineWebInspector();
4359
+
4360
+ declare global {
4361
+ interface HTMLElementTagNameMap {
4362
+ "cpk-web-inspector": WebInspectorElement;
4363
+ }
4364
+ }