@copilotkit/web-inspector 1.62.3 → 1.63.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
@@ -65,6 +65,13 @@ import {
65
65
  trackTalkToEngineerClicked,
66
66
  trackThreadsEmptyEnabledViewed,
67
67
  trackThreadsEnabledViewed,
68
+ trackThreadsExampleSelected,
69
+ trackThreadsExampleTourCompleted,
70
+ trackThreadsExampleTourDismissed,
71
+ trackThreadsExampleTourReopened,
72
+ trackThreadsExampleTourStarted,
73
+ trackThreadsExampleTourStepViewed,
74
+ trackThreadsExampleViewed,
68
75
  trackThreadsIntelligenceSignupClicked,
69
76
  trackThreadsLockedViewed,
70
77
  trackMemoriesTabClicked,
@@ -112,7 +119,17 @@ const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed si
112
119
  const MAX_AGENT_EVENTS = 200;
113
120
  const MAX_TOTAL_EVENTS = 500;
114
121
  const INTELLIGENCE_SIGNUP_URL = "https://go.copilotkit.ai/intelligence-signup";
122
+ const THREADS_INTELLIGENCE_SIGNIN_URL =
123
+ "https://dashboard.operations.copilotkit.ai/sign-in";
115
124
  const TALK_TO_ENGINEER_URL = "https://www.copilotkit.ai/talk-to-an-engineer";
125
+ const THREADS_DOCS_URL = "https://docs.copilotkit.ai/threads";
126
+ const SELF_HOSTED_INTELLIGENCE_URL =
127
+ "https://docs.copilotkit.ai/premium/self-hosting";
128
+ const THREADS_EXAMPLE_OVERVIEW_VIDEO_URL =
129
+ "https://cdn.copilotkit.ai/corp-site/videos/copilotkit-generative-ui-agentic-frontend-demo.webm";
130
+ const THREADS_EXAMPLE_TOUR_STORAGE_KEY =
131
+ "cpk:inspector:threads-example-tour:v1";
132
+ const THREADS_EXAMPLE_AGENT_ID = "threads-feature";
116
133
 
117
134
  type ThreadServiceStatus = "available" | "unavailable" | "unknown" | "error";
118
135
 
@@ -376,6 +393,235 @@ type RuntimeStateFetchResult =
376
393
  | { status: "available"; state: Record<string, unknown> | null }
377
394
  | { status: "not-available" };
378
395
 
396
+ type ExampleThread = ɵThread & { isExample: true };
397
+
398
+ type ExampleThreadDetails = {
399
+ messages: ThreadDebuggerMessage[];
400
+ events: ThreadDebuggerEvent[];
401
+ state: Record<string, unknown>;
402
+ };
403
+
404
+ const THREADS_EXAMPLE_THREADS: ExampleThread[] = [
405
+ {
406
+ id: "example-realtime-sync",
407
+ name: "Realtime thread sync",
408
+ agentId: THREADS_EXAMPLE_AGENT_ID,
409
+ organizationId: "example-organization",
410
+ createdById: "example-user",
411
+ archived: false,
412
+ createdAt: "2026-07-08T16:00:00.000Z",
413
+ updatedAt: "2026-07-08T16:30:00.000Z",
414
+ isExample: true,
415
+ },
416
+ {
417
+ id: "example-manage-history",
418
+ name: "Manage saved conversations",
419
+ agentId: THREADS_EXAMPLE_AGENT_ID,
420
+ organizationId: "example-organization",
421
+ createdById: "example-user",
422
+ archived: false,
423
+ createdAt: "2026-07-07T17:45:00.000Z",
424
+ updatedAt: "2026-07-07T18:15:00.000Z",
425
+ isExample: true,
426
+ },
427
+ {
428
+ id: "example-inspect-runs",
429
+ name: "Inspect durable run history",
430
+ agentId: THREADS_EXAMPLE_AGENT_ID,
431
+ organizationId: "example-organization",
432
+ createdById: "example-user",
433
+ archived: false,
434
+ createdAt: "2026-07-06T20:15:00.000Z",
435
+ updatedAt: "2026-07-06T20:45:00.000Z",
436
+ isExample: true,
437
+ },
438
+ ];
439
+
440
+ const THREADS_EXAMPLE_DETAILS: Record<string, ExampleThreadDetails> = {
441
+ "example-realtime-sync": {
442
+ messages: [
443
+ {
444
+ id: "example-sync-user",
445
+ role: "user",
446
+ content: "Resume the checkout support thread from yesterday.",
447
+ },
448
+ {
449
+ id: "example-sync-assistant",
450
+ role: "assistant",
451
+ content:
452
+ "I found the saved thread, restored the cart state, and continued from the latest user message.",
453
+ },
454
+ ],
455
+ events: [
456
+ {
457
+ type: "RUN_STARTED",
458
+ timestamp: "2026-07-08T16:30:00.000Z",
459
+ payload: {
460
+ threadId: "example-realtime-sync",
461
+ agentId: THREADS_EXAMPLE_AGENT_ID,
462
+ },
463
+ },
464
+ {
465
+ type: "MESSAGES_SNAPSHOT",
466
+ timestamp: "2026-07-08T16:30:01.000Z",
467
+ payload: {
468
+ messageCount: 6,
469
+ source: "thread-history",
470
+ },
471
+ },
472
+ {
473
+ type: "STATE_SNAPSHOT",
474
+ timestamp: "2026-07-08T16:30:02.000Z",
475
+ payload: {
476
+ cartId: "cart_demo_42",
477
+ checkoutStep: "shipping",
478
+ resumed: true,
479
+ },
480
+ },
481
+ {
482
+ type: "RUN_FINISHED",
483
+ timestamp: "2026-07-08T16:30:04.000Z",
484
+ payload: {
485
+ status: "completed",
486
+ },
487
+ },
488
+ ],
489
+ state: {
490
+ cartId: "cart_demo_42",
491
+ checkoutStep: "shipping",
492
+ userIntent: "resume_previous_checkout",
493
+ persistedThread: true,
494
+ },
495
+ },
496
+ "example-manage-history": {
497
+ messages: [
498
+ {
499
+ id: "example-history-user",
500
+ role: "user",
501
+ content: "Rename this saved support conversation for the handoff.",
502
+ },
503
+ {
504
+ id: "example-history-assistant",
505
+ role: "assistant",
506
+ content:
507
+ "Renamed the thread and kept the prior messages available for the next session.",
508
+ },
509
+ ],
510
+ events: [
511
+ {
512
+ type: "RUN_STARTED",
513
+ timestamp: "2026-07-07T18:15:00.000Z",
514
+ payload: {
515
+ threadId: "example-manage-history",
516
+ agentId: THREADS_EXAMPLE_AGENT_ID,
517
+ },
518
+ },
519
+ {
520
+ type: "CUSTOM_EVENT",
521
+ timestamp: "2026-07-07T18:15:01.000Z",
522
+ payload: {
523
+ action: "thread_renamed",
524
+ previousName: "Untitled",
525
+ name: "Billing escalation handoff",
526
+ },
527
+ },
528
+ {
529
+ type: "RUN_FINISHED",
530
+ timestamp: "2026-07-07T18:15:03.000Z",
531
+ payload: {
532
+ status: "completed",
533
+ },
534
+ },
535
+ ],
536
+ state: {
537
+ name: "Billing escalation handoff",
538
+ savedMessages: 14,
539
+ lastHandoff: "support-team",
540
+ },
541
+ },
542
+ "example-inspect-runs": {
543
+ messages: [
544
+ {
545
+ id: "example-inspect-user",
546
+ role: "user",
547
+ content: "Why did the assistant recommend the enterprise plan?",
548
+ },
549
+ {
550
+ id: "example-inspect-assistant",
551
+ role: "assistant",
552
+ content:
553
+ "The recommendation came from the account size, SSO requirement, and audit-log constraint in state.",
554
+ },
555
+ ],
556
+ events: [
557
+ {
558
+ type: "RUN_STARTED",
559
+ timestamp: "2026-07-06T20:45:00.000Z",
560
+ payload: {
561
+ threadId: "example-inspect-runs",
562
+ agentId: THREADS_EXAMPLE_AGENT_ID,
563
+ },
564
+ },
565
+ {
566
+ type: "TOOL_CALL_START",
567
+ timestamp: "2026-07-06T20:45:01.000Z",
568
+ payload: {
569
+ toolCallId: "call_account_lookup",
570
+ toolName: "lookupAccount",
571
+ },
572
+ },
573
+ {
574
+ type: "TOOL_CALL_RESULT",
575
+ timestamp: "2026-07-06T20:45:02.000Z",
576
+ payload: {
577
+ toolCallId: "call_account_lookup",
578
+ seats: 220,
579
+ requiresSso: true,
580
+ },
581
+ },
582
+ {
583
+ type: "RUN_FINISHED",
584
+ timestamp: "2026-07-06T20:45:04.000Z",
585
+ payload: {
586
+ status: "completed",
587
+ },
588
+ },
589
+ ],
590
+ state: {
591
+ accountTier: "growth",
592
+ seats: 220,
593
+ requiresSso: true,
594
+ auditLogsRequired: true,
595
+ },
596
+ },
597
+ };
598
+
599
+ const THREADS_EXAMPLE_TOUR_STEPS: ReadonlyArray<{
600
+ tab: ThreadDetailsTab;
601
+ label: string;
602
+ title: string;
603
+ body: string;
604
+ }> = [
605
+ {
606
+ tab: "timeline",
607
+ label: "Timeline",
608
+ title: "Read the run as a story",
609
+ body: "The timeline turns messages, tool calls, state changes, and run markers into a scannable debugging trail.",
610
+ },
611
+ {
612
+ tab: "raw-events",
613
+ label: "Raw AG-UI Events",
614
+ title: "Drop into the protocol payloads",
615
+ body: "Raw events show the exact AG-UI stream behind the timeline when you need to verify ordering or payload shape.",
616
+ },
617
+ {
618
+ tab: "state",
619
+ label: "State",
620
+ title: "Check the durable state",
621
+ body: "The state tab shows the saved values that make a thread resumable across sessions.",
622
+ },
623
+ ];
624
+
379
625
  // ─── JSON syntax highlighter ─────────────────────────────────────────────────
380
626
  // Inline-styled so shadow DOM encapsulation preserves colors when the output
381
627
  // is injected via unsafeHTML. Only for structured data — never raw user HTML.
@@ -602,6 +848,11 @@ class CpkThreadList extends LitElement {
602
848
  color: #57575b;
603
849
  }
604
850
 
851
+ .cpk-tl__pill--example {
852
+ background: rgba(133, 236, 206, 0.22);
853
+ color: #189370;
854
+ }
855
+
605
856
  /* ── Empty state ── */
606
857
  .cpk-tl__empty {
607
858
  padding: 32px 16px;
@@ -697,6 +948,13 @@ class CpkThreadList extends LitElement {
697
948
  </div>
698
949
  <div class="cpk-tl__meta">
699
950
  <span class="cpk-tl__pill">${thread.agentId}</span>
951
+ ${
952
+ (thread as Partial<ExampleThread>).isExample
953
+ ? html`
954
+ <span class="cpk-tl__pill cpk-tl__pill--example">Example</span>
955
+ `
956
+ : nothing
957
+ }
700
958
  </div>
701
959
  </div>
702
960
  `,
@@ -973,6 +1231,10 @@ export class CpkThreadInspector extends LitElement {
973
1231
  this.maybeFetchTabData(id);
974
1232
  }
975
1233
 
1234
+ selectTab(id: ThreadDetailsTab): void {
1235
+ this.activateTab(id);
1236
+ }
1237
+
976
1238
  private maybeFetchTabData(id: ThreadDetailsTab): void {
977
1239
  // Lazy-trigger the events / state fetches so their (potentially huge)
978
1240
  // JSON.parse only blocks the main thread after the user has shown
@@ -3937,6 +4199,19 @@ export class WebInspectorElement extends LitElement {
3937
4199
  // don't inflate funnel counts beyond one signal per intent type per banner.
3938
4200
  private clickedBannerIds: Set<string> = new Set();
3939
4201
  private viewedThreadsTelemetryStates: Set<string> = new Set();
4202
+ private viewedExampleThreadIds: Set<string> = new Set();
4203
+ private selectedExampleThreadIds: Set<string> = new Set();
4204
+ private viewedExampleTourSteps: Set<string> = new Set();
4205
+ private exampleThreadProviders: Map<string, ThreadDebuggerProvider> =
4206
+ new Map();
4207
+ private exampleTourDismissed = false;
4208
+ private exampleTourActive = false;
4209
+ private exampleTourStep = 0;
4210
+ private exampleTourAutoShown = false;
4211
+ private threadsExampleOverviewVideoShouldLoad = false;
4212
+ private threadsExampleOverviewVideoReady = false;
4213
+ private threadsExampleOverviewVideoLoadTimer: number | null = null;
4214
+ private threadsExampleOverviewVideoIdleCallbackId: number | null = null;
3940
4215
 
3941
4216
  get core(): CopilotKitCore | null {
3942
4217
  return this._core;
@@ -4031,6 +4306,31 @@ export class WebInspectorElement extends LitElement {
4031
4306
  return this.getThreadServiceStatus() !== "unavailable";
4032
4307
  }
4033
4308
 
4309
+ private getActiveThreadsState(): {
4310
+ displayThreads: ɵThread[];
4311
+ threadsErrorMessage: string | null;
4312
+ } {
4313
+ const displayThreads =
4314
+ this.selectedContext === "all-agents"
4315
+ ? this._threads
4316
+ : (this._threadsByAgent.get(this.selectedContext) ?? []);
4317
+
4318
+ // Surface a thread-store load error inline. For "all-agents" we report
4319
+ // the first error encountered across all agents (good enough for a
4320
+ // debugging surface — the per-agent context filter narrows down the
4321
+ // culprit). For a specific agent we use that agent's error directly.
4322
+ let threadsErrorMessage: string | null = null;
4323
+ if (this.selectedContext === "all-agents") {
4324
+ const firstError = this._threadsErrorByAgent.values().next().value;
4325
+ threadsErrorMessage = firstError?.message ?? null;
4326
+ } else {
4327
+ threadsErrorMessage =
4328
+ this._threadsErrorByAgent.get(this.selectedContext)?.message ?? null;
4329
+ }
4330
+
4331
+ return { displayThreads, threadsErrorMessage };
4332
+ }
4333
+
4034
4334
  private getThreadsTelemetryProps(
4035
4335
  extra: Partial<InspectorThreadTelemetryProps> = {},
4036
4336
  options: { includeUrlAttribution?: boolean } = {},
@@ -4072,10 +4372,32 @@ export class WebInspectorElement extends LitElement {
4072
4372
  return this.appendRefParam(INTELLIGENCE_SIGNUP_URL, "cpk-inspector");
4073
4373
  }
4074
4374
 
4375
+ private getThreadsIntelligenceSignupUrl(): string {
4376
+ return this.appendRefParam(
4377
+ THREADS_INTELLIGENCE_SIGNIN_URL,
4378
+ "cpk-inspector",
4379
+ );
4380
+ }
4381
+
4075
4382
  private getTalkToEngineerUrl(): string {
4076
4383
  return this.appendRefParam(TALK_TO_ENGINEER_URL, "cpk-inspector-threads");
4077
4384
  }
4078
4385
 
4386
+ private getThreadsTalkToEngineerUrl(): string {
4387
+ return this.appendRefParam(TALK_TO_ENGINEER_URL, "cpk-inspector-threads");
4388
+ }
4389
+
4390
+ private getThreadsDocsUrl(): string {
4391
+ return this.appendRefParam(THREADS_DOCS_URL, "cpk-inspector-threads");
4392
+ }
4393
+
4394
+ private getSelfHostedIntelligenceUrl(): string {
4395
+ return this.appendRefParam(
4396
+ SELF_HOSTED_INTELLIGENCE_URL,
4397
+ "cpk-inspector-threads",
4398
+ );
4399
+ }
4400
+
4079
4401
  private subscribeToThreadStore(agentId: string, store: ɵThreadStore): void {
4080
4402
  if (!this.areThreadEndpointsAvailable()) return;
4081
4403
  if (this._threadStoreSubscriptions.has(agentId)) return;
@@ -4114,6 +4436,7 @@ export class WebInspectorElement extends LitElement {
4114
4436
  if (this._threads.length === 0) return;
4115
4437
  const stillValid =
4116
4438
  this.selectedThreadId != null &&
4439
+ !this.isExampleThreadId(this.selectedThreadId) &&
4117
4440
  this._threads.some((t) => t.id === this.selectedThreadId);
4118
4441
  if (!stillValid) {
4119
4442
  // Threads are sorted most-recently-updated first
@@ -5703,6 +6026,35 @@ ${argsString}</pre
5703
6026
  .cpk-tab-active {
5704
6027
  cursor: pointer;
5705
6028
  }
6029
+ .cpk-threads-overview-video-frame {
6030
+ position: relative;
6031
+ display: block;
6032
+ width: 100%;
6033
+ max-width: 440px;
6034
+ aspect-ratio: 16 / 9;
6035
+ margin: 0 0 14px;
6036
+ overflow: hidden;
6037
+ border: 1px solid #dbdbe5;
6038
+ border-radius: 8px;
6039
+ background:
6040
+ linear-gradient(
6041
+ 135deg,
6042
+ rgba(190, 194, 255, 0.18),
6043
+ rgba(133, 236, 206, 0.12)
6044
+ ),
6045
+ #ffffff;
6046
+ box-shadow: 0 8px 20px rgba(1, 5, 7, 0.08);
6047
+ }
6048
+ .cpk-threads-overview-video {
6049
+ width: 100%;
6050
+ height: 100%;
6051
+ object-fit: cover;
6052
+ opacity: 0;
6053
+ transition: opacity 220ms ease;
6054
+ }
6055
+ .cpk-threads-overview-video[data-ready="true"] {
6056
+ opacity: 1;
6057
+ }
5706
6058
 
5707
6059
  /* ── Header control buttons (dock, close) — first row only ───── */
5708
6060
  .drag-handle > div:first-child button {
@@ -5885,6 +6237,7 @@ ${argsString}</pre
5885
6237
 
5886
6238
  // Load state early (before first render) so menu selection is correct
5887
6239
  this.hydrateStateFromStorageEarly();
6240
+ this.exampleTourDismissed = this.readThreadsExampleTourDismissed();
5888
6241
  this.tryAutoAttachCore();
5889
6242
  this.ensureAnnouncementLoading();
5890
6243
  }
@@ -5919,6 +6272,17 @@ ${argsString}</pre
5919
6272
  clearTimeout(this.transitionTimeoutId);
5920
6273
  this.transitionTimeoutId = null;
5921
6274
  }
6275
+ if (this.threadsExampleOverviewVideoLoadTimer !== null) {
6276
+ window.clearTimeout(this.threadsExampleOverviewVideoLoadTimer);
6277
+ this.threadsExampleOverviewVideoLoadTimer = null;
6278
+ }
6279
+ if (
6280
+ this.threadsExampleOverviewVideoIdleCallbackId !== null &&
6281
+ typeof window.cancelIdleCallback === "function"
6282
+ ) {
6283
+ window.cancelIdleCallback(this.threadsExampleOverviewVideoIdleCallbackId);
6284
+ this.threadsExampleOverviewVideoIdleCallbackId = null;
6285
+ }
5922
6286
  this.removeDockStyles(true); // Clean up any docking styles, skip transition
5923
6287
  this.detachFromCore();
5924
6288
  }
@@ -6188,6 +6552,21 @@ ${argsString}</pre
6188
6552
  </button>
6189
6553
  `;
6190
6554
  })}
