@copilotkit/web-inspector 1.56.2 → 1.56.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/web-inspector",
3
- "version": "1.56.2",
3
+ "version": "1.56.4",
4
4
  "description": "Lit-based web component for the CopilotKit web inspector",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,7 +23,7 @@
23
23
  "lit": "^3.2.0",
24
24
  "lucide": "^0.525.0",
25
25
  "marked": "^12.0.2",
26
- "@copilotkit/core": "1.56.2"
26
+ "@copilotkit/core": "1.56.4"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@tailwindcss/cli": "^4.1.11",
@@ -7,9 +7,9 @@ import {
7
7
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
8
8
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9
9
 
10
- type MockAgentController = {
11
- emit: (key: keyof AgentSubscriber, payload: unknown) => void;
12
- };
10
+ // --- Types for accessing LitElement-private reactive properties ---
11
+ // WebInspectorElement stores these as private Lit reactive properties.
12
+ // There's no public API to read them, so the cast is unavoidable in tests.
13
13
 
14
14
  type InspectorInternals = {
15
15
  flattenedEvents: Array<{ type: string }>;
@@ -24,6 +24,8 @@ type InspectorContextInternals = {
24
24
  persistState: () => void;
25
25
  };
26
26
 
27
+ // --- Mock agent factory ---
28
+
27
29
  type MockAgentExtras = Partial<{
28
30
  messages: unknown;
29
31
  state: unknown;
@@ -31,13 +33,21 @@ type MockAgentExtras = Partial<{
31
33
  toolRenderers: Record<string, unknown>;
32
34
  }>;
33
35
 
36
+ type MockAgentController = {
37
+ // Each subscriber method has a different parameter shape — TypeScript
38
+ // can't narrow a dynamic key lookup, so the internal cast is unavoidable.
39
+ emit: (key: keyof AgentSubscriber, payload: unknown) => void;
40
+ /** Simulate AbstractAgent.setState(): mutate the mock's state and notify subscribers. */
41
+ simulateSetState: (newState: Record<string, unknown>) => void;
42
+ };
43
+
34
44
  function createMockAgent(
35
45
  agentId: string,
36
46
  extras: MockAgentExtras = {},
37
47
  ): { agent: AbstractAgent; controller: MockAgentController } {
38
48
  const subscribers = new Set<AgentSubscriber>();
39
49
 
40
- const agent = {
50
+ const agentObj = {
41
51
  agentId,
42
52
  ...extras,
43
53
  subscribe(subscriber: AgentSubscriber) {
@@ -57,9 +67,25 @@ function createMockAgent(
57
67
  });
58
68
  };
59
69
 
60
- return { agent: agent as unknown as AbstractAgent, controller: { emit } };
70
+ const simulateSetState = (newState: Record<string, unknown>) => {
71
+ agentObj.state = newState;
72
+ emit("onStateChanged", {
73
+ state: newState,
74
+ messages: agentObj.messages ?? [],
75
+ agent: agentObj,
76
+ });
77
+ };
78
+
79
+ // AbstractAgent is an abstract class — our plain-object mock satisfies
80
+ // the subset the inspector uses but can't extend the class.
81
+ return {
82
+ agent: agentObj as unknown as AbstractAgent,
83
+ controller: { emit, simulateSetState },
84
+ };
61
85
  }
62
86
 
87
+ // --- Mock core factory ---
88
+
63
89
  type MockCore = {
64
90
  agents: Record<string, AbstractAgent>;
65
91
  context: Record<string, unknown>;
@@ -87,6 +113,8 @@ function createMockCore(initialAgents: Record<string, AbstractAgent> = {}) {
87
113
  core,
88
114
  emitAgentsChanged(nextAgents = core.agents) {
89
115
  core.agents = nextAgents;
116
+ // CopilotKitCore is a full class — our mock only covers what the
117
+ // inspector reads, so this cast is unavoidable.
90
118
  subscribers.forEach((subscriber) =>
91
119
  subscriber.onAgentsChanged?.({
92
120
  copilotkit: core as unknown as CopilotKitCore,
@@ -108,7 +136,33 @@ function createMockCore(initialAgents: Record<string, AbstractAgent> = {}) {
108
136
  };
109
137
  }
110
138
 
139
+ // --- Test helpers ---
140
+
141
+ /** Create inspector, attach to DOM, wire up mock core. */
142
+ function createInspectorWithCore(core: MockCore) {
143
+ const inspector = new WebInspectorElement();
144
+ document.body.appendChild(inspector);
145
+ // WebInspectorElement["core"] is a CopilotKitCore instance — our MockCore
146
+ // only implements the subset exercised by these tests.
147
+ inspector.core = core as unknown as WebInspectorElement["core"];
148
+ return inspector;
149
+ }
150
+
151
+ /** Access private Lit reactive properties on the inspector. */
152
+ function getInternals(inspector: WebInspectorElement) {
153
+ return inspector as unknown as InspectorInternals;
154
+ }
155
+
156
+ /** Access context-related private properties on the inspector. */
157
+ function getContextInternals(inspector: WebInspectorElement) {
158
+ return inspector as unknown as InspectorContextInternals;
159
+ }
160
+
161
+ // --- Tests ---
162
+
111
163
  describe("WebInspectorElement", () => {
164
+ let mockClipboard: { writeText: ReturnType<typeof vi.fn> };
165
+
112
166
  beforeEach(() => {
113
167
  document.body.innerHTML = "";
114
168
 
@@ -130,7 +184,9 @@ describe("WebInspectorElement", () => {
130
184
  key: (index: number) => Object.keys(store)[index] ?? null,
131
185
  });
132
186
 
133
- const mockClipboard = { writeText: vi.fn().mockResolvedValue(undefined) };
187
+ mockClipboard = { writeText: vi.fn().mockResolvedValue(undefined) };
188
+ // navigator.clipboard is readonly in DOM types — assigning
189
+ // the mock requires a cast in jsdom-style test environments.
134
190
  (navigator as unknown as { clipboard: typeof mockClipboard }).clipboard =
135
191
  mockClipboard;
136
192
  });
@@ -148,10 +204,7 @@ describe("WebInspectorElement", () => {
148
204
  },
149
205
  });
150
206
  const { core, emitAgentsChanged } = createMockCore({ alpha: agent });
151
-
152
- const inspector = new WebInspectorElement();
153
- document.body.appendChild(inspector);
154
- inspector.core = core as unknown as WebInspectorElement["core"];
207
+ const inspector = createInspectorWithCore(core);
155
208
 
156
209
  emitAgentsChanged();
157
210
  await inspector.updateComplete;
@@ -160,27 +213,26 @@ describe("WebInspectorElement", () => {
160
213
  controller.emit("onMessagesSnapshotEvent", { event: { id: "msg-1" } });
161
214
  await inspector.updateComplete;
162
215
 
163
- const inspectorHandle = inspector as unknown as InspectorInternals;
216
+ const internals = getInternals(inspector);
164
217
 
165
- const flattened = inspectorHandle.flattenedEvents;
166
- expect(flattened.some((evt) => evt.type === "RUN_STARTED")).toBe(true);
167
- expect(flattened.some((evt) => evt.type === "MESSAGES_SNAPSHOT")).toBe(
168
- true,
169
- );
170
218
  expect(
171
- inspectorHandle.agentMessages.get("alpha")?.[0]?.contentText,
172
- ).toContain("hi there");
173
- expect(inspectorHandle.agentStates.get("alpha")).toBeDefined();
219
+ internals.flattenedEvents.some((evt) => evt.type === "RUN_STARTED"),
220
+ ).toBe(true);
174
221
  expect(
175
- inspectorHandle.cachedTools.some((tool) => tool.name === "greet"),
222
+ internals.flattenedEvents.some((evt) => evt.type === "MESSAGES_SNAPSHOT"),
176
223
  ).toBe(true);
224
+ expect(internals.agentMessages.get("alpha")?.[0]?.contentText).toContain(
225
+ "hi there",
226
+ );
227
+ expect(internals.agentStates.get("alpha")).toBeDefined();
228
+ expect(internals.cachedTools.some((tool) => tool.name === "greet")).toBe(
229
+ true,
230
+ );
177
231
  });
178
232
 
179
233
  it("normalizes context, persists state, and copies context values", async () => {
180
234
  const { core, emitContextChanged } = createMockCore();
181
- const inspector = new WebInspectorElement();
182
- document.body.appendChild(inspector);
183
- inspector.core = core as unknown as WebInspectorElement["core"];
235
+ const inspector = createInspectorWithCore(core);
184
236
 
185
237
  emitContextChanged({
186
238
  ctxA: { value: { nested: true } },
@@ -188,22 +240,47 @@ describe("WebInspectorElement", () => {
188
240
  });
189
241
  await inspector.updateComplete;
190
242
 
191
- const inspectorHandle = inspector as unknown as InspectorContextInternals;
192
- const contextStore = inspectorHandle.contextStore;
193
- const ctxA = contextStore.ctxA!;
194
- const ctxB = contextStore.ctxB!;
243
+ const contextInternals = getContextInternals(inspector);
244
+ const ctxA = contextInternals.contextStore.ctxA!;
245
+ const ctxB = contextInternals.contextStore.ctxB!;
195
246
  expect(ctxA.value).toMatchObject({ nested: true });
196
247
  expect(ctxB.description).toBe("Described");
197
248
 
198
- await inspectorHandle.copyContextValue({ nested: true }, "ctxA");
199
- const clipboard = (
200
- navigator as unknown as {
201
- clipboard: { writeText: ReturnType<typeof vi.fn> };
202
- }
203
- ).clipboard.writeText as ReturnType<typeof vi.fn>;
204
- expect(clipboard).toHaveBeenCalledTimes(1);
249
+ await contextInternals.copyContextValue({ nested: true }, "ctxA");
250
+ expect(mockClipboard.writeText).toHaveBeenCalledTimes(1);
205
251
 
206
- inspectorHandle.persistState();
252
+ contextInternals.persistState();
207
253
  expect(localStorage.getItem("cpk:inspector:state")).toBeTruthy();
208
254
  });
255
+
256
+ it("syncs agent state on direct setState (onStateChanged without pipeline events)", async () => {
257
+ // Simulates a selfManagedAgent where agent.setState() is called directly
258
+ // from UI code, bypassing the AG-UI event pipeline. Before the fix,
259
+ // only pipeline event handlers (onStateSnapshotEvent, onStateDeltaEvent)
260
+ // updated the inspector — onStateChanged was not subscribed to, so
261
+ // direct setState() left the inspector stale.
262
+ const { agent, controller } = createMockAgent("counter", {
263
+ state: { counter: 0 },
264
+ });
265
+ const { core, emitAgentsChanged } = createMockCore({ counter: agent });
266
+ const inspector = createInspectorWithCore(core);
267
+
268
+ emitAgentsChanged();
269
+ await inspector.updateComplete;
270
+
271
+ const internals = getInternals(inspector);
272
+
273
+ // Initial state should be captured on subscription
274
+ expect(internals.agentStates.get("counter")).toEqual({ counter: 0 });
275
+
276
+ // Simulate agent.setState({ counter: 1 })
277
+ controller.simulateSetState({ counter: 1 });
278
+ await inspector.updateComplete;
279
+ expect(internals.agentStates.get("counter")).toEqual({ counter: 1 });
280
+
281
+ // Simulate a second setState to verify repeated updates propagate
282
+ controller.simulateSetState({ counter: 5 });
283
+ await inspector.updateComplete;
284
+ expect(internals.agentStates.get("counter")).toEqual({ counter: 5 });
285
+ });
209
286
  });
package/src/index.ts CHANGED
@@ -496,6 +496,9 @@ export class WebInspectorElement extends LitElement {
496
496
  onMessagesChanged: () => {
497
497
  this.syncAgentMessages(agent);
498
498
  },
499
+ onStateChanged: () => {
500
+ this.syncAgentState(agent);
501
+ },
499
502
  onRawEvent: ({ event }) => {
500
503
  this.recordAgentEvent(agentId, "RAW_EVENT", event);
501
504
  },
@@ -581,16 +584,23 @@ export class WebInspectorElement extends LitElement {
581
584
  return;
582
585
  }
583
586
 
584
- const messages = this.normalizeAgentMessages(
585
- (agent as { messages?: unknown }).messages,
586
- );
587
- if (messages) {
588
- this.agentMessages.set(agent.agentId, messages);
589
- } else {
590
- this.agentMessages.delete(agent.agentId);
591
- }
587
+ try {
588
+ const messages = this.normalizeAgentMessages(
589
+ (agent as { messages?: unknown }).messages,
590
+ );
591
+ if (messages) {
592
+ this.agentMessages.set(agent.agentId, messages);
593
+ } else {
594
+ this.agentMessages.delete(agent.agentId);
595
+ }
592
596
 
593
- this.requestUpdate();
597
+ this.requestUpdate();
598
+ } catch (error) {
599
+ console.error(
600
+ `[CopilotKit Inspector] Failed to sync messages for agent "${agent.agentId}":`,
601
+ error,
602
+ );
603
+ }
594
604
  }
595
605
 
596
606
  private syncAgentState(agent: AbstractAgent): void {
@@ -598,15 +608,22 @@ export class WebInspectorElement extends LitElement {
598
608
  return;
599
609
  }
600
610
 
601
- const state = (agent as { state?: unknown }).state;
611
+ try {
612
+ const state = (agent as { state?: unknown }).state;
602
613
 
603
- if (state === undefined || state === null) {
604
- this.agentStates.delete(agent.agentId);
605
- } else {
606
- this.agentStates.set(agent.agentId, this.sanitizeForLogging(state));
607
- }
614
+ if (state === undefined || state === null) {
615
+ this.agentStates.delete(agent.agentId);
616
+ } else {
617
+ this.agentStates.set(agent.agentId, this.sanitizeForLogging(state));
618
+ }
608
619
 
609
- this.requestUpdate();
620
+ this.requestUpdate();
621
+ } catch (error) {
622
+ console.error(
623
+ `[CopilotKit Inspector] Failed to sync state for agent "${agent.agentId}":`,
624
+ error,
625
+ );
626
+ }
610
627
  }
611
628
 
612
629
  private updateContextOptions(agentIds: Set<string>): void {