@copilotkitnext/web-inspector 0.0.28 → 0.0.29

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.
package/src/index.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { LitElement, css, html, nothing, unsafeCSS } from "lit";
2
2
  import { styleMap } from "lit/directives/style-map.js";
3
3
  import tailwindStyles from "./styles/generated.css";
4
- import logoMarkUrl from "./assets/logo-mark.svg";
4
+ import inspectorLogoUrl from "./assets/inspector-logo.svg";
5
+ import inspectorLogoIconUrl from "./assets/inspector-logo-icon.svg";
5
6
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
7
+ import { marked } from "marked";
6
8
  import { icons } from "lucide";
7
- import type { CopilotKitCore, CopilotKitCoreSubscriber } from "@copilotkitnext/core";
9
+ import {
10
+ CopilotKitCore,
11
+ CopilotKitCoreRuntimeConnectionStatus,
12
+ type CopilotKitCoreSubscriber,
13
+ type CopilotKitCoreErrorCode,
14
+ } from "@copilotkitnext/core";
8
15
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
9
16
  import type { Anchor, ContextKey, ContextState, DockMode, Position, Size } from "./lib/types";
10
17
  import {
@@ -26,7 +33,7 @@ import {
26
33
  isValidDockMode,
27
34
  } from "./lib/persistence";
28
35
 
29
- export const WEB_INSPECTOR_TAG = "web-inspector" as const;
36
+ export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const;
30
37
 
31
38
  type LucideIconName = keyof typeof icons;
32
39
 
@@ -43,34 +50,108 @@ const DRAG_THRESHOLD = 6;
43
50
  const MIN_WINDOW_WIDTH = 600;
44
51
  const MIN_WINDOW_WIDTH_DOCKED_LEFT = 420;
45
52
  const MIN_WINDOW_HEIGHT = 200;
46
- const COOKIE_NAME = "copilotkit_inspector_state";
47
- const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days
53
+ const INSPECTOR_STORAGE_KEY = "cpk:inspector:state";
54
+ const ANNOUNCEMENT_STORAGE_KEY = "cpk:inspector:announcements";
55
+ const ANNOUNCEMENT_URL = "https://cdn.copilotkit.ai/announcements.json";
48
56
  const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 };
49
57
  const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 560 };
50
58
  const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar
51
59
  const MAX_AGENT_EVENTS = 200;
52
60
  const MAX_TOTAL_EVENTS = 500;
53
61
 
62
+ type InspectorAgentEventType =
63
+ | "RUN_STARTED"
64
+ | "RUN_FINISHED"
65
+ | "RUN_ERROR"
66
+ | "TEXT_MESSAGE_START"
67
+ | "TEXT_MESSAGE_CONTENT"
68
+ | "TEXT_MESSAGE_END"
69
+ | "TOOL_CALL_START"
70
+ | "TOOL_CALL_ARGS"
71
+ | "TOOL_CALL_END"
72
+ | "TOOL_CALL_RESULT"
73
+ | "STATE_SNAPSHOT"
74
+ | "STATE_DELTA"
75
+ | "MESSAGES_SNAPSHOT"
76
+ | "RAW_EVENT"
77
+ | "CUSTOM_EVENT";
78
+
79
+ const AGENT_EVENT_TYPES: readonly InspectorAgentEventType[] = [
80
+ "RUN_STARTED",
81
+ "RUN_FINISHED",
82
+ "RUN_ERROR",
83
+ "TEXT_MESSAGE_START",
84
+ "TEXT_MESSAGE_CONTENT",
85
+ "TEXT_MESSAGE_END",
86
+ "TOOL_CALL_START",
87
+ "TOOL_CALL_ARGS",
88
+ "TOOL_CALL_END",
89
+ "TOOL_CALL_RESULT",
90
+ "STATE_SNAPSHOT",
91
+ "STATE_DELTA",
92
+ "MESSAGES_SNAPSHOT",
93
+ "RAW_EVENT",
94
+ "CUSTOM_EVENT",
95
+ ] as const;
96
+
97
+ type SanitizedValue =
98
+ | string
99
+ | number
100
+ | boolean
101
+ | null
102
+ | SanitizedValue[]
103
+ | { [key: string]: SanitizedValue };
104
+
105
+ type InspectorToolCall = {
106
+ id?: string;
107
+ function?: {
108
+ name?: string;
109
+ arguments?: SanitizedValue | string;
110
+ };
111
+ toolName?: string;
112
+ status?: string;
113
+ };
114
+
115
+ type InspectorMessage = {
116
+ id?: string;
117
+ role: string;
118
+ contentText: string;
119
+ contentRaw?: SanitizedValue;
120
+ toolCalls: InspectorToolCall[];
121
+ };
122
+
123
+ type InspectorToolDefinition = {
124
+ agentId: string;
125
+ name: string;
126
+ description?: string;
127
+ parameters?: unknown;
128
+ type: "handler" | "renderer";
129
+ };
130
+
54
131
  type InspectorEvent = {
55
132
  id: string;
56
133
  agentId: string;
57
- type: string;
134
+ type: InspectorAgentEventType;
58
135
  timestamp: number;
59
- payload: unknown;
136
+ payload: SanitizedValue;
60
137
  };
61
138
 
