@copilotkit/web-inspector 1.64.1 → 1.64.2-canary.1785366148

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
@@ -84,16 +84,26 @@ import type {
84
84
  } from "./lib/telemetry.js";
85
85
 
86
86
  export type { Anchor } from "./lib/types.js";
87
+ export { buildCapabilityRows as ɵbuildCapabilityRows };
88
+ export type { CapabilityToolRow as ɵCapabilityToolRow };
87
89
 
88
90
  export const WEB_INSPECTOR_TAG = "cpk-web-inspector" as const;
89
91
  export const THREAD_INSPECTOR_TAG = "cpk-thread-inspector" as const;
90
92
 
93
+ /**
94
+ * User-facing label for the memory surface (nav item + view header). The menu
95
+ * KEY stays "memories" for persistence/telemetry stability; only the label
96
+ * changed from "Learning" to "Memory".
97
+ */
98
+ const MEMORY_VIEW_LABEL = "Memory";
99
+
91
100
  type LucideIconName = keyof typeof icons;
92
101
 
93
102
  type MenuKey =
94
103
  | "ag-ui-events"
95
104
  | "agents"
96
105
  | "frontend-tools"
106
+ | "capabilities"
97
107
  | "agent-context"
98
108
  | "threads"
99
109
  | "memories"
@@ -122,6 +132,11 @@ const INTELLIGENCE_SIGNUP_URL = "https://go.copilotkit.ai/intelligence-signup";
122
132
  const THREADS_INTELLIGENCE_SIGNIN_URL =
123
133
  "https://dashboard.operations.copilotkit.ai/sign-in";
124
134
  const TALK_TO_ENGINEER_URL = "https://www.copilotkit.ai/talk-to-an-engineer";
135
+ // Label for the Capabilities tab (client-authoritative dev experimentation
136
+ // surface: toggle frontend tools + A2UI catalog components on/off, enforced
137
+ // immediately via core.setToolEnabled / core.setCatalogComponentEnabled).
138
+ // Renameable — keep the display string in this one place.
139
+ const CAPABILITIES_TAB_LABEL = "Capabilities";
125
140
  const THREADS_DOCS_URL = "https://docs.copilotkit.ai/threads";
126
141
  const SELF_HOSTED_INTELLIGENCE_URL =
127
142
  "https://docs.copilotkit.ai/premium/self-hosting";
@@ -220,6 +235,55 @@ type InspectorToolDefinition = {
220
235
  type: "handler" | "renderer";
221
236
  };
222
237
 
238
+ // ─── Capabilities tab view-models ────────────────────────────────────────────
239
+ // A single toggle row. `key` is the stable identity used as a Lit list key; for
240
+ // tools it is `${agentId}:${name}` (agentId "" for global tools), for catalog
241
+ // components it is the component name.
242
+ type CapabilityToolRow = {
243
+ key: string;
244
+ name: string;
245
+ description?: string;
246
+ agentId?: string;
247
+ enabled: boolean;
248
+ };
249
+
250
+ // Minimal structural view of CopilotKitCore that the pure helper needs, so
251
+ // buildCapabilityRows is trivially unit-testable with a plain object. Method
252
+ // names MUST match the A1 contract exactly.
253
+ type CapabilityToolSource = {
254
+ tools?: ReadonlyArray<{
255
+ name: string;
256
+ description?: string;
257
+ agentId?: string;
258
+ }>;
259
+ isToolEnabled: (name: string, agentId?: string) => boolean;
260
+ };
261
+
262
+ /**
263
+ * Map core.tools (the registry INCLUDING disabled tools) into Capabilities-tab
264
+ * frontend-tool rows. Pure: no DOM, no `this`. Reads current on/off state from
265
+ * core.isToolEnabled(name, agentId?) per the A1 contract.
266
+ */
267
+ function buildCapabilityRows(core: CapabilityToolSource): CapabilityToolRow[] {
268
+ const rows: CapabilityToolRow[] = [];
269
+ for (const tool of core.tools ?? []) {
270
+ const agentId = tool.agentId ?? "";
271
+ const key = `${agentId}:${tool.name}`;
272
+ rows.push({
273
+ key,
274
+ name: tool.name,
275
+ description: tool.description,
276
+ agentId: tool.agentId,
277
+ enabled: core.isToolEnabled(tool.name, tool.agentId),
278
+ });
279
+ }
280
+ return rows.sort((a, b) => {
281
+ const agentCompare = (a.agentId ?? "").localeCompare(b.agentId ?? "");
282
+ if (agentCompare !== 0) return agentCompare;
283
+ return a.name.localeCompare(b.name);
284
+ });
285
+ }
286
+
223
287
  type InspectorEvent = {
224
288
  id: string;
225
289
  agentId: string;
@@ -3700,6 +3764,51 @@ ${unsafeHTML(highlightedJson(stateValue))}</pre
3700
3764
  }
