@copilotkit/web-inspector 1.61.2 → 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
@@ -68,6 +68,7 @@ import type { InspectorThreadTelemetryProps } from "./lib/telemetry.js";
68
68
  export type { Anchor } from "./lib/types.js";
69
69
 
70
70
  export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const;
71
+ export const THREAD_INSPECTOR_TAG = "cpk-thread-inspector" as const;
71
72
 
72
73
  type LucideIconName = keyof typeof icons;
73
74
 
@@ -200,15 +201,62 @@ type InspectorEvent = {
200
201
 
201
202
  // ─── Thread details types ────────────────────────────────────────────────────
202
203
 
203
- 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 = {
204
215
  id: string;
205
216
  role: string;
206
217
  content?: string;
207
- toolCalls?: Array<{ id: string; name: string; args: string }>;
218
+ toolCalls?: ThreadDebuggerToolCall[];
208
219
  toolCallId?: string;
209
220
  /** Present when role === "activity" (Generative UI output). */
210
221
  activityType?: string;
211
- }
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
+ };
212
260
 
213
261
  interface ConversationUser {
214
262
  id: string;
@@ -282,9 +330,39 @@ interface ApiAgentEvent {
282
330
  type: string;
283
331
  timestamp: string | number;
284
332
  payload: Record<string, unknown>;
333
+ sourceIndex?: number;
334
+ rawEvent?: ThreadDebuggerEvent;
285
335
  }
286
336
 
287
- 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" };
288
366
 
289
367
  // ─── JSON syntax highlighter ─────────────────────────────────────────────────
290
368
  // Inline-styled so shadow DOM encapsulation preserves colors when the output
@@ -618,28 +696,30 @@ class CpkThreadList extends LitElement {
618
696
  ${
619
697
  this.errorMessage
620
698
  ? html`
621
- <svg
622
- width="24"
623
- height="24"
624
- viewBox="0 0 24 24"
625
- fill="none"
626
- stroke="currentColor"
627
- stroke-width="1.5"
628
- stroke-linecap="round"
629
- stroke-linejoin="round"
630
- 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;"
631
718
  >
632
- <circle cx="12" cy="12" r="10" />
633
- <line x1="12" y1="8" x2="12" y2="12" />
634
- <line x1="12" y1="16" x2="12.01" y2="16" />
635
- </svg>
636
- <div>
637
- Failed to load threads
638
- <div style="font-size:11px;margin-top:4px;color:#c0333a;">
639
- ${this.errorMessage}
640
- </div>
719
+ ${this.errorMessage}
641
720
  </div>
642
- `
721
+ </div>
722
+ `
643
723
  : this.threads.length === 0
644
724
  ? html`
645
725
  <svg
@@ -671,21 +751,14 @@ class CpkThreadList extends LitElement {
671
751
  }
672
752
  }
673
753
 
674
- // ─── cpk-thread-details ──────────────────────────────────────────────────────
675
- // Renders the selected thread's conversation, agent state, and AG-UI events.
676
- // Conversation comes from the runtime's `/threads/:id/messages` endpoint
677
- // (always thread-accurate). Agent state and AG-UI events accept live inputs
678
- // (`agentStateInput`, `agentEventsInput`) from the parent inspector's ongoing
679
- // agent subscriptions; when those are absent we fall back to the per-thread
680
- // fetched data via `/threads/:id/{events,state}`.
681
-
682
- // Exported (with the underscore-prefixed name signalling internal/test-only)
683
- // so unit tests can pin down the per-panel template-cache invariants without
684
- // reaching through `customElements`. Production consumers continue to use the
685
- // `cpk-thread-details` custom element registered below.
686
- 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 {
687
759
  static properties = {
688
760
  threadId: { attribute: false },
761
+ provider: { attribute: false },
689
762
  thread: { attribute: false },
690
763
  runtimeUrl: { attribute: false },
691
764
  headers: { attribute: false },
@@ -694,6 +767,7 @@ export class ɵCpkThreadDetails extends LitElement {
694
767
  agentEventsInput: { attribute: false },
695
768
  liveMessageVersion: { attribute: false },
696
769
  _tab: { state: true },
770
+ _fetchedMetadata: { state: true },
697
771
  _conversation: { state: true },
698
772
  _fetchedEvents: { state: true },
699
773
  _fetchedState: { state: true },
@@ -714,7 +788,8 @@ export class ɵCpkThreadDetails extends LitElement {
714
788
  };
715
789
 
716
790
  threadId: string | null = null;
717
- thread: ɵThread | null = null;
791
+ provider: ThreadDebuggerProvider | null = null;
792
+ thread: ThreadDebuggerMetadata | ɵThread | null = null;
718
793
  runtimeUrl = "";
719
794
  headers: Record<string, string> = {};
720
795
  threadInspectionAvailable = false;
@@ -728,7 +803,8 @@ export class ɵCpkThreadDetails extends LitElement {
728
803
  */
729
804
  liveMessageVersion = 0;
730
805
 
731
- private _tab: ThreadDetailsTab = "conversation";
806
+ private _tab: ThreadDetailsTab = "timeline";
807
+ private _fetchedMetadata: ThreadDebuggerMetadata | null = null;
732
808
  private _conversation: ConversationItem[] = [];
733
809
  private _fetchedEvents: ApiAgentEvent[] | null = null;
734
810
  private _fetchedState: Record<string, unknown> | null = null;
@@ -761,9 +837,9 @@ export class ɵCpkThreadDetails extends LitElement {
761
837
  * switching back to AG-UI Events on a thread with hundreds of events
762
838
  * triggers a multi-second DOM-creation pass each time.
763
839
  *
764
- * Reset to {"conversation"} when the selected thread changes.
840
+ * Reset to {"timeline"} when the selected thread changes.
765
841
  */
766
- private _activatedTabs: Set<ThreadDetailsTab> = new Set(["conversation"]);
842
+ private _activatedTabs: Set<ThreadDetailsTab> = new Set(["timeline"]);
767
843
  /**
768
844
  * Memoized per-panel templates keyed by the inputs they render from.
769
845
  * When the underlying data hasn't changed (same `_conversation` /
@@ -775,9 +851,17 @@ export class ɵCpkThreadDetails extends LitElement {
775
851
  * reference; if any element flips, the cache misses and rebuilds.
776
852
  */
777
853
  private _panelTplCache: Map<
778
- ThreadDetailsTab,
854
+ ThreadDetailsPanelCacheSlot,
779
855
  { key: readonly unknown[]; tpl: TemplateResult }
780
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;
781
865
  /**
782
866
  * Tracks whether we've fetched events for the current thread yet. Events
783
867
  * fetch lazily on first sub-tab click so a large response's JSON.parse
@@ -790,8 +874,9 @@ export class ɵCpkThreadDetails extends LitElement {
790
874
  * lazy-load reasoning as `_eventsFetched`.
791
875
  */
792
876
  private _stateFetched = false;
793
- private _lastFetchedThreadId: string | null = null;
877
+ private _lastLoadKey: string | null = null;
794
878
  private _lastSeenLiveMessageVersion = 0;
879
+ private _metadataAbort: AbortController | null = null;
795
880
  private _messagesAbort: AbortController | null = null;
796
881
  private _eventsAbort: AbortController | null = null;
797
882
  private _stateAbort: AbortController | null = null;
@@ -801,18 +886,53 @@ export class ɵCpkThreadDetails extends LitElement {
801
886
  private _dividerStartWidth = 0;
802
887
 
803
888
  static readonly COLLAPSE_THRESHOLD = 800;
804
- private static readonly TAB_LIST: ReadonlyArray<{
889
+ static readonly TAB_LIST: ReadonlyArray<{
805
890
  id: ThreadDetailsTab;
806
891
  label: string;
807
892
  }> = [
808
- { id: "conversation", label: "Conversation" },
809
- { id: "agent-state", label: "Agent State" },
810
- { 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" },
811
896
  ];
812
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
+
813
933
  private renderTabContent(id: ThreadDetailsTab): TemplateResult {
814
- if (id === "conversation") return this.renderConversation();
815
- if (id === "agent-state") return this.renderState();
934
+ if (id === "timeline") return this.renderTimeline();
935
+ if (id === "state") return this.renderState();
816
936
  return this.renderEvents();
817
937
  }
818
938
 
@@ -845,10 +965,10 @@ export class ɵCpkThreadDetails extends LitElement {
845
965
  // the entire panel for seconds — including making the tab buttons
846
966
  // themselves feel unresponsive.
847
967
  if (!this.threadId) return;
848
- if (id === "ag-ui-events" && !this._eventsFetched) {
968
+ if ((id === "timeline" || id === "raw-events") && !this._eventsFetched) {
849
969
  this._eventsFetched = true;
850
970
  void this.fetchEvents(this.threadId);
851
- } else if (id === "agent-state" && !this._stateFetched) {
971
+ } else if (id === "state" && !this._stateFetched) {
852
972
  this._stateFetched = true;
853
973
  void this.fetchState(this.threadId);
854
974
  }
@@ -980,6 +1100,42 @@ export class ɵCpkThreadDetails extends LitElement {
980
1100
  flex-shrink: 0;
981
1101
  }
982
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
+
983
1139
  /*
984
1140
  * Each tab's content is wrapped in this panel so the keep-mounted
985
1141
  * inactive panels can be hidden via display:none without disturbing
@@ -1200,6 +1356,81 @@ export class ɵCpkThreadDetails extends LitElement {
1200
1356
  background: #e9e9ef;
1201
1357
  }
1202
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
+
1203
1434
  /* ── Generative UI ──────────────────────────────────────────────── */
1204
1435
  @keyframes cpk-genui-enter {
1205
1436
  from {
@@ -1409,36 +1640,27 @@ export class ɵCpkThreadDetails extends LitElement {
1409
1640
  `;
1410
1641
 
1411
1642
  updated(_changed: Map<string, unknown>): void {
1412
- if (this.threadId !== this._lastFetchedThreadId) {
1413
- this._lastFetchedThreadId = this.threadId;
1643
+ const loadKey = this.currentLoadKey();
1644
+ if (loadKey !== this._lastLoadKey) {
1645
+ this._lastLoadKey = loadKey;
1414
1646
  this._lastSeenLiveMessageVersion = this.liveMessageVersion;
1415
- this._tab = "conversation";
1416
- this._activatedTabs = new Set(["conversation"]);
1417
- this._panelTplCache = new Map();
1418
- this._expandedTools = new Set();
1419
- this._expandedMessages = new Set();
1420
- this._messagesAbort?.abort();
1421
- this._messagesAbort = null;
1422
- this._eventsAbort?.abort();
1423
- this._eventsAbort = null;
1424
- this._stateAbort?.abort();
1425
- this._stateAbort = null;
1426
- // Reset cleared so the next click into events/state triggers a fresh
1427
- // fetch. Eagerly clear `_fetchedEvents` / `_fetchedState` so the empty
1428
- // state doesn't briefly show last thread's data.
1429
- this._eventsFetched = false;
1430
- this._stateFetched = false;
1431
- this._fetchedEvents = null;
1432
- this._fetchedState = null;
1647
+ this.resetLoadedThreadData();
1433
1648
 
1434
1649
  if (this.threadId) {
1435
- // Conversation is the default tab and shows immediately on thread
1436
- // open, so fetch eagerly. Events and state are only visible once the
1437
- // user clicks their sub-tab; deferring those fetches prevents a long
1438
- // JSON.parse of a large events payload from blocking the main thread
1439
- // before the user has even shown intent to view them.
1440
- 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
+ }
1441
1662
  } else {
1663
+ this._fetchedMetadata = null;
1442
1664
  this._conversation = [];
1443
1665
  }
1444
1666
  } else if (
@@ -1459,6 +1681,90 @@ export class ɵCpkThreadDetails extends LitElement {
1459
1681
  }
1460
1682
  }
1461
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
+
1462
1768
  /**
1463
1769
  * Fetch the canonical conversation for `threadId` from the runtime.
1464
1770
  *
@@ -1473,10 +1779,11 @@ export class ɵCpkThreadDetails extends LitElement {
1473
1779
  threadId: string,
1474
1780
  silent: boolean = false,
1475
1781
  ): Promise<void> {
1476
- if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1782
+ if (!this.canFetchMessages()) {
1477
1783
  if (!silent) this._conversation = [];
1478
1784
  return;
1479
1785
  }
1786
+ this._messagesAbort?.abort();
1480
1787
  const controller = new AbortController();
1481
1788
  this._messagesAbort = controller;
1482
1789
  if (!silent) {
@@ -1484,15 +1791,13 @@ export class ɵCpkThreadDetails extends LitElement {
1484
1791
  this._messagesError = null;
1485
1792
  }
1486
1793
  try {
1487
- const res = await fetch(
1488
- this.getThreadInspectionUrl(threadId, "messages"),
1489
- { headers: { ...this.headers }, signal: controller.signal },
1490
- );
1491
- if (controller.signal.aborted || this.threadId !== threadId) return;
1492
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1493
- 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);
1494
1799
  if (controller.signal.aborted || this.threadId !== threadId) return;
1495
- this._conversation = this.mapMessages(data.messages);
1800
+ this._conversation = this.mapMessages(messages);
1496
1801
  } catch (err) {
1497
1802
  if (err instanceof Error && err.name === "AbortError") return;
1498
1803
  if (!silent) {
@@ -1510,45 +1815,51 @@ export class ɵCpkThreadDetails extends LitElement {
1510
1815
  }
1511
1816
 
1512
1817
  private async fetchEvents(threadId: string): Promise<void> {
1513
- this._eventsNotAvailable = false;
1514
- if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1818
+ if (!this.canFetchEvents()) {
1515
1819
  this._fetchedEvents = null;
1516
1820
  return;
1517
1821
  }
1822
+ this._eventsAbort?.abort();
1518
1823
  const controller = new AbortController();
1519
1824
  this._eventsAbort = controller;
1520
1825
  this._loadingEvents = true;
1521
1826
  this._eventsError = null;
1522
1827
  try {
1523
- const res = await fetch(this.getThreadInspectionUrl(threadId, "events"), {
1524
- headers: { ...this.headers },
1525
- signal: controller.signal,
1526
- });
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);
1527
1836
  // Drop results if a newer fetch superseded this one (thread switched
1528
- // mid-flight). Without this, switching A→B can leave thread B's view
1529
- // 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.
1530
1840
  if (controller.signal.aborted || this.threadId !== threadId) return;
1531
- if (res.status === 501) {
1532
- // Endpoint not supported on this runtime (e.g. Intelligence platform).
1533
- // Mark unavailable so we don't misleadingly fall back to the parent's
1534
- // live agent events — those are agent-keyed, not thread-keyed, and
1535
- // would render identical across every thread on the same agent.
1841
+ if (result.status === "not-available") {
1536
1842
  this._eventsNotAvailable = true;
1537
- this._fetchedEvents = null;
1843
+ this._fetchedEvents = [];
1844
+ if (this.canFetchMessages()) {
1845
+ void this.fetchMessages(threadId);
1846
+ }
1538
1847
  return;
1539
1848
  }
1540
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1541
- const data = (await res.json()) as {
1542
- events: Array<Record<string, unknown>>;
1543
- };
1544
- if (controller.signal.aborted || this.threadId !== threadId) return;
1545
- 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
+ }
1546
1854
  } catch (err) {
1547
1855
  if (err instanceof Error && err.name === "AbortError") return;
1548
1856
  if (this.threadId !== threadId) return;
1549
1857
  this._eventsError =
1550
1858
  err instanceof Error ? err.message : "Failed to load events";
1551
1859
  this._fetchedEvents = [];
1860
+ if (this.canFetchMessages()) {
1861
+ void this.fetchMessages(threadId);
1862
+ }
1552
1863
  } finally {
1553
1864
  if (!controller.signal.aborted && this.threadId === threadId) {
1554
1865
  this._loadingEvents = false;
@@ -1557,32 +1868,31 @@ export class ɵCpkThreadDetails extends LitElement {
1557
1868
  }
1558
1869
 
1559
1870
  private async fetchState(threadId: string): Promise<void> {
1560
- this._stateNotAvailable = false;
1561
- if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1871
+ if (!this.canFetchState()) {
1562
1872
  this._fetchedState = null;
1563
1873
  return;
1564
1874
  }
1875
+ this._stateAbort?.abort();
1565
1876
  const controller = new AbortController();
1566
1877
  this._stateAbort = controller;
1567
1878
  this._loadingState = true;
1568
1879
  this._stateError = null;
1569
1880
  try {
1570
- const res = await fetch(this.getThreadInspectionUrl(threadId, "state"), {
1571
- headers: { ...this.headers },
1572
- signal: controller.signal,
1573
- });
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);
1574
1889
  if (controller.signal.aborted || this.threadId !== threadId) return;
1575
- if (res.status === 501) {
1890
+ if (result.status === "not-available") {
1576
1891
  this._stateNotAvailable = true;
1577
1892
  this._fetchedState = null;
1578
1893
  return;
1579
1894
  }
1580
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1581
- const data = (await res.json()) as {
1582
- state: Record<string, unknown> | null;
1583
- };
1584
- if (controller.signal.aborted || this.threadId !== threadId) return;
1585
- this._fetchedState = data.state ?? null;
1895
+ this._fetchedState = result.state ?? null;
1586
1896
  } catch (err) {
1587
1897
  if (err instanceof Error && err.name === "AbortError") return;
1588
1898
  if (this.threadId !== threadId) return;
@@ -1596,6 +1906,55 @@ export class ɵCpkThreadDetails extends LitElement {
1596
1906
  }
1597
1907
  }
1598
1908
 
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
+
1599
1958
  private getThreadInspectionUrl(
1600
1959
  threadId: string,
1601
1960
  resource: "messages" | "events" | "state",
@@ -1603,7 +1962,7 @@ export class ɵCpkThreadDetails extends LitElement {
1603
1962
  return `${this.runtimeUrl.replace(/\/+$/, "")}/threads/${encodeURIComponent(threadId)}/${resource}`;
1604
1963
  }
1605
1964
 
1606
- private mapMessages(messages: ApiThreadMessage[]): ConversationItem[] {
1965
+ private mapMessages(messages: ThreadDebuggerMessage[]): ConversationItem[] {
1607
1966
  const items: ConversationItem[] = [];
1608
1967
  const toolCallMap = new Map<string, ConversationToolCall>();
1609
1968
  for (const msg of messages) {
@@ -1618,19 +1977,20 @@ export class ɵCpkThreadDetails extends LitElement {
1618
1977
  if (msg.toolCalls?.length) {
1619
1978
  for (const tc of msg.toolCalls) {
1620
1979
  let args: Record<string, unknown> = {};
1621
- try {
1622
- args = JSON.parse(tc.args) as Record<string, unknown>;
1623
- } catch (err) {
1624
- // Inspector is a debugging surface — surface malformed payloads
1625
- // instead of silently substituting `{}`. The sentinel lets the
1626
- // renderer flag "raw arguments — failed to parse" if/when it
1627
- // grows that branch; the console.error gives anyone with the
1628
- // devtools open immediate visibility into the offending blob.
1629
- console.error(
1630
- "[CopilotKit Inspector] Failed to parse tool-call arguments",
1631
- { toolCallId: tc.id, raw: tc.args, error: err },
1632
- );
1633
- 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;
1634
1994
  }
1635
1995
  const item: ConversationToolCall = {
1636
1996
  id: tc.id,
@@ -1684,22 +2044,284 @@ export class ɵCpkThreadDetails extends LitElement {
1684
2044
  return items;
1685
2045
  }
1686
2046
 
1687
- private mapApiEvents(
1688
- events: Array<Record<string, unknown>>,
1689
- ): ApiAgentEvent[] {
1690
- return events.map((event) => {
1691
- const { type, timestamp, ...rest } = event;
2047
+ private mapApiEvents(events: ThreadDebuggerEvent[]): ApiAgentEvent[] {
2048
+ return events.map((event, index) => {
2049
+ const { type, timestamp, payload, ...rest } = event;
1692
2050
  return {
1693
2051
  type: typeof type === "string" ? type : "UNKNOWN",
1694
2052
  timestamp:
1695
2053
  typeof timestamp === "string" || typeof timestamp === "number"
1696
2054
  ? timestamp
1697
2055
  : Date.now(),
1698
- payload: rest,
2056
+ payload: payload ?? rest,
2057
+ sourceIndex: index + 1,
2058
+ rawEvent: event,
1699
2059
  };
1700
2060
  });
1701
2061
  }
1702
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
+
1703
2325
  private get renderItems(): RenderItem[] {
1704
2326
  const items = this._conversation;
1705
2327
  const result: RenderItem[] = [];
@@ -1742,7 +2364,7 @@ export class ɵCpkThreadDetails extends LitElement {
1742
2364
  }
1743
2365
 
1744
2366
  private get duration(): string {
1745
- const t = this.thread;
2367
+ const t = this.metadata;
1746
2368
  if (!t?.createdAt || !t?.updatedAt) return "—";
1747
2369
  const ms =
1748
2370
  new Date(t.updatedAt).getTime() - new Date(t.createdAt).getTime();
@@ -1775,7 +2397,16 @@ export class ɵCpkThreadDetails extends LitElement {
1775
2397
  // threads (those would render identically for every thread on the same
1776
2398
  // agent and mislead the reader).
1777
2399
  if (this._eventsNotAvailable) return [];
1778
- 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;
1779
2410
  }
1780
2411
 
1781
2412
  private get activeState(): Record<string, unknown> | null {
@@ -1793,6 +2424,10 @@ export class ɵCpkThreadDetails extends LitElement {
1793
2424
  return id.length > 20 ? id.slice(0, 8) + "…" : id;
1794
2425
  }
1795
2426
 
2427
+ private get metadata(): ThreadDebuggerMetadata | null {
2428
+ return this._fetchedMetadata ?? this.thread ?? null;
2429
+ }
2430
+
1796
2431
  private fmtTime(dateStr: string | null | undefined): string {
1797
2432
  if (!dateStr) return "—";
1798
2433
  const d = new Date(dateStr);
@@ -1841,7 +2476,7 @@ export class ɵCpkThreadDetails extends LitElement {
1841
2476
  <!-- Tab bar -->
1842
2477
  <div class="cpk-td__tabs-header">
1843
2478
  <div class="cpk-td__tab-group" role="tablist">
1844
- ${ɵCpkThreadDetails.TAB_LIST.map(
2479
+ ${CpkThreadInspector.TAB_LIST.map(
1845
2480
  (tab) => html`
1846
2481
  <button
1847
2482
  role="tab"
@@ -1857,6 +2492,7 @@ export class ɵCpkThreadDetails extends LitElement {
1857
2492
  </div>
1858
2493
  ${this.renderPanelToggle()}
1859
2494
  </div>
2495
+ ${this.renderMetadataStrip()}
1860
2496
 
1861
2497
  <!-- Scrollable content -->
1862
2498
  <div class="cpk-td__content">
@@ -1867,18 +2503,18 @@ export class ɵCpkThreadDetails extends LitElement {
1867
2503
  `
1868
2504
  : nothing
1869
2505
  }
1870
- ${ɵCpkThreadDetails.TAB_LIST.map((tab) =>
2506
+ ${CpkThreadInspector.TAB_LIST.map((tab) =>
1871
2507
  this._activatedTabs.has(tab.id)
1872
2508
  ? html`<div
1873
- class="cpk-td__panel"
1874
- style=${
1875
- this._tab === tab.id && !this._panelInitializing
1876
- ? ""
1877
- : "display:none"
1878
- }
1879
- >
1880
- ${this.renderTabContent(tab.id)}
1881
- </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>`
1882
2518
  : nothing,
1883
2519
  )}
1884
2520
  </div>
@@ -1915,6 +2551,134 @@ export class ɵCpkThreadDetails extends LitElement {
1915
2551
  `;
1916
2552
  }
1917
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
+ >
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
+ }
2678
+ </div>
2679
+ `;
2680
+ }
2681
+
1918
2682
  private renderConversation() {
1919
2683
  if (this._loadingMessages) {
1920
2684
  return html`
@@ -1951,7 +2715,7 @@ export class ɵCpkThreadDetails extends LitElement {
1951
2715
  // `_conversation` — without those keys the cache returns the
1952
2716
  // pre-toggle template and the disclosure appears broken.
1953
2717
  return this.cachedPanelTpl(
1954
- "conversation",
2718
+ "timeline-fallback",
1955
2719
  [this._conversation, this._expandedTools, this._expandedMessages],
1956
2720
  () => {
1957
2721
  const items = this.renderItems;
@@ -1970,10 +2734,21 @@ export class ɵCpkThreadDetails extends LitElement {
1970
2734
  * change without the listed key flipping.
1971
2735
  */
1972
2736
  private cachedPanelTpl(
1973
- slot: ThreadDetailsTab,
2737
+ slot: ThreadDetailsPanelCacheSlot,
1974
2738
  key: readonly unknown[],
1975
2739
  build: () => TemplateResult,
1976
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 {
1977
2752
  const cached = this._panelTplCache.get(slot);
1978
2753
  if (
1979
2754
  cached &&
@@ -1982,9 +2757,7 @@ export class ɵCpkThreadDetails extends LitElement {
1982
2757
  ) {
1983
2758
  return cached.tpl;
1984
2759
  }
1985
- const tpl = build();
1986
- this._panelTplCache.set(slot, { key, tpl });
1987
- return tpl;
2760
+ return null;
1988
2761
  }
1989
2762
 
1990
2763
  private renderRenderItem(item: RenderItem) {
@@ -2015,7 +2788,7 @@ export class ɵCpkThreadDetails extends LitElement {
2015
2788
 
2016
2789
  private renderBubble(item: ConversationUser | ConversationAssistant) {
2017
2790
  const isUser = item.type === "user";
2018
- const threshold = ɵCpkThreadDetails.COLLAPSE_THRESHOLD;
2791
+ const threshold = CpkThreadInspector.COLLAPSE_THRESHOLD;
2019
2792
  const expanded = this._expandedMessages.has(item.id);
2020
2793
  const tooLong = item.content.length > threshold;
2021
2794
  const shown =
@@ -2198,7 +2971,7 @@ ${unsafeHTML(highlightedJson(item.result))}</pre
2198
2971
  `;
2199
2972
  }
2200
2973
  const stateValue = this.activeState;
2201
- return this.cachedPanelTpl("agent-state", [stateValue], () => {
2974
+ return this.cachedPanelTpl("state", [stateValue], () => {
2202
2975
  return html`<pre class="cpk-td__json-block">
2203
2976
  ${unsafeHTML(highlightedJson(stateValue))}</pre
2204
2977
  >`;
@@ -2238,11 +3011,11 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
2238
3011
  </div>
2239
3012
  `;
2240
3013
  }
2241
- return this.cachedPanelTpl("ag-ui-events", [events], () => {
3014
+ return this.cachedPanelTpl("raw-events", [events], () => {
2242
3015
  return html`${events.map((event) => {
2243
3016
  const { bg, fg } = eventColors(event.type);
2244
3017
  return html`
2245
- <div class="cpk-td__event">
3018
+ <div class="cpk-td__event" data-source-index=${event.sourceIndex}>
2246
3019
  <div class="cpk-td__event-header" style="background:${bg}">
2247
3020
  <span class="cpk-td__event-type" style="color:${fg}"
2248
3021
  >${event.type}</span
@@ -2252,7 +3025,7 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
2252
3025
  >
2253
3026
  </div>
2254
3027
  <pre class="cpk-td__event-payload">
2255
- ${unsafeHTML(highlightedJson(event.payload))}</pre
3028
+ ${unsafeHTML(highlightedJson(event.rawEvent ?? event))}</pre
2256
3029
  >
2257
3030
  </div>
2258
3031
  `;
@@ -2291,29 +3064,42 @@ ${unsafeHTML(highlightedJson(event.payload))}</pre
2291
3064
 
2292
3065
  private renderDetailPanel() {
2293
3066
  const counts = this.activityCounts;
3067
+ const metadata = this.metadata;
2294
3068
  return html`
2295
3069
  <!-- Thread -->
2296
3070
  <div class="cpk-tdp__section-title">Thread</div>
2297
3071
  <div class="cpk-tdp__row">
2298
3072
  <span class="cpk-tdp__label">ID</span>
2299
3073
  <span class="cpk-tdp__value cpk-tdp__value--wrap"
2300
- >${this.shortId(this.thread?.id)}</span
3074
+ >${this.shortId(metadata?.id)}</span
2301
3075
  >
2302
3076
  </div>
2303
3077
  <div class="cpk-tdp__row">
2304
3078
  <span class="cpk-tdp__label">Name</span>
2305
- <span class="cpk-tdp__value">${this.thread?.name ?? "—"}</span>
3079
+ <span class="cpk-tdp__value">${metadata?.name ?? "—"}</span>
2306
3080
  </div>
2307
3081
  <div class="cpk-tdp__row">
2308
3082
  <span class="cpk-tdp__label">Agent</span>
2309
3083
  <span class="cpk-tdp__value cpk-tdp__value--truncate"
2310
- >${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
2311
3091
  >
2312
3092
  </div>
2313
3093
  <div class="cpk-tdp__row">
2314
3094
  <span class="cpk-tdp__label">Created by</span>
2315
3095
  <span class="cpk-tdp__value cpk-tdp__value--truncate"
2316
- >${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
2317
3103
  >
2318
3104
  </div>
2319
3105
 
@@ -2323,11 +3109,11 @@ ${unsafeHTML(highlightedJson(event.payload))}</pre
2323
3109
  <div class="cpk-tdp__section-title">Timestamps</div>
2324
3110
  <div class="cpk-tdp__row">
2325
3111
  <span class="cpk-tdp__label">Created</span>
2326
- <span class="cpk-tdp__value">${this.fmtTime(this.thread?.createdAt)}</span>
3112
+ <span class="cpk-tdp__value">${this.fmtTime(metadata?.createdAt)}</span>
2327
3113
  </div>
2328
3114
  <div class="cpk-tdp__row">
2329
3115
  <span class="cpk-tdp__label">Updated</span>
2330
- <span class="cpk-tdp__value">${this.fmtTime(this.thread?.updatedAt)}</span>
3116
+ <span class="cpk-tdp__value">${this.fmtTime(metadata?.updatedAt)}</span>
2331
3117
  </div>
2332
3118
  <div class="cpk-tdp__row">
2333
3119
  <span class="cpk-tdp__label">Duration</span>
@@ -2354,9 +3140,17 @@ ${unsafeHTML(highlightedJson(event.payload))}</pre
2354
3140
  }
2355
3141
  }
2356
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
+
2357
3148
  if (!customElements.get("cpk-thread-list")) {
2358
3149
  customElements.define("cpk-thread-list", CpkThreadList);
2359
3150
  }
3151
+ if (!customElements.get(THREAD_INSPECTOR_TAG)) {
3152
+ customElements.define(THREAD_INSPECTOR_TAG, CpkThreadInspector);
3153
+ }
2360
3154
  if (!customElements.get("cpk-thread-details")) {
2361
3155
  customElements.define("cpk-thread-details", ɵCpkThreadDetails);
2362
3156
  }
@@ -2752,6 +3546,7 @@ export class WebInspectorElement extends LitElement {
2752
3546
  },
2753
3547
  onHeadersChanged: ({ headers }) => {
2754
3548
  this.updateOwnedThreadStoreHeaders(headers);
3549
+ this.requestUpdate();
2755
3550
  },
2756
3551
  onError: ({ code, error }) => {
2757
3552
  this.lastCoreError = { code, message: error.message };
@@ -2760,6 +3555,14 @@ export class WebInspectorElement extends LitElement {
2760
3555
  onAgentsChanged: ({ agents }) => {
2761
3556
  this.processAgentsChanged(agents);
2762
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
+ },
2763
3566
  onContextChanged: ({ context }) => {
2764
3567
  this.contextStore = this.normalizeContextStore(context);
2765
3568
  this.requestUpdate();
@@ -5832,7 +6635,9 @@ ${argsString}</pre
5832
6635
 
5833
6636
  <div class="space-y-2">
5834
6637
  <h3 class="text-sm text-slate-500">Privacy</h3>
5835
- <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
+ >
5836
6641
  <p class="text-sm text-gray-600 flex items-start gap-2">
5837
6642
  <span>${optedOut ? "❌" : "✅"}</span>
5838
6643
  <span>
@@ -5848,7 +6653,8 @@ ${argsString}</pre
5848
6653
  href=${TELEMETRY_DOCS_URL}
5849
6654
  target="_blank"
5850
6655
  rel="noopener"
5851
- >Learn more →</a>
6656
+ >Learn more →</a
6657
+ >
5852
6658
  </div>
5853
6659
  </div>
5854
6660
  </div>
@@ -6527,28 +7333,30 @@ ${argsString}</pre
6527
7333
  <div class="relative h-full w-full overflow-y-auto overflow-x-hidden">
6528
7334
  <table class="w-full table-fixed border-collapse text-xs box-border">
6529
7335
  <colgroup>
6530
- <col style="width:${this.evtColWidths[0]}px">
6531
- <col style="width:${this.evtColWidths[1]}px">
6532
- <col style="width:${this.evtColWidths[2]}px">
6533
- <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 />
6534
7340
  </colgroup>
6535
7341
  <thead class="sticky top-0 z-10">
6536
7342
  <tr class="bg-white">
6537
7343
  ${["Agent", "Time", "Event Type"].map(
6538
- (label, col) => html`
6539
- <th
6540
- class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
6541
- style="position:relative;overflow:hidden;"
6542
- >
6543
- ${label}
6544
- <div
6545
- style="position:absolute;top:0;right:0;width:5px;height:100%;cursor:col-resize;user-select:none;background:transparent;"
6546
- @pointerdown=${(e: PointerEvent) => this._onEvtColResizeStart(e, col)}
6547
- @pointermove=${(e: PointerEvent) => this._onEvtColResizeMove(e)}
6548
- @pointerup=${() => this._onEvtColResizeEnd()}
6549
- @pointercancel=${() => this._onEvtColResizeEnd()}
6550
- ></div>
6551
- </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>`,
6552
7360
  )}
6553
7361
  <th
6554
7362
  class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
@@ -6880,8 +7688,14 @@ ${prettyEvent}</pre
6880
7688
  ? html`
6881
7689
  <div class="w-full text-xs">
6882
7690
  <div class="flex bg-gray-50">
6883
- <div class="w-40 shrink-0 px-4 py-2 font-medium text-gray-700">Role</div>
6884
- <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>
6885
7699
  </div>
6886
7700
  <div class="divide-y divide-gray-200">
6887
7701
  ${messages.map((msg) => {
@@ -6904,7 +7718,9 @@ ${prettyEvent}</pre
6904
7718
  <div class="flex items-start">
6905
7719
  <div class="w-40 shrink-0 px-4 py-2">
6906
7720
  <span
6907
- 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
+ }"
6908
7724
  >
6909
7725
  ${role}
6910
7726
  </span>
@@ -7910,7 +8726,9 @@ ${prettyEvent}</pre
7910
8726
  return nothing;
7911
8727
  }
7912
8728
 
7913
- 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
+ >
7914
8732
  <div
7915
8733
  class="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-900"
7916
8734
  >
@@ -7929,7 +8747,13 @@ ${prettyEvent}</pre
7929
8747
  ${this.renderIcon("X")}
7930
8748
  </button>
7931
8749
  </div>
7932
- <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
+ >
7933
8757
  <div
7934
8758
  class="announcement-content"
7935
8759
  @click=${this.handleAnnouncementContentClick}