@copilotkit/web-inspector 1.62.2 → 1.62.3

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 {
@@ -60,10 +67,14 @@ import {
60
67
  trackThreadsEnabledViewed,
61
68
  trackThreadsIntelligenceSignupClicked,
62
69
  trackThreadsLockedViewed,
70
+ trackMemoriesTabClicked,
63
71
  trackThreadsTabClicked,
64
72
  trackThreadsTalkToEngineerClicked,
65
73
  } from "./lib/telemetry.js";
66
- import type { InspectorThreadTelemetryProps } from "./lib/telemetry.js";
74
+ import type {
75
+ InspectorMemoryTelemetryProps,
76
+ InspectorThreadTelemetryProps,
77
+ } from "./lib/telemetry.js";
67
78
 
68
79
  export type { Anchor } from "./lib/types.js";
69
80
 
@@ -78,6 +89,7 @@ type MenuKey =
78
89
  | "frontend-tools"
79
90
  | "agent-context"
80
91
  | "threads"
92
+ | "memories"
81
93
  | "settings";
82
94
 
83
95
  type MenuItem = {
@@ -3418,6 +3430,368 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
3418
3430
  }
3419
3431
  }
3420
3432
 
3433
+ // ─── cpk-memory-list ─────────────────────────────────────────────────────────
3434
+
3435
+ /** Memory kind values including the "all" sentinel used by the filter UI. */
3436
+ type MemoryKindFilter = "all" | "topical" | "episodic" | "operational";
3437
+
3438
+ class CpkMemoryList extends LitElement {
3439
+ static properties = {
3440
+ memories: { attribute: false },
3441
+ search: { state: true },
3442
+ kind: { state: true },
3443
+ };
3444
+
3445
+ /** Ordered (newest-first) list of memories supplied by the parent. */
3446
+ memories: Memory[] = [];
3447
+ private search = "";
3448
+ private kind: MemoryKindFilter = "all";
3449
+
3450
+ static styles = css`
3451
+ @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&family=Spline+Sans+Mono:wght@400;500&display=swap");
3452
+
3453
+ :host {
3454
+ display: flex;
3455
+ flex-direction: column;
3456
+ height: 100%;
3457
+ overflow: hidden;
3458
+ }
3459
+
3460
+ .cpk-ml {
3461
+ font-family: "Plus Jakarta Sans", sans-serif;
3462
+ display: flex;
3463
+ flex-direction: column;
3464
+ height: 100%;
3465
+ overflow: hidden;
3466
+ background: #f7f7f9;
3467
+ }
3468
+
3469
+ /* ── Search ── */
3470
+ .cpk-ml__search {
3471
+ padding: 10px 12px;
3472
+ border-bottom: 1px solid #dbdbe5;
3473
+ flex-shrink: 0;
3474
+ }
3475
+
3476
+ .cpk-ml__search-input {
3477
+ width: 100%;
3478
+ box-sizing: border-box;
3479
+ font-family: "Plus Jakarta Sans", sans-serif;
3480
+ font-size: 12px;
3481
+ padding: 7px 10px;
3482
+ border-radius: 6px;
3483
+ border: 1px solid #dbdbe5;
3484
+ background: #ffffff;
3485
+ color: #010507;
3486
+ outline: none;
3487
+ transition: border-color 0.15s;
3488
+ }
3489
+
3490
+ .cpk-ml__search-input:focus {
3491
+ border-color: #bec2ff;
3492
+ }
3493
+
3494
+ /* ── Kind filter ── */
3495
+ .cpk-ml__filter {
3496
+ display: flex;
3497
+ gap: 4px;
3498
+ padding: 8px 12px;
3499
+ border-bottom: 1px solid #dbdbe5;
3500
+ flex-shrink: 0;
3501
+ flex-wrap: wrap;
3502
+ }
3503
+
3504
+ .cpk-ml__filter-seg {
3505
+ font-family: "Plus Jakarta Sans", sans-serif;
3506
+ font-size: 11px;
3507
+ font-weight: 500;
3508
+ padding: 3px 9px;
3509
+ border-radius: 5px;
3510
+ border: 1px solid #dbdbe5;
3511
+ background: #ffffff;
3512
+ color: #57575b;
3513
+ cursor: pointer;
3514
+ transition:
3515
+ background 0.1s,
3516
+ border-color 0.1s,
3517
+ color 0.1s;
3518
+ user-select: none;
3519
+ }
3520
+
3521
+ .cpk-ml__filter-seg:hover {
3522
+ background: #f0f0f5;
3523
+ }
3524
+
3525
+ .cpk-ml__filter-seg--active {
3526
+ background: #bec2ff1a;
3527
+ border-color: #bec2ff;
3528
+ color: #010507;
3529
+ }
3530
+
3531
+ .cpk-ml__filter-count {
3532
+ font-family: "Spline Sans Mono", monospace;
3533
+ font-size: 9px;
3534
+ margin-left: 4px;
3535
+ color: #838389;
3536
+ }
3537
+
3538
+ /* ── List ── */
3539
+ .cpk-ml__list {
3540
+ flex: 1;
3541
+ overflow-y: auto;
3542
+ padding: 8px 12px;
3543
+ display: flex;
3544
+ flex-direction: column;
3545
+ gap: 8px;
3546
+ }
3547
+
3548
+ /* ── Card ── */
3549
+ .cpk-ml__card {
3550
+ background: #ffffff;
3551
+ border: 1px solid #e9e9ef;
3552
+ border-radius: 8px;
3553
+ padding: 10px 12px;
3554
+ display: flex;
3555
+ flex-direction: column;
3556
+ gap: 6px;
3557
+ }
3558
+
3559
+ .cpk-ml__card-badges {
3560
+ display: flex;
3561
+ gap: 6px;
3562
+ align-items: center;
3563
+ flex-wrap: wrap;
3564
+ }
3565
+
3566
+ /* Kind badge — color per kind */
3567
+ .cpk-ml__kind-badge {
3568
+ font-family: "Spline Sans Mono", monospace;
3569
+ font-size: 9px;
3570
+ padding: 1px 7px;
3571
+ border-radius: 4px;
3572
+ text-transform: uppercase;
3573
+ font-weight: 500;
3574
+ white-space: nowrap;
3575
+ }
3576
+
3577
+ .cpk-ml__kind-badge--topical {
3578
+ background: #eee6fe;
3579
+ color: #57575b;
3580
+ }
3581
+
3582
+ .cpk-ml__kind-badge--episodic {
3583
+ background: #e6f4fe;
3584
+ color: #2d5f80;
3585
+ }
3586
+
3587
+ .cpk-ml__kind-badge--operational {
3588
+ background: #e6feee;
3589
+ color: #2d6645;
3590
+ }
3591
+
3592
+ /* Scope badge */
3593
+ .cpk-ml__scope-badge {
3594
+ font-family: "Spline Sans Mono", monospace;
3595
+ font-size: 9px;
3596
+ padding: 1px 7px;
3597
+ border-radius: 4px;
3598
+ text-transform: uppercase;
3599
+ font-weight: 500;
3600
+ white-space: nowrap;
3601
+ background: #f0f0f5;
3602
+ color: #838389;
3603
+ }
3604
+
3605
+ /* Content */
3606
+ .cpk-ml__content {
3607
+ font-size: 12px;
3608
+ color: #010507;
3609
+ line-height: 1.5;
3610
+ word-break: break-word;
3611
+ }
3612
+
3613
+ /* Footer */
3614
+ .cpk-ml__footer {
3615
+ display: flex;
3616
+ align-items: center;
3617
+ justify-content: space-between;
3618
+ gap: 8px;
3619
+ margin-top: 2px;
3620
+ }
3621
+
3622
+ .cpk-ml__footer-threads {
3623
+ font-size: 10px;
3624
+ color: #838389;
3625
+ }
3626
+
3627
+ .cpk-ml__footer-id {
3628
+ font-family: "Spline Sans Mono", monospace;
3629
+ font-size: 9px;
3630
+ color: #c0c0c8;
3631
+ }
3632
+
3633
+ /* ── Empty state ── */
3634
+ .cpk-ml__empty {
3635
+ padding: 32px 16px;
3636
+ text-align: center;
3637
+ color: #838389;
3638
+ font-size: 12px;
3639
+ display: flex;
3640
+ flex-direction: column;
3641
+ align-items: center;
3642
+ gap: 8px;
3643
+ }
3644
+
3645
+ .cpk-ml__empty-icon {
3646
+ color: #c0c0c8;
3647
+ }
3648
+ `;
3649
+
3650
+ /** Memories that pass the current text search (before kind filter). */
3651
+ private get searchFiltered(): Memory[] {
3652
+ const q = this.search.trim().toLowerCase();
3653
+ if (!q) return this.memories;
3654
+ return this.memories.filter((m) => m.content.toLowerCase().includes(q));
3655
+ }
3656
+
3657
+ /** Memories that pass both search and kind filter. */
3658
+ private get filtered(): Memory[] {
3659
+ const searched = this.searchFiltered;
3660
+ if (this.kind === "all") return searched;
3661
+ return searched.filter((m) => m.kind === this.kind);
3662
+ }
3663
+
3664
+ /** Count of search-filtered memories for a given kind (for segment labels). */
3665
+ private countForKind(kind: Exclude<MemoryKindFilter, "all">): number {
3666
+ return this.searchFiltered.filter((m) => m.kind === kind).length;
3667
+ }
3668
+
3669
+ private onSearchInput = (event: Event): void => {
3670
+ this.search = (event.target as HTMLInputElement).value;
3671
+ };
3672
+
3673
+ private onKindClick = (event: Event): void => {
3674
+ const seg = (event.target as HTMLElement).closest("[data-kind]");
3675
+ if (!seg) return;
3676
+ const k = (seg as HTMLElement).dataset["kind"] as MemoryKindFilter;
3677
+ this.kind = k;
3678
+ };
3679
+
3680
+ /** Truncate an id to first-4…last-4 characters. */
3681
+ private shortId(id: string): string {
3682
+ if (id.length <= 12) return id;
3683
+ return `${id.slice(0, 4)}…${id.slice(-4)}`;
3684
+ }
3685
+
3686
+ private renderKindBadge(kind: string): TemplateResult {
3687
+ return html`<span class="cpk-ml__kind-badge cpk-ml__kind-badge--${kind}"
3688
+ >${kind}</span
3689
+ >`;
3690
+ }
3691
+
3692
+ private renderEmpty(): TemplateResult {
3693
+ const q = this.search.trim();
3694
+ if (this.memories.length === 0) {
3695
+ return html`
3696
+ <div class="cpk-ml__empty">
3697
+ <svg
3698
+ width="24"
3699
+ height="24"
3700
+ viewBox="0 0 24 24"
3701
+ fill="none"
3702
+ stroke="currentColor"
3703
+ stroke-width="1.5"
3704
+ stroke-linecap="round"
3705
+ stroke-linejoin="round"
3706
+ class="cpk-ml__empty-icon"
3707
+ >
3708
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
3709
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
3710
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
3711
+ </svg>
3712
+ No memories yet — tell the agent a durable fact and watch it appear.
3713
+ </div>
3714
+ `;
3715
+ }
3716
+ if (q) {
3717
+ return html`
3718
+ <div class="cpk-ml__empty">
3719
+ No memories match &ldquo;${q}&rdquo;.
3720
+ </div>
3721
+ `;
3722
+ }
3723
+ return html`
3724
+ <div class="cpk-ml__empty">No ${this.kind} memories yet.</div>
3725
+ `;
3726
+ }
3727
+
3728
+ render() {
3729
+ const filtered = this.filtered;
3730
+ const kinds: Array<Exclude<MemoryKindFilter, "all">> = [
3731
+ "topical",
3732
+ "episodic",
3733
+ "operational",
3734
+ ];
3735
+
3736
+ return html`
3737
+ <div class="cpk-ml">
3738
+ <!-- Search -->
3739
+ <div class="cpk-ml__search">
3740
+ <input
3741
+ type="text"
3742
+ placeholder="Search memories…"
3743
+ .value=${this.search}
3744
+ @input=${this.onSearchInput}
3745
+ class="cpk-ml__search-input"
3746
+ />
3747
+ </div>
3748
+
3749
+ <!-- Kind filter -->
3750
+ <div class="cpk-ml__filter" @click=${this.onKindClick}>
3751
+ <button
3752
+ class="cpk-ml__filter-seg ${this.kind === "all" ? "cpk-ml__filter-seg--active" : ""}"
3753
+ data-kind="all"
3754
+ >
3755
+ All<span class="cpk-ml__filter-count">${this.searchFiltered.length}</span>
3756
+ </button>
3757
+ ${kinds.map(
3758
+ (k) => html`
3759
+ <button
3760
+ class="cpk-ml__filter-seg ${this.kind === k ? "cpk-ml__filter-seg--active" : ""}"
3761
+ data-kind="${k}"
3762
+ >
3763
+ ${k}<span class="cpk-ml__filter-count">${this.countForKind(k)}</span>
3764
+ </button>
3765
+ `,
3766
+ )}
3767
+ </div>
3768
+
3769
+ <!-- Memory list -->
3770
+ <div class="cpk-ml__list">
3771
+ ${filtered.map(
3772
+ (m) => html`
3773
+ <div class="cpk-ml__card">
3774
+ <div class="cpk-ml__card-badges">
3775
+ ${this.renderKindBadge(m.kind)}
3776
+ <span class="cpk-ml__scope-badge">${m.scope}</span>
3777
+ </div>
3778
+ <div class="cpk-ml__content">${m.content}</div>
3779
+ <div class="cpk-ml__footer">
3780
+ <span class="cpk-ml__footer-threads"
3781
+ >${m.sourceThreadIds.length} source thread${m.sourceThreadIds.length === 1 ? "" : "s"}</span
3782
+ >
3783
+ <span class="cpk-ml__footer-id">${this.shortId(m.id)}</span>
3784
+ </div>
3785
+ </div>
3786
+ `,
3787
+ )}
3788
+ ${filtered.length === 0 ? this.renderEmpty() : nothing}
3789
+ </div>
3790
+ </div>
3791
+ `;
3792
+ }
3793
+ }
3794
+
3421
3795
  // Backwards-compatible internal element name used by the full CopilotKit
