@copilotkit/web-inspector 1.56.5 → 1.57.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.
@@ -1,4 +1,4 @@
1
- import { WebInspectorElement } from "../index";
1
+ import { WebInspectorElement, ɵCpkThreadDetails } from "../index";
2
2
  import {
3
3
  CopilotKitCore,
4
4
  CopilotKitCoreRuntimeConnectionStatus,
@@ -94,6 +94,8 @@ type MockCore = {
94
94
  subscribe: (subscriber: CopilotKitCoreSubscriber) => {
95
95
  unsubscribe: () => void;
96
96
  };
97
+ getThreadStores: () => Record<string, never>;
98
+ getThreadStore: (agentId: string) => undefined;
97
99
  };
98
100
 
99
101
  function createMockCore(initialAgents: Record<string, AbstractAgent> = {}) {
@@ -107,6 +109,12 @@ function createMockCore(initialAgents: Record<string, AbstractAgent> = {}) {
107
109
  subscribers.add(subscriber);
108
110
  return { unsubscribe: () => subscribers.delete(subscriber) };
109
111
  },
112
+ getThreadStores() {
113
+ return {};
114
+ },
115
+ getThreadStore(_agentId: string) {
116
+ return undefined;
117
+ },
110
118
  };
111
119
 
112
120
  return {
@@ -284,3 +292,173 @@ describe("WebInspectorElement", () => {
284
292
  expect(internals.agentStates.get("counter")).toEqual({ counter: 5 });
285
293
  });
286
294
  });
