@copilotkit/web-inspector 1.56.5 → 1.57.0

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,16 +1,24 @@
1
1
  import { LitElement, css, html, nothing, unsafeCSS } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ import { marked } from "marked";
2
4
  import { styleMap } from "lit/directives/style-map.js";
3
5
  import tailwindStyles from "./styles/generated.css";
4
6
  import inspectorLogoUrl from "./assets/inspector-logo.svg";
5
7
  import inspectorLogoIconUrl from "./assets/inspector-logo-icon.svg";
6
8
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
7
- import { marked } from "marked";
8
9
  import { icons } from "lucide";
10
+ import type { CopilotKitCore } from "@copilotkit/core";
9
11
  import {
10
- CopilotKitCore,
11
12
  CopilotKitCoreRuntimeConnectionStatus,
12
- type CopilotKitCoreSubscriber,
13
- type CopilotKitCoreErrorCode,
13
+ ɵselectThreads,
14
+ ɵselectThreadsError,
15
+ ɵcreateThreadStore,
16
+ } from "@copilotkit/core";
17
+ import type {
18
+ CopilotKitCoreSubscriber,
19
+ CopilotKitCoreErrorCode,
20
+ ɵThreadStore,
21
+ ɵThread,
14
22
  } from "@copilotkit/core";
15
23
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
16
24
  import type {
@@ -33,18 +41,23 @@ import {
33
41
  import {
34
42
  loadInspectorState,
35
43
  saveInspectorState,
36
- type PersistedState,
37
44
  isValidAnchor,
38
45
  isValidPosition,
39
46
  isValidSize,
40
47
  isValidDockMode,
41
48
  } from "./lib/persistence";
49
+ import type { PersistedState } from "./lib/persistence";
42
50
 
43
51
  export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const;
44
52
 
45
53
  type LucideIconName = keyof typeof icons;
46
54
 
47
- type MenuKey = "ag-ui-events" | "agents" | "frontend-tools" | "agent-context";
55
+ type MenuKey =
56
+ | "ag-ui-events"
57
+ | "agents"
58
+ | "frontend-tools"
59
+ | "agent-context"
60
+ | "threads";
48
61
 
49
62
  type MenuItem = {
50
63
  key: MenuKey;
@@ -61,7 +74,7 @@ const INSPECTOR_STORAGE_KEY = "cpk:inspector:state";
61
74
  const ANNOUNCEMENT_STORAGE_KEY = "cpk:inspector:announcements";
62
75
  const ANNOUNCEMENT_URL = "https://cdn.copilotkit.ai/announcements.json";
63
76
  const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 };
64
- const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 560 };
77
+ const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 700 };
65
78
  const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar
66
79
  const MAX_AGENT_EVENTS = 200;
67
80
  const MAX_TOTAL_EVENTS = 500;
@@ -87,7 +100,9 @@ type InspectorAgentEventType =
87
100
  | "REASONING_MESSAGE_CONTENT"
88
101
  | "REASONING_MESSAGE_END"
89
102
  | "REASONING_END"
90
- | "REASONING_ENCRYPTED_VALUE";
103
+ | "REASONING_ENCRYPTED_VALUE"
104
+ | "ACTIVITY_SNAPSHOT"
105
+ | "ACTIVITY_DELTA";
91
106
 