62
139
  export class WebInspectorElement extends LitElement {
63
140
  static properties = {
64
141
  core: { attribute: false },
142
+ autoAttachCore: { type: Boolean, attribute: "auto-attach-core" },
65
143
  } as const;
66
144
 
67
145
  private _core: CopilotKitCore | null = null;
68
146
  private coreSubscriber: CopilotKitCoreSubscriber | null = null;
69
147
  private coreUnsubscribe: (() => void) | null = null;
148
+ private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null;
149
+ private coreProperties: Readonly<Record<string, unknown>> = {};
150
+ private lastCoreError: { code: CopilotKitCoreErrorCode; message: string } | null = null;
70
151
  private agentSubscriptions: Map<string, () => void> = new Map();
71
152
  private agentEvents: Map<string, InspectorEvent[]> = new Map();
72
- private agentMessages: Map<string, unknown[]> = new Map();
73
- private agentStates: Map<string, unknown> = new Map();
153
+ private agentMessages: Map<string, InspectorMessage[]> = new Map();
154
+ private agentStates: Map<string, SanitizedValue> = new Map();
74
155
  private flattenedEvents: InspectorEvent[] = [];
75
156
  private eventCounter = 0;
76
157
  private contextStore: Record<string, { description?: string; value: unknown }> = {};
@@ -89,6 +170,22 @@ export class WebInspectorElement extends LitElement {
89
170
  private previousBodyMargins: { left: string; bottom: string } | null = null;
90
171
  private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;
91
172
  private pendingSelectedContext: string | null = null;
173
+ private autoAttachCore = true;
174
+ private attemptedAutoAttach = false;
175
+ private cachedTools: InspectorToolDefinition[] = [];
176
+ private toolSignature = "";
177
+ private eventFilterText = "";
178
+ private eventTypeFilter: InspectorAgentEventType | "all" = "all";
179
+
180
+ private announcementMarkdown: string | null = null;
181
+ private announcementHtml: string | null = null;
182
+ private announcementTimestamp: string | null = null;
183
+ private announcementPreviewText: string | null = null;
184
+ private hasUnseenAnnouncement = false;
185
+ private announcementLoaded = false;
186
+ private announcementLoadError: unknown = null;
187
+ private announcementPromise: Promise<void> | null = null;
188
+ private showAnnouncementPreview = true;
92
189
 
93
190
  get core(): CopilotKitCore | null {
94
191
  return this._core;
@@ -137,18 +234,34 @@ export class WebInspectorElement extends LitElement {
137
234
 
138
235
  private readonly menuItems: MenuItem[] = [
139
236
  { key: "ag-ui-events", label: "AG-UI Events", icon: "Zap" },
140
- { key: "agents", label: "Agents", icon: "Bot" },
237
+ { key: "agents", label: "Agent", icon: "Bot" },
141
238
  { key: "frontend-tools", label: "Frontend Tools", icon: "Hammer" },
142
- { key: "agent-context", label: "Agent Context", icon: "FileText" },
239
+ { key: "agent-context", label: "Context", icon: "FileText" },
143
240
  ];
144
241
 
145
242
  private attachToCore(core: CopilotKitCore): void {
243
+ this.runtimeStatus = core.runtimeConnectionStatus;
244
+ this.coreProperties = core.properties;
245
+ this.lastCoreError = null;
246
+
146
247
  this.coreSubscriber = {
248
+ onRuntimeConnectionStatusChanged: ({ status }) => {
249
+ this.runtimeStatus = status;
250
+ this.requestUpdate();
251
+ },
252
+ onPropertiesChanged: ({ properties }) => {
253
+ this.coreProperties = properties;
254
+ this.requestUpdate();
255
+ },
256
+ onError: ({ code, error }) => {
257
+ this.lastCoreError = { code, message: error.message };
258
+ this.requestUpdate();
259
+ },
147
260
  onAgentsChanged: ({ agents }) => {
148
261
  this.processAgentsChanged(agents);
149
262
  },
150
263
  onContextChanged: ({ context }) => {
151
- this.contextStore = { ...context };
264
+ this.contextStore = this.normalizeContextStore(context);
152
265
  this.requestUpdate();
153
266
  },
154
267
  } satisfies CopilotKitCoreSubscriber;
@@ -158,7 +271,7 @@ export class WebInspectorElement extends LitElement {
158
271
 
159
272
  // Initialize context from core
160
273
  if (core.context) {
161
- this.contextStore = { ...core.context };
274
+ this.contextStore = this.normalizeContextStore(core.context);
162
275
  }
163
276
  }
164
277
 
@@ -168,6 +281,11 @@ export class WebInspectorElement extends LitElement {
168
281
  this.coreUnsubscribe = null;
169
282
  }
170
283
  this.coreSubscriber = null;
284
+ this.runtimeStatus = null;
285
+ this.lastCoreError = null;
286
+ this.coreProperties = {};
287
+ this.cachedTools = [];
288
+ this.toolSignature = "";
171
289
  this.teardownAgentSubscriptions();
172
290
  }
173
291
 
@@ -204,9 +322,62 @@ export class WebInspectorElement extends LitElement {
204
322
  }
205
323
 
206
324
  this.updateContextOptions(seenAgentIds);
325
+ this.refreshToolsSnapshot();
207
326
  this.requestUpdate();
208
327
  }
209
328
 
329
+ private refreshToolsSnapshot(): void {
330
+ if (!this._core) {
331
+ if (this.cachedTools.length > 0) {
332
+ this.cachedTools = [];
333
+ this.toolSignature = "";
334
+ this.requestUpdate();
335
+ }
336
+ return;
337
+ }
338
+
339
+ const tools = this.extractToolsFromAgents();
340
+ const signature = JSON.stringify(
341
+ tools.map((tool) => ({
342
+ agentId: tool.agentId,
343
+ name: tool.name,
344
+ type: tool.type,
345
+ hasDescription: Boolean(tool.description),
346
+ hasParameters: Boolean(tool.parameters),
347
+ })),
348
+ );
349
+
350
+ if (signature !== this.toolSignature) {
351
+ this.toolSignature = signature;
352
+ this.cachedTools = tools;
353
+ this.requestUpdate();
354
+ }
355
+ }
356
+
357
+ private tryAutoAttachCore(): void {
358
+ if (this.attemptedAutoAttach || this._core || !this.autoAttachCore || typeof window === "undefined") {
359
+ return;
360
+ }
361
+
362
+ this.attemptedAutoAttach = true;
363
+
364
+ const globalWindow = window as unknown as Record<string, unknown>;
365
+ const globalCandidates: Array<unknown> = [
366
+ // Common app-level globals used during development
367
+ globalWindow.__COPILOTKIT_CORE__,
368
+ (globalWindow.copilotkit as { core?: unknown } | undefined)?.core,
369
+ globalWindow.copilotkitCore,
370
+ ];
371
+
372
+ const foundCore = globalCandidates.find(
373
+ (candidate): candidate is CopilotKitCore => !!candidate && typeof candidate === "object",
374
+ );
375
+
376
+ if (foundCore) {
377
+ this.core = foundCore;
378
+ }
379
+ }
380
+
210
381
  private subscribeToAgent(agent: AbstractAgent): void {
211
382
  if (!agent.agentId) {
212
383
  return;
@@ -288,14 +459,15 @@ export class WebInspectorElement extends LitElement {
288
459
  }
289
460
  }
290
461
 
291
- private recordAgentEvent(agentId: string, type: string, payload: unknown): void {
462
+ private recordAgentEvent(agentId: string, type: InspectorAgentEventType, payload: unknown): void {
292
463
  const eventId = `${agentId}:${++this.eventCounter}`;
464
+ const normalizedPayload = this.normalizeEventPayload(type, payload);
293
465
  const event: InspectorEvent = {
294
466
  id: eventId,
295
467
  agentId,
296
468
  type,
297
469
  timestamp: Date.now(),
298
- payload,
470
+ payload: normalizedPayload,
299
471
  };
300
472
 
301
473
  const currentAgentEvents = this.agentEvents.get(agentId) ?? [];
@@ -303,6 +475,7 @@ export class WebInspectorElement extends LitElement {
303
475
  this.agentEvents.set(agentId, nextAgentEvents);
304
476
 
305
477
  this.flattenedEvents = [event, ...this.flattenedEvents].slice(0, MAX_TOTAL_EVENTS);
478
+ this.refreshToolsSnapshot();
306
479
  this.requestUpdate();
307
480
  }
308
481
 
@@ -311,9 +484,8 @@ export class WebInspectorElement extends LitElement {
311
484
  return;
312
485
  }
313
486
 
314
- const messages = (agent as { messages?: unknown }).messages;
315
-
316
- if (Array.isArray(messages)) {
487
+ const messages = this.normalizeAgentMessages((agent as { messages?: unknown }).messages);
488
+ if (messages) {
317
489
  this.agentMessages.set(agent.agentId, messages);
318
490
  } else {
319
491
  this.agentMessages.delete(agent.agentId);
@@ -332,7 +504,7 @@ export class WebInspectorElement extends LitElement {
332
504
  if (state === undefined || state === null) {
333
505
  this.agentStates.delete(agent.agentId);
334
506
  } else {
335
- this.agentStates.set(agent.agentId, state);
507
+ this.agentStates.set(agent.agentId, this.sanitizeForLogging(state));
336
508
  }
337
509
 
338
510
  this.requestUpdate();
@@ -397,17 +569,42 @@ export class WebInspectorElement extends LitElement {
397
569
  return this.agentEvents.get(this.selectedContext) ?? [];
398
570
  }
399
571
 
400
- private getLatestStateForAgent(agentId: string): unknown | null {
572
+ private filterEvents(events: InspectorEvent[]): InspectorEvent[] {
573
+ const query = this.eventFilterText.trim().toLowerCase();
574
+
575
+ return events.filter((event) => {
576
+ if (this.eventTypeFilter !== "all" && event.type !== this.eventTypeFilter) {
577
+ return false;
578
+ }
579
+
580
+ if (!query) {
581
+ return true;
582
+ }
583
+
584
+ const payloadText = this.stringifyPayload(event.payload, false).toLowerCase();
585
+ return (
586
+ event.type.toLowerCase().includes(query) ||
587
+ event.agentId.toLowerCase().includes(query) ||
588
+ payloadText.includes(query)
589
+ );
590
+ });
591
+ }
592
+
593
+ private getLatestStateForAgent(agentId: string): SanitizedValue | null {
401
594
  if (this.agentStates.has(agentId)) {
402
- return this.agentStates.get(agentId);
595
+ const value = this.agentStates.get(agentId);
596
+ return value === undefined ? null : value;
403
597
  }
404
598
 
405
599
  const events = this.agentEvents.get(agentId) ?? [];
406
600
  const stateEvent = events.find((e) => e.type === "STATE_SNAPSHOT");
407
- return stateEvent?.payload ?? null;
601
+ if (!stateEvent) {
602
+ return null;
603
+ }
604
+ return stateEvent.payload;
408
605
  }
409
606
 
410
- private getLatestMessagesForAgent(agentId: string): unknown[] | null {
607
+ private getLatestMessagesForAgent(agentId: string): InspectorMessage[] | null {
411
608
  const messages = this.agentMessages.get(agentId);
412
609
  return messages ?? null;
413
610
  }
@@ -445,22 +642,11 @@ export class WebInspectorElement extends LitElement {
445
642
 
446
643
  const messages = this.agentMessages.get(agentId);
447
644
 
448
- const toolCallCount = Array.isArray(messages)
449
- ? (messages as unknown[]).reduce<number>((count, rawMessage) => {
450
- if (!rawMessage || typeof rawMessage !== 'object') {
451
- return count;
452
- }
453
-
454
- const toolCalls = (rawMessage as { toolCalls?: unknown }).toolCalls;
455
- if (!Array.isArray(toolCalls)) {
456
- return count;
457
- }
458
-
459
- return count + toolCalls.length;
460
- }, 0)
645
+ const toolCallCount = messages
646
+ ? messages.reduce((count, message) => count + (message.toolCalls?.length ?? 0), 0)
461
647
  : events.filter((e) => e.type === "TOOL_CALL_END").length;
462
648
 
463
- const messageCount = Array.isArray(messages) ? messages.length : 0;
649
+ const messageCount = messages?.length ?? 0;
464
650
 
465
651
  return {
466
652
  totalEvents: events.length,
@@ -471,7 +657,7 @@ export class WebInspectorElement extends LitElement {
471
657
  };
472
658
  }
473
659
 
474
- private renderToolCallDetails(toolCalls: unknown[]) {
660
+ private renderToolCallDetails(toolCalls: InspectorToolCall[]) {
475
661
  if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
476
662
  return nothing;
477
663
  }
@@ -479,10 +665,9 @@ export class WebInspectorElement extends LitElement {
479
665
  return html`
480
666
  <div class="mt-2 space-y-2">
481
667
  ${toolCalls.map((call, index) => {
482
- const toolCall = call as any;
483
- const functionName = typeof toolCall?.function?.name === 'string' ? toolCall.function.name : 'Unknown function';
484
- const callId = typeof toolCall?.id === 'string' ? toolCall.id : `tool-call-${index + 1}`;
485
- const argsString = this.formatToolCallArguments(toolCall?.function?.arguments);
668
+ const functionName = call.function?.name ?? call.toolName ?? "Unknown function";
669
+ const callId = typeof call?.id === "string" ? call.id : `tool-call-${index + 1}`;
670
+ const argsString = this.formatToolCallArguments(call.function?.arguments);
486
671
  return html`
487
672
  <div class="rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-700">
488
673
  <div class="flex flex-wrap items-center justify-between gap-1 font-medium text-gray-900">
@@ -508,7 +693,7 @@ export class WebInspectorElement extends LitElement {
508
693
  try {
509
694
  const parsed = JSON.parse(args);
510
695
  return JSON.stringify(parsed, null, 2);
511
- } catch (error) {
696
+ } catch {
512
697
  return args;
513
698
  }
514
699
  }
@@ -516,7 +701,7 @@ export class WebInspectorElement extends LitElement {
516
701
  if (typeof args === 'object') {
517
702
  try {
518
703
  return JSON.stringify(args, null, 2);
519
- } catch (error) {
704
+ } catch {
520
705
  return String(args);
521
706
  }
522
707
  }
@@ -622,7 +807,7 @@ export class WebInspectorElement extends LitElement {
622
807
  private extractEventFromPayload(payload: unknown): unknown {
623
808
  // If payload is an object with an 'event' field, extract it
624
809
  if (payload && typeof payload === "object" && "event" in payload) {
625
- return (payload as any).event;
810
+ return (payload as Record<string, unknown>).event;
626
811
  }
627
812
  // Otherwise, assume the payload itself is the event
628
813
  return payload;
@@ -695,6 +880,152 @@ export class WebInspectorElement extends LitElement {
695
880
  z-index: 50;
696
881
  background: transparent;
697
882
  }
883
+
884
+ .tooltip-target {
885
+ position: relative;
886
+ }
887
+
888
+ .tooltip-target::after {
889
+ content: attr(data-tooltip);
890
+ position: absolute;
891
+ top: calc(100% + 6px);
892
+ left: 50%;
893
+ transform: translateX(-50%) translateY(-4px);
894
+ white-space: nowrap;
895
+ background: rgba(17, 24, 39, 0.95);
896
+ color: white;
897
+ padding: 4px 8px;
898
+ border-radius: 6px;
899
+ font-size: 10px;
900
+ line-height: 1.2;
901
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
902
+ opacity: 0;
903
+ pointer-events: none;
904
+ transition: opacity 120ms ease, transform 120ms ease;
905
+ z-index: 4000;
906
+ }
907
+
908
+ .tooltip-target:hover::after {
909
+ opacity: 1;
910
+ transform: translateX(-50%) translateY(0);
911
+ }
912
+
913
+ .announcement-preview {
914
+ position: absolute;
915
+ top: 50%;
916
+ transform: translateY(-50%);
917
+ min-width: 300px;
918
+ max-width: 300px;
919
+ background: white;
920
+ color: #111827;
921
+ font-size: 13px;
922
+ line-height: 1.4;
923
+ border-radius: 12px;
924
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.22);
925
+ padding: 10px 12px;
926
+ display: inline-flex;
927
+ align-items: flex-start;
928
+ gap: 8px;
929
+ z-index: 4500;
930
+ animation: fade-slide-in 160ms ease;
931
+ border: 1px solid rgba(148, 163, 184, 0.35);
932
+ white-space: normal;
933
+ word-break: break-word;
934
+ text-align: left;
935
+ }
936
+
937
+ .announcement-preview[data-side="left"] {
938
+ right: 100%;
939
+ margin-right: 10px;
940
+ }
941
+
942
+ .announcement-preview[data-side="right"] {
943
+ left: 100%;
944
+ margin-left: 10px;
945
+ }
946
+
947
+ .announcement-preview__arrow {
948
+ position: absolute;
949
+ width: 10px;
950
+ height: 10px;
951
+ background: white;
952
+ border: 1px solid rgba(148, 163, 184, 0.35);
953
+ transform: rotate(45deg);
954
+ top: 50%;
955
+ margin-top: -5px;
956
+ z-index: -1;
957
+ }
958
+
959
+ .announcement-preview[data-side="left"] .announcement-preview__arrow {
960
+ right: -5px;
961
+ box-shadow: 6px -6px 10px rgba(15, 23, 42, 0.12);
962
+ }
963
+
964
+ .announcement-preview[data-side="right"] .announcement-preview__arrow {
965
+ left: -5px;
966
+ box-shadow: -6px 6px 10px rgba(15, 23, 42, 0.12);
967
+ }
968
+
969
+ .announcement-dismiss {
970
+ color: #6b7280;
971
+ font-size: 12px;
972
+ padding: 2px 8px;
973
+ border-radius: 8px;
974
+ border: 1px solid rgba(148, 163, 184, 0.5);
975
+ background: rgba(248, 250, 252, 0.9);
976
+ transition: background 120ms ease, color 120ms ease;
977
+ }
978
+
979
+ .announcement-dismiss:hover {
980
+ background: rgba(241, 245, 249, 1);
981
+ color: #111827;
982
+ }
983
+
984
+ .announcement-content {
985
+ color: #111827;
986
+ font-size: 14px;
987
+ line-height: 1.6;
988
+ }
989
+
990
+ .announcement-content h1,
991
+ .announcement-content h2,
992
+ .announcement-content h3 {
993
+ font-weight: 700;
994
+ margin: 0.4rem 0 0.2rem;
995
+ }
996
+
997
+ .announcement-content h1 {
998
+ font-size: 1.1rem;
999
+ }
1000
+
1001
+ .announcement-content h2 {
1002
+ font-size: 1rem;
1003
+ }
1004
+
1005
+ .announcement-content h3 {
1006
+ font-size: 0.95rem;
1007
+ }
1008
+
1009
+ .announcement-content p {
1010
+ margin: 0.25rem 0;
1011
+ }
1012
+
1013
+ .announcement-content ul {
1014
+ list-style: disc;
1015
+ padding-left: 1.25rem;
1016
+ margin: 0.3rem 0;
1017
+ }
1018
+
1019
+ .announcement-content ol {
1020
+ list-style: decimal;
1021
+ padding-left: 1.25rem;
1022
+ margin: 0.3rem 0;
1023
+ }
1024
+
1025
+ .announcement-content a {
1026
+ color: #0f766e;
1027
+ text-decoration: underline;
1028
+ }
698
1029
  `,
699
1030
  ];
700
1031
 
@@ -705,7 +1036,9 @@ export class WebInspectorElement extends LitElement {
705
1036
  window.addEventListener("pointerdown", this.handleGlobalPointerDown as EventListener);
706
1037
 
707
1038
  // Load state early (before first render) so menu selection is correct
708
- this.hydrateStateFromCookieEarly();
1039
+ this.hydrateStateFromStorageEarly();
1040
+ this.tryAutoAttachCore();
1041
+ this.ensureAnnouncementLoading();
709
1042
  }
710
1043
  }
711
1044
 
@@ -724,6 +1057,10 @@ export class WebInspectorElement extends LitElement {
724
1057
  return;
725
1058
  }
726
1059
 
1060
+ if (!this._core) {
1061
+ this.tryAutoAttachCore();
1062
+ }
1063
+
727
1064
  this.measureContext("button");
728
1065
  this.measureContext("window");
729
1066
 
@@ -733,7 +1070,7 @@ export class WebInspectorElement extends LitElement {
733
1070
  this.contextState.window.anchor = { horizontal: "right", vertical: "top" };
734
1071
  this.contextState.window.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
735
1072
 
736
- this.hydrateStateFromCookie();
1073
+ this.hydrateStateFromStorage();
737
1074
 
738
1075
  // Apply docking styles if open and docked (skip transition on initial load)
739
1076
  if (this.isOpen && this.dockMode !== 'floating') {
@@ -750,6 +1087,8 @@ export class WebInspectorElement extends LitElement {
750
1087
  }
751
1088
  }
752
1089
 
1090
+ this.ensureAnnouncementLoading();
1091
+
753
1092
  this.updateHostTransform(this.isOpen ? "window" : "button");
754
1093
  }
755
1094
 
@@ -761,6 +1100,7 @@ export class WebInspectorElement extends LitElement {
761
1100
  const buttonClasses = [
762
1101
  "console-button",
763
1102
  "group",
1103
+ "relative",
764
1104
  "pointer-events-auto",
765
1105
  "inline-flex",
766
1106
  "h-12",
@@ -803,7 +1143,8 @@ export class WebInspectorElement extends LitElement {
803
1143
  @pointercancel=${this.handlePointerCancel}
804
1144
  @click=${this.handleButtonClick}
805
1145
  >
806
- <img src=${logoMarkUrl} alt="" class="h-7 w-7" loading="lazy" />
1146
+ ${this.renderAnnouncementPreview()}
1147
+ <img src=${inspectorLogoIconUrl} alt="Inspector logo" class="h-5 w-auto" loading="lazy" />
807
1148
  </button>
808
1149
  `;
809
1150
  }
@@ -812,7 +1153,6 @@ export class WebInspectorElement extends LitElement {
812
1153
  const windowState = this.contextState.window;
813
1154
  const isDocked = this.dockMode !== 'floating';
814
1155
  const isTransitioning = this.hasAttribute('data-transitioning');
815
- const isCollapsed = this.dockMode === 'docked-left';
816
1156
 
817
1157
  const windowStyles = isDocked
818
1158
  ? this.getDockedWindowStyles()
@@ -823,8 +1163,17 @@ export class WebInspectorElement extends LitElement {
823
1163
  minHeight: `${MIN_WINDOW_HEIGHT}px`,
824
1164
  };
825
1165
 
826
- const contextDropdown = this.renderContextDropdown();
827
- const hasContextDropdown = contextDropdown !== nothing;
1166
+ const hasContextDropdown = this.contextOptions.length > 0;
1167
+ const contextDropdown = hasContextDropdown ? this.renderContextDropdown() : nothing;
1168
+ const coreStatus = this.getCoreStatusSummary();
1169
+ const agentSelector = hasContextDropdown
1170
+ ? contextDropdown
1171
+ : html`
1172
+ <div class="flex items-center gap-2 rounded-md border border-dashed border-gray-200 px-2 py-1 text-xs text-gray-400">
1173
+ <span>${this.renderIcon("Bot")}</span>
1174
+ <span class="truncate">No agents available</span>
1175
+ </div>
1176
+ `;
828
1177
 
829
1178
  return html`
830
1179
  <section
@@ -846,150 +1195,82 @@ export class WebInspectorElement extends LitElement {
846
1195
  ></div>
847
1196
  `
848
1197
  : nothing}
849
- <div class="flex flex-1 overflow-hidden bg-white text-gray-800">
850
- <nav
851
- class="flex ${isCollapsed ? 'w-16' : 'w-56'} shrink-0 flex-col justify-between border-r border-gray-200 bg-gray-50/50 px-3 pb-3 pt-3 text-xs transition-all duration-300"
852
- aria-label="Inspector sections"
1198
+ <div class="flex flex-1 flex-col overflow-hidden bg-white text-gray-800">
1199
+ <div
1200
+ class="drag-handle relative z-30 flex flex-col border-b border-gray-200 bg-white/95 backdrop-blur-sm ${isDocked ? '' : (this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab')}"
1201
+ data-drag-context="window"
1202
+ @pointerdown=${isDocked ? undefined : this.handlePointerDown}
1203
+ @pointermove=${isDocked ? undefined : this.handlePointerMove}
1204
+ @pointerup=${isDocked ? undefined : this.handlePointerUp}
1205
+ @pointercancel=${isDocked ? undefined : this.handlePointerCancel}
853
1206
  >
854
- <div class="flex flex-col gap-4 overflow-y-auto">
855
- <div
856
- class="flex items-center ${isCollapsed ? 'justify-center' : 'gap-2 pl-1'} touch-none select-none ${this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab'}"
857
- data-drag-context="window"
858
- @pointerdown=${this.handlePointerDown}
859
- @pointermove=${this.handlePointerMove}
860
- @pointerup=${this.handlePointerUp}
861
- @pointercancel=${this.handlePointerCancel}
862
- title="${isCollapsed ? 'Acme Inc - Enterprise' : ''}"
863
- >
864
- <span
865
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 text-white pointer-events-none"
866
- >
867
- ${this.renderIcon("Building2")}
868
- </span>
869
- ${!isCollapsed
870
- ? html`
871
- <div class="flex flex-1 flex-col leading-tight pointer-events-none">
872
- <span class="text-sm font-semibold text-gray-900">Acme Inc</span>
873
- <span class="text-[10px] text-gray-500">Enterprise</span>
874
- </div>
875
- `
876
- : nothing}
1207
+ <div class="flex flex-wrap items-center gap-3 px-4 py-3">
1208
+ <div class="flex items-center min-w-0">
1209
+ <img src=${inspectorLogoUrl} alt="Inspector logo" class="h-6 w-auto" loading="lazy" />
877
1210
  </div>
878
-
879
- <div class="flex flex-col gap-2 pt-2">
880
- ${!isCollapsed
881
- ? html`<div class="px-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">Platform</div>`
882
- : nothing}
883
- <div class="flex flex-col gap-0.5">
884
- ${this.menuItems.map(({ key, label, icon }) => {
885
- const isSelected = this.selectedMenu === key;
886
- const buttonClasses = [
887
- "group flex w-full items-center",
888
- isCollapsed ? "justify-center p-2" : "gap-2 px-2 py-1.5",
889
- "rounded-md text-left text-xs transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300",
890
- isSelected
891
- ? "bg-gray-900 text-white"
892
- : "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
893
- ].join(" ");
894
-
895
- const badgeClasses = isSelected
896
- ? "bg-gray-800 text-white"
897
- : "bg-white border border-gray-200 text-gray-500 group-hover:border-gray-300 group-hover:text-gray-700";
898
-
899
- return html`
900
- <button
901
- type="button"
902
- class=${buttonClasses}
903
- aria-pressed=${isSelected}
904
- title="${isCollapsed ? label : ''}"
905
- @click=${() => this.handleMenuSelect(key)}
906
- >
907
- <span
908
- class="flex h-6 w-6 items-center justify-center rounded ${isCollapsed && isSelected ? 'text-white' : isCollapsed ? 'text-gray-600' : badgeClasses}"
909
- aria-hidden="true"
910
- >
911
- ${this.renderIcon(icon)}
912
- </span>
913
- ${!isCollapsed
914
- ? html`
915
- <span class="flex-1">${label}</span>
916
- <span class="text-gray-400 opacity-60">${this.renderIcon("ChevronRight")}</span>
917
- `
918
- : nothing}
919
- </button>
920
- `;
921
- })}
1211
+ <div class="ml-auto flex min-w-0 items-center gap-2">
1212
+ <div class="min-w-[160px] max-w-xs">
1213
+ ${agentSelector}
1214
+ </div>
1215
+ <div class="flex items-center gap-1">
1216
+ ${this.renderDockControls()}
1217
+ <button
1218
+ 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"
1219
+ type="button"
1220
+ aria-label="Close Web Inspector"
1221
+ @pointerdown=${this.handleClosePointerDown}
1222
+ @click=${this.handleCloseClick}
1223
+ >
1224
+ ${this.renderIcon("X")}
1225
+ </button>
922
1226
  </div>
923
1227
  </div>
924
1228
  </div>
925
-
926
- <div
927
- class="relative flex items-center ${isCollapsed ? 'justify-center p-1' : ''} rounded-lg border border-gray-200 bg-white ${isCollapsed ? '' : 'px-2 py-2'} text-left text-xs text-gray-700 cursor-pointer hover:bg-gray-50 transition"
928
- title="${isCollapsed ? 'John Snow - john@snow.com' : ''}"
929
- >
930
- <span
931
- class="${isCollapsed ? 'w-8 h-8 shrink-0' : 'w-6 h-6'} flex items-center justify-center overflow-hidden rounded bg-gray-100 text-[10px] font-semibold text-gray-700"
932
- >
933
- JS
934
- </span>
935
- ${!isCollapsed
936
- ? html`
937
- <div class="pl-2 flex flex-1 flex-col leading-tight">
938
- <span class="font-medium text-gray-900">John Snow</span>
939
- <span class="text-[10px] text-gray-500">john@snow.com</span>
940
- </div>
941
- <span class="text-gray-300">${this.renderIcon("ChevronRight")}</span>
942
- `
943
- : nothing}
1229
+ <div class="flex flex-wrap items-center gap-2 border-t border-gray-100 px-3 py-2 text-xs">
1230
+ ${this.menuItems.map(({ key, label, icon }) => {
1231
+ const isSelected = this.selectedMenu === key;
1232
+ const tabClasses = [
1233
+ "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",
1234
+ isSelected ? "bg-gray-900 text-white shadow-sm" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
1235
+ ].join(" ");
1236
+
1237
+ return html`
1238
+ <button
1239
+ type="button"
1240
+ class=${tabClasses}
1241
+ aria-pressed=${isSelected}
1242
+ @click=${() => this.handleMenuSelect(key)}
1243
+ >
1244
+ <span class="text-gray-400 ${isSelected ? 'text-white' : ''}">
1245
+ ${this.renderIcon(icon)}
1246
+ </span>
1247
+ <span>${label}</span>
1248
+ </button>
1249
+ `;
1250
+ })}
944
1251
  </div>
945
- </nav>
946
- <div class="relative flex flex-1 flex-col overflow-hidden">
947
- <div
948
- class="drag-handle flex items-center justify-between border-b border-gray-200 px-4 py-3 touch-none select-none ${isDocked ? '' : (this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab')}"
949
- data-drag-context="window"
950
- @pointerdown=${isDocked ? undefined : this.handlePointerDown}
951
- @pointermove=${isDocked ? undefined : this.handlePointerMove}
952
- @pointerup=${isDocked ? undefined : this.handlePointerUp}
953
- @pointercancel=${isDocked ? undefined : this.handlePointerCancel}
954
- >
955
- <div class="flex min-w-0 flex-1 items-center gap-2 text-xs text-gray-500">
956
- <div class="flex min-w-0 flex-1 items-center text-xs text-gray-600">
957
- <span class="flex shrink-0 items-center gap-1">
958
- <span>🪁</span>
959
- <span class="font-medium whitespace-nowrap">CopilotKit Inspector</span>
960
- </span>
961
- <span class="mx-3 h-3 w-px shrink-0 bg-gray-200"></span>
962
- <span class="shrink-0 text-gray-400">
963
- ${this.renderIcon(this.getSelectedMenu().icon)}
964
- </span>
965
- <span class="ml-2 truncate">${this.getSelectedMenu().label}</span>
966
- ${hasContextDropdown
967
- ? html`
968
- <span class="mx-3 h-3 w-px shrink-0 bg-gray-200"></span>
969
- <div class="min-w-0">${contextDropdown}</div>
970
- `
971
- : nothing}
972
- </div>
1252
+ </div>
1253
+ <div class="flex flex-1 flex-col overflow-hidden">
1254
+ <div class="flex-1 overflow-auto">
1255
+ ${this.renderAnnouncementPanel()}
1256
+ ${this.renderCoreWarningBanner()}
1257
+ ${this.renderMainContent()}
1258
+ <slot></slot>
973
1259
  </div>
974
- <div class="flex items-center gap-1">
975
- ${this.renderDockControls()}
976
- <button
977
- class="flex h-6 w-6 items-center justify-center rounded 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"
978
- type="button"
979
- aria-label="Close Web Inspector"
980
- @pointerdown=${this.handleClosePointerDown}
981
- @click=${this.handleCloseClick}
1260
+ <div class="border-t border-gray-200 bg-gray-50 px-4 py-2">
1261
+ <div
1262
+ class="flex items-center gap-2 rounded-md px-3 py-2 text-xs ${coreStatus.tone} w-full overflow-hidden my-1"
1263
+ title=${coreStatus.description}
982
1264
  >
983
- ${this.renderIcon("X")}
984
- </button>
1265
+ <span class="flex h-6 w-6 items-center justify-center rounded bg-white/60">
1266
+ ${this.renderIcon("Activity")}
1267
+ </span>
1268
+ <span class="font-medium">${coreStatus.label}</span>
1269
+ <span class="truncate text-[11px] opacity-80">${coreStatus.description}</span>
1270
+ </div>
985
1271
  </div>
986
1272
  </div>
987
- <div class="flex-1 overflow-auto">
988
- ${this.renderMainContent()}
989
- <slot></slot>
990
- </div>
991
1273
  </div>
992
- </div>
993
1274
  <div
994
1275
  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"
995
1276
  role="presentation"
@@ -1015,12 +1296,12 @@ export class WebInspectorElement extends LitElement {
1015
1296
  `;
1016
1297
  }
1017
1298
 
1018
- private hydrateStateFromCookieEarly(): void {
1299
+ private hydrateStateFromStorageEarly(): void {
1019
1300
  if (typeof document === "undefined" || typeof window === "undefined") {
1020
1301
  return;
1021
1302
  }
1022
1303
 
1023
- const persisted = loadInspectorState(COOKIE_NAME);
1304
+ const persisted = loadInspectorState(INSPECTOR_STORAGE_KEY);
1024
1305
  if (!persisted) {
1025
1306
  return;
1026
1307
  }
@@ -1050,12 +1331,12 @@ export class WebInspectorElement extends LitElement {
1050
1331
  }
1051
1332
  }
1052
1333
 
1053
- private hydrateStateFromCookie(): void {
1334
+ private hydrateStateFromStorage(): void {
1054
1335
  if (typeof document === "undefined" || typeof window === "undefined") {
1055
1336
  return;
1056
1337
  }
1057
1338
 
1058
- const persisted = loadInspectorState(COOKIE_NAME);
1339
+ const persisted = loadInspectorState(INSPECTOR_STORAGE_KEY);
1059
1340
  if (!persisted) {
1060
1341
  return;
1061
1342
  }
@@ -1115,6 +1396,11 @@ export class WebInspectorElement extends LitElement {
1115
1396
  const contextAttr = target?.dataset.dragContext;
1116
1397
  const context: ContextKey = contextAttr === "window" ? "window" : "button";
1117
1398
 
1399
+ const eventTarget = event.target as HTMLElement | null;
1400
+ if (context === "window" && eventTarget?.closest("button")) {
1401
+ return;
1402
+ }
1403
+
1118
1404
  this.pointerContext = context;
1119
1405
  this.measureContext(context);
1120
1406
 
@@ -1433,7 +1719,7 @@ export class WebInspectorElement extends LitElement {
1433
1719
  selectedMenu: this.selectedMenu,
1434
1720
  selectedContext: this.selectedContext,
1435
1721
  };
1436
- saveInspectorState(COOKIE_NAME, state, COOKIE_MAX_AGE_SECONDS);
1722
+ saveInspectorState(INSPECTOR_STORAGE_KEY, state);
1437
1723
  this.pendingSelectedContext = state.selectedContext ?? null;
1438
1724
  }
1439
1725
 
@@ -1463,7 +1749,6 @@ export class WebInspectorElement extends LitElement {
1463
1749
  // Clean up previous dock state
1464
1750
  this.removeDockStyles();
1465
1751
 
1466
- const previousMode = this.dockMode;
1467
1752
  this.dockMode = mode;
1468
1753
 
1469
1754
  if (mode !== 'floating') {
@@ -1644,6 +1929,10 @@ export class WebInspectorElement extends LitElement {
1644
1929
  return;
1645
1930
  }
1646
1931
 
1932
+ this.showAnnouncementPreview = false; // hide the bubble once the inspector is opened
1933
+
1934
+ this.ensureAnnouncementLoading();
1935
+
1647
1936
  this.isOpen = true;
1648
1937
  this.persistState(); // Save the open state
1649
1938
 
@@ -1666,6 +1955,7 @@ export class WebInspectorElement extends LitElement {
1666
1955
  // Update transform for docked position
1667
1956
  this.updateHostTransform("window");
1668
1957
  }
1958
+
1669
1959
  });
1670
1960
  }
1671
1961
 
@@ -1719,7 +2009,7 @@ export class WebInspectorElement extends LitElement {
1719
2009
  // Show dock left button
1720
2010
  return html`
1721
2011
  <button
1722
- class="flex h-6 w-6 items-center justify-center rounded 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"
2012
+ 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"
1723
2013
  type="button"
1724
2014
  aria-label="Dock to left"
1725
2015
  title="Dock Left"
@@ -1732,7 +2022,7 @@ export class WebInspectorElement extends LitElement {
1732
2022
  // Show float button
1733
2023
  return html`
1734
2024
  <button
1735
- class="flex h-6 w-6 items-center justify-center rounded 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"
2025
+ 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"
1736
2026
  type="button"
1737
2027
  aria-label="Float window"
1738
2028
  title="Float"
@@ -1777,6 +2067,179 @@ export class WebInspectorElement extends LitElement {
1777
2067
  .join(" ");
1778
2068
  }
1779
2069
 
2070
+ private sanitizeForLogging(value: unknown, depth = 0, seen = new WeakSet<object>()): SanitizedValue {
2071
+ if (value === undefined) {
2072
+ return "[undefined]";
2073
+ }
2074
+
2075
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
2076
+ return value;
2077
+ }
2078
+
2079
+ if (typeof value === "string") {
2080
+ return value;
2081
+ }
2082
+
2083
+ if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {
2084
+ return String(value);
2085
+ }
2086
+
2087
+ if (value instanceof Date) {
2088
+ return value.toISOString();
2089
+ }
2090
+
2091
+ if (Array.isArray(value)) {
2092
+ if (depth >= 4) {
2093
+ return "[Truncated depth]" as SanitizedValue;
2094
+ }
2095
+ return value.map((item) => this.sanitizeForLogging(item, depth + 1, seen));
2096
+ }
2097
+
2098
+ if (typeof value === "object") {
2099
+ if (seen.has(value as object)) {
2100
+ return "[Circular]" as SanitizedValue;
2101
+ }
2102
+ seen.add(value as object);
2103
+
2104
+ if (depth >= 4) {
2105
+ return "[Truncated depth]" as SanitizedValue;
2106
+ }
2107
+
2108
+ const result: Record<string, SanitizedValue> = {};
2109
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
2110
+ result[key] = this.sanitizeForLogging(entry, depth + 1, seen);
2111
+ }
2112
+ return result;
2113
+ }
2114
+
2115
+ return String(value);
2116
+ }
2117
+
2118
+ private normalizeEventPayload(_type: InspectorAgentEventType, payload: unknown): SanitizedValue {
2119
+ if (payload && typeof payload === "object" && "event" in payload) {
2120
+ const { event, ...rest } = payload as Record<string, unknown>;
2121
+ const cleaned = Object.keys(rest).length === 0 ? event : { event, ...rest };
2122
+ return this.sanitizeForLogging(cleaned);
2123
+ }
2124
+
2125
+ return this.sanitizeForLogging(payload);
2126
+ }
2127
+
2128
+ private normalizeMessageContent(content: unknown): string {
2129
+ if (typeof content === "string") {
2130
+ return content;
2131
+ }
2132
+
2133
+ if (content && typeof content === "object" && "text" in (content as Record<string, unknown>)) {
2134
+ const maybeText = (content as Record<string, unknown>).text;
2135
+ if (typeof maybeText === "string") {
2136
+ return maybeText;
2137
+ }
2138
+ }
2139
+
2140
+ if (content === null || content === undefined) {
2141
+ return "";
2142
+ }
2143
+
2144
+ if (typeof content === "object") {
2145
+ try {
2146
+ return JSON.stringify(this.sanitizeForLogging(content));
2147
+ } catch {
2148
+ return "";
2149
+ }
2150
+ }
2151
+
2152
+ return String(content);
2153
+ }
2154
+
2155
+ private normalizeToolCalls(raw: unknown): InspectorToolCall[] {
2156
+ if (!Array.isArray(raw)) {
2157
+ return [];
2158
+ }
2159
+
2160
+ return raw
2161
+ .map((entry) => {
2162
+ if (!entry || typeof entry !== "object") {
2163
+ return null;
2164
+ }
2165
+ const call = entry as Record<string, unknown>;
2166
+ const fn = call.function as Record<string, unknown> | undefined;
2167
+ const functionName = typeof fn?.name === "string" ? fn.name : typeof call.toolName === "string" ? call.toolName : undefined;
2168
+ const args = fn && "arguments" in fn ? (fn as Record<string, unknown>).arguments : call.arguments;
2169
+
2170
+ const normalized: InspectorToolCall = {
2171
+ id: typeof call.id === "string" ? call.id : undefined,
2172
+ toolName: typeof call.toolName === "string" ? call.toolName : functionName,
2173
+ status: typeof call.status === "string" ? call.status : undefined,
2174
+ };
2175
+
2176
+ if (functionName) {
2177
+ normalized.function = {
2178
+ name: functionName,
2179
+ arguments: this.sanitizeForLogging(args),
2180
+ };
2181
+ }
2182
+
2183
+ return normalized;
2184
+ })
2185
+ .filter((call): call is InspectorToolCall => Boolean(call));
2186
+ }
2187
+
2188
+ private normalizeAgentMessage(message: unknown): InspectorMessage | null {
2189
+ if (!message || typeof message !== "object") {
2190
+ return null;
2191
+ }
2192
+
2193
+ const raw = message as Record<string, unknown>;
2194
+ const role = typeof raw.role === "string" ? raw.role : "unknown";
2195
+ const contentText = this.normalizeMessageContent(raw.content);
2196
+ const toolCalls = this.normalizeToolCalls(raw.toolCalls);
2197
+
2198
+ return {
2199
+ id: typeof raw.id === "string" ? raw.id : undefined,
2200
+ role,
2201
+ contentText,
2202
+ contentRaw: raw.content !== undefined ? this.sanitizeForLogging(raw.content) : undefined,
2203
+ toolCalls,
2204
+ };
2205
+ }
2206
+
2207
+ private normalizeAgentMessages(messages: unknown): InspectorMessage[] | null {
2208
+ if (!Array.isArray(messages)) {
2209
+ return null;
2210
+ }
2211
+
2212
+ const normalized = messages
2213
+ .map((message) => this.normalizeAgentMessage(message))
2214
+ .filter((msg): msg is InspectorMessage => msg !== null);
2215
+
2216
+ return normalized;
2217
+ }
2218
+
2219
+ private normalizeContextStore(
2220
+ context: Readonly<Record<string, unknown>> | null | undefined,
2221
+ ): Record<string, { description?: string; value: unknown }> {
2222
+ if (!context || typeof context !== "object") {
2223
+ return {};
2224
+ }
2225
+
2226
+ const normalized: Record<string, { description?: string; value: unknown }> = {};
2227
+ for (const [key, entry] of Object.entries(context)) {
2228
+ if (entry && typeof entry === "object" && "value" in (entry as Record<string, unknown>)) {
2229
+ const candidate = entry as Record<string, unknown>;
2230
+ const description =
2231
+ typeof candidate.description === "string" && candidate.description.trim().length > 0
2232
+ ? candidate.description
2233
+ : undefined;
2234
+ normalized[key] = { description, value: candidate.value };
2235
+ } else {
2236
+ normalized[key] = { value: entry };
2237
+ }
2238
+ }
2239
+
2240
+ return normalized;
2241
+ }
2242
+
1780
2243
  private contextOptions: Array<{ key: string; label: string }> = [
1781
2244
  { key: "all-agents", label: "All Agents" },
1782
2245
  ];
@@ -1786,12 +2249,75 @@ export class WebInspectorElement extends LitElement {
1786
2249
  private copiedEvents: Set<string> = new Set();
1787
2250
  private expandedTools: Set<string> = new Set();
1788
2251
  private expandedContextItems: Set<string> = new Set();
2252
+ private copiedContextItems: Set<string> = new Set();
1789
2253
 
1790
2254
  private getSelectedMenu(): MenuItem {
1791
2255
  const found = this.menuItems.find((item) => item.key === this.selectedMenu);
1792
2256
  return found ?? this.menuItems[0]!;
1793
2257
  }
1794
2258
 
2259
+ private renderCoreWarningBanner() {
2260
+ if (this._core) {
2261
+ return nothing;
2262
+ }
2263
+
2264
+ return html`
2265
+ <div 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">
2266
+ <span class="mt-0.5 shrink-0 text-amber-600">${this.renderIcon("AlertTriangle")}</span>
2267
+ <div class="space-y-1">
2268
+ <div class="font-semibold text-amber-900">CopilotKit core not attached</div>
2269
+ <p class="text-[11px] leading-snug text-amber-800">
2270
+ Pass a live <code>CopilotKitCore</code> instance to <code>&lt;cpk-web-inspector&gt;</code> or expose it on
2271
+ <code>window.__COPILOTKIT_CORE__</code> for auto-attach.
2272
+ </p>
2273
+ </div>
2274
+ </div>
2275
+ `;
2276
+ }
2277
+
2278
+ private getCoreStatusSummary(): { label: string; tone: string; description: string } {
2279
+ if (!this._core) {
2280
+ return {
2281
+ label: "Core not attached",
2282
+ tone: "border border-amber-200 bg-amber-50 text-amber-800",
2283
+ description: "Pass a CopilotKitCore instance to <cpk-web-inspector> or enable auto-attach.",
2284
+ };
2285
+ }
2286
+
2287
+ const status = this.runtimeStatus ?? CopilotKitCoreRuntimeConnectionStatus.Disconnected;
2288
+ const lastErrorMessage = this.lastCoreError?.message;
2289
+
2290
+ if (status === CopilotKitCoreRuntimeConnectionStatus.Error) {
2291
+ return {
2292
+ label: "Runtime error",
2293
+ tone: "border border-rose-200 bg-rose-50 text-rose-700",
2294
+ description: lastErrorMessage ?? "CopilotKit runtime reported an error.",
2295
+ };
2296
+ }
2297
+
2298
+ if (status === CopilotKitCoreRuntimeConnectionStatus.Connecting) {
2299
+ return {
2300
+ label: "Connecting",
2301
+ tone: "border border-amber-200 bg-amber-50 text-amber-800",
2302
+ description: "Waiting for CopilotKit runtime to finish connecting.",
2303
+ };
2304
+ }
2305
+
2306
+ if (status === CopilotKitCoreRuntimeConnectionStatus.Connected) {
2307
+ return {
2308
+ label: "Connected",
2309
+ tone: "border border-emerald-200 bg-emerald-50 text-emerald-700",
2310
+ description: "Live runtime connection established.",
2311
+ };
2312
+ }
2313
+
2314
+ return {
2315
+ label: "Disconnected",
2316
+ tone: "border border-gray-200 bg-gray-50 text-gray-700",
2317
+ description: lastErrorMessage ?? "Waiting for CopilotKit runtime to connect.",
2318
+ };
2319
+ }
2320
+
1795
2321
  private renderMainContent() {
1796
2322
  if (this.selectedMenu === "ag-ui-events") {
1797
2323
  return this.renderEventsTable();
@@ -1809,17 +2335,13 @@ export class WebInspectorElement extends LitElement {
1809
2335
  return this.renderContextView();
1810
2336
  }
1811
2337
 
1812
- // Default placeholder content for other sections
1813
- return html`
1814
- <div class="flex flex-col gap-3 p-4">
1815
- <div class="h-24 rounded-lg bg-gray-50"></div>
1816
- <div class="h-20 rounded-lg bg-gray-50"></div>
1817
- </div>
1818
- `;
2338
+ return nothing;
1819
2339
  }
1820
2340
 
1821
2341
  private renderEventsTable() {
1822
2342
  const events = this.getEventsForSelectedContext();
2343
+ const filteredEvents = this.filterEvents(events);
2344
+ const selectedLabel = this.selectedContext === "all-agents" ? "all agents" : `agent ${this.selectedContext}`;
1823
2345
 
1824
2346
  if (events.length === 0) {
1825
2347
  return html`
@@ -1835,83 +2357,221 @@ export class WebInspectorElement extends LitElement {
1835
2357
  `;
1836
2358
  }
1837
2359
 
2360
+ if (filteredEvents.length === 0) {
2361
+ return html`
2362
+ <div class="flex h-full items-center justify-center px-4 py-8 text-center">
2363
+ <div class="max-w-md space-y-3">
2364
+ <div class="flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
2365
+ ${this.renderIcon("Filter")}
2366
+ </div>
2367
+ <p class="text-sm text-gray-600">No events match the current filters.</p>
2368
+ <div>
2369
+ <button
2370
+ type="button"
2371
+ 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"
2372
+ @click=${this.resetEventFilters}
2373
+ >
2374
+ ${this.renderIcon("RefreshCw")}
2375
+ <span>Reset filters</span>
2376
+ </button>
2377
+ </div>
2378
+ </div>
2379
+ </div>
2380
+ `;
2381
+ }
2382
+
1838
2383
  return html`
1839
- <div class="relative h-full overflow-auto">
1840
- <table class="w-full border-separate border-spacing-0 text-xs">
1841
- <thead class="sticky top-0 z-10">
1842
- <tr class="bg-white">
1843
- <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1844
- Agent
1845
- </th>
1846
- <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1847
- Time
1848
- </th>
1849
- <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1850
- Event Type
1851
- </th>
1852
- <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1853
- AG-UI Event
1854
- </th>
1855
- </tr>
1856
- </thead>
1857
- <tbody>
1858
- ${events.map((event, index) => {
1859
- const rowBg = index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
1860
- const badgeClasses = this.getEventBadgeClasses(event.type);
1861
- const extractedEvent = this.extractEventFromPayload(event.payload);
1862
- const inlineEvent = this.stringifyPayload(extractedEvent, false) || "—";
1863
- const prettyEvent = this.stringifyPayload(extractedEvent, true) || inlineEvent;
1864
- const isExpanded = this.expandedRows.has(event.id);
1865
-
1866
- return html`
1867
- <tr
1868
- class="${rowBg} cursor-pointer transition hover:bg-blue-50/50"
1869
- @click=${() => this.toggleRowExpansion(event.id)}
1870
- >
1871
- <td class="border-l border-r border-b border-gray-200 px-3 py-2">
1872
- <span class="font-mono text-[11px] text-gray-600">${event.agentId}</span>
1873
- </td>
1874
- <td class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[11px] text-gray-600">
1875
- <span title=${new Date(event.timestamp).toLocaleString()}>
1876
- ${new Date(event.timestamp).toLocaleTimeString()}
1877
- </span>
1878
- </td>
1879
- <td class="border-r border-b border-gray-200 px-3 py-2">
1880
- <span class=${badgeClasses}>${event.type}</span>
1881
- </td>
1882
- <td class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[10px] text-gray-600 ${isExpanded ? '' : 'truncate max-w-xs'}">
1883
- ${isExpanded
1884
- ? html`
1885
- <div class="group relative">
1886
- <pre class="m-0 whitespace-pre-wrap text-[10px] font-mono text-gray-600">${prettyEvent}</pre>
1887
- <button
1888
- class="absolute right-0 top-0 cursor-pointer rounded px-2 py-1 text-[10px] opacity-0 transition group-hover:opacity-100 ${
1889
- this.copiedEvents.has(event.id)
1890
- ? 'bg-green-100 text-green-700'
1891
- : 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900'
1892
- }"
1893
- @click=${(e: Event) => {
1894
- e.stopPropagation();
1895
- this.copyToClipboard(prettyEvent, event.id);
1896
- }}
1897
- >
1898
- ${this.copiedEvents.has(event.id)
1899
- ? html`<span>✓ Copied</span>`
1900
- : html`<span>Copy</span>`}
1901
- </button>
1902
- </div>
1903
- `
1904
- : inlineEvent}
1905
- </td>
1906
- </tr>
1907
- `;
1908
- })}
1909
- </tbody>
1910
- </table>
2384
+ <div class="flex h-full flex-col">
2385
+ <div class="flex flex-col gap-1.5 border-b border-gray-200 bg-white px-4 py-2.5">
2386
+ <div class="flex flex-wrap items-center gap-2">
2387
+ <div class="relative min-w-[200px] flex-1">
2388
+ <input
2389
+ type="search"
2390
+ 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"
2391
+ placeholder="Search agent, type, payload"
2392
+ .value=${this.eventFilterText}
2393
+ @input=${this.handleEventFilterInput}
2394
+ />
2395
+ </div>
2396
+ <select
2397
+ 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"
2398
+ .value=${this.eventTypeFilter}
2399
+ @change=${this.handleEventTypeChange}
2400
+ >
2401
+ <option value="all">All event types</option>
2402
+ ${AGENT_EVENT_TYPES.map(
2403
+ (type) =>
2404
+ html`<option value=${type}>${type.toLowerCase().replace(/_/g, " ")}</option>`,
2405
+ )}
2406
+ </select>
2407
+ <div class="flex items-center gap-1 text-[11px]">
2408
+ <button
2409
+ type="button"
2410
+ 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"
2411
+ title="Reset filters"
2412
+ data-tooltip="Reset filters"
2413
+ aria-label="Reset filters"
2414
+ @click=${this.resetEventFilters}
2415
+ ?disabled=${!this.eventFilterText && this.eventTypeFilter === "all"}
2416
+ >
2417
+ ${this.renderIcon("RotateCw")}
2418
+ </button>
2419
+ <button
2420
+ type="button"
2421
+ 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"
2422
+ title="Export JSON"
2423
+ data-tooltip="Export JSON"
2424
+ aria-label="Export JSON"
2425
+ @click=${() => this.exportEvents(filteredEvents)}
2426
+ ?disabled=${filteredEvents.length === 0}
2427
+ >
2428
+ ${this.renderIcon("Download")}
2429
+ </button>
2430
+ <button
2431
+ type="button"
2432
+ 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"
2433
+ title="Clear events"
2434
+ data-tooltip="Clear events"
2435
+ aria-label="Clear events"
2436
+ @click=${this.handleClearEvents}
2437
+ ?disabled=${events.length === 0}
2438
+ >
2439
+ ${this.renderIcon("Trash2")}
2440
+ </button>
2441
+ </div>
2442
+ </div>
2443
+ <div class="text-[11px] text-gray-500">
2444
+ Showing ${filteredEvents.length} of ${events.length}${this.selectedContext === "all-agents" ? "" : ` for ${selectedLabel}`}
2445
+ </div>
2446
+ </div>
2447
+ <div class="relative h-full w-full overflow-y-auto overflow-x-hidden">
2448
+ <table class="w-full table-fixed border-collapse text-xs box-border">
2449
+ <thead class="sticky top-0 z-10">
2450
+ <tr class="bg-white">
2451
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
2452
+ Agent
2453
+ </th>
2454
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
2455
+ Time
2456
+ </th>
2457
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
2458
+ Event Type
2459
+ </th>
2460
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
2461
+ AG-UI Event
2462
+ </th>
2463
+ </tr>
2464
+ </thead>
2465
+ <tbody>
2466
+ ${filteredEvents.map((event, index) => {
2467
+ const rowBg = index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
2468
+ const badgeClasses = this.getEventBadgeClasses(event.type);
2469
+ const extractedEvent = this.extractEventFromPayload(event.payload);
2470
+ const inlineEvent = this.stringifyPayload(extractedEvent, false) || "—";
2471
+ const prettyEvent = this.stringifyPayload(extractedEvent, true) || inlineEvent;
2472
+ const isExpanded = this.expandedRows.has(event.id);
2473
+
2474
+ return html`
2475
+ <tr
2476
+ class="${rowBg} cursor-pointer transition hover:bg-blue-50/50"
2477
+ @click=${() => this.toggleRowExpansion(event.id)}
2478
+ >
2479
+ <td class="border-l border-r border-b border-gray-200 px-3 py-2">
2480
+ <span class="font-mono text-[11px] text-gray-600">${event.agentId}</span>
2481
+ </td>
2482
+ <td class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[11px] text-gray-600">
2483
+ <span title=${new Date(event.timestamp).toLocaleString()}>
2484
+ ${new Date(event.timestamp).toLocaleTimeString()}
2485
+ </span>
2486
+ </td>
2487
+ <td class="border-r border-b border-gray-200 px-3 py-2">
2488
+ <span class=${badgeClasses}>${event.type}</span>
2489
+ </td>
2490
+ <td class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[10px] text-gray-600 ${isExpanded ? '' : 'truncate max-w-xs'}">
2491
+ ${isExpanded
2492
+ ? html`
2493
+ <div class="group relative">
2494
+ <pre class="m-0 whitespace-pre-wrap break-words text-[10px] font-mono text-gray-600">${prettyEvent}</pre>
2495
+ <button
2496
+ class="absolute right-0 top-0 cursor-pointer rounded px-2 py-1 text-[10px] opacity-0 transition group-hover:opacity-100 ${
2497
+ this.copiedEvents.has(event.id)
2498
+ ? 'bg-green-100 text-green-700'
2499
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900'
2500
+ }"
2501
+ @click=${(e: Event) => {
2502
+ e.stopPropagation();
2503
+ this.copyToClipboard(prettyEvent, event.id);
2504
+ }}
2505
+ >
2506
+ ${this.copiedEvents.has(event.id)
2507
+ ? html`<span>✓ Copied</span>`
2508
+ : html`<span>Copy</span>`}
2509
+ </button>
2510
+ </div>
2511
+ `
2512
+ : inlineEvent}
2513
+ </td>
2514
+ </tr>
2515
+ `;
2516
+ })}
2517
+ </tbody>
2518
+ </table>
2519
+ </div>
1911
2520
  </div>
1912
2521
  `;
1913
2522
  }
1914
2523
 
2524
+ private handleEventFilterInput(event: Event): void {
2525
+ const target = event.target as HTMLInputElement | null;
2526
+ this.eventFilterText = target?.value ?? "";
2527
+ this.requestUpdate();
2528
+ }
2529
+
2530
+ private handleEventTypeChange(event: Event): void {
2531
+ const target = event.target as HTMLSelectElement | null;
2532
+ const value = target?.value as InspectorAgentEventType | "all" | undefined;
2533
+ if (!value) {
2534
+ return;
2535
+ }
2536
+ this.eventTypeFilter = value;
2537
+ this.requestUpdate();
2538
+ }
2539
+
2540
+ private resetEventFilters(): void {
2541
+ this.eventFilterText = "";
2542
+ this.eventTypeFilter = "all";
2543
+ this.requestUpdate();
2544
+ }
2545
+
2546
+ private handleClearEvents = (): void => {
2547
+ if (this.selectedContext === "all-agents") {
2548
+ this.agentEvents.clear();
2549
+ this.flattenedEvents = [];
2550
+ } else {
2551
+ this.agentEvents.delete(this.selectedContext);
2552
+ this.flattenedEvents = this.flattenedEvents.filter((event) => event.agentId !== this.selectedContext);
2553
+ }
2554
+
2555
+ this.expandedRows.clear();
2556
+ this.copiedEvents.clear();
2557
+ this.requestUpdate();
2558
+ };
2559
+
2560
+ private exportEvents(events: InspectorEvent[]): void {
2561
+ try {
2562
+ const payload = JSON.stringify(events, null, 2);
2563
+ const blob = new Blob([payload], { type: "application/json" });
2564
+ const url = URL.createObjectURL(blob);
2565
+ const anchor = document.createElement("a");
2566
+ anchor.href = url;
2567
+ anchor.download = `copilotkit-events-${Date.now()}.json`;
2568
+ anchor.click();
2569
+ URL.revokeObjectURL(url);
2570
+ } catch (error) {
2571
+ console.error("Failed to export events", error);
2572
+ }
2573
+ }
2574
+
1915
2575
  private renderAgentsView() {
1916
2576
  // Show message if "all-agents" is selected or no agents available
1917
2577
  if (this.selectedContext === "all-agents") {
@@ -2013,7 +2673,7 @@ export class WebInspectorElement extends LitElement {
2013
2673
  <h4 class="text-sm font-semibold text-gray-900">Current Messages</h4>
2014
2674
  </div>
2015
2675
  <div class="overflow-auto">
2016
- ${messages && Array.isArray(messages) && messages.length > 0
2676
+ ${messages && messages.length > 0
2017
2677
  ? html`
2018
2678
  <table class="w-full text-xs">
2019
2679
  <thead class="bg-gray-50">
@@ -2023,26 +2683,20 @@ export class WebInspectorElement extends LitElement {
2023
2683
  </tr>
2024
2684
  </thead>
2025
2685
  <tbody class="divide-y divide-gray-200">
2026
- ${messages.map((msg: any, idx: number) => {
2027
- const role = msg?.role ?? "unknown";
2686
+ ${messages.map((msg) => {
2687
+ const role = msg.role || "unknown";
2028
2688
  const roleColors: Record<string, string> = {
2029
2689
  user: "bg-blue-100 text-blue-800",
2030
2690
  assistant: "bg-green-100 text-green-800",
2031
2691
  system: "bg-gray-100 text-gray-800",
2692
+ tool: "bg-amber-100 text-amber-800",
2032
2693
  unknown: "bg-gray-100 text-gray-600",
2033
2694
  };
2034
2695
 
2035
- const rawContent = typeof msg?.content === "string"
2036
- ? msg.content
2037
- : msg?.content != null
2038
- ? JSON.stringify(msg.content)
2039
- : "";
2040
-
2041
- const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
2696
+ const rawContent = msg.contentText ?? "";
2697
+ const toolCalls = msg.toolCalls ?? [];
2042
2698
  const hasContent = rawContent.trim().length > 0;
2043
- const contentFallback = toolCalls.length > 0
2044
- ? "Invoked tool call"
2045
- : "—";
2699
+ const contentFallback = toolCalls.length > 0 ? "Invoked tool call" : "—";
2046
2700
 
2047
2701
  return html`
2048
2702
  <tr>
@@ -2080,15 +2734,6 @@ export class WebInspectorElement extends LitElement {
2080
2734
  }
2081
2735
 
2082
2736
  private renderContextDropdown() {
2083
- // Agent Context doesn't use the dropdown - it's global
2084
- if (this.selectedMenu === "agent-context") {
2085
- return nothing;
2086
- }
2087
-
2088
- if (this.selectedMenu !== "ag-ui-events" && this.selectedMenu !== "agents" && this.selectedMenu !== "frontend-tools") {
2089
- return nothing;
2090
- }
2091
-
2092
2737
  // Filter out "all-agents" when in agents view
2093
2738
  const filteredOptions = this.selectedMenu === "agents"
2094
2739
  ? this.contextOptions.filter((opt) => opt.key !== "all-agents")
@@ -2097,10 +2742,10 @@ export class WebInspectorElement extends LitElement {
2097
2742
  const selectedLabel = filteredOptions.find((opt) => opt.key === this.selectedContext)?.label ?? "";
2098
2743
 
2099
2744
  return html`
2100
- <div class="relative min-w-0 flex-1" data-context-dropdown-root="true">
2745
+ <div class="relative z-40 min-w-0 flex-1" data-context-dropdown-root="true">
2101
2746
  <button
2102
2747
  type="button"
2103
- class="flex w-full min-w-0 max-w-[150px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
2748
+ 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"
2104
2749
  @pointerdown=${this.handleContextDropdownToggle}
2105
2750
  >
2106
2751
  <span class="truncate flex-1 text-left">${selectedLabel}</span>
@@ -2187,7 +2832,8 @@ export class WebInspectorElement extends LitElement {
2187
2832
  `;
2188
2833
  }
2189
2834
 
2190
- const allTools = this.extractToolsFromAgents();
2835
+ this.refreshToolsSnapshot();
2836
+ const allTools = this.cachedTools;
2191
2837
 
2192
2838
  if (allTools.length === 0) {
2193
2839
  return html`
@@ -2206,7 +2852,7 @@ export class WebInspectorElement extends LitElement {
2206
2852
  // Filter tools by selected agent
2207
2853
  const filteredTools = this.selectedContext === "all-agents"
2208
2854
  ? allTools
2209
- : allTools.filter(tool => tool.agentId === this.selectedContext);
2855
+ : allTools.filter((tool) => !tool.agentId || tool.agentId === this.selectedContext);
2210
2856
 
2211
2857
  return html`
2212
2858
  <div class="flex h-full flex-col overflow-hidden">
@@ -2219,39 +2865,43 @@ export class WebInspectorElement extends LitElement {
2219
2865
  `;
2220
2866
  }
2221
2867
 
2222
- private extractToolsFromAgents(): Array<{
2223
- agentId: string;
2224
- name: string;
2225
- description?: string;
2226
- parameters?: unknown;
2227
- type: 'handler' | 'renderer';
2228
- }> {
2868
+ private extractToolsFromAgents(): InspectorToolDefinition[] {
2229
2869
  if (!this._core) {
2230
2870
  return [];
2231
2871
  }
2232
2872
 
2233
- const tools: Array<{
2234
- agentId: string;
2235
- name: string;
2236
- description?: string;
2237
- parameters?: unknown;
2238
- type: 'handler' | 'renderer';
2239
- }> = [];
2873
+ const tools: InspectorToolDefinition[] = [];
2874
+
2875
+ // Start with tools registered on the core (frontend tools / HIL)
2876
+ for (const coreTool of this._core.tools ?? []) {
2877
+ tools.push({
2878
+ agentId: coreTool.agentId ?? "",
2879
+ name: coreTool.name,
2880
+ description: coreTool.description,
2881
+ parameters: coreTool.parameters,
2882
+ type: 'handler',
2883
+ });
2884
+ }
2240
2885
 
2886
+ // Augment with agent-level tool handlers/renderers
2241
2887
  for (const [agentId, agent] of Object.entries(this._core.agents)) {
2242
2888
  if (!agent) continue;
2243
2889
 
2244
2890
  // Try to extract tool handlers
2245
- const handlers = (agent as any).toolHandlers;
2891
+ const handlers = (agent as { toolHandlers?: Record<string, unknown> }).toolHandlers;
2246
2892
  if (handlers && typeof handlers === 'object') {
2247
2893
  for (const [toolName, handler] of Object.entries(handlers)) {
2248
2894
  if (handler && typeof handler === 'object') {
2249
- const handlerObj = handler as any;
2895
+ const handlerObj = handler as Record<string, unknown>;
2250
2896
  tools.push({
2251
2897
  agentId,
2252
2898
  name: toolName,
2253
- description: handlerObj.description || handlerObj.tool?.description,
2254
- parameters: handlerObj.parameters || handlerObj.tool?.parameters,
2899
+ description:
2900
+ (typeof handlerObj.description === "string" && handlerObj.description) ||
2901
+ (handlerObj.tool as { description?: string } | undefined)?.description,
2902
+ parameters:
2903
+ handlerObj.parameters ??
2904
+ (handlerObj.tool as { parameters?: unknown } | undefined)?.parameters,
2255
2905
  type: 'handler',
2256
2906
  });
2257
2907
  }
@@ -2259,18 +2909,22 @@ export class WebInspectorElement extends LitElement {
2259
2909
  }
2260
2910
 
2261
2911
  // Try to extract tool renderers
2262
- const renderers = (agent as any).toolRenderers;
2912
+ const renderers = (agent as { toolRenderers?: Record<string, unknown> }).toolRenderers;
2263
2913
  if (renderers && typeof renderers === 'object') {
2264
2914
  for (const [toolName, renderer] of Object.entries(renderers)) {
2265
2915
  // Don't duplicate if we already have it as a handler
2266
2916
  if (!tools.some(t => t.agentId === agentId && t.name === toolName)) {
2267
2917
  if (renderer && typeof renderer === 'object') {
2268
- const rendererObj = renderer as any;
2918
+ const rendererObj = renderer as Record<string, unknown>;
2269
2919
  tools.push({
2270
2920
  agentId,
2271
2921
  name: toolName,
2272
- description: rendererObj.description || rendererObj.tool?.description,
2273
- parameters: rendererObj.parameters || rendererObj.tool?.parameters,
2922
+ description:
2923
+ (typeof rendererObj.description === "string" && rendererObj.description) ||
2924
+ (rendererObj.tool as { description?: string } | undefined)?.description,
2925
+ parameters:
2926
+ rendererObj.parameters ??
2927
+ (rendererObj.tool as { parameters?: unknown } | undefined)?.parameters,
2274
2928
  type: 'renderer',
2275
2929
  });
2276
2930
  }
@@ -2286,13 +2940,7 @@ export class WebInspectorElement extends LitElement {
2286
2940
  });
2287
2941
  }
2288
2942
 
2289
- private renderToolCard(tool: {
2290
- agentId: string;
2291
- name: string;
2292
- description?: string;
2293
- parameters?: unknown;
2294
- type: 'handler' | 'renderer';
2295
- }) {
2943
+ private renderToolCard(tool: InspectorToolDefinition) {
2296
2944
  const isExpanded = this.expandedTools.has(`${tool.agentId}:${tool.name}`);
2297
2945
  const schema = this.extractSchemaInfo(tool.parameters);
2298
2946
 
@@ -2423,18 +3071,27 @@ export class WebInspectorElement extends LitElement {
2423
3071
  }
2424
3072
 
2425
3073
  // Try Zod schema introspection
2426
- const zodDef = (parameters as any)._def;
2427
- if (zodDef) {
3074
+ const zodDef = (parameters as { _def?: Record<string, unknown> })._def;
3075
+ if (zodDef && typeof zodDef === "object") {
2428
3076
  // Handle Zod object schema
2429
3077
  if (zodDef.typeName === 'ZodObject') {
2430
- const shape = zodDef.shape?.() || zodDef.shape;
3078
+ const rawShape = zodDef.shape;
3079
+ const shape =
3080
+ typeof rawShape === "function"
3081
+ ? (rawShape as () => Record<string, unknown>)()
3082
+ : (rawShape as Record<string, unknown> | undefined);
3083
+
3084
+ if (!shape || typeof shape !== "object") {
3085
+ return result;
3086
+ }
2431
3087
  const requiredKeys = new Set<string>();
2432
3088
 
2433
3089
  // Get required fields
2434
3090
  if (zodDef.unknownKeys === 'strict' || !zodDef.catchall) {
2435
- Object.keys(shape || {}).forEach(key => {
2436
- const fieldDef = shape[key]?._def;
2437
- if (fieldDef && !this.isZodOptional(shape[key])) {
3091
+ Object.keys(shape || {}).forEach((key) => {
3092
+ const candidate = (shape as Record<string, unknown>)[key];
3093
+ const fieldDef = (candidate as { _def?: Record<string, unknown> } | undefined)?._def;
3094
+ if (fieldDef && !this.isZodOptional(candidate)) {
2438
3095
  requiredKeys.add(key);
2439
3096
  }
2440
3097
  });
@@ -2453,20 +3110,27 @@ export class WebInspectorElement extends LitElement {
2453
3110
  });
2454
3111
  }
2455
3112
  }
2456
- } else if ((parameters as any).type === 'object' && (parameters as any).properties) {
3113
+ } else if (
3114
+ (parameters as { type?: string; properties?: Record<string, unknown> }).type === 'object' &&
3115
+ (parameters as { properties?: Record<string, unknown> }).properties
3116
+ ) {
2457
3117
  // Handle JSON Schema format
2458
- const props = (parameters as any).properties;
2459
- const required = new Set((parameters as any).required || []);
3118
+ const props = (parameters as { properties?: Record<string, unknown> }).properties;
3119
+ const required = new Set(
3120
+ Array.isArray((parameters as { required?: string[] }).required)
3121
+ ? (parameters as { required?: string[] }).required
3122
+ : [],
3123
+ );
2460
3124
 
2461
- for (const [key, value] of Object.entries(props)) {
2462
- const prop = value as any;
3125
+ for (const [key, value] of Object.entries(props ?? {})) {
3126
+ const prop = value as Record<string, unknown>;
2463
3127
  result.properties.push({
2464
3128
  name: key,
2465
- type: prop.type,
2466
- description: prop.description,
3129
+ type: prop.type as string | undefined,
3130
+ description: typeof prop.description === "string" ? prop.description : undefined,
2467
3131
  required: required.has(key),
2468
3132
  defaultValue: prop.default,
2469
- enum: prop.enum,
3133
+ enum: Array.isArray(prop.enum) ? prop.enum : undefined,
2470
3134
  });
2471
3135
  }
2472
3136
  }
@@ -2474,10 +3138,11 @@ export class WebInspectorElement extends LitElement {
2474
3138
  return result;
2475
3139
  }
2476
3140
 
2477
- private isZodOptional(zodSchema: any): boolean {
2478
- if (!zodSchema?._def) return false;
3141
+ private isZodOptional(zodSchema: unknown): boolean {
3142
+ const schema = zodSchema as { _def?: Record<string, unknown> };
3143
+ if (!schema?._def) return false;
2479
3144
 
2480
- const def = zodSchema._def;
3145
+ const def = schema._def;
2481
3146
 
2482
3147
  // Check if it's explicitly optional or nullable
2483
3148
  if (def.typeName === 'ZodOptional' || def.typeName === 'ZodNullable') {
@@ -2492,7 +3157,7 @@ export class WebInspectorElement extends LitElement {
2492
3157
  return false;
2493
3158
  }
2494
3159
 
2495
- private extractZodFieldInfo(zodSchema: any): {
3160
+ private extractZodFieldInfo(zodSchema: unknown): {
2496
3161
  type?: string;
2497
3162
  description?: string;
2498
3163
  defaultValue?: unknown;
@@ -2505,23 +3170,26 @@ export class WebInspectorElement extends LitElement {
2505
3170
  enum?: unknown[];
2506
3171
  } = {};
2507
3172
 
2508
- if (!zodSchema?._def) return info;
3173
+ const schema = zodSchema as { _def?: Record<string, unknown> };
3174
+ if (!schema?._def) return info;
2509
3175
 
2510
- let currentSchema = zodSchema;
2511
- let def = currentSchema._def;
3176
+ let currentSchema = schema as { _def?: Record<string, unknown> };
3177
+ let def = currentSchema._def as Record<string, unknown>;
2512
3178
 
2513
3179
  // Unwrap optional/nullable
2514
3180
  while (def.typeName === 'ZodOptional' || def.typeName === 'ZodNullable' || def.typeName === 'ZodDefault') {
2515
3181
  if (def.typeName === 'ZodDefault' && def.defaultValue !== undefined) {
2516
3182
  info.defaultValue = typeof def.defaultValue === 'function' ? def.defaultValue() : def.defaultValue;
2517
3183
  }
2518
- currentSchema = def.innerType;
3184
+ currentSchema = (def.innerType as { _def?: Record<string, unknown> }) ?? currentSchema;
2519
3185
  if (!currentSchema?._def) break;
2520
- def = currentSchema._def;
3186
+ def = currentSchema._def as Record<string, unknown>;
2521
3187
  }
2522
3188
 
2523
3189
  // Extract description
2524
- info.description = def.description;
3190
+ info.description = typeof def.description === "string" ? def.description : undefined;
3191
+
3192
+ const typeName = typeof def.typeName === "string" ? def.typeName : undefined;
2525
3193
 
2526
3194
  // Extract type
2527
3195
  const typeMap: Record<string, string> = {
@@ -2536,12 +3204,12 @@ export class WebInspectorElement extends LitElement {
2536
3204
  ZodAny: 'any',
2537
3205
  ZodUnknown: 'unknown',
2538
3206
  };
2539
- info.type = typeMap[def.typeName] || def.typeName?.replace('Zod', '').toLowerCase();
3207
+ info.type = typeName ? typeMap[typeName] || typeName.replace('Zod', '').toLowerCase() : undefined;
2540
3208
 
2541
3209
  // Extract enum values
2542
- if (def.typeName === 'ZodEnum' && Array.isArray(def.values)) {
2543
- info.enum = def.values;
2544
- } else if (def.typeName === 'ZodLiteral' && def.value !== undefined) {
3210
+ if (typeName === 'ZodEnum' && Array.isArray(def.values)) {
3211
+ info.enum = def.values as unknown[];
3212
+ } else if (typeName === 'ZodLiteral' && def.value !== undefined) {
2545
3213
  info.enum = [def.value];
2546
3214
  }
2547
3215
 
@@ -2589,6 +3257,7 @@ export class WebInspectorElement extends LitElement {
2589
3257
  const isExpanded = this.expandedContextItems.has(id);
2590
3258
  const valuePreview = this.getContextValuePreview(context.value);
2591
3259
  const hasValue = context.value !== undefined && context.value !== null;
3260
+ const title = context.description?.trim() || id;
2592
3261
 
2593
3262
  return html`
2594
3263
  <div class="rounded-lg border border-gray-200 bg-white overflow-hidden">
@@ -2599,11 +3268,9 @@ export class WebInspectorElement extends LitElement {
2599
3268
  >
2600
3269
  <div class="flex items-start justify-between gap-3">
2601
3270
  <div class="flex-1 min-w-0">
2602
- ${context.description
2603
- ? html`<p class="text-sm font-medium text-gray-900 mb-1">${context.description}</p>`
2604
- : html`<p class="text-sm font-medium text-gray-500 italic mb-1">No description</p>`}
3271
+ <p class="text-sm font-medium text-gray-900 mb-1">${title}</p>
2605
3272
  <div class="flex items-center gap-2 text-xs text-gray-500">
2606
- <span class="font-mono">${id.substring(0, 8)}...</span>
3273
+ <span class="font-mono truncate inline-block align-middle" style="max-width: 180px;">${id}</span>
2607
3274
  ${hasValue
2608
3275
  ? html`
2609
3276
  <span class="text-gray-300">•</span>
@@ -2627,7 +3294,19 @@ export class WebInspectorElement extends LitElement {
2627
3294
  </div>
2628
3295
  ${hasValue
2629
3296
  ? html`
2630
- <h5 class="mb-2 text-xs font-semibold text-gray-700">Value</h5>
3297
+ <div class="mb-2 flex items-center justify-between gap-2">
3298
+ <h5 class="text-xs font-semibold text-gray-700">Value</h5>
3299
+ <button
3300
+ 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"
3301
+ type="button"
3302
+ @click=${(e: Event) => {
3303
+ e.stopPropagation();
3304
+ void this.copyContextValue(context.value, id);
3305
+ }}
3306
+ >
3307
+ ${this.copiedContextItems.has(id) ? "Copied" : "Copy JSON"}
3308
+ </button>
3309
+ </div>
2631
3310
  <div class="rounded-md border border-gray-200 bg-white p-3">
2632
3311
  <pre class="overflow-auto text-xs text-gray-800 max-h-96"><code>${this.formatContextValue(context.value)}</code></pre>
2633
3312
  </div>
@@ -2688,11 +3367,31 @@ export class WebInspectorElement extends LitElement {
2688
3367
 
2689
3368
  try {
2690
3369
  return JSON.stringify(value, null, 2);
2691
- } catch (error) {
3370
+ } catch {
2692
3371
  return String(value);
2693
3372
  }
2694
3373
  }
2695
3374
 
3375
+ private async copyContextValue(value: unknown, contextId: string): Promise<void> {
3376
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
3377
+ console.warn("Clipboard API is not available in this environment.");
3378
+ return;
3379
+ }
3380
+
3381
+ const serialized = this.formatContextValue(value);
3382
+ try {
3383
+ await navigator.clipboard.writeText(serialized);
3384
+ this.copiedContextItems.add(contextId);
3385
+ this.requestUpdate();
3386
+ setTimeout(() => {
3387
+ this.copiedContextItems.delete(contextId);
3388
+ this.requestUpdate();
3389
+ }, 1500);
3390
+ } catch (error) {
3391
+ console.error("Failed to copy context value:", error);
3392
+ }
3393
+ }
3394
+
2696
3395
  private toggleContextExpansion(contextId: string): void {
2697
3396
  if (this.expandedContextItems.has(contextId)) {
2698
3397
  this.expandedContextItems.delete(contextId);
@@ -2731,6 +3430,218 @@ export class WebInspectorElement extends LitElement {
2731
3430
  }
2732
3431
  this.requestUpdate();
2733
3432
  }
3433
+
3434
+ private renderAnnouncementPanel() {
3435
+ if (!this.isOpen) {
3436
+ return nothing;
3437
+ }
3438
+
3439
+ // Ensure loading is triggered even if we mounted in an already-open state
3440
+ this.ensureAnnouncementLoading();
3441
+
3442
+ if (!this.hasUnseenAnnouncement) {
3443
+ return nothing;
3444
+ }
3445
+
3446
+ if (!this.announcementLoaded && !this.announcementMarkdown) {
3447
+ return html`<div 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)]">
3448
+ <div class="flex items-center gap-2 font-semibold">
3449
+ <span class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm">
3450
+ ${this.renderIcon("Megaphone")}
3451
+ </span>
3452
+ <span>Loading latest announcement…</span>
3453
+ </div>
3454
+ </div>`;
3455
+ }
3456
+
3457
+ if (this.announcementLoadError) {
3458
+ return html`<div 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)]">
3459
+ <div class="flex items-center gap-2 font-semibold">
3460
+ <span class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-rose-600 text-white shadow-sm">
3461
+ ${this.renderIcon("Megaphone")}
3462
+ </span>
3463
+ <span>Announcement unavailable</span>
3464
+ </div>
3465
+ <p class="mt-2 text-xs text-rose-800">We couldn’t load the latest notice. Please try opening the inspector again.</p>
3466
+ </div>`;
3467
+ }
3468
+
3469
+ if (!this.announcementMarkdown) {
3470
+ return nothing;
3471
+ }
3472
+
3473
+ const content = this.announcementHtml
3474
+ ? unsafeHTML(this.announcementHtml)
3475
+ : html`<pre class="whitespace-pre-wrap text-sm text-gray-900">${this.announcementMarkdown}</pre>`;
3476
+
3477
+ return html`<div 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)]">
3478
+ <div class="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-900">
3479
+ <span class="inline-flex h-7 w-7 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm">
3480
+ ${this.renderIcon("Megaphone")}
3481
+ </span>
3482
+ <span>Announcement</span>
3483
+ <button class="announcement-dismiss ml-auto" type="button" @click=${this.handleDismissAnnouncement} aria-label="Dismiss announcement">
3484
+ Dismiss
3485
+ </button>
3486
+ </div>
3487
+ <div class="announcement-content text-sm leading-relaxed text-gray-900">${content}</div>
3488
+ </div>`;
3489
+ }
3490
+
3491
+ private ensureAnnouncementLoading(): void {
3492
+ if (this.announcementPromise || typeof window === "undefined" || typeof fetch === "undefined") {
3493
+ return;
3494
+ }
3495
+ this.announcementPromise = this.fetchAnnouncement();
3496
+ }
3497
+
3498
+ private renderAnnouncementPreview() {
3499
+ if (!this.hasUnseenAnnouncement || !this.showAnnouncementPreview || !this.announcementPreviewText) {
3500
+ return nothing;
3501
+ }
3502
+
3503
+ const side = this.contextState.button.anchor.horizontal === "left" ? "right" : "left";
3504
+
3505
+ return html`<div
3506
+ class="announcement-preview"
3507
+ data-side=${side}
3508
+ role="note"
3509
+ @click=${() => this.handleAnnouncementPreviewClick()}
3510
+ >
3511
+ <span>${this.announcementPreviewText}</span>
3512
+ <span class="announcement-preview__arrow"></span>
3513
+ </div>`;
3514
+ }
3515
+
3516
+ private handleAnnouncementPreviewClick(): void {
3517
+ this.showAnnouncementPreview = false;
3518
+ this.openInspector();
3519
+ }
3520
+
3521
+ private handleDismissAnnouncement = (): void => {
3522
+ this.markAnnouncementSeen();
3523
+ };
3524
+
3525
+ private async fetchAnnouncement(): Promise<void> {
3526
+ try {
3527
+ const response = await fetch(ANNOUNCEMENT_URL, { cache: "no-cache" });
3528
+ if (!response.ok) {
3529
+ throw new Error(`Failed to load announcement (${response.status})`);
3530
+ }
3531
+
3532
+ const data = (await response.json()) as {
3533
+ timestamp?: unknown;
3534
+ previewText?: unknown;
3535
+ announcement?: unknown;
3536
+ };
3537
+
3538
+ const timestamp = typeof data?.timestamp === "string" ? data.timestamp : null;
3539
+ const previewText = typeof data?.previewText === "string" ? data.previewText : null;
3540
+ const markdown = typeof data?.announcement === "string" ? data.announcement : null;
3541
+
3542
+ if (!timestamp || !markdown) {
3543
+ throw new Error("Malformed announcement payload");
3544
+ }
3545
+
3546
+ const storedTimestamp = this.loadStoredAnnouncementTimestamp();
3547
+
3548
+ this.announcementTimestamp = timestamp;
3549
+ this.announcementPreviewText = previewText ?? "";
3550
+ this.announcementMarkdown = markdown;
3551
+ this.hasUnseenAnnouncement = (!storedTimestamp || storedTimestamp !== timestamp) && !!this.announcementPreviewText;
3552
+ this.showAnnouncementPreview = this.hasUnseenAnnouncement;
3553
+ this.announcementHtml = await this.convertMarkdownToHtml(markdown);
3554
+ this.announcementLoaded = true;
3555
+
3556
+ this.requestUpdate();
3557
+ } catch (error) {
3558
+ this.announcementLoadError = error;
3559
+ this.announcementLoaded = true;
3560
+ this.requestUpdate();
3561
+ }
3562
+ }
3563
+
3564
+ private async convertMarkdownToHtml(markdown: string): Promise<string | null> {
3565
+ const renderer = new marked.Renderer();
3566
+ renderer.link = (href, title, text) => {
3567
+ const safeHref = this.escapeHtmlAttr(this.appendRefParam(href ?? ""));
3568
+ const titleAttr = title ? ` title="${this.escapeHtmlAttr(title)}"` : "";
3569
+ return `<a href="${safeHref}" target="_blank" rel="noopener"${titleAttr}>${text}</a>`;
3570
+ };
3571
+ return marked.parse(markdown, { renderer });
3572
+ }
3573
+
3574
+ private appendRefParam(href: string): string {
3575
+ try {
3576
+ const url = new URL(href, typeof window !== "undefined" ? window.location.href : "https://copilotkit.ai");
3577
+ if (!url.searchParams.has("ref")) {
3578
+ url.searchParams.append("ref", "cpk-inspector");
3579
+ }
3580
+ return url.toString();
3581
+ } catch {
3582
+ return href;
3583
+ }
3584
+ }
3585
+
3586
+ private escapeHtmlAttr(value: string): string {
3587
+ return value
3588
+ .replace(/&/g, "&amp;")
3589
+ .replace(/</g, "&lt;")
3590
+ .replace(/>/g, "&gt;")
3591
+ .replace(/\"/g, "&quot;")
3592
+ .replace(/'/g, "&#39;");
3593
+ }
3594
+
3595
+ private loadStoredAnnouncementTimestamp(): string | null {
3596
+ if (typeof window === "undefined" || !window.localStorage) {
3597
+ return null;
3598
+ }
3599
+ try {
3600
+ const raw = window.localStorage.getItem(ANNOUNCEMENT_STORAGE_KEY);
3601
+ if (!raw) {
3602
+ return null;
3603
+ }
3604
+ const parsed = JSON.parse(raw);
3605
+ if (parsed && typeof parsed.timestamp === "string") {
3606
+ return parsed.timestamp;
3607
+ }
3608
+ // Backward compatibility: previous shape { hash }
3609
+ return null;
3610
+ } catch {
3611
+ // ignore malformed storage
3612
+ }
3613
+ return null;
3614
+ }
3615
+
3616
+ private persistAnnouncementTimestamp(timestamp: string): void {
3617
+ if (typeof window === "undefined" || !window.localStorage) {
3618
+ return;
3619
+ }
3620
+ try {
3621
+ const payload = JSON.stringify({ timestamp });
3622
+ window.localStorage.setItem(ANNOUNCEMENT_STORAGE_KEY, payload);
3623
+ } catch {
3624
+ // Non-fatal if storage is unavailable
3625
+ }
3626
+ }
3627
+
3628
+ private markAnnouncementSeen(): void {
3629
+ // Clear badge only when explicitly dismissed
3630
+ this.hasUnseenAnnouncement = false;
3631
+ this.showAnnouncementPreview = false;
3632
+
3633
+ if (!this.announcementTimestamp) {
3634
+ // If still loading, attempt once more after promise resolves; avoid infinite requeues
3635
+ if (this.announcementPromise && !this.announcementLoaded) {
3636
+ void this.announcementPromise.then(() => this.markAnnouncementSeen()).catch(() => undefined);
3637
+ }
3638
+ this.requestUpdate();
3639
+ return;
3640
+ }
3641
+
3642
+ this.persistAnnouncementTimestamp(this.announcementTimestamp);
3643
+ this.requestUpdate();
3644
+ }
2734
3645
  }
2735
3646
 
2736
3647
  export function defineWebInspector(): void {
@@ -2743,6 +3654,6 @@ defineWebInspector();
2743
3654
 
2744
3655
  declare global {
2745
3656
  interface HTMLElementTagNameMap {
2746
- "web-inspector": WebInspectorElement;
3657
+ "cpk-web-inspector": WebInspectorElement;
2747
3658
  }
2748
3659
  }