3701
3765
  }
3702
3766
 
3767
+ // ─── memory recall relevance helpers ─────────────────────────────────────────
3768
+
3769
+ /**
3770
+ * Normalizes a memory's raw recall score to a 0..1 relevance ratio relative to
3771
+ * the strongest result in the same result set. Recall scores are RRF scores
3772
+ * (relative), so a bar is only meaningful against the set max. Returns
3773
+ * `undefined` when no meaningful ranking exists (empty set, non-positive max,
3774
+ * missing score) so the caller renders no bar.
3775
+ */
3776
+ function normalizeRelevance(
3777
+ score: number | undefined,
3778
+ maxScore: number,
3779
+ ): number | undefined {
3780
+ if (maxScore <= 0) return undefined;
3781
+ if (score === undefined || !Number.isFinite(score)) return undefined;
3782
+ const ratio = score / maxScore;
3783
+ if (ratio <= 0) return 0;
3784
+ return ratio > 1 ? 1 : ratio;
3785
+ }
3786
+
3787
+ /** Largest finite `score` across a result set, or 0 when none present. */
3788
+ function maxRecallScore(memories: readonly Memory[]): number {
3789
+ let max = 0;
3790
+ for (const m of memories) {
3791
+ const s = m.score;
3792
+ if (typeof s === "number" && Number.isFinite(s) && s > max) max = s;
3793
+ }
3794
+ return max;
3795
+ }
3796
+
3797
+ /**
3798
+ * Percent width for a relevance bar. Mirrors the banking reference
3799
+ * (`max(6, round(rel*100))%`) so a matched-but-weak result still shows a sliver.
3800
+ * Returns a whole number in [6, 100].
3801
+ */
3802
+ function relevanceBarWidth(relevance: number): number {
3803
+ return Math.max(6, Math.min(100, Math.round(relevance * 100)));
3804
+ }
3805
+
3806
+ export {
3807
+ normalizeRelevance as ɵnormalizeRelevance,
3808
+ maxRecallScore as ɵmaxRecallScore,
3809
+ relevanceBarWidth as ɵrelevanceBarWidth,
3810
+ };
3811
+
3703
3812
  // ─── cpk-memory-list ─────────────────────────────────────────────────────────
3704
3813
 
3705
3814
  /** Memory kind values including the "all" sentinel used by the filter UI. */