295
+
296
+ // ─────────────────────────────────────────────────────────────────────────
297
+ // CpkThreadDetails — per-panel TemplateResult cache invariants
298
+ // ─────────────────────────────────────────────────────────────────────────
299
+ //
300
+ // The conversation / agent-state / events panels each cache the rendered
301
+ // TemplateResult by reference of the underlying data so tab switches don't
302
+ // re-iterate over hundreds of items. These tests pin down the cache-key
303
+ // contract: when the data reassigns, the cache MUST drop, and when the
304
+ // thread changes, ALL three caches MUST reset. A future edit that mutates
305
+ // the data in place (instead of reassigning) or forgets to null one of the
306
+ // caches in `updated()` would silently render stale content under a new
307
+ // thread, which is undetectable by manual QA on a single thread.
308
+
309
+ type ThreadDetailsInternals = {
310
+ threadId: string | null;
311
+ liveMessageVersion: number;
312
+ _conversation: Array<Record<string, unknown>>;
313
+ _fetchedState: Record<string, unknown> | null;
314
+ _fetchedEvents: Array<unknown> | null;
315
+ _expandedTools: Set<string>;
316
+ _expandedMessages: Set<string>;
317
+ _stateNotAvailable: boolean;
318
+ _eventsNotAvailable: boolean;
319
+ _loadingMessages: boolean;
320
+ _loadingState: boolean;
321
+ _loadingEvents: boolean;
322
+ _panelTplCache: Map<string, { key: readonly unknown[]; tpl: unknown }>;
323
+ renderConversation: () => unknown;
324
+ renderState: () => unknown;
325
+ renderEvents: () => unknown;
326
+ };
327
+
328
+ function createThreadDetails(): {
329
+ el: ɵCpkThreadDetails;
330
+ internals: ThreadDetailsInternals;
331
+ } {
332
+ const el = new ɵCpkThreadDetails();
333
+ document.body.appendChild(el);
334
+ // Same cast pattern as `getInternals` above — there's no public surface
335
+ // for the cache slots, so the test reaches through a typed view.
336
+ const internals = el as unknown as ThreadDetailsInternals;
337
+ return { el, internals };
338
+ }
339
+
340
+ describe("ɵCpkThreadDetails caching", () => {
341
+ beforeEach(() => {
342
+ document.body.innerHTML = "";
343
+ });
344
+
345
+ /**
346
+ * Drive the threadId-change `updated()` block once so its reset path
347
+ * runs on entry, then seed the data the test cares about AFTER. If we
348
+ * seed before the first updateComplete, `updated()` immediately nulls
349
+ * `_fetchedState` / `_fetchedEvents` / `_conversation` (and
350
+ * `fetchMessages` re-clears `_conversation` when no `runtimeUrl` is
351
+ * configured, as in this jsdom test), so the assertions below would
352
+ * be running against an empty element.
353
+ */
354
+ async function settleThread(
355
+ el: ɵCpkThreadDetails,
356
+ internals: ThreadDetailsInternals,
357
+ threadId: string,
358
+ ): Promise<void> {
359
+ internals.threadId = threadId;
360
+ await el.updateComplete;
361
+ }
362
+
363
+ it("threadId change drops all three template caches", async () => {
364
+ const { el, internals } = createThreadDetails();
365
+ await settleThread(el, internals, "t1");
366
+
367
+ // Hand-build cache entries for all three panels so we don't have to
368
+ // drive every render path through the DOM. The presence of any entry
369
+ // is what the assertion below checks for; what they hold is irrelevant.
370
+ internals._panelTplCache.set("conversation", { key: [], tpl: "c" });
371
+ internals._panelTplCache.set("agent-state", { key: [], tpl: "s" });
372
+ internals._panelTplCache.set("ag-ui-events", { key: [], tpl: "e" });
373
+
374
+ // Switch to thread t2 — the threadId branch in `updated()` should
375
+ // empty the cache map.
376
+ internals.threadId = "t2";
377
+ await el.updateComplete;
378
+
379
+ expect(internals._panelTplCache.size).toBe(0);
380
+ });
381
+
382
+ it("conversation cache invalidates when _conversation is reassigned", async () => {
383
+ const { el, internals } = createThreadDetails();
384
+ await settleThread(el, internals, "t1");
385
+
386
+ internals._conversation = [
387
+ { id: "m1", type: "user", content: "hi", createdAt: "" },
388
+ ];
389
+
390
+ const tplA = internals.renderConversation();
391
+ expect(internals._panelTplCache.get("conversation")?.tpl).toBe(tplA);
392
+
393
+ // Cache hit: same array reference, same expand sets — same TemplateResult.
394
+ expect(internals.renderConversation()).toBe(tplA);
395
+
396
+ // New array reference (the streaming refetch path always reassigns
397
+ // via `this._conversation = this.mapMessages(...)` rather than
398
+ // mutating in place — that contract is what this test pins).
399
+ internals._conversation = [
400
+ { id: "m1", type: "user", content: "hi", createdAt: "" },
401
+ { id: "m2", type: "assistant", content: "hello", createdAt: "" },
402
+ ];
403
+
404
+ const tplB = internals.renderConversation();
405
+ expect(tplB).not.toBe(tplA);
406
+ expect(internals._panelTplCache.get("conversation")?.tpl).toBe(tplB);
407
+ });
408
+
409
+ it("conversation cache invalidates when expand state changes", async () => {
410
+ // Regression guard: an earlier version keyed the cache only on
411
+ // `_conversation`, so toggling a tool-call expand or a "Show more"
412
+ // on a long message returned the pre-toggle template. The cache key
413
+ // now includes both expand sets.
414
+ const { el, internals } = createThreadDetails();
415
+ await settleThread(el, internals, "t1");
416
+
417
+ internals._conversation = [
418
+ {
419
+ id: "tc1",
420
+ type: "tool_call",
421
+ toolName: "doThing",
422
+ toolCallId: "tc1",
423
+ arguments: { x: 1 },
424
+ result: null,
425
+ createdAt: "",
426
+ },
427
+ ];
428
+
429
+ const collapsed = internals.renderConversation();
430
+ expect(internals.renderConversation()).toBe(collapsed);
431
+
432
+ // Simulating `toggleToolExpand("tc1")` — production code always
433
+ // builds a fresh Set, so reference equality flips.
434
+ internals._expandedTools = new Set(["tc1"]);
435
+
436
+ const expanded = internals.renderConversation();
437
+ expect(expanded).not.toBe(collapsed);
438
+
439
+ // Same for the long-message "Show more" path.
440
+ internals._expandedMessages = new Set(["m1"]);
441
+
442
+ expect(internals.renderConversation()).not.toBe(expanded);
443
+ });
444
+
445
+ it("state and events caches invalidate when their fetched data is reassigned", async () => {
446
+ const { el, internals } = createThreadDetails();
447
+ await settleThread(el, internals, "t1");
448
+
449
+ internals._fetchedState = { foo: "bar" };
450
+ internals._fetchedEvents = [{ type: "RUN_STARTED" }];
451
+
452
+ const stateA = internals.renderState();
453
+ const eventsA = internals.renderEvents();
454
+ expect(internals.renderState()).toBe(stateA);
455
+ expect(internals.renderEvents()).toBe(eventsA);
456
+
457
+ // Reassign both — fresh references after a refetch.
458
+ internals._fetchedState = { foo: "baz" };
459
+ internals._fetchedEvents = [{ type: "RUN_FINISHED" }];
460
+
461
+ expect(internals.renderState()).not.toBe(stateA);
462
+ expect(internals.renderEvents()).not.toBe(eventsA);
463
+ });
464
+ });