92
107
  const AGENT_EVENT_TYPES: readonly InspectorAgentEventType[] = [
93
108
  "RUN_STARTED",
@@ -111,49 +126,2206 @@ const AGENT_EVENT_TYPES: readonly InspectorAgentEventType[] = [
111
126
  "REASONING_MESSAGE_END",
112
127
  "REASONING_END",
113
128
  "REASONING_ENCRYPTED_VALUE",
129
+ "ACTIVITY_SNAPSHOT",
130
+ "ACTIVITY_DELTA",
114
131
  ] as const;
115
132
 
116
- type SanitizedValue =
117
- | string
118
- | number
119
- | boolean
120
- | null
121
- | SanitizedValue[]
122
- | { [key: string]: SanitizedValue };
133
+ type SanitizedValue =
134
+ | string
135
+ | number
136
+ | boolean
137
+ | null
138
+ | SanitizedValue[]
139
+ | { [key: string]: SanitizedValue };
140
+
141
+ type InspectorToolCall = {
142
+ id?: string;
143
+ function?: {
144
+ name?: string;
145
+ arguments?: SanitizedValue | string;
146
+ };
147
+ toolName?: string;
148
+ status?: string;
149
+ };
150
+
151
+ type InspectorMessage = {
152
+ id?: string;
153
+ role: string;
154
+ contentText: string;
155
+ contentRaw?: SanitizedValue;
156
+ toolCalls: InspectorToolCall[];
157
+ /** Populated for role="activity" messages (Generative UI). */
158
+ activityType?: string;
159
+ };
160
+
161
+ type InspectorToolDefinition = {
162
+ agentId: string;
163
+ name: string;
164
+ description?: string;
165
+ parameters?: unknown;
166
+ type: "handler" | "renderer";
167
+ };
168
+
169
+ type InspectorEvent = {
170
+ id: string;
171
+ agentId: string;
172
+ type: InspectorAgentEventType;
173
+ timestamp: number;
174
+ payload: SanitizedValue;
175
+ };
176
+
177
+ // ─── Thread details types ────────────────────────────────────────────────────
178
+
179
+ interface ApiThreadMessage {
180
+ id: string;
181
+ role: string;
182
+ content?: string;
183
+ toolCalls?: Array<{ id: string; name: string; args: string }>;
184
+ toolCallId?: string;
185
+ /** Present when role === "activity" (Generative UI output). */
186
+ activityType?: string;
187
+ }
188
+
189
+ interface ConversationUser {
190
+ id: string;
191
+ type: "user";
192
+ content: string;
193
+ createdAt: string;
194
+ }
195
+
196
+ interface ConversationAssistant {
197
+ id: string;
198
+ type: "assistant";
199
+ content: string;
200
+ createdAt: string;
201
+ }
202
+
203
+ interface ConversationToolCall {
204
+ id: string;
205
+ type: "tool_call";
206
+ toolName: string;
207
+ toolCallId: string;
208
+ arguments: Record<string, unknown>;
209
+ result: Record<string, unknown> | null;
210
+ createdAt: string;
211
+ groupId?: string;
212
+ }
213
+
214
+ interface ConversationReasoning {
215
+ id: string;
216
+ type: "reasoning";
217
+ duration: string;
218
+ createdAt: string;
219
+ }
220
+
221
+ interface ConversationStateUpdate {
222
+ id: string;
223
+ type: "state_update";
224
+ createdAt: string;
225
+ }
226
+
227
+ interface ConversationAgentResponded {
228
+ id: string;
229
+ type: "agent_responded";
230
+ createdAt: string;
231
+ }
232
+
233
+ interface ConversationGenerativeUIItem {
234
+ id: string;
235
+ type: "generative-ui";
236
+ activityType: string;
237
+ createdAt: string;
238
+ }
239
+
240
+ interface ToolCallGroup {
241
+ type: "tool_call_group";
242
+ id: string;
243
+ items: ConversationToolCall[];
244
+ }
245
+
246
+ type ConversationItem =
247
+ | ConversationUser
248
+ | ConversationAssistant
249
+ | ConversationToolCall
250
+ | ConversationReasoning
251
+ | ConversationStateUpdate
252
+ | ConversationAgentResponded
253
+ | ConversationGenerativeUIItem;
254
+
255
+ type RenderItem = ConversationItem | ToolCallGroup;
256
+
257
+ interface ApiAgentEvent {
258
+ type: string;
259
+ timestamp: string | number;
260
+ payload: Record<string, unknown>;
261
+ }
262
+
263
+ type ThreadDetailsTab = "conversation" | "agent-state" | "ag-ui-events";
264
+
265
+ // ─── JSON syntax highlighter ─────────────────────────────────────────────────
266
+ // Inline-styled so shadow DOM encapsulation preserves colors when the output
267
+ // is injected via unsafeHTML. Only for structured data — never raw user HTML.
268
+
269
+ function escapeHtml(s: string): string {
270
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
271
+ }
272
+
273
+ // Memoize highlight output by payload reference. Tab switches cause Lit to
274
+ // re-render the active panel from scratch, and the JSON.stringify + regex
275
+ // pass below is by far the most expensive thing in the events / state
276
+ // panels (potentially MB of agent state). Caching by object reference
277
+ // turns subsequent renders of an unchanged event list into near-zero JS work.
278
+ const highlightedJsonCache = new WeakMap<object, string>();
279
+
280
+ function highlightedJson(obj: unknown): string {
281
+ if (typeof obj === "object" && obj !== null) {
282
+ const cached = highlightedJsonCache.get(obj);
283
+ if (cached !== undefined) return cached;
284
+ }
285
+ const colors = {
286
+ key: "#5558B2",
287
+ str: "#189370",
288
+ num: "#996300",
289
+ bool: "#c0333a",
290
+ nil: "#838389",
291
+ };
292
+ const json = JSON.stringify(obj, null, 2);
293
+ if (!json) return "";
294
+ const parts: string[] = [];
295
+ let lastIndex = 0;
296
+ const re =
297
+ /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g;
298
+ let match: RegExpExecArray | null;
299
+ while ((match = re.exec(json)) !== null) {
300
+ parts.push(escapeHtml(json.slice(lastIndex, match.index)));
301
+ const m = match[0];
302
+ let color = colors.num;
303
+ if (m.startsWith('"')) {
304
+ color = m.trimEnd().endsWith(":") ? colors.key : colors.str;
305
+ } else if (m === "true" || m === "false") {
306
+ color = colors.bool;
307
+ } else if (m === "null") {
308
+ color = colors.nil;
309
+ }
310
+ parts.push(`<span style="color:${color}">${escapeHtml(m)}</span>`);
311
+ lastIndex = match.index + m.length;
312
+ }
313
+ parts.push(escapeHtml(json.slice(lastIndex)));
314
+ const result = parts.join("");
315
+ if (typeof obj === "object" && obj !== null) {
316
+ highlightedJsonCache.set(obj, result);
317
+ }
318
+ return result;
319
+ }
320
+
321
+ function eventColors(type: string): { bg: string; fg: string } {
322
+ if (type.startsWith("TEXT_MESSAGE")) return { bg: "#EEE6FE", fg: "#57575B" };
323
+ if (type.startsWith("TOOL_CALL"))
324
+ return { bg: "rgba(133,236,206,0.15)", fg: "#189370" };
325
+ if (type.startsWith("STATE"))
326
+ return { bg: "rgba(190,194,255,0.102)", fg: "#5558B2" };
327
+ if (type.startsWith("RUN_") || type.startsWith("STEP_"))
328
+ return { bg: "rgba(255,172,77,0.2)", fg: "#996300" };
329
+ if (type === "ERROR") return { bg: "rgba(250,95,103,0.13)", fg: "#c0333a" };
330
+ return { bg: "#F7F7F9", fg: "#838389" };
331
+ }
332
+
333
+ function formatTimestamp(ts: string | number): string {
334
+ const date = typeof ts === "number" ? new Date(ts) : new Date(ts);
335
+ if (Number.isNaN(date.getTime())) return "";
336
+ const ms = date.getMilliseconds().toString().padStart(3, "0");
337
+ return (
338
+ date.toLocaleTimeString("en-US", {
339
+ hour: "2-digit",
340
+ minute: "2-digit",
341
+ second: "2-digit",
342
+ hour12: false,
343
+ }) +
344
+ "." +
345
+ ms
346
+ );
347
+ }
348
+
349
+ // ─── cpk-thread-list ────────────────────────────────────────────────────────
350
+
351
+ class CpkThreadList extends LitElement {
352
+ static properties = {
353
+ threads: { attribute: false },
354
+ selectedThreadId: { attribute: false },
355
+ errorMessage: { attribute: false },
356
+ _query: { state: true },
357
+ };
358
+ threads: ɵThread[] = [];
359
+ selectedThreadId: string | null = null;
360
+ /**
361
+ * Non-null when the underlying thread store reported a load error
362
+ * (REST list rejection, Phoenix subscribe failure, retry exhaustion).
363
+ * Surfaced inline so users see a real error state instead of stale or
364
+ * empty data with no indication of what went wrong.
365
+ */
366
+ errorMessage: string | null = null;
367
+ private _query = "";
368
+
369
+ static styles = css`
370
+ @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&family=Spline+Sans+Mono:wght@400;500&display=swap");
371
+
372
+ :host {
373
+ display: flex;
374
+ flex-direction: column;
375
+ height: 100%;
376
+ overflow: hidden;
377
+ }
378
+
379
+ .cpk-tl {
380
+ font-family: "Plus Jakarta Sans", sans-serif;
381
+ display: flex;
382
+ flex-direction: column;
383
+ height: 100%;
384
+ overflow: hidden;
385
+ background: #f7f7f9;
386
+ }
387
+
388
+ /* ── Search ── */
389
+ .cpk-tl__search {
390
+ padding: 10px 12px;
391
+ border-bottom: 1px solid #dbdbe5;
392
+ flex-shrink: 0;
393
+ }
394
+
395
+ .cpk-tl__search-input {
396
+ width: 100%;
397
+ box-sizing: border-box;
398
+ font-family: "Plus Jakarta Sans", sans-serif;
399
+ font-size: 12px;
400
+ padding: 7px 10px;
401
+ border-radius: 6px;
402
+ border: 1px solid #dbdbe5;
403
+ background: #ffffff;
404
+ color: #010507;
405
+ outline: none;
406
+ transition: border-color 0.15s;
407
+ }
408
+
409
+ .cpk-tl__search-input:focus {
410
+ border-color: #bec2ff;
411
+ }
412
+
413
+ /* ── List ── */
414
+ .cpk-tl__list {
415
+ flex: 1;
416
+ overflow-y: auto;
417
+ }
418
+
419
+ /* ── Thread item ── */
420
+ .cpk-tl__item {
421
+ padding: 11px 13px;
422
+ cursor: pointer;
423
+ border-bottom: 1px solid #e9e9ef;
424
+ border-left: 3px solid transparent;
425
+ transition: background 0.1s;
426
+ }
427
+
428
+ .cpk-tl__item:hover {
429
+ background: #ffffff;
430
+ }
431
+
432
+ .cpk-tl__item--active {
433
+ background: #bec2ff1a;
434
+ border-left-color: #bec2ff;
435
+ }
436
+
437
+ .cpk-tl__item--active:hover {
438
+ background: #bec2ff33;
439
+ }
440
+
441
+ .cpk-tl__row1 {
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 8px;
445
+ margin-bottom: 3px;
446
+ }
447
+
448
+ .cpk-tl__name {
449
+ font-size: 12px;
450
+ font-weight: 500;
451
+ color: #010507;
452
+ flex: 1;
453
+ overflow: hidden;
454
+ text-overflow: ellipsis;
455
+ white-space: nowrap;
456
+ }
457
+
458
+ .cpk-tl__name--unnamed {
459
+ color: #838389;
460
+ font-style: italic;
461
+ font-weight: 400;
462
+ }
463
+
464
+ .cpk-tl__time {
465
+ font-family: "Spline Sans Mono", monospace;
466
+ font-size: 10px;
467
+ color: #838389;
468
+ flex-shrink: 0;
469
+ }
470
+
471
+ .cpk-tl__meta {
472
+ display: flex;
473
+ gap: 6px;
474
+ align-items: center;
475
+ flex-wrap: wrap;
476
+ }
477
+
478
+ .cpk-tl__pill {
479
+ font-family: "Spline Sans Mono", monospace;
480
+ font-size: 9px;
481
+ padding: 1px 7px;
482
+ border-radius: 4px;
483
+ text-transform: uppercase;
484
+ font-weight: 500;
485
+ white-space: nowrap;
486
+ background: #eee6fe;
487
+ color: #57575b;
488
+ }
489
+
490
+ /* ── Empty state ── */
491
+ .cpk-tl__empty {
492
+ padding: 32px 16px;
493
+ text-align: center;
494
+ color: #838389;
495
+ font-size: 12px;
496
+ display: flex;
497
+ flex-direction: column;
498
+ align-items: center;
499
+ gap: 8px;
500
+ }
501
+
502
+ .cpk-tl__empty-icon {
503
+ color: #c0c0c8;
504
+ }
505
+ `;
506
+
507
+ private relativeTime(dateStr: string): string {
508
+ const date = new Date(dateStr);
509
+ const diffMs = Date.now() - date.getTime();
510
+ const diffSec = Math.floor(diffMs / 1000);
511
+ if (diffSec < 60) return `${diffSec}s ago`;
512
+ const diffMin = Math.floor(diffSec / 60);
513
+ if (diffMin < 60) return `${diffMin}m ago`;
514
+ const diffH = Math.floor(diffMin / 60);
515
+ if (diffH < 24) return `${diffH}h ago`;
516
+ const diffD = Math.floor(diffH / 24);
517
+ return `${diffD}d ago`;
518
+ }
519
+
520
+ private get filtered(): ɵThread[] {
521
+ const q = this._query.toLowerCase();
522
+ if (!q) return this.threads;
523
+ return this.threads.filter(
524
+ (t) =>
525
+ (t.name?.toLowerCase().includes(q) ?? false) ||
526
+ t.agentId.toLowerCase().includes(q) ||
527
+ t.id.toLowerCase().includes(q),
528
+ );
529
+ }
530
+
531
+ private onThreadClick(threadId: string): void {
532
+ this.dispatchEvent(
533
+ new CustomEvent("threadSelected", {
534
+ detail: threadId,
535
+ bubbles: true,
536
+ composed: true,
537
+ }),
538
+ );
539
+ }
540
+
541
+ private onSearchInput = (event: Event): void => {
542
+ this._query = (event.target as HTMLInputElement).value;
543
+ };
544
+
545
+ render() {
546
+ const filtered = this.filtered;
547
+ return html`
548
+ <div class="cpk-tl">
549
+ <!-- Search -->
550
+ <div class="cpk-tl__search">
551
+ <input
552
+ type="text"
553
+ placeholder="Search threads…"
554
+ .value=${this._query}
555
+ @input=${this.onSearchInput}
556
+ class="cpk-tl__search-input"
557
+ />
558
+ </div>
559
+
560
+ <!-- Thread list -->
561
+ <div class="cpk-tl__list">
562
+ ${filtered.map(
563
+ (thread) => html`
564
+ <div
565
+ class="cpk-tl__item ${
566
+ this.selectedThreadId === thread.id
567
+ ? "cpk-tl__item--active"
568
+ : ""
569
+ }"
570
+ @click=${() => this.onThreadClick(thread.id)}
571
+ >
572
+ <div class="cpk-tl__row1">
573
+ <span
574
+ class="cpk-tl__name ${
575
+ !thread.name ? "cpk-tl__name--unnamed" : ""
576
+ }"
577
+ >${thread.name ?? "Untitled"}</span
578
+ >
579
+ <span class="cpk-tl__time"
580
+ >${this.relativeTime(thread.updatedAt)}</span
581
+ >
582
+ </div>
583
+ <div class="cpk-tl__meta">
584
+ <span class="cpk-tl__pill">${thread.agentId}</span>
585
+ </div>
586
+ </div>
587
+ `,
588
+ )}
589
+ ${
590
+ filtered.length === 0
591
+ ? html`
592
+ <div class="cpk-tl__empty">
593
+ ${
594
+ this.errorMessage
595
+ ? html`
596
+ <svg
597
+ width="24"
598
+ height="24"
599
+ viewBox="0 0 24 24"
600
+ fill="none"
601
+ stroke="currentColor"
602
+ stroke-width="1.5"
603
+ stroke-linecap="round"
604
+ stroke-linejoin="round"
605
+ class="cpk-tl__empty-icon"
606
+ >
607
+ <circle cx="12" cy="12" r="10" />
608
+ <line x1="12" y1="8" x2="12" y2="12" />
609
+ <line x1="12" y1="16" x2="12.01" y2="16" />
610
+ </svg>
611
+ <div>
612
+ Failed to load threads
613
+ <div style="font-size:11px;margin-top:4px;color:#c0333a;">
614
+ ${this.errorMessage}
615
+ </div>
616
+ </div>
617
+ `
618
+ : this.threads.length === 0
619
+ ? html`
620
+ <svg
621
+ width="24"
622
+ height="24"
623
+ viewBox="0 0 24 24"
624
+ fill="none"
625
+ stroke="currentColor"
626
+ stroke-width="1.5"
627
+ stroke-linecap="round"
628
+ stroke-linejoin="round"
629
+ class="cpk-tl__empty-icon"
630
+ >
631
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
632
+ </svg>
633
+ No threads yet
634
+ `
635
+ : html`
636
+ No threads match your search.
637
+ `
638
+ }
639
+ </div>
640
+ `
641
+ : nothing
642
+ }
643
+ </div>
644
+ </div>
645
+ `;
646
+ }
647
+ }
648
+
649
+ // ─── cpk-thread-details ──────────────────────────────────────────────────────
650
+ // Renders the selected thread's conversation, agent state, and AG-UI events.
651
+ // Conversation comes from the runtime's `/threads/:id/messages` endpoint
652
+ // (always thread-accurate). Agent state and AG-UI events accept live inputs
653
+ // (`agentStateInput`, `agentEventsInput`) from the parent inspector's ongoing
654
+ // agent subscriptions; when those are absent we fall back to the per-thread
655
+ // fetched data via `/threads/:id/{events,state}`.
656
+
657
+ // Exported (with the underscore-prefixed name signalling internal/test-only)
658
+ // so unit tests can pin down the per-panel template-cache invariants without
659
+ // reaching through `customElements`. Production consumers continue to use the
660
+ // `cpk-thread-details` custom element registered below.
661
+ export class ɵCpkThreadDetails extends LitElement {
662
+ static properties = {
663
+ threadId: { attribute: false },
664
+ thread: { attribute: false },
665
+ runtimeUrl: { attribute: false },
666
+ headers: { attribute: false },
667
+ agentStateInput: { attribute: false },
668
+ agentEventsInput: { attribute: false },
669
+ liveMessageVersion: { attribute: false },
670
+ _tab: { state: true },
671
+ _conversation: { state: true },
672
+ _fetchedEvents: { state: true },
673
+ _fetchedState: { state: true },
674
+ _loadingMessages: { state: true },
675
+ _loadingEvents: { state: true },
676
+ _loadingState: { state: true },
677
+ _messagesError: { state: true },
678
+ _eventsError: { state: true },
679
+ _stateError: { state: true },
680
+ _expandedTools: { state: true },
681
+ _expandedMessages: { state: true },
682
+ _showDetailPanel: { state: true },
683
+ _detailPanelWidth: { state: true },
684
+ _eventsNotAvailable: { state: true },
685
+ _stateNotAvailable: { state: true },
686
+ _panelInitializing: { state: true },
687
+ _activatedTabs: { state: true },
688
+ };
689
+
690
+ threadId: string | null = null;
691
+ thread: ɵThread | null = null;
692
+ runtimeUrl = "";
693
+ headers: Record<string, string> = {};
694
+ agentStateInput: Record<string, unknown> | null = null;
695
+ agentEventsInput: ApiAgentEvent[] = [];
696
+ /**
697
+ * Monotonic per-thread counter the parent inspector ticks every time the
698
+ * agent currently running on this thread emits a message change. When this
699
+ * prop changes for the same `threadId`, we re-fetch `/threads/:id/messages`
700
+ * so the conversation view reflects live streaming output.
701
+ */
702
+ liveMessageVersion = 0;
703
+
704
+ private _tab: ThreadDetailsTab = "conversation";
705
+ private _conversation: ConversationItem[] = [];
706
+ private _fetchedEvents: ApiAgentEvent[] | null = null;
707
+ private _fetchedState: Record<string, unknown> | null = null;
708
+ private _loadingMessages = false;
709
+ private _loadingEvents = false;
710
+ private _loadingState = false;
711
+ private _messagesError: string | null = null;
712
+ private _eventsError: string | null = null;
713
+ private _stateError: string | null = null;
714
+ private _expandedTools = new Set<string>();
715
+ private _expandedMessages = new Set<string>();
716
+ private _showDetailPanel = false;
717
+ private _detailPanelWidth = 250;
718
+ /** True when the /events endpoint returned 501 — don't fall back to live data. */
719
+ private _eventsNotAvailable = false;
720
+ /** True when the /state endpoint returned 501 — don't fall back to live data. */
721
+ private _stateNotAvailable = false;
722
+ /**
723
+ * Briefly true after a tab switch so the active-tab highlight + a generic
724
+ * "Loading…" placeholder paint before the heavy per-tab render runs. Without
725
+ * this, large event/conversation lists block the next paint and the user
726
+ * sees the click as unresponsive for seconds.
727
+ */
728
+ private _panelInitializing = false;
729
+ /**
730
+ * Tabs that have been opened at least once for the current thread. Once a
731
+ * tab is activated, its rendered DOM stays mounted (we hide inactive tabs
732
+ * via display:none) so flipping back to it is just a CSS swap rather than
733
+ * tearing down and rebuilding the entire panel from scratch. Without this,
734
+ * switching back to AG-UI Events on a thread with hundreds of events
735
+ * triggers a multi-second DOM-creation pass each time.
736
+ *
737
+ * Reset to {"conversation"} when the selected thread changes.
738
+ */
739
+ private _activatedTabs: Set<ThreadDetailsTab> = new Set(["conversation"]);
740
+ /**
741
+ * Memoized per-panel templates keyed by the inputs they render from.
742
+ * When the underlying data hasn't changed (same `_conversation` /
743
+ * `_fetchedState` / events array reference, plus expand-state for the
744
+ * conversation panel), we return the previously built TemplateResult.
745
+ * Lit then sees "same template, same values" and skips the diff entirely,
746
+ * so re-rendering on tab switch is near-zero work even when the panel
747
+ * content is large. The key is an opaque tuple compared element-wise by
748
+ * reference; if any element flips, the cache misses and rebuilds.
749
+ */
750
+ private _panelTplCache: Map<
751
+ ThreadDetailsTab,
752
+ { key: readonly unknown[]; tpl: TemplateResult }
753
+ > = new Map();
754
+ /**
755
+ * Tracks whether we've fetched events for the current thread yet. Events
756
+ * fetch lazily on first sub-tab click so a large response's JSON.parse
757
+ * doesn't block the main thread when the user only ever cares about the
758
+ * conversation.
759
+ */
760
+ private _eventsFetched = false;
761
+ /**
762
+ * Tracks whether we've fetched state for the current thread yet. Same
763
+ * lazy-load reasoning as `_eventsFetched`.
764
+ */
765
+ private _stateFetched = false;
766
+ private _lastFetchedThreadId: string | null = null;
767
+ private _lastSeenLiveMessageVersion = 0;
768
+ private _messagesAbort: AbortController | null = null;
769
+ private _eventsAbort: AbortController | null = null;
770
+ private _stateAbort: AbortController | null = null;
771
+ private _dividerResizing = false;
772
+ private _dividerPointerId = -1;
773
+ private _dividerStartX = 0;
774
+ private _dividerStartWidth = 0;
775
+
776
+ static readonly COLLAPSE_THRESHOLD = 800;
777
+ private static readonly TAB_LIST: ReadonlyArray<{
778
+ id: ThreadDetailsTab;
779
+ label: string;
780
+ }> = [
781
+ { id: "conversation", label: "Conversation" },
782
+ { id: "agent-state", label: "Agent State" },
783
+ { id: "ag-ui-events", label: "AG-UI Events" },
784
+ ];
785
+
786
+ private renderTabContent(id: ThreadDetailsTab): TemplateResult {
787
+ if (id === "conversation") return this.renderConversation();
788
+ if (id === "agent-state") return this.renderState();
789
+ return this.renderEvents();
790
+ }
791
+
792
+ private activateTab(id: ThreadDetailsTab): void {
793
+ if (this._tab === id) return;
794
+ const isFirstActivation = !this._activatedTabs.has(id);
795
+ this._tab = id;
796
+ if (isFirstActivation) {
797
+ // First time opening this tab: paint a "Loading…" overlay for one
798
+ // frame so the tab highlight + spinner appear before the heavy
799
+ // per-tab render runs (events list, state JSON). The rAF batches
800
+ // mounting the panel into `_activatedTabs` and clearing the spinner
801
+ // into a single subsequent paint. Subsequent activations are pure
802
+ // CSS toggles via display:none on the already-mounted panel — no
803
+ // re-render required.
804
+ this._panelInitializing = true;
805
+ requestAnimationFrame(() => {
806
+ this._activatedTabs = new Set([...this._activatedTabs, id]);
807
+ this._panelInitializing = false;
808
+ });
809
+ }
810
+ this.maybeFetchTabData(id);
811
+ }
812
+
813
+ private maybeFetchTabData(id: ThreadDetailsTab): void {
814
+ // Lazy-trigger the events / state fetches so their (potentially huge)
815
+ // JSON.parse only blocks the main thread after the user has shown
816
+ // intent to view that sub-tab. Without lazy-load, the eager fetch runs
817
+ // as soon as the thread opens and a single large response can stall
818
+ // the entire panel for seconds — including making the tab buttons
819
+ // themselves feel unresponsive.
820
+ if (!this.threadId) return;
821
+ if (id === "ag-ui-events" && !this._eventsFetched) {
822
+ this._eventsFetched = true;
823
+ void this.fetchEvents(this.threadId);
824
+ } else if (id === "agent-state" && !this._stateFetched) {
825
+ this._stateFetched = true;
826
+ void this.fetchState(this.threadId);
827
+ }
828
+ }
829
+
830
+ static styles = css`
831
+ @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&family=Spline+Sans+Mono:wght@400;500&display=swap");
832
+
833
+ /* ── Root ────────────────────────────────────────────────────────── */
834
+ :host {
835
+ display: flex;
836
+ flex-direction: row;
837
+ overflow: hidden;
838
+ }
839
+
840
+ .cpk-td {
841
+ font-family: "Plus Jakarta Sans", sans-serif;
842
+ font-size: 13px;
843
+ display: flex;
844
+ flex-direction: row;
845
+ width: 100%;
846
+ height: 100%;
847
+ overflow: hidden;
848
+ background: #ffffff;
849
+ }
850
+
851
+ /* ── Left area ───────────────────────────────────────────────────── */
852
+ .cpk-td__left {
853
+ flex: 1;
854
+ min-width: 0;
855
+ display: flex;
856
+ flex-direction: column;
857
+ overflow: hidden;
858
+ }
859
+
860
+ /* ── Tab bar header ──────────────────────────────────────────────── */
861
+ .cpk-td__tabs-header {
862
+ /* No top/right padding so tabs and toggle sit flush against the
863
+ top and right edges of the inspector. */
864
+ padding: 0 0 0 12px;
865
+ border-bottom: 1px solid #dbdbe5;
866
+ flex-shrink: 0;
867
+ display: flex;
868
+ align-items: stretch;
869
+ }
870
+
871
+ .cpk-td__tab-group {
872
+ display: flex;
873
+ gap: 0;
874
+ margin-bottom: -1px;
875
+ /* Allow the tab list to shrink rather than pushing the panel-toggle
876
+ button past the right edge of the inspector when horizontal space
877
+ gets tight (the drawer being open eats noticeably into width). */
878
+ min-width: 0;
879
+ flex-shrink: 1;
880
+ overflow: hidden;
881
+ }
882
+
883
+ .cpk-td__tab {
884
+ font-family: "Plus Jakarta Sans", sans-serif;
885
+ font-size: 11px;
886
+ font-weight: 500;
887
+ padding: 10px 12px;
888
+ border: none;
889
+ border-bottom: 2px solid transparent;
890
+ cursor: pointer;
891
+ background: transparent;
892
+ color: #838389;
893
+ transition:
894
+ color 0.12s,
895
+ border-color 0.12s;
896
+ white-space: nowrap;
897
+ }
898
+
899
+ .cpk-td__tab:hover {
900
+ color: #010507;
901
+ }
902
+
903
+ .cpk-td__tab--active {
904
+ color: #010507;
905
+ border-bottom-color: #bec2ff;
906
+ }
907
+
908
+ /* Toggle is a separate control, not a tab — so it does NOT use the
909
+ tabs' bottom-border active indicator. Instead, a subtle filled
910
+ state communicates "the drawer is open," and a vertical separator
911
+ on the left visually divorces it from the tab group. */
912
+ .cpk-td__panel-toggle {
913
+ margin-left: auto;
914
+ align-self: stretch;
915
+ display: flex;
916
+ align-items: center;
917
+ justify-content: center;
918
+ padding: 0 12px;
919
+ border: none;
920
+ border-left: 1px solid #dbdbe5;
921
+ background: transparent;
922
+ color: #838389;
923
+ cursor: pointer;
924
+ flex-shrink: 0;
925
+ transition:
926
+ color 0.12s,
927
+ background 0.12s;
928
+ }
929
+ .cpk-td__panel-toggle:hover {
930
+ color: #010507;
931
+ background: #f4f4f9;
932
+ }
933
+ .cpk-td__panel-toggle--active {
934
+ color: #5558b2;
935
+ background: #eee6fe;
936
+ }
937
+ .cpk-td__panel-toggle--active:hover {
938
+ background: #e4d8fc;
939
+ }
940
+
941
+ /* ── Scrollable content ──────────────────────────────────────────── */
942
+ .cpk-td__content {
943
+ flex: 1;
944
+ overflow-y: auto;
945
+ padding: 16px;
946
+ display: flex;
947
+ flex-direction: column;
948
+ gap: 8px;
949
+ }
950
+
951
+ /* Pin direct children so expanded tool bodies don't get flex-shrunk. */
952
+ .cpk-td__content > * {
953
+ flex-shrink: 0;
954
+ }
955
+
956
+ /*
957
+ * Each tab's content is wrapped in this panel so the keep-mounted
958
+ * inactive panels can be hidden via display:none without disturbing
959
+ * the gap between visible siblings. The flex column + gap gives each
960
+ * conversation item / event row breathing room (the cpk-td__content
961
+ * rule above no longer reaches them now that they are nested inside
962
+ * the per-panel wrapper).
963
+ */
964
+ .cpk-td__panel {
965
+ display: flex;
966
+ flex-direction: column;
967
+ gap: 12px;
968
+ }
969
+ .cpk-td__panel > * {
970
+ flex-shrink: 0;
971
+ }
972
+
973
+ /* ── Empty state ─────────────────────────────────────────────────── */
974
+ .cpk-td__empty-state {
975
+ flex: 1;
976
+ display: flex;
977
+ flex-direction: column;
978
+ align-items: center;
979
+ justify-content: center;
980
+ gap: 8px;
981
+ color: #838389;
982
+ font-size: 13px;
983
+ padding: 40px 0;
984
+ }
985
+
986
+ .cpk-td__empty-hint {
987
+ font-size: 11px;
988
+ color: #838389;
989
+ text-align: center;
990
+ max-width: 220px;
991
+ line-height: 1.5;
992
+ }
993
+
994
+ /* ── Status messages ─────────────────────────────────────────────── */
995
+ .cpk-td__status {
996
+ padding: 16px;
997
+ font-size: 12px;
998
+ color: #838389;
999
+ text-align: center;
1000
+ }
1001
+
1002
+ .cpk-td__status--error {
1003
+ color: #c0333a;
1004
+ }
1005
+
1006
+ /* ── Conversation bubbles ────────────────────────────────────────── */
1007
+ .cpk-td__bubble {
1008
+ display: flex;
1009
+ margin-bottom: 2px;
1010
+ }
1011
+
1012
+ .cpk-td__bubble--user {
1013
+ justify-content: flex-end;
1014
+ }
1015
+
1016
+ .cpk-td__bubble--assistant {
1017
+ justify-content: flex-start;
1018
+ }
1019
+
1020
+ .cpk-td__bubble-inner {
1021
+ padding: 9px 14px;
1022
+ max-width: 75%;
1023
+ font-size: 13px;
1024
+ line-height: 1.55;
1025
+ }
1026
+
1027
+ .cpk-td__bubble-inner--user {
1028
+ background: #eee6fe;
1029
+ color: #57575b;
1030
+ border-radius: 10px 10px 3px 10px;
1031
+ }
1032
+
1033
+ .cpk-td__show-more {
1034
+ display: inline-block;
1035
+ margin-top: 4px;
1036
+ font-size: 11px;
1037
+ font-weight: 500;
1038
+ color: #57575b;
1039
+ cursor: pointer;
1040
+ text-decoration: underline;
1041
+ text-underline-offset: 2px;
1042
+ }
1043
+
1044
+ .cpk-td__bubble-inner--assistant {
1045
+ background: #f7f7f9;
1046
+ color: #010507;
1047
+ border-radius: 10px 10px 10px 3px;
1048
+ border: 1px solid #e9e9ef;
1049
+ }
1050
+
1051
+ /* ── Tool call blocks ────────────────────────────────────────────── */
1052
+ .cpk-td__tool-block {
1053
+ border: 1px solid #e9e9ef;
1054
+ border-radius: 6px;
1055
+ overflow: hidden;
1056
+ }
1057
+
1058
+ .cpk-td__tool-header {
1059
+ display: flex;
1060
+ align-items: center;
1061
+ gap: 6px;
1062
+ padding: 6px 10px;
1063
+ background: rgba(133, 236, 206, 0.15);
1064
+ cursor: pointer;
1065
+ font-size: 11px;
1066
+ user-select: none;
1067
+ }
1068
+
1069
+ .cpk-td__tool-header:hover {
1070
+ background: rgba(133, 236, 206, 0.22);
1071
+ }
1072
+
1073
+ .cpk-td__tool-name {
1074
+ font-family: "Spline Sans Mono", monospace;
1075
+ font-size: 10px;
1076
+ font-weight: 500;
1077
+ color: #189370;
1078
+ text-transform: uppercase;
1079
+ flex: 1;
1080
+ }
1081
+
1082
+ .cpk-td__tool-status {
1083
+ font-family: "Spline Sans Mono", monospace;
1084
+ font-size: 9px;
1085
+ text-transform: uppercase;
1086
+ color: #189370;
1087
+ }
1088
+
1089
+ .cpk-td__tool-status--pending {
1090
+ color: #996300;
1091
+ }
1092
+
1093
+ .cpk-td__tool-chevron {
1094
+ color: #838389;
1095
+ font-size: 10px;
1096
+ }
1097
+
1098
+ .cpk-td__tool-body {
1099
+ padding: 8px 10px;
1100
+ border-top: 1px solid #e9e9ef;
1101
+ background: #ffffff;
1102
+ }
1103
+
1104
+ .cpk-td__tool-section-label {
1105
+ font-family: "Spline Sans Mono", monospace;
1106
+ font-size: 9px;
1107
+ font-weight: 500;
1108
+ color: #838389;
1109
+ text-transform: uppercase;
1110
+ margin-bottom: 4px;
1111
+ letter-spacing: 0.3px;
1112
+ }
1113
+
1114
+ .cpk-td__tool-pre {
1115
+ margin: 0;
1116
+ font-family: "Spline Sans Mono", monospace;
1117
+ font-size: 10px;
1118
+ background: #f7f7f9;
1119
+ padding: 6px 8px;
1120
+ border-radius: 4px;
1121
+ overflow-x: auto;
1122
+ white-space: pre-wrap;
1123
+ word-break: break-all;
1124
+ color: #010507;
1125
+ line-height: 1.6;
1126
+ }
1127
+
1128
+ /* ── Tool call group ─────────────────────────────────────────────── */
1129
+ .cpk-td__tool-group {
1130
+ border: 1px solid #e9e9ef;
1131
+ border-radius: 6px;
1132
+ overflow: hidden;
1133
+ }
1134
+
1135
+ .cpk-td__tool-group-header {
1136
+ padding: 5px 10px;
1137
+ background: rgba(133, 236, 206, 0.15);
1138
+ font-family: "Spline Sans Mono", monospace;
1139
+ font-size: 10px;
1140
+ color: #189370;
1141
+ text-transform: uppercase;
1142
+ font-weight: 500;
1143
+ border-bottom: 1px solid #e9e9ef;
1144
+ }
1145
+
1146
+ .cpk-td__tool-group .cpk-td__tool-block {
1147
+ border: none;
1148
+ border-bottom: 1px solid #e9e9ef;
1149
+ border-radius: 0;
1150
+ }
1151
+
1152
+ .cpk-td__tool-group .cpk-td__tool-block:last-child {
1153
+ border-bottom: none;
1154
+ }
1155
+
1156
+ /* ── Inline chips (reasoning / state update) ─────────────────────── */
1157
+ .cpk-td__inline-chip {
1158
+ display: flex;
1159
+ align-items: center;
1160
+ gap: 8px;
1161
+ padding: 5px 0;
1162
+ color: #838389;
1163
+ font-family: "Spline Sans Mono", monospace;
1164
+ font-size: 9px;
1165
+ text-transform: uppercase;
1166
+ }
1167
+
1168
+ .cpk-td__inline-chip::before,
1169
+ .cpk-td__inline-chip::after {
1170
+ content: "";
1171
+ flex: 1;
1172
+ height: 1px;
1173
+ background: #e9e9ef;
1174
+ }
1175
+
1176
+ /* ── Generative UI ──────────────────────────────────────────────── */
1177
+ @keyframes cpk-genui-enter {
1178
+ from {
1179
+ opacity: 0;
1180
+ transform: translateY(8px);
1181
+ }
1182
+ to {
1183
+ opacity: 1;
1184
+ transform: translateY(0);
1185
+ }
1186
+ }
1187
+
1188
+ .cpk-td__genui {
1189
+ display: flex;
1190
+ flex-direction: column;
1191
+ gap: 6px;
1192
+ padding: 4px 16px 8px;
1193
+ animation: cpk-genui-enter 0.25s cubic-bezier(0.16, 1, 0.3, 1) both;
1194
+ }
1195
+
1196
+ .cpk-td__genui-badge {
1197
+ display: inline-flex;
1198
+ align-items: center;
1199
+ gap: 4px;
1200
+ padding: 2px 8px;
1201
+ border-radius: 4px;
1202
+ background: #eee6fe;
1203
+ color: #57575b;
1204
+ font-size: 10px;
1205
+ font-weight: 600;
1206
+ align-self: flex-start;
1207
+ }
1208
+
1209
+ .cpk-td__genui-card {
1210
+ overflow: hidden;
1211
+ border-radius: 12px;
1212
+ border: 1px solid #e2e8f0;
1213
+ background: #fff;
1214
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
1215
+ }
1216
+
1217
+ .cpk-td__genui-placeholder {
1218
+ padding: 8px 12px;
1219
+ border-radius: 8px;
1220
+ border: 1px solid #ede9fe;
1221
+ background: #f5f3ff;
1222
+ color: #7c3aed;
1223
+ font-size: 11px;
1224
+ }
1225
+
1226
+ /* ── AG-UI Events ────────────────────────────────────────────────── */
1227
+ .cpk-td__event {
1228
+ flex-shrink: 0;
1229
+ border: 1px solid #e9e9ef;
1230
+ border-radius: 6px;
1231
+ overflow: hidden;
1232
+ /*
1233
+ * content-visibility: auto lets the browser skip layout + paint for
1234
+ * off-screen events while keeping them in the DOM (so scroll size
1235
+ * stays correct). Without this, switching back to AG-UI Events on a
1236
+ * thread with hundreds of events triggers a full layout pass over
1237
+ * every event row, which on Martha's intelligence-backed example
1238
+ * shows up as a multi-second freeze each time the panel becomes
1239
+ * visible. The intrinsic-size hint avoids the visible jump as the
1240
+ * browser swaps in real heights when items scroll into view.
1241
+ */
1242
+ content-visibility: auto;
1243
+ contain-intrinsic-size: 0 80px;
1244
+ }
1245
+
1246
+ .cpk-td__event-header {
1247
+ display: flex;
1248
+ justify-content: space-between;
1249
+ align-items: center;
1250
+ padding: 5px 10px;
1251
+ }
1252
+
1253
+ .cpk-td__event-type {
1254
+ font-family: "Spline Sans Mono", monospace;
1255
+ font-size: 9px;
1256
+ font-weight: 500;
1257
+ text-transform: uppercase;
1258
+ }
1259
+
1260
+ .cpk-td__event-time {
1261
+ font-family: "Spline Sans Mono", monospace;
1262
+ font-size: 9px;
1263
+ color: #838389;
1264
+ }
1265
+
1266
+ .cpk-td__event-payload {
1267
+ margin: 0;
1268
+ font-family: "Spline Sans Mono", monospace;
1269
+ font-size: 10px;
1270
+ line-height: 1.6;
1271
+ white-space: pre-wrap;
1272
+ word-break: break-all;
1273
+ color: #57575b;
1274
+ padding: 8px 10px;
1275
+ border-top: 1px solid #e9e9ef;
1276
+ }
1277
+
1278
+ /* ── JSON block (agent state) ────────────────────────────────────── */
1279
+ .cpk-td__json-block {
1280
+ margin: 0;
1281
+ font-family: "Spline Sans Mono", monospace;
1282
+ font-size: 11px;
1283
+ line-height: 1.8;
1284
+ white-space: pre-wrap;
1285
+ word-break: break-all;
1286
+ color: #57575b;
1287
+ }
1288
+
1289
+ /* ── Resize divider ──────────────────────────────────────────────── */
1290
+ /* Floats over the drawer's left edge so the toggle and the drawer
1291
+ touch directly without a 4px flex-gap between them. The hit zone
1292
+ is wider than its visual hint to make it easy to grab. */
1293
+ .cpk-td__detail-divider {
1294
+ position: absolute;
1295
+ top: 0;
1296
+ bottom: 0;
1297
+ left: -3px;
1298
+ width: 7px;
1299
+ cursor: col-resize;
1300
+ background: transparent;
1301
+ z-index: 5;
1302
+ }
1303
+
1304
+ .cpk-td__detail-divider:hover {
1305
+ background: rgba(190, 194, 255, 0.3);
1306
+ }
1307
+
1308
+ /* ── Right detail panel ──────────────────────────────────────────── */
1309
+ .cpk-td__detail {
1310
+ flex-shrink: 0;
1311
+ overflow: hidden;
1312
+ background: #f7f7f9;
1313
+ display: flex;
1314
+ flex-direction: column;
1315
+ gap: 0;
1316
+ padding: 0;
1317
+ box-sizing: border-box;
1318
+ position: relative;
1319
+ /* Slide open/closed via width + padding transition. When closed,
1320
+ width and padding are 0 so the drawer fully collapses. */
1321
+ transition:
1322
+ width 220ms cubic-bezier(0.4, 0, 0.2, 1),
1323
+ padding 220ms cubic-bezier(0.4, 0, 0.2, 1);
1324
+ }
1325
+
1326
+ .cpk-td__detail[data-open="true"] {
1327
+ overflow-y: auto;
1328
+ padding: 16px;
1329
+ }
1330
+
1331
+ .cpk-tdp__section-title {
1332
+ font-family: "Spline Sans Mono", monospace;
1333
+ font-size: 10px;
1334
+ font-weight: 500;
1335
+ color: #838389;
1336
+ text-transform: uppercase;
1337
+ letter-spacing: 0.6px;
1338
+ margin-bottom: 8px;
1339
+ }
1340
+
1341
+ .cpk-tdp__divider {
1342
+ height: 1px;
1343
+ background: #dbdbe5;
1344
+ margin: 14px 0;
1345
+ }
1346
+
1347
+ .cpk-tdp__row {
1348
+ display: flex;
1349
+ justify-content: space-between;
1350
+ align-items: flex-start;
1351
+ padding: 3px 0;
1352
+ gap: 8px;
1353
+ }
1354
+
1355
+ .cpk-tdp__label {
1356
+ color: #838389;
1357
+ font-size: 11px;
1358
+ white-space: nowrap;
1359
+ flex-shrink: 0;
1360
+ }
1361
+
1362
+ .cpk-tdp__value {
1363
+ color: #010507;
1364
+ font-family: "Spline Sans Mono", monospace;
1365
+ font-size: 11px;
1366
+ text-align: right;
1367
+ min-width: 0;
1368
+ }
1369
+
1370
+ .cpk-tdp__value--truncate {
1371
+ overflow: hidden;
1372
+ text-overflow: ellipsis;
1373
+ white-space: nowrap;
1374
+ max-width: 130px;
1375
+ }
1376
+
1377
+ .cpk-tdp__value--wrap {
1378
+ white-space: normal;
1379
+ word-break: break-all;
1380
+ text-align: right;
1381
+ }
1382
+ `;
1383
+
1384
+ updated(_changed: Map<string, unknown>): void {
1385
+ if (this.threadId !== this._lastFetchedThreadId) {
1386
+ this._lastFetchedThreadId = this.threadId;
1387
+ this._lastSeenLiveMessageVersion = this.liveMessageVersion;
1388
+ this._tab = "conversation";
1389
+ this._activatedTabs = new Set(["conversation"]);
1390
+ this._panelTplCache = new Map();
1391
+ this._expandedTools = new Set();
1392
+ this._expandedMessages = new Set();
1393
+ this._messagesAbort?.abort();
1394
+ this._messagesAbort = null;
1395
+ this._eventsAbort?.abort();
1396
+ this._eventsAbort = null;
1397
+ this._stateAbort?.abort();
1398
+ this._stateAbort = null;
1399
+ // Reset cleared so the next click into events/state triggers a fresh
1400
+ // fetch. Eagerly clear `_fetchedEvents` / `_fetchedState` so the empty
1401
+ // state doesn't briefly show last thread's data.
1402
+ this._eventsFetched = false;
1403
+ this._stateFetched = false;
1404
+ this._fetchedEvents = null;
1405
+ this._fetchedState = null;
1406
+
1407
+ if (this.threadId) {
1408
+ // Conversation is the default tab and shows immediately on thread
1409
+ // open, so fetch eagerly. Events and state are only visible once the
1410
+ // user clicks their sub-tab; deferring those fetches prevents a long
1411
+ // JSON.parse of a large events payload from blocking the main thread
1412
+ // before the user has even shown intent to view them.
1413
+ void this.fetchMessages(this.threadId);
1414
+ } else {
1415
+ this._conversation = [];
1416
+ }
1417
+ } else if (
1418
+ this.threadId &&
1419
+ this.liveMessageVersion !== this._lastSeenLiveMessageVersion
1420
+ ) {
1421
+ // Same thread, but the parent inspector signalled new agent-emitted
1422
+ // messages on this thread (via `liveMessageVersion`). Re-fetch the
1423
+ // canonical conversation from the runtime so streaming output flows
1424
+ // into the view without us reimplementing AG-UI → ConversationItem
1425
+ // mapping in the parent. `silent: true` so the loading-state indicator
1426
+ // doesn't flash between every streaming chunk and we keep the
1427
+ // last-good view on transient fetch errors.
1428
+ this._lastSeenLiveMessageVersion = this.liveMessageVersion;
1429
+ this._messagesAbort?.abort();
1430
+ this._messagesAbort = null;
1431
+ void this.fetchMessages(this.threadId, true);
1432
+ }
1433
+ }
1434
+
1435
+ /**
1436
+ * Fetch the canonical conversation for `threadId` from the runtime.
1437
+ *
1438
+ * `silent` is true for live re-fetches triggered by `liveMessageVersion`
1439
+ * bumps during streaming. In that mode we never toggle the loading state
1440
+ * (which would flash "Loading messages…" between every message) and we
1441
+ * keep the previous conversation on transient errors instead of blanking
1442
+ * it. Initial threadId-change fetches use the default (`silent=false`)
1443
+ * so users see an explicit loading indicator on first load.
1444
+ */
1445
+ private async fetchMessages(
1446
+ threadId: string,
1447
+ silent: boolean = false,
1448
+ ): Promise<void> {
1449
+ if (!this.runtimeUrl) {
1450
+ if (!silent) this._conversation = [];
1451
+ return;
1452
+ }
1453
+ const controller = new AbortController();
1454
+ this._messagesAbort = controller;
1455
+ if (!silent) {
1456
+ this._loadingMessages = true;
1457
+ this._messagesError = null;
1458
+ }
1459
+ try {
1460
+ const res = await fetch(
1461
+ `${this.runtimeUrl}/threads/${encodeURIComponent(threadId)}/messages`,
1462
+ { headers: { ...this.headers }, signal: controller.signal },
1463
+ );
1464
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1465
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1466
+ const data = (await res.json()) as { messages: ApiThreadMessage[] };
1467
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1468
+ this._conversation = this.mapMessages(data.messages);
1469
+ } catch (err) {
1470
+ if (err instanceof Error && err.name === "AbortError") return;
1471
+ if (!silent) {
1472
+ this._messagesError =
1473
+ err instanceof Error ? err.message : "Failed to load messages";
1474
+ this._conversation = [];
1475
+ }
1476
+ // Silent mode: keep last-good conversation, don't surface the error.
1477
+ // The next successful live re-fetch will recover automatically.
1478
+ } finally {
1479
+ if (!silent && !controller.signal.aborted) {
1480
+ this._loadingMessages = false;
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ private async fetchEvents(threadId: string): Promise<void> {
1486
+ this._eventsNotAvailable = false;
1487
+ if (!this.runtimeUrl) {
1488
+ this._fetchedEvents = null;
1489
+ return;
1490
+ }
1491
+ const controller = new AbortController();
1492
+ this._eventsAbort = controller;
1493
+ this._loadingEvents = true;
1494
+ this._eventsError = null;
1495
+ try {
1496
+ const res = await fetch(
1497
+ `${this.runtimeUrl}/threads/${encodeURIComponent(threadId)}/events`,
1498
+ { headers: { ...this.headers }, signal: controller.signal },
1499
+ );
1500
+ // Drop results if a newer fetch superseded this one (thread switched
1501
+ // mid-flight). Without this, switching A→B can leave thread B's view
1502
+ // showing thread A's events when A's request resolves last.
1503
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1504
+ if (res.status === 501) {
1505
+ // Endpoint not supported on this runtime (e.g. Intelligence platform).
1506
+ // Mark unavailable so we don't misleadingly fall back to the parent's
1507
+ // live agent events — those are agent-keyed, not thread-keyed, and
1508
+ // would render identical across every thread on the same agent.
1509
+ this._eventsNotAvailable = true;
1510
+ this._fetchedEvents = null;
1511
+ return;
1512
+ }
1513
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1514
+ const data = (await res.json()) as {
1515
+ events: Array<Record<string, unknown>>;
1516
+ };
1517
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1518
+ this._fetchedEvents = this.mapApiEvents(data.events);
1519
+ } catch (err) {
1520
+ if (err instanceof Error && err.name === "AbortError") return;
1521
+ if (this.threadId !== threadId) return;
1522
+ this._eventsError =
1523
+ err instanceof Error ? err.message : "Failed to load events";
1524
+ this._fetchedEvents = [];
1525
+ } finally {
1526
+ if (!controller.signal.aborted && this.threadId === threadId) {
1527
+ this._loadingEvents = false;
1528
+ }
1529
+ }
1530
+ }
1531
+
1532
+ private async fetchState(threadId: string): Promise<void> {
1533
+ this._stateNotAvailable = false;
1534
+ if (!this.runtimeUrl) {
1535
+ this._fetchedState = null;
1536
+ return;
1537
+ }
1538
+ const controller = new AbortController();
1539
+ this._stateAbort = controller;
1540
+ this._loadingState = true;
1541
+ this._stateError = null;
1542
+ try {
1543
+ const res = await fetch(
1544
+ `${this.runtimeUrl}/threads/${encodeURIComponent(threadId)}/state`,
1545
+ { headers: { ...this.headers }, signal: controller.signal },
1546
+ );
1547
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1548
+ if (res.status === 501) {
1549
+ this._stateNotAvailable = true;
1550
+ this._fetchedState = null;
1551
+ return;
1552
+ }
1553
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1554
+ const data = (await res.json()) as {
1555
+ state: Record<string, unknown> | null;
1556
+ };
1557
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1558
+ this._fetchedState = data.state ?? null;
1559
+ } catch (err) {
1560
+ if (err instanceof Error && err.name === "AbortError") return;
1561
+ if (this.threadId !== threadId) return;
1562
+ this._stateError =
1563
+ err instanceof Error ? err.message : "Failed to load state";
1564
+ this._fetchedState = null;
1565
+ } finally {
1566
+ if (!controller.signal.aborted && this.threadId === threadId) {
1567
+ this._loadingState = false;
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ private mapMessages(messages: ApiThreadMessage[]): ConversationItem[] {
1573
+ const items: ConversationItem[] = [];
1574
+ const toolCallMap = new Map<string, ConversationToolCall>();
1575
+ for (const msg of messages) {
1576
+ if (msg.role === "user" && msg.content) {
1577
+ items.push({
1578
+ id: msg.id,
1579
+ type: "user",
1580
+ content: msg.content,
1581
+ createdAt: "",
1582
+ });
1583
+ } else if (msg.role === "assistant") {
1584
+ if (msg.toolCalls?.length) {
1585
+ for (const tc of msg.toolCalls) {
1586
+ let args: Record<string, unknown> = {};
1587
+ try {
1588
+ args = JSON.parse(tc.args) as Record<string, unknown>;
1589
+ } catch (err) {
1590
+ // Inspector is a debugging surface — surface malformed payloads
1591
+ // instead of silently substituting `{}`. The sentinel lets the
1592
+ // renderer flag "raw arguments — failed to parse" if/when it
1593
+ // grows that branch; the console.error gives anyone with the
1594
+ // devtools open immediate visibility into the offending blob.
1595
+ console.error(
1596
+ "[CopilotKit Inspector] Failed to parse tool-call arguments",
1597
+ { toolCallId: tc.id, raw: tc.args, error: err },
1598
+ );
1599
+ args = { __parseError: true, __raw: tc.args };
1600
+ }
1601
+ const item: ConversationToolCall = {
1602
+ id: tc.id,
1603
+ type: "tool_call",
1604
+ toolName: tc.name,
1605
+ toolCallId: tc.id,
1606
+ arguments: args,
1607
+ result: null,
1608
+ createdAt: "",
1609
+ };
1610
+ toolCallMap.set(tc.id, item);
1611
+ items.push(item);
1612
+ }
1613
+ }
1614
+ if (msg.content) {
1615
+ items.push({
1616
+ id: msg.id,
1617
+ type: "assistant",
1618
+ content: msg.content,
1619
+ createdAt: "",
1620
+ });
1621
+ }
1622
+ } else if (msg.role === "activity") {
1623
+ items.push({
1624
+ id: msg.id,
1625
+ type: "generative-ui",
1626
+ activityType: msg.activityType ?? "unknown",
1627
+ createdAt: "",
1628
+ });
1629
+ } else if (msg.role === "tool" && msg.toolCallId) {
1630
+ const tc = toolCallMap.get(msg.toolCallId);
1631
+ if (tc) {
1632
+ try {
1633
+ tc.result = JSON.parse(msg.content ?? "{}") as Record<
1634
+ string,
1635
+ unknown
1636
+ >;
1637
+ } catch (err) {
1638
+ // See the comment on the assistant tool-call args parse above —
1639
+ // same rationale, same sentinel shape so the renderer can treat
1640
+ // both consistently.
1641
+ console.error(
1642
+ "[CopilotKit Inspector] Failed to parse tool-call result content",
1643
+ { toolCallId: msg.toolCallId, raw: msg.content, error: err },
1644
+ );
1645
+ tc.result = { __parseError: true, __raw: msg.content ?? null };
1646
+ }
1647
+ }
1648
+ }
1649
+ }
1650
+ return items;
1651
+ }
1652
+
1653
+ private mapApiEvents(
1654
+ events: Array<Record<string, unknown>>,
1655
+ ): ApiAgentEvent[] {
1656
+ return events.map((event) => {
1657
+ const { type, timestamp, ...rest } = event;
1658
+ return {
1659
+ type: typeof type === "string" ? type : "UNKNOWN",
1660
+ timestamp:
1661
+ typeof timestamp === "string" || typeof timestamp === "number"
1662
+ ? timestamp
1663
+ : Date.now(),
1664
+ payload: rest,
1665
+ };
1666
+ });
1667
+ }
1668
+
1669
+ private get renderItems(): RenderItem[] {
1670
+ const items = this._conversation;
1671
+ const result: RenderItem[] = [];
1672
+ const seen = new Set<string>();
1673
+ for (const item of items) {
1674
+ if (item.type === "agent_responded") continue;
1675
+ if (item.type !== "tool_call" || !item.groupId) {
1676
+ result.push(item);
1677
+ continue;
1678
+ }
1679
+ if (seen.has(item.groupId)) continue;
1680
+ seen.add(item.groupId);
1681
+ const group: ToolCallGroup = {
1682
+ type: "tool_call_group",
1683
+ id: item.groupId,
1684
+ items: items.filter(
1685
+ (i): i is ConversationToolCall =>
1686
+ i.type === "tool_call" && i.groupId === item.groupId,
1687
+ ),
1688
+ };
1689
+ result.push(group);
1690
+ }
1691
+ return result;
1692
+ }
1693
+
1694
+ private get activityCounts(): {
1695
+ messages: number;
1696
+ toolCalls: number;
1697
+ generativeUi: number;
1698
+ } {
1699
+ let messages = 0;
1700
+ let toolCalls = 0;
1701
+ let generativeUi = 0;
1702
+ for (const item of this._conversation) {
1703
+ if (item.type === "user" || item.type === "assistant") messages++;
1704
+ if (item.type === "tool_call") toolCalls++;
1705
+ if (item.type === "generative-ui") generativeUi++;
1706
+ }
1707
+ return { messages, toolCalls, generativeUi };
1708
+ }
1709
+
1710
+ private get duration(): string {
1711
+ const t = this.thread;
1712
+ if (!t?.createdAt || !t?.updatedAt) return "—";
1713
+ const ms =
1714
+ new Date(t.updatedAt).getTime() - new Date(t.createdAt).getTime();
1715
+ if (ms < 0) return "—";
1716
+ if (ms < 1000) return `${ms}ms`;
1717
+ const s = Math.floor(ms / 1000);
1718
+ if (s < 60) return `${s}s`;
1719
+ const m = Math.floor(s / 60);
1720
+ const rs = s % 60;
1721
+ return `${m}m ${rs}s`;
1722
+ }
1723
+
1724
+ private toggleToolExpand(id: string): void {
1725
+ const next = new Set(this._expandedTools);
1726
+ if (next.has(id)) next.delete(id);
1727
+ else next.add(id);
1728
+ this._expandedTools = next;
1729
+ }
1730
+
1731
+ private toggleMessageExpand(id: string): void {
1732
+ const next = new Set(this._expandedMessages);
1733
+ if (next.has(id)) next.delete(id);
1734
+ else next.add(id);
1735
+ this._expandedMessages = next;
1736
+ }
1737
+
1738
+ private get activeEvents(): ApiAgentEvent[] {
1739
+ // When the endpoint explicitly returned 501 we report no events rather
1740
+ // than leaking the parent's agent-keyed live events across historical
1741
+ // threads (those would render identically for every thread on the same
1742
+ // agent and mislead the reader).
1743
+ if (this._eventsNotAvailable) return [];
1744
+ return this._fetchedEvents ?? this.agentEventsInput ?? [];
1745
+ }
1746
+
1747
+ private get activeState(): Record<string, unknown> | null {
1748
+ if (this._stateNotAvailable) return null;
1749
+ return this._fetchedState ?? this.agentStateInput ?? null;
1750
+ }
1751
+
1752
+ private hasRenderableState(): boolean {
1753
+ const s = this.activeState;
1754
+ return !!s && typeof s === "object" && Object.keys(s).length > 0;
1755
+ }
1756
+
1757
+ private shortId(id: string | null | undefined): string {
1758
+ if (!id) return "—";
1759
+ return id.length > 20 ? id.slice(0, 8) + "…" : id;
1760
+ }
1761
+
1762
+ private fmtTime(dateStr: string | null | undefined): string {
1763
+ if (!dateStr) return "—";
1764
+ const d = new Date(dateStr);
1765
+ if (Number.isNaN(d.getTime())) return "—";
1766
+ return d.toLocaleTimeString("en-US", {
1767
+ hour: "2-digit",
1768
+ minute: "2-digit",
1769
+ second: "2-digit",
1770
+ hour12: false,
1771
+ });
1772
+ }
1773
+
1774
+ private onDetailDividerDown = (event: PointerEvent): void => {
1775
+ this._dividerResizing = true;
1776
+ this._dividerPointerId = event.pointerId;
1777
+ this._dividerStartX = event.clientX;
1778
+ this._dividerStartWidth = this._detailPanelWidth;
1779
+ (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
1780
+ event.preventDefault();
1781
+ };
1782
+
1783
+ private onDetailDividerMove = (event: PointerEvent): void => {
1784
+ if (!this._dividerResizing || this._dividerPointerId !== event.pointerId)
1785
+ return;
1786
+ const delta = this._dividerStartX - event.clientX;
1787
+ this._detailPanelWidth = Math.max(
1788
+ 160,
1789
+ Math.min(400, this._dividerStartWidth + delta),
1790
+ );
1791
+ };
1792
+
1793
+ private onDetailDividerUp = (event: PointerEvent): void => {
1794
+ if (this._dividerPointerId !== event.pointerId) return;
1795
+ const target = event.currentTarget as HTMLElement;
1796
+ if (target.hasPointerCapture(this._dividerPointerId)) {
1797
+ target.releasePointerCapture(this._dividerPointerId);
1798
+ }
1799
+ this._dividerResizing = false;
1800
+ };
1801
+
1802
+ render() {
1803
+ return html`
1804
+ <div class="cpk-td">
1805
+ <!-- ── Left area: tabs + content ─────────────────────────────────── -->
1806
+ <div class="cpk-td__left">
1807
+ <!-- Tab bar -->
1808
+ <div class="cpk-td__tabs-header">
1809
+ <div class="cpk-td__tab-group" role="tablist">
1810
+ ${ɵCpkThreadDetails.TAB_LIST.map(
1811
+ (tab) => html`
1812
+ <button
1813
+ role="tab"
1814
+ class="cpk-td__tab ${
1815
+ this._tab === tab.id ? "cpk-td__tab--active" : ""
1816
+ }"
1817
+ @click=${() => this.activateTab(tab.id)}
1818
+ >
1819
+ ${tab.label}
1820
+ </button>
1821
+ `,
1822
+ )}
1823
+ </div>
1824
+ ${this.renderPanelToggle()}
1825
+ </div>
1826
+
1827
+ <!-- Scrollable content -->
1828
+ <div class="cpk-td__content">
1829
+ ${
1830
+ this._panelInitializing
1831
+ ? html`
1832
+ <div class="cpk-td__status">Loading…</div>
1833
+ `
1834
+ : nothing
1835
+ }
1836
+ ${ɵCpkThreadDetails.TAB_LIST.map((tab) =>
1837
+ this._activatedTabs.has(tab.id)
1838
+ ? html`<div
1839
+ class="cpk-td__panel"
1840
+ style=${
1841
+ this._tab === tab.id && !this._panelInitializing
1842
+ ? ""
1843
+ : "display:none"
1844
+ }
1845
+ >
1846
+ ${this.renderTabContent(tab.id)}
1847
+ </div>`
1848
+ : nothing,
1849
+ )}
1850
+ </div>
1851
+ </div>
1852
+
1853
+ <!--
1854
+ Drawer always rendered so width animates between 0 and its
1855
+ target. Divider lives INSIDE the drawer and is absolutely
1856
+ positioned over its left edge so the toggle (rightmost of the
1857
+ tab row) and the drawer touch with no flex-gap between them.
1858
+ -->
1859
+ <div
1860
+ class="cpk-td__detail"
1861
+ data-open=${this._showDetailPanel ? "true" : "false"}
1862
+ style="width:${this._showDetailPanel ? this._detailPanelWidth : 0}px"
1863
+ aria-hidden=${this._showDetailPanel ? "false" : "true"}
1864
+ >
1865
+ ${
1866
+ this._showDetailPanel
1867
+ ? html`
1868
+ <div
1869
+ class="cpk-td__detail-divider"
1870
+ @pointerdown=${this.onDetailDividerDown}
1871
+ @pointermove=${this.onDetailDividerMove}
1872
+ @pointerup=${this.onDetailDividerUp}
1873
+ @pointercancel=${this.onDetailDividerUp}
1874
+ ></div>
1875
+ `
1876
+ : nothing
1877
+ }
1878
+ ${this.renderDetailPanel()}
1879
+ </div>
1880
+ </div>
1881
+ `;
1882
+ }
1883
+
1884
+ private renderConversation() {
1885
+ if (this._loadingMessages) {
1886
+ return html`
1887
+ <div class="cpk-td__status">Loading messages…</div>
1888
+ `;
1889
+ }
1890
+ if (this._messagesError) {
1891
+ return html`<div class="cpk-td__status cpk-td__status--error">
1892
+ ${this._messagesError}
1893
+ </div>`;
1894
+ }
1895
+ if (this._conversation.length === 0) {
1896
+ return html`
1897
+ <div class="cpk-td__empty-state">
1898
+ <svg
1899
+ width="28"
1900
+ height="28"
1901
+ viewBox="0 0 24 24"
1902
+ fill="none"
1903
+ stroke="currentColor"
1904
+ stroke-width="1.5"
1905
+ stroke-linecap="round"
1906
+ stroke-linejoin="round"
1907
+ >
1908
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
1909
+ </svg>
1910
+ <span>No messages yet</span>
1911
+ </div>
1912
+ `;
1913
+ }
1914
+ // Expand state is part of the cache key because clicking a tool-call
1915
+ // header or the "Show more" button on a long message replaces
1916
+ // `_expandedTools` / `_expandedMessages` without touching
1917
+ // `_conversation` — without those keys the cache returns the
1918
+ // pre-toggle template and the disclosure appears broken.
1919
+ return this.cachedPanelTpl(
1920
+ "conversation",
1921
+ [this._conversation, this._expandedTools, this._expandedMessages],
1922
+ () => {
1923
+ const items = this.renderItems;
1924
+ return html`${items.map((item) => this.renderRenderItem(item))}`;
1925
+ },
1926
+ );
1927
+ }
1928
+
1929
+ /**
1930
+ * Memoize the rendered TemplateResult for `slot` keyed by tuple
1931
+ * element-wise reference equality. The hot path for tab switches: when
1932
+ * the underlying data hasn't changed, return the previously built
1933
+ * TemplateResult so Lit's diff short-circuits. Each panel's `key` is
1934
+ * the tuple of inputs the template reads — pass everything the template
1935
+ * depends on, or the cache will return stale output when those inputs
1936
+ * change without the listed key flipping.
1937
+ */
1938
+ private cachedPanelTpl(
1939
+ slot: ThreadDetailsTab,
1940
+ key: readonly unknown[],
1941
+ build: () => TemplateResult,
1942
+ ): TemplateResult {
1943
+ const cached = this._panelTplCache.get(slot);
1944
+ if (
1945
+ cached &&
1946
+ cached.key.length === key.length &&
1947
+ cached.key.every((v, i) => v === key[i])
1948
+ ) {
1949
+ return cached.tpl;
1950
+ }
1951
+ const tpl = build();
1952
+ this._panelTplCache.set(slot, { key, tpl });
1953
+ return tpl;
1954
+ }
1955
+
1956
+ private renderRenderItem(item: RenderItem) {
1957
+ switch (item.type) {
1958
+ case "user":
1959
+ case "assistant":
1960
+ return this.renderBubble(item);
1961
+ case "tool_call":
1962
+ return this.renderToolBlock(item);
1963
+ case "tool_call_group":
1964
+ return this.renderToolGroup(item);
1965
+ case "reasoning":
1966
+ return html`<div class="cpk-td__inline-chip">
1967
+ <span>Reasoned for ${item.duration}</span>
1968
+ </div>`;
1969
+ case "state_update":
1970
+ return html`
1971
+ <div class="cpk-td__inline-chip">
1972
+ <span>Updated agent state</span>
1973
+ </div>
1974
+ `;
1975
+ case "generative-ui":
1976
+ return this.renderGenerativeUI(item);
1977
+ case "agent_responded":
1978
+ return nothing;
1979
+ }
1980
+ }
1981
+
1982
+ private renderBubble(item: ConversationUser | ConversationAssistant) {
1983
+ const isUser = item.type === "user";
1984
+ const threshold = ɵCpkThreadDetails.COLLAPSE_THRESHOLD;
1985
+ const expanded = this._expandedMessages.has(item.id);
1986
+ const tooLong = item.content.length > threshold;
1987
+ const shown =
1988
+ tooLong && !expanded
1989
+ ? item.content.slice(0, threshold) + "…"
1990
+ : item.content;
1991
+ return html`
1992
+ <div
1993
+ class="cpk-td__bubble ${
1994
+ isUser ? "cpk-td__bubble--user" : "cpk-td__bubble--assistant"
1995
+ }"
1996
+ >
1997
+ <div
1998
+ class="cpk-td__bubble-inner ${
1999
+ isUser
2000
+ ? "cpk-td__bubble-inner--user"
2001
+ : "cpk-td__bubble-inner--assistant"
2002
+ }"
2003
+ >
2004
+ ${shown}
2005
+ ${
2006
+ tooLong
2007
+ ? html`<span
2008
+ class="cpk-td__show-more"
2009
+ @click=${() => this.toggleMessageExpand(item.id)}
2010
+ >${expanded ? "Show less" : "Show more"}</span
2011
+ >`
2012
+ : nothing
2013
+ }
2014
+ </div>
2015
+ </div>
2016
+ `;
2017
+ }
2018
+
2019
+ private renderToolBlock(item: ConversationToolCall) {
2020
+ const expanded = this._expandedTools.has(item.id);
2021
+ return html`
2022
+ <div class="cpk-td__tool-block">
2023
+ <div
2024
+ class="cpk-td__tool-header"
2025
+ @click=${() => this.toggleToolExpand(item.id)}
2026
+ >
2027
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
2028
+ <path
2029
+ d="M1 9C1 9 2 7 5 7C8 7 9 9 9 9M5 1C5 1 7 2.5 7 4.5C7 6.5 5 7 5 7C5 7 3 6.5 3 4.5C3 2.5 5 1 5 1Z"
2030
+ stroke="#189370"
2031
+ stroke-width="1.2"
2032
+ stroke-linecap="round"
2033
+ stroke-linejoin="round"
2034
+ />
2035
+ </svg>
2036
+ <span class="cpk-td__tool-name">${item.toolName}</span>
2037
+ ${
2038
+ item.result || Object.keys(item.arguments).length > 0
2039
+ ? html`
2040
+ <span class="cpk-td__tool-status">DONE</span>
2041
+ `
2042
+ : html`
2043
+ <span class="cpk-td__tool-status cpk-td__tool-status--pending">PENDING</span>
2044
+ `
2045
+ }
2046
+ <span class="cpk-td__tool-chevron">${expanded ? "▾" : "▸"}</span>
2047
+ </div>
2048
+ ${
2049
+ expanded
2050
+ ? html`
2051
+ <div class="cpk-td__tool-body">
2052
+ <div class="cpk-td__tool-section-label">Arguments</div>
2053
+ <pre class="cpk-td__tool-pre">
2054
+ ${unsafeHTML(highlightedJson(item.arguments))}</pre
2055
+ >
2056
+ ${
2057
+ item.result
2058
+ ? html`
2059
+ <div
2060
+ class="cpk-td__tool-section-label"
2061
+ style="margin-top:8px"
2062
+ >
2063
+ Result
2064
+ </div>
2065
+ <pre class="cpk-td__tool-pre">
2066
+ ${unsafeHTML(highlightedJson(item.result))}</pre
2067
+ >
2068
+ `
2069
+ : nothing
2070
+ }
2071
+ </div>
2072
+ `
2073
+ : nothing
2074
+ }
2075
+ </div>
2076
+ `;
2077
+ }
2078
+
2079
+ private renderToolGroup(group: ToolCallGroup) {
2080
+ return html`
2081
+ <div class="cpk-td__tool-group">
2082
+ <div class="cpk-td__tool-group-header">
2083
+ ${group.items.length} tool call${group.items.length !== 1 ? "s" : ""}
2084
+ </div>
2085
+ ${group.items.map((tc) => this.renderToolBlock(tc))}
2086
+ </div>
2087
+ `;
2088
+ }
2089
+
2090
+ private renderGenerativeUI(item: ConversationGenerativeUIItem) {
2091
+ return html`
2092
+ <div class="cpk-td__genui">
2093
+ <div class="cpk-td__genui-badge">
2094
+ <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor">
2095
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
2096
+ </svg>
2097
+ Generative UI
2098
+ </div>
2099
+ <div class="cpk-td__genui-placeholder">
2100
+ ${item.activityType} — rendered in chat
2101
+ </div>
2102
+ </div>
2103
+ `;
2104
+ }
2105
+
2106
+ private renderState() {
2107
+ if (this._loadingState) {
2108
+ return html`
2109
+ <div class="cpk-td__status">Loading state…</div>
2110
+ `;
2111
+ }
2112
+ if (this._stateError) {
2113
+ return html`<div class="cpk-td__status cpk-td__status--error">
2114
+ ${this._stateError}
2115
+ </div>`;
2116
+ }
2117
+ if (this._stateNotAvailable) {
2118
+ return html`
2119
+ <div class="cpk-td__empty-state">
2120
+ <svg
2121
+ width="28"
2122
+ height="28"
2123
+ viewBox="0 0 24 24"
2124
+ fill="none"
2125
+ stroke="currentColor"
2126
+ stroke-width="1.5"
2127
+ stroke-linecap="round"
2128
+ stroke-linejoin="round"
2129
+ >
2130
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
2131
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
2132
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
2133
+ </svg>
2134
+ <span>State history not available</span>
2135
+ <span class="cpk-td__empty-hint"
2136
+ >This runtime doesn't yet expose per-thread agent state. Available when
2137
+ running against the in-memory runner.</span
2138
+ >
2139
+ </div>
2140
+ `;
2141
+ }
2142
+ if (!this.hasRenderableState()) {
2143
+ return html`
2144
+ <div class="cpk-td__empty-state">
2145
+ <svg
2146
+ width="28"
2147
+ height="28"
2148
+ viewBox="0 0 24 24"
2149
+ fill="none"
2150
+ stroke="currentColor"
2151
+ stroke-width="1.5"
2152
+ stroke-linecap="round"
2153
+ stroke-linejoin="round"
2154
+ >
2155
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
2156
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
2157
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
2158
+ </svg>
2159
+ <span>No state captured</span>
2160
+ <span class="cpk-td__empty-hint"
2161
+ >Emitted live from STATE_SNAPSHOT events.</span
2162
+ >
2163
+ </div>
2164
+ `;
2165
+ }
2166
+ const stateValue = this.activeState;
2167
+ return this.cachedPanelTpl("agent-state", [stateValue], () => {
2168
+ return html`<pre class="cpk-td__json-block">
2169
+ ${unsafeHTML(highlightedJson(stateValue))}</pre
2170
+ >`;
2171
+ });
2172
+ }
2173
+
2174
+ private renderEvents() {
2175
+ if (this._loadingEvents) {
2176
+ return html`
2177
+ <div class="cpk-td__status">Loading events…</div>
2178
+ `;
2179
+ }
2180
+ if (this._eventsError) {
2181
+ return html`<div class="cpk-td__status cpk-td__status--error">
2182
+ ${this._eventsError}
2183
+ </div>`;
2184
+ }
2185
+ if (this._eventsNotAvailable) {
2186
+ return html`
2187
+ <div class="cpk-td__empty-state">
2188
+ <span>Event history not available</span>
2189
+ <span class="cpk-td__empty-hint"
2190
+ >This runtime doesn't yet expose per-thread AG-UI events. Available when
2191
+ running against the in-memory runner.</span
2192
+ >
2193
+ </div>
2194
+ `;
2195
+ }
2196
+ const events = this.activeEvents;
2197
+ if (events.length === 0) {
2198
+ return html`
2199
+ <div class="cpk-td__empty-state">
2200
+ <span>No events captured</span>
2201
+ <span class="cpk-td__empty-hint"
2202
+ >Events are recorded live. Run the agent to see them here.</span
2203
+ >
2204
+ </div>
2205
+ `;
2206
+ }
2207
+ return this.cachedPanelTpl("ag-ui-events", [events], () => {
2208
+ return html`${events.map((event) => {
2209
+ const { bg, fg } = eventColors(event.type);
2210
+ return html`
2211
+ <div class="cpk-td__event">
2212
+ <div class="cpk-td__event-header" style="background:${bg}">
2213
+ <span class="cpk-td__event-type" style="color:${fg}"
2214
+ >${event.type}</span
2215
+ >
2216
+ <span class="cpk-td__event-time"
2217
+ >${formatTimestamp(event.timestamp)}</span
2218
+ >
2219
+ </div>
2220
+ <pre class="cpk-td__event-payload">
2221
+ ${unsafeHTML(highlightedJson(event.payload))}</pre
2222
+ >
2223
+ </div>
2224
+ `;
2225
+ })}`;
2226
+ });
2227
+ }
2228
+
2229
+ private renderPanelToggle() {
2230
+ return html`
2231
+ <button
2232
+ class="cpk-td__panel-toggle ${
2233
+ this._showDetailPanel ? "cpk-td__panel-toggle--active" : ""
2234
+ }"
2235
+ @click=${() => {
2236
+ this._showDetailPanel = !this._showDetailPanel;
2237
+ }}
2238
+ title="Toggle thread details"
2239
+ type="button"
2240
+ >
2241
+ <svg
2242
+ width="14"
2243
+ height="14"
2244
+ viewBox="0 0 24 24"
2245
+ fill="none"
2246
+ stroke="currentColor"
2247
+ stroke-width="2"
2248
+ stroke-linecap="round"
2249
+ stroke-linejoin="round"
2250
+ >
2251
+ <rect x="3" y="3" width="18" height="18" rx="2" />
2252
+ <line x1="15" y1="3" x2="15" y2="21" />
2253
+ </svg>
2254
+ </button>
2255
+ `;
2256
+ }
2257
+
2258
+ private renderDetailPanel() {
2259
+ const counts = this.activityCounts;
2260
+ return html`
2261
+ <!-- Thread -->
2262
+ <div class="cpk-tdp__section-title">Thread</div>
2263
+ <div class="cpk-tdp__row">
2264
+ <span class="cpk-tdp__label">ID</span>
2265
+ <span class="cpk-tdp__value cpk-tdp__value--wrap"
2266
+ >${this.shortId(this.thread?.id)}</span
2267
+ >
2268
+ </div>
2269
+ <div class="cpk-tdp__row">
2270
+ <span class="cpk-tdp__label">Name</span>
2271
+ <span class="cpk-tdp__value">${this.thread?.name ?? "—"}</span>
2272
+ </div>
2273
+ <div class="cpk-tdp__row">
2274
+ <span class="cpk-tdp__label">Agent</span>
2275
+ <span class="cpk-tdp__value cpk-tdp__value--truncate"
2276
+ >${this.thread?.agentId ?? "—"}</span
2277
+ >
2278
+ </div>
2279
+ <div class="cpk-tdp__row">
2280
+ <span class="cpk-tdp__label">Created by</span>
2281
+ <span class="cpk-tdp__value cpk-tdp__value--truncate"
2282
+ >${this.thread?.createdById ?? "—"}</span
2283
+ >
2284
+ </div>
2285
+
2286
+ <div class="cpk-tdp__divider"></div>
123
2287
 
124
- type InspectorToolCall = {
125
- id?: string;
126
- function?: {
127
- name?: string;
128
- arguments?: SanitizedValue | string;
129
- };
130
- toolName?: string;
131
- status?: string;
132
- };
2288
+ <!-- Timestamps -->
2289
+ <div class="cpk-tdp__section-title">Timestamps</div>
2290
+ <div class="cpk-tdp__row">
2291
+ <span class="cpk-tdp__label">Created</span>
2292
+ <span class="cpk-tdp__value">${this.fmtTime(this.thread?.createdAt)}</span>
2293
+ </div>
2294
+ <div class="cpk-tdp__row">
2295
+ <span class="cpk-tdp__label">Updated</span>
2296
+ <span class="cpk-tdp__value">${this.fmtTime(this.thread?.updatedAt)}</span>
2297
+ </div>
2298
+ <div class="cpk-tdp__row">
2299
+ <span class="cpk-tdp__label">Duration</span>
2300
+ <span class="cpk-tdp__value">${this.duration}</span>
2301
+ </div>
133
2302
 
134
- type InspectorMessage = {
135
- id?: string;
136
- role: string;
137
- contentText: string;
138
- contentRaw?: SanitizedValue;
139
- toolCalls: InspectorToolCall[];
140
- };
2303
+ <div class="cpk-tdp__divider"></div>
141
2304
 
142
- type InspectorToolDefinition = {
143
- agentId: string;
144
- name: string;
145
- description?: string;
146
- parameters?: unknown;
147
- type: "handler" | "renderer";
148
- };
2305
+ <!-- Activity -->
2306
+ <div class="cpk-tdp__section-title">Activity</div>
2307
+ <div class="cpk-tdp__row">
2308
+ <span class="cpk-tdp__label">Messages</span>
2309
+ <span class="cpk-tdp__value">${counts.messages}</span>
2310
+ </div>
2311
+ <div class="cpk-tdp__row">
2312
+ <span class="cpk-tdp__label">Tool calls</span>
2313
+ <span class="cpk-tdp__value">${counts.toolCalls}</span>
2314
+ </div>
2315
+ <div class="cpk-tdp__row">
2316
+ <span class="cpk-tdp__label">AG-UI events</span>
2317
+ <span class="cpk-tdp__value">${this.activeEvents.length}</span>
2318
+ </div>
2319
+ `;
2320
+ }
2321
+ }
149
2322
 
150
- type InspectorEvent = {
151
- id: string;
152
- agentId: string;
153
- type: InspectorAgentEventType;
154
- timestamp: number;
155
- payload: SanitizedValue;
156
- };
2323
+ if (!customElements.get("cpk-thread-list")) {
2324
+ customElements.define("cpk-thread-list", CpkThreadList);
2325
+ }
2326
+ if (!customElements.get("cpk-thread-details")) {
2327
+ customElements.define("cpk-thread-details", ɵCpkThreadDetails);
2328
+ }
157
2329
 
158
2330
  export class WebInspectorElement extends LitElement {
159
2331
  static properties = {
@@ -173,6 +2345,17 @@ export class WebInspectorElement extends LitElement {
173
2345
  private agentSubscriptions: Map<string, () => void> = new Map();
174
2346
  private agentEvents: Map<string, InspectorEvent[]> = new Map();
175
2347
  private agentMessages: Map<string, InspectorMessage[]> = new Map();
2348
+ // Tracks which thread each agent is currently running on. Populated from the
2349
+ // agent instance handed to `onAgentRunStarted` so that, when that agent
2350
+ // subsequently emits message changes, we can bump the per-thread live
2351
+ // version (below) only for the thread the messages actually belong to.
2352
+ private agentRunThreadId: Map<string, string> = new Map();
2353
+ // Per-thread monotonic version that ticks every time an agent currently
2354
+ // running on that thread emits a message change. `cpk-thread-details`
2355
+ // watches this prop and re-fetches `/threads/:id/messages` when it changes,
2356
+ // which is how live updates flow into the conversation view without
2357
+ // duplicating the runtime's message-shape conversion in the inspector.
2358
+ private liveMessageVersion: Map<string, number> = new Map();
176
2359
  private agentStates: Map<string, SanitizedValue> = new Map();
177
2360
  private flattenedEvents: InspectorEvent[] = [];
178
2361
  private eventCounter = 0;
@@ -190,6 +2373,22 @@ export class WebInspectorElement extends LitElement {
190
2373
  private draggedDuringInteraction = false;
191
2374
  private ignoreNextButtonClick = false;
192
2375
  private selectedMenu: MenuKey = "ag-ui-events";
2376
+ private selectedThreadId: string | null = null;
2377
+ private threadListWidth = 290;
2378
+ private threadDividerResizing = false;
2379
+ private threadDividerPointerId = -1;
2380
+ private threadDividerStartX = 0;
2381
+ private threadDividerStartWidth = 0;
2382
+ private _threads: ɵThread[] = [];
2383
+ private _threadStoreSubscriptions: Map<string, () => void> = new Map();
2384
+ private _threadsByAgent: Map<string, ɵThread[]> = new Map();
2385
+ // Error from each agent's thread store (REST list rejection, Phoenix
2386
+ // subscribe failure, retry exhaustion). When non-empty for the active
2387
+ // selection, the threads view renders an error state instead of stale
2388
+ // data with no indication.
2389
+ private _threadsErrorByAgent: Map<string, Error> = new Map();
2390
+ // Thread stores created and owned by the inspector (keyed by agentId)
2391
+ private _ownedThreadStores: Map<string, ɵThreadStore> = new Map();
193
2392
  private contextMenuOpen = false;
194
2393
  private dockMode: DockMode = "floating";
195
2394
  private previousBodyMargins: { left: string; bottom: string } | null = null;
@@ -203,16 +2402,22 @@ export class WebInspectorElement extends LitElement {
203
2402
  private toolSignature = "";
204
2403
  private eventFilterText = "";
205
2404
  private eventTypeFilter: InspectorAgentEventType | "all" = "all";
2405
+ // Column widths for the AG-UI events table (agent, time, event-type; last col is auto)
2406
+ private evtColWidths = [100, 80, 150];
2407
+ private _evtColResize: {
2408
+ col: number;
2409
+ startX: number;
2410
+ startW: number;
2411
+ } | null = null;
206
2412
 
207
- private announcementMarkdown: string | null = null;
208
2413
  private announcementHtml: string | null = null;
209
2414
  private announcementTimestamp: string | null = null;
210
2415
  private announcementPreviewText: string | null = null;
211
2416
  private hasUnseenAnnouncement = false;
212
2417
  private announcementLoaded = false;
213
- private announcementLoadError: unknown = null;
214
2418
  private announcementPromise: Promise<void> | null = null;
215
2419
  private showAnnouncementPreview = true;
2420
+ private announcementExpanded = false;
216
2421
 
217
2422
  get core(): CopilotKitCore | null {
218
2423
  return this._core;
@@ -259,12 +2464,140 @@ export class WebInspectorElement extends LitElement {
259
2464
  private resizeInitialSize: { width: number; height: number } | null = null;
260
2465
  private isResizing = false;
261
2466
 
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
- ];
2467
+ private readonly customTabIcons: Record<string, string> = {
2468
+ threads: `<svg class="h-3.5 w-3.5" width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.04167 15C8.29167 15 7.65972 14.7431 7.14583 14.2292C6.63194 13.7153 6.375 13.0972 6.375 12.375C6.375 11.3194 6.80208 10.3646 7.65625 9.51042C8.51042 8.65625 9.57639 8.125 10.8542 7.91667C10.8125 7.41667 10.6875 7.03819 10.4792 6.78125C10.2708 6.52431 9.98611 6.39583 9.625 6.39583C9.20833 6.39583 8.75694 6.56944 8.27083 6.91667C7.78472 7.26389 7.20833 7.83333 6.54167 8.625C5.45833 9.91667 4.66319 10.7569 4.15625 11.1458C3.64931 11.5347 3.10417 11.7292 2.52083 11.7292C1.8125 11.7292 1.21528 11.4653 0.729167 10.9375C0.243056 10.4097 0 9.77083 0 9.02083C0 8.27083 0.163194 7.50347 0.489583 6.71875C0.815972 5.93403 1.36806 4.99306 2.14583 3.89583C2.40972 3.53472 2.60417 3.22917 2.72917 2.97917C2.85417 2.72917 2.91667 2.52778 2.91667 2.375C2.91667 2.27778 2.89931 2.20486 2.86458 2.15625C2.82986 2.10764 2.77778 2.08333 2.70833 2.08333C2.56944 2.08333 2.39583 2.17014 2.1875 2.34375C1.97917 2.51736 1.73611 2.78472 1.45833 3.14583L0 1.66667C0.444444 1.125 0.895833 0.711806 1.35417 0.427083C1.8125 0.142361 2.26389 0 2.70833 0C3.34722 0 3.88889 0.222222 4.33333 0.666667C4.77778 1.11111 5 1.66667 5 2.33333C5 2.73611 4.89583 3.18056 4.6875 3.66667C4.47917 4.15278 4.13194 4.73611 3.64583 5.41667C3.11806 6.16667 2.72569 6.82639 2.46875 7.39583C2.21181 7.96528 2.08333 8.46528 2.08333 8.89583C2.08333 9.13194 2.12153 9.31597 2.19792 9.44792C2.27431 9.57986 2.38194 9.64583 2.52083 9.64583C2.65972 9.64583 2.78125 9.60764 2.88542 9.53125C2.98958 9.45486 3.18056 9.27083 3.45833 8.97917C3.63889 8.78472 3.85417 8.54514 4.10417 8.26042C4.35417 7.97569 4.65972 7.625 5.02083 7.20833C5.89583 6.16667 6.6875 5.42361 7.39583 4.97917C8.10417 4.53472 8.84722 4.3125 9.625 4.3125C10.5556 4.3125 11.3194 4.625 11.9167 5.25C12.5139 5.875 12.8542 6.72917 12.9375 7.8125H15V9.89583H12.9375C12.8264 11.4514 12.4201 12.691 11.7188 13.6146C11.0174 14.5382 10.125 15 9.04167 15ZM9.08333 12.9167C9.52778 12.9167 9.90278 12.6632 10.2083 12.1562C10.5139 11.6493 10.7222 10.9444 10.8333 10.0417C10.1944 10.1944 9.63889 10.4965 9.16667 10.9479C8.69444 11.3993 8.45833 11.8472 8.45833 12.2917C8.45833 12.4861 8.51389 12.6389 8.625 12.75C8.73611 12.8611 8.88889 12.9167 9.08333 12.9167Z" fill="currentColor"/></svg>`,
2469
+ };
2470
+
2471
+ private get menuItems(): MenuItem[] {
2472
+ const hasFrontendTools = (this._core?.tools?.length ?? 0) > 0;
2473
+ return [
2474
+ {
2475
+ key: "ag-ui-events",
2476
+ label: "AG-UI Events",
2477
+ icon: "Zap" as LucideIconName,
2478
+ },
2479
+ { key: "agents", label: "Agent", icon: "Bot" as LucideIconName },
2480
+ ...(hasFrontendTools
2481
+ ? [
2482
+ {
2483
+ key: "frontend-tools" as const,
2484
+ label: "Frontend Tools",
2485
+ icon: "Hammer" as LucideIconName,
2486
+ },
2487
+ ]
2488
+ : []),
2489
+ {
2490
+ key: "agent-context",
2491
+ label: "Context",
2492
+ icon: "FileText" as LucideIconName,
2493
+ },
2494
+ {
2495
+ key: "threads",
2496
+ label: "Threads",
2497
+ icon: "MessageSquare" as LucideIconName,
2498
+ },
2499
+ ];
2500
+ }
2501
+
2502
+ private subscribeToThreadStore(agentId: string, store: ɵThreadStore): void {
2503
+ if (this._threadStoreSubscriptions.has(agentId)) return;
2504
+ const threadsSub = store.select(ɵselectThreads).subscribe((threads) => {
2505
+ this._threadsByAgent.set(agentId, threads as ɵThread[]);
2506
+ this._threads = Array.from(this._threadsByAgent.values()).flat();
2507
+ this.autoSelectLatestThread();
2508
+ this.requestUpdate();
2509
+ });
2510
+ const errorSub = store.select(ɵselectThreadsError).subscribe((error) => {
2511
+ if (error) {
2512
+ this._threadsErrorByAgent.set(agentId, error);
2513
+ } else {
2514
+ this._threadsErrorByAgent.delete(agentId);
2515
+ }
2516
+ this.requestUpdate();
2517
+ });
2518
+ this._threadStoreSubscriptions.set(agentId, () => {
2519
+ threadsSub.unsubscribe();
2520
+ errorSub.unsubscribe();
2521
+ });
2522
+ // Populate immediately from current state
2523
+ const initialState = store.getState();
2524
+ this._threadsByAgent.set(agentId, ɵselectThreads(initialState));
2525
+ const initialError = ɵselectThreadsError(initialState);
2526
+ if (initialError) {
2527
+ this._threadsErrorByAgent.set(agentId, initialError);
2528
+ } else {
2529
+ this._threadsErrorByAgent.delete(agentId);
2530
+ }
2531
+ this._threads = Array.from(this._threadsByAgent.values()).flat();
2532
+ this.autoSelectLatestThread();
2533
+ }
2534
+
2535
+ private autoSelectLatestThread(): void {
2536
+ if (this._threads.length === 0) return;
2537
+ const stillValid =
2538
+ this.selectedThreadId != null &&
2539
+ this._threads.some((t) => t.id === this.selectedThreadId);
2540
+ if (!stillValid) {
2541
+ // Threads are sorted most-recently-updated first
2542
+ this.selectedThreadId = this._threads[0]!.id;
2543
+ }
2544
+ }
2545
+
2546
+ private teardownThreadStoreSubscriptions(): void {
2547
+ for (const unsub of this._threadStoreSubscriptions.values()) {
2548
+ unsub();
2549
+ }
2550
+ this._threadStoreSubscriptions.clear();
2551
+ this._threadsByAgent.clear();
2552
+ this._threadsErrorByAgent.clear();
2553
+ this._threads = [];
2554
+ }
2555
+
2556
+ private ensureOwnedThreadStore(agentId: string): void {
2557
+ if (this._ownedThreadStores.has(agentId)) return;
2558
+ // Don't overwrite a store already registered by useThreads() or another external caller
2559
+ if (this.core?.getThreadStore(agentId)) return;
2560
+ const core = this.core;
2561
+ if (!core?.runtimeUrl) return;
2562
+
2563
+ const store = ɵcreateThreadStore({ fetch: globalThis.fetch });
2564
+ store.start();
2565
+ store.setContext({
2566
+ runtimeUrl: core.runtimeUrl,
2567
+ headers: {},
2568
+ agentId,
2569
+ });
2570
+ this._ownedThreadStores.set(agentId, store);
2571
+ // Subscribe directly so threads render even before the registry callback
2572
+ // fires (some published-core code paths land on the subscriber after
2573
+ // registerThreadStore returns).
2574
+ this.subscribeToThreadStore(agentId, store);
2575
+ core.registerThreadStore(agentId, store);
2576
+ }
2577
+
2578
+ private refreshOwnedThreadStore(agentId: string): void {
2579
+ const store = this._ownedThreadStores.get(agentId);
2580
+ if (!store) return;
2581
+ // refresh() re-fetches without resetting threads to [] first, so the list
2582
+ // stays visible while new data loads and survives transient fetch failures.
2583
+ store.refresh();
2584
+ }
2585
+
2586
+ private removeOwnedThreadStore(agentId: string): void {
2587
+ const store = this._ownedThreadStores.get(agentId);
2588
+ if (!store) return;
2589
+ store.stop();
2590
+ this.core?.unregisterThreadStore(agentId);
2591
+ this._ownedThreadStores.delete(agentId);
2592
+ }
2593
+
2594
+ private teardownOwnedThreadStores(): void {
2595
+ for (const [agentId, store] of this._ownedThreadStores) {
2596
+ store.stop();
2597
+ this.core?.unregisterThreadStore(agentId);
2598
+ }
2599
+ this._ownedThreadStores.clear();
2600
+ }
268
2601
 
269
2602
  private attachToCore(core: CopilotKitCore): void {
270
2603
  this.runtimeStatus = core.runtimeConnectionStatus;
@@ -274,6 +2607,15 @@ export class WebInspectorElement extends LitElement {
274
2607
  this.coreSubscriber = {
275
2608
  onRuntimeConnectionStatusChanged: ({ status }) => {
276
2609
  this.runtimeStatus = status;
2610
+ if (status === "connected") {
2611
+ for (const agentId of this._ownedThreadStores.keys()) {
2612
+ this.refreshOwnedThreadStore(agentId);
2613
+ }
2614
+ } else {
2615
+ // Clear stale thread data immediately when the server goes away
2616
+ this._threadsByAgent.clear();
2617
+ this._threads = [];
2618
+ }
277
2619
  this.requestUpdate();
278
2620
  },
279
2621
  onPropertiesChanged: ({ properties }) => {
@@ -287,23 +2629,52 @@ export class WebInspectorElement extends LitElement {
287
2629
  onAgentsChanged: ({ agents }) => {
288
2630
  this.processAgentsChanged(agents);
289
2631
  },
290
- onAgentRunStarted: ({ agent }) => {
291
- // Per-thread clones are not in the agent registry, so
292
- // onAgentsChanged never fires for them. Subscribe here so
293
- // the inspector captures their AG-UI events.
294
- if (agent?.agentId) {
295
- this.subscribeToAgent(agent);
296
- }
297
- },
298
2632
  onContextChanged: ({ context }) => {
299
2633
  this.contextStore = this.normalizeContextStore(context);
300
2634
  this.requestUpdate();
301
2635
  },
2636
+ onThreadStoreRegistered: ({ agentId, store }) => {
2637
+ this.subscribeToThreadStore(agentId, store);
2638
+ this.requestUpdate();
2639
+ },
2640
+ onThreadStoreUnregistered: ({ agentId }) => {
2641
+ const unsub = this._threadStoreSubscriptions.get(agentId);
2642
+ if (unsub) {
2643
+ unsub();
2644
+ this._threadStoreSubscriptions.delete(agentId);
2645
+ }
2646
+ this._threadsByAgent.delete(agentId);
2647
+ this._threadsErrorByAgent.delete(agentId);
2648
+ this._threads = Array.from(this._threadsByAgent.values()).flat();
2649
+ this.requestUpdate();
2650
+ },
2651
+ onAgentRunStarted: ({ agent }) => {
2652
+ // Subscribe to the concrete agent instance about to run. This handles
2653
+ // per-thread clones that are not in core.agents and therefore not
2654
+ // reachable via onAgentsChanged. Replacing an existing subscription for
2655
+ // the same agentId is safe: the previous instance emits no more events
2656
+ // once a new run starts on a fresh clone.
2657
+ this.subscribeToAgent(agent);
2658
+ const runThreadId = (agent as { threadId?: string }).threadId;
2659
+ if (agent.agentId && runThreadId) {
2660
+ this.agentRunThreadId.set(agent.agentId, runThreadId);
2661
+ }
2662
+ this.requestUpdate();
2663
+ },
302
2664
  } satisfies CopilotKitCoreSubscriber;
303
2665
 
304
2666
  this.coreUnsubscribe = core.subscribe(this.coreSubscriber).unsubscribe;
305
2667
  this.processAgentsChanged(core.agents);
306
2668
 
2669
+ // Subscribe to any already-registered thread stores. `getThreadStores` was
2670
+ // added in the same release as this inspector; guard so consumers still on
2671
+ // an older @copilotkit/core don't throw when assigning `inspector.core`.
2672
+ const threadStores =
2673
+ typeof core.getThreadStores === "function" ? core.getThreadStores() : {};
2674
+ for (const [agentId, store] of Object.entries(threadStores)) {
2675
+ this.subscribeToThreadStore(agentId, store);
2676
+ }
2677
+
307
2678
  // Initialize context from core
308
2679
  if (core.context) {
309
2680
  this.contextStore = this.normalizeContextStore(core.context);
@@ -322,6 +2693,8 @@ export class WebInspectorElement extends LitElement {
322
2693
  this.cachedTools = [];
323
2694
  this.toolSignature = "";
324
2695
  this.teardownAgentSubscriptions();
2696
+ this.teardownThreadStoreSubscriptions();
2697
+ this.teardownOwnedThreadStores();
325
2698
  }
326
2699
 
327
2700
  private teardownAgentSubscriptions(): void {
@@ -347,6 +2720,7 @@ export class WebInspectorElement extends LitElement {
347
2720
  }
348
2721
  seenAgentIds.add(agent.agentId);
349
2722
  this.subscribeToAgent(agent);
2723
+ this.ensureOwnedThreadStore(agent.agentId);
350
2724
  }
351
2725
 
352
2726
  for (const agentId of Array.from(this.agentSubscriptions.keys())) {
@@ -355,6 +2729,10 @@ export class WebInspectorElement extends LitElement {
355
2729
  this.agentEvents.delete(agentId);
356
2730
  this.agentMessages.delete(agentId);
357
2731
  this.agentStates.delete(agentId);
2732
+ // Do NOT remove owned thread stores here — they are independent of
2733
+ // whether the agent appears in core.agents (published cores discover
2734
+ // agents asynchronously so agents may be empty on first fire). Stores
2735
+ // are torn down in teardownOwnedThreadStores() when the core detaches.
358
2736
  }
359
2737
  }
360
2738
 
@@ -436,6 +2814,7 @@ export class WebInspectorElement extends LitElement {
436
2814
  },
437
2815
  onRunFinishedEvent: ({ event, result }) => {
438
2816
  this.recordAgentEvent(agentId, "RUN_FINISHED", { event, result });
2817
+ this.refreshOwnedThreadStore(agentId);
439
2818
  },
440
2819
  onRunErrorEvent: ({ event }) => {
441
2820
  this.recordAgentEvent(agentId, "RUN_ERROR", event);
@@ -529,6 +2908,14 @@ export class WebInspectorElement extends LitElement {
529
2908
  onReasoningEncryptedValueEvent: ({ event }) => {
530
2909
  this.recordAgentEvent(agentId, "REASONING_ENCRYPTED_VALUE", event);
531
2910
  },
2911
+ onActivitySnapshotEvent: ({ event }) => {
2912
+ this.recordAgentEvent(agentId, "ACTIVITY_SNAPSHOT", event);
2913
+ this.syncAgentMessages(agent);
2914
+ },
2915
+ onActivityDeltaEvent: ({ event }) => {
2916
+ this.recordAgentEvent(agentId, "ACTIVITY_DELTA", event);
2917
+ this.syncAgentMessages(agent);
2918
+ },
532
2919
  };
533
2920
 
534
2921
  const { unsubscribe } = agent.subscribe(subscriber);
@@ -549,6 +2936,32 @@ export class WebInspectorElement extends LitElement {
549
2936
  }
550
2937
  }
551
2938
 
2939
+ private mapMessagesToConversation(
2940
+ messages: InspectorMessage[] | null,
2941
+ ): { id: string; type: string; content: string; createdAt: string }[] | null {
2942
+ if (!messages) return null;
2943
+ return messages
2944
+ .filter(
2945
+ (m) =>
2946
+ m.role === "user" || m.role === "assistant" || m.role === "activity",
2947
+ )
2948
+ .map((m, i) => ({
2949
+ id: m.id ?? `msg-${i}`,
2950
+ type:
2951
+ m.role === "user"
2952
+ ? "user"
2953
+ : m.role === "activity"
2954
+ ? "generative-ui"
2955
+ : "assistant",
2956
+ // For activity messages, store the activityType as a label so the
2957
+ // renderer has something meaningful to display.
2958
+ // TODO: render activity payload once available.
2959
+ content:
2960
+ m.role === "activity" ? (m.activityType ?? "unknown") : m.contentText,
2961
+ createdAt: "",
2962
+ }));
2963
+ }
2964
+
552
2965
  private recordAgentEvent(
553
2966
  agentId: string,
554
2967
  type: InspectorAgentEventType,
@@ -594,6 +3007,19 @@ export class WebInspectorElement extends LitElement {
594
3007
  this.agentMessages.delete(agent.agentId);
595
3008
  }
596
3009
 
3010
+ // Bump the live-message version for whichever thread this agent is
3011
+ // currently running on. cpk-thread-details watches this for the
3012
+ // selected thread and re-fetches `/threads/:id/messages` when it ticks,
3013
+ // so the conversation view stays in sync with the streaming agent
3014
+ // without the parent re-implementing AG-UI → ConversationItem mapping.
3015
+ const runThreadId = this.agentRunThreadId.get(agent.agentId);
3016
+ if (runThreadId) {
3017
+ this.liveMessageVersion.set(
3018
+ runThreadId,
3019
+ (this.liveMessageVersion.get(runThreadId) ?? 0) + 1,
3020
+ );
3021
+ }
3022
+
597
3023
  this.requestUpdate();
598
3024
  } catch (error) {
599
3025
  console.error(
@@ -648,14 +3074,25 @@ export class WebInspectorElement extends LitElement {
648
3074
  if (pendingContext) {
649
3075
  const isPendingAvailable =
650
3076
  pendingContext === "all-agents" || agentIds.has(pendingContext);
651
- if (isPendingAvailable) {
3077
+ // Only restore a specific-agent selection when there is exactly one
3078
+ // agent registered. With multiple agents, fall back to "all-agents" so
3079
+ // events from any agent are visible regardless of what was persisted.
3080
+ const shouldRestore =
3081
+ isPendingAvailable &&
3082
+ (pendingContext === "all-agents" || agentIds.size === 1);
3083
+ if (shouldRestore) {
652
3084
  if (this.selectedContext !== pendingContext) {
653
3085
  this.selectedContext = pendingContext;
654
3086
  this.expandedRows.clear();
655
3087
  }
656
3088
  this.pendingSelectedContext = null;
657
3089
  } else if (agentIds.size > 0) {
658
- // Agents are loaded but the pending selection no longer exists
3090
+ // Persisted selection is unavailable or inappropriate for multiple
3091
+ // agents — reset to "all-agents" so nothing is silently filtered.
3092
+ if (this.selectedContext !== "all-agents") {
3093
+ this.selectedContext = "all-agents";
3094
+ this.expandedRows.clear();
3095
+ }
659
3096
  this.pendingSelectedContext = null;
660
3097
  }
661
3098
  }
@@ -665,15 +3102,13 @@ export class WebInspectorElement extends LitElement {
665
3102
  );
666
3103
 
667
3104
  if (!hasSelectedContext && this.pendingSelectedContext === null) {
668
- // Auto-select "default" agent if it exists, otherwise first agent, otherwise "all-agents"
3105
+ // When there is exactly one agent, auto-select it so the view is
3106
+ // immediately focused. When multiple agents are registered (e.g. "default"
3107
+ // + "openai"), keep "all-agents" so events from any agent are visible.
669
3108
  let nextSelected: string = "all-agents";
670
3109
 
671
- if (agentIds.has("default")) {
672
- nextSelected = "default";
673
- } else if (agentIds.size > 0) {
674
- nextSelected = Array.from(agentIds).sort((a, b) =>
675
- a.localeCompare(b),
676
- )[0]!;
3110
+ if (agentIds.size === 1) {
3111
+ nextSelected = Array.from(agentIds)[0]!;
677
3112
  }
678
3113
 
679
3114
  if (this.selectedContext !== nextSelected) {
@@ -1003,6 +3438,7 @@ ${argsString}</pre
1003
3438
  z-index: 2147483646;
1004
3439
  display: block;
1005
3440
  will-change: transform;
3441
+ font-family: "Plus Jakarta Sans", system-ui, sans-serif;
1006
3442
  }
1007
3443
 
1008
3444
  :host([data-transitioning="true"]) {
@@ -1058,13 +3494,14 @@ ${argsString}</pre
1058
3494
  left: 50%;
1059
3495
  transform: translateX(-50%) translateY(-4px);
1060
3496
  white-space: nowrap;
1061
- background: rgba(17, 24, 39, 0.95);
3497
+ background: rgba(1, 5, 7, 0.95);
1062
3498
  color: white;
1063
3499
  padding: 4px 8px;
1064
3500
  border-radius: 6px;
1065
3501
  font-size: 10px;
3502
+ font-family: "Plus Jakarta Sans", system-ui, sans-serif;
1066
3503
  line-height: 1.2;
1067
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
3504
+ box-shadow: 0 4px 10px rgba(1, 5, 7, 0.18);
1068
3505
  opacity: 0;
1069
3506
  pointer-events: none;
1070
3507
  transition:
@@ -1085,116 +3522,599 @@ ${argsString}</pre
1085
3522
  min-width: 300px;
1086
3523
  max-width: 300px;
1087
3524
  background: white;
1088
- color: #111827;
3525
+ color: #010507;
1089
3526
  font-size: 13px;
3527
+ font-family: "Plus Jakarta Sans", system-ui, sans-serif;
1090
3528
  line-height: 1.4;
1091
3529
  border-radius: 12px;
1092
- box-shadow: 0 12px 28px rgba(15, 23, 42, 0.22);
3530
+ box-shadow: 0 12px 28px rgba(1, 5, 7, 0.12);
1093
3531
  padding: 10px 12px;
1094
3532
  display: inline-flex;
1095
3533
  align-items: flex-start;
1096
3534
  gap: 8px;
1097
3535
  z-index: 4500;
1098
3536
  animation: fade-slide-in 160ms ease;
1099
- border: 1px solid rgba(148, 163, 184, 0.35);
3537
+ border: 1px solid rgba(219, 219, 229, 0.4);
1100
3538
  white-space: normal;
1101
3539
  word-break: break-word;
1102
3540
  text-align: left;
1103
3541
  }
1104
3542
 
1105
- .announcement-preview[data-side="left"] {
1106
- right: 100%;
1107
- margin-right: 10px;
3543
+ .announcement-preview[data-side="left"] {
3544
+ right: 100%;
3545
+ margin-right: 10px;
3546
+ }
3547
+
3548
+ .announcement-preview[data-side="right"] {
3549
+ left: 100%;
3550
+ margin-left: 10px;
3551
+ }
3552
+
3553
+ .announcement-preview__arrow {
3554
+ position: absolute;
3555
+ width: 10px;
3556
+ height: 10px;
3557
+ background: white;
3558
+ border: 1px solid rgba(219, 219, 229, 0.4);
3559
+ transform: rotate(45deg);
3560
+ top: 50%;
3561
+ margin-top: -5px;
3562
+ z-index: -1;
3563
+ }
3564
+
3565
+ .announcement-preview[data-side="left"] .announcement-preview__arrow {
3566
+ right: -5px;
3567
+ box-shadow: 6px -6px 10px rgba(1, 5, 7, 0.08);
3568
+ }
3569
+
3570
+ .announcement-preview[data-side="right"] .announcement-preview__arrow {
3571
+ left: -5px;
3572
+ box-shadow: -6px 6px 10px rgba(1, 5, 7, 0.08);
3573
+ }
3574
+
3575
+ .announcement-dismiss {
3576
+ background: none;
3577
+ border: none;
3578
+ cursor: pointer;
3579
+ color: #838389;
3580
+ width: 28px;
3581
+ height: 28px;
3582
+ display: flex;
3583
+ align-items: center;
3584
+ justify-content: center;
3585
+ border-radius: 6px;
3586
+ padding: 0;
3587
+ transition:
3588
+ background 120ms ease,
3589
+ color 120ms ease;
3590
+ }
3591
+
3592
+ .announcement-dismiss:hover {
3593
+ background: rgba(0, 0, 0, 0.06);
3594
+ color: #010507;
3595
+ }
3596
+
3597
+ /* ── Agent tab section cards ─────────────────────────────────────── */
3598
+ .cpk-section-card {
3599
+ border-radius: 8px;
3600
+ background: #ffffff;
3601
+ overflow: hidden;
3602
+ }
3603
+
3604
+ /* ── Agent icon bubble ───────────────────────────────────────────── */
3605
+ .cpk-agent-icon {
3606
+ background-color: #f0f0f4 !important;
3607
+ color: #57575b !important;
3608
+ }
3609
+
3610
+ /* ── Agent stat cards ────────────────────────────────────────────── */
3611
+ .cpk-stat-card {
3612
+ background-color: #ffffff !important;
3613
+ border: 1px solid #dbdbe5 !important;
3614
+ }
3615
+ button.cpk-stat-card:hover {
3616
+ background-color: #f7f7f9 !important;
3617
+ }
3618
+
3619
+ /* ── Circle chevron (Frontend Tools + Context) ──────────────────── */
3620
+ .cpk-chevron-circle {
3621
+ display: inline-flex;
3622
+ align-items: center;
3623
+ justify-content: center;
3624
+ width: 24px;
3625
+ height: 24px;
3626
+ border-radius: 50%;
3627
+ background-color: #f0f0f4;
3628
+ color: #838389;
3629
+ flex-shrink: 0;
3630
+ transition: transform 0.2s;
3631
+ }
3632
+ .cpk-chevron-circle svg {
3633
+ width: 14px !important;
3634
+ height: 14px !important;
3635
+ }
3636
+ .cpk-chevron-circle--open {
3637
+ transform: rotate(180deg);
3638
+ }
3639
+
3640
+ /* ── Inline copy button ─────────────────────────────────────────── */
3641
+ .cpk-copy-btn {
3642
+ font-size: 10px;
3643
+ font-weight: 500;
3644
+ color: #57575b;
3645
+ background: #ffffff;
3646
+ border: 1px solid #dbdbe5;
3647
+ cursor: pointer;
3648
+ padding: 2px 8px;
3649
+ border-radius: 4px;
3650
+ flex-shrink: 0;
3651
+ transition:
3652
+ background-color 0.15s,
3653
+ border-color 0.15s;
3654
+ }
3655
+ .cpk-copy-btn:hover {
3656
+ background-color: #f0f0f4;
3657
+ border-color: #afafb7;
3658
+ }
3659
+
3660
+ .cpk-section-header {
3661
+ background: #e8edf5;
3662
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
3663
+ padding: 10px 16px;
3664
+ }
3665
+ .cpk-section-header h4 {
3666
+ font-size: 11px;
3667
+ font-weight: 600;
3668
+ color: #181c1f;
3669
+ margin: 0;
3670
+ }
3671
+
3672
+ /* Inputs/selects inside the lavender header need an explicit white bg */
3673
+ .cpk-section-header input,
3674
+ .cpk-section-header select {
3675
+ background-color: #ffffff !important;
3676
+ box-shadow: none !important;
3677
+ }
3678
+ .cpk-section-header select {
3679
+ padding-right: 24px !important;
3680
+ }
3681
+ /* Events table column headers */
3682
+ table thead th {
3683
+ font-weight: 600 !important;
3684
+ }
3685
+
3686
+ .announcement-content {
3687
+ color: #1f2230;
3688
+ font-size: 13px;
3689
+ font-family: "Plus Jakarta Sans", system-ui, sans-serif;
3690
+ line-height: 1.55;
3691
+ }
3692
+
3693
+ .announcement-content h1,
3694
+ .announcement-content h2,
3695
+ .announcement-content h3 {
3696
+ color: #010507;
3697
+ font-weight: 700;
3698
+ line-height: 1.3;
3699
+ margin: 0.9rem 0 0.4rem;
3700
+ }
3701
+ .announcement-content > h1:first-child,
3702
+ .announcement-content > h2:first-child,
3703
+ .announcement-content > h3:first-child {
3704
+ margin-top: 0;
3705
+ }
3706
+
3707
+ .announcement-content h1 {
3708
+ font-size: 1.15rem;
3709
+ letter-spacing: -0.01em;
3710
+ }
3711
+ .announcement-content h2 {
3712
+ font-size: 1rem;
3713
+ }
3714
+ .announcement-content h3 {
3715
+ font-size: 0.9rem;
3716
+ text-transform: none;
3717
+ }
3718
+
3719
+ .announcement-content p {
3720
+ margin: 0.45rem 0;
3721
+ }
3722
+
3723
+ .announcement-content strong {
3724
+ color: #010507;
3725
+ font-weight: 700;
3726
+ }
3727
+
3728
+ .announcement-content ul {
3729
+ list-style: disc;
3730
+ padding-left: 1.25rem;
3731
+ margin: 0.45rem 0;
3732
+ }
3733
+
3734
+ .announcement-content ol {
3735
+ list-style: decimal;
3736
+ padding-left: 1.25rem;
3737
+ margin: 0.45rem 0;
3738
+ }
3739
+
3740
+ .announcement-content li + li {
3741
+ margin-top: 0.15rem;
3742
+ }
3743
+
3744
+ .announcement-content a {
3745
+ color: #757cf2;
3746
+ text-decoration: underline;
3747
+ }
3748
+
3749
+ .announcement-content :not(pre) > code {
3750
+ background: #f3f3f7;
3751
+ border: 1px solid #e4e4ec;
3752
+ border-radius: 4px;
3753
+ padding: 1px 5px;
3754
+ font-size: 0.85em;
3755
+ color: #4a3a8a;
3756
+ }
3757
+
3758
+ .announcement-code {
3759
+ position: relative;
3760
+ margin: 0.6rem 0;
3761
+ }
3762
+
3763
+ .announcement-code pre {
3764
+ background: #0f1117;
3765
+ color: #e6e8f2;
3766
+ border-radius: 8px;
3767
+ padding: 10px 12px;
3768
+ overflow-x: auto;
3769
+ font-size: 12px;
3770
+ line-height: 1.5;
3771
+ white-space: pre;
3772
+ }
3773
+
3774
+ .announcement-code pre code::after {
3775
+ content: "";
3776
+ display: inline-block;
3777
+ width: 80px;
3778
+ }
3779
+
3780
+ .announcement-code__copy-shield {
3781
+ position: absolute;
3782
+ top: 4px;
3783
+ right: 4px;
3784
+ padding: 4px 4px 4px 24px;
3785
+ border-top-right-radius: 8px;
3786
+ background: linear-gradient(
3787
+ to right,
3788
+ rgba(15, 17, 23, 0) 0%,
3789
+ rgba(15, 17, 23, 0.95) 40%,
3790
+ #0f1117 100%
3791
+ );
3792
+ pointer-events: none;
3793
+ }
3794
+
3795
+ .announcement-code pre code {
3796
+ background: transparent;
3797
+ border: none;
3798
+ padding: 0;
3799
+ color: inherit;
3800
+ font-size: inherit;
3801
+ }
3802
+
3803
+ .announcement-code pre::-webkit-scrollbar {
3804
+ height: 6px;
3805
+ }
3806
+ .announcement-code pre::-webkit-scrollbar-track {
3807
+ background: transparent;
3808
+ }
3809
+ .announcement-code pre::-webkit-scrollbar-thumb {
3810
+ background: rgba(255, 255, 255, 0.2);
3811
+ border-radius: 3px;
3812
+ }
3813
+
3814
+ .announcement-code__copy {
3815
+ position: relative;
3816
+ pointer-events: auto;
3817
+ padding: 3px 8px;
3818
+ font-family: "Plus Jakarta Sans", system-ui, sans-serif;
3819
+ font-size: 11px;
3820
+ font-weight: 600;
3821
+ color: #e6e8f2;
3822
+ background: #1f222d;
3823
+ border: 1px solid rgba(255, 255, 255, 0.15);
3824
+ border-radius: 5px;
3825
+ cursor: pointer;
3826
+ transition:
3827
+ background 0.12s ease,
3828
+ color 0.12s ease;
3829
+ }
3830
+ .announcement-code__copy:hover {
3831
+ background: #2a2e3c;
3832
+ }
3833
+ .announcement-code__copy[data-copied="true"] {
3834
+ background: #eee6fe;
3835
+ color: #6430ab;
3836
+ border-color: transparent;
3837
+ }
3838
+
3839
+ .announcement-body {
3840
+ position: relative;
3841
+ overflow: hidden;
3842
+ transition: max-height 0.25s ease;
3843
+ }
3844
+ .announcement-body--collapsed {
3845
+ max-height: 72px;
3846
+ }
3847
+ .announcement-body--expanded {
3848
+ max-height: 2000px;
3849
+ }
3850
+ .announcement-fade {
3851
+ position: absolute;
3852
+ bottom: 0;
3853
+ left: 0;
3854
+ right: 0;
3855
+ height: 48px;
3856
+ background: linear-gradient(to bottom, transparent, #ffffff);
3857
+ pointer-events: none;
3858
+ }
3859
+ .announcement-toggle {
3860
+ display: block;
3861
+ width: 100%;
3862
+ margin-top: 6px;
3863
+ padding: 0;
3864
+ background: none;
3865
+ border: none;
3866
+ font-family: "Plus Jakarta Sans", system-ui, sans-serif;
3867
+ font-size: 12px;
3868
+ font-weight: 500;
3869
+ color: #757cf2;
3870
+ cursor: pointer;
3871
+ text-align: center;
3872
+ }
3873
+ .announcement-toggle:hover {
3874
+ color: #6430ab;
1108
3875
  }
1109
3876
 
1110
- .announcement-preview[data-side="right"] {
1111
- left: 100%;
1112
- margin-left: 10px;
3877
+ /* ── Brand typography ────────────────────────────────────────── */
3878
+ /* Override Tailwind font-mono stack → Spline Sans Mono */
3879
+ .font-mono,
3880
+ pre,
3881
+ code {
3882
+ font-family: "Spline Sans Mono", ui-monospace, "Cascadia Code", monospace;
1113
3883
  }
1114
3884
 
1115
- .announcement-preview__arrow {
1116
- position: absolute;
1117
- width: 10px;
1118
- height: 10px;
1119
- background: white;
1120
- border: 1px solid rgba(148, 163, 184, 0.35);
1121
- transform: rotate(45deg);
1122
- top: 50%;
1123
- margin-top: -5px;
1124
- z-index: -1;
3885
+ /* ── Floating button ─────────────────────────────────────────── */
3886
+ .console-button {
3887
+ background-color: rgba(1, 5, 7, 0.95) !important;
3888
+ border-color: rgba(190, 194, 255, 0.25) !important;
3889
+ box-shadow:
3890
+ 0 0 0 1px rgba(190, 194, 255, 0.15),
3891
+ 0 4px 14px rgba(1, 5, 7, 0.28) !important;
3892
+ }
3893
+ .console-button:hover {
3894
+ background-color: rgba(1, 5, 7, 1) !important;
3895
+ border-color: rgba(190, 194, 255, 0.45) !important;
3896
+ }
3897
+ .console-button:focus-visible {
3898
+ outline-color: #bec2ff !important;
1125
3899
  }
1126
3900
 
1127
- .announcement-preview[data-side="left"] .announcement-preview__arrow {
1128
- right: -5px;
1129
- box-shadow: 6px -6px 10px rgba(15, 23, 42, 0.12);
3901
+ /* ── Inspector window ────────────────────────────────────────── */
3902
+ .inspector-window {
3903
+ border-color: #dbdbe5 !important;
3904
+ box-shadow:
3905
+ 0 8px 32px rgba(1, 5, 7, 0.1),
3906
+ 0 2px 8px rgba(1, 5, 7, 0.06) !important;
1130
3907
  }
1131
3908
 
1132
- .announcement-preview[data-side="right"] .announcement-preview__arrow {
1133
- left: -5px;
1134
- box-shadow: -6px 6px 10px rgba(15, 23, 42, 0.12);
3909
+ /* ── Header drag area ────────────────────────────────────────── */
3910
+ .drag-handle {
3911
+ border-bottom-color: #dbdbe5 !important;
3912
+ /* Subtle pale lavender gradient — brand "light, spacious" surface */
3913
+ background: linear-gradient(180deg, #f4f4fd 0%, #ffffff 100%) !important;
1135
3914
  }
1136
3915
 
1137
- .announcement-dismiss {
1138
- color: #6b7280;
1139
- font-size: 12px;
1140
- padding: 2px 8px;
1141
- border-radius: 8px;
1142
- border: 1px solid rgba(148, 163, 184, 0.5);
1143
- background: rgba(248, 250, 252, 0.9);
1144
- transition:
1145
- background 120ms ease,
1146
- color 120ms ease;
3916
+ /* Tab strip row: soft off-white, separated from content */
3917
+ .drag-handle > div:last-child {
3918
+ border-top-color: #e2e2ea !important;
3919
+ background-color: #fafafc !important;
1147
3920
  }
1148
3921
 
1149
- .announcement-dismiss:hover {
1150
- background: rgba(241, 245, 249, 1);
1151
- color: #111827;
3922
+ /* ── Tab buttons ─────────────────────────────────────────────── */
3923
+ /*
3924
+ * Named classes owned by this component — no Tailwind conflict.
3925
+ * Active: brand surface/surfaceContainerActive (lilac tint) +
3926
+ * border/borderActionEnabled underline.
3927
+ * Dark fill is for primary action buttons only, not nav tabs.
3928
+ */
3929
+ .cpk-tab-active {
3930
+ background-color: rgba(190, 194, 255, 0.18);
3931
+ color: #010507;
3932
+ font-weight: 600;
3933
+ }
3934
+ .cpk-tab-active .cpk-tab-icon {
3935
+ color: #757cf2;
3936
+ }
3937
+ .cpk-tab-inactive {
3938
+ background-color: transparent;
3939
+ color: #2b2b2b;
3940
+ }
3941
+ .cpk-tab-inactive .cpk-tab-icon {
3942
+ color: #838389;
3943
+ }
3944
+ .cpk-tab-inactive:hover {
3945
+ background-color: rgba(190, 194, 255, 0.08);
3946
+ color: #010507;
3947
+ cursor: pointer;
3948
+ }
3949
+ .cpk-tab-active {
3950
+ cursor: pointer;
1152
3951
  }
1153
3952
 
1154
- .announcement-content {
1155
- color: #111827;
1156
- font-size: 14px;
1157
- line-height: 1.6;
3953
+ /* ── Header control buttons (dock, close) — first row only ───── */
3954
+ .drag-handle > div:first-child button {
3955
+ color: #838389 !important;
3956
+ }
3957
+ .drag-handle > div:first-child button:hover {
3958
+ background-color: #f0f0f4 !important;
3959
+ color: #57575b !important;
3960
+ }
3961
+ .drag-handle > div:first-child button:focus-visible {
3962
+ outline-color: #bec2ff !important;
1158
3963
  }
1159
3964
 
1160
- .announcement-content h1,
1161
- .announcement-content h2,
1162
- .announcement-content h3 {
1163
- font-weight: 700;
1164
- margin: 0.4rem 0 0.2rem;
3965
+ /* ── Agent/context dropdown ──────────────────────────────────── */
3966
+ [data-context-dropdown-root="true"] > button {
3967
+ border-color: #dbdbe5 !important;
3968
+ color: #010507 !important;
3969
+ }
3970
+ [data-context-dropdown-root="true"] > button:hover {
3971
+ border-color: #bec2ff !important;
3972
+ background-color: #f7f7f9 !important;
3973
+ }
3974
+ [data-context-dropdown-root="true"] > button > span:last-child {
3975
+ color: #838389 !important;
3976
+ }
3977
+ [data-context-dropdown-root="true"] > div {
3978
+ border-color: #dbdbe5 !important;
3979
+ box-shadow: 0 4px 12px rgba(1, 5, 7, 0.08) !important;
3980
+ }
3981
+ [data-context-dropdown-root="true"] > div button:hover,
3982
+ [data-context-dropdown-root="true"] > div button:focus {
3983
+ background-color: #f7f7f9 !important;
1165
3984
  }
1166
3985
 
1167
- .announcement-content h1 {
1168
- font-size: 1.1rem;
3986
+ /* ── Status bar (bottom chrome) ──────────────────────────────── */
3987
+ .inspector-window > div > div:last-child {
3988
+ border-top-color: #dbdbe5 !important;
3989
+ background-color: #f7f7f9 !important;
1169
3990
  }
1170
3991
 
1171
- .announcement-content h2 {
1172
- font-size: 1rem;
3992
+ /* ── Resize handle ───────────────────────────────────────────── */
3993
+ .resize-handle {
3994
+ color: #838389 !important;
3995
+ }
3996
+ .resize-handle:hover {
3997
+ color: #57575b !important;
1173
3998
  }
1174
3999
 
1175
- .announcement-content h3 {
1176
- font-size: 0.95rem;
4000
+ /* ── AG-UI Events tab ────────────────────────────────────────── */
4001
+ /* Row hover: replace blue tint with brand lilac */
4002
+ tr:hover td {
4003
+ background-color: rgba(190, 194, 255, 0.08) !important;
4004
+ }
4005
+ /* Reset/dark action button */
4006
+ button[class*="bg-gray-900"] {
4007
+ background-color: #010507 !important;
4008
+ }
4009
+ button[class*="bg-gray-800"] {
4010
+ background-color: #2b2b2b !important;
4011
+ }
4012
+ /* Copy "copied" state: generic green → brand mint */
4013
+ button[class*="bg-green-100"] {
4014
+ background-color: rgba(133, 236, 206, 0.2) !important;
4015
+ color: #189370 !important;
1177
4016
  }
1178
4017
 
1179
- .announcement-content p {
1180
- margin: 0.25rem 0;
4018
+ /* ── Agents tab ──────────────────────────────────────────────── */
4019
+ /* Agent icon bubble: blue → lilac */
4020
+ span[class*="bg-blue-100"]:not([class*="text-blue-800"]) {
4021
+ background-color: rgba(190, 194, 255, 0.15) !important;
4022
+ }
4023
+ span[class*="text-blue-600"] {
4024
+ color: #757cf2 !important;
4025
+ }
4026
+ /* Running badge: emerald → mint */
4027
+ span[class*="bg-emerald-50"] {
4028
+ background-color: rgba(133, 236, 206, 0.15) !important;
4029
+ }
4030
+ span[class*="text-emerald-700"] {
4031
+ color: #189370 !important;
4032
+ }
4033
+ /* Running status dot */
4034
+ span[class*="bg-emerald-500"] {
4035
+ background-color: #85ecce !important;
4036
+ }
4037
+ /* Idle dot */
4038
+ span[class*="bg-gray-400"] {
4039
+ background-color: #afafb7 !important;
4040
+ }
4041
+ /* User role badge (blue → lilac) */
4042
+ span[class*="bg-blue-100"][class*="text-blue-800"] {
4043
+ background-color: rgba(190, 194, 255, 0.22) !important;
4044
+ border: 1px solid rgba(190, 194, 255, 0.45) !important;
4045
+ color: #57575b !important;
4046
+ }
4047
+ /* Assistant role badge (green → mint) */
4048
+ span[class*="bg-green-100"][class*="text-green-800"] {
4049
+ background-color: rgba(133, 236, 206, 0.18) !important;
4050
+ border: 1px solid rgba(133, 236, 206, 0.4) !important;
4051
+ color: #189370 !important;
4052
+ }
4053
+ /* Tool role badge (amber → orange brand) */
4054
+ span[class*="bg-amber-100"][class*="text-amber-800"] {
4055
+ background-color: rgba(255, 172, 77, 0.15) !important;
4056
+ color: #57575b !important;
1181
4057
  }
1182
4058
 
1183
- .announcement-content ul {
1184
- list-style: disc;
1185
- padding-left: 1.25rem;
1186
- margin: 0.3rem 0;
4059
+ /* ── Frontend Tools tab ──────────────────────────────────────── */
4060
+ /* Handler badge (blue → lilac) */
4061
+ span[class*="bg-blue-50"][class*="text-blue-700"] {
4062
+ background-color: rgba(190, 194, 255, 0.12) !important;
4063
+ border-color: rgba(190, 194, 255, 0.3) !important;
4064
+ color: #010507 !important;
4065
+ }
4066
+ /* Renderer badge (purple → lilac-adjacent) */
4067
+ span[class*="bg-purple-50"][class*="text-purple-700"] {
4068
+ background-color: rgba(190, 194, 255, 0.12) !important;
4069
+ border-color: rgba(190, 194, 255, 0.3) !important;
4070
+ color: #57575b !important;
4071
+ }
4072
+ /* Required badge (rose → brand red) */
4073
+ span[class*="bg-rose-50"][class*="text-rose-700"] {
4074
+ background-color: rgba(250, 95, 103, 0.1) !important;
4075
+ border-color: rgba(250, 95, 103, 0.25) !important;
4076
+ color: #fa5f67 !important;
4077
+ }
4078
+ /* Code/default value blocks */
4079
+ code[class*="bg-gray-100"],
4080
+ span[class*="bg-gray-100"] {
4081
+ background-color: #f0f0f4 !important;
1187
4082
  }
1188
4083
 
1189
- .announcement-content ol {
1190
- list-style: decimal;
1191
- padding-left: 1.25rem;
1192
- margin: 0.3rem 0;
4084
+ /* ── Connected status bar: match threads header mint (#5BE4BB) ──── */
4085
+ /* Outer strip bg + top border + text when connected badge is present */
4086
+ .inspector-window
4087
+ > div
4088
+ > div:last-child
4089
+ > div:last-child:has(div[class*="bg-emerald-50"]) {
4090
+ background-color: rgba(91, 228, 187, 0.08) !important;
4091
+ border-top-color: rgba(91, 228, 187, 0.3) !important;
4092
+ color: #189370 !important;
4093
+ }
4094
+ /* Inner badge — slightly more opaque on the mint bg */
4095
+ div[class*="bg-emerald-50"][class*="border-emerald-200"] {
4096
+ background-color: rgba(91, 228, 187, 0.12) !important;
4097
+ border-color: rgba(91, 228, 187, 0.4) !important;
4098
+ color: #189370 !important;
4099
+ }
4100
+ /* Icon bubble inside connected badge → mint tint */
4101
+ div[class*="bg-emerald-50"] span[class*="bg-white"] {
4102
+ background-color: rgba(91, 228, 187, 0.3) !important;
1193
4103
  }
1194
4104
 
1195
- .announcement-content a {
1196
- color: #0f766e;
1197
- text-decoration: underline;
4105
+ /* ── Announcement panel ──────────────────────────────────────── */
4106
+ div[class*="border-slate-200"][class*="bg-white"] {
4107
+ border-color: #dbdbe5 !important;
4108
+ }
4109
+ /* Announcement icon bubble: black → brand light lavender + lilac icon */
4110
+ span[class*="bg-slate-900"],
4111
+ div[class*="bg-slate-900"] {
4112
+ background-color: #eee6fe !important;
4113
+ color: #757cf2 !important;
4114
+ }
4115
+ span[class*="text-slate-800"],
4116
+ div[class*="text-slate-800"] {
4117
+ color: #010507 !important;
1198
4118
  }
1199
4119
  `,
1200
4120
  ];
@@ -1202,6 +4122,7 @@ ${argsString}</pre
1202
4122
  connectedCallback(): void {
1203
4123
  super.connectedCallback();
1204
4124
  if (typeof window !== "undefined") {
4125
+ this.ensureBrandFonts();
1205
4126
  window.addEventListener("resize", this.handleResize);
1206
4127
  window.addEventListener(
1207
4128
  "pointerdown",
@@ -1215,6 +4136,17 @@ ${argsString}</pre
1215
4136
  }
1216
4137
  }
1217
4138
 
4139
+ private ensureBrandFonts(): void {
4140
+ const FONT_LINK_ID = "cpk-inspector-brand-fonts";
4141
+ if (document.getElementById(FONT_LINK_ID)) return;
4142
+ const link = document.createElement("link");
4143
+ link.id = FONT_LINK_ID;
4144
+ link.rel = "stylesheet";
4145
+ link.href =
4146
+ "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600&family=Spline+Sans+Mono:wght@600&display=swap";
4147
+ document.head.appendChild(link);
4148
+ }
4149
+
1218
4150
  disconnectedCallback(): void {
1219
4151
  super.disconnectedCallback();
1220
4152
  if (typeof window !== "undefined") {
@@ -1309,7 +4241,7 @@ ${argsString}</pre
1309
4241
  "focus-visible:outline",
1310
4242
  "focus-visible:outline-2",
1311
4243
  "focus-visible:outline-offset-2",
1312
- "focus-visible:outline-rose-500",
4244
+ "focus-visible:outline-[#BEC2FF]",
1313
4245
  "touch-none",
1314
4246
  "select-none",
1315
4247
  this.isDragging ? "cursor-grabbing" : "cursor-grab",
@@ -1442,9 +4374,7 @@ ${argsString}</pre
1442
4374
  const isSelected = this.selectedMenu === key;
1443
4375
  const tabClasses = [
1444
4376
  "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",
1445
- isSelected
1446
- ? "bg-gray-900 text-white shadow-sm"
1447
- : "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
4377
+ isSelected ? "cpk-tab-active" : "cpk-tab-inactive",
1448
4378
  ].join(" ");
1449
4379
 
1450
4380
  return html`
@@ -1454,10 +4384,12 @@ ${argsString}</pre
1454
4384
  aria-pressed=${isSelected}
1455
4385
  @click=${() => this.handleMenuSelect(key)}
1456
4386
  >
1457
- <span
1458
- class="text-gray-400 ${isSelected ? "text-white" : ""}"
1459
- >
1460
- ${this.renderIcon(icon)}
4387
+ <span class="cpk-tab-icon">
4388
+ ${
4389
+ key in this.customTabIcons
4390
+ ? unsafeHTML(this.customTabIcons[key])
4391
+ : this.renderIcon(icon)
4392
+ }
1461
4393
  </span>
1462
4394
  <span>${label}</span>
1463
4395
  </button>
@@ -1466,8 +4398,8 @@ ${argsString}</pre
1466
4398
  </div>
1467
4399
  </div>
1468
4400
  <div class="flex flex-1 flex-col overflow-hidden">
1469
- <div class="flex-1 overflow-auto">
1470
- ${this.renderAnnouncementPanel()}
4401
+ <div id="cpk-main-scroll" class="flex-1 overflow-auto">
4402
+ ${this.renderAnnouncementBanner()}
1471
4403
  ${this.renderCoreWarningBanner()} ${this.renderMainContent()}
1472
4404
  <slot></slot>
1473
4405
  </div>
@@ -2531,6 +5463,8 @@ ${argsString}</pre
2531
5463
  ? this.sanitizeForLogging(raw.content)
2532
5464
  : undefined,
2533
5465
  toolCalls,
5466
+ activityType:
5467
+ typeof raw.activityType === "string" ? raw.activityType : undefined,
2534
5468
  };
2535
5469
  }
2536
5470
 
@@ -2587,11 +5521,6 @@ ${argsString}</pre
2587
5521
  private expandedContextItems: Set<string> = new Set();
2588
5522
  private copiedContextItems: Set<string> = new Set();
2589
5523
 
2590
- private getSelectedMenu(): MenuItem {
2591
- const found = this.menuItems.find((item) => item.key === this.selectedMenu);
2592
- return found ?? this.menuItems[0]!;
2593
- }
2594
-
2595
5524
  private renderCoreWarningBanner() {
2596
5525
  if (this._core) {
2597
5526
  return nothing;
@@ -2686,9 +5615,156 @@ ${argsString}</pre
2686
5615
  return this.renderContextView();
2687
5616
  }
2688
5617
 
5618
+ if (this.selectedMenu === "threads") {
5619
+ return this.renderThreadsView();
5620
+ }
5621
+
2689
5622
  return nothing;
2690
5623
  }
2691
5624
 
5625
+ private handleThreadDividerPointerDown = (event: PointerEvent) => {
5626
+ this.threadDividerResizing = true;
5627
+ this.threadDividerPointerId = event.pointerId;
5628
+ this.threadDividerStartX = event.clientX;
5629
+ this.threadDividerStartWidth = this.threadListWidth;
5630
+ (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
5631
+ event.preventDefault();
5632
+ };
5633
+
5634
+ private handleThreadDividerPointerMove = (event: PointerEvent) => {
5635
+ if (
5636
+ !this.threadDividerResizing ||
5637
+ this.threadDividerPointerId !== event.pointerId
5638
+ )
5639
+ return;
5640
+ const delta = event.clientX - this.threadDividerStartX;
5641
+ this.threadListWidth = Math.max(
5642
+ 180,
5643
+ Math.min(480, this.threadDividerStartWidth + delta),
5644
+ );
5645
+ this.requestUpdate();
5646
+ };
5647
+
5648
+ private handleThreadDividerPointerUp = (event: PointerEvent) => {
5649
+ if (this.threadDividerPointerId !== event.pointerId) return;
5650
+ const target = event.currentTarget as HTMLElement;
5651
+ if (target.hasPointerCapture(this.threadDividerPointerId)) {
5652
+ target.releasePointerCapture(this.threadDividerPointerId);
5653
+ }
5654
+ this.threadDividerResizing = false;
5655
+ };
5656
+
5657
+ private renderThreadsView() {
5658
+ const displayThreads =
5659
+ this.selectedContext === "all-agents"
5660
+ ? this._threads
5661
+ : (this._threadsByAgent.get(this.selectedContext) ?? []);
5662
+
5663
+ // Surface a thread-store load error inline. For "all-agents" we report
5664
+ // the first error encountered across all agents (good enough for a
5665
+ // debugging surface — the per-agent context filter narrows down the
5666
+ // culprit). For a specific agent we use that agent's error directly.
5667
+ let threadsErrorMessage: string | null = null;
5668
+ if (this.selectedContext === "all-agents") {
5669
+ const firstError = this._threadsErrorByAgent.values().next().value;
5670
+ threadsErrorMessage = firstError?.message ?? null;
5671
+ } else {
5672
+ threadsErrorMessage =
5673
+ this._threadsErrorByAgent.get(this.selectedContext)?.message ?? null;
5674
+ }
5675
+
5676
+ const selectedThread =
5677
+ this.selectedThreadId != null
5678
+ ? (displayThreads.find((t) => t.id === this.selectedThreadId) ?? null)
5679
+ : null;
5680
+
5681
+ return html`
5682
+ <div style="display:flex;height:100%;overflow:hidden;">
5683
+ <!-- Left sidebar: thread list -->
5684
+ <div
5685
+ style="width:${this.threadListWidth}px;flex-shrink:0;overflow:hidden;display:flex;flex-direction:column;border-right:1px solid #DBDBE5;"
5686
+ >
5687
+ <cpk-thread-list
5688
+ style="height:100%;"
5689
+ .threads=${displayThreads}
5690
+ .selectedThreadId=${this.selectedThreadId}
5691
+ .errorMessage=${threadsErrorMessage}
5692
+ @threadSelected=${(e: CustomEvent<string>) => {
5693
+ this.selectedThreadId = e.detail;
5694
+ this.requestUpdate();
5695
+ }}
5696
+ ></cpk-thread-list>
5697
+ </div>
5698
+
5699
+ <!-- Resize divider -->
5700
+ <div
5701
+ style="width:4px;flex-shrink:0;cursor:col-resize;background:transparent;position:relative;z-index:1;"
5702
+ @pointerdown=${this.handleThreadDividerPointerDown}
5703
+ @pointermove=${this.handleThreadDividerPointerMove}
5704
+ @pointerup=${this.handleThreadDividerPointerUp}
5705
+ @pointercancel=${this.handleThreadDividerPointerUp}
5706
+ ></div>
5707
+
5708
+ <!-- Center + right: thread details or empty state -->
5709
+ <div style="flex:1;min-width:0;overflow:hidden;display:flex;">
5710
+ ${
5711
+ this.selectedThreadId
5712
+ ? html`<cpk-thread-details
5713
+ style="flex:1;min-width:0;"
5714
+ .threadId=${this.selectedThreadId}
5715
+ .thread=${selectedThread}
5716
+ .runtimeUrl=${this._core?.runtimeUrl ?? ""}
5717
+ .headers=${this._core?.headers ?? {}}
5718
+ .liveMessageVersion=${
5719
+ this.selectedThreadId
5720
+ ? (this.liveMessageVersion.get(this.selectedThreadId) ??
5721
+ 0)
5722
+ : 0
5723
+ }
5724
+ .agentStateInput=${
5725
+ selectedThread
5726
+ ? this.getLatestStateForAgent(selectedThread.agentId)
5727
+ : null
5728
+ }
5729
+ .agentEventsInput=${
5730
+ selectedThread
5731
+ ? (this.agentEvents.get(selectedThread.agentId) ?? [])
5732
+ : []
5733
+ }
5734
+ ></cpk-thread-details>`
5735
+ : html`
5736
+ <div
5737
+ style="
5738
+ flex: 1;
5739
+ display: flex;
5740
+ flex-direction: column;
5741
+ align-items: center;
5742
+ justify-content: center;
5743
+ gap: 8px;
5744
+ color: #838389;
5745
+ "
5746
+ >
5747
+ <svg
5748
+ width="32"
5749
+ height="32"
5750
+ viewBox="0 0 24 24"
5751
+ fill="none"
5752
+ stroke="#c0c0c8"
5753
+ stroke-width="1.5"
5754
+ stroke-linecap="round"
5755
+ stroke-linejoin="round"
5756
+ >
5757
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
5758
+ </svg>
5759
+ <span style="font-size: 13px">${displayThreads.length === 0 ? "No threads yet" : "Select a thread to inspect"}</span>
5760
+ </div>
5761
+ `
5762
+ }
5763
+ </div>
5764
+ </div>
5765
+ `;
5766
+ }
5767
+
2692
5768
  private renderEventsTable() {
2693
5769
  const events = this.getEventsForSelectedContext();
2694
5770
  const filteredEvents = this.filterEvents(events);
@@ -2700,19 +5776,15 @@ ${argsString}</pre
2700
5776
  if (events.length === 0) {
2701
5777
  return html`
2702
5778
  <div
2703
- class="flex h-full items-center justify-center px-4 py-8 text-center"
5779
+ class="flex h-full flex-col items-center justify-center gap-2 px-4 py-10 text-center"
2704
5780
  >
2705
- <div class="max-w-md">
2706
- <div
2707
- class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8"
2708
- >
2709
- ${this.renderIcon("Zap")}
2710
- </div>
2711
- <p class="text-sm text-gray-600">No events yet</p>
2712
- <p class="mt-2 text-xs text-gray-500">
2713
- Trigger an agent run to see live activity.
2714
- </p>
5781
+ <div class="text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
5782
+ ${this.renderIcon("Zap")}
2715
5783
  </div>
5784
+ <span class="text-sm text-gray-600">No events yet</span>
5785
+ <span class="max-w-[240px] text-xs leading-snug text-gray-400"
5786
+ >Events are recorded live. Run the agent to see them here.</span
5787
+ >
2716
5788
  </div>
2717
5789
  `;
2718
5790
  }
@@ -2823,23 +5895,30 @@ ${argsString}</pre
2823
5895
  </div>
2824
5896
  <div class="relative h-full w-full overflow-y-auto overflow-x-hidden">
2825
5897
  <table class="w-full table-fixed border-collapse text-xs box-border">
5898
+ <colgroup>
5899
+ <col style="width:${this.evtColWidths[0]}px">
5900
+ <col style="width:${this.evtColWidths[1]}px">
5901
+ <col style="width:${this.evtColWidths[2]}px">
5902
+ <col>
5903
+ </colgroup>
2826
5904
  <thead class="sticky top-0 z-10">
2827
5905
  <tr class="bg-white">
5906
+ ${["Agent", "Time", "Event Type"].map(
5907
+ (label, col) => html`
2828
5908
  <th
2829
5909
  class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
5910
+ style="position:relative;overflow:hidden;"
2830
5911
  >
2831
- Agent
2832
- </th>
2833
- <th
2834
- class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2835
- >
2836
- Time
2837
- </th>
2838
- <th
2839
- class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2840
- >
2841
- Event Type
2842
- </th>
5912
+ ${label}
5913
+ <div
5914
+ style="position:absolute;top:0;right:0;width:5px;height:100%;cursor:col-resize;user-select:none;background:transparent;"
5915
+ @pointerdown=${(e: PointerEvent) => this._onEvtColResizeStart(e, col)}
5916
+ @pointermove=${(e: PointerEvent) => this._onEvtColResizeMove(e)}
5917
+ @pointerup=${() => this._onEvtColResizeEnd()}
5918
+ @pointercancel=${() => this._onEvtColResizeEnd()}
5919
+ ></div>
5920
+ </th>`,
5921
+ )}
2843
5922
  <th
2844
5923
  class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
2845
5924
  >
@@ -2954,6 +6033,30 @@ ${prettyEvent}</pre
2954
6033
  this.requestUpdate();
2955
6034
  }
2956
6035
 
6036
+ private _onEvtColResizeStart(e: PointerEvent, col: number): void {
6037
+ e.preventDefault();
6038
+ e.stopPropagation();
6039
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
6040
+ this._evtColResize = {
6041
+ col,
6042
+ startX: e.clientX,
6043
+ startW: this.evtColWidths[col] ?? 0,
6044
+ };
6045
+ }
6046
+
6047
+ private _onEvtColResizeMove(e: PointerEvent): void {
6048
+ if (!this._evtColResize) return;
6049
+ const { col, startX, startW } = this._evtColResize;
6050
+ this.evtColWidths = this.evtColWidths.map((w, i) =>
6051
+ i === col ? Math.max(40, startW + (e.clientX - startX)) : w,
6052
+ );
6053
+ this.requestUpdate();
6054
+ }
6055
+
6056
+ private _onEvtColResizeEnd(): void {
6057
+ this._evtColResize = null;
6058
+ }
6059
+
2957
6060
  private handleClearEvents = (): void => {
2958
6061
  if (this.selectedContext === "all-agents") {
2959
6062
  this.agentEvents.clear();
@@ -3026,7 +6129,7 @@ ${prettyEvent}</pre
3026
6129
  <div class="flex items-start justify-between mb-4">
3027
6130
  <div class="flex items-center gap-3">
3028
6131
  <div
3029
- class="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600"
6132
+ class="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600 cpk-agent-icon"
3030
6133
  >
3031
6134
  ${this.renderIcon("Bot")}
3032
6135
  </div>
@@ -3062,7 +6165,7 @@ ${prettyEvent}</pre
3062
6165
  <div class="grid grid-cols-2 gap-4 md:grid-cols-4">
3063
6166
  <button
3064
6167
  type="button"
3065
- class="rounded-md bg-gray-50 px-3 py-2 text-left transition hover:bg-gray-100 cursor-pointer overflow-hidden"
6168
+ class="rounded-md bg-gray-50 px-3 py-2 text-left transition hover:bg-gray-100 cursor-pointer overflow-hidden cpk-stat-card"
3066
6169
  @click=${() => this.handleMenuSelect("ag-ui-events")}
3067
6170
  title="View all events in AG-UI Events"
3068
6171
  >
@@ -3073,7 +6176,9 @@ ${prettyEvent}</pre
3073
6176
  ${stats.totalEvents}
3074
6177
  </div>
3075
6178
  </button>
3076
- <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
6179
+ <div
6180
+ class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden cpk-stat-card"
6181
+ >
3077
6182
  <div class="truncate whitespace-nowrap text-xs text-gray-600">
3078
6183
  Messages
3079
6184
  </div>
@@ -3081,7 +6186,9 @@ ${prettyEvent}</pre
3081
6186
  ${stats.messages}
3082
6187
  </div>
3083
6188
  </div>
3084
- <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
6189
+ <div
6190
+ class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden cpk-stat-card"
6191
+ >
3085
6192
  <div class="truncate whitespace-nowrap text-xs text-gray-600">
3086
6193
  Tool Calls
3087
6194
  </div>
@@ -3089,7 +6196,9 @@ ${prettyEvent}</pre
3089
6196
  ${stats.toolCalls}
3090
6197
  </div>
3091
6198
  </div>
3092
- <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
6199
+ <div
6200
+ class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden cpk-stat-card"
6201
+ >
3093
6202
  <div class="truncate whitespace-nowrap text-xs text-gray-600">
3094
6203
  Errors
3095
6204
  </div>
@@ -3101,9 +6210,9 @@ ${prettyEvent}</pre
3101
6210
  </div>
3102
6211
 
3103
6212
  <!-- Current State Section -->
3104
- <div class="rounded-lg border border-gray-200 bg-white">
3105
- <div class="border-b border-gray-200 px-4 py-3">
3106
- <h4 class="text-sm font-semibold text-gray-900">Current State</h4>
6213
+ <div class="cpk-section-card">
6214
+ <div class="cpk-section-header">
6215
+ <h4>Current State</h4>
3107
6216
  </div>
3108
6217
  <div class="overflow-auto p-4">
3109
6218
  ${
@@ -3115,7 +6224,7 @@ ${prettyEvent}</pre
3115
6224
  `
3116
6225
  : html`
3117
6226
  <div
3118
- class="flex h-40 items-center justify-center text-xs text-gray-500"
6227
+ class="flex h-12 items-center justify-center text-xs text-gray-500"
3119
6228
  >
3120
6229
  <div class="flex items-center gap-2 text-gray-500">
3121
6230
  <span class="text-lg text-gray-400"
@@ -3130,32 +6239,20 @@ ${prettyEvent}</pre
3130
6239
  </div>
3131
6240
 
3132
6241
  <!-- Current Messages Section -->
3133
- <div class="rounded-lg border border-gray-200 bg-white">
3134
- <div class="border-b border-gray-200 px-4 py-3">
3135
- <h4 class="text-sm font-semibold text-gray-900">
3136
- Current Messages
3137
- </h4>
6242
+ <div class="cpk-section-card">
6243
+ <div class="cpk-section-header">
6244
+ <h4>Current Messages</h4>
3138
6245
  </div>
3139
6246
  <div class="overflow-auto">
3140
6247
  ${
3141
6248
  messages && messages.length > 0
3142
6249
  ? html`
3143
- <table class="w-full text-xs">
3144
- <thead class="bg-gray-50">
3145
- <tr>
3146
- <th
3147
- class="px-4 py-2 text-left font-medium text-gray-700"
3148
- >
3149
- Role
3150
- </th>
3151
- <th
3152
- class="px-4 py-2 text-left font-medium text-gray-700"
3153
- >
3154
- Content
3155
- </th>
3156
- </tr>
3157
- </thead>
3158
- <tbody class="divide-y divide-gray-200">
6250
+ <div class="w-full text-xs">
6251
+ <div class="flex bg-gray-50">
6252
+ <div class="w-40 shrink-0 px-4 py-2 font-medium text-gray-700">Role</div>
6253
+ <div class="flex-1 px-4 py-2 font-medium text-gray-700">Content</div>
6254
+ </div>
6255
+ <div class="divide-y divide-gray-200">
3159
6256
  ${messages.map((msg) => {
3160
6257
  const role = msg.role || "unknown";
3161
6258
  const roleColors: Record<string, string> = {
@@ -3173,27 +6270,23 @@ ${prettyEvent}</pre
3173
6270
  toolCalls.length > 0 ? "Invoked tool call" : "—";
3174
6271
 
3175
6272
  return html`
3176
- <tr>
3177
- <td class="px-4 py-2 align-top">
6273
+ <div class="flex items-start">
6274
+ <div class="w-40 shrink-0 px-4 py-2">
3178
6275
  <span
3179
- class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${
3180
- roleColors[role] || roleColors.unknown
3181
- }"
6276
+ class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${roleColors[role] || roleColors.unknown}"
3182
6277
  >
3183
6278
  ${role}
3184
6279
  </span>
3185
- </td>
3186
- <td class="px-4 py-2">
6280
+ </div>
6281
+ <div class="flex-1 px-4 py-2">
3187
6282
  ${
3188
6283
  hasContent
3189
6284
  ? html`<div
3190
- class="max-w-2xl whitespace-pre-wrap break-words text-gray-700"
6285
+ class="whitespace-pre-line break-words text-gray-700"
3191
6286
  >
3192
6287
  ${rawContent}
3193
6288
  </div>`
3194
- : html`<div
3195
- class="text-xs italic text-gray-400"
3196
- >
6289
+ : html`<div class="italic text-gray-400">
3197
6290
  ${contentFallback}
3198
6291
  </div>`
3199
6292
  }
@@ -3202,16 +6295,16 @@ ${prettyEvent}</pre
3202
6295
  ? this.renderToolCallDetails(toolCalls)
3203
6296
  : nothing
3204
6297
  }
3205
- </td>
3206
- </tr>
6298
+ </div>
6299
+ </div>
3207
6300
  `;
3208
6301
  })}
3209
- </tbody>
3210
- </table>
6302
+ </div>
6303
+ </div>
3211
6304
  `
3212
6305
  : html`
3213
6306
  <div
3214
- class="flex h-40 items-center justify-center text-xs text-gray-500"
6307
+ class="flex h-12 items-center justify-center text-xs text-gray-500"
3215
6308
  >
3216
6309
  <div class="flex items-center gap-2 text-gray-500">
3217
6310
  <span class="text-lg text-gray-400"
@@ -3300,22 +6393,51 @@ ${prettyEvent}</pre
3300
6393
  return;
3301
6394
  }
3302
6395
 
6396
+ const previousMenu = this.selectedMenu;
3303
6397
  this.selectedMenu = key;
3304
6398
 
3305
- // If switching to agents view and "all-agents" is selected, switch to default or first agent
6399
+ // If switching to agents view and "all-agents" is selected, switch to the most recently active agent
3306
6400
  if (key === "agents" && this.selectedContext === "all-agents") {
3307
6401
  const agentOptions = this.contextOptions.filter(
3308
6402
  (opt) => opt.key !== "all-agents",
3309
6403
  );
3310
6404
  if (agentOptions.length > 0) {
3311
- // Try to find "default" agent first
3312
- const defaultAgent = agentOptions.find((opt) => opt.key === "default");
3313
- this.selectedContext = defaultAgent
3314
- ? defaultAgent.key
6405
+ // Pick the agent with the most recent activity; fall back to first
6406
+ const mostRecent = agentOptions.reduce<{
6407
+ key: string;
6408
+ ts: number;
6409
+ } | null>((best, opt) => {
6410
+ const ts = this.getAgentStats(opt.key).lastActivity ?? -1;
6411
+ return best === null || ts > best.ts ? { key: opt.key, ts } : best;
6412
+ }, null);
6413
+ this.selectedContext = mostRecent
6414
+ ? mostRecent.key
3315
6415
  : agentOptions[0]!.key;
3316
6416
  }
3317
6417
  }
3318
6418
 
6419
+ // If leaving the agents view with multiple agents registered, restore
6420
+ // "all-agents" so the Events tab isn't silently filtered to one agent.
6421
+ if (previousMenu === "agents" && key !== "agents") {
6422
+ const agentCount = this.contextOptions.filter(
6423
+ (opt) => opt.key !== "all-agents",
6424
+ ).length;
6425
+ if (agentCount > 1) {
6426
+ this.selectedContext = "all-agents";
6427
+ }
6428
+ }
6429
+
6430
+ if (key === "threads") {
6431
+ this.autoSelectLatestThread();
6432
+ }
6433
+
6434
+ if (key === "ag-ui-events" || key === "agents") {
6435
+ requestAnimationFrame(() => {
6436
+ const scroller = this.shadowRoot?.getElementById("cpk-main-scroll");
6437
+ if (scroller) scroller.scrollTop = 0;
6438
+ });
6439
+ }
6440
+
3319
6441
  this.contextMenuOpen = false;
3320
6442
  this.persistState();
3321
6443
  this.requestUpdate();
@@ -3948,9 +7070,19 @@ ${prettyEvent}</pre
3948
7070
  <div class="mb-3">
3949
7071
  <h5 class="mb-1 text-xs font-semibold text-gray-700">ID</h5>
3950
7072
  <code
3951
- class="block rounded bg-white border border-gray-200 px-2 py-1 text-[10px] font-mono text-gray-600"
7073
+ class="font-mono text-xs font-medium text-gray-800 flex-1 truncate min-w-0"
3952
7074
  >${id}</code
3953
7075
  >
7076
+ <button
7077
+ type="button"
7078
+ class="cpk-copy-btn"
7079
+ @click=${(e: Event) => {
7080
+ e.stopPropagation();
7081
+ void this.copyContextValue(id, `${id}:id`);
7082
+ }}
7083
+ >
7084
+ ${this.copiedContextItems.has(`${id}:id`) ? "✓" : "Copy"}
7085
+ </button>
3954
7086
  </div>
3955
7087
  ${
3956
7088
  hasValue
@@ -3960,8 +7092,8 @@ ${prettyEvent}</pre
3960
7092
  Value
3961
7093
  </h5>
3962
7094
  <button
3963
- 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"
3964
7095
  type="button"
7096
+ class="cpk-copy-btn"
3965
7097
  @click=${(e: Event) => {
3966
7098
  e.stopPropagation();
3967
7099
  void this.copyContextValue(context.value, id);
@@ -3974,15 +7106,6 @@ ${prettyEvent}</pre
3974
7106
  }
3975
7107
  </button>
3976
7108
  </div>
3977
- <div
3978
- class="rounded-md border border-gray-200 bg-white p-3"
3979
- >
3980
- <pre
3981
- class="overflow-auto text-xs text-gray-800 max-h-96"
3982
- ><code>${this.formatContextValue(
3983
- context.value,
3984
- )}</code></pre>
3985
- </div>
3986
7109
  `
3987
7110
  : html`
3988
7111
  <div class="flex items-center justify-center py-4 text-xs text-gray-500">
@@ -4004,7 +7127,7 @@ ${prettyEvent}</pre
4004
7127
  }
4005
7128
 
4006
7129
  if (typeof value === "string") {
4007
- return value.length > 50 ? `${value.substring(0, 50)}...` : value;
7130
+ return value.length > 50 ? `${value.slice(0, 50)}...` : value;
4008
7131
  }
4009
7132
 
4010
7133
  if (typeof value === "number" || typeof value === "boolean") {
@@ -4112,70 +7235,34 @@ ${prettyEvent}</pre
4112
7235
  this.requestUpdate();
4113
7236
  }
4114
7237
 
4115
- private renderAnnouncementPanel() {
4116
- if (!this.isOpen) {
4117
- return nothing;
4118
- }
4119
-
4120
- // Ensure loading is triggered even if we mounted in an already-open state
4121
- this.ensureAnnouncementLoading();
4122
-
7238
+ private renderAnnouncementBanner() {
4123
7239
  if (!this.hasUnseenAnnouncement) {
4124
7240
  return nothing;
4125
7241
  }
4126
7242
 
4127
- if (!this.announcementLoaded && !this.announcementMarkdown) {
4128
- return html`<div
4129
- 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)]"
4130
- >
4131
- <div class="flex items-center gap-2 font-semibold">
4132
- <span
4133
- class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm"
4134
- >
4135
- ${this.renderIcon("Megaphone")}
4136
- </span>
4137
- <span>Loading latest announcement…</span>
4138
- </div>
4139
- </div>`;
4140
- }
4141
-
4142
- if (this.announcementLoadError) {
7243
+ if (!this.announcementLoaded && !this.announcementHtml) {
4143
7244
  return html`<div
4144
- 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)]"
7245
+ class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-slate-800"
4145
7246
  >
4146
- <div class="flex items-center gap-2 font-semibold">
4147
- <span
4148
- class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-rose-600 text-white shadow-sm"
4149
- >
4150
- ${this.renderIcon("Megaphone")}
4151
- </span>
4152
- <span>Announcement unavailable</span>
4153
- </div>
4154
- <p class="mt-2 text-xs text-rose-800">
4155
- We couldn’t load the latest notice. Please try opening the inspector
4156
- again.
4157
- </p>
7247
+ <span
7248
+ class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm"
7249
+ >
7250
+ ${this.renderIcon("Megaphone")}
7251
+ </span>
7252
+ <span>Loading latest announcement…</span>
4158
7253
  </div>`;
4159
7254
  }
4160
7255
 
4161
- if (!this.announcementMarkdown) {
7256
+ if (!this.announcementHtml) {
4162
7257
  return nothing;
4163
7258
  }
4164
7259
 
4165
- const content = this.announcementHtml
4166
- ? unsafeHTML(this.announcementHtml)
4167
- : html`<pre class="whitespace-pre-wrap text-sm text-gray-900">
4168
- ${this.announcementMarkdown}</pre
4169
- >`;
4170
-
4171
- return html`<div
4172
- 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)]"
4173
- >
7260
+ return html`<div class="mx-4 mt-3 mb-3 rounded-xl border border-slate-200 bg-white px-4 py-3">
4174
7261
  <div
4175
- class="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-900"
7262
+ class="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-900"
4176
7263
  >
4177
7264
  <span
4178
- class="inline-flex h-7 w-7 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm"
7265
+ class="inline-flex h-5 w-5 items-center justify-center rounded-md bg-slate-900 text-white shadow-sm"
4179
7266
  >
4180
7267
  ${this.renderIcon("Megaphone")}
4181
7268
  </span>
@@ -4186,12 +7273,34 @@ ${this.announcementMarkdown}</pre
4186
7273
  @click=${this.handleDismissAnnouncement}
4187
7274
  aria-label="Dismiss announcement"
4188
7275
  >
4189
- Dismiss
7276
+ ${this.renderIcon("X")}
4190
7277
  </button>
4191
7278
  </div>
4192
- <div class="announcement-content text-sm leading-relaxed text-gray-900">
4193
- ${content}
7279
+ <div class="announcement-body ${this.announcementExpanded ? "announcement-body--expanded" : "announcement-body--collapsed"}">
7280
+ <div
7281
+ class="announcement-content"
7282
+ @click=${this.handleAnnouncementContentClick}
7283
+ >
7284
+ ${unsafeHTML(this.announcementHtml)}
7285
+ </div>
7286
+ ${
7287
+ !this.announcementExpanded
7288
+ ? html`
7289
+ <div class="announcement-fade"></div>
7290
+ `
7291
+ : nothing
7292
+ }
4194
7293
  </div>
7294
+ <button
7295
+ class="announcement-toggle"
7296
+ type="button"
7297
+ @click=${() => {
7298
+ this.announcementExpanded = !this.announcementExpanded;
7299
+ this.requestUpdate();
7300
+ }}
7301
+ >
7302
+ ${this.announcementExpanded ? "Show less ↑" : "Show more ↓"}
7303
+ </button>
4195
7304
  </div>`;
4196
7305
  }
4197
7306
 
@@ -4266,7 +7375,6 @@ ${this.announcementMarkdown}</pre
4266
7375
 
4267
7376
  this.announcementTimestamp = timestamp;
4268
7377
  this.announcementPreviewText = previewText ?? "";
4269
- this.announcementMarkdown = markdown;
4270
7378
  this.hasUnseenAnnouncement =
4271
7379
  (!storedTimestamp || storedTimestamp !== timestamp) &&
4272
7380
  !!this.announcementPreviewText;
@@ -4276,7 +7384,11 @@ ${this.announcementMarkdown}</pre
4276
7384
 
4277
7385
  this.requestUpdate();
4278
7386
  } catch (error) {
4279
- this.announcementLoadError = error;
7387
+ // Swallowing here would hide non-network failures (malformed JSON, the
7388
+ // explicit "Malformed announcement payload" throw above, exceptions
7389
+ // from `convertMarkdownToHtml`). At minimum, surface in the console so
7390
+ // a stale announcement is debuggable.
7391
+ console.warn("[CopilotKit Inspector] Failed to load announcement", error);
4280
7392
  this.announcementLoaded = true;
4281
7393
  this.requestUpdate();
4282
7394
  }
@@ -4291,9 +7403,76 @@ ${this.announcementMarkdown}</pre
4291
7403
  const titleAttr = title ? ` title="${this.escapeHtmlAttr(title)}"` : "";
4292
7404
  return `<a href="${safeHref}" target="_blank" rel="noopener"${titleAttr}>${text}</a>`;
4293
7405
  };
4294
- return marked.parse(markdown, { renderer });
7406
+ renderer.code = (code, lang) => {
7407
+ const safeLang = (lang ?? "").replace(/[^a-z0-9-]/gi, "");
7408
+ const langClass = safeLang ? ` class="language-${safeLang}"` : "";
7409
+ const escaped = escapeHtml(code);
7410
+ const encoded = this.encodeBase64(code);
7411
+ return `<div class="announcement-code"><pre><code${langClass}>${escaped}</code></pre><div class="announcement-code__copy-shield"><button type="button" class="announcement-code__copy" data-copy="${encoded}" aria-label="Copy code">Copy</button></div></div>`;
7412
+ };
7413
+ return marked.parse(markdown, { renderer, async: false });
7414
+ }
7415
+
7416
+ private copyResetTimeouts = new WeakMap<HTMLButtonElement, number>();
7417
+
7418
+ private encodeBase64(value: string): string {
7419
+ if (typeof window === "undefined" || typeof window.btoa !== "function") {
7420
+ return "";
7421
+ }
7422
+ // btoa only accepts Latin-1; round-trip via TextEncoder to keep full UTF-8.
7423
+ const bytes = new TextEncoder().encode(value);
7424
+ let binary = "";
7425
+ for (const b of bytes) binary += String.fromCharCode(b);
7426
+ return window.btoa(binary);
7427
+ }
7428
+
7429
+ private decodeBase64(value: string): string {
7430
+ if (typeof window === "undefined" || typeof window.atob !== "function") {
7431
+ return "";
7432
+ }
7433
+ const decoded = window.atob(value);
7434
+ const bytes = new Uint8Array(decoded.length);
7435
+ for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
7436
+ return new TextDecoder().decode(bytes);
4295
7437
  }
4296
7438
 
7439
+ private handleAnnouncementContentClick = (event: Event): void => {
7440
+ const target = event.target instanceof HTMLElement ? event.target : null;
7441
+ const button = target?.closest(".announcement-code__copy");
7442
+ if (!(button instanceof HTMLButtonElement)) {
7443
+ return;
7444
+ }
7445
+ event.preventDefault();
7446
+ event.stopPropagation();
7447
+ const encoded = button.getAttribute("data-copy") ?? "";
7448
+ const code = this.decodeBase64(encoded);
7449
+ if (!code) {
7450
+ return;
7451
+ }
7452
+ const showCopied = () => {
7453
+ const existing = this.copyResetTimeouts.get(button);
7454
+ if (existing !== undefined) {
7455
+ window.clearTimeout(existing);
7456
+ }
7457
+ button.setAttribute("data-copied", "true");
7458
+ button.setAttribute("aria-label", "Code copied");
7459
+ button.textContent = "Copied";
7460
+ const id = window.setTimeout(() => {
7461
+ button.removeAttribute("data-copied");
7462
+ button.setAttribute("aria-label", "Copy code");
7463
+ button.textContent = "Copy";
7464
+ this.copyResetTimeouts.delete(button);
7465
+ }, 1500);
7466
+ this.copyResetTimeouts.set(button, id);
7467
+ };
7468
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
7469
+ navigator.clipboard.writeText(code).then(showCopied, () => {
7470
+ // ignore — clipboard may be unavailable (insecure context, denied
7471
+ // permission, focus loss); button silently stays in idle state.
7472
+ });
7473
+ }
7474
+ };
7475
+
4297
7476
  private appendRefParam(href: string): string {
4298
7477
  try {
4299
7478
  const url = new URL(
@@ -4312,12 +7491,7 @@ ${this.announcementMarkdown}</pre
4312
7491
  }
4313
7492
 
4314
7493
  private escapeHtmlAttr(value: string): string {
4315
- return value
4316
- .replace(/&/g, "&amp;")
4317
- .replace(/</g, "&lt;")
4318
- .replace(/>/g, "&gt;")
4319
- .replace(/"/g, "&quot;")
4320
- .replace(/'/g, "&#39;");
7494
+ return escapeHtml(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4321
7495
  }
4322
7496
 
4323
7497
  private loadStoredAnnouncementTimestamp(): string | null {