@@ -3708,12 +3817,24 @@ type MemoryKindFilter = "all" | "topical" | "episodic" | "operational";
3708
3817
  class CpkMemoryList extends LitElement {
3709
3818
  static properties = {
3710
3819
  memories: { attribute: false },
3820
+ recallResults: { attribute: false },
3821
+ recallLoading: { attribute: false },
3822
+ recallError: { attribute: false },
3823
+ recallQueryText: { attribute: false },
3711
3824
  search: { state: true },
3712
3825
  kind: { state: true },
3713
3826
  };
3714
3827
 
3715
3828
  /** Ordered (newest-first) list of memories supplied by the parent. */
3716
3829
  memories: Memory[] = [];
3830
+ /** Semantic-recall results. `null` = no recall run (section hidden); `[]` = ran, no matches. */
3831
+ recallResults: Memory[] | null = null;
3832
+ /** True while a recall request is in flight. */
3833
+ recallLoading = false;
3834
+ /** Error message from the most recent recall attempt, or null. */
3835
+ recallError: string | null = null;
3836
+ /** The recall input text (owned by the parent). */
3837
+ recallQueryText = "";
3717
3838
  private search = "";
3718
3839
  private kind: MemoryKindFilter = "all";
3719
3840
 
@@ -3915,6 +4036,117 @@ class CpkMemoryList extends LitElement {
3915
4036
  .cpk-ml__empty-icon {
3916
4037
  color: #c0c0c8;
3917
4038
  }
4039
+
4040
+ /* ── Recall ── */
4041
+ .cpk-ml__recall {
4042
+ display: flex;
4043
+ gap: 6px;
4044
+ padding: 10px 12px;
4045
+ border-bottom: 1px solid #dbdbe5;
4046
+ flex-shrink: 0;
4047
+ }
4048
+ .cpk-ml__recall-input {
4049
+ flex: 1;
4050
+ box-sizing: border-box;
4051
+ font-family: "Plus Jakarta Sans", sans-serif;
4052
+ font-size: 12px;
4053
+ padding: 7px 10px;
4054
+ border-radius: 6px;
4055
+ border: 1px solid #dbdbe5;
4056
+ background: #fff;
4057
+ color: #010507;
4058
+ outline: none;
4059
+ transition: border-color 0.15s;
4060
+ }
4061
+ .cpk-ml__recall-input:focus {
4062
+ border-color: #bec2ff;
4063
+ }
4064
+ .cpk-ml__recall-btn {
4065
+ font-family: "Plus Jakarta Sans", sans-serif;
4066
+ font-size: 12px;
4067
+ font-weight: 500;
4068
+ padding: 7px 12px;
4069
+ border-radius: 6px;
4070
+ border: 1px solid #dbdbe5;
4071
+ background: #fff;
4072
+ color: #010507;
4073
+ cursor: pointer;
4074
+ transition: background 0.1s;
4075
+ }
4076
+ .cpk-ml__recall-btn:hover:not(:disabled) {
4077
+ background: #f0f0f5;
4078
+ }
4079
+ .cpk-ml__recall-btn:disabled {
4080
+ opacity: 0.4;
4081
+ cursor: default;
4082
+ }
4083
+ .cpk-ml__recall-section {
4084
+ flex-shrink: 0;
4085
+ max-height: 45%;
4086
+ overflow-y: auto;
4087
+ padding: 8px 12px;
4088
+ border-bottom: 1px solid #dbdbe5;
4089
+ background: #fbfbfd;
4090
+ display: flex;
4091
+ flex-direction: column;
4092
+ gap: 8px;
4093
+ }
4094
+ .cpk-ml__recall-header {
4095
+ display: flex;
4096
+ align-items: center;
4097
+ justify-content: space-between;
4098
+ gap: 8px;
4099
+ }
4100
+ .cpk-ml__recall-title {
4101
+ font-family: "Plus Jakarta Sans", sans-serif;
4102
+ font-size: 12px;
4103
+ font-weight: 600;
4104
+ color: #010507;
4105
+ }
4106
+ .cpk-ml__recall-clear {
4107
+ font-family: "Plus Jakarta Sans", sans-serif;
4108
+ font-size: 10px;
4109
+ color: #838389;
4110
+ background: none;
4111
+ border: none;
4112
+ cursor: pointer;
4113
+ padding: 0;
4114
+ }
4115
+ .cpk-ml__recall-clear:hover {
4116
+ color: #010507;
4117
+ }
4118
+ .cpk-ml__recall-msg {
4119
+ font-size: 11px;
4120
+ color: #838389;
4121
+ line-height: 1.45;
4122
+ }
4123
+ .cpk-ml__recall-msg--error {
4124
+ color: #c0333a;
4125
+ }
4126
+
4127
+ /* ── Relevance bar ── */
4128
+ .cpk-ml__relevance {
4129
+ height: 4px;
4130
+ width: 100%;
4131
+ overflow: hidden;
4132
+ border-radius: 9999px;
4133
+ background: #f0f0f5;
4134
+ }
4135
+ .cpk-ml__relevance-fill {
4136
+ height: 100%;
4137
+ border-radius: 9999px;
4138
+ background: #6366f1;
4139
+ }
4140
+
4141
+ /* ── Scope badge variants ── */
4142
+ .cpk-ml__scope-badge--user {
4143
+ background: #f0f0f5;
4144
+ color: #838389;
4145
+ }
4146
+ .cpk-ml__scope-badge--project {
4147
+ background: #fef3c7;
4148
+ color: #92660c;
4149
+ }
3918
4150
  `;
3919
4151
 
3920
4152
  /** Memories that pass the current text search (before kind filter). */
@@ -3959,6 +4191,135 @@ class CpkMemoryList extends LitElement {
3959
4191
  >`;
3960
4192
  }
3961
4193
 
4194
+ private renderScopeBadge(scope: string): TemplateResult {
4195
+ const variant = scope === "project" ? "project" : "user";
4196
+ return html`<span
4197
+ class="cpk-ml__scope-badge cpk-ml__scope-badge--${variant}"
4198
+ >${scope}</span
4199
+ >`;
4200
+ }
4201
+
4202
+ /**
4203
+ * Renders one memory card. `relevance` (0..1) is supplied only for recall
4204
+ * results — when present a relevance bar is drawn; the full list omits it.
4205
+ */
4206
+ private renderCard(m: Memory, relevance?: number): TemplateResult {
4207
+ const threads = m.sourceThreadIds.length;
4208
+ return html`
4209
+ <div class="cpk-ml__card">
4210
+ <div class="cpk-ml__card-badges">
4211
+ ${this.renderKindBadge(m.kind)}${this.renderScopeBadge(m.scope)}
4212
+ </div>
4213
+ <div class="cpk-ml__content">${m.content}</div>
4214
+ ${
4215
+ relevance !== undefined
4216
+ ? html`<div class="cpk-ml__relevance">
4217
+ <div
4218
+ class="cpk-ml__relevance-fill"
4219
+ style="width:${relevanceBarWidth(relevance)}%;"
4220
+ ></div>
4221
+ </div>`
4222
+ : nothing
4223
+ }
4224
+ <div class="cpk-ml__footer">
4225
+ <span class="cpk-ml__footer-threads"
4226
+ >${threads} source thread${threads === 1 ? "" : "s"}</span
4227
+ >
4228
+ <span class="cpk-ml__footer-id">${this.shortId(m.id)}</span>
4229
+ </div>
4230
+ </div>
4231
+ `;
4232
+ }
4233
+
4234
+ private onRecallInput = (event: Event): void => {
4235
+ const value = (event.target as HTMLInputElement).value;
4236
+ this.recallQueryText = value;
4237
+ this.dispatchEvent(
4238
+ new CustomEvent<string>("recallQueryChanged", {
4239
+ detail: value,
4240
+ bubbles: true,
4241
+ composed: true,
4242
+ }),
4243
+ );
4244
+ };
4245
+
4246
+ private onRecallSubmit = (event: Event): void => {
4247
+ event.preventDefault();
4248
+ const query = this.recallQueryText.trim();
4249
+ if (query.length === 0 || this.recallLoading) return;
4250
+ this.dispatchEvent(
4251
+ new CustomEvent<string>("recallSubmitted", {
4252
+ detail: query,
4253
+ bubbles: true,
4254
+ composed: true,
4255
+ }),
4256
+ );
4257
+ };
4258
+
4259
+ private onRecallClear = (): void => {
4260
+ this.dispatchEvent(
4261
+ new CustomEvent("recallCleared", { bubbles: true, composed: true }),
4262
+ );
4263
+ };
4264
+
4265
+ private renderRecallForm(): TemplateResult {
4266
+ const disabled =
4267
+ this.recallLoading || this.recallQueryText.trim().length === 0;
4268
+ return html`
4269
+ <form class="cpk-ml__recall" @submit=${this.onRecallSubmit}>
4270
+ <input
4271
+ type="text"
4272
+ placeholder="Recall by meaning…"
4273
+ aria-label="Recall memories by meaning"
4274
+ class="cpk-ml__recall-input"
4275
+ .value=${this.recallQueryText}
4276
+ @input=${this.onRecallInput}
4277
+ />
4278
+ <button type="submit" class="cpk-ml__recall-btn" ?disabled=${disabled}>
4279
+ ${this.recallLoading ? "…" : "Recall"}
4280
+ </button>
4281
+ </form>
4282
+ `;
4283
+ }
4284
+
4285
+ private renderRecallSection(): TemplateResult {
4286
+ const results = this.recallResults;
4287
+ if (results === null) return html``;
4288
+ const max = maxRecallScore(results);
4289
+ return html`
4290
+ <section
4291
+ class="cpk-ml__recall-section"
4292
+ aria-label="Semantic recall results"
4293
+ >
4294
+ <div class="cpk-ml__recall-header">
4295
+ <span class="cpk-ml__recall-title"
4296
+ >Semantic recall (${results.length})</span
4297
+ >
4298
+ <button
4299
+ type="button"
4300
+ class="cpk-ml__recall-clear"
4301
+ @click=${this.onRecallClear}
4302
+ >
4303
+ Clear
4304
+ </button>
4305
+ </div>
4306
+ ${
4307
+ this.recallError
4308
+ ? html`<p class="cpk-ml__recall-msg cpk-ml__recall-msg--error">
4309
+ Recall failed: ${this.recallError}
4310
+ </p>`
4311
+ : results.length === 0
4312
+ ? html`
4313
+ <p class="cpk-ml__recall-msg">No memories matched that query.</p>
4314
+ `
4315
+ : results.map((m) =>
4316
+ this.renderCard(m, normalizeRelevance(m.score, max)),
4317
+ )
4318
+ }
4319
+ </section>
4320
+ `;
4321
+ }
4322
+
3962
4323
  private renderEmpty(): TemplateResult {
3963
4324
  const q = this.search.trim();
3964
4325
  if (this.memories.length === 0) {
@@ -4005,6 +4366,9 @@ class CpkMemoryList extends LitElement {
4005
4366
 
4006
4367
  return html`
4007
4368
  <div class="cpk-ml">
4369
+ <!-- Semantic recall -->
4370
+ ${this.renderRecallForm()} ${this.renderRecallSection()}
4371
+
4008
4372
  <!-- Search -->
4009
4373
  <div class="cpk-ml__search">
4010
4374
  <input
@@ -4038,23 +4402,7 @@ class CpkMemoryList extends LitElement {
4038
4402
 
4039
4403
  <!-- Memory list -->
4040
4404
  <div class="cpk-ml__list">
4041
- ${filtered.map(
4042
- (m) => html`
4043
- <div class="cpk-ml__card">
4044
- <div class="cpk-ml__card-badges">
4045
- ${this.renderKindBadge(m.kind)}
4046
- <span class="cpk-ml__scope-badge">${m.scope}</span>
4047
- </div>
4048
- <div class="cpk-ml__content">${m.content}</div>
4049
- <div class="cpk-ml__footer">
4050
- <span class="cpk-ml__footer-threads"
4051
- >${m.sourceThreadIds.length} source thread${m.sourceThreadIds.length === 1 ? "" : "s"}</span
4052
- >
4053
- <span class="cpk-ml__footer-id">${this.shortId(m.id)}</span>
4054
- </div>
4055
- </div>
4056
- `,
4057
- )}
4405
+ ${filtered.map((m) => this.renderCard(m))}
4058
4406
  ${filtered.length === 0 ? this.renderEmpty() : nothing}
4059
4407
  </div>
4060
4408
  </div>
@@ -4084,6 +4432,7 @@ export class WebInspectorElement extends LitElement {
4084
4432
  static properties = {
4085
4433
  core: { attribute: false },
4086
4434
  autoAttachCore: { type: Boolean, attribute: "auto-attach-core" },
4435
+ _capabilitiesVersion: { state: true },
4087
4436
  } as const;
4088
4437
 
4089
4438
  private _core: CopilotKitCore | null = null;
@@ -4108,6 +4457,15 @@ export class WebInspectorElement extends LitElement {
4108
4457
  // SDK). Distinct from `_memoriesAvailable` (memory not enabled on an
4109
4458
  // otherwise-current deployment) so the teaser can show upgrade-the-SDK copy.
4110
4459
  private _memoryStoreUnsupported = false;
4460
+ // ── Semantic recall (B3) ──────────────────────────────────────────────
4461
+ // `null` = no recall run yet (section hidden). `[]` = ran, no matches.
4462
+ private _recallResults: Memory[] | null = null;
4463
+ private _recallLoading = false;
4464
+ private _recallError: string | null = null;
4465
+ private _recallQuery = "";
4466
+ // Monotonic token so a slow recall resolving after a newer one / Clear /
4467
+ // detach is ignored — last-write-wins without racing state.
4468
+ private _recallSeq = 0;
4111
4469
  private runtimeStatus: CopilotKitCoreRuntimeConnectionStatus | null = null;
4112
4470
  private coreProperties: Readonly<Record<string, unknown>> = {};
4113
4471
  private lastCoreError: {
@@ -4167,6 +4525,12 @@ export class WebInspectorElement extends LitElement {
4167
4525
  private attemptedAutoAttach = false;
4168
4526
  private cachedTools: InspectorToolDefinition[] = [];
4169
4527
  private toolSignature = "";
4528
+ // Bumped after every core.setToolEnabled / core.setCatalogComponentEnabled
4529
+ // call so the Capabilities tab re-paints from the fresh isToolEnabled /
4530
+ // isCatalogComponentEnabled getters. There is no core subscriber for
4531
+ // enablement changes — the inspector itself drives the toggle, so we force
4532
+ // the re-render locally.
4533
+ private _capabilitiesVersion = 0;
4170
4534
  private eventFilterText = "";
4171
4535
  private eventTypeFilter: InspectorAgentEventType | "all" = "all";
4172
4536
  // Column widths for the AG-UI events table (agent, time, event-type; last col is auto)
@@ -4272,6 +4636,8 @@ export class WebInspectorElement extends LitElement {
4272
4636
 
4273
4637
  private get menuItems(): MenuItem[] {
4274
4638
  const hasFrontendTools = (this._core?.tools?.length ?? 0) > 0;
4639
+ const hasCatalog = (this._core?.catalogComponents?.length ?? 0) > 0;
4640
+ const hasCapabilities = hasFrontendTools || hasCatalog;
4275
4641
  return [
4276
4642
  {
4277
4643
  key: "ag-ui-events",
@@ -4288,6 +4654,15 @@ export class WebInspectorElement extends LitElement {
4288
4654
  },
4289
4655
  ]
4290
4656
  : []),
4657
+ ...(hasCapabilities
4658
+ ? [
4659
+ {
4660
+ key: "capabilities" as const,
4661
+ label: CAPABILITIES_TAB_LABEL,
4662
+ icon: "SlidersHorizontal" as LucideIconName,
4663
+ },
4664
+ ]
4665
+ : []),
4291
4666
  {
4292
4667
  key: "agent-context",
4293
4668
  label: "Context",
@@ -4298,7 +4673,11 @@ export class WebInspectorElement extends LitElement {
4298
4673
  label: "Threads",
4299
4674
  icon: "MessageSquare" as LucideIconName,
4300
4675
  },
4301
- { key: "memories", label: "Learning", icon: "Brain" as LucideIconName },
4676
+ {
4677
+ key: "memories",
4678
+ label: MEMORY_VIEW_LABEL,
4679
+ icon: "Brain" as LucideIconName,
4680
+ },
4302
4681
  ];
4303
4682
  }
4304
4683
 
@@ -4712,6 +5091,58 @@ export class WebInspectorElement extends LitElement {
4712
5091
  this.requestUpdate();
4713
5092
  }
4714
5093
 
5094
+ /**
5095
+ * Runs a semantic recall via the memory store (`core.getMemoryStore().recall`,
5096
+ * from B2) and stores ranked results. Guarded by a monotonic sequence token
5097
+ * so a stale request cannot overwrite a newer result / Clear / detach. Only
5098
+ * reachable from the Intelligence-gated memory view, so it inherits the gate.
5099
+ */
5100
+ private runRecall(query: string): void {
5101
+ const trimmed = query.trim();
5102
+ if (trimmed.length === 0) return;
5103
+ const store = this._core?.getMemoryStore?.();
5104
+ if (!store || typeof store.recall !== "function") {
5105
+ this._recallResults = [];
5106
+ this._recallError = "Recall is not supported by this SDK version.";
5107
+ this._recallLoading = false;
5108
+ this.requestUpdate();
5109
+ return;
5110
+ }
5111
+
5112
+ const seq = ++this._recallSeq;
5113
+ this._recallLoading = true;
5114
+ this._recallError = null;
5115
+ this.requestUpdate();
5116
+
5117
+ store
5118
+ .recall(trimmed)
5119
+ .then((results) => {
5120
+ if (seq !== this._recallSeq) return;
5121
+ this._recallResults = results;
5122
+ this._recallError = null;
5123
+ this._recallLoading = false;
5124
+ this.requestUpdate();
5125
+ })
5126
+ .catch((error: unknown) => {
5127
+ if (seq !== this._recallSeq) return;
5128
+ this._recallResults = [];
5129
+ this._recallError =
5130
+ error instanceof Error ? error.message : "unknown error";
5131
+ this._recallLoading = false;
5132
+ this.requestUpdate();
5133
+ });
5134
+ }
5135
+
5136
+ /** Clears recall results/section and cancels any in-flight recall. */
5137
+ private clearRecall(): void {
5138
+ this._recallSeq += 1;
5139
+ this._recallResults = null;
5140
+ this._recallError = null;
5141
+ this._recallLoading = false;
5142
+ this._recallQuery = "";
5143
+ this.requestUpdate();
5144
+ }
5145
+
4715
5146
  private detachFromCore(): void {
4716
5147
  if (this.coreUnsubscribe) {
4717
5148
  this.coreUnsubscribe();
@@ -4728,6 +5159,13 @@ export class WebInspectorElement extends LitElement {
4728
5159
  // activation re-subscribes (and re-evaluates SDK support) cleanly.
4729
5160
  this._memorySubscribed = false;
4730
5161
  this._memoryStoreUnsupported = false;
5162
+ // Reset recall state and bump the sequence token so any in-flight recall
5163
+ // resolving after detach is ignored.
5164
+ this._recallSeq += 1;
5165
+ this._recallResults = null;
5166
+ this._recallLoading = false;
5167
+ this._recallError = null;
5168
+ this._recallQuery = "";
4731
5169
  this.coreSubscriber = null;
4732
5170
  this.runtimeStatus = null;
4733
5171
  this.lastCoreError = null;
@@ -7791,6 +8229,10 @@ ${argsString}</pre
7791
8229
  return this.renderToolsView();
7792
8230
  }
7793
8231
 
8232
+ if (this.selectedMenu === "capabilities") {
8233
+ return this.renderCapabilitiesView();
8234
+ }
8235
+
7794
8236
  if (this.selectedMenu === "agent-context") {
7795
8237
  return this.renderContextView();
7796
8238
  }
@@ -8987,7 +9429,7 @@ ${argsString}</pre
8987
9429
  return html`
8988
9430
  <div style="display:flex;height:100%;overflow:hidden;flex-direction:column;">
8989
9431
  <div class="cpk-section-header" style="display:flex;align-items:center;justify-content:space-between;">
8990
- <h4>Learning</h4>
9432
+ <h4>${MEMORY_VIEW_LABEL}</h4>
8991
9433
  <div style="display:flex;align-items:center;gap:6px;">
8992
9434
  ${this.renderMemoryRealtimeIndicator()}
8993
9435
  <span
@@ -9046,6 +9488,19 @@ ${argsString}</pre
9046
9488
  <cpk-memory-list
9047
9489
  style="height:100%;"
9048
9490
  .memories=${this._memories}
9491
+ .recallResults=${this._recallResults}
9492
+ .recallLoading=${this._recallLoading}
9493
+ .recallError=${this._recallError}
9494
+ .recallQueryText=${this._recallQuery}
9495
+ @recallQueryChanged=${(e: CustomEvent<string>) => {
9496
+ this._recallQuery = e.detail;
9497
+ }}
9498
+ @recallSubmitted=${(e: CustomEvent<string>) => {
9499
+ this.runRecall(e.detail);
9500
+ }}
9501
+ @recallCleared=${() => {
9502
+ this.clearRecall();
9503
+ }}
9049
9504
  ></cpk-memory-list>
9050
9505
  </div>
9051
9506
  </div>
@@ -9908,6 +10363,152 @@ ${prettyEvent}</pre
9908
10363
  this.requestUpdate();
9909
10364
  }
9910
10365
 
10366
+ private renderCapabilitiesView() {
10367
+ if (!this._core) {
10368
+ return html`
10369
+ <div
10370
+ class="flex h-full items-center justify-center px-4 py-8 text-xs text-gray-500"
10371
+ >
10372
+ No core instance available
10373
+ </div>
10374
+ `;
10375
+ }
10376
+
10377
+ const toolRows = buildCapabilityRows(
10378
+ this._core as unknown as CapabilityToolSource,
10379
+ );
10380
+ const catalog = this._core.catalogComponents ?? [];
10381
+ const hasCatalog = catalog.length > 0;
10382
+
10383
+ if (toolRows.length === 0 && !hasCatalog) {
10384
+ return html`
10385
+ <div class="flex h-full items-center justify-center px-4 py-8 text-center">
10386
+ <div class="max-w-md">
10387
+ <div class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
10388
+ ${this.renderIcon("SlidersHorizontal")}
10389
+ </div>
10390
+ <p class="text-sm text-gray-600">No capabilities registered</p>
10391
+ <p class="mt-2 text-xs text-gray-500">
10392
+ Frontend tools and A2UI catalog components will appear here once
10393
+ they are registered on the CopilotKit core.
10394
+ </p>
10395
+ </div>
10396
+ </div>
10397
+ `;
10398
+ }
10399
+
10400
+ return html`
10401
+ <div class="flex h-full flex-col overflow-hidden">
10402
+ <div class="overflow-auto p-4">
10403
+ <div class="space-y-3">
10404
+ <p class="text-xs text-gray-500">
10405
+ Toggle a capability off to omit it from what the agent sees. This
10406
+ is a client-side experimentation surface and takes effect
10407
+ immediately.
10408
+ </p>
10409
+ </div>
10410
+
10411
+ ${
10412
+ toolRows.length > 0
10413
+ ? html`
10414
+ <div class="mt-4 space-y-2">
10415
+ <h3 class="text-sm text-slate-500">Frontend tools</h3>
10416
+ <div class="space-y-2">
10417
+ ${toolRows.map((row) => this.renderCapabilityRow(row))}
10418
+ </div>
10419
+ </div>
10420
+ `
10421
+ : nothing
10422
+ }
10423
+
10424
+ ${
10425
+ hasCatalog
10426
+ ? html`
10427
+ <div class="mt-6 space-y-2">
10428
+ <h3 class="text-sm text-slate-500">A2UI catalog components</h3>
10429
+ <div class="space-y-2">
10430
+ ${catalog.map((component) =>
10431
+ this.renderCapabilityRow({
10432
+ key: component.name,
10433
+ name: component.name,
10434
+ description: component.description,
10435
+ enabled: this._core!.isCatalogComponentEnabled(
10436
+ component.name,
10437
+ ),
10438
+ }),
10439
+ )}
10440
+ </div>
10441
+ </div>
10442
+ `
10443
+ : nothing
10444
+ }
10445
+ </div>
10446
+ </div>
10447
+ `;
10448
+ }
10449
+
10450
+ private renderCapabilityRow(row: CapabilityToolRow) {
10451
+ // Frontend-tool keys are always `${agentId}:${name}` (agentId may be ""),
10452
+ // so they contain a ":"; catalog keys are the bare component name.
10453
+ const isTool = row.key.includes(":");
10454
+ return html`
10455
+ <div class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3">
10456
+ <div class="min-w-0 flex-1">
10457
+ <div class="flex items-center gap-2">
10458
+ <span class="font-mono text-sm font-semibold text-gray-900">${row.name}</span>
10459
+ ${
10460
+ row.agentId
10461
+ ? html`<span class="inline-flex items-center gap-1 text-xs text-gray-500">
10462
+ ${this.renderIcon("Bot")}<span class="font-mono">${row.agentId}</span>
10463
+ </span>`
10464
+ : nothing
10465
+ }
10466
+ </div>
10467
+ ${row.description ? html`<p class="mt-1 text-xs text-gray-600">${row.description}</p>` : nothing}
10468
+ </div>
10469
+ ${this.renderCapabilitySwitch(row.enabled, () =>
10470
+ isTool
10471
+ ? this.handleToggleTool(row)
10472
+ : this.handleToggleCatalogComponent(row.name),
10473
+ )}
10474
+ </div>
10475
+ `;
10476
+ }
10477
+
10478
+ private renderCapabilitySwitch(enabled: boolean, onToggle: () => void) {
10479
+ const track = enabled ? "bg-emerald-500" : "bg-gray-300";
10480
+ const knob = enabled ? "translate-x-4" : "translate-x-0.5";
10481
+ return html`
10482
+ <button
10483
+ type="button"
10484
+ role="switch"
10485
+ aria-checked=${enabled ? "true" : "false"}
10486
+ class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300 ${track}"
10487
+ @click=${onToggle}
10488
+ >
10489
+ <span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${knob}"></span>
10490
+ </button>
10491
+ `;
10492
+ }
10493
+
10494
+ private handleToggleTool(row: CapabilityToolRow): void {
10495
+ if (!this._core) return;
10496
+ const next = !row.enabled;
10497
+ // A1 contract: setToolEnabled(name, enabled, agentId?). Pass agentId only
10498
+ // when the tool is agent-scoped so global tools toggle globally.
10499
+ this._core.setToolEnabled(row.name, next, row.agentId);
10500
+ this._capabilitiesVersion += 1;
10501
+ this.requestUpdate();
10502
+ }
10503
+
10504
+ private handleToggleCatalogComponent(name: string): void {
10505
+ if (!this._core) return;
10506
+ const next = !this._core.isCatalogComponentEnabled(name);
10507
+ this._core.setCatalogComponentEnabled(name, next);
10508
+ this._capabilitiesVersion += 1;
10509
+ this.requestUpdate();
10510
+ }
10511
+
9911
10512
  private renderToolsView() {
9912
10513
  if (!this._core) {
9913
10514
  return html`