6555
+ ${
6556
+ this.selectedMenu === "threads"
6557
+ ? html`
6558
+ <a
6559
+ href=${this.getThreadsTalkToEngineerUrl()}
6560
+ target="_blank"
6561
+ rel="noopener"
6562
+ style="margin-left:auto;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;"
6563
+ @click=${this.handleTalkToEngineerClick}
6564
+ >
6565
+ Talk to an Engineer
6566
+ </a>
6567
+ `
6568
+ : nothing
6569
+ }
6191
6570
  </div>
6192
6571
  </div>
6193
6572
  <div class="flex flex-1 flex-col overflow-hidden">
@@ -7566,6 +7945,490 @@ ${argsString}</pre
7566
7945
  }
7567
7946
  }
7568
7947
 
7948
+ private shouldRenderExampleThreads(
7949
+ displayThreads: ɵThread[],
7950
+ threadsErrorMessage: string | null,
7951
+ ): boolean {
7952
+ return (
7953
+ this.areThreadEndpointsAvailable() &&
7954
+ !threadsErrorMessage &&
7955
+ displayThreads.length === 0
7956
+ );
7957
+ }
7958
+
7959
+ private getVisibleThreads(
7960
+ displayThreads: ɵThread[],
7961
+ threadsErrorMessage: string | null,
7962
+ ): ɵThread[] {
7963
+ return this.shouldRenderExampleThreads(displayThreads, threadsErrorMessage)
7964
+ ? THREADS_EXAMPLE_THREADS
7965
+ : displayThreads;
7966
+ }
7967
+
7968
+ private isExampleThreadId(threadId: string | null | undefined): boolean {
7969
+ return THREADS_EXAMPLE_THREADS.some((thread) => thread.id === threadId);
7970
+ }
7971
+
7972
+ private getExampleThreadProvider(threadId: string): ThreadDebuggerProvider {
7973
+ const cached = this.exampleThreadProviders.get(threadId);
7974
+ if (cached) return cached;
7975
+ const thread = THREADS_EXAMPLE_THREADS.find((item) => item.id === threadId);
7976
+ const details = THREADS_EXAMPLE_DETAILS[threadId];
7977
+ const provider: ThreadDebuggerProvider = {
7978
+ getThreadMetadata: async () =>
7979
+ thread
7980
+ ? {
7981
+ id: thread.id,
7982
+ name: thread.name,
7983
+ agentId: thread.agentId,
7984
+ endUserId: "example-user",
7985
+ status: "completed",
7986
+ updatedAt: thread.updatedAt,
7987
+ }
7988
+ : null,
7989
+ getMessages: async () => details?.messages ?? [],
7990
+ getEvents: async () => details?.events ?? [],
7991
+ getState: async () => details?.state ?? null,
7992
+ };
7993
+ this.exampleThreadProviders.set(threadId, provider);
7994
+ return provider;
7995
+ }
7996
+
7997
+ private trackThreadsExampleViewedOnce(): void {
7998
+ if (this.core?.telemetryDisabled) return;
7999
+ for (const thread of THREADS_EXAMPLE_THREADS) {
8000
+ if (this.viewedExampleThreadIds.has(thread.id)) continue;
8001
+ this.viewedExampleThreadIds.add(thread.id);
8002
+ trackThreadsExampleViewed(
8003
+ this.getThreadsTelemetryProps({
8004
+ example_thread_id: thread.id,
8005
+ thread_count: 0,
8006
+ }),
8007
+ );
8008
+ }
8009
+ }
8010
+
8011
+ private trackThreadsExampleSelectedOnce(threadId: string): void {
8012
+ if (this.core?.telemetryDisabled) return;
8013
+ if (this.selectedExampleThreadIds.has(threadId)) return;
8014
+ this.selectedExampleThreadIds.add(threadId);
8015
+ trackThreadsExampleSelected(
8016
+ this.getThreadsTelemetryProps({
8017
+ example_thread_id: threadId,
8018
+ thread_count: 0,
8019
+ }),
8020
+ );
8021
+ }
8022
+
8023
+ private getCurrentExampleTourProps(): InspectorThreadTelemetryProps {
8024
+ const step =
8025
+ THREADS_EXAMPLE_TOUR_STEPS[this.exampleTourStep] ??
8026
+ THREADS_EXAMPLE_TOUR_STEPS[0]!;
8027
+ return this.getThreadsTelemetryProps({
8028
+ example_thread_id: this.selectedThreadId ?? undefined,
8029
+ thread_count: 0,
8030
+ tour_step: this.exampleTourStep + 1,
8031
+ tour_tab: step?.tab,
8032
+ });
8033
+ }
8034
+
8035
+ private trackThreadsExampleTourStepViewedOnce(): void {
8036
+ if (this.core?.telemetryDisabled || !this.selectedThreadId) return;
8037
+ const step =
8038
+ THREADS_EXAMPLE_TOUR_STEPS[this.exampleTourStep] ??
8039
+ THREADS_EXAMPLE_TOUR_STEPS[0]!;
8040
+ if (!step) return;
8041
+ const key = `${this.selectedThreadId}:${this.exampleTourStep}`;
8042
+ if (this.viewedExampleTourSteps.has(key)) return;
8043
+ this.viewedExampleTourSteps.add(key);
8044
+ trackThreadsExampleTourStepViewed(this.getCurrentExampleTourProps());
8045
+ }
8046
+
8047
+ private syncExampleTourTab(): void {
8048
+ const step =
8049
+ THREADS_EXAMPLE_TOUR_STEPS[this.exampleTourStep] ??
8050
+ THREADS_EXAMPLE_TOUR_STEPS[0]!;
8051
+ if (!step) return;
8052
+ void this.updateComplete.then(() => {
8053
+ const details = this.shadowRoot?.querySelector("cpk-thread-details") as
8054
+ | (ɵCpkThreadDetails & { selectTab?: (id: ThreadDetailsTab) => void })
8055
+ | null;
8056
+ details?.selectTab?.(step.tab);
8057
+ });
8058
+ }
8059
+
8060
+ private startExampleTour(autoStarted: boolean): void {
8061
+ if (!this.selectedThreadId) return;
8062
+ this.exampleTourActive = true;
8063
+ this.exampleTourStep = 0;
8064
+ if (autoStarted) {
8065
+ this.exampleTourAutoShown = true;
8066
+ if (!this.core?.telemetryDisabled) {
8067
+ trackThreadsExampleTourStarted(this.getCurrentExampleTourProps());
8068
+ }
8069
+ } else if (!this.core?.telemetryDisabled) {
8070
+ trackThreadsExampleTourReopened(this.getCurrentExampleTourProps());
8071
+ }
8072
+ this.trackThreadsExampleTourStepViewedOnce();
8073
+ this.syncExampleTourTab();
8074
+ this.requestUpdate();
8075
+ }
8076
+
8077
+ private setExampleTourStep(nextStep: number): void {
8078
+ this.exampleTourStep = Math.max(
8079
+ 0,
8080
+ Math.min(THREADS_EXAMPLE_TOUR_STEPS.length - 1, nextStep),
8081
+ );
8082
+ this.trackThreadsExampleTourStepViewedOnce();
8083
+ this.syncExampleTourTab();
8084
+ this.requestUpdate();
8085
+ }
8086
+
8087
+ private dismissExampleTour(method: "skip" | "done"): void {
8088
+ if (!this.selectedThreadId) return;
8089
+ const props = {
8090
+ ...this.getCurrentExampleTourProps(),
8091
+ dismiss_method: method,
8092
+ };
8093
+ this.exampleTourActive = false;
8094
+ this.exampleTourDismissed = true;
8095
+ this.writeThreadsExampleTourDismissed();
8096
+ if (!this.core?.telemetryDisabled) {
8097
+ if (method === "done") {
8098
+ trackThreadsExampleTourCompleted(props);
8099
+ } else {
8100
+ trackThreadsExampleTourDismissed(props);
8101
+ }
8102
+ }
8103
+ this.requestUpdate();
8104
+ }
8105
+
8106
+ private handleThreadsThreadSelected(
8107
+ threadId: string,
8108
+ showingExamples: boolean,
8109
+ ): void {
8110
+ if (showingExamples && this.selectedThreadId === threadId) {
8111
+ this.selectedThreadId = null;
8112
+ this.exampleTourActive = false;
8113
+ this.requestUpdate();
8114
+ return;
8115
+ }
8116
+
8117
+ this.selectedThreadId = threadId;
8118
+ if (showingExamples && this.isExampleThreadId(threadId)) {
8119
+ this.trackThreadsExampleSelectedOnce(threadId);
8120
+ if (!this.exampleTourDismissed && !this.exampleTourAutoShown) {
8121
+ this.startExampleTour(true);
8122
+ } else {
8123
+ this.exampleTourActive = false;
8124
+ }
8125
+ } else {
8126
+ this.exampleTourActive = false;
8127
+ }
8128
+ this.requestUpdate();
8129
+ }
8130
+
8131
+ private readThreadsExampleTourDismissed(): boolean {
8132
+ if (typeof window === "undefined") return false;
8133
+ try {
8134
+ const raw = window.localStorage.getItem(THREADS_EXAMPLE_TOUR_STORAGE_KEY);
8135
+ if (!raw) return false;
8136
+ const value = JSON.parse(raw) as { dismissed?: unknown };
8137
+ return value.dismissed === true;
8138
+ } catch {
8139
+ return false;
8140
+ }
8141
+ }
8142
+
8143
+ private writeThreadsExampleTourDismissed(): void {
8144
+ if (typeof window === "undefined") return;
8145
+ try {
8146
+ window.localStorage.setItem(
8147
+ THREADS_EXAMPLE_TOUR_STORAGE_KEY,
8148
+ JSON.stringify({ dismissed: true }),
8149
+ );
8150
+ } catch {
8151
+ // Persistence is best-effort; the inspector should keep working without it.
8152
+ }
8153
+ }
8154
+
8155
+ private scheduleThreadsExampleOverviewVideoLoad(): void {
8156
+ if (
8157
+ this.threadsExampleOverviewVideoShouldLoad ||
8158
+ this.threadsExampleOverviewVideoLoadTimer !== null ||
8159
+ this.threadsExampleOverviewVideoIdleCallbackId !== null ||
8160
+ typeof window === "undefined"
8161
+ ) {
8162
+ return;
8163
+ }
8164
+
8165
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
8166
+ return;
8167
+ }
8168
+
8169
+ const loadVideo = () => {
8170
+ this.threadsExampleOverviewVideoLoadTimer = null;
8171
+ this.threadsExampleOverviewVideoIdleCallbackId = null;
8172
+ this.threadsExampleOverviewVideoShouldLoad = true;
8173
+ this.requestUpdate();
8174
+ };
8175
+
8176
+ if (typeof window.requestIdleCallback === "function") {
8177
+ this.threadsExampleOverviewVideoIdleCallbackId =
8178
+ window.requestIdleCallback(loadVideo, { timeout: 1200 });
8179
+ return;
8180
+ }
8181
+
8182
+ this.threadsExampleOverviewVideoLoadTimer = window.setTimeout(
8183
+ loadVideo,
8184
+ 450,
8185
+ );
8186
+ }
8187
+
8188
+ private handleThreadsExampleOverviewVideoLoaded = (): void => {
8189
+ this.threadsExampleOverviewVideoReady = true;
8190
+ this.requestUpdate();
8191
+ };
8192
+
8193
+ private renderThreadsExampleOverviewVideo() {
8194
+ this.scheduleThreadsExampleOverviewVideoLoad();
8195
+
8196
+ return html`
8197
+ <div class="cpk-threads-overview-video-frame" aria-hidden="true">
8198
+ ${
8199
+ this.threadsExampleOverviewVideoShouldLoad
8200
+ ? html`
8201
+ <video
8202
+ class="cpk-threads-overview-video"
8203
+ data-ready=${this.threadsExampleOverviewVideoReady}
8204
+ src=${THREADS_EXAMPLE_OVERVIEW_VIDEO_URL}
8205
+ autoplay
8206
+ loop
8207
+ muted
8208
+ playsinline
8209
+ preload="metadata"
8210
+ @loadeddata=${this.handleThreadsExampleOverviewVideoLoaded}
8211
+ ></video>
8212
+ `
8213
+ : nothing
8214
+ }
8215
+ </div>
8216
+ `;
8217
+ }
8218
+
8219
+ private renderThreadsExampleOverview() {
8220
+ return html`
8221
+ <div
8222
+ style="
8223
+ flex: 1;
8224
+ min-width: 0;
8225
+ display: flex;
8226
+ align-items: center;
8227
+ justify-content: center;
8228
+ padding: 32px;
8229
+ background: #f7f7f9;
8230
+ "
8231
+ >
8232
+ <div style="max-width: 440px; color: #57575b;">
8233
+ <h2
8234
+ style="
8235
+ margin: 0 0 10px;
8236
+ font-size: 20px;
8237
+ line-height: 1.25;
8238
+ font-weight: 600;
8239
+ color: #010507;
8240
+ "
8241
+ >
8242
+ Threads are persistent, inspectable conversations
8243
+ </h2>
8244
+ ${this.renderThreadsExampleOverviewVideo()}
8245
+ <p
8246
+ style="
8247
+ margin: 0 0 16px;
8248
+ font-size: 13px;
8249
+ line-height: 1.55;
8250
+ color: #57575b;
8251
+ "
8252
+ >
8253
+ Take a tour with the example threads in the sidebar. Then, start
8254
+ chatting in your app to create the first real thread.
8255
+ </p>
8256
+ <div style="display:flex;flex-wrap:wrap;gap:8px;">
8257
+ <a
8258
+ href=${this.getThreadsDocsUrl()}
8259
+ target="_blank"
8260
+ rel="noopener"
8261
+ style="
8262
+ display: inline-flex;
8263
+ align-items: center;
8264
+ justify-content: center;
8265
+ gap: 6px;
8266
+ min-height: 34px;
8267
+ border-radius: 6px;
8268
+ background: #010507;
8269
+ padding: 8px 12px;
8270
+ font-size: 12px;
8271
+ font-weight: 600;
8272
+ color: #ffffff;
8273
+ text-decoration: none;
8274
+ "
8275
+ >
8276
+ Learn how Threads work
8277
+ </a>
8278
+ <a
8279
+ href=${this.getSelfHostedIntelligenceUrl()}
8280
+ target="_blank"
8281
+ rel="noopener"
8282
+ style="
8283
+ display: inline-flex;
8284
+ align-items: center;
8285
+ justify-content: center;
8286
+ gap: 6px;
8287
+ min-height: 34px;
8288
+ border-radius: 6px;
8289
+ border: 1px solid #dbdbe5;
8290
+ background: #ffffff;
8291
+ padding: 8px 12px;
8292
+ font-size: 12px;
8293
+ font-weight: 600;
8294
+ color: #010507;
8295
+ text-decoration: none;
8296
+ "
8297
+ >
8298
+ Explore self-hosted Intelligence
8299
+ </a>
8300
+ </div>
8301
+ </div>
8302
+ </div>
8303
+ `;
8304
+ }
8305
+
8306
+ private renderThreadsExampleTour() {
8307
+ if (
8308
+ !this.selectedThreadId ||
8309
+ !this.isExampleThreadId(this.selectedThreadId)
8310
+ ) {
8311
+ return nothing;
8312
+ }
8313
+
8314
+ if (!this.exampleTourActive) {
8315
+ return html`
8316
+ <button
8317
+ type="button"
8318
+ style="
8319
+ position: absolute;
8320
+ right: 16px;
8321
+ bottom: 16px;
8322
+ z-index: 2;
8323
+ border: 1px solid #dbdbe5;
8324
+ border-radius: 6px;
8325
+ background: #ffffff;
8326
+ padding: 7px 10px;
8327
+ color: #57575b;
8328
+ font-size: 12px;
8329
+ font-weight: 600;
8330
+ cursor: pointer;
8331
+ box-shadow: 0 8px 18px rgba(1, 5, 7, 0.08);
8332
+ "
8333
+ @click=${() => this.startExampleTour(false)}
8334
+ >
8335
+ Show tour
8336
+ </button>
8337
+ `;
8338
+ }
8339
+
8340
+ const step =
8341
+ THREADS_EXAMPLE_TOUR_STEPS[this.exampleTourStep] ??
8342
+ THREADS_EXAMPLE_TOUR_STEPS[0]!;
8343
+ const isFirst = this.exampleTourStep === 0;
8344
+ const isLast =
8345
+ this.exampleTourStep === THREADS_EXAMPLE_TOUR_STEPS.length - 1;
8346
+
8347
+ return html`
8348
+ <div
8349
+ role="dialog"
8350
+ aria-label="Example thread tour"
8351
+ style="
8352
+ position: absolute;
8353
+ right: 16px;
8354
+ bottom: 16px;
8355
+ z-index: 3;
8356
+ width: min(340px, calc(100% - 32px));
8357
+ border: 1px solid #dbdbe5;
8358
+ border-radius: 8px;
8359
+ background: #ffffff;
8360
+ padding: 14px;
8361
+ box-shadow: 0 16px 36px rgba(1, 5, 7, 0.14);
8362
+ color: #57575b;
8363
+ "
8364
+ >
8365
+ <div
8366
+ style="
8367
+ margin-bottom: 8px;
8368
+ font-family: 'Spline Sans Mono', monospace;
8369
+ font-size: 10px;
8370
+ font-weight: 600;
8371
+ color: #189370;
8372
+ text-transform: uppercase;
8373
+ "
8374
+ >
8375
+ ${this.exampleTourStep + 1}/${THREADS_EXAMPLE_TOUR_STEPS.length}
8376
+ ${step.label}
8377
+ </div>
8378
+ <div
8379
+ style="
8380
+ margin-bottom: 6px;
8381
+ font-size: 14px;
8382
+ line-height: 1.35;
8383
+ font-weight: 600;
8384
+ color: #010507;
8385
+ "
8386
+ >
8387
+ ${step.title}
8388
+ </div>
8389
+ <div style="font-size: 12px; line-height: 1.5; color: #57575b;">
8390
+ ${step.body}
8391
+ </div>
8392
+ <div
8393
+ style="
8394
+ display: flex;
8395
+ justify-content: space-between;
8396
+ gap: 8px;
8397
+ margin-top: 14px;
8398
+ "
8399
+ >
8400
+ <button
8401
+ type="button"
8402
+ style="border:0;background:transparent;color:#838389;font-size:12px;font-weight:600;cursor:pointer;padding:7px 0;"
8403
+ @click=${() => this.dismissExampleTour("skip")}
8404
+ >
8405
+ Skip
8406
+ </button>
8407
+ <div style="display:flex;gap:8px;">
8408
+ <button
8409
+ type="button"
8410
+ style="border:1px solid #dbdbe5;border-radius:6px;background:#ffffff;color:#57575b;font-size:12px;font-weight:600;cursor:pointer;padding:7px 10px;"
8411
+ ?disabled=${isFirst}
8412
+ @click=${() => this.setExampleTourStep(this.exampleTourStep - 1)}
8413
+ >
8414
+ Back
8415
+ </button>
8416
+ <button
8417
+ type="button"
8418
+ style="border:1px solid #010507;border-radius:6px;background:#010507;color:#ffffff;font-size:12px;font-weight:600;cursor:pointer;padding:7px 10px;"
8419
+ @click=${() =>
8420
+ isLast
8421
+ ? this.dismissExampleTour("done")
8422
+ : this.setExampleTourStep(this.exampleTourStep + 1)}
8423
+ >
8424
+ ${isLast ? "Done" : "Next"}
8425
+ </button>
8426
+ </div>
8427
+ </div>
8428
+ </div>
8429
+ `;
8430
+ }
8431
+
7569
8432
  private renderThreadsLockedBackgroundMockup() {
7570
8433
  const threadRows = [
7571
8434
  { width: 74, accent: true },
@@ -7813,29 +8676,7 @@ ${argsString}</pre
7813
8676
  "
7814
8677
  >
7815
8678
  <a
7816
- href=${this.getTalkToEngineerUrl()}
7817
- target="_blank"
7818
- rel="noopener"
7819
- style="
7820
- display: inline-flex;
7821
- min-height: 34px;
7822
- align-items: center;
7823
- justify-content: center;
7824
- gap: 6px;
7825
- border-radius: 6px;
7826
- background: #010507;
7827
- padding: 8px 12px;
7828
- font-size: 12px;
7829
- font-weight: 600;
7830
- color: #ffffff;
7831
- text-decoration: none;
7832
- "
7833
- @click=${this.handleThreadsTalkToEngineerClick}
7834
- >
7835
- Talk to an Engineer
7836
- </a>
7837
- <a
7838
- href=${this.getIntelligenceSignupUrl()}
8679
+ href=${this.getThreadsIntelligenceSignupUrl()}
7839
8680
  target="_blank"
7840
8681
  rel="noopener"
7841
8682
  style="
@@ -8208,28 +9049,26 @@ ${argsString}</pre
8208
9049
  return this.renderThreadsLockedView();
8209
9050
  }
8210
9051
 
8211
- const displayThreads =
8212
- this.selectedContext === "all-agents"
8213
- ? this._threads
8214
- : (this._threadsByAgent.get(this.selectedContext) ?? []);
9052
+ const { displayThreads, threadsErrorMessage } =
9053
+ this.getActiveThreadsState();
8215
9054
 
8216
- // Surface a thread-store load error inline. For "all-agents" we report
8217
- // the first error encountered across all agents (good enough for a
8218
- // debugging surface — the per-agent context filter narrows down the
8219
- // culprit). For a specific agent we use that agent's error directly.
8220
- let threadsErrorMessage: string | null = null;
8221
- if (this.selectedContext === "all-agents") {
8222
- const firstError = this._threadsErrorByAgent.values().next().value;
8223
- threadsErrorMessage = firstError?.message ?? null;
8224
- } else {
8225
- threadsErrorMessage =
8226
- this._threadsErrorByAgent.get(this.selectedContext)?.message ?? null;
9055
+ const showingExamples = this.shouldRenderExampleThreads(
9056
+ displayThreads,
9057
+ threadsErrorMessage,
9058
+ );
9059
+ const visibleThreads = this.getVisibleThreads(
9060
+ displayThreads,
9061
+ threadsErrorMessage,
9062
+ );
9063
+ if (showingExamples) {
9064
+ this.trackThreadsExampleViewedOnce();
8227
9065
  }
8228
9066
 
8229
9067
  const selectedThread =
8230
9068
  this.selectedThreadId != null
8231
- ? (displayThreads.find((t) => t.id === this.selectedThreadId) ?? null)
9069
+ ? (visibleThreads.find((t) => t.id === this.selectedThreadId) ?? null)
8232
9070
  : null;
9071
+ const selectedThreadIsExample = this.isExampleThreadId(selectedThread?.id);
8233
9072
 
8234
9073
  if (!threadsErrorMessage) {
8235
9074
  this.trackThreadsViewStateOnce(
@@ -8240,19 +9079,6 @@ ${argsString}</pre
8240
9079
 
8241
9080
  return html`
8242
9081
  <div style="display:flex;height:100%;overflow:hidden;flex-direction:column;">
8243
- <div
8244
- style="display:flex;align-items:center;justify-content:flex-end;border-bottom:1px solid #DBDBE5;background:#ffffff;padding:8px 12px;flex-shrink:0;"
8245
- >
8246
- <a
8247
- href=${this.getTalkToEngineerUrl()}
8248
- target="_blank"
8249
- rel="noopener"
8250
- 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;"
8251
- @click=${this.handleTalkToEngineerClick}
8252
- >
8253
- Talk to an Engineer
8254
- </a>
8255
- </div>
8256
9082
  <div style="display:flex;min-height:0;flex:1;overflow:hidden;">
8257
9083
  <!-- Left sidebar: thread list -->
8258
9084
  <div
@@ -8260,12 +9086,11 @@ ${argsString}</pre
8260
9086
  >
8261
9087
  <cpk-thread-list
8262
9088
  style="height:100%;"
8263
- .threads=${displayThreads}
9089
+ .threads=${visibleThreads}
8264
9090
  .selectedThreadId=${this.selectedThreadId}
8265
9091
  .errorMessage=${threadsErrorMessage}
8266
9092
  @threadSelected=${(e: CustomEvent<string>) => {
8267
- this.selectedThreadId = e.detail;
8268
- this.requestUpdate();
9093
+ this.handleThreadsThreadSelected(e.detail, showingExamples);
8269
9094
  }}
8270
9095
  ></cpk-thread-list>
8271
9096
  </div>
@@ -8280,16 +9105,22 @@ ${argsString}</pre
8280
9105
  ></div>
8281
9106
 
8282
9107
  <!-- Center + right: thread details or empty state -->
8283
- <div style="flex:1;min-width:0;overflow:hidden;display:flex;">
9108
+ <div style="flex:1;min-width:0;overflow:hidden;display:flex;position:relative;">
8284
9109
  ${
8285
9110
  selectedThread
8286
9111
  ? html`<cpk-thread-details
8287
9112
  style="flex:1;min-width:0;"
8288
9113
  .threadId=${selectedThread.id}
8289
9114
  .thread=${selectedThread}
9115
+ .provider=${
9116
+ selectedThreadIsExample
9117
+ ? this.getExampleThreadProvider(selectedThread.id)
9118
+ : null
9119
+ }
8290
9120
  .runtimeUrl=${this._core?.runtimeUrl ?? ""}
8291
9121
  .headers=${this._core?.headers ?? {}}
8292
9122
  .threadInspectionAvailable=${
9123
+ selectedThreadIsExample ||
8293
9124
  this._core?.threadEndpoints?.inspect !== false
8294
9125
  }
8295
9126
  .liveMessageVersion=${
@@ -8301,8 +9132,11 @@ ${argsString}</pre
8301
9132
  .agentEventsInput=${
8302
9133
  this.agentEvents.get(selectedThread.agentId) ?? []
8303
9134
  }
8304
- ></cpk-thread-details>`
8305
- : html`
9135
+ ></cpk-thread-details>
9136
+ ${selectedThreadIsExample ? this.renderThreadsExampleTour() : nothing}`
9137
+ : showingExamples
9138
+ ? this.renderThreadsExampleOverview()
9139
+ : html`
8306
9140
  <div
8307
9141
  style="
8308
9142
  flex: 1;