3422
3796
  // Inspector shell. Keep this class thin so the public body remains the single
3423
3797
  // implementation.
@@ -3432,6 +3806,9 @@ if (!customElements.get(THREAD_INSPECTOR_TAG)) {
3432
3806
  if (!customElements.get("cpk-thread-details")) {
3433
3807
  customElements.define("cpk-thread-details", ɵCpkThreadDetails);
3434
3808
  }
3809
+ if (!customElements.get("cpk-memory-list")) {
3810
+ customElements.define("cpk-memory-list", CpkMemoryList);
3811
+ }
3435
3812
 
3436
3813
  export class WebInspectorElement extends LitElement {
3437
3814
  static properties = {
@@ -3442,6 +3819,25 @@ export class WebInspectorElement extends LitElement {
3442
3819
  private _core: CopilotKitCore | null = null;
3443
3820
  private coreSubscriber: CopilotKitCoreSubscriber | null = null;
3444
3821
  private coreUnsubscribe: (() => void) | null = null;
3822
+ private _memories: Memory[] = [];
3823
+ private _memoriesLoading = false;
3824
+ private _memoriesError: Error | null = null;
3825
+ private _memoriesAvailable = true;
3826
+ // Realtime-connection health, independent of `_memoriesAvailable` (the REST
3827
+ // list route). Drives the "live" indicator: only "connected" shows "live".
3828
+ private _memoriesRealtimeStatus: MemoryRealtimeStatus = "connecting";
3829
+ private _memoryUnsub: (() => void) | null = null;
3830
+ // Lazy-subscription guard. The memory store is created + started + opens
3831
+ // realtime the first time `core.getMemoryStore()` is called, so we defer
3832
+ // that call until the user actually activates the Memories tab (see
3833
+ // `ensureMemorySubscription`). This flag prevents a repeated tab click from
3834
+ // double-subscribing; `detachFromCore` resets it so a later attach + tab
3835
+ // activation re-subscribes cleanly.
3836
+ private _memorySubscribed = false;
3837
+ // True when the attached core predates `getMemoryStore` (older @copilotkit
3838
+ // SDK). Distinct from `_memoriesAvailable` (memory not enabled on an
3839
+ // otherwise-current deployment) so the teaser can show upgrade-the-SDK copy.
3840
+ private _memoryStoreUnsupported = false;
3445
3841
  private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null;
3446
3842
  private coreProperties: Readonly<Record<string, unknown>> = {};
3447
3843
  private lastCoreError: {
@@ -3619,6 +4015,7 @@ export class WebInspectorElement extends LitElement {
3619
4015
  label: "Threads",
3620
4016
  icon: "MessageSquare" as LucideIconName,
3621
4017
  },
4018
+ { key: "memories", label: "Learning", icon: "Brain" as LucideIconName },
3622
4019
  ];
3623
4020
  }
3624
4021
 
@@ -3660,6 +4057,17 @@ export class WebInspectorElement extends LitElement {
3660
4057
  };
3661
4058
  }
3662
4059
 
4060
+ private getMemoriesTelemetryProps(): InspectorMemoryTelemetryProps {
4061
+ const distinctId = !this.core?.telemetryDisabled
4062
+ ? getTelemetryDistinctIdForUrl()
4063
+ : null;
4064
+ return {
4065
+ posthog_distinct_id: distinctId ?? undefined,
4066
+ memory_count: this._memories.length,
4067
+ available: this._memoriesAvailable,
4068
+ };
4069
+ }
4070
+
3663
4071
  private getIntelligenceSignupUrl(): string {
3664
4072
  return this.appendRefParam(INTELLIGENCE_SIGNUP_URL, "cpk-inspector");
3665
4073
  }
@@ -3886,6 +4294,91 @@ export class WebInspectorElement extends LitElement {
3886
4294
  if (core.context) {
3887
4295
  this.contextStore = this.normalizeContextStore(core.context);
3888
4296
  }
4297
+
4298
+ // NOTE: the memory store is intentionally NOT touched here. Calling
4299
+ // `core.getMemoryStore()` lazily creates + `.start()`s the store and opens
4300
+ // a realtime connection, so merely attaching the inspector would spin up a
4301
+ // memory store + realtime even in apps that never use memory. Instead, the
4302
+ // store is created + subscribed on first Memories-tab activation via
4303
+ // `ensureMemorySubscription` (user-initiated, acceptable). Attaching the
4304
+ // inspector creates nothing.
4305
+ //
4306
+ // Exception: if the Memories tab is ALREADY the active tab when core is
4307
+ // wired (e.g. core attaches after `firstUpdated` restored a persisted
4308
+ // `selectedMenu: "memories"`), subscribe now so the live store + realtime
4309
+ // status paint instead of the stuck defaults. This preserves INSP-1 (no
4310
+ // unconditional subscribe on attach) because it is gated on the active tab,
4311
+ // and is safe to call when already subscribed — `ensureMemorySubscription`
4312
+ // early-returns on `_memorySubscribed`.
4313
+ if (this.selectedMenu === "memories") {
4314
+ this.ensureMemorySubscription();
4315
+ }
4316
+ }
4317
+
4318
+ /**
4319
+ * Lazily subscribes to the singleton memory store the first time the user
4320
+ * activates the Memories tab. This is deferred out of `attachToCore` because
4321
+ * `core.getMemoryStore()` is what creates + starts the store and opens
4322
+ * realtime — doing it on attach would start memory for apps that never use
4323
+ * it. Idempotent: repeated tab activations are guarded by
4324
+ * `_memorySubscribed`. On an older @copilotkit/core without `getMemoryStore`,
4325
+ * records the unsupported state so the teaser can guide an SDK upgrade.
4326
+ */
4327
+ private ensureMemorySubscription(): void {
4328
+ if (this._memorySubscribed) {
4329
+ return;
4330
+ }
4331
+ const core = this._core;
4332
+ if (!core) {
4333
+ return;
4334
+ }
4335
+
4336
+ // Guard like getThreadStores: older @copilotkit/core has no getMemoryStore.
4337
+ // When absent, flag the unsupported state so the teaser shows upgrade copy
4338
+ // instead of throwing a TypeError that would break the entire inspector.
4339
+ if (typeof core.getMemoryStore !== "function") {
4340
+ this._memoryStoreUnsupported = true;
4341
+ this._memoriesAvailable = false;
4342
+ this.requestUpdate();
4343
+ return;
4344
+ }
4345
+
4346
+ this._memorySubscribed = true;
4347
+ this._memoryStoreUnsupported = false;
4348
+
4349
+ // First touch of getMemoryStore() — creates + starts the store, opens realtime.
4350
+ const memoryStore = core.getMemoryStore();
4351
+ const ms = memoryStore.getState();
4352
+ this._memories = ɵselectMemories(ms);
4353
+ this._memoriesLoading = ɵselectMemoriesIsLoading(ms);
4354
+ this._memoriesError = ɵselectMemoriesError(ms);
4355
+ this._memoriesAvailable = ɵselectMemoriesAvailable(ms);
4356
+ this._memoriesRealtimeStatus = ɵselectMemoriesRealtimeStatus(ms);
4357
+ const memSubs = [
4358
+ memoryStore.select(ɵselectMemories).subscribe((v) => {
4359
+ this._memories = v;
4360
+ this.requestUpdate();
4361
+ }),
4362
+ memoryStore.select(ɵselectMemoriesIsLoading).subscribe((v) => {
4363
+ this._memoriesLoading = v;
4364
+ this.requestUpdate();
4365
+ }),
4366
+ memoryStore.select(ɵselectMemoriesError).subscribe((v) => {
4367
+ this._memoriesError = v;
4368
+ this.requestUpdate();
4369
+ }),
4370
+ memoryStore.select(ɵselectMemoriesAvailable).subscribe((v) => {
4371
+ this._memoriesAvailable = v;
4372
+ this.requestUpdate();
4373
+ }),
4374
+ // Group E — realtime connection health.
4375
+ memoryStore.select(ɵselectMemoriesRealtimeStatus).subscribe((v) => {
4376
+ this._memoriesRealtimeStatus = v;
4377
+ this.requestUpdate();
4378
+ }),
4379
+ ];
4380
+ this._memoryUnsub = () => memSubs.forEach((s) => s.unsubscribe());
4381
+ this.requestUpdate();
3889
4382
  }
3890
4383
 
3891
4384
  private detachFromCore(): void {
@@ -3893,6 +4386,17 @@ export class WebInspectorElement extends LitElement {
3893
4386
  this.coreUnsubscribe();
3894
4387
  this.coreUnsubscribe = null;
3895
4388
  }
4389
+ this._memoryUnsub?.();
4390
+ this._memoryUnsub = null;
4391
+ this._memories = [];
4392
+ this._memoriesLoading = false;
4393
+ this._memoriesError = null;
4394
+ this._memoriesAvailable = true;
4395
+ this._memoriesRealtimeStatus = "connecting";
4396
+ // Reset the lazy-subscription guards so a later attach + Memories-tab
4397
+ // activation re-subscribes (and re-evaluates SDK support) cleanly.
4398
+ this._memorySubscribed = false;
4399
+ this._memoryStoreUnsupported = false;
3896
4400
  this.coreSubscriber = null;
3897
4401
  this.runtimeStatus = null;
3898
4402
  this.lastCoreError = null;
@@ -5439,6 +5943,18 @@ ${argsString}</pre
5439
5943
 
5440
5944
  this.hydrateStateFromStorage();
5441
5945
 
5946
+ // `hydrateStateFromStorage` may have restored `selectedMenu: "memories"`.
5947
+ // The memory subscription is normally created on a Memories-tab CLICK via
5948
+ // `handleMenuSelect`, which never fires when the tab boots already active —
5949
+ // leaving the realtime indicator stuck on the default "connecting" and the
5950
+ // list empty until the user toggles tabs. Subscribe now when the Memories
5951
+ // tab is the active tab. Gated on the active tab to preserve INSP-1 (no
5952
+ // unconditional subscribe), and safe if core is not yet attached or already
5953
+ // subscribed — `ensureMemorySubscription` early-returns in both cases.
5954
+ if (this.selectedMenu === "memories") {
5955
+ this.ensureMemorySubscription();
5956
+ }
5957
+
5442
5958
  // Apply docking styles if open and docked (skip transition on initial load)
5443
5959
  if (this.isOpen && this.dockMode !== "floating") {
5444
5960
  this.applyDockStyles(true);
@@ -6896,6 +7412,10 @@ ${argsString}</pre
6896
7412
  return this.renderThreadsView();
6897
7413
  }
6898
7414
 
7415
+ if (this.selectedMenu === "memories") {
7416
+ return this.renderMemoriesView();
7417
+ }
7418
+
6899
7419
  if (this.selectedMenu === "settings") {
6900
7420
  return this.renderSettingsPanel();
6901
7421
  }
@@ -7343,6 +7863,346 @@ ${argsString}</pre
7343
7863
  `;
7344
7864
  }
7345
7865
 
7866
+ /**
7867
+ * Renders the realtime-connection indicator in the memory-store header.
7868
+ * Only `"connected"` shows the live (green-dot) state; `"connecting"` shows a
7869
+ * muted amber "reconnecting" and `"unavailable"` a muted grey "offline", so
7870
+ * the indicator never claims "live" over a frozen snapshot once the realtime
7871
+ * socket has permanently given up.
7872
+ */
7873
+ private renderMemoryRealtimeIndicator() {
7874
+ const status = this._memoriesRealtimeStatus;
7875
+ const connected = status === "connected";
7876
+ const dotColor = connected
7877
+ ? "#22c55e"
7878
+ : status === "connecting"
7879
+ ? "#f59e0b"
7880
+ : "#9ca3af";
7881
+ const label =
7882
+ status === "connected"
7883
+ ? "live"
7884
+ : status === "connecting"
7885
+ ? "reconnecting"
7886
+ : "offline";
7887
+ return html`
7888
+ <span
7889
+ style="
7890
+ display: inline-flex;
7891
+ align-items: center;
7892
+ gap: 4px;
7893
+ font-size: 10px;
7894
+ font-weight: 500;
7895
+ color: ${connected ? "#57575b" : "#838389"};
7896
+ "
7897
+ >
7898
+ <span
7899
+ style="
7900
+ display: inline-block;
7901
+ width: 6px;
7902
+ height: 6px;
7903
+ border-radius: 50%;
7904
+ background: ${dotColor};
7905
+ "
7906
+ ></span>
7907
+ ${label}
7908
+ </span>
7909
+ `;
7910
+ }
7911
+
7912
+ private renderMemoriesView() {
7913
+ // 1. Locked teaser — intelligence not configured or memories not available.
7914
+ if (!this.core?.intelligence || !this._memoriesAvailable) {
7915
+ return html`
7916
+ <div
7917
+ style="
7918
+ position: relative;
7919
+ height: 100%;
7920
+ display: flex;
7921
+ align-items: center;
7922
+ justify-content: center;
7923
+ padding: 32px;
7924
+ overflow: hidden;
7925
+ background: #ffffff;
7926
+ "
7927
+ >
7928
+ ${this.renderThreadsLockedBackgroundMockup()}
7929
+ <div
7930
+ aria-hidden="true"
7931
+ style="
7932
+ position: absolute;
7933
+ inset: 0;
7934
+ pointer-events: none;
7935
+ background:
7936
+ 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%);
7937
+ "
7938
+ ></div>
7939
+ <div
7940
+ style="
7941
+ position: relative;
7942
+ z-index: 1;
7943
+ max-width: 440px;
7944
+ text-align: center;
7945
+ color: #57575b;
7946
+ "
7947
+ >
7948
+ <div
7949
+ aria-hidden="true"
7950
+ style="
7951
+ margin: 0 auto 18px;
7952
+ display: flex;
7953
+ justify-content: center;
7954
+ "
7955
+ >
7956
+ <div
7957
+ style="
7958
+ display: flex;
7959
+ height: 44px;
7960
+ width: 44px;
7961
+ align-items: center;
7962
+ justify-content: center;
7963
+ border: 1px solid #dfd6fb;
7964
+ border-radius: 8px;
7965
+ background: #eee6fe;
7966
+ color: #57575b;
7967
+ box-shadow: 0 8px 18px rgba(87, 87, 91, 0.14);
7968
+ "
7969
+ >
7970
+ ${this.renderIcon("Lock")}
7971
+ </div>
7972
+ </div>
7973
+ <h2
7974
+ style="
7975
+ margin: 0 0 8px;
7976
+ font-size: 16px;
7977
+ line-height: 1.35;
7978
+ font-weight: 600;
7979
+ color: #010507;
7980
+ "
7981
+ >
7982
+ Long-term memory
7983
+ </h2>
7984
+ <p
7985
+ style="
7986
+ margin: 0 auto 18px;
7987
+ max-width: 380px;
7988
+ font-size: 13px;
7989
+ line-height: 1.55;
7990
+ color: #57575b;
7991
+ "
7992
+ >
7993
+ ${
7994
+ this._memoryStoreUnsupported
7995
+ ? "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."
7996
+ : "Long-term memory isn't enabled on this deployment."
7997
+ }
7998
+ </p>
7999
+ <div
8000
+ style="
8001
+ display: flex;
8002
+ flex-wrap: wrap;
8003
+ justify-content: center;
8004
+ gap: 8px;
8005
+ "
8006
+ >
8007
+ <a
8008
+ href=${this.getTalkToEngineerUrl()}
8009
+ target="_blank"
8010
+ rel="noopener"
8011
+ style="
8012
+ display: inline-flex;
8013
+ min-height: 34px;
8014
+ align-items: center;
8015
+ justify-content: center;
8016
+ gap: 6px;
8017
+ border-radius: 6px;
8018
+ background: #010507;
8019
+ padding: 8px 12px;
8020
+ font-size: 12px;
8021
+ font-weight: 600;
8022
+ color: #ffffff;
8023
+ text-decoration: none;
8024
+ "
8025
+ @click=${this.handleThreadsTalkToEngineerClick}
8026
+ >
8027
+ Talk to an Engineer
8028
+ </a>
8029
+ <a
8030
+ href=${this.getIntelligenceSignupUrl()}
8031
+ target="_blank"
8032
+ rel="noopener"
8033
+ style="
8034
+ display: inline-flex;
8035
+ min-height: 34px;
8036
+ align-items: center;
8037
+ justify-content: center;
8038
+ gap: 6px;
8039
+ border-radius: 6px;
8040
+ border: 1px solid #dbdbe5;
8041
+ background: #ffffff;
8042
+ padding: 8px 12px;
8043
+ font-size: 12px;
8044
+ font-weight: 600;
8045
+ color: #57575b;
8046
+ text-decoration: none;
8047
+ "
8048
+ @click=${this.handleThreadsIntelligenceSignupClick}
8049
+ >
8050
+ Sign up for Intelligence
8051
+ </a>
8052
+ </div>
8053
+ </div>
8054
+ </div>
8055
+ `;
8056
+ }
8057
+
8058
+ // 2. Full-screen error — only for a snapshot-LOAD failure (no memories
8059
+ // loaded). A mutation failure that arrives while memories are already on
8060
+ // screen must NOT blank the list; it is surfaced inline below (step 4).
8061
+ if (this._memoriesError && this._memories.length === 0) {
8062
+ return html`
8063
+ <div
8064
+ style="
8065
+ display: flex;
8066
+ height: 100%;
8067
+ flex-direction: column;
8068
+ align-items: center;
8069
+ justify-content: center;
8070
+ gap: 8px;
8071
+ color: #838389;
8072
+ "
8073
+ >
8074
+ <svg
8075
+ width="24"
8076
+ height="24"
8077
+ viewBox="0 0 24 24"
8078
+ fill="none"
8079
+ stroke="#c0333a"
8080
+ stroke-width="1.5"
8081
+ stroke-linecap="round"
8082
+ stroke-linejoin="round"
8083
+ >
8084
+ <circle cx="12" cy="12" r="10" />
8085
+ <line x1="12" y1="8" x2="12" y2="12" />
8086
+ <line x1="12" y1="16" x2="12.01" y2="16" />
8087
+ </svg>
8088
+ <span style="font-size: 13px; color: #c0333a;">
8089
+ Failed to load memories
8090
+ </span>
8091
+ <span
8092
+ style="
8093
+ max-width: 320px;
8094
+ text-align: center;
8095
+ font-size: 11px;
8096
+ line-height: 1.5;
8097
+ color: #c0333a;
8098
+ "
8099
+ >
8100
+ ${this._memoriesError.message}
8101
+ </span>
8102
+ </div>
8103
+ `;
8104
+ }
8105
+
8106
+ // 3. Initial loading placeholder (no memories yet to show behind it).
8107
+ if (this._memoriesLoading && this._memories.length === 0) {
8108
+ return html`
8109
+ <div
8110
+ style="
8111
+ display: flex;
8112
+ height: 100%;
8113
+ flex-direction: column;
8114
+ align-items: center;
8115
+ justify-content: center;
8116
+ gap: 8px;
8117
+ color: #838389;
8118
+ "
8119
+ >
8120
+ <svg
8121
+ width="24"
8122
+ height="24"
8123
+ viewBox="0 0 24 24"
8124
+ fill="none"
8125
+ stroke="#c0c0c8"
8126
+ stroke-width="1.5"
8127
+ stroke-linecap="round"
8128
+ stroke-linejoin="round"
8129
+ >
8130
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
8131
+ </svg>
8132
+ <span style="font-size: 13px">Loading memories…</span>
8133
+ </div>
8134
+ `;
8135
+ }
8136
+
8137
+ // 4. Content — header + memory list.
8138
+ return html`
8139
+ <div style="display:flex;height:100%;overflow:hidden;flex-direction:column;">
8140
+ <div class="cpk-section-header" style="display:flex;align-items:center;justify-content:space-between;">
8141
+ <h4>Learning</h4>
8142
+ <div style="display:flex;align-items:center;gap:6px;">
8143
+ ${this.renderMemoryRealtimeIndicator()}
8144
+ <span
8145
+ style="
8146
+ font-size: 11px;
8147
+ font-weight: 500;
8148
+ color: #57575b;
8149
+ background: rgba(0,0,0,0.07);
8150
+ border-radius: 9999px;
8151
+ padding: 1px 7px;
8152
+ "
8153
+ >
8154
+ ${this._memories.length}
8155
+ </span>
8156
+ </div>
8157
+ </div>
8158
+ ${
8159
+ this._memoriesError
8160
+ ? html`
8161
+ <div
8162
+ role="alert"
8163
+ style="
8164
+ display: flex;
8165
+ align-items: flex-start;
8166
+ gap: 8px;
8167
+ flex-shrink: 0;
8168
+ border-bottom: 1px solid #f1c7c9;
8169
+ background: #fdf3f3;
8170
+ padding: 8px 12px;
8171
+ color: #c0333a;
8172
+ font-size: 12px;
8173
+ line-height: 1.45;
8174
+ "
8175
+ >
8176
+ <svg
8177
+ width="16"
8178
+ height="16"
8179
+ viewBox="0 0 24 24"
8180
+ fill="none"
8181
+ stroke="#c0333a"
8182
+ stroke-width="1.5"
8183
+ stroke-linecap="round"
8184
+ stroke-linejoin="round"
8185
+ style="flex-shrink:0;margin-top:1px;"
8186
+ >
8187
+ <circle cx="12" cy="12" r="10" />
8188
+ <line x1="12" y1="8" x2="12" y2="12" />
8189
+ <line x1="12" y1="16" x2="12.01" y2="16" />
8190
+ </svg>
8191
+ <span>Action failed: ${this._memoriesError.message}</span>
8192
+ </div>
8193
+ `
8194
+ : nothing
8195
+ }
8196
+ <div style="flex:1;min-height:0;overflow:hidden;">
8197
+ <cpk-memory-list
8198
+ style="height:100%;"
8199
+ .memories=${this._memories}
8200
+ ></cpk-memory-list>
8201
+ </div>
8202
+ </div>
8203
+ `;
8204
+ }
8205
+
7346
8206
  private renderThreadsView() {
7347
8207
  if (!this.areThreadEndpointsAvailable()) {
7348
8208
  return this.renderThreadsLockedView();
@@ -8162,6 +9022,16 @@ ${prettyEvent}</pre
8162
9022
  this.autoSelectLatestThread();
8163
9023
  }
8164
9024
 
9025
+ if (key === "memories") {
9026
+ // Lazily create + subscribe to the memory store on first activation. This
9027
+ // is the only place that touches getMemoryStore(), so the store/realtime
9028
+ // are never started just by attaching the inspector.
9029
+ this.ensureMemorySubscription();
9030
+ if (previousMenu !== "memories" && !this.core?.telemetryDisabled) {
9031
+ trackMemoriesTabClicked(this.getMemoriesTelemetryProps());
9032
+ }
9033
+ }
9034
+
8165
9035
  if (key === "ag-ui-events" || key === "agents") {
8166
9036
  requestAnimationFrame(() => {
8167
9037
  const scroller = this.shadowRoot?.getElementById("cpk-main-scroll");