@copilotkit/web-inspector 1.61.1 → 1.62.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
@@ -28,7 +28,7 @@ import type {
28
28
  DockMode,
29
29
  Position,
30
30
  Size,
31
- } from "./lib/types";
31
+ } from "./lib/types.js";
32
32
  import {
33
33
  applyAnchorPosition as applyAnchorPositionHelper,
34
34
  centerContext as centerContextHelper,
@@ -37,7 +37,7 @@ import {
37
37
  updateAnchorFromPosition as updateAnchorFromPositionHelper,
38
38
  updateSizeFromElement,
39
39
  clampSize as clampSizeToViewport,
40
- } from "./lib/context-helpers";
40
+ } from "./lib/context-helpers.js";
41
41
  import {
42
42
  loadInspectorState,
43
43
  saveInspectorState,
@@ -45,19 +45,30 @@ import {
45
45
  isValidPosition,
46
46
  isValidSize,
47
47
  isValidDockMode,
48
- } from "./lib/persistence";
49
- import type { PersistedState } from "./lib/persistence";
48
+ } from "./lib/persistence.js";
49
+ import type { PersistedState } from "./lib/persistence.js";
50
50
  import {
51
51
  TELEMETRY_DOCS_URL,
52
52
  ensureTelemetryDistinctId,
53
+ getRuntimeUrlType,
53
54
  getTelemetryDistinctIdForUrl,
54
55
  maybeShowDisclosure,
55
56
  trackBannerClicked,
56
57
  trackBannerViewed,
58
+ trackTalkToEngineerClicked,
59
+ trackThreadsEmptyEnabledViewed,
60
+ trackThreadsEnabledViewed,
61
+ trackThreadsIntelligenceSignupClicked,
62
+ trackThreadsLockedViewed,
57
63
  trackThreadsTabClicked,
58
- } from "./lib/telemetry";
64
+ trackThreadsTalkToEngineerClicked,
65
+ } from "./lib/telemetry.js";
66
+ import type { InspectorThreadTelemetryProps } from "./lib/telemetry.js";
67
+
68
+ export type { Anchor } from "./lib/types.js";
59
69
 
60
70
  export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const;
71
+ export const THREAD_INSPECTOR_TAG = "cpk-thread-inspector" as const;
61
72
 
62
73
  type LucideIconName = keyof typeof icons;
63
74
 
@@ -88,6 +99,10 @@ const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 700 };
88
99
  const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar
89
100
  const MAX_AGENT_EVENTS = 200;
90
101
  const MAX_TOTAL_EVENTS = 500;
102
+ const INTELLIGENCE_SIGNUP_URL = "https://go.copilotkit.ai/intelligence-signup";
103
+ const TALK_TO_ENGINEER_URL = "https://www.copilotkit.ai/talk-to-an-engineer";
104
+
105
+ type ThreadServiceStatus = "available" | "unavailable" | "unknown" | "error";
91
106
 
92
107
  type InspectorAgentEventType =
93
108
  | "RUN_STARTED"
