@copilotkit/web-inspector 1.62.2 → 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
@@ -13,12 +13,19 @@ import {
13
13
  ɵselectThreads,
14
14
  ɵselectThreadsError,
15
15
  ɵcreateThreadStore,
16
+ ɵselectMemories,
17
+ ɵselectMemoriesIsLoading,
18
+ ɵselectMemoriesError,
19
+ ɵselectMemoriesAvailable,
20
+ ɵselectMemoriesRealtimeStatus,
16
21
  } from "@copilotkit/core";
17
22
  import type {
18
23
  CopilotKitCoreSubscriber,
19
24
  CopilotKitCoreErrorCode,
20
25
  ɵThreadStore,
21
26
  ɵThread,
27
+ Memory,
28
+ MemoryRealtimeStatus,
22
29
  } from "@copilotkit/core";
23
30
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
24
31
  import type {
@@ -58,12 +65,23 @@ import {
58
65
  trackTalkToEngineerClicked,
59
66
  trackThreadsEmptyEnabledViewed,
60
67
  trackThreadsEnabledViewed,
68
+ trackThreadsExampleSelected,
69
+ trackThreadsExampleTourCompleted,
70
+ trackThreadsExampleTourDismissed,
71
+ trackThreadsExampleTourReopened,
72
+ trackThreadsExampleTourStarted,
73
+ trackThreadsExampleTourStepViewed,
74
+ trackThreadsExampleViewed,
61
75
  trackThreadsIntelligenceSignupClicked,
62
76
  trackThreadsLockedViewed,
77
+ trackMemoriesTabClicked,
63
78
  trackThreadsTabClicked,
64
79
  trackThreadsTalkToEngineerClicked,
65
80
  } from "./lib/telemetry.js";
66
- import type { InspectorThreadTelemetryProps } from "./lib/telemetry.js";
81
+ import type {
82
+ InspectorMemoryTelemetryProps,
83
+ InspectorThreadTelemetryProps,
84
+ } from "./lib/telemetry.js";
67
85
 
68
86
  export type { Anchor } from "./lib/types.js";
69
87
 
@@ -78,6 +96,7 @@ type MenuKey =
78
96
  | "frontend-tools"
79
97
  | "agent-context"
80
98
  | "threads"
99
+ | "memories"
81
100
  | "settings";
82
101
 
83
102
  type MenuItem = {
@@ -100,7 +119,17 @@ const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed si
100
119
  const MAX_AGENT_EVENTS = 200;
101
120
  const MAX_TOTAL_EVENTS = 500;
102
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";
103
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";
104
133
 
105
134
  type ThreadServiceStatus = "available" | "unavailable" | "unknown" | "error";
106
135
 
@@ -364,6 +393,235 @@ type RuntimeStateFetchResult =
364
393
  | { status: "available"; state: Record<string, unknown> | null }
365
394
  | { status: "not-available" };
366
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
+
367
625
  // ─── JSON syntax highlighter ─────────────────────────────────────────────────
368
626
  // Inline-styled so shadow DOM encapsulation preserves colors when the output
369
627
  // is injected via unsafeHTML. Only for structured data — never raw user HTML.
@@ -590,6 +848,11 @@ class CpkThreadList extends LitElement {
590
848
  color: #57575b;
591
849
  }
592
850
 
851
+ .cpk-tl__pill--example {
852
+ background: rgba(133, 236, 206, 0.22);
853
+ color: #189370;
854
+ }
855
+
593
856
  /* ── Empty state ── */
594
857
  .cpk-tl__empty {
595
858
  padding: 32px 16px;
@@ -685,6 +948,13 @@ class CpkThreadList extends LitElement {
685
948
  </div>
686
949
  <div class="cpk-tl__meta">
687
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
+ }
688
958
  </div>
689
959
  </div>
690
960
  `,
@@ -961,6 +1231,10 @@ export class CpkThreadInspector extends LitElement {
961
1231
  this.maybeFetchTabData(id);
962
1232
  }
963
1233
 
1234
+ selectTab(id: ThreadDetailsTab): void {
1235
+ this.activateTab(id);
1236
+ }
1237
+
964
1238
  private maybeFetchTabData(id: ThreadDetailsTab): void {
965
1239
  // Lazy-trigger the events / state fetches so their (potentially huge)
966
1240
  // JSON.parse only blocks the main thread after the user has shown
@@ -3418,166 +3692,563 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
3418
3692
  }
3419
3693
  }
3420
3694
 
3421
- // Backwards-compatible internal element name used by the full CopilotKit
3422
- // Inspector shell. Keep this class thin so the public body remains the single
3423
- // implementation.
3424
- export class ɵCpkThreadDetails extends CpkThreadInspector {}
3695
+ // ─── cpk-memory-list ─────────────────────────────────────────────────────────
3425
3696
 
3426
- if (!customElements.get("cpk-thread-list")) {
3427
- customElements.define("cpk-thread-list", CpkThreadList);
3428
- }
3429
- if (!customElements.get(THREAD_INSPECTOR_TAG)) {
3430
- customElements.define(THREAD_INSPECTOR_TAG, CpkThreadInspector);
3431
- }
3432
- if (!customElements.get("cpk-thread-details")) {
3433
- customElements.define("cpk-thread-details", ɵCpkThreadDetails);
3434
- }
3697
+ /** Memory kind values including the "all" sentinel used by the filter UI. */
3698
+ type MemoryKindFilter = "all" | "topical" | "episodic" | "operational";
3435
3699
 
3436
- export class WebInspectorElement extends LitElement {
3700
+ class CpkMemoryList extends LitElement {
3437
3701
  static properties = {
3438
- core: { attribute: false },
3439
- autoAttachCore: { type: Boolean, attribute: "auto-attach-core" },
3440
- } as const;
3441
-
3442
- private _core: CopilotKitCore | null = null;
3443
- private coreSubscriber: CopilotKitCoreSubscriber | null = null;
3444
- private coreUnsubscribe: (() => void) | null = null;
3445
- private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null;
3446
- private coreProperties: Readonly<Record<string, unknown>> = {};
3447
- private lastCoreError: {
3448
- code: CopilotKitCoreErrorCode;
3449
- message: string;
3450
- } | null = null;
3451
- private agentSubscriptions: Map<string, () => void> = new Map();
3452
- private agentEvents: Map<string, InspectorEvent[]> = new Map();
3453
- private agentMessages: Map<string, InspectorMessage[]> = new Map();
3454
- // Per-thread monotonic version that ticks every time an agent currently
3455
- // running on that thread emits a message change. `cpk-thread-details`
3456
- // watches this prop and re-fetches `/threads/:id/messages` when it changes,
3457
- // which is how live updates flow into the conversation view without
3458
- // duplicating the runtime's message-shape conversion in the inspector.
3459
- private liveMessageVersion: Map<string, number> = new Map();
3460
- private agentStates: Map<string, SanitizedValue> = new Map();
3461
- private flattenedEvents: InspectorEvent[] = [];
3462
- private eventCounter = 0;
3463
- private contextStore: Record<
3464
- string,
3465
- { description?: string; value: unknown }
3466
- > = {};
3702
+ memories: { attribute: false },
3703
+ search: { state: true },
3704
+ kind: { state: true },
3705
+ };
3467
3706
 
3468
- private pointerId: number | null = null;
3469
- private dragStart: Position | null = null;
3470
- private dragOffset: Position = { x: 0, y: 0 };
3471
- private isDragging = false;
3472
- private pointerContext: ContextKey | null = null;
3473
- private isOpen = false;
3474
- private draggedDuringInteraction = false;
3475
- private ignoreNextButtonClick = false;
3476
- private selectedMenu: MenuKey = "ag-ui-events";
3477
- private selectedThreadId: string | null = null;
3478
- private threadListWidth = 290;
3479
- private threadDividerResizing = false;
3480
- private threadDividerPointerId = -1;
3481
- private threadDividerStartX = 0;
3482
- private threadDividerStartWidth = 0;
3483
- private _threads: ɵThread[] = [];
3484
- private _threadStoreSubscriptions: Map<string, () => void> = new Map();
3485
- private _threadsByAgent: Map<string, ɵThread[]> = new Map();
3486
- // Error from each agent's thread store (REST list rejection, Phoenix
3487
- // subscribe failure, retry exhaustion). When non-empty for the active
3488
- // selection, the threads view renders an error state instead of stale
3489
- // data with no indication.
3490
- private _threadsErrorByAgent: Map<string, Error> = new Map();
3491
- // Thread stores created and owned by the inspector (keyed by agentId)
3492
- private _ownedThreadStores: Map<string, ɵThreadStore> = new Map();
3493
- private contextMenuOpen = false;
3494
- private dockMode: DockMode = "floating";
3495
- private previousBodyMargins: { left: string; bottom: string } | null = null;
3496
- private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;
3497
- private bodyTransitionTimeoutIds: Set<ReturnType<typeof setTimeout>> =
3498
- new Set();
3499
- private pendingSelectedContext: string | null = null;
3500
- private autoAttachCore = true;
3501
- private attemptedAutoAttach = false;
3502
- private cachedTools: InspectorToolDefinition[] = [];
3503
- private toolSignature = "";
3504
- private eventFilterText = "";
3505
- private eventTypeFilter: InspectorAgentEventType | "all" = "all";
3506
- // Column widths for the AG-UI events table (agent, time, event-type; last col is auto)
3507
- private evtColWidths = [100, 80, 150];
3508
- private _evtColResize: {
3509
- col: number;
3510
- startX: number;
3511
- startW: number;
3512
- } | null = null;
3707
+ /** Ordered (newest-first) list of memories supplied by the parent. */
3708
+ memories: Memory[] = [];
3709
+ private search = "";
3710
+ private kind: MemoryKindFilter = "all";
3513
3711
 
3514
- private announcementHtml: string | null = null;
3515
- private announcementTimestamp: string | null = null;
3516
- private announcementPreviewText: string | null = null;
3517
- // Forward-compat for an optional `cta_label` field on the announcement
3518
- // CDN payload (e.g. "Try threads", "New feature"). The current schema
3519
- // ({timestamp, previewText, announcement}) doesn't carry it, so this is
3520
- // null in production today; we read it defensively in fetchAnnouncement
3521
- // so a future CDN-side schema bump lights up `cta_label` on banner_clicked
3522
- // without an inspector release.
3523
- private announcementCtaLabel: string | null = null;
3524
- private hasUnseenAnnouncement = false;
3525
- private announcementLoaded = false;
3526
- private announcementPromise: Promise<void> | null = null;
3527
- private showAnnouncementPreview = true;
3528
- private announcementExpanded = false;
3529
- // Per-instance dedup for `oss.inspector.banner_viewed` so the event fires
3530
- // at most once per announcement timestamp per inspector mount. Plan calls
3531
- // for "de-dup per timestamp per session"; instance-scoping is closer
3532
- // to per-mount than per-tab (sessionStorage), but for the inspector the
3533
- // distinction is academic — inspector instances rarely outlive the page.
3534
- private viewedBannerTimestamps: Set<string> = new Set();
3535
- private pendingBannerViewed: {
3536
- banner_id: string;
3537
- cta_label?: string;
3538
- } | null = null;
3539
- // Per-instance dedup for `oss.inspector.banner_clicked` (keyed by
3540
- // `${bannerId}:${cta}`) so copy-button retries and accidental multi-clicks
3541
- // don't inflate funnel counts beyond one signal per intent type per banner.
3542
- private clickedBannerIds: Set<string> = new Set();
3543
- private viewedThreadsTelemetryStates: Set<string> = new Set();
3712
+ static styles = css`
3713
+ @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&family=Spline+Sans+Mono:wght@400;500&display=swap");
3544
3714
 
3545
- get core(): CopilotKitCore | null {
3546
- return this._core;
3547
- }
3715
+ :host {
3716
+ display: flex;
3717
+ flex-direction: column;
3718
+ height: 100%;
3719
+ overflow: hidden;
3720
+ }
3548
3721
 
3549
- set core(value: CopilotKitCore | null) {
3550
- const oldValue = this._core;
3551
- if (oldValue === value) {
3552
- return;
3722
+ .cpk-ml {
3723
+ font-family: "Plus Jakarta Sans", sans-serif;
3724
+ display: flex;
3725
+ flex-direction: column;
3726
+ height: 100%;
3727
+ overflow: hidden;
3728
+ background: #f7f7f9;
3553
3729
  }
3554
3730
 
3555
- this.detachFromCore();
3731
+ /* ── Search ── */
3732
+ .cpk-ml__search {
3733
+ padding: 10px 12px;
3734
+ border-bottom: 1px solid #dbdbe5;
3735
+ flex-shrink: 0;
3736
+ }
3556
3737
 
3557
- this._core = value ?? null;
3558
- this.requestUpdate("core", oldValue);
3738
+ .cpk-ml__search-input {
3739
+ width: 100%;
3740
+ box-sizing: border-box;
3741
+ font-family: "Plus Jakarta Sans", sans-serif;
3742
+ font-size: 12px;
3743
+ padding: 7px 10px;
3744
+ border-radius: 6px;
3745
+ border: 1px solid #dbdbe5;
3746
+ background: #ffffff;
3747
+ color: #010507;
3748
+ outline: none;
3749
+ transition: border-color 0.15s;
3750
+ }
3559
3751
 
3560
- if (this._core) {
3561
- this.attachToCore(this._core);
3752
+ .cpk-ml__search-input:focus {
3753
+ border-color: #bec2ff;
3562
3754
  }
3563
- }
3564
3755
 
3565
- private readonly contextState: Record<ContextKey, ContextState> = {
3566
- button: {
3567
- position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
3568
- size: { ...DEFAULT_BUTTON_SIZE },
3569
- anchor: { horizontal: "right", vertical: "top" },
3570
- anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
3571
- },
3572
- window: {
3573
- position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
3574
- size: { ...DEFAULT_WINDOW_SIZE },
3575
- anchor: { horizontal: "right", vertical: "top" },
3576
- anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
3577
- },
3578
- };
3756
+ /* ── Kind filter ── */
3757
+ .cpk-ml__filter {
3758
+ display: flex;
3759
+ gap: 4px;
3760
+ padding: 8px 12px;
3761
+ border-bottom: 1px solid #dbdbe5;
3762
+ flex-shrink: 0;
3763
+ flex-wrap: wrap;
3764
+ }
3579
3765
 
3580
- private hasCustomPosition: Record<ContextKey, boolean> = {
3766
+ .cpk-ml__filter-seg {
3767
+ font-family: "Plus Jakarta Sans", sans-serif;
3768
+ font-size: 11px;
3769
+ font-weight: 500;
3770
+ padding: 3px 9px;
3771
+ border-radius: 5px;
3772
+ border: 1px solid #dbdbe5;
3773
+ background: #ffffff;
3774
+ color: #57575b;
3775
+ cursor: pointer;
3776
+ transition:
3777
+ background 0.1s,
3778
+ border-color 0.1s,
3779
+ color 0.1s;
3780
+ user-select: none;
3781
+ }
3782
+
3783
+ .cpk-ml__filter-seg:hover {
3784
+ background: #f0f0f5;
3785
+ }
3786
+
3787
+ .cpk-ml__filter-seg--active {
3788
+ background: #bec2ff1a;
3789
+ border-color: #bec2ff;
3790
+ color: #010507;
3791
+ }
3792
+
3793
+ .cpk-ml__filter-count {
3794
+ font-family: "Spline Sans Mono", monospace;
3795
+ font-size: 9px;
3796
+ margin-left: 4px;
3797
+ color: #838389;
3798
+ }
3799
+
3800
+ /* ── List ── */
3801
+ .cpk-ml__list {
3802
+ flex: 1;
3803
+ overflow-y: auto;
3804
+ padding: 8px 12px;
3805
+ display: flex;
3806
+ flex-direction: column;
3807
+ gap: 8px;
3808
+ }
3809
+
3810
+ /* ── Card ── */
3811
+ .cpk-ml__card {
3812
+ background: #ffffff;
3813
+ border: 1px solid #e9e9ef;
3814
+ border-radius: 8px;
3815
+ padding: 10px 12px;
3816
+ display: flex;
3817
+ flex-direction: column;
3818
+ gap: 6px;
3819
+ }
3820
+
3821
+ .cpk-ml__card-badges {
3822
+ display: flex;
3823
+ gap: 6px;
3824
+ align-items: center;
3825
+ flex-wrap: wrap;
3826
+ }
3827
+
3828
+ /* Kind badge — color per kind */
3829
+ .cpk-ml__kind-badge {
3830
+ font-family: "Spline Sans Mono", monospace;
3831
+ font-size: 9px;
3832
+ padding: 1px 7px;
3833
+ border-radius: 4px;
3834
+ text-transform: uppercase;
3835
+ font-weight: 500;
3836
+ white-space: nowrap;
3837
+ }
3838
+
3839
+ .cpk-ml__kind-badge--topical {
3840
+ background: #eee6fe;
3841
+ color: #57575b;
3842
+ }
3843
+
3844
+ .cpk-ml__kind-badge--episodic {
3845
+ background: #e6f4fe;
3846
+ color: #2d5f80;
3847
+ }
3848
+
3849
+ .cpk-ml__kind-badge--operational {
3850
+ background: #e6feee;
3851
+ color: #2d6645;
3852
+ }
3853
+
3854
+ /* Scope badge */
3855
+ .cpk-ml__scope-badge {
3856
+ font-family: "Spline Sans Mono", monospace;
3857
+ font-size: 9px;
3858
+ padding: 1px 7px;
3859
+ border-radius: 4px;
3860
+ text-transform: uppercase;
3861
+ font-weight: 500;
3862
+ white-space: nowrap;
3863
+ background: #f0f0f5;
3864
+ color: #838389;
3865
+ }
3866
+
3867
+ /* Content */
3868
+ .cpk-ml__content {
3869
+ font-size: 12px;
3870
+ color: #010507;
3871
+ line-height: 1.5;
3872
+ word-break: break-word;
3873
+ }
3874
+
3875
+ /* Footer */
3876
+ .cpk-ml__footer {
3877
+ display: flex;
3878
+ align-items: center;
3879
+ justify-content: space-between;
3880
+ gap: 8px;
3881
+ margin-top: 2px;
3882
+ }
3883
+
3884
+ .cpk-ml__footer-threads {
3885
+ font-size: 10px;
3886
+ color: #838389;
3887
+ }
3888
+
3889
+ .cpk-ml__footer-id {
3890
+ font-family: "Spline Sans Mono", monospace;
3891
+ font-size: 9px;
3892
+ color: #c0c0c8;
3893
+ }
3894
+
3895
+ /* ── Empty state ── */
3896
+ .cpk-ml__empty {
3897
+ padding: 32px 16px;
3898
+ text-align: center;
3899
+ color: #838389;
3900
+ font-size: 12px;
3901
+ display: flex;
3902
+ flex-direction: column;
3903
+ align-items: center;
3904
+ gap: 8px;
3905
+ }
3906
+
3907
+ .cpk-ml__empty-icon {
3908
+ color: #c0c0c8;
3909
+ }
3910
+ `;
3911
+
3912
+ /** Memories that pass the current text search (before kind filter). */
3913
+ private get searchFiltered(): Memory[] {
3914
+ const q = this.search.trim().toLowerCase();
3915
+ if (!q) return this.memories;
3916
+ return this.memories.filter((m) => m.content.toLowerCase().includes(q));
3917
+ }
3918
+
3919
+ /** Memories that pass both search and kind filter. */
3920
+ private get filtered(): Memory[] {
3921
+ const searched = this.searchFiltered;
3922
+ if (this.kind === "all") return searched;
3923
+ return searched.filter((m) => m.kind === this.kind);
3924
+ }
3925
+
3926
+ /** Count of search-filtered memories for a given kind (for segment labels). */
3927
+ private countForKind(kind: Exclude<MemoryKindFilter, "all">): number {
3928
+ return this.searchFiltered.filter((m) => m.kind === kind).length;
3929
+ }
3930
+
3931
+ private onSearchInput = (event: Event): void => {
3932
+ this.search = (event.target as HTMLInputElement).value;
3933
+ };
3934
+
3935
+ private onKindClick = (event: Event): void => {
3936
+ const seg = (event.target as HTMLElement).closest("[data-kind]");
3937
+ if (!seg) return;
3938
+ const k = (seg as HTMLElement).dataset["kind"] as MemoryKindFilter;
3939
+ this.kind = k;
3940
+ };
3941
+
3942
+ /** Truncate an id to first-4…last-4 characters. */
3943
+ private shortId(id: string): string {
3944
+ if (id.length <= 12) return id;
3945
+ return `${id.slice(0, 4)}…${id.slice(-4)}`;
3946
+ }
3947
+
3948
+ private renderKindBadge(kind: string): TemplateResult {
3949
+ return html`<span class="cpk-ml__kind-badge cpk-ml__kind-badge--${kind}"
3950
+ >${kind}</span
3951
+ >`;
3952
+ }
3953
+
3954
+ private renderEmpty(): TemplateResult {
3955
+ const q = this.search.trim();
3956
+ if (this.memories.length === 0) {
3957
+ return html`
3958
+ <div class="cpk-ml__empty">
3959
+ <svg
3960
+ width="24"
3961
+ height="24"
3962
+ viewBox="0 0 24 24"
3963
+ fill="none"
3964
+ stroke="currentColor"
3965
+ stroke-width="1.5"
3966
+ stroke-linecap="round"
3967
+ stroke-linejoin="round"
3968
+ class="cpk-ml__empty-icon"
3969
+ >
3970
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
3971
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
3972
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
3973
+ </svg>
3974
+ No memories yet — tell the agent a durable fact and watch it appear.
3975
+ </div>
3976
+ `;
3977
+ }
3978
+ if (q) {
3979
+ return html`
3980
+ <div class="cpk-ml__empty">
3981
+ No memories match &ldquo;${q}&rdquo;.
3982
+ </div>
3983
+ `;
3984
+ }
3985
+ return html`
3986
+ <div class="cpk-ml__empty">No ${this.kind} memories yet.</div>
3987
+ `;
3988
+ }
3989
+
3990
+ render() {
3991
+ const filtered = this.filtered;
3992
+ const kinds: Array<Exclude<MemoryKindFilter, "all">> = [
3993
+ "topical",
3994
+ "episodic",
3995
+ "operational",
3996
+ ];
3997
+
3998
+ return html`
3999
+ <div class="cpk-ml">
4000
+ <!-- Search -->
4001
+ <div class="cpk-ml__search">
4002
+ <input
4003
+ type="text"
4004
+ placeholder="Search memories…"
4005
+ .value=${this.search}
4006
+ @input=${this.onSearchInput}
4007
+ class="cpk-ml__search-input"
4008
+ />
4009
+ </div>
4010
+
4011
+ <!-- Kind filter -->
4012
+ <div class="cpk-ml__filter" @click=${this.onKindClick}>
4013
+ <button
4014
+ class="cpk-ml__filter-seg ${this.kind === "all" ? "cpk-ml__filter-seg--active" : ""}"
4015
+ data-kind="all"
4016
+ >
4017
+ All<span class="cpk-ml__filter-count">${this.searchFiltered.length}</span>
4018
+ </button>
4019
+ ${kinds.map(
4020
+ (k) => html`
4021
+ <button
4022
+ class="cpk-ml__filter-seg ${this.kind === k ? "cpk-ml__filter-seg--active" : ""}"
4023
+ data-kind="${k}"
4024
+ >
4025
+ ${k}<span class="cpk-ml__filter-count">${this.countForKind(k)}</span>
4026
+ </button>
4027
+ `,
4028
+ )}
4029
+ </div>
4030
+
4031
+ <!-- Memory list -->
4032
+ <div class="cpk-ml__list">
4033
+ ${filtered.map(
4034
+ (m) => html`
4035
+ <div class="cpk-ml__card">
4036
+ <div class="cpk-ml__card-badges">
4037
+ ${this.renderKindBadge(m.kind)}
4038
+ <span class="cpk-ml__scope-badge">${m.scope}</span>
4039
+ </div>
4040
+ <div class="cpk-ml__content">${m.content}</div>
4041
+ <div class="cpk-ml__footer">
4042
+ <span class="cpk-ml__footer-threads"
4043
+ >${m.sourceThreadIds.length} source thread${m.sourceThreadIds.length === 1 ? "" : "s"}</span
4044
+ >
4045
+ <span class="cpk-ml__footer-id">${this.shortId(m.id)}</span>
4046
+ </div>
4047
+ </div>
4048
+ `,
4049
+ )}
4050
+ ${filtered.length === 0 ? this.renderEmpty() : nothing}
4051
+ </div>
4052
+ </div>
4053
+ `;
4054
+ }
4055
+ }
4056
+
4057
+ // Backwards-compatible internal element name used by the full CopilotKit
4058
+ // Inspector shell. Keep this class thin so the public body remains the single
4059
+ // implementation.
4060
+ export class ɵCpkThreadDetails extends CpkThreadInspector {}
4061
+
4062
+ if (!customElements.get("cpk-thread-list")) {
4063
+ customElements.define("cpk-thread-list", CpkThreadList);
4064
+ }
4065
+ if (!customElements.get(THREAD_INSPECTOR_TAG)) {
4066
+ customElements.define(THREAD_INSPECTOR_TAG, CpkThreadInspector);
4067
+ }
4068
+ if (!customElements.get("cpk-thread-details")) {
4069
+ customElements.define("cpk-thread-details", ɵCpkThreadDetails);
4070
+ }
4071
+ if (!customElements.get("cpk-memory-list")) {
4072
+ customElements.define("cpk-memory-list", CpkMemoryList);
4073
+ }
4074
+
4075
+ export class WebInspectorElement extends LitElement {
4076
+ static properties = {
4077
+ core: { attribute: false },
4078
+ autoAttachCore: { type: Boolean, attribute: "auto-attach-core" },
4079
+ } as const;
4080
+
4081
+ private _core: CopilotKitCore | null = null;
4082
+ private coreSubscriber: CopilotKitCoreSubscriber | null = null;
4083
+ private coreUnsubscribe: (() => void) | null = null;
4084
+ private _memories: Memory[] = [];
4085
+ private _memoriesLoading = false;
4086
+ private _memoriesError: Error | null = null;
4087
+ private _memoriesAvailable = true;
4088
+ // Realtime-connection health, independent of `_memoriesAvailable` (the REST
4089
+ // list route). Drives the "live" indicator: only "connected" shows "live".
4090
+ private _memoriesRealtimeStatus: MemoryRealtimeStatus = "connecting";
4091
+ private _memoryUnsub: (() => void) | null = null;
4092
+ // Lazy-subscription guard. The memory store is created + started + opens
4093
+ // realtime the first time `core.getMemoryStore()` is called, so we defer
4094
+ // that call until the user actually activates the Memories tab (see
4095
+ // `ensureMemorySubscription`). This flag prevents a repeated tab click from
4096
+ // double-subscribing; `detachFromCore` resets it so a later attach + tab
4097
+ // activation re-subscribes cleanly.
4098
+ private _memorySubscribed = false;
4099
+ // True when the attached core predates `getMemoryStore` (older @copilotkit
4100
+ // SDK). Distinct from `_memoriesAvailable` (memory not enabled on an
4101
+ // otherwise-current deployment) so the teaser can show upgrade-the-SDK copy.
4102
+ private _memoryStoreUnsupported = false;
4103
+ private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null;
4104
+ private coreProperties: Readonly<Record<string, unknown>> = {};
4105
+ private lastCoreError: {
4106
+ code: CopilotKitCoreErrorCode;
4107
+ message: string;
4108
+ } | null = null;
4109
+ private agentSubscriptions: Map<string, () => void> = new Map();
4110
+ private agentEvents: Map<string, InspectorEvent[]> = new Map();
4111
+ private agentMessages: Map<string, InspectorMessage[]> = new Map();
4112
+ // Per-thread monotonic version that ticks every time an agent currently
4113
+ // running on that thread emits a message change. `cpk-thread-details`
4114
+ // watches this prop and re-fetches `/threads/:id/messages` when it changes,
4115
+ // which is how live updates flow into the conversation view without
4116
+ // duplicating the runtime's message-shape conversion in the inspector.
4117
+ private liveMessageVersion: Map<string, number> = new Map();
4118
+ private agentStates: Map<string, SanitizedValue> = new Map();
4119
+ private flattenedEvents: InspectorEvent[] = [];
4120
+ private eventCounter = 0;
4121
+ private contextStore: Record<
4122
+ string,
4123
+ { description?: string; value: unknown }
4124
+ > = {};
4125
+
4126
+ private pointerId: number | null = null;
4127
+ private dragStart: Position | null = null;
4128
+ private dragOffset: Position = { x: 0, y: 0 };
4129
+ private isDragging = false;
4130
+ private pointerContext: ContextKey | null = null;
4131
+ private isOpen = false;
4132
+ private draggedDuringInteraction = false;
4133
+ private ignoreNextButtonClick = false;
4134
+ private selectedMenu: MenuKey = "ag-ui-events";
4135
+ private selectedThreadId: string | null = null;
4136
+ private threadListWidth = 290;
4137
+ private threadDividerResizing = false;
4138
+ private threadDividerPointerId = -1;
4139
+ private threadDividerStartX = 0;
4140
+ private threadDividerStartWidth = 0;
4141
+ private _threads: ɵThread[] = [];
4142
+ private _threadStoreSubscriptions: Map<string, () => void> = new Map();
4143
+ private _threadsByAgent: Map<string, ɵThread[]> = new Map();
4144
+ // Error from each agent's thread store (REST list rejection, Phoenix
4145
+ // subscribe failure, retry exhaustion). When non-empty for the active
4146
+ // selection, the threads view renders an error state instead of stale
4147
+ // data with no indication.
4148
+ private _threadsErrorByAgent: Map<string, Error> = new Map();
4149
+ // Thread stores created and owned by the inspector (keyed by agentId)
4150
+ private _ownedThreadStores: Map<string, ɵThreadStore> = new Map();
4151
+ private contextMenuOpen = false;
4152
+ private dockMode: DockMode = "floating";
4153
+ private previousBodyMargins: { left: string; bottom: string } | null = null;
4154
+ private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;
4155
+ private bodyTransitionTimeoutIds: Set<ReturnType<typeof setTimeout>> =
4156
+ new Set();
4157
+ private pendingSelectedContext: string | null = null;
4158
+ private autoAttachCore = true;
4159
+ private attemptedAutoAttach = false;
4160
+ private cachedTools: InspectorToolDefinition[] = [];
4161
+ private toolSignature = "";
4162
+ private eventFilterText = "";
4163
+ private eventTypeFilter: InspectorAgentEventType | "all" = "all";
4164
+ // Column widths for the AG-UI events table (agent, time, event-type; last col is auto)
4165
+ private evtColWidths = [100, 80, 150];
4166
+ private _evtColResize: {
4167
+ col: number;
4168
+ startX: number;
4169
+ startW: number;
4170
+ } | null = null;
4171
+
4172
+ private announcementHtml: string | null = null;
4173
+ private announcementTimestamp: string | null = null;
4174
+ private announcementPreviewText: string | null = null;
4175
+ // Forward-compat for an optional `cta_label` field on the announcement
4176
+ // CDN payload (e.g. "Try threads", "New feature"). The current schema
4177
+ // ({timestamp, previewText, announcement}) doesn't carry it, so this is
4178
+ // null in production today; we read it defensively in fetchAnnouncement
4179
+ // so a future CDN-side schema bump lights up `cta_label` on banner_clicked
4180
+ // without an inspector release.
4181
+ private announcementCtaLabel: string | null = null;
4182
+ private hasUnseenAnnouncement = false;
4183
+ private announcementLoaded = false;
4184
+ private announcementPromise: Promise<void> | null = null;
4185
+ private showAnnouncementPreview = true;
4186
+ private announcementExpanded = false;
4187
+ // Per-instance dedup for `oss.inspector.banner_viewed` so the event fires
4188
+ // at most once per announcement timestamp per inspector mount. Plan calls
4189
+ // for "de-dup per timestamp per session"; instance-scoping is closer
4190
+ // to per-mount than per-tab (sessionStorage), but for the inspector the
4191
+ // distinction is academic — inspector instances rarely outlive the page.
4192
+ private viewedBannerTimestamps: Set<string> = new Set();
4193
+ private pendingBannerViewed: {
4194
+ banner_id: string;
4195
+ cta_label?: string;
4196
+ } | null = null;
4197
+ // Per-instance dedup for `oss.inspector.banner_clicked` (keyed by
4198
+ // `${bannerId}:${cta}`) so copy-button retries and accidental multi-clicks
4199
+ // don't inflate funnel counts beyond one signal per intent type per banner.
4200
+ private clickedBannerIds: Set<string> = new Set();
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;
4215
+
4216
+ get core(): CopilotKitCore | null {
4217
+ return this._core;
4218
+ }
4219
+
4220
+ set core(value: CopilotKitCore | null) {
4221
+ const oldValue = this._core;
4222
+ if (oldValue === value) {
4223
+ return;
4224
+ }
4225
+
4226
+ this.detachFromCore();
4227
+
4228
+ this._core = value ?? null;
4229
+ this.requestUpdate("core", oldValue);
4230
+
4231
+ if (this._core) {
4232
+ this.attachToCore(this._core);
4233
+ }
4234
+ }
4235
+
4236
+ private readonly contextState: Record<ContextKey, ContextState> = {
4237
+ button: {
4238
+ position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
4239
+ size: { ...DEFAULT_BUTTON_SIZE },
4240
+ anchor: { horizontal: "right", vertical: "top" },
4241
+ anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
4242
+ },
4243
+ window: {
4244
+ position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
4245
+ size: { ...DEFAULT_WINDOW_SIZE },
4246
+ anchor: { horizontal: "right", vertical: "top" },
4247
+ anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
4248
+ },
4249
+ };
4250
+
4251
+ private hasCustomPosition: Record<ContextKey, boolean> = {
3581
4252
  button: false,
3582
4253
  window: false,
3583
4254
  };
@@ -3619,6 +4290,7 @@ export class WebInspectorElement extends LitElement {
3619
4290
  label: "Threads",
3620
4291
  icon: "MessageSquare" as LucideIconName,
3621
4292
  },
4293
+ { key: "memories", label: "Learning", icon: "Brain" as LucideIconName },
3622
4294
  ];
3623
4295
  }
3624
4296
 
@@ -3634,6 +4306,31 @@ export class WebInspectorElement extends LitElement {
3634
4306
  return this.getThreadServiceStatus() !== "unavailable";
3635
4307
  }
3636
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
+
3637
4334
  private getThreadsTelemetryProps(
3638
4335
  extra: Partial<InspectorThreadTelemetryProps> = {},
3639
4336
  options: { includeUrlAttribution?: boolean } = {},
@@ -3660,12 +4357,45 @@ export class WebInspectorElement extends LitElement {
3660
4357
  };
3661
4358
  }
3662
4359
 
3663
- private getIntelligenceSignupUrl(): string {
3664
- return this.appendRefParam(INTELLIGENCE_SIGNUP_URL, "cpk-inspector");
4360
+ private getMemoriesTelemetryProps(): InspectorMemoryTelemetryProps {
4361
+ const distinctId = !this.core?.telemetryDisabled
4362
+ ? getTelemetryDistinctIdForUrl()
4363
+ : null;
4364
+ return {
4365
+ posthog_distinct_id: distinctId ?? undefined,
4366
+ memory_count: this._memories.length,
4367
+ available: this._memoriesAvailable,
4368
+ };
4369
+ }
4370
+
4371
+ private getIntelligenceSignupUrl(): string {
4372
+ return this.appendRefParam(INTELLIGENCE_SIGNUP_URL, "cpk-inspector");
4373
+ }
4374
+
4375
+ private getThreadsIntelligenceSignupUrl(): string {
4376
+ return this.appendRefParam(
4377
+ THREADS_INTELLIGENCE_SIGNIN_URL,
4378
+ "cpk-inspector",
4379
+ );
4380
+ }
4381
+
4382
+ private getTalkToEngineerUrl(): string {
4383
+ return this.appendRefParam(TALK_TO_ENGINEER_URL, "cpk-inspector-threads");
4384
+ }
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");
3665
4392
  }
3666
4393
 
3667
- private getTalkToEngineerUrl(): string {
3668
- return this.appendRefParam(TALK_TO_ENGINEER_URL, "cpk-inspector-threads");
4394
+ private getSelfHostedIntelligenceUrl(): string {
4395
+ return this.appendRefParam(
4396
+ SELF_HOSTED_INTELLIGENCE_URL,
4397
+ "cpk-inspector-threads",
4398
+ );
3669
4399
  }
3670
4400
 
3671
4401
  private subscribeToThreadStore(agentId: string, store: ɵThreadStore): void {
@@ -3706,6 +4436,7 @@ export class WebInspectorElement extends LitElement {
3706
4436
  if (this._threads.length === 0) return;
3707
4437
  const stillValid =
3708
4438
  this.selectedThreadId != null &&
4439
+ !this.isExampleThreadId(this.selectedThreadId) &&
3709
4440
  this._threads.some((t) => t.id === this.selectedThreadId);
3710
4441
  if (!stillValid) {
3711
4442
  // Threads are sorted most-recently-updated first
@@ -3886,6 +4617,91 @@ export class WebInspectorElement extends LitElement {
3886
4617
  if (core.context) {
3887
4618
  this.contextStore = this.normalizeContextStore(core.context);
3888
4619
  }
4620
+
4621
+ // NOTE: the memory store is intentionally NOT touched here. Calling
4622
+ // `core.getMemoryStore()` lazily creates + `.start()`s the store and opens
4623
+ // a realtime connection, so merely attaching the inspector would spin up a
4624
+ // memory store + realtime even in apps that never use memory. Instead, the
4625
+ // store is created + subscribed on first Memories-tab activation via
4626
+ // `ensureMemorySubscription` (user-initiated, acceptable). Attaching the
4627
+ // inspector creates nothing.
4628
+ //
4629
+ // Exception: if the Memories tab is ALREADY the active tab when core is
4630
+ // wired (e.g. core attaches after `firstUpdated` restored a persisted
4631
+ // `selectedMenu: "memories"`), subscribe now so the live store + realtime
4632
+ // status paint instead of the stuck defaults. This preserves INSP-1 (no
4633
+ // unconditional subscribe on attach) because it is gated on the active tab,
4634
+ // and is safe to call when already subscribed — `ensureMemorySubscription`
4635
+ // early-returns on `_memorySubscribed`.
4636
+ if (this.selectedMenu === "memories") {
4637
+ this.ensureMemorySubscription();
4638
+ }
4639
+ }
4640
+
4641
+ /**
4642
+ * Lazily subscribes to the singleton memory store the first time the user
4643
+ * activates the Memories tab. This is deferred out of `attachToCore` because
4644
+ * `core.getMemoryStore()` is what creates + starts the store and opens
4645
+ * realtime — doing it on attach would start memory for apps that never use
4646
+ * it. Idempotent: repeated tab activations are guarded by
4647
+ * `_memorySubscribed`. On an older @copilotkit/core without `getMemoryStore`,
4648
+ * records the unsupported state so the teaser can guide an SDK upgrade.
4649
+ */
4650
+ private ensureMemorySubscription(): void {
4651
+ if (this._memorySubscribed) {
4652
+ return;
4653
+ }
4654
+ const core = this._core;
4655
+ if (!core) {
4656
+ return;
4657
+ }
4658
+
4659
+ // Guard like getThreadStores: older @copilotkit/core has no getMemoryStore.
4660
+ // When absent, flag the unsupported state so the teaser shows upgrade copy
4661
+ // instead of throwing a TypeError that would break the entire inspector.
4662
+ if (typeof core.getMemoryStore !== "function") {
4663
+ this._memoryStoreUnsupported = true;
4664
+ this._memoriesAvailable = false;
4665
+ this.requestUpdate();
4666
+ return;
4667
+ }
4668
+
4669
+ this._memorySubscribed = true;
4670
+ this._memoryStoreUnsupported = false;
4671
+
4672
+ // First touch of getMemoryStore() — creates + starts the store, opens realtime.
4673
+ const memoryStore = core.getMemoryStore();
4674
+ const ms = memoryStore.getState();
4675
+ this._memories = ɵselectMemories(ms);
4676
+ this._memoriesLoading = ɵselectMemoriesIsLoading(ms);
4677
+ this._memoriesError = ɵselectMemoriesError(ms);
4678
+ this._memoriesAvailable = ɵselectMemoriesAvailable(ms);
4679
+ this._memoriesRealtimeStatus = ɵselectMemoriesRealtimeStatus(ms);
4680
+ const memSubs = [
4681
+ memoryStore.select(ɵselectMemories).subscribe((v) => {
4682
+ this._memories = v;
4683
+ this.requestUpdate();
4684
+ }),
4685
+ memoryStore.select(ɵselectMemoriesIsLoading).subscribe((v) => {
4686
+ this._memoriesLoading = v;
4687
+ this.requestUpdate();
4688
+ }),
4689
+ memoryStore.select(ɵselectMemoriesError).subscribe((v) => {
4690
+ this._memoriesError = v;
4691
+ this.requestUpdate();
4692
+ }),
4693
+ memoryStore.select(ɵselectMemoriesAvailable).subscribe((v) => {
4694
+ this._memoriesAvailable = v;
4695
+ this.requestUpdate();
4696
+ }),
4697
+ // Group E — realtime connection health.
4698
+ memoryStore.select(ɵselectMemoriesRealtimeStatus).subscribe((v) => {
4699
+ this._memoriesRealtimeStatus = v;
4700
+ this.requestUpdate();
4701
+ }),
4702
+ ];
4703
+ this._memoryUnsub = () => memSubs.forEach((s) => s.unsubscribe());
4704
+ this.requestUpdate();
3889
4705
  }
3890
4706
 
3891
4707
  private detachFromCore(): void {
@@ -3893,6 +4709,17 @@ export class WebInspectorElement extends LitElement {
3893
4709
  this.coreUnsubscribe();
3894
4710
  this.coreUnsubscribe = null;
3895
4711
  }
4712
+ this._memoryUnsub?.();
4713
+ this._memoryUnsub = null;
4714
+ this._memories = [];
4715
+ this._memoriesLoading = false;
4716
+ this._memoriesError = null;
4717
+ this._memoriesAvailable = true;
4718
+ this._memoriesRealtimeStatus = "connecting";
4719
+ // Reset the lazy-subscription guards so a later attach + Memories-tab
4720
+ // activation re-subscribes (and re-evaluates SDK support) cleanly.
4721
+ this._memorySubscribed = false;
4722
+ this._memoryStoreUnsupported = false;
3896
4723
  this.coreSubscriber = null;
3897
4724
  this.runtimeStatus = null;
3898
4725
  this.lastCoreError = null;
@@ -5199,6 +6026,35 @@ ${argsString}</pre
5199
6026
  .cpk-tab-active {
5200
6027
  cursor: pointer;
5201
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
+ }
5202
6058
 
5203
6059
  /* ── Header control buttons (dock, close) — first row only ───── */
5204
6060
  .drag-handle > div:first-child button {
@@ -5381,6 +6237,7 @@ ${argsString}</pre
5381
6237
 
5382
6238
  // Load state early (before first render) so menu selection is correct
5383
6239
  this.hydrateStateFromStorageEarly();
6240
+ this.exampleTourDismissed = this.readThreadsExampleTourDismissed();
5384
6241
  this.tryAutoAttachCore();
5385
6242
  this.ensureAnnouncementLoading();
5386
6243
  }
@@ -5415,6 +6272,17 @@ ${argsString}</pre
5415
6272
  clearTimeout(this.transitionTimeoutId);
5416
6273
  this.transitionTimeoutId = null;
5417
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
+ }
5418
6286
  this.removeDockStyles(true); // Clean up any docking styles, skip transition
5419
6287
  this.detachFromCore();
5420
6288
  }
@@ -5439,6 +6307,18 @@ ${argsString}</pre
5439
6307
 
5440
6308
  this.hydrateStateFromStorage();
5441
6309
 
6310
+ // `hydrateStateFromStorage` may have restored `selectedMenu: "memories"`.
6311
+ // The memory subscription is normally created on a Memories-tab CLICK via
6312
+ // `handleMenuSelect`, which never fires when the tab boots already active —
6313
+ // leaving the realtime indicator stuck on the default "connecting" and the
6314
+ // list empty until the user toggles tabs. Subscribe now when the Memories
6315
+ // tab is the active tab. Gated on the active tab to preserve INSP-1 (no
6316
+ // unconditional subscribe), and safe if core is not yet attached or already
6317
+ // subscribed — `ensureMemorySubscription` early-returns in both cases.
6318
+ if (this.selectedMenu === "memories") {
6319
+ this.ensureMemorySubscription();
6320
+ }
6321
+
5442
6322
  // Apply docking styles if open and docked (skip transition on initial load)
5443
6323
  if (this.isOpen && this.dockMode !== "floating") {
5444
6324
  this.applyDockStyles(true);
@@ -5672,6 +6552,21 @@ ${argsString}</pre
5672
6552
  </button>
5673
6553
  `;
5674
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
+ }
5675
6570
  </div>
5676
6571
  </div>
5677
6572
  <div class="flex flex-1 flex-col overflow-hidden">
@@ -6896,6 +7791,10 @@ ${argsString}</pre
6896
7791
  return this.renderThreadsView();
6897
7792
  }
6898
7793
 
7794
+ if (this.selectedMenu === "memories") {
7795
+ return this.renderMemoriesView();
7796
+ }
7797
+
6899
7798
  if (this.selectedMenu === "settings") {
6900
7799
  return this.renderSettingsPanel();
6901
7800
  }
@@ -7025,25 +7924,509 @@ ${argsString}</pre
7025
7924
  if (target.hasPointerCapture(this.threadDividerPointerId)) {
7026
7925
  target.releasePointerCapture(this.threadDividerPointerId);
7027
7926
  }
7028
- this.threadDividerResizing = false;
7029
- };
7927
+ this.threadDividerResizing = false;
7928
+ };
7929
+
7930
+ private trackThreadsViewStateOnce(
7931
+ state: "locked" | "empty_enabled" | "enabled",
7932
+ threadCount: number,
7933
+ ): void {
7934
+ if (this.core?.telemetryDisabled) return;
7935
+ const key = `${state}:${this.getThreadServiceStatus()}`;
7936
+ if (this.viewedThreadsTelemetryStates.has(key)) return;
7937
+ this.viewedThreadsTelemetryStates.add(key);
7938
+ const props = this.getThreadsTelemetryProps({ thread_count: threadCount });
7939
+ if (state === "locked") {
7940
+ trackThreadsLockedViewed(props);
7941
+ } else if (state === "empty_enabled") {
7942
+ trackThreadsEmptyEnabledViewed(props);
7943
+ } else {
7944
+ trackThreadsEnabledViewed(props);
7945
+ }
7946
+ }
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
+ }
7030
8313
 
7031
- private trackThreadsViewStateOnce(
7032
- state: "locked" | "empty_enabled" | "enabled",
7033
- threadCount: number,
7034
- ): void {
7035
- if (this.core?.telemetryDisabled) return;
7036
- const key = `${state}:${this.getThreadServiceStatus()}`;
7037
- if (this.viewedThreadsTelemetryStates.has(key)) return;
7038
- this.viewedThreadsTelemetryStates.add(key);
7039
- const props = this.getThreadsTelemetryProps({ thread_count: threadCount });
7040
- if (state === "locked") {
7041
- trackThreadsLockedViewed(props);
7042
- } else if (state === "empty_enabled") {
7043
- trackThreadsEmptyEnabledViewed(props);
7044
- } else {
7045
- trackThreadsEnabledViewed(props);
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
+ `;
7046
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
+ `;
7047
8430
  }
7048
8431
 
7049
8432
  private renderThreadsLockedBackgroundMockup() {
@@ -7293,7 +8676,7 @@ ${argsString}</pre
7293
8676
  "
7294
8677
  >
7295
8678
  <a
7296
- href=${this.getTalkToEngineerUrl()}
8679
+ href=${this.getThreadsIntelligenceSignupUrl()}
7297
8680
  target="_blank"
7298
8681
  rel="noopener"
7299
8682
  style="
@@ -7303,42 +8686,360 @@ ${argsString}</pre
7303
8686
  justify-content: center;
7304
8687
  gap: 6px;
7305
8688
  border-radius: 6px;
7306
- background: #010507;
8689
+ border: 1px solid #dbdbe5;
8690
+ background: #ffffff;
7307
8691
  padding: 8px 12px;
7308
8692
  font-size: 12px;
7309
8693
  font-weight: 600;
7310
- color: #ffffff;
8694
+ color: #57575b;
7311
8695
  text-decoration: none;
7312
8696
  "
7313
- @click=${this.handleThreadsTalkToEngineerClick}
8697
+ @click=${this.handleThreadsIntelligenceSignupClick}
7314
8698
  >
7315
- Talk to an Engineer
8699
+ Sign up for Intelligence
7316
8700
  </a>
7317
- <a
7318
- href=${this.getIntelligenceSignupUrl()}
7319
- target="_blank"
7320
- rel="noopener"
8701
+ </div>
8702
+ </div>
8703
+ </div>
8704
+ `;
8705
+ }
8706
+
8707
+ /**
8708
+ * Renders the realtime-connection indicator in the memory-store header.
8709
+ * Only `"connected"` shows the live (green-dot) state; `"connecting"` shows a
8710
+ * muted amber "reconnecting" and `"unavailable"` a muted grey "offline", so
8711
+ * the indicator never claims "live" over a frozen snapshot once the realtime
8712
+ * socket has permanently given up.
8713
+ */
8714
+ private renderMemoryRealtimeIndicator() {
8715
+ const status = this._memoriesRealtimeStatus;
8716
+ const connected = status === "connected";
8717
+ const dotColor = connected
8718
+ ? "#22c55e"
8719
+ : status === "connecting"
8720
+ ? "#f59e0b"
8721
+ : "#9ca3af";
8722
+ const label =
8723
+ status === "connected"
8724
+ ? "live"
8725
+ : status === "connecting"
8726
+ ? "reconnecting"
8727
+ : "offline";
8728
+ return html`
8729
+ <span
8730
+ style="
8731
+ display: inline-flex;
8732
+ align-items: center;
8733
+ gap: 4px;
8734
+ font-size: 10px;
8735
+ font-weight: 500;
8736
+ color: ${connected ? "#57575b" : "#838389"};
8737
+ "
8738
+ >
8739
+ <span
8740
+ style="
8741
+ display: inline-block;
8742
+ width: 6px;
8743
+ height: 6px;
8744
+ border-radius: 50%;
8745
+ background: ${dotColor};
8746
+ "
8747
+ ></span>
8748
+ ${label}
8749
+ </span>
8750
+ `;
8751
+ }
8752
+
8753
+ private renderMemoriesView() {
8754
+ // 1. Locked teaser — intelligence not configured or memories not available.
8755
+ if (!this.core?.intelligence || !this._memoriesAvailable) {
8756
+ return html`
8757
+ <div
8758
+ style="
8759
+ position: relative;
8760
+ height: 100%;
8761
+ display: flex;
8762
+ align-items: center;
8763
+ justify-content: center;
8764
+ padding: 32px;
8765
+ overflow: hidden;
8766
+ background: #ffffff;
8767
+ "
8768
+ >
8769
+ ${this.renderThreadsLockedBackgroundMockup()}
8770
+ <div
8771
+ aria-hidden="true"
8772
+ style="
8773
+ position: absolute;
8774
+ inset: 0;
8775
+ pointer-events: none;
8776
+ background:
8777
+ radial-gradient(circle at center, rgba(255,255,255,0.9) 0, rgba(255,255,255,0.78) 24%, rgba(255,255,255,0.34) 48%, rgba(255,255,255,0.56) 100%);
8778
+ "
8779
+ ></div>
8780
+ <div
8781
+ style="
8782
+ position: relative;
8783
+ z-index: 1;
8784
+ max-width: 440px;
8785
+ text-align: center;
8786
+ color: #57575b;
8787
+ "
8788
+ >
8789
+ <div
8790
+ aria-hidden="true"
7321
8791
  style="
7322
- display: inline-flex;
7323
- min-height: 34px;
7324
- align-items: center;
8792
+ margin: 0 auto 18px;
8793
+ display: flex;
7325
8794
  justify-content: center;
7326
- gap: 6px;
7327
- border-radius: 6px;
7328
- border: 1px solid #dbdbe5;
7329
- background: #ffffff;
7330
- padding: 8px 12px;
7331
- font-size: 12px;
8795
+ "
8796
+ >
8797
+ <div
8798
+ style="
8799
+ display: flex;
8800
+ height: 44px;
8801
+ width: 44px;
8802
+ align-items: center;
8803
+ justify-content: center;
8804
+ border: 1px solid #dfd6fb;
8805
+ border-radius: 8px;
8806
+ background: #eee6fe;
8807
+ color: #57575b;
8808
+ box-shadow: 0 8px 18px rgba(87, 87, 91, 0.14);
8809
+ "
8810
+ >
8811
+ ${this.renderIcon("Lock")}
8812
+ </div>
8813
+ </div>
8814
+ <h2
8815
+ style="
8816
+ margin: 0 0 8px;
8817
+ font-size: 16px;
8818
+ line-height: 1.35;
7332
8819
  font-weight: 600;
8820
+ color: #010507;
8821
+ "
8822
+ >
8823
+ Long-term memory
8824
+ </h2>
8825
+ <p
8826
+ style="
8827
+ margin: 0 auto 18px;
8828
+ max-width: 380px;
8829
+ font-size: 13px;
8830
+ line-height: 1.55;
7333
8831
  color: #57575b;
7334
- text-decoration: none;
7335
8832
  "
7336
- @click=${this.handleThreadsIntelligenceSignupClick}
7337
8833
  >
7338
- Sign up for Intelligence
7339
- </a>
8834
+ ${
8835
+ this._memoryStoreUnsupported
8836
+ ? "Long-term memory isn't available in this version of the @copilotkit SDK. Upgrade @copilotkit/core (and @copilotkit/react) to a version that supports memory."
8837
+ : "Long-term memory isn't enabled on this deployment."
8838
+ }
8839
+ </p>
8840
+ <div
8841
+ style="
8842
+ display: flex;
8843
+ flex-wrap: wrap;
8844
+ justify-content: center;
8845
+ gap: 8px;
8846
+ "
8847
+ >
8848
+ <a
8849
+ href=${this.getTalkToEngineerUrl()}
8850
+ target="_blank"
8851
+ rel="noopener"
8852
+ style="
8853
+ display: inline-flex;
8854
+ min-height: 34px;
8855
+ align-items: center;
8856
+ justify-content: center;
8857
+ gap: 6px;
8858
+ border-radius: 6px;
8859
+ background: #010507;
8860
+ padding: 8px 12px;
8861
+ font-size: 12px;
8862
+ font-weight: 600;
8863
+ color: #ffffff;
8864
+ text-decoration: none;
8865
+ "
8866
+ @click=${this.handleThreadsTalkToEngineerClick}
8867
+ >
8868
+ Talk to an Engineer
8869
+ </a>
8870
+ <a
8871
+ href=${this.getIntelligenceSignupUrl()}
8872
+ target="_blank"
8873
+ rel="noopener"
8874
+ style="
8875
+ display: inline-flex;
8876
+ min-height: 34px;
8877
+ align-items: center;
8878
+ justify-content: center;
8879
+ gap: 6px;
8880
+ border-radius: 6px;
8881
+ border: 1px solid #dbdbe5;
8882
+ background: #ffffff;
8883
+ padding: 8px 12px;
8884
+ font-size: 12px;
8885
+ font-weight: 600;
8886
+ color: #57575b;
8887
+ text-decoration: none;
8888
+ "
8889
+ @click=${this.handleThreadsIntelligenceSignupClick}
8890
+ >
8891
+ Sign up for Intelligence
8892
+ </a>
8893
+ </div>
8894
+ </div>
8895
+ </div>
8896
+ `;
8897
+ }
8898
+
8899
+ // 2. Full-screen error — only for a snapshot-LOAD failure (no memories
8900
+ // loaded). A mutation failure that arrives while memories are already on
8901
+ // screen must NOT blank the list; it is surfaced inline below (step 4).
8902
+ if (this._memoriesError && this._memories.length === 0) {
8903
+ return html`
8904
+ <div
8905
+ style="
8906
+ display: flex;
8907
+ height: 100%;
8908
+ flex-direction: column;
8909
+ align-items: center;
8910
+ justify-content: center;
8911
+ gap: 8px;
8912
+ color: #838389;
8913
+ "
8914
+ >
8915
+ <svg
8916
+ width="24"
8917
+ height="24"
8918
+ viewBox="0 0 24 24"
8919
+ fill="none"
8920
+ stroke="#c0333a"
8921
+ stroke-width="1.5"
8922
+ stroke-linecap="round"
8923
+ stroke-linejoin="round"
8924
+ >
8925
+ <circle cx="12" cy="12" r="10" />
8926
+ <line x1="12" y1="8" x2="12" y2="12" />
8927
+ <line x1="12" y1="16" x2="12.01" y2="16" />
8928
+ </svg>
8929
+ <span style="font-size: 13px; color: #c0333a;">
8930
+ Failed to load memories
8931
+ </span>
8932
+ <span
8933
+ style="
8934
+ max-width: 320px;
8935
+ text-align: center;
8936
+ font-size: 11px;
8937
+ line-height: 1.5;
8938
+ color: #c0333a;
8939
+ "
8940
+ >
8941
+ ${this._memoriesError.message}
8942
+ </span>
8943
+ </div>
8944
+ `;
8945
+ }
8946
+
8947
+ // 3. Initial loading placeholder (no memories yet to show behind it).
8948
+ if (this._memoriesLoading && this._memories.length === 0) {
8949
+ return html`
8950
+ <div
8951
+ style="
8952
+ display: flex;
8953
+ height: 100%;
8954
+ flex-direction: column;
8955
+ align-items: center;
8956
+ justify-content: center;
8957
+ gap: 8px;
8958
+ color: #838389;
8959
+ "
8960
+ >
8961
+ <svg
8962
+ width="24"
8963
+ height="24"
8964
+ viewBox="0 0 24 24"
8965
+ fill="none"
8966
+ stroke="#c0c0c8"
8967
+ stroke-width="1.5"
8968
+ stroke-linecap="round"
8969
+ stroke-linejoin="round"
8970
+ >
8971
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
8972
+ </svg>
8973
+ <span style="font-size: 13px">Loading memories…</span>
8974
+ </div>
8975
+ `;
8976
+ }
8977
+
8978
+ // 4. Content — header + memory list.
8979
+ return html`
8980
+ <div style="display:flex;height:100%;overflow:hidden;flex-direction:column;">
8981
+ <div class="cpk-section-header" style="display:flex;align-items:center;justify-content:space-between;">
8982
+ <h4>Learning</h4>
8983
+ <div style="display:flex;align-items:center;gap:6px;">
8984
+ ${this.renderMemoryRealtimeIndicator()}
8985
+ <span
8986
+ style="
8987
+ font-size: 11px;
8988
+ font-weight: 500;
8989
+ color: #57575b;
8990
+ background: rgba(0,0,0,0.07);
8991
+ border-radius: 9999px;
8992
+ padding: 1px 7px;
8993
+ "
8994
+ >
8995
+ ${this._memories.length}
8996
+ </span>
7340
8997
  </div>
7341
8998
  </div>
8999
+ ${
9000
+ this._memoriesError
9001
+ ? html`
9002
+ <div
9003
+ role="alert"
9004
+ style="
9005
+ display: flex;
9006
+ align-items: flex-start;
9007
+ gap: 8px;
9008
+ flex-shrink: 0;
9009
+ border-bottom: 1px solid #f1c7c9;
9010
+ background: #fdf3f3;
9011
+ padding: 8px 12px;
9012
+ color: #c0333a;
9013
+ font-size: 12px;
9014
+ line-height: 1.45;
9015
+ "
9016
+ >
9017
+ <svg
9018
+ width="16"
9019
+ height="16"
9020
+ viewBox="0 0 24 24"
9021
+ fill="none"
9022
+ stroke="#c0333a"
9023
+ stroke-width="1.5"
9024
+ stroke-linecap="round"
9025
+ stroke-linejoin="round"
9026
+ style="flex-shrink:0;margin-top:1px;"
9027
+ >
9028
+ <circle cx="12" cy="12" r="10" />
9029
+ <line x1="12" y1="8" x2="12" y2="12" />
9030
+ <line x1="12" y1="16" x2="12.01" y2="16" />
9031
+ </svg>
9032
+ <span>Action failed: ${this._memoriesError.message}</span>
9033
+ </div>
9034
+ `
9035
+ : nothing
9036
+ }
9037
+ <div style="flex:1;min-height:0;overflow:hidden;">
9038
+ <cpk-memory-list
9039
+ style="height:100%;"
9040
+ .memories=${this._memories}
9041
+ ></cpk-memory-list>
9042
+ </div>
7342
9043
  </div>
7343
9044
  `;
7344
9045
  }
@@ -7348,28 +9049,26 @@ ${argsString}</pre
7348
9049
  return this.renderThreadsLockedView();
7349
9050
  }
7350
9051
 
7351
- const displayThreads =
7352
- this.selectedContext === "all-agents"
7353
- ? this._threads
7354
- : (this._threadsByAgent.get(this.selectedContext) ?? []);
9052
+ const { displayThreads, threadsErrorMessage } =
9053
+ this.getActiveThreadsState();
7355
9054
 
7356
- // Surface a thread-store load error inline. For "all-agents" we report
7357
- // the first error encountered across all agents (good enough for a
7358
- // debugging surface — the per-agent context filter narrows down the
7359
- // culprit). For a specific agent we use that agent's error directly.
7360
- let threadsErrorMessage: string | null = null;
7361
- if (this.selectedContext === "all-agents") {
7362
- const firstError = this._threadsErrorByAgent.values().next().value;
7363
- threadsErrorMessage = firstError?.message ?? null;
7364
- } else {
7365
- threadsErrorMessage =
7366
- 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();
7367
9065
  }
7368
9066
 
7369
9067
  const selectedThread =
7370
9068
  this.selectedThreadId != null
7371
- ? (displayThreads.find((t) => t.id === this.selectedThreadId) ?? null)
9069
+ ? (visibleThreads.find((t) => t.id === this.selectedThreadId) ?? null)
7372
9070
  : null;
9071
+ const selectedThreadIsExample = this.isExampleThreadId(selectedThread?.id);
7373
9072
 
7374
9073
  if (!threadsErrorMessage) {
7375
9074
  this.trackThreadsViewStateOnce(
@@ -7380,19 +9079,6 @@ ${argsString}</pre
7380
9079
 
7381
9080
  return html`
7382
9081
  <div style="display:flex;height:100%;overflow:hidden;flex-direction:column;">
7383
- <div
7384
- style="display:flex;align-items:center;justify-content:flex-end;border-bottom:1px solid #DBDBE5;background:#ffffff;padding:8px 12px;flex-shrink:0;"
7385
- >
7386
- <a
7387
- href=${this.getTalkToEngineerUrl()}
7388
- target="_blank"
7389
- rel="noopener"
7390
- 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;"
7391
- @click=${this.handleTalkToEngineerClick}
7392
- >
7393
- Talk to an Engineer
7394
- </a>
7395
- </div>
7396
9082
  <div style="display:flex;min-height:0;flex:1;overflow:hidden;">
7397
9083
  <!-- Left sidebar: thread list -->
7398
9084
  <div
@@ -7400,12 +9086,11 @@ ${argsString}</pre
7400
9086
  >
7401
9087
  <cpk-thread-list
7402
9088
  style="height:100%;"
7403
- .threads=${displayThreads}
9089
+ .threads=${visibleThreads}
7404
9090
  .selectedThreadId=${this.selectedThreadId}
7405
9091
  .errorMessage=${threadsErrorMessage}
7406
9092
  @threadSelected=${(e: CustomEvent<string>) => {
7407
- this.selectedThreadId = e.detail;
7408
- this.requestUpdate();
9093
+ this.handleThreadsThreadSelected(e.detail, showingExamples);
7409
9094
  }}
7410
9095
  ></cpk-thread-list>
7411
9096
  </div>
@@ -7420,16 +9105,22 @@ ${argsString}</pre
7420
9105
  ></div>
7421
9106
 
7422
9107
  <!-- Center + right: thread details or empty state -->
7423
- <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;">
7424
9109
  ${
7425
9110
  selectedThread
7426
9111
  ? html`<cpk-thread-details
7427
9112
  style="flex:1;min-width:0;"
7428
9113
  .threadId=${selectedThread.id}
7429
9114
  .thread=${selectedThread}
9115
+ .provider=${
9116
+ selectedThreadIsExample
9117
+ ? this.getExampleThreadProvider(selectedThread.id)
9118
+ : null
9119
+ }
7430
9120
  .runtimeUrl=${this._core?.runtimeUrl ?? ""}
7431
9121
  .headers=${this._core?.headers ?? {}}
7432
9122
  .threadInspectionAvailable=${
9123
+ selectedThreadIsExample ||
7433
9124
  this._core?.threadEndpoints?.inspect !== false
7434
9125
  }
7435
9126
  .liveMessageVersion=${
@@ -7441,8 +9132,11 @@ ${argsString}</pre
7441
9132
  .agentEventsInput=${
7442
9133
  this.agentEvents.get(selectedThread.agentId) ?? []
7443
9134
  }
7444
- ></cpk-thread-details>`
7445
- : html`
9135
+ ></cpk-thread-details>
9136
+ ${selectedThreadIsExample ? this.renderThreadsExampleTour() : nothing}`
9137
+ : showingExamples
9138
+ ? this.renderThreadsExampleOverview()
9139
+ : html`
7446
9140
  <div
7447
9141
  style="
7448
9142
  flex: 1;
@@ -8162,6 +9856,16 @@ ${prettyEvent}</pre
8162
9856
  this.autoSelectLatestThread();
8163
9857
  }
8164
9858
 
9859
+ if (key === "memories") {
9860
+ // Lazily create + subscribe to the memory store on first activation. This
9861
+ // is the only place that touches getMemoryStore(), so the store/realtime
9862
+ // are never started just by attaching the inspector.
9863
+ this.ensureMemorySubscription();
9864
+ if (previousMenu !== "memories" && !this.core?.telemetryDisabled) {
9865
+ trackMemoriesTabClicked(this.getMemoriesTelemetryProps());
9866
+ }
9867
+ }
9868
+
8165
9869
  if (key === "ag-ui-events" || key === "agents") {
8166
9870
  requestAnimationFrame(() => {
8167
9871
  const scroller = this.shadowRoot?.getElementById("cpk-main-scroll");