@@ -186,15 +201,62 @@ type InspectorEvent = {
186
201
 
187
202
  // ─── Thread details types ────────────────────────────────────────────────────
188
203
 
189
- interface ApiThreadMessage {
204
+ export type ThreadDebuggerProviderLoadOptions = {
205
+ signal: AbortSignal;
206
+ };
207
+
208
+ export type ThreadDebuggerToolCall = {
209
+ id: string;
210
+ name: string;
211
+ args: string | Record<string, unknown>;
212
+ };
213
+
214
+ export type ThreadDebuggerMessage = {
190
215
  id: string;
191
216
  role: string;
192
217
  content?: string;
193
- toolCalls?: Array<{ id: string; name: string; args: string }>;
218
+ toolCalls?: ThreadDebuggerToolCall[];
194
219
  toolCallId?: string;
195
220
  /** Present when role === "activity" (Generative UI output). */
196
221
  activityType?: string;
197
- }
222
+ };
223
+
224
+ export type ThreadDebuggerEvent = {
225
+ type: string;
226
+ timestamp: string | number;
227
+ payload?: Record<string, unknown>;
228
+ [key: string]: unknown;
229
+ };
230
+
231
+ export type ThreadDebuggerMetadata = {
232
+ id: string;
233
+ name?: string | null;
234
+ agentId?: string | null;
235
+ endUserId?: string | null;
236
+ createdById?: string | null;
237
+ status?: string | null;
238
+ createdAt?: string | null;
239
+ updatedAt?: string | null;
240
+ };
241
+
242
+ export type ThreadDebuggerProvider = {
243
+ getThreadMetadata?: (
244
+ threadId: string,
245
+ options: ThreadDebuggerProviderLoadOptions,
246
+ ) => Promise<ThreadDebuggerMetadata | null>;
247
+ getMessages?: (
248
+ threadId: string,
249
+ options: ThreadDebuggerProviderLoadOptions,
250
+ ) => Promise<ThreadDebuggerMessage[]>;
251
+ getEvents?: (
252
+ threadId: string,
253
+ options: ThreadDebuggerProviderLoadOptions,
254
+ ) => Promise<ThreadDebuggerEvent[]>;
255
+ getState?: (
256
+ threadId: string,
257
+ options: ThreadDebuggerProviderLoadOptions,
258
+ ) => Promise<Record<string, unknown> | null>;
259
+ };
198
260
 
199
261
  interface ConversationUser {
200
262
  id: string;
@@ -268,9 +330,39 @@ interface ApiAgentEvent {
268
330
  type: string;
269
331
  timestamp: string | number;
270
332
  payload: Record<string, unknown>;
333
+ sourceIndex?: number;
334
+ rawEvent?: ThreadDebuggerEvent;
271
335
  }
272
336
 
273
- type ThreadDetailsTab = "conversation" | "agent-state" | "ag-ui-events";
337
+ type ThreadDetailsTab = "timeline" | "state" | "raw-events";
338
+ type ThreadDetailsPanelCacheSlot = ThreadDetailsTab | "timeline-fallback";
339
+
340
+ type TimelineItemKind =
341
+ | "message"
342
+ | "tool"
343
+ | "state"
344
+ | "run"
345
+ | "event"
346
+ | "warning";
347
+
348
+ type TimelineItem = {
349
+ id: string;
350
+ kind: TimelineItemKind;
351
+ title: string;
352
+ body?: string;
353
+ timestamp: string | number;
354
+ sourceIndex: number;
355
+ severity?: "warning" | "error";
356
+ details?: Record<string, unknown>;
357
+ };
358
+
359
+ type RuntimeEventsFetchResult =
360
+ | { status: "available"; events: ThreadDebuggerEvent[] }
361
+ | { status: "not-available" };
362
+
363
+ type RuntimeStateFetchResult =
364
+ | { status: "available"; state: Record<string, unknown> | null }
365
+ | { status: "not-available" };
274
366
 
275
367
  // ─── JSON syntax highlighter ─────────────────────────────────────────────────
276
368
  // Inline-styled so shadow DOM encapsulation preserves colors when the output
@@ -334,9 +426,10 @@ function eventColors(type: string): { bg: string; fg: string } {
334
426
  return { bg: "rgba(133,236,206,0.15)", fg: "#189370" };
335
427
  if (type.startsWith("STATE"))
336
428
  return { bg: "rgba(190,194,255,0.102)", fg: "#5558B2" };
429
+ if (type === "RUN_ERROR" || type === "ERROR")
430
+ return { bg: "rgba(250,95,103,0.13)", fg: "#c0333a" };
337
431
  if (type.startsWith("RUN_") || type.startsWith("STEP_"))
338
432
  return { bg: "rgba(255,172,77,0.2)", fg: "#996300" };
339
- if (type === "ERROR") return { bg: "rgba(250,95,103,0.13)", fg: "#c0333a" };
340
433
  return { bg: "#F7F7F9", fg: "#838389" };
341
434
  }
342
435
 
@@ -603,28 +696,30 @@ class CpkThreadList extends LitElement {
603
696
  ${
604
697
  this.errorMessage
605
698
  ? html`
606
- <svg
607
- width="24"
608
- height="24"
609
- viewBox="0 0 24 24"
610
- fill="none"
611
- stroke="currentColor"
612
- stroke-width="1.5"
613
- stroke-linecap="round"
614
- stroke-linejoin="round"
615
- class="cpk-tl__empty-icon"
699
+ <svg
700
+ width="24"
701
+ height="24"
702
+ viewBox="0 0 24 24"
703
+ fill="none"
704
+ stroke="currentColor"
705
+ stroke-width="1.5"
706
+ stroke-linecap="round"
707
+ stroke-linejoin="round"
708
+ class="cpk-tl__empty-icon"
709
+ >
710
+ <circle cx="12" cy="12" r="10" />
711
+ <line x1="12" y1="8" x2="12" y2="12" />
712
+ <line x1="12" y1="16" x2="12.01" y2="16" />
713
+ </svg>
714
+ <div>
715
+ Failed to load threads
716
+ <div
717
+ style="font-size:11px;margin-top:4px;color:#c0333a;"
616
718
  >
617
- <circle cx="12" cy="12" r="10" />
618
- <line x1="12" y1="8" x2="12" y2="12" />
619
- <line x1="12" y1="16" x2="12.01" y2="16" />
620
- </svg>
621
- <div>
622
- Failed to load threads
623
- <div style="font-size:11px;margin-top:4px;color:#c0333a;">
624
- ${this.errorMessage}
625
- </div>
719
+ ${this.errorMessage}
626
720
  </div>
627
- `
721
+ </div>
722
+ `
628
723
  : this.threads.length === 0
629
724
  ? html`
630
725
  <svg
@@ -656,21 +751,14 @@ class CpkThreadList extends LitElement {
656
751
  }
657
752
  }
658
753
 
659
- // ─── cpk-thread-details ──────────────────────────────────────────────────────
660
- // Renders the selected thread's conversation, agent state, and AG-UI events.
661
- // Conversation comes from the runtime's `/threads/:id/messages` endpoint
662
- // (always thread-accurate). Agent state and AG-UI events accept live inputs
663
- // (`agentStateInput`, `agentEventsInput`) from the parent inspector's ongoing
664
- // agent subscriptions; when those are absent we fall back to the per-thread
665
- // fetched data via `/threads/:id/{events,state}`.
666
-
667
- // Exported (with the underscore-prefixed name signalling internal/test-only)
668
- // so unit tests can pin down the per-panel template-cache invariants without
669
- // reaching through `customElements`. Production consumers continue to use the
670
- // `cpk-thread-details` custom element registered below.
671
- export class ɵCpkThreadDetails extends LitElement {
754
+ // ─── cpk-thread-inspector ────────────────────────────────────────────────────
755
+ // Renders the selected thread's read-only timeline, state, raw AG-UI events,
756
+ // and compact technical metadata. External hosts provide a ThreadDebuggerProvider;
757
+ // the legacy CopilotKit Inspector wrapper can still pass runtime URL inputs.
758
+ export class CpkThreadInspector extends LitElement {
672
759
  static properties = {
673
760
  threadId: { attribute: false },
761
+ provider: { attribute: false },
674
762
  thread: { attribute: false },
675
763
  runtimeUrl: { attribute: false },
676
764
  headers: { attribute: false },
@@ -679,6 +767,7 @@ export class ɵCpkThreadDetails extends LitElement {
679
767
  agentEventsInput: { attribute: false },
680
768
  liveMessageVersion: { attribute: false },
681
769
  _tab: { state: true },
770
+ _fetchedMetadata: { state: true },
682
771
  _conversation: { state: true },
683
772
  _fetchedEvents: { state: true },
684
773
  _fetchedState: { state: true },
@@ -699,7 +788,8 @@ export class ɵCpkThreadDetails extends LitElement {
699
788
  };
700
789
 
701
790
  threadId: string | null = null;
702
- thread: ɵThread | null = null;
791
+ provider: ThreadDebuggerProvider | null = null;
792
+ thread: ThreadDebuggerMetadata | ɵThread | null = null;
703
793
  runtimeUrl = "";
704
794
  headers: Record<string, string> = {};
705
795
  threadInspectionAvailable = false;
@@ -713,7 +803,8 @@ export class ɵCpkThreadDetails extends LitElement {
713
803
  */
714
804
  liveMessageVersion = 0;
715
805
 
716
- private _tab: ThreadDetailsTab = "conversation";
806
+ private _tab: ThreadDetailsTab = "timeline";
807
+ private _fetchedMetadata: ThreadDebuggerMetadata | null = null;
717
808
  private _conversation: ConversationItem[] = [];
718
809
  private _fetchedEvents: ApiAgentEvent[] | null = null;
719
810
  private _fetchedState: Record<string, unknown> | null = null;
@@ -746,9 +837,9 @@ export class ɵCpkThreadDetails extends LitElement {
746
837
  * switching back to AG-UI Events on a thread with hundreds of events
747
838
  * triggers a multi-second DOM-creation pass each time.
748
839
  *
749
- * Reset to {"conversation"} when the selected thread changes.
840
+ * Reset to {"timeline"} when the selected thread changes.
750
841
  */
751
- private _activatedTabs: Set<ThreadDetailsTab> = new Set(["conversation"]);
842
+ private _activatedTabs: Set<ThreadDetailsTab> = new Set(["timeline"]);
752
843
  /**
753
844
  * Memoized per-panel templates keyed by the inputs they render from.
754
845
  * When the underlying data hasn't changed (same `_conversation` /
@@ -760,9 +851,17 @@ export class ɵCpkThreadDetails extends LitElement {
760
851
  * reference; if any element flips, the cache misses and rebuilds.
761
852
  */
762
853
  private _panelTplCache: Map<
763
- ThreadDetailsTab,
854
+ ThreadDetailsPanelCacheSlot,
764
855
  { key: readonly unknown[]; tpl: TemplateResult }
765
856
  > = new Map();
857
+ private _timelineItemsCache: {
858
+ events: ApiAgentEvent[];
859
+ items: TimelineItem[];
860
+ } | null = null;
861
+ private _liveEventsWithSourceIndexCache: {
862
+ events: ApiAgentEvent[];
863
+ indexedEvents: ApiAgentEvent[];
864
+ } | null = null;
766
865
  /**
767
866
  * Tracks whether we've fetched events for the current thread yet. Events
768
867
  * fetch lazily on first sub-tab click so a large response's JSON.parse
@@ -775,8 +874,9 @@ export class ɵCpkThreadDetails extends LitElement {
775
874
  * lazy-load reasoning as `_eventsFetched`.
776
875
  */
777
876
  private _stateFetched = false;
778
- private _lastFetchedThreadId: string | null = null;
877
+ private _lastLoadKey: string | null = null;
779
878
  private _lastSeenLiveMessageVersion = 0;
879
+ private _metadataAbort: AbortController | null = null;
780
880
  private _messagesAbort: AbortController | null = null;
781
881
  private _eventsAbort: AbortController | null = null;
782
882
  private _stateAbort: AbortController | null = null;
@@ -786,18 +886,53 @@ export class ɵCpkThreadDetails extends LitElement {
786
886
  private _dividerStartWidth = 0;
787
887
 
788
888
  static readonly COLLAPSE_THRESHOLD = 800;
789
- private static readonly TAB_LIST: ReadonlyArray<{
889
+ static readonly TAB_LIST: ReadonlyArray<{
790
890
  id: ThreadDetailsTab;
791
891
  label: string;
792
892
  }> = [
793
- { id: "conversation", label: "Conversation" },
794
- { id: "agent-state", label: "Agent State" },
795
- { id: "ag-ui-events", label: "AG-UI Events" },
893
+ { id: "timeline", label: "Timeline" },
894
+ { id: "raw-events", label: "Raw AG-UI Events" },
895
+ { id: "state", label: "State" },
796
896
  ];
797
897
 
898
+ private static providerIds = new WeakMap<ThreadDebuggerProvider, number>();
899
+ private static nextProviderId = 1;
900
+
901
+ private static providerLoadKey(
902
+ provider: ThreadDebuggerProvider | null,
903
+ ): string {
904
+ if (!provider) return "provider:none";
905
+ let id = CpkThreadInspector.providerIds.get(provider);
906
+ if (!id) {
907
+ id = CpkThreadInspector.nextProviderId;
908
+ CpkThreadInspector.nextProviderId += 1;
909
+ CpkThreadInspector.providerIds.set(provider, id);
910
+ }
911
+ return [
912
+ `provider:${id}`,
913
+ provider.getThreadMetadata ? "metadata:1" : "metadata:0",
914
+ provider.getMessages ? "messages:1" : "messages:0",
915
+ provider.getEvents ? "events:1" : "events:0",
916
+ provider.getState ? "state:1" : "state:0",
917
+ ].join("|");
918
+ }
919
+
920
+ /**
921
+ * Build a deterministic signature for runtime fetch headers so auth/CSRF
922
+ * changes invalidate cached thread data even when the selected thread is
923
+ * otherwise unchanged.
924
+ */
925
+ private static headersLoadKey(headers: Record<string, string>): string {
926
+ return JSON.stringify(
927
+ Object.entries(headers).sort(([leftKey], [rightKey]) =>
928
+ leftKey.localeCompare(rightKey),
929
+ ),
930
+ );
931
+ }
932
+
798
933
  private renderTabContent(id: ThreadDetailsTab): TemplateResult {
799
- if (id === "conversation") return this.renderConversation();
800
- if (id === "agent-state") return this.renderState();
934
+ if (id === "timeline") return this.renderTimeline();
935
+ if (id === "state") return this.renderState();
801
936
  return this.renderEvents();
802
937
  }
803
938
 
@@ -830,10 +965,10 @@ export class ɵCpkThreadDetails extends LitElement {
830
965
  // the entire panel for seconds — including making the tab buttons
831
966
  // themselves feel unresponsive.
832
967
  if (!this.threadId) return;
833
- if (id === "ag-ui-events" && !this._eventsFetched) {
968
+ if ((id === "timeline" || id === "raw-events") && !this._eventsFetched) {
834
969
  this._eventsFetched = true;
835
970
  void this.fetchEvents(this.threadId);
836
- } else if (id === "agent-state" && !this._stateFetched) {
971
+ } else if (id === "state" && !this._stateFetched) {
837
972
  this._stateFetched = true;
838
973
  void this.fetchState(this.threadId);
839
974
  }
@@ -965,6 +1100,42 @@ export class ɵCpkThreadDetails extends LitElement {
965
1100
  flex-shrink: 0;
966
1101
  }
967
1102
 
1103
+ .cpk-td__metadata-strip {
1104
+ display: flex;
1105
+ gap: 6px;
1106
+ flex-wrap: wrap;
1107
+ padding: 10px 16px;
1108
+ border-bottom: 1px solid #e9e9ef;
1109
+ background: #fbfbfd;
1110
+ flex-shrink: 0;
1111
+ }
1112
+
1113
+ .cpk-td__metadata-pill {
1114
+ display: inline-flex;
1115
+ align-items: center;
1116
+ gap: 5px;
1117
+ max-width: 220px;
1118
+ padding: 3px 7px;
1119
+ border: 1px solid #e9e9ef;
1120
+ border-radius: 5px;
1121
+ background: #ffffff;
1122
+ color: #57575b;
1123
+ font-family: "Spline Sans Mono", monospace;
1124
+ font-size: 10px;
1125
+ white-space: nowrap;
1126
+ }
1127
+
1128
+ .cpk-td__metadata-label {
1129
+ color: #838389;
1130
+ text-transform: uppercase;
1131
+ font-size: 9px;
1132
+ }
1133
+
1134
+ .cpk-td__metadata-value {
1135
+ overflow: hidden;
1136
+ text-overflow: ellipsis;
1137
+ }
1138
+
968
1139
  /*
969
1140
  * Each tab's content is wrapped in this panel so the keep-mounted
970
1141
  * inactive panels can be hidden via display:none without disturbing
@@ -1185,6 +1356,81 @@ export class ɵCpkThreadDetails extends LitElement {
1185
1356
  background: #e9e9ef;
1186
1357
  }
1187
1358
 
1359
+ /* ── Interaction timeline ───────────────────────────────────────── */
1360
+ .cpk-td__timeline-item {
1361
+ border: 1px solid #e9e9ef;
1362
+ border-radius: 6px;
1363
+ background: #ffffff;
1364
+ overflow: hidden;
1365
+ }
1366
+
1367
+ .cpk-td__timeline-item--warning {
1368
+ border-color: rgba(250, 95, 103, 0.35);
1369
+ background: rgba(250, 95, 103, 0.04);
1370
+ }
1371
+
1372
+ .cpk-td__timeline-header {
1373
+ display: flex;
1374
+ align-items: center;
1375
+ gap: 8px;
1376
+ padding: 7px 10px;
1377
+ background: #f7f7f9;
1378
+ }
1379
+
1380
+ .cpk-td__timeline-kind {
1381
+ font-family: "Spline Sans Mono", monospace;
1382
+ font-size: 9px;
1383
+ font-weight: 600;
1384
+ text-transform: uppercase;
1385
+ color: #5558b2;
1386
+ }
1387
+
1388
+ .cpk-td__timeline-title {
1389
+ flex: 1;
1390
+ min-width: 0;
1391
+ font-size: 12px;
1392
+ font-weight: 500;
1393
+ color: #010507;
1394
+ overflow: hidden;
1395
+ text-overflow: ellipsis;
1396
+ white-space: nowrap;
1397
+ }
1398
+
1399
+ .cpk-td__timeline-time {
1400
+ font-family: "Spline Sans Mono", monospace;
1401
+ font-size: 9px;
1402
+ color: #838389;
1403
+ flex-shrink: 0;
1404
+ }
1405
+
1406
+ .cpk-td__timeline-body {
1407
+ padding: 9px 10px;
1408
+ font-size: 12px;
1409
+ line-height: 1.55;
1410
+ color: #57575b;
1411
+ white-space: pre-wrap;
1412
+ word-break: break-word;
1413
+ border-top: 1px solid #e9e9ef;
1414
+ }
1415
+
1416
+ .cpk-td__source-link {
1417
+ margin: 0;
1418
+ padding: 0;
1419
+ border: none;
1420
+ background: transparent;
1421
+ color: #5558b2;
1422
+ cursor: pointer;
1423
+ font-family: "Spline Sans Mono", monospace;
1424
+ font-size: 9px;
1425
+ text-decoration: underline;
1426
+ text-underline-offset: 2px;
1427
+ flex-shrink: 0;
1428
+ }
1429
+
1430
+ .cpk-td__source-link:hover {
1431
+ color: #010507;
1432
+ }
1433
+
1188
1434
  /* ── Generative UI ──────────────────────────────────────────────── */
1189
1435
  @keyframes cpk-genui-enter {
1190
1436
  from {
@@ -1394,36 +1640,27 @@ export class ɵCpkThreadDetails extends LitElement {
1394
1640
  `;
1395
1641
 
1396
1642
  updated(_changed: Map<string, unknown>): void {
1397
- if (this.threadId !== this._lastFetchedThreadId) {
1398
- this._lastFetchedThreadId = this.threadId;
1643
+ const loadKey = this.currentLoadKey();
1644
+ if (loadKey !== this._lastLoadKey) {
1645
+ this._lastLoadKey = loadKey;
1399
1646
  this._lastSeenLiveMessageVersion = this.liveMessageVersion;
1400
- this._tab = "conversation";
1401
- this._activatedTabs = new Set(["conversation"]);
1402
- this._panelTplCache = new Map();
1403
- this._expandedTools = new Set();
1404
- this._expandedMessages = new Set();
1405
- this._messagesAbort?.abort();
1406
- this._messagesAbort = null;
1407
- this._eventsAbort?.abort();
1408
- this._eventsAbort = null;
1409
- this._stateAbort?.abort();
1410
- this._stateAbort = null;
1411
- // Reset cleared so the next click into events/state triggers a fresh
1412
- // fetch. Eagerly clear `_fetchedEvents` / `_fetchedState` so the empty
1413
- // state doesn't briefly show last thread's data.
1414
- this._eventsFetched = false;
1415
- this._stateFetched = false;
1416
- this._fetchedEvents = null;
1417
- this._fetchedState = null;
1647
+ this.resetLoadedThreadData();
1418
1648
 
1419
1649
  if (this.threadId) {
1420
- // Conversation is the default tab and shows immediately on thread
1421
- // open, so fetch eagerly. Events and state are only visible once the
1422
- // user clicks their sub-tab; deferring those fetches prevents a long
1423
- // JSON.parse of a large events payload from blocking the main thread
1424
- // before the user has even shown intent to view them.
1425
- void this.fetchMessages(this.threadId);
1650
+ // Timeline is the default tab and should be event-derived. Fetch
1651
+ // events eagerly; the raw tab reuses the same response when opened.
1652
+ void this.fetchMetadata(this.threadId);
1653
+ if (this.canFetchEvents()) {
1654
+ this._eventsFetched = true;
1655
+ void this.fetchEvents(this.threadId);
1656
+ } else {
1657
+ // Last-resort compatibility path for consumers that only implement
1658
+ // messages. New integrations should provide events so Timeline can
1659
+ // expose source references and decode warnings.
1660
+ void this.fetchMessages(this.threadId);
1661
+ }
1426
1662
  } else {
1663
+ this._fetchedMetadata = null;
1427
1664
  this._conversation = [];
1428
1665
  }
1429
1666
  } else if (
@@ -1444,6 +1681,90 @@ export class ɵCpkThreadDetails extends LitElement {
1444
1681
  }
1445
1682
  }
1446
1683
 
1684
+ private canFetchMessages(): boolean {
1685
+ return (
1686
+ !!this.provider?.getMessages ||
1687
+ (!!this.runtimeUrl && this.threadInspectionAvailable)
1688
+ );
1689
+ }
1690
+
1691
+ private canFetchEvents(): boolean {
1692
+ return (
1693
+ !!this.provider?.getEvents ||
1694
+ (!!this.runtimeUrl && this.threadInspectionAvailable)
1695
+ );
1696
+ }
1697
+
1698
+ private canFetchState(): boolean {
1699
+ return (
1700
+ !!this.provider?.getState ||
1701
+ (!!this.runtimeUrl && this.threadInspectionAvailable)
1702
+ );
1703
+ }
1704
+
1705
+ private currentLoadKey(): string {
1706
+ return [
1707
+ this.threadId ?? "thread:none",
1708
+ CpkThreadInspector.providerLoadKey(this.provider),
1709
+ `runtime:${this.runtimeUrl}`,
1710
+ `headers:${CpkThreadInspector.headersLoadKey(this.headers)}`,
1711
+ `inspect:${this.threadInspectionAvailable ? "1" : "0"}`,
1712
+ ].join("||");
1713
+ }
1714
+
1715
+ private resetLoadedThreadData(): void {
1716
+ this._tab = "timeline";
1717
+ this._activatedTabs = new Set(["timeline"]);
1718
+ this._panelTplCache = new Map();
1719
+ this._timelineItemsCache = null;
1720
+ this._liveEventsWithSourceIndexCache = null;
1721
+ this._expandedTools = new Set();
1722
+ this._expandedMessages = new Set();
1723
+ this._metadataAbort?.abort();
1724
+ this._metadataAbort = null;
1725
+ this._messagesAbort?.abort();
1726
+ this._messagesAbort = null;
1727
+ this._eventsAbort?.abort();
1728
+ this._eventsAbort = null;
1729
+ this._stateAbort?.abort();
1730
+ this._stateAbort = null;
1731
+ // Reset cleared so the next click into events/state triggers a fresh
1732
+ // fetch. Eagerly clear fetched data so a provider/runtime swap cannot
1733
+ // briefly show the old source's values for the same threadId.
1734
+ this._eventsFetched = false;
1735
+ this._stateFetched = false;
1736
+ this._eventsNotAvailable = false;
1737
+ this._stateNotAvailable = false;
1738
+ this._loadingMessages = false;
1739
+ this._loadingEvents = false;
1740
+ this._loadingState = false;
1741
+ this._messagesError = null;
1742
+ this._eventsError = null;
1743
+ this._stateError = null;
1744
+ this._fetchedMetadata = null;
1745
+ this._conversation = [];
1746
+ this._fetchedEvents = null;
1747
+ this._fetchedState = null;
1748
+ }
1749
+
1750
+ private async fetchMetadata(threadId: string): Promise<void> {
1751
+ if (!this.provider?.getThreadMetadata) return;
1752
+ this._metadataAbort?.abort();
1753
+ const controller = new AbortController();
1754
+ this._metadataAbort = controller;
1755
+ try {
1756
+ const metadata = await this.provider.getThreadMetadata(threadId, {
1757
+ signal: controller.signal,
1758
+ });
1759
+ if (controller.signal.aborted || this.threadId !== threadId) return;
1760
+ this._fetchedMetadata = metadata;
1761
+ } catch (err) {
1762
+ if (err instanceof Error && err.name === "AbortError") return;
1763
+ if (this.threadId !== threadId) return;
1764
+ this._fetchedMetadata = null;
1765
+ }
1766
+ }
1767
+
1447
1768
  /**
1448
1769
  * Fetch the canonical conversation for `threadId` from the runtime.
1449
1770
  *
@@ -1458,10 +1779,11 @@ export class ɵCpkThreadDetails extends LitElement {
1458
1779
  threadId: string,
1459
1780
  silent: boolean = false,
1460
1781
  ): Promise<void> {
1461
- if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1782
+ if (!this.canFetchMessages()) {
1462
1783
  if (!silent) this._conversation = [];
1463
1784
  return;
1464
1785
  }
1786
+ this._messagesAbort?.abort();
1465
1787
  const controller = new AbortController();
1466
1788
  this._messagesAbort = controller;
1467
1789
  if (!silent) {
@@ -1469,15 +1791,13 @@ export class ɵCpkThreadDetails extends LitElement {
1469
1791
  this._messagesError = null;
1470
1792
  }
1471
1793
  try {
1472
- const res = await fetch(
1473
- `${this.runtimeUrl}/threads/${encodeURIComponent(threadId)}/messages`,
1474
- { headers: { ...this.headers }, signal: controller.signal },
1475
- );
1476
- if (controller.signal.aborted || this.threadId !== threadId) return;
1477
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1478
- const data = (await res.json()) as { messages: ApiThreadMessage[] };
1794
+ const messages = this.provider?.getMessages
1795
+ ? await this.provider.getMessages(threadId, {
1796
+ signal: controller.signal,
1797
+ })
1798
+ : await this.fetchRuntimeMessages(threadId, controller.signal);
1479
1799
  if (controller.signal.aborted || this.threadId !== threadId) return;
1480
- this._conversation = this.mapMessages(data.messages);
1800
+ this._conversation = this.mapMessages(messages);
1481
1801
  } catch (err) {
1482
1802
  if (err instanceof Error && err.name === "AbortError") return;
1483
1803
  if (!silent) {
@@ -1495,45 +1815,51 @@ export class ɵCpkThreadDetails extends LitElement {
1495
1815
  }
1496
1816
 
1497
1817
  private async fetchEvents(threadId: string): Promise<void> {
1498
- this._eventsNotAvailable = false;
1499
- if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1818
+ if (!this.canFetchEvents()) {
1500
1819
  this._fetchedEvents = null;
1501
1820
  return;
1502
1821
  }
1822
+ this._eventsAbort?.abort();
1503
1823
  const controller = new AbortController();
1504
1824
  this._eventsAbort = controller;
1505
1825
  this._loadingEvents = true;
1506
1826
  this._eventsError = null;
1507
1827
  try {
1508
- const res = await fetch(
1509
- `${this.runtimeUrl}/threads/${encodeURIComponent(threadId)}/events`,
1510
- { headers: { ...this.headers }, signal: controller.signal },
1511
- );
1828
+ const result = this.provider?.getEvents
1829
+ ? {
1830
+ status: "available" as const,
1831
+ events: await this.provider.getEvents(threadId, {
1832
+ signal: controller.signal,
1833
+ }),
1834
+ }
1835
+ : await this.fetchRuntimeEvents(threadId, controller.signal);
1512
1836
  // Drop results if a newer fetch superseded this one (thread switched
1513
- // mid-flight). Without this, switching A→B can leave thread B's view
1514
- // showing thread A's events when A's request resolves last.
1837
+ // or provider/runtime changed mid-flight). Without this, switching A→B
1838
+ // can leave thread B's view showing thread A's events when A's request
1839
+ // resolves last.
1515
1840
  if (controller.signal.aborted || this.threadId !== threadId) return;
1516
- if (res.status === 501) {
1517
- // Endpoint not supported on this runtime (e.g. Intelligence platform).
1518
- // Mark unavailable so we don't misleadingly fall back to the parent's
1519
- // live agent events — those are agent-keyed, not thread-keyed, and
1520
- // would render identical across every thread on the same agent.
1841
+ if (result.status === "not-available") {
1521
1842
  this._eventsNotAvailable = true;
1522
- this._fetchedEvents = null;
1843
+ this._fetchedEvents = [];
1844
+ if (this.canFetchMessages()) {
1845
+ void this.fetchMessages(threadId);
1846
+ }
1523
1847
  return;
1524
1848
  }
1525
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1526
- const data = (await res.json()) as {
1527
- events: Array<Record<string, unknown>>;
1528
- };
1529
- if (controller.signal.aborted || this.threadId !== threadId) return;
1530
- this._fetchedEvents = this.mapApiEvents(data.events);
1849
+ const mappedEvents = this.mapApiEvents(result.events);
1850
+ this._fetchedEvents = mappedEvents;
1851
+ if (mappedEvents.length === 0 && this.canFetchMessages()) {
1852
+ void this.fetchMessages(threadId);
1853
+ }
1531
1854
  } catch (err) {
1532
1855
  if (err instanceof Error && err.name === "AbortError") return;
1533
1856
  if (this.threadId !== threadId) return;
1534
1857
  this._eventsError =
1535
1858
  err instanceof Error ? err.message : "Failed to load events";
1536
1859
  this._fetchedEvents = [];
1860
+ if (this.canFetchMessages()) {
1861
+ void this.fetchMessages(threadId);
1862
+ }
1537
1863
  } finally {
1538
1864
  if (!controller.signal.aborted && this.threadId === threadId) {
1539
1865
  this._loadingEvents = false;
@@ -1542,32 +1868,31 @@ export class ɵCpkThreadDetails extends LitElement {
1542
1868
  }
1543
1869
 
1544
1870
  private async fetchState(threadId: string): Promise<void> {
1545
- this._stateNotAvailable = false;
1546
- if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1871
+ if (!this.canFetchState()) {
1547
1872
  this._fetchedState = null;
1548
1873
  return;
1549
1874
  }
1875
+ this._stateAbort?.abort();
1550
1876
  const controller = new AbortController();
1551
1877
  this._stateAbort = controller;
1552
1878
  this._loadingState = true;
1553
1879
  this._stateError = null;
1554
1880
  try {
1555
- const res = await fetch(
1556
- `${this.runtimeUrl}/threads/${encodeURIComponent(threadId)}/state`,
1557
- { headers: { ...this.headers }, signal: controller.signal },
1558
- );
1881
+ const result = this.provider?.getState
1882
+ ? {
1883
+ status: "available" as const,
1884
+ state: await this.provider.getState(threadId, {
1885
+ signal: controller.signal,
1886
+ }),
1887
+ }
1888
+ : await this.fetchRuntimeState(threadId, controller.signal);
1559
1889
  if (controller.signal.aborted || this.threadId !== threadId) return;
1560
- if (res.status === 501) {
1890
+ if (result.status === "not-available") {
1561
1891
  this._stateNotAvailable = true;
1562
1892
  this._fetchedState = null;
1563
1893
  return;
1564
1894
  }
1565
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1566
- const data = (await res.json()) as {
1567
- state: Record<string, unknown> | null;
1568
- };
1569
- if (controller.signal.aborted || this.threadId !== threadId) return;
1570
- this._fetchedState = data.state ?? null;
1895
+ this._fetchedState = result.state ?? null;
1571
1896
  } catch (err) {
1572
1897
  if (err instanceof Error && err.name === "AbortError") return;
1573
1898
  if (this.threadId !== threadId) return;
@@ -1581,7 +1906,63 @@ export class ɵCpkThreadDetails extends LitElement {
1581
1906
  }
1582
1907
  }
1583
1908
 
1584
- private mapMessages(messages: ApiThreadMessage[]): ConversationItem[] {
1909
+ private async fetchRuntimeMessages(
1910
+ threadId: string,
1911
+ signal: AbortSignal,
1912
+ ): Promise<ThreadDebuggerMessage[]> {
1913
+ const res = await fetch(this.getThreadInspectionUrl(threadId, "messages"), {
1914
+ headers: { ...this.headers },
1915
+ signal,
1916
+ });
1917
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1918
+ const data = (await res.json()) as { messages: ThreadDebuggerMessage[] };
1919
+ return data.messages;
1920
+ }
1921
+
1922
+ private async fetchRuntimeEvents(
1923
+ threadId: string,
1924
+ signal: AbortSignal,
1925
+ ): Promise<RuntimeEventsFetchResult> {
1926
+ const res = await fetch(this.getThreadInspectionUrl(threadId, "events"), {
1927
+ headers: { ...this.headers },
1928
+ signal,
1929
+ });
1930
+ if (res.status === 501) {
1931
+ return { status: "not-available" };
1932
+ }
1933
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1934
+ const data = (await res.json()) as {
1935
+ events: ThreadDebuggerEvent[];
1936
+ };
1937
+ return { status: "available", events: data.events };
1938
+ }
1939
+
1940
+ private async fetchRuntimeState(
1941
+ threadId: string,
1942
+ signal: AbortSignal,
1943
+ ): Promise<RuntimeStateFetchResult> {
1944
+ const res = await fetch(this.getThreadInspectionUrl(threadId, "state"), {
1945
+ headers: { ...this.headers },
1946
+ signal,
1947
+ });
1948
+ if (res.status === 501) {
1949
+ return { status: "not-available" };
1950
+ }
1951
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1952
+ const data = (await res.json()) as {
1953
+ state: Record<string, unknown> | null;
1954
+ };
1955
+ return { status: "available", state: data.state ?? null };
1956
+ }
1957
+
1958
+ private getThreadInspectionUrl(
1959
+ threadId: string,
1960
+ resource: "messages" | "events" | "state",
1961
+ ): string {
1962
+ return `${this.runtimeUrl.replace(/\/+$/, "")}/threads/${encodeURIComponent(threadId)}/${resource}`;
1963
+ }
1964
+
1965
+ private mapMessages(messages: ThreadDebuggerMessage[]): ConversationItem[] {
1585
1966
  const items: ConversationItem[] = [];
1586
1967
  const toolCallMap = new Map<string, ConversationToolCall>();
1587
1968
  for (const msg of messages) {
@@ -1596,19 +1977,20 @@ export class ɵCpkThreadDetails extends LitElement {
1596
1977
  if (msg.toolCalls?.length) {
1597
1978
  for (const tc of msg.toolCalls) {
1598
1979
  let args: Record<string, unknown> = {};
1599
- try {
1600
- args = JSON.parse(tc.args) as Record<string, unknown>;
1601
- } catch (err) {
1602
- // Inspector is a debugging surface — surface malformed payloads
1603
- // instead of silently substituting `{}`. The sentinel lets the
1604
- // renderer flag "raw arguments — failed to parse" if/when it
1605
- // grows that branch; the console.error gives anyone with the
1606
- // devtools open immediate visibility into the offending blob.
1607
- console.error(
1608
- "[CopilotKit Inspector] Failed to parse tool-call arguments",
1609
- { toolCallId: tc.id, raw: tc.args, error: err },
1610
- );
1611
- args = { __parseError: true, __raw: tc.args };
1980
+ if (typeof tc.args === "string") {
1981
+ try {
1982
+ args = JSON.parse(tc.args) as Record<string, unknown>;
1983
+ } catch (err) {
1984
+ // Inspector is a debugging surface surface malformed payloads
1985
+ // instead of silently substituting `{}`.
1986
+ console.error(
1987
+ "[CopilotKit Inspector] Failed to parse tool-call arguments",
1988
+ { toolCallId: tc.id, raw: tc.args, error: err },
1989
+ );
1990
+ args = { __parseError: true, __raw: tc.args };
1991
+ }
1992
+ } else {
1993
+ args = tc.args;
1612
1994
  }
1613
1995
  const item: ConversationToolCall = {
1614
1996
  id: tc.id,
@@ -1662,22 +2044,284 @@ export class ɵCpkThreadDetails extends LitElement {
1662
2044
  return items;
1663
2045
  }
1664
2046
 
1665
- private mapApiEvents(
1666
- events: Array<Record<string, unknown>>,
1667
- ): ApiAgentEvent[] {
1668
- return events.map((event) => {
1669
- const { type, timestamp, ...rest } = event;
2047
+ private mapApiEvents(events: ThreadDebuggerEvent[]): ApiAgentEvent[] {
2048
+ return events.map((event, index) => {
2049
+ const { type, timestamp, payload, ...rest } = event;
1670
2050
  return {
1671
2051
  type: typeof type === "string" ? type : "UNKNOWN",
1672
2052
  timestamp:
1673
2053
  typeof timestamp === "string" || typeof timestamp === "number"
1674
2054
  ? timestamp
1675
2055
  : Date.now(),
1676
- payload: rest,
2056
+ payload: payload ?? rest,
2057
+ sourceIndex: index + 1,
2058
+ rawEvent: event,
1677
2059
  };
1678
2060
  });
1679
2061
  }
1680
2062
 
2063
+ private get activeTimelineItems(): TimelineItem[] {
2064
+ return this.timelineItemsForEvents(this.activeEvents);
2065
+ }
2066
+
2067
+ private timelineItemsForEvents(events: ApiAgentEvent[]): TimelineItem[] {
2068
+ if (this._timelineItemsCache?.events === events) {
2069
+ return this._timelineItemsCache.items;
2070
+ }
2071
+ const items = this.timelineItemsFromEvents(events);
2072
+ this._timelineItemsCache = { events, items };
2073
+ return items;
2074
+ }
2075
+
2076
+ private timelineItemsFromEvents(events: ApiAgentEvent[]): TimelineItem[] {
2077
+ if (events.length === 0) return [];
2078
+
2079
+ const items: TimelineItem[] = [];
2080
+ const messageItems = new Map<string, TimelineItem>();
2081
+ const toolItems = new Map<string, TimelineItem & { rawArgs?: string }>();
2082
+
2083
+ const readString = (
2084
+ payload: Record<string, unknown>,
2085
+ keys: string[],
2086
+ ): string | null => {
2087
+ for (const key of keys) {
2088
+ const value = payload[key];
2089
+ if (typeof value === "string") return value;
2090
+ }
2091
+ return null;
2092
+ };
2093
+
2094
+ const sourceIndexFor = (event: ApiAgentEvent): number =>
2095
+ event.sourceIndex ?? 0;
2096
+
2097
+ const appendWarning = (
2098
+ event: ApiAgentEvent,
2099
+ title: string,
2100
+ body: string,
2101
+ severity: "warning" | "error" = "warning",
2102
+ ): void => {
2103
+ const sourceIndex = sourceIndexFor(event);
2104
+ items.push({
2105
+ id: `warning-${sourceIndex}-${items.length}`,
2106
+ kind: "warning",
2107
+ title,
2108
+ body,
2109
+ timestamp: event.timestamp,
2110
+ sourceIndex,
2111
+ severity,
2112
+ });
2113
+ };
2114
+
2115
+ const ensureMessage = (
2116
+ event: ApiAgentEvent,
2117
+ role: string,
2118
+ ): TimelineItem => {
2119
+ const sourceIndex = sourceIndexFor(event);
2120
+ const key =
2121
+ readString(event.payload, ["messageId", "message_id", "id"]) ??
2122
+ `message-${sourceIndex}`;
2123
+ let item = messageItems.get(key);
2124
+ if (!item) {
2125
+ item = {
2126
+ id: `message-${key}`,
2127
+ kind: "message",
2128
+ title: `${role || "message"} message`,
2129
+ body: "",
2130
+ timestamp: event.timestamp,
2131
+ sourceIndex,
2132
+ };
2133
+ messageItems.set(key, item);
2134
+ items.push(item);
2135
+ }
2136
+ return item;
2137
+ };
2138
+
2139
+ const ensureTool = (
2140
+ event: ApiAgentEvent,
2141
+ ): TimelineItem & {
2142
+ rawArgs?: string;
2143
+ } => {
2144
+ const sourceIndex = sourceIndexFor(event);
2145
+ const key =
2146
+ readString(event.payload, [
2147
+ "toolCallId",
2148
+ "tool_call_id",
2149
+ "id",
2150
+ "callId",
2151
+ ]) ?? `tool-${sourceIndex}`;
2152
+ let item = toolItems.get(key);
2153
+ if (!item) {
2154
+ item = {
2155
+ id: `tool-${key}`,
2156
+ kind: "tool",
2157
+ title:
2158
+ readString(event.payload, [
2159
+ "toolCallName",
2160
+ "toolName",
2161
+ "name",
2162
+ "functionName",
2163
+ ]) ?? "Tool call",
2164
+ body: "",
2165
+ timestamp: event.timestamp,
2166
+ sourceIndex,
2167
+ };
2168
+ toolItems.set(key, item);
2169
+ items.push(item);
2170
+ }
2171
+ return item;
2172
+ };
2173
+
2174
+ for (const event of events) {
2175
+ const { type, payload } = event;
2176
+ const sourceIndex = sourceIndexFor(event);
2177
+
2178
+ if (type === "UNKNOWN") {
2179
+ appendWarning(
2180
+ event,
2181
+ "Unknown AG-UI event",
2182
+ "The event is missing a string type and could not be normalized.",
2183
+ );
2184
+ continue;
2185
+ }
2186
+
2187
+ if (type === "RUN_STARTED" || type === "STEP_STARTED") {
2188
+ items.push({
2189
+ id: `${type}-${sourceIndex}`,
2190
+ kind: "run",
2191
+ title: type === "RUN_STARTED" ? "Run started" : "Step started",
2192
+ timestamp: event.timestamp,
2193
+ sourceIndex,
2194
+ details: payload,
2195
+ });
2196
+ continue;
2197
+ }
2198
+
2199
+ if (type === "RUN_FINISHED" || type === "STEP_FINISHED") {
2200
+ items.push({
2201
+ id: `${type}-${sourceIndex}`,
2202
+ kind: "run",
2203
+ title: type === "RUN_FINISHED" ? "Run finished" : "Step finished",
2204
+ timestamp: event.timestamp,
2205
+ sourceIndex,
2206
+ details: payload,
2207
+ });
2208
+ continue;
2209
+ }
2210
+
2211
+ if (type === "RUN_ERROR" || type === "ERROR") {
2212
+ items.push({
2213
+ id: `${type}-${sourceIndex}`,
2214
+ kind: "warning",
2215
+ title: "Run error",
2216
+ body: readString(payload, ["message", "error", "description"]) ?? "",
2217
+ timestamp: event.timestamp,
2218
+ sourceIndex,
2219
+ severity: "error",
2220
+ details: payload,
2221
+ });
2222
+ continue;
2223
+ }
2224
+
2225
+ if (type === "TEXT_MESSAGE_START") {
2226
+ ensureMessage(event, readString(payload, ["role"]) ?? "assistant");
2227
+ continue;
2228
+ }
2229
+
2230
+ if (type === "TEXT_MESSAGE_CONTENT") {
2231
+ const item = ensureMessage(
2232
+ event,
2233
+ readString(payload, ["role"]) ?? "assistant",
2234
+ );
2235
+ item.body = `${item.body ?? ""}${
2236
+ readString(payload, ["delta", "content", "text"]) ?? ""
2237
+ }`;
2238
+ continue;
2239
+ }
2240
+
2241
+ if (type === "TEXT_MESSAGE_END") {
2242
+ ensureMessage(event, readString(payload, ["role"]) ?? "assistant");
2243
+ continue;
2244
+ }
2245
+
2246
+ if (type === "TOOL_CALL_START") {
2247
+ ensureTool(event);
2248
+ continue;
2249
+ }
2250
+
2251
+ if (type === "TOOL_CALL_ARGS") {
2252
+ const item = ensureTool(event);
2253
+ const chunk =
2254
+ readString(payload, ["args", "arguments", "delta"]) ??
2255
+ (typeof payload.args === "object"
2256
+ ? JSON.stringify(payload.args)
2257
+ : null);
2258
+ if (chunk) {
2259
+ item.rawArgs = `${item.rawArgs ?? ""}${chunk}`;
2260
+ item.body = item.rawArgs;
2261
+ }
2262
+ continue;
2263
+ }
2264
+
2265
+ if (type === "TOOL_CALL_END") {
2266
+ const item = ensureTool(event);
2267
+ if (item.rawArgs) {
2268
+ try {
2269
+ JSON.parse(item.rawArgs);
2270
+ } catch {
2271
+ appendWarning(
2272
+ event,
2273
+ "Could not decode tool call arguments",
2274
+ item.rawArgs,
2275
+ );
2276
+ }
2277
+ }
2278
+ continue;
2279
+ }
2280
+
2281
+ if (type === "TOOL_CALL_RESULT") {
2282
+ const item = ensureTool(event);
2283
+ const result = readString(payload, ["result", "content", "delta"]);
2284
+ if (result) {
2285
+ item.body = item.body
2286
+ ? `${item.body}\nResult: ${result}`
2287
+ : `Result: ${result}`;
2288
+ try {
2289
+ JSON.parse(result);
2290
+ } catch {
2291
+ appendWarning(event, "Could not decode tool result", result);
2292
+ }
2293
+ }
2294
+ continue;
2295
+ }
2296
+
2297
+ if (type.startsWith("STATE_")) {
2298
+ items.push({
2299
+ id: `${type}-${sourceIndex}`,
2300
+ kind: "state",
2301
+ title:
2302
+ type === "STATE_SNAPSHOT"
2303
+ ? "State snapshot captured"
2304
+ : "State delta captured",
2305
+ timestamp: event.timestamp,
2306
+ sourceIndex,
2307
+ details: payload,
2308
+ });
2309
+ continue;
2310
+ }
2311
+
2312
+ items.push({
2313
+ id: `event-${sourceIndex}`,
2314
+ kind: "event",
2315
+ title: type,
2316
+ timestamp: event.timestamp,
2317
+ sourceIndex,
2318
+ details: payload,
2319
+ });
2320
+ }
2321
+
2322
+ return items;
2323
+ }
2324
+
1681
2325
  private get renderItems(): RenderItem[] {
1682
2326
  const items = this._conversation;
1683
2327
  const result: RenderItem[] = [];
@@ -1720,7 +2364,7 @@ export class ɵCpkThreadDetails extends LitElement {
1720
2364
  }
1721
2365
 
1722
2366
  private get duration(): string {
1723
- const t = this.thread;
2367
+ const t = this.metadata;
1724
2368
  if (!t?.createdAt || !t?.updatedAt) return "—";
1725
2369
  const ms =
1726
2370
  new Date(t.updatedAt).getTime() - new Date(t.createdAt).getTime();
@@ -1753,7 +2397,16 @@ export class ɵCpkThreadDetails extends LitElement {
1753
2397
  // threads (those would render identically for every thread on the same
1754
2398
  // agent and mislead the reader).
1755
2399
  if (this._eventsNotAvailable) return [];
1756
- return this._fetchedEvents ?? this.agentEventsInput ?? [];
2400
+ const events = this._fetchedEvents ?? this.agentEventsInput ?? [];
2401
+ if (events.every((event) => event.sourceIndex != null)) return events;
2402
+ if (this._liveEventsWithSourceIndexCache?.events === events) {
2403
+ return this._liveEventsWithSourceIndexCache.indexedEvents;
2404
+ }
2405
+ const indexedEvents = events.map((event, index) =>
2406
+ event.sourceIndex == null ? { ...event, sourceIndex: index + 1 } : event,
2407
+ );
2408
+ this._liveEventsWithSourceIndexCache = { events, indexedEvents };
2409
+ return indexedEvents;
1757
2410
  }
1758
2411
 
1759
2412
  private get activeState(): Record<string, unknown> | null {
@@ -1771,6 +2424,10 @@ export class ɵCpkThreadDetails extends LitElement {
1771
2424
  return id.length > 20 ? id.slice(0, 8) + "…" : id;
1772
2425
  }
1773
2426
 
2427
+ private get metadata(): ThreadDebuggerMetadata | null {
2428
+ return this._fetchedMetadata ?? this.thread ?? null;
2429
+ }
2430
+
1774
2431
  private fmtTime(dateStr: string | null | undefined): string {
1775
2432
  if (!dateStr) return "—";
1776
2433
  const d = new Date(dateStr);
@@ -1819,7 +2476,7 @@ export class ɵCpkThreadDetails extends LitElement {
1819
2476
  <!-- Tab bar -->
1820
2477
  <div class="cpk-td__tabs-header">
1821
2478
  <div class="cpk-td__tab-group" role="tablist">
1822
- ${ɵCpkThreadDetails.TAB_LIST.map(
2479
+ ${CpkThreadInspector.TAB_LIST.map(
1823
2480
  (tab) => html`
1824
2481
  <button
1825
2482
  role="tab"
@@ -1835,6 +2492,7 @@ export class ɵCpkThreadDetails extends LitElement {
1835
2492
  </div>
1836
2493
  ${this.renderPanelToggle()}
1837
2494
  </div>
2495
+ ${this.renderMetadataStrip()}
1838
2496
 
1839
2497
  <!-- Scrollable content -->
1840
2498
  <div class="cpk-td__content">
@@ -1845,50 +2503,178 @@ export class ɵCpkThreadDetails extends LitElement {
1845
2503
  `
1846
2504
  : nothing
1847
2505
  }
1848
- ${ɵCpkThreadDetails.TAB_LIST.map((tab) =>
2506
+ ${CpkThreadInspector.TAB_LIST.map((tab) =>
1849
2507
  this._activatedTabs.has(tab.id)
1850
2508
  ? html`<div
1851
- class="cpk-td__panel"
1852
- style=${
1853
- this._tab === tab.id && !this._panelInitializing
1854
- ? ""
1855
- : "display:none"
1856
- }
1857
- >
1858
- ${this.renderTabContent(tab.id)}
1859
- </div>`
2509
+ class="cpk-td__panel"
2510
+ style=${
2511
+ this._tab === tab.id && !this._panelInitializing
2512
+ ? ""
2513
+ : "display:none"
2514
+ }
2515
+ >
2516
+ ${this.renderTabContent(tab.id)}
2517
+ </div>`
1860
2518
  : nothing,
1861
2519
  )}
1862
2520
  </div>
1863
2521
  </div>
1864
2522
 
1865
- <!--
1866
- Drawer always rendered so width animates between 0 and its
1867
- target. Divider lives INSIDE the drawer and is absolutely
1868
- positioned over its left edge so the toggle (rightmost of the
1869
- tab row) and the drawer touch with no flex-gap between them.
1870
- -->
1871
- <div
1872
- class="cpk-td__detail"
1873
- data-open=${this._showDetailPanel ? "true" : "false"}
1874
- style="width:${this._showDetailPanel ? this._detailPanelWidth : 0}px"
1875
- aria-hidden=${this._showDetailPanel ? "false" : "true"}
1876
- >
1877
- ${
1878
- this._showDetailPanel
1879
- ? html`
1880
- <div
1881
- class="cpk-td__detail-divider"
1882
- @pointerdown=${this.onDetailDividerDown}
1883
- @pointermove=${this.onDetailDividerMove}
1884
- @pointerup=${this.onDetailDividerUp}
1885
- @pointercancel=${this.onDetailDividerUp}
1886
- ></div>
1887
- `
1888
- : nothing
1889
- }
1890
- ${this.renderDetailPanel()}
2523
+ <!--
2524
+ Drawer always rendered so width animates between 0 and its
2525
+ target. Divider lives INSIDE the drawer and is absolutely
2526
+ positioned over its left edge so the toggle (rightmost of the
2527
+ tab row) and the drawer touch with no flex-gap between them.
2528
+ -->
2529
+ <div
2530
+ class="cpk-td__detail"
2531
+ data-open=${this._showDetailPanel ? "true" : "false"}
2532
+ style="width:${this._showDetailPanel ? this._detailPanelWidth : 0}px"
2533
+ aria-hidden=${this._showDetailPanel ? "false" : "true"}
2534
+ >
2535
+ ${
2536
+ this._showDetailPanel
2537
+ ? html`
2538
+ <div
2539
+ class="cpk-td__detail-divider"
2540
+ @pointerdown=${this.onDetailDividerDown}
2541
+ @pointermove=${this.onDetailDividerMove}
2542
+ @pointerup=${this.onDetailDividerUp}
2543
+ @pointercancel=${this.onDetailDividerUp}
2544
+ ></div>
2545
+ `
2546
+ : nothing
2547
+ }
2548
+ ${this.renderDetailPanel()}
2549
+ </div>
2550
+ </div>
2551
+ `;
2552
+ }
2553
+
2554
+ private renderMetadataStrip() {
2555
+ const metadata = this.metadata;
2556
+ const pills: Array<{ label: string; value: string | null | undefined }> = [
2557
+ { label: "Thread", value: metadata?.id ?? this.threadId },
2558
+ { label: "Agent", value: metadata?.agentId },
2559
+ {
2560
+ label: "End user",
2561
+ value: metadata?.endUserId ?? metadata?.createdById,
2562
+ },
2563
+ { label: "Status", value: metadata?.status },
2564
+ ].filter((pill) => pill.value != null && pill.value !== "");
2565
+
2566
+ if (pills.length === 0) return nothing;
2567
+
2568
+ return html`
2569
+ <div class="cpk-td__metadata-strip" aria-label="Thread metadata">
2570
+ ${pills.map(
2571
+ (pill) => html`
2572
+ <span class="cpk-td__metadata-pill" title=${pill.value ?? ""}>
2573
+ <span class="cpk-td__metadata-label">${pill.label}</span>
2574
+ <span class="cpk-td__metadata-value"
2575
+ >${this.shortId(pill.value)}</span
2576
+ >
2577
+ </span>
2578
+ `,
2579
+ )}
2580
+ </div>
2581
+ `;
2582
+ }
2583
+
2584
+ private revealSourceEvent(sourceIndex: number): void {
2585
+ this._activatedTabs = new Set([...this._activatedTabs, "raw-events"]);
2586
+ this._tab = "raw-events";
2587
+ this.requestUpdate();
2588
+ requestAnimationFrame(() => {
2589
+ const source = this.shadowRoot?.querySelector<HTMLElement>(
2590
+ `[data-source-index="${sourceIndex}"]`,
2591
+ );
2592
+ source?.scrollIntoView?.({ block: "center" });
2593
+ });
2594
+ }
2595
+
2596
+ private renderTimeline() {
2597
+ if (this._loadingEvents) {
2598
+ return html`
2599
+ <div class="cpk-td__status">Loading timeline…</div>
2600
+ `;
2601
+ }
2602
+ if (this._eventsError) {
2603
+ return html`<div class="cpk-td__status cpk-td__status--error">
2604
+ ${this._eventsError}
2605
+ </div>`;
2606
+ }
2607
+ if (this._eventsNotAvailable) {
2608
+ if (this._conversation.length > 0) return this.renderConversation();
2609
+ if (this._loadingMessages) return this.renderConversation();
2610
+ return html`
2611
+ <div class="cpk-td__empty-state">
2612
+ <span>Timeline event history not available</span>
2613
+ <span class="cpk-td__empty-hint"
2614
+ >This runtime doesn't yet expose per-thread AG-UI events. Check State for
2615
+ the latest snapshot when available.</span
2616
+ >
2617
+ </div>
2618
+ `;
2619
+ }
2620
+
2621
+ const events = this.activeEvents;
2622
+ const cachedTimeline = this.getCachedPanelTpl("timeline", [events]);
2623
+ if (cachedTimeline) return cachedTimeline;
2624
+
2625
+ const timelineItems = this.timelineItemsForEvents(events);
2626
+ if (timelineItems.length === 0) {
2627
+ if (this._conversation.length > 0) return this.renderConversation();
2628
+ if (this._loadingMessages) return this.renderConversation();
2629
+ return html`
2630
+ <div class="cpk-td__empty-state">
2631
+ <span>No timeline events captured</span>
2632
+ <span class="cpk-td__empty-hint"
2633
+ >Timeline rows are normalized from AG-UI events. Open Raw AG-UI Events or
2634
+ State to inspect the available thread data.</span
2635
+ >
2636
+ </div>
2637
+ `;
2638
+ }
2639
+
2640
+ return this.cachedPanelTpl("timeline", [events], () => {
2641
+ return html`${timelineItems.map((item) => this.renderTimelineItem(item))}`;
2642
+ });
2643
+ }
2644
+
2645
+ private renderTimelineItem(item: TimelineItem) {
2646
+ const isWarning = item.kind === "warning";
2647
+ return html`
2648
+ <div
2649
+ class="cpk-td__timeline-item ${
2650
+ isWarning ? "cpk-td__timeline-item--warning" : ""
2651
+ }"
2652
+ >
2653
+ <div class="cpk-td__timeline-header">
2654
+ <span class="cpk-td__timeline-kind"
2655
+ >${item.severity === "error" ? "error" : item.kind}</span
2656
+ >
2657
+ <span class="cpk-td__timeline-title">${item.title}</span>
2658
+ <button
2659
+ type="button"
2660
+ class="cpk-td__source-link"
2661
+ @click=${() => this.revealSourceEvent(item.sourceIndex)}
2662
+ >
2663
+ Source event #${item.sourceIndex}
2664
+ </button>
2665
+ <span class="cpk-td__timeline-time"
2666
+ >${formatTimestamp(item.timestamp)}</span
2667
+ >
1891
2668
  </div>
2669
+ ${
2670
+ item.body
2671
+ ? html`<div class="cpk-td__timeline-body">${item.body}</div>`
2672
+ : item.details
2673
+ ? html`<pre class="cpk-td__timeline-body">
2674
+ ${unsafeHTML(highlightedJson(item.details))}</pre
2675
+ >`
2676
+ : nothing
2677
+ }
1892
2678
  </div>
1893
2679
  `;
1894
2680
  }
@@ -1929,7 +2715,7 @@ export class ɵCpkThreadDetails extends LitElement {
1929
2715
  // `_conversation` — without those keys the cache returns the
1930
2716
  // pre-toggle template and the disclosure appears broken.
1931
2717
  return this.cachedPanelTpl(
1932
- "conversation",
2718
+ "timeline-fallback",
1933
2719
  [this._conversation, this._expandedTools, this._expandedMessages],
1934
2720
  () => {
1935
2721
  const items = this.renderItems;
@@ -1948,10 +2734,21 @@ export class ɵCpkThreadDetails extends LitElement {
1948
2734
  * change without the listed key flipping.
1949
2735
  */
1950
2736
  private cachedPanelTpl(
1951
- slot: ThreadDetailsTab,
2737
+ slot: ThreadDetailsPanelCacheSlot,
1952
2738
  key: readonly unknown[],
1953
2739
  build: () => TemplateResult,
1954
2740
  ): TemplateResult {
2741
+ const cached = this.getCachedPanelTpl(slot, key);
2742
+ if (cached) return cached;
2743
+ const tpl = build();
2744
+ this._panelTplCache.set(slot, { key, tpl });
2745
+ return tpl;
2746
+ }
2747
+
2748
+ private getCachedPanelTpl(
2749
+ slot: ThreadDetailsPanelCacheSlot,
2750
+ key: readonly unknown[],
2751
+ ): TemplateResult | null {
1955
2752
  const cached = this._panelTplCache.get(slot);
1956
2753
  if (
1957
2754
  cached &&
@@ -1960,9 +2757,7 @@ export class ɵCpkThreadDetails extends LitElement {
1960
2757
  ) {
1961
2758
  return cached.tpl;
1962
2759
  }
1963
- const tpl = build();
1964
- this._panelTplCache.set(slot, { key, tpl });
1965
- return tpl;
2760
+ return null;
1966
2761
  }
1967
2762
 
1968
2763
  private renderRenderItem(item: RenderItem) {
@@ -1993,7 +2788,7 @@ export class ɵCpkThreadDetails extends LitElement {
1993
2788
 
1994
2789
  private renderBubble(item: ConversationUser | ConversationAssistant) {
1995
2790
  const isUser = item.type === "user";
1996
- const threshold = ɵCpkThreadDetails.COLLAPSE_THRESHOLD;
2791
+ const threshold = CpkThreadInspector.COLLAPSE_THRESHOLD;
1997
2792
  const expanded = this._expandedMessages.has(item.id);
1998
2793
  const tooLong = item.content.length > threshold;
1999
2794
  const shown =
@@ -2176,7 +2971,7 @@ ${unsafeHTML(highlightedJson(item.result))}</pre
2176
2971
  `;
2177
2972
  }
2178
2973
  const stateValue = this.activeState;
2179
- return this.cachedPanelTpl("agent-state", [stateValue], () => {
2974
+ return this.cachedPanelTpl("state", [stateValue], () => {
2180
2975
  return html`<pre class="cpk-td__json-block">
2181
2976
  ${unsafeHTML(highlightedJson(stateValue))}</pre
2182
2977
  >`;
@@ -2216,11 +3011,11 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
2216
3011
  </div>
2217
3012
  `;
2218
3013
  }
2219
- return this.cachedPanelTpl("ag-ui-events", [events], () => {
3014
+ return this.cachedPanelTpl("raw-events", [events], () => {
2220
3015
  return html`${events.map((event) => {
2221
3016
  const { bg, fg } = eventColors(event.type);
2222
3017
  return html`
2223
- <div class="cpk-td__event">
3018
+ <div class="cpk-td__event" data-source-index=${event.sourceIndex}>
2224
3019
  <div class="cpk-td__event-header" style="background:${bg}">
2225
3020
  <span class="cpk-td__event-type" style="color:${fg}"
2226
3021
  >${event.type}</span
@@ -2230,7 +3025,7 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
2230
3025
  >
2231
3026
  </div>
2232
3027
  <pre class="cpk-td__event-payload">
2233
- ${unsafeHTML(highlightedJson(event.payload))}</pre
3028
+ ${unsafeHTML(highlightedJson(event.rawEvent ?? event))}</pre
2234
3029
  >
2235
3030
  </div>
2236
3031
  `;
@@ -2269,29 +3064,42 @@ ${unsafeHTML(highlightedJson(event.payload))}</pre
2269
3064
 
2270
3065
  private renderDetailPanel() {
2271
3066
  const counts = this.activityCounts;
3067
+ const metadata = this.metadata;
2272
3068
  return html`
2273
3069
  <!-- Thread -->
2274
3070
  <div class="cpk-tdp__section-title">Thread</div>
2275
3071
  <div class="cpk-tdp__row">
2276
3072
  <span class="cpk-tdp__label">ID</span>
2277
3073
  <span class="cpk-tdp__value cpk-tdp__value--wrap"
2278
- >${this.shortId(this.thread?.id)}</span
3074
+ >${this.shortId(metadata?.id)}</span
2279
3075
  >
2280
3076
  </div>
2281
3077
  <div class="cpk-tdp__row">
2282
3078
  <span class="cpk-tdp__label">Name</span>
2283
- <span class="cpk-tdp__value">${this.thread?.name ?? "—"}</span>
3079
+ <span class="cpk-tdp__value">${metadata?.name ?? "—"}</span>
2284
3080
  </div>
2285
3081
  <div class="cpk-tdp__row">
2286
3082
  <span class="cpk-tdp__label">Agent</span>
2287
3083
  <span class="cpk-tdp__value cpk-tdp__value--truncate"
2288
- >${this.thread?.agentId ?? "—"}</span
3084
+ >${metadata?.agentId ?? "—"}</span
3085
+ >
3086
+ </div>
3087
+ <div class="cpk-tdp__row">
3088
+ <span class="cpk-tdp__label">End user</span>
3089
+ <span class="cpk-tdp__value cpk-tdp__value--truncate"
3090
+ >${metadata?.endUserId ?? "—"}</span
2289
3091
  >
2290
3092
  </div>
2291
3093
  <div class="cpk-tdp__row">
2292
3094
  <span class="cpk-tdp__label">Created by</span>
2293
3095
  <span class="cpk-tdp__value cpk-tdp__value--truncate"
2294
- >${this.thread?.createdById ?? "—"}</span
3096
+ >${metadata?.createdById ?? "—"}</span
3097
+ >
3098
+ </div>
3099
+ <div class="cpk-tdp__row">
3100
+ <span class="cpk-tdp__label">Status</span>
3101
+ <span class="cpk-tdp__value cpk-tdp__value--truncate"
3102
+ >${metadata?.status ?? "—"}</span
2295
3103
  >
2296
3104
  </div>
2297
3105
 
@@ -2301,11 +3109,11 @@ ${unsafeHTML(highlightedJson(event.payload))}</pre
2301
3109
  <div class="cpk-tdp__section-title">Timestamps</div>
2302
3110
  <div class="cpk-tdp__row">
2303
3111
  <span class="cpk-tdp__label">Created</span>
2304
- <span class="cpk-tdp__value">${this.fmtTime(this.thread?.createdAt)}</span>
3112
+ <span class="cpk-tdp__value">${this.fmtTime(metadata?.createdAt)}</span>
2305
3113
  </div>
2306
3114
  <div class="cpk-tdp__row">
2307
3115
  <span class="cpk-tdp__label">Updated</span>
2308
- <span class="cpk-tdp__value">${this.fmtTime(this.thread?.updatedAt)}</span>
3116
+ <span class="cpk-tdp__value">${this.fmtTime(metadata?.updatedAt)}</span>
2309
3117
  </div>
2310
3118
  <div class="cpk-tdp__row">
2311
3119
  <span class="cpk-tdp__label">Duration</span>
@@ -2332,9 +3140,17 @@ ${unsafeHTML(highlightedJson(event.payload))}</pre
2332
3140
  }
2333
3141
  }
2334
3142
 
3143
+ // Backwards-compatible internal element name used by the full CopilotKit
3144
+ // Inspector shell. Keep this class thin so the public body remains the single
3145
+ // implementation.
3146
+ export class ɵCpkThreadDetails extends CpkThreadInspector {}
3147
+
2335
3148
  if (!customElements.get("cpk-thread-list")) {
2336
3149
  customElements.define("cpk-thread-list", CpkThreadList);
2337
3150
  }
3151
+ if (!customElements.get(THREAD_INSPECTOR_TAG)) {
3152
+ customElements.define(THREAD_INSPECTOR_TAG, CpkThreadInspector);
3153
+ }
2338
3154
  if (!customElements.get("cpk-thread-details")) {
2339
3155
  customElements.define("cpk-thread-details", ɵCpkThreadDetails);
2340
3156
  }
@@ -2446,6 +3262,7 @@ export class WebInspectorElement extends LitElement {
2446
3262
  // `${bannerId}:${cta}`) so copy-button retries and accidental multi-clicks
2447
3263
  // don't inflate funnel counts beyond one signal per intent type per banner.
2448
3264
  private clickedBannerIds: Set<string> = new Set();
3265
+ private viewedThreadsTelemetryStates: Set<string> = new Set();
2449
3266
 
2450
3267
  get core(): CopilotKitCore | null {
2451
3268
  return this._core;
@@ -2527,7 +3344,54 @@ export class WebInspectorElement extends LitElement {
2527
3344
  ];
2528
3345
  }
2529
3346
 
3347
+ private getThreadServiceStatus(): ThreadServiceStatus {
3348
+ if (!this._core) return "unknown";
3349
+ if (!this._core.threadEndpoints) return "unknown";
3350
+ return this._core.threadEndpoints?.list === false
3351
+ ? "unavailable"
3352
+ : "available";
3353
+ }
3354
+
3355
+ private areThreadEndpointsAvailable(): boolean {
3356
+ return this.getThreadServiceStatus() !== "unavailable";
3357
+ }
3358
+
3359
+ private getThreadsTelemetryProps(
3360
+ extra: Partial<InspectorThreadTelemetryProps> = {},
3361
+ options: { includeUrlAttribution?: boolean } = {},
3362
+ ): InspectorThreadTelemetryProps {
3363
+ const distinctId =
3364
+ options.includeUrlAttribution && !this.core?.telemetryDisabled
3365
+ ? getTelemetryDistinctIdForUrl()
3366
+ : null;
3367
+ const threadServiceStatus = this.getThreadServiceStatus();
3368
+ return {
3369
+ posthog_distinct_id: distinctId ?? undefined,
3370
+ intelligence_status:
3371
+ threadServiceStatus === "available"
3372
+ ? "intelligence_enabled"
3373
+ : threadServiceStatus === "unavailable"
3374
+ ? "intelligence_not_enabled"
3375
+ : "unknown",
3376
+ thread_service_status: threadServiceStatus,
3377
+ license_status: this.core?.licenseStatus ?? undefined,
3378
+ runtime_mode: this.core?.runtimeMode ?? undefined,
3379
+ runtime_url_type: getRuntimeUrlType(this.core?.runtimeUrl),
3380
+ telemetry_disabled: this.core?.telemetryDisabled ?? false,
3381
+ ...extra,
3382
+ };
3383
+ }
3384
+
3385
+ private getIntelligenceSignupUrl(): string {
3386
+ return this.appendRefParam(INTELLIGENCE_SIGNUP_URL, "cpk-inspector");
3387
+ }
3388
+
3389
+ private getTalkToEngineerUrl(): string {
3390
+ return this.appendRefParam(TALK_TO_ENGINEER_URL, "cpk-inspector-threads");
3391
+ }
3392
+
2530
3393
  private subscribeToThreadStore(agentId: string, store: ɵThreadStore): void {
3394
+ if (!this.areThreadEndpointsAvailable()) return;
2531
3395
  if (this._threadStoreSubscriptions.has(agentId)) return;
2532
3396
  const threadsSub = store.select(ɵselectThreads).subscribe((threads) => {
2533
3397
  this._threadsByAgent.set(agentId, threads as ɵThread[]);
@@ -2587,7 +3451,7 @@ export class WebInspectorElement extends LitElement {
2587
3451
  if (this.core?.getThreadStore(agentId)) return;
2588
3452
  const core = this.core;
2589
3453
  if (!core?.runtimeUrl) return;
2590
- if (core.threadEndpoints?.list === false) return;
3454
+ if (!this.areThreadEndpointsAvailable()) return;
2591
3455
 
2592
3456
  const store = ɵcreateThreadStore({ fetch: globalThis.fetch });
2593
3457
  store.start();
@@ -2626,6 +3490,7 @@ export class WebInspectorElement extends LitElement {
2626
3490
  store.setContext({
2627
3491
  runtimeUrl: core.runtimeUrl,
2628
3492
  headers: { ...headers },
3493
+ wsUrl: core.intelligence?.wsUrl,
2629
3494
  agentId,
2630
3495
  });
2631
3496
  }
@@ -2661,7 +3526,7 @@ export class WebInspectorElement extends LitElement {
2661
3526
  maybeShowDisclosure();
2662
3527
  }
2663
3528
  this.flushPendingBannerViewed();
2664
- if (core.threadEndpoints?.list !== false) {
3529
+ if (this.areThreadEndpointsAvailable()) {
2665
3530
  for (const agentId of this._ownedThreadStores.keys()) {
2666
3531
  this.refreshOwnedThreadStore(agentId);
2667
3532
  }
@@ -2681,6 +3546,7 @@ export class WebInspectorElement extends LitElement {
2681
3546
  },
2682
3547
  onHeadersChanged: ({ headers }) => {
2683
3548
  this.updateOwnedThreadStoreHeaders(headers);
3549
+ this.requestUpdate();
2684
3550
  },
2685
3551
  onError: ({ code, error }) => {
2686
3552
  this.lastCoreError = { code, message: error.message };
@@ -2689,6 +3555,14 @@ export class WebInspectorElement extends LitElement {
2689
3555
  onAgentsChanged: ({ agents }) => {
2690
3556
  this.processAgentsChanged(agents);
2691
3557
  },
3558
+ onAgentRunStarted: ({ agent }) => {
3559
+ // Per-thread clones (from useAgent) are not in the agent registry, so
3560
+ // onAgentsChanged never fires for them. Subscribe to the running
3561
+ // instance here so its AG-UI events reach the event timeline.
3562
+ if (agent?.agentId) {
3563
+ this.subscribeToAgent(agent);
3564
+ }
3565
+ },
2692
3566
  onContextChanged: ({ context }) => {
2693
3567
  this.contextStore = this.normalizeContextStore(context);
2694
3568
  this.requestUpdate();
@@ -2713,6 +3587,14 @@ export class WebInspectorElement extends LitElement {
2713
3587
  this.coreUnsubscribe = core.subscribe(this.coreSubscriber).unsubscribe;
2714
3588
  this.processAgentsChanged(core.agents);
2715
3589
 
3590
+ if (core.runtimeConnectionStatus === "connected") {
3591
+ if (!core.telemetryDisabled) {
3592
+ ensureTelemetryDistinctId();
3593
+ maybeShowDisclosure();
3594
+ }
3595
+ this.flushPendingBannerViewed();
3596
+ }
3597
+
2716
3598
  // Subscribe to any already-registered thread stores. `getThreadStores` was
2717
3599
  // added in the same release as this inspector; guard so consumers still on
2718
3600
  // an older @copilotkit/core don't throw when assigning `inspector.core`.
@@ -2859,8 +3741,11 @@ export class WebInspectorElement extends LitElement {
2859
3741
  onRunStartedEvent: ({ event }) => {
2860
3742
  this.recordAgentEvent(agentId, "RUN_STARTED", event);
2861
3743
  },
2862
- onRunFinishedEvent: ({ event, result }) => {
2863
- this.recordAgentEvent(agentId, "RUN_FINISHED", { event, result });
3744
+ onRunFinishedEvent: (params) => {
3745
+ this.recordAgentEvent(agentId, "RUN_FINISHED", {
3746
+ event: params.event,
3747
+ result: "result" in params ? params.result : undefined,
3748
+ });
2864
3749
  this.refreshOwnedThreadStore(agentId);
2865
3750
  },
2866
3751
  onRunErrorEvent: ({ event }) => {
@@ -3404,6 +4289,10 @@ ${argsString}</pre
3404
4289
  const base =
3405
4290
  "font-mono text-[10px] font-medium inline-flex items-center rounded-sm px-1.5 py-0.5 border";
3406
4291
 
4292
+ if (type === "RUN_ERROR") {
4293
+ return `${base} bg-rose-50 text-rose-700 border-rose-200`;
4294
+ }
4295
+
3407
4296
  if (type.startsWith("RUN_")) {
3408
4297
  return `${base} bg-blue-50 text-blue-700 border-blue-200`;
3409
4298
  }
@@ -3428,10 +4317,6 @@ ${argsString}</pre
3428
4317
  return `${base} bg-sky-50 text-sky-700 border-sky-200`;
3429
4318
  }
3430
4319
 
3431
- if (type === "RUN_ERROR") {
3432
- return `${base} bg-rose-50 text-rose-700 border-rose-200`;
3433
- }
3434
-
3435
4320
  return `${base} bg-gray-100 text-gray-600 border-gray-200`;
3436
4321
  }
3437
4322
 
@@ -5750,7 +6635,9 @@ ${argsString}</pre
5750
6635
 
5751
6636
  <div class="space-y-2">
5752
6637
  <h3 class="text-sm text-slate-500">Privacy</h3>
5753
- <div class="rounded-lg border border-slate-200 bg-white p-4 space-y-3">
6638
+ <div
6639
+ class="rounded-lg border border-slate-200 bg-white p-4 space-y-3"
6640
+ >
5754
6641
  <p class="text-sm text-gray-600 flex items-start gap-2">
5755
6642
  <span>${optedOut ? "❌" : "✅"}</span>
5756
6643
  <span>
@@ -5766,7 +6653,8 @@ ${argsString}</pre
5766
6653
  href=${TELEMETRY_DOCS_URL}
5767
6654
  target="_blank"
5768
6655
  rel="noopener"
5769
- >Learn more →</a>
6656
+ >Learn more →</a
6657
+ >
5770
6658
  </div>
5771
6659
  </div>
5772
6660
  </div>
@@ -5791,6 +6679,45 @@ ${argsString}</pre
5791
6679
  });
5792
6680
  }
5793
6681
 
6682
+ private handleTalkToEngineerClick = (): void => {
6683
+ if (this.core?.telemetryDisabled) return;
6684
+ trackTalkToEngineerClicked(
6685
+ this.getThreadsTelemetryProps(
6686
+ {
6687
+ cta: "talk_to_engineer",
6688
+ cta_surface: "threads_header",
6689
+ },
6690
+ { includeUrlAttribution: true },
6691
+ ),
6692
+ );
6693
+ };
6694
+
6695
+ private handleThreadsIntelligenceSignupClick = (): void => {
6696
+ if (this.core?.telemetryDisabled) return;
6697
+ trackThreadsIntelligenceSignupClicked(
6698
+ this.getThreadsTelemetryProps(
6699
+ {
6700
+ cta: "signup",
6701
+ cta_surface: "threads_locked",
6702
+ },
6703
+ { includeUrlAttribution: true },
6704
+ ),
6705
+ );
6706
+ };
6707
+
6708
+ private handleThreadsTalkToEngineerClick = (): void => {
6709
+ if (this.core?.telemetryDisabled) return;
6710
+ trackThreadsTalkToEngineerClicked(
6711
+ this.getThreadsTelemetryProps(
6712
+ {
6713
+ cta: "talk_to_engineer",
6714
+ cta_surface: "threads_locked",
6715
+ },
6716
+ { includeUrlAttribution: true },
6717
+ ),
6718
+ );
6719
+ };
6720
+
5794
6721
  private handleThreadDividerPointerDown = (event: PointerEvent) => {
5795
6722
  this.threadDividerResizing = true;
5796
6723
  this.threadDividerPointerId = event.pointerId;
@@ -5823,7 +6750,326 @@ ${argsString}</pre
5823
6750
  this.threadDividerResizing = false;
5824
6751
  };
5825
6752
 
6753
+ private trackThreadsViewStateOnce(
6754
+ state: "locked" | "empty_enabled" | "enabled",
6755
+ threadCount: number,
6756
+ ): void {
6757
+ if (this.core?.telemetryDisabled) return;
6758
+ const key = `${state}:${this.getThreadServiceStatus()}`;
6759
+ if (this.viewedThreadsTelemetryStates.has(key)) return;
6760
+ this.viewedThreadsTelemetryStates.add(key);
6761
+ const props = this.getThreadsTelemetryProps({ thread_count: threadCount });
6762
+ if (state === "locked") {
6763
+ trackThreadsLockedViewed(props);
6764
+ } else if (state === "empty_enabled") {
6765
+ trackThreadsEmptyEnabledViewed(props);
6766
+ } else {
6767
+ trackThreadsEnabledViewed(props);
6768
+ }
6769
+ }
6770
+
6771
+ private renderThreadsLockedBackgroundMockup() {
6772
+ const threadRows = [
6773
+ { width: 74, accent: true },
6774
+ { width: 92 },
6775
+ { width: 68 },
6776
+ { width: 84 },
6777
+ { width: 58 },
6778
+ { width: 76 },
6779
+ ];
6780
+
6781
+ return html`
6782
+ <div
6783
+ aria-hidden="true"
6784
+ style="
6785
+ position: absolute;
6786
+ inset: 0;
6787
+ display: grid;
6788
+ grid-template-columns: minmax(180px, 28%) 1fr;
6789
+ overflow: hidden;
6790
+ opacity: 0.58;
6791
+ pointer-events: none;
6792
+ "
6793
+ >
6794
+ <div
6795
+ style="
6796
+ display: flex;
6797
+ flex-direction: column;
6798
+ gap: 12px;
6799
+ padding: 28px 24px;
6800
+ border-right: 1px solid #dbdbe5;
6801
+ background: #fafafa;
6802
+ "
6803
+ >
6804
+ ${threadRows.map(
6805
+ (row) => html`
6806
+ <div
6807
+ style="
6808
+ padding: 12px;
6809
+ border-radius: 8px;
6810
+ background: ${row.accent ? "#eee6fe" : "#ffffff"};
6811
+ box-shadow: inset 0 0 0 1px #eeeef4;
6812
+ "
6813
+ >
6814
+ <div
6815
+ style="
6816
+ height: 8px;
6817
+ width: ${row.width}%;
6818
+ border-radius: 99px;
6819
+ background: ${row.accent ? "#a984f5" : "#d7d7df"};
6820
+ "
6821
+ ></div>
6822
+ <div
6823
+ style="
6824
+ height: 6px;
6825
+ width: 88%;
6826
+ margin-top: 10px;
6827
+ border-radius: 99px;
6828
+ background: #e3e3eb;
6829
+ "
6830
+ ></div>
6831
+ <div
6832
+ style="
6833
+ height: 6px;
6834
+ width: 62%;
6835
+ margin-top: 7px;
6836
+ border-radius: 99px;
6837
+ background: #e8e8ef;
6838
+ "
6839
+ ></div>
6840
+ </div>
6841
+ `,
6842
+ )}
6843
+ </div>
6844
+ <div
6845
+ style="
6846
+ min-width: 0;
6847
+ padding: 42px 48px;
6848
+ background: #ffffff;
6849
+ "
6850
+ >
6851
+ <div
6852
+ style="
6853
+ height: 10px;
6854
+ width: 180px;
6855
+ border-radius: 99px;
6856
+ background: #d7d7df;
6857
+ "
6858
+ ></div>
6859
+ <div
6860
+ style="
6861
+ height: 8px;
6862
+ width: min(520px, 58%);
6863
+ margin-top: 28px;
6864
+ border-radius: 99px;
6865
+ background: #e3e3eb;
6866
+ "
6867
+ ></div>
6868
+ <div
6869
+ style="
6870
+ height: 8px;
6871
+ width: min(430px, 48%);
6872
+ margin-top: 12px;
6873
+ border-radius: 99px;
6874
+ background: #e8e8ef;
6875
+ "
6876
+ ></div>
6877
+ <div
6878
+ style="
6879
+ display: grid;
6880
+ grid-template-columns: repeat(2, minmax(0, 1fr));
6881
+ gap: 16px;
6882
+ max-width: 620px;
6883
+ margin-top: 30px;
6884
+ "
6885
+ >
6886
+ <div
6887
+ style="
6888
+ height: 116px;
6889
+ border-radius: 8px;
6890
+ background: #f5f5f8;
6891
+ box-shadow: inset 0 0 0 1px #eeeef4;
6892
+ "
6893
+ ></div>
6894
+ <div
6895
+ style="
6896
+ height: 116px;
6897
+ border-radius: 8px;
6898
+ background: #f5f5f8;
6899
+ box-shadow: inset 0 0 0 1px #eeeef4;
6900
+ "
6901
+ ></div>
6902
+ </div>
6903
+ <div
6904
+ style="
6905
+ height: 10px;
6906
+ width: min(680px, 74%);
6907
+ margin-top: 34px;
6908
+ border-radius: 99px;
6909
+ background: #e3e3eb;
6910
+ "
6911
+ ></div>
6912
+ <div
6913
+ style="
6914
+ height: 10px;
6915
+ width: min(560px, 60%);
6916
+ margin-top: 14px;
6917
+ border-radius: 99px;
6918
+ background: #e8e8ef;
6919
+ "
6920
+ ></div>
6921
+ </div>
6922
+ </div>
6923
+ `;
6924
+ }
6925
+
6926
+ private renderThreadsLockedView() {
6927
+ this.trackThreadsViewStateOnce("locked", 0);
6928
+ return html`
6929
+ <div
6930
+ style="
6931
+ position: relative;
6932
+ height: 100%;
6933
+ display: flex;
6934
+ align-items: center;
6935
+ justify-content: center;
6936
+ padding: 32px;
6937
+ overflow: hidden;
6938
+ background: #ffffff;
6939
+ "
6940
+ >
6941
+ ${this.renderThreadsLockedBackgroundMockup()}
6942
+ <div
6943
+ aria-hidden="true"
6944
+ style="
6945
+ position: absolute;
6946
+ inset: 0;
6947
+ pointer-events: none;
6948
+ background:
6949
+ radial-gradient(circle at center, rgba(255,255,255,0.9) 0, rgba(255,255,255,0.78) 24%, rgba(255,255,255,0.34) 48%, rgba(255,255,255,0.56) 100%);
6950
+ "
6951
+ ></div>
6952
+ <div
6953
+ style="
6954
+ position: relative;
6955
+ z-index: 1;
6956
+ max-width: 440px;
6957
+ text-align: center;
6958
+ color: #57575b;
6959
+ "
6960
+ >
6961
+ <div
6962
+ aria-hidden="true"
6963
+ style="
6964
+ margin: 0 auto 18px;
6965
+ display: flex;
6966
+ justify-content: center;
6967
+ "
6968
+ >
6969
+ <div
6970
+ style="
6971
+ display: flex;
6972
+ height: 44px;
6973
+ width: 44px;
6974
+ align-items: center;
6975
+ justify-content: center;
6976
+ border: 1px solid #dfd6fb;
6977
+ border-radius: 8px;
6978
+ background: #eee6fe;
6979
+ color: #57575b;
6980
+ box-shadow: 0 8px 18px rgba(87, 87, 91, 0.14);
6981
+ "
6982
+ >
6983
+ ${this.renderIcon("Lock")}
6984
+ </div>
6985
+ </div>
6986
+ <h2
6987
+ style="
6988
+ margin: 0 0 8px;
6989
+ font-size: 16px;
6990
+ line-height: 1.35;
6991
+ font-weight: 600;
6992
+ color: #010507;
6993
+ "
6994
+ >
6995
+ Enable Intelligence to inspect Threads.
6996
+ </h2>
6997
+ <p
6998
+ style="
6999
+ margin: 0 auto 18px;
7000
+ max-width: 380px;
7001
+ font-size: 13px;
7002
+ line-height: 1.55;
7003
+ color: #57575b;
7004
+ "
7005
+ >
7006
+ Persist conversations and inspect saved thread history from the
7007
+ Inspector.
7008
+ </p>
7009
+ <div
7010
+ style="
7011
+ display: flex;
7012
+ flex-wrap: wrap;
7013
+ justify-content: center;
7014
+ gap: 8px;
7015
+ "
7016
+ >
7017
+ <a
7018
+ href=${this.getTalkToEngineerUrl()}
7019
+ target="_blank"
7020
+ rel="noopener"
7021
+ style="
7022
+ display: inline-flex;
7023
+ min-height: 34px;
7024
+ align-items: center;
7025
+ justify-content: center;
7026
+ gap: 6px;
7027
+ border-radius: 6px;
7028
+ background: #010507;
7029
+ padding: 8px 12px;
7030
+ font-size: 12px;
7031
+ font-weight: 600;
7032
+ color: #ffffff;
7033
+ text-decoration: none;
7034
+ "
7035
+ @click=${this.handleThreadsTalkToEngineerClick}
7036
+ >
7037
+ Talk to an Engineer
7038
+ </a>
7039
+ <a
7040
+ href=${this.getIntelligenceSignupUrl()}
7041
+ target="_blank"
7042
+ rel="noopener"
7043
+ style="
7044
+ display: inline-flex;
7045
+ min-height: 34px;
7046
+ align-items: center;
7047
+ justify-content: center;
7048
+ gap: 6px;
7049
+ border-radius: 6px;
7050
+ border: 1px solid #dbdbe5;
7051
+ background: #ffffff;
7052
+ padding: 8px 12px;
7053
+ font-size: 12px;
7054
+ font-weight: 600;
7055
+ color: #57575b;
7056
+ text-decoration: none;
7057
+ "
7058
+ @click=${this.handleThreadsIntelligenceSignupClick}
7059
+ >
7060
+ Sign up for Intelligence
7061
+ </a>
7062
+ </div>
7063
+ </div>
7064
+ </div>
7065
+ `;
7066
+ }
7067
+
5826
7068
  private renderThreadsView() {
7069
+ if (!this.areThreadEndpointsAvailable()) {
7070
+ return this.renderThreadsLockedView();
7071
+ }
7072
+
5827
7073
  const displayThreads =
5828
7074
  this.selectedContext === "all-agents"
5829
7075
  ? this._threads
@@ -5847,91 +7093,110 @@ ${argsString}</pre
5847
7093
  ? (displayThreads.find((t) => t.id === this.selectedThreadId) ?? null)
5848
7094
  : null;
5849
7095
 
7096
+ if (!threadsErrorMessage) {
7097
+ this.trackThreadsViewStateOnce(
7098
+ displayThreads.length === 0 ? "empty_enabled" : "enabled",
7099
+ displayThreads.length,
7100
+ );
7101
+ }
7102
+
5850
7103
  return html`
5851
- <div style="display:flex;height:100%;overflow:hidden;">
5852
- <!-- Left sidebar: thread list -->
7104
+ <div style="display:flex;height:100%;overflow:hidden;flex-direction:column;">
5853
7105
  <div
5854
- style="width:${this.threadListWidth}px;flex-shrink:0;overflow:hidden;display:flex;flex-direction:column;border-right:1px solid #DBDBE5;"
7106
+ style="display:flex;align-items:center;justify-content:flex-end;border-bottom:1px solid #DBDBE5;background:#ffffff;padding:8px 12px;flex-shrink:0;"
5855
7107
  >
5856
- <cpk-thread-list
5857
- style="height:100%;"
5858
- .threads=${displayThreads}
5859
- .selectedThreadId=${this.selectedThreadId}
5860
- .errorMessage=${threadsErrorMessage}
5861
- @threadSelected=${(e: CustomEvent<string>) => {
5862
- this.selectedThreadId = e.detail;
5863
- this.requestUpdate();
5864
- }}
5865
- ></cpk-thread-list>
7108
+ <a
7109
+ href=${this.getTalkToEngineerUrl()}
7110
+ target="_blank"
7111
+ rel="noopener"
7112
+ style="display:inline-flex;align-items:center;gap:6px;border-radius:6px;border:1px solid #dbdbe5;background:#ffffff;padding:7px 10px;font-size:12px;font-weight:600;color:#57575b;text-decoration:none;"
7113
+ @click=${this.handleTalkToEngineerClick}
7114
+ >
7115
+ Talk to an Engineer
7116
+ </a>
5866
7117
  </div>
7118
+ <div style="display:flex;min-height:0;flex:1;overflow:hidden;">
7119
+ <!-- Left sidebar: thread list -->
7120
+ <div
7121
+ style="width:${this.threadListWidth}px;flex-shrink:0;overflow:hidden;display:flex;flex-direction:column;border-right:1px solid #DBDBE5;"
7122
+ >
7123
+ <cpk-thread-list
7124
+ style="height:100%;"
7125
+ .threads=${displayThreads}
7126
+ .selectedThreadId=${this.selectedThreadId}
7127
+ .errorMessage=${threadsErrorMessage}
7128
+ @threadSelected=${(e: CustomEvent<string>) => {
7129
+ this.selectedThreadId = e.detail;
7130
+ this.requestUpdate();
7131
+ }}
7132
+ ></cpk-thread-list>
7133
+ </div>
5867
7134
 
5868
- <!-- Resize divider -->
5869
- <div
5870
- style="width:4px;flex-shrink:0;cursor:col-resize;background:transparent;position:relative;z-index:1;"
5871
- @pointerdown=${this.handleThreadDividerPointerDown}
5872
- @pointermove=${this.handleThreadDividerPointerMove}
5873
- @pointerup=${this.handleThreadDividerPointerUp}
5874
- @pointercancel=${this.handleThreadDividerPointerUp}
5875
- ></div>
5876
-
5877
- <!-- Center + right: thread details or empty state -->
5878
- <div style="flex:1;min-width:0;overflow:hidden;display:flex;">
5879
- ${
5880
- this.selectedThreadId
5881
- ? html`<cpk-thread-details
5882
- style="flex:1;min-width:0;"
5883
- .threadId=${this.selectedThreadId}
5884
- .thread=${selectedThread}
5885
- .runtimeUrl=${this._core?.runtimeUrl ?? ""}
5886
- .headers=${this._core?.headers ?? {}}
5887
- .threadInspectionAvailable=${
5888
- this._core?.threadEndpoints?.inspect !== false
5889
- }
5890
- .liveMessageVersion=${
5891
- this.selectedThreadId
5892
- ? (this.liveMessageVersion.get(this.selectedThreadId) ??
5893
- 0)
5894
- : 0
5895
- }
5896
- .agentStateInput=${
5897
- selectedThread
5898
- ? this.getLatestStateForAgent(selectedThread.agentId)
5899
- : null
5900
- }
5901
- .agentEventsInput=${
5902
- selectedThread
5903
- ? (this.agentEvents.get(selectedThread.agentId) ?? [])
5904
- : []
5905
- }
5906
- ></cpk-thread-details>`
5907
- : html`
5908
- <div
5909
- style="
5910
- flex: 1;
5911
- display: flex;
5912
- flex-direction: column;
5913
- align-items: center;
5914
- justify-content: center;
5915
- gap: 8px;
5916
- color: #838389;
5917
- "
5918
- >
5919
- <svg
5920
- width="32"
5921
- height="32"
5922
- viewBox="0 0 24 24"
5923
- fill="none"
5924
- stroke="#c0c0c8"
5925
- stroke-width="1.5"
5926
- stroke-linecap="round"
5927
- stroke-linejoin="round"
7135
+ <!-- Resize divider -->
7136
+ <div
7137
+ style="width:4px;flex-shrink:0;cursor:col-resize;background:transparent;position:relative;z-index:1;"
7138
+ @pointerdown=${this.handleThreadDividerPointerDown}
7139
+ @pointermove=${this.handleThreadDividerPointerMove}
7140
+ @pointerup=${this.handleThreadDividerPointerUp}
7141
+ @pointercancel=${this.handleThreadDividerPointerUp}
7142
+ ></div>
7143
+
7144
+ <!-- Center + right: thread details or empty state -->
7145
+ <div style="flex:1;min-width:0;overflow:hidden;display:flex;">
7146
+ ${
7147
+ selectedThread
7148
+ ? html`<cpk-thread-details
7149
+ style="flex:1;min-width:0;"
7150
+ .threadId=${selectedThread.id}
7151
+ .thread=${selectedThread}
7152
+ .runtimeUrl=${this._core?.runtimeUrl ?? ""}
7153
+ .headers=${this._core?.headers ?? {}}
7154
+ .threadInspectionAvailable=${
7155
+ this._core?.threadEndpoints?.inspect !== false
7156
+ }
7157
+ .liveMessageVersion=${
7158
+ this.liveMessageVersion.get(selectedThread.id) ?? 0
7159
+ }
7160
+ .agentStateInput=${this.getLatestStateForAgent(
7161
+ selectedThread.agentId,
7162
+ )}
7163
+ .agentEventsInput=${
7164
+ this.agentEvents.get(selectedThread.agentId) ?? []
7165
+ }
7166
+ ></cpk-thread-details>`
7167
+ : html`
7168
+ <div
7169
+ style="
7170
+ flex: 1;
7171
+ display: flex;
7172
+ flex-direction: column;
7173
+ align-items: center;
7174
+ justify-content: center;
7175
+ gap: 8px;
7176
+ color: #838389;
7177
+ "
5928
7178
  >
5929
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
5930
- </svg>
5931
- <span style="font-size: 13px">${displayThreads.length === 0 ? "No threads yet" : "Select a thread to inspect"}</span>
5932
- </div>
5933
- `
5934
- }
7179
+ <svg
7180
+ width="32"
7181
+ height="32"
7182
+ viewBox="0 0 24 24"
7183
+ fill="none"
7184
+ stroke="#c0c0c8"
7185
+ stroke-width="1.5"
7186
+ stroke-linecap="round"
7187
+ stroke-linejoin="round"
7188
+ >
7189
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
7190
+ </svg>
7191
+ <span style="font-size: 13px">${
7192
+ displayThreads.length === 0
7193
+ ? "No threads yet"
7194
+ : "Select a thread to inspect"
7195
+ }</span>
7196
+ </div>
7197
+ `
7198
+ }
7199
+ </div>
5935
7200
  </div>
5936
7201
  </div>
5937
7202
  `;
@@ -6068,28 +7333,30 @@ ${argsString}</pre
6068
7333
  <div class="relative h-full w-full overflow-y-auto overflow-x-hidden">
6069
7334
  <table class="w-full table-fixed border-collapse text-xs box-border">
6070
7335
  <colgroup>
6071
- <col style="width:${this.evtColWidths[0]}px">
6072
- <col style="width:${this.evtColWidths[1]}px">
6073
- <col style="width:${this.evtColWidths[2]}px">
6074
- <col>
7336
+ <col style="width:${this.evtColWidths[0]}px" />
7337
+ <col style="width:${this.evtColWidths[1]}px" />
7338
+ <col style="width:${this.evtColWidths[2]}px" />
7339
+ <col />
6075
7340
  </colgroup>
6076
7341
  <thead class="sticky top-0 z-10">
6077
7342
  <tr class="bg-white">
6078
7343
  ${["Agent", "Time", "Event Type"].map(
6079
- (label, col) => html`
6080
- <th
6081
- class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
6082
- style="position:relative;overflow:hidden;"
6083
- >
6084
- ${label}
6085
- <div
6086
- style="position:absolute;top:0;right:0;width:5px;height:100%;cursor:col-resize;user-select:none;background:transparent;"
6087
- @pointerdown=${(e: PointerEvent) => this._onEvtColResizeStart(e, col)}
6088
- @pointermove=${(e: PointerEvent) => this._onEvtColResizeMove(e)}
6089
- @pointerup=${() => this._onEvtColResizeEnd()}
6090
- @pointercancel=${() => this._onEvtColResizeEnd()}
6091
- ></div>
6092
- </th>`,
7344
+ (label, col) =>
7345
+ html` <th
7346
+ class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
7347
+ style="position:relative;overflow:hidden;"
7348
+ >
7349
+ ${label}
7350
+ <div
7351
+ style="position:absolute;top:0;right:0;width:5px;height:100%;cursor:col-resize;user-select:none;background:transparent;"
7352
+ @pointerdown=${(e: PointerEvent) =>
7353
+ this._onEvtColResizeStart(e, col)}
7354
+ @pointermove=${(e: PointerEvent) =>
7355
+ this._onEvtColResizeMove(e)}
7356
+ @pointerup=${() => this._onEvtColResizeEnd()}
7357
+ @pointercancel=${() => this._onEvtColResizeEnd()}
7358
+ ></div>
7359
+ </th>`,
6093
7360
  )}
6094
7361
  <th
6095
7362
  class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
@@ -6421,8 +7688,14 @@ ${prettyEvent}</pre
6421
7688
  ? html`
6422
7689
  <div class="w-full text-xs">
6423
7690
  <div class="flex bg-gray-50">
6424
- <div class="w-40 shrink-0 px-4 py-2 font-medium text-gray-700">Role</div>
6425
- <div class="flex-1 px-4 py-2 font-medium text-gray-700">Content</div>
7691
+ <div
7692
+ class="w-40 shrink-0 px-4 py-2 font-medium text-gray-700"
7693
+ >
7694
+ Role
7695
+ </div>
7696
+ <div class="flex-1 px-4 py-2 font-medium text-gray-700">
7697
+ Content
7698
+ </div>
6426
7699
  </div>
6427
7700
  <div class="divide-y divide-gray-200">
6428
7701
  ${messages.map((msg) => {
@@ -6445,7 +7718,9 @@ ${prettyEvent}</pre
6445
7718
  <div class="flex items-start">
6446
7719
  <div class="w-40 shrink-0 px-4 py-2">
6447
7720
  <span
6448
- class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${roleColors[role] || roleColors.unknown}"
7721
+ class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${
7722
+ roleColors[role] || roleColors.unknown
7723
+ }"
6449
7724
  >
6450
7725
  ${role}
6451
7726
  </span>
@@ -6603,8 +7878,8 @@ ${prettyEvent}</pre
6603
7878
  }
6604
7879
 
6605
7880
  if (key === "threads") {
6606
- if (this.selectedMenu !== "threads" && !this.core?.telemetryDisabled) {
6607
- trackThreadsTabClicked();
7881
+ if (previousMenu !== "threads" && !this.core?.telemetryDisabled) {
7882
+ trackThreadsTabClicked(this.getThreadsTelemetryProps());
6608
7883
  }
6609
7884
  this.autoSelectLatestThread();
6610
7885
  }
@@ -7284,6 +8559,22 @@ ${prettyEvent}</pre
7284
8559
  }
7285
8560
  </button>
7286
8561
  </div>
8562
+ <pre
8563
+ style="
8564
+ margin: 0;
8565
+ max-height: 180px;
8566
+ overflow: auto;
8567
+ white-space: pre-wrap;
8568
+ word-break: break-word;
8569
+ border-radius: 6px;
8570
+ border: 1px solid #eeeef4;
8571
+ background: #f7f7f9;
8572
+ padding: 10px;
8573
+ font-size: 11px;
8574
+ line-height: 1.5;
8575
+ color: #2d2d30;
8576
+ "
8577
+ >${this.formatContextValue(context.value)}</pre>
7287
8578
  `
7288
8579
  : html`
7289
8580
  <div class="flex items-center justify-center py-4 text-xs text-gray-500">
@@ -7435,7 +8726,9 @@ ${prettyEvent}</pre
7435
8726
  return nothing;
7436
8727
  }
7437
8728
 
7438
- return html`<div class="mx-4 mt-3 mb-3 rounded-xl border border-slate-200 bg-white px-4 py-3">
8729
+ return html`<div
8730
+ class="mx-4 mt-3 mb-3 rounded-xl border border-slate-200 bg-white px-4 py-3"
8731
+ >
7439
8732
  <div
7440
8733
  class="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-900"
7441
8734
  >
@@ -7454,7 +8747,13 @@ ${prettyEvent}</pre
7454
8747
  ${this.renderIcon("X")}
7455
8748
  </button>
7456
8749
  </div>
7457
- <div class="announcement-body ${this.announcementExpanded ? "announcement-body--expanded" : "announcement-body--collapsed"}">
8750
+ <div
8751
+ class="announcement-body ${
8752
+ this.announcementExpanded
8753
+ ? "announcement-body--expanded"
8754
+ : "announcement-body--collapsed"
8755
+ }"
8756
+ >
7458
8757
  <div
7459
8758
  class="announcement-content"
7460
8759
  @click=${this.handleAnnouncementContentClick}
@@ -7632,10 +8931,15 @@ ${prettyEvent}</pre
7632
8931
  ): Promise<string | null> {
7633
8932
  const renderer = new marked.Renderer();
7634
8933
  renderer.link = (href, title, text) => {
7635
- const safeHref = this.escapeHtmlAttr(this.appendRefParam(href ?? ""));
8934
+ const safeHref = this.escapeHtmlAttr(
8935
+ this.isSafeAnnouncementHref(href ?? "")
8936
+ ? this.appendRefParam(href ?? "")
8937
+ : "#",
8938
+ );
7636
8939
  const titleAttr = title ? ` title="${this.escapeHtmlAttr(title)}"` : "";
7637
8940
  return `<a href="${safeHref}" target="_blank" rel="noopener"${titleAttr}>${text}</a>`;
7638
8941
  };
8942
+ renderer.html = (html) => escapeHtml(html);
7639
8943
  renderer.code = (code, lang) => {
7640
8944
  const safeLang = (lang ?? "").replace(/[^a-z0-9-]/gi, "");
7641
8945
  const langClass = safeLang ? ` class="language-${safeLang}"` : "";
@@ -7646,6 +8950,24 @@ ${prettyEvent}</pre
7646
8950
  return marked.parse(markdown, { renderer, async: false });
7647
8951
  }
7648
8952
 
8953
+ private isSafeAnnouncementHref(href: string): boolean {
8954
+ try {
8955
+ const url = new URL(
8956
+ href,
8957
+ typeof window !== "undefined"
8958
+ ? window.location.href
8959
+ : "https://copilotkit.ai",
8960
+ );
8961
+ return (
8962
+ url.protocol === "http:" ||
8963
+ url.protocol === "https:" ||
8964
+ url.protocol === "mailto:"
8965
+ );
8966
+ } catch {
8967
+ return false;
8968
+ }
8969
+ }
8970
+
7649
8971
  private copyResetTimeouts = new WeakMap<HTMLButtonElement, number>();
7650
8972
 
7651
8973
  private encodeBase64(value: string): string {
@@ -7710,8 +9032,9 @@ ${prettyEvent}</pre
7710
9032
  }
7711
9033
  };
7712
9034
 
7713
- private appendRefParam(href: string): string {
9035
+ private appendRefParam(href: string, ref = "cpk-inspector"): string {
7714
9036
  try {
9037
+ const isRootRelative = href.startsWith("/") && !href.startsWith("//");
7715
9038
  const url = new URL(
7716
9039
  href,
7717
9040
  typeof window !== "undefined"
@@ -7719,7 +9042,7 @@ ${prettyEvent}</pre
7719
9042
  : "https://copilotkit.ai",
7720
9043
  );
7721
9044
  if (!url.searchParams.has("ref")) {
7722
- url.searchParams.append("ref", "cpk-inspector");
9045
+ url.searchParams.append("ref", ref);
7723
9046
  }
7724
9047
  // Propagate the inspector's anonymous distinct-ID so the website /
7725
9048
  // Ops API can call posthog.alias(...) on signup-flow landing and
@@ -7728,19 +9051,28 @@ ${prettyEvent}</pre
7728
9051
  // suppresses cross-domain ID leaks too.
7729
9052
  if (
7730
9053
  !url.searchParams.has("posthog_distinct_id") &&
7731
- !this.core?.telemetryDisabled
9054
+ !this.core?.telemetryDisabled &&
9055
+ this.isCopilotKitDestination(url)
7732
9056
  ) {
7733
9057
  const distinctId = getTelemetryDistinctIdForUrl();
7734
9058
  if (distinctId) {
7735
9059
  url.searchParams.append("posthog_distinct_id", distinctId);
7736
9060
  }
7737
9061
  }
9062
+ if (isRootRelative) {
9063
+ return `${url.pathname}${url.search}${url.hash}`;
9064
+ }
7738
9065
  return url.toString();
7739
9066
  } catch {
7740
9067
  return href;
7741
9068
  }
7742
9069
  }
7743
9070
 
9071
+ private isCopilotKitDestination(url: URL): boolean {
9072
+ const hostname = url.hostname.toLowerCase();
9073
+ return hostname === "copilotkit.ai" || hostname.endsWith(".copilotkit.ai");
9074
+ }
9075
+
7744
9076
  private escapeHtmlAttr(value: string): string {
7745
9077
  return escapeHtml(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
7746
9078
  }