@runtypelabs/persona 4.22.0 → 4.22.1

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.
Files changed (93) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
  4. package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +136 -6
  8. package/dist/animations-extra.d.ts +136 -6
  9. package/dist/approval-ui.d.cts +148 -14
  10. package/dist/approval-ui.d.ts +148 -14
  11. package/dist/artifacts-ui.d.cts +148 -14
  12. package/dist/artifacts-ui.d.ts +148 -14
  13. package/dist/codegen.cjs +1 -1
  14. package/dist/codegen.js +1 -1
  15. package/dist/context-mentions.d.cts +148 -14
  16. package/dist/context-mentions.d.ts +148 -14
  17. package/dist/event-stream-view-GL3ADRY2.js +1 -0
  18. package/dist/event-stream-view.cjs +1 -1
  19. package/dist/event-stream-view.d.cts +148 -14
  20. package/dist/event-stream-view.d.ts +148 -14
  21. package/dist/event-stream-view.js +1 -1
  22. package/dist/forms-ui.d.cts +161 -23
  23. package/dist/forms-ui.d.ts +161 -23
  24. package/dist/history-view.d.cts +136 -6
  25. package/dist/history-view.d.ts +136 -6
  26. package/dist/index.cjs +67 -67
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +166 -27
  29. package/dist/index.d.ts +166 -27
  30. package/dist/index.global.js +51 -51
  31. package/dist/index.global.js.map +1 -1
  32. package/dist/index.js +67 -67
  33. package/dist/index.js.map +1 -1
  34. package/dist/session-reconnect.d.cts +148 -14
  35. package/dist/session-reconnect.d.ts +148 -14
  36. package/dist/smart-dom-reader.d.cts +148 -14
  37. package/dist/smart-dom-reader.d.ts +148 -14
  38. package/dist/testing.cjs +3 -3
  39. package/dist/testing.d.cts +3 -4
  40. package/dist/testing.d.ts +3 -4
  41. package/dist/testing.js +3 -3
  42. package/dist/theme-editor-preview.cjs +71 -71
  43. package/dist/theme-editor-preview.d.cts +148 -14
  44. package/dist/theme-editor-preview.d.ts +148 -14
  45. package/dist/theme-editor-preview.js +48 -48
  46. package/dist/theme-editor.d.cts +148 -14
  47. package/dist/theme-editor.d.ts +148 -14
  48. package/dist/voice-runtime.d.cts +6 -2
  49. package/dist/voice-runtime.d.ts +6 -2
  50. package/package.json +1 -1
  51. package/src/ask-user-question-tool.ts +1 -1
  52. package/src/client-history-rest.test.ts +2 -1
  53. package/src/client-visitor-history.test.ts +20 -4
  54. package/src/client.composer-options.test.ts +1 -1
  55. package/src/client.interrupt.test.ts +2 -2
  56. package/src/client.test.ts +477 -915
  57. package/src/client.ts +134 -494
  58. package/src/client.unified-tools.test.ts +76 -0
  59. package/src/components/event-stream-badge-theming.test.ts +3 -0
  60. package/src/components/event-stream-view.test.ts +76 -76
  61. package/src/components/event-stream-view.ts +5 -0
  62. package/src/components/tool-bubble.test.ts +12 -0
  63. package/src/components/tool-bubble.ts +13 -2
  64. package/src/event-stream-view-bundle.test.ts +2 -2
  65. package/src/generated/runtype-openapi-contract.ts +129 -1
  66. package/src/internal/runtype-history-provider.test.ts +2 -2
  67. package/src/session-history.test.ts +1 -1
  68. package/src/session.quote.test.ts +1 -1
  69. package/src/session.resubmit-from.test.ts +1 -1
  70. package/src/session.test.ts +5 -5
  71. package/src/session.ts +95 -36
  72. package/src/suggest-replies-tool.ts +1 -1
  73. package/src/testing/mock-stream.test.ts +37 -16
  74. package/src/testing/mock-stream.ts +14 -9
  75. package/src/types.ts +19 -14
  76. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  77. package/src/ui.composer-defer-one.test.ts +6 -34
  78. package/src/ui.composer-draft-persistence.test.ts +1 -1
  79. package/src/ui.composer-quote.test.ts +1 -1
  80. package/src/ui.message-history-actions.test.ts +1 -1
  81. package/src/ui.streaming-coalescing.test.ts +10 -13
  82. package/src/ui.ts +67 -32
  83. package/src/ui.typing-indicator.test.ts +216 -0
  84. package/src/ui.voice-custom-provider.test.ts +211 -0
  85. package/src/utils/event-stream-buffer.test.ts +13 -13
  86. package/src/utils/event-stream-capture.test.ts +20 -20
  87. package/src/utils/throughput-tracker.test.ts +100 -142
  88. package/src/utils/throughput-tracker.ts +18 -132
  89. package/src/utils/voice-support.ts +18 -1
  90. package/src/voice/voice-turn-tracker.ts +18 -0
  91. package/src/webmcp-bridge.ts +1 -1
  92. package/dist/event-stream-view-FKWDY3UB.js +0 -1
  93. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
@@ -133,3 +133,219 @@ describe("standalone typing indicator bubble", () => {
133
133
  destroy();
134
134
  });
135
135
  });
136
+
137
+ /**
138
+ * Regression coverage for hidden tool/reasoning rows. With
139
+ * `features.showToolCalls: false` the tool row never renders, so it must not
140
+ * count as "the assistant is already responding" and hide the standalone dots
141
+ * for the whole tool phase.
142
+ */
143
+ describe("standalone typing indicator with hidden activity rows", () => {
144
+ afterEach(() => {
145
+ document.body.innerHTML = "";
146
+ if (typeof localStorage !== "undefined") localStorage.clear();
147
+ vi.restoreAllMocks();
148
+ });
149
+
150
+ const mountStreaming = async (config: Record<string, unknown>) => {
151
+ global.fetch = vi.fn().mockImplementation(
152
+ () => new Promise(() => {})
153
+ ) as unknown as typeof fetch;
154
+
155
+ const mount = createMount();
156
+ const controller = createAgentExperience(mount, {
157
+ apiUrl: "https://api.example.com/chat",
158
+ launcher: { enabled: false },
159
+ ...config,
160
+ } as unknown as Parameters<typeof createAgentExperience>[1]);
161
+
162
+ controller.submitMessage("hello");
163
+ await Promise.resolve();
164
+ await Promise.resolve();
165
+ return { mount, controller };
166
+ };
167
+
168
+ // The session orders by createdAt first, then by its Date.now()-based
169
+ // sequence, so injected rows need both stamped in the future to sort after
170
+ // the user message the way real stream rows do.
171
+ const seq = (n: number) => Date.now() + 60_000 + n;
172
+ const at = (n: number) => new Date(seq(n)).toISOString();
173
+
174
+ const injectTool = (
175
+ controller: ReturnType<typeof createAgentExperience>,
176
+ id: string,
177
+ status: "running" | "complete",
178
+ sequence: number
179
+ ) => {
180
+ controller.injectTestMessage({
181
+ type: "message",
182
+ message: {
183
+ id,
184
+ role: "assistant",
185
+ content: "",
186
+ createdAt: new Date(sequence).toISOString(),
187
+ sequence,
188
+ streaming: status !== "complete",
189
+ variant: "tool",
190
+ toolCall: { id, name: "lookup", status },
191
+ },
192
+ });
193
+ };
194
+
195
+ const injectReasoning = (
196
+ controller: ReturnType<typeof createAgentExperience>,
197
+ id: string,
198
+ status: "streaming" | "complete",
199
+ sequence: number
200
+ ) => {
201
+ controller.injectTestMessage({
202
+ type: "message",
203
+ message: {
204
+ id,
205
+ role: "assistant",
206
+ content: "",
207
+ createdAt: new Date(sequence).toISOString(),
208
+ sequence,
209
+ streaming: status !== "complete",
210
+ variant: "reasoning",
211
+ reasoning: { id, status, chunks: ["thinking"] },
212
+ },
213
+ });
214
+ };
215
+
216
+ const typingIndicator = (mount: HTMLElement) =>
217
+ mount.querySelector<HTMLElement>('[data-typing-indicator="true"]');
218
+
219
+
220
+ it("keeps the dots while a hidden tool call is running", async () => {
221
+ const { mount, controller } = await mountStreaming({
222
+ features: { showToolCalls: false },
223
+ });
224
+ expect(typingIndicator(mount)).not.toBeNull();
225
+
226
+ injectTool(controller, "tool-1", "running", seq(1));
227
+ expect(mount.querySelector(".persona-tool-bubble")).toBeNull();
228
+ expect(typingIndicator(mount)).not.toBeNull();
229
+
230
+ controller.destroy();
231
+ });
232
+
233
+ it("keeps the dots after a hidden tool call completes, before the next text", async () => {
234
+ const { mount, controller } = await mountStreaming({
235
+ features: { showToolCalls: false },
236
+ });
237
+
238
+ injectTool(controller, "tool-1", "running", seq(1));
239
+ injectTool(controller, "tool-1", "complete", seq(1));
240
+ expect(mount.querySelector(".persona-tool-bubble")).toBeNull();
241
+ expect(typingIndicator(mount)).not.toBeNull();
242
+
243
+ controller.destroy();
244
+ });
245
+
246
+ it("keeps the dots when a hidden tool call follows an intermediate assistant text", async () => {
247
+ const { mount, controller } = await mountStreaming({
248
+ features: { showToolCalls: false },
249
+ });
250
+
251
+ controller.injectTestMessage({
252
+ type: "message",
253
+ message: {
254
+ id: "text-1",
255
+ role: "assistant",
256
+ content: "Let me look that up.",
257
+ createdAt: at(1),
258
+ sequence: seq(1),
259
+ streaming: false,
260
+ },
261
+ });
262
+ injectTool(controller, "tool-1", "complete", seq(2));
263
+ expect(typingIndicator(mount)).not.toBeNull();
264
+
265
+ controller.destroy();
266
+ });
267
+
268
+ it("still hides the dots while a visible tool call renders its own bubble", async () => {
269
+ const { mount, controller } = await mountStreaming({
270
+ features: { showToolCalls: true },
271
+ });
272
+
273
+ injectTool(controller, "tool-1", "running", seq(1));
274
+ expect(mount.querySelector(".persona-tool-bubble")).not.toBeNull();
275
+ expect(typingIndicator(mount)).toBeNull();
276
+
277
+ controller.destroy();
278
+ });
279
+
280
+ it("keeps the dots while hidden reasoning streams", async () => {
281
+ const { mount, controller } = await mountStreaming({
282
+ features: { showReasoning: false },
283
+ });
284
+
285
+ injectReasoning(controller, "reason-1", "streaming", seq(1));
286
+ expect(typingIndicator(mount)).not.toBeNull();
287
+
288
+ injectReasoning(controller, "reason-1", "complete", seq(1));
289
+ expect(typingIndicator(mount)).not.toBeNull();
290
+
291
+ controller.destroy();
292
+ });
293
+
294
+ it("keeps the dots after a visible tool call is removed by completedVisibility", async () => {
295
+ const { mount, controller } = await mountStreaming({
296
+ features: {
297
+ showToolCalls: true,
298
+ toolCallDisplay: { completedVisibility: "removed" },
299
+ },
300
+ });
301
+
302
+ injectTool(controller, "tool-1", "running", seq(1));
303
+ expect(mount.querySelector(".persona-tool-bubble")).not.toBeNull();
304
+ expect(typingIndicator(mount)).toBeNull();
305
+
306
+ injectTool(controller, "tool-1", "complete", seq(1));
307
+ expect(mount.querySelector(".persona-tool-bubble")).toBeNull();
308
+ expect(typingIndicator(mount)).not.toBeNull();
309
+
310
+ controller.destroy();
311
+ });
312
+
313
+ it("keeps the dots after visible reasoning is removed by completedVisibility", async () => {
314
+ const { mount, controller } = await mountStreaming({
315
+ features: {
316
+ showReasoning: true,
317
+ reasoningDisplay: { completedVisibility: "removed" },
318
+ },
319
+ });
320
+
321
+ injectReasoning(controller, "reason-1", "streaming", seq(1));
322
+ expect(typingIndicator(mount)).toBeNull();
323
+
324
+ injectReasoning(controller, "reason-1", "complete", seq(1));
325
+ expect(typingIndicator(mount)).not.toBeNull();
326
+
327
+ controller.destroy();
328
+ });
329
+
330
+ it("hides the dots once a real assistant message is streaming", async () => {
331
+ const { mount, controller } = await mountStreaming({
332
+ features: { showToolCalls: false },
333
+ });
334
+
335
+ injectTool(controller, "tool-1", "complete", seq(1));
336
+ controller.injectTestMessage({
337
+ type: "message",
338
+ message: {
339
+ id: "text-1",
340
+ role: "assistant",
341
+ content: "Here you go",
342
+ createdAt: at(2),
343
+ sequence: seq(2),
344
+ streaming: true,
345
+ },
346
+ });
347
+ expect(typingIndicator(mount)).toBeNull();
348
+
349
+ controller.destroy();
350
+ });
351
+ });
@@ -0,0 +1,211 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
4
+ import { createAgentExperience } from "./ui";
5
+ import type { VoiceProvider, VoiceStatus } from "./types";
6
+
7
+ const controllers: ReturnType<typeof createAgentExperience>[] = [];
8
+ beforeEach(() => { window.scrollTo = vi.fn(); });
9
+ afterEach(() => {
10
+ for (const controller of controllers.splice(0)) controller.destroy();
11
+ document.body.innerHTML = "";
12
+ window.localStorage.clear();
13
+ vi.unstubAllGlobals();
14
+ });
15
+
16
+ function fixture(mode: "none" | "cancel" | "barge-in" = "cancel") {
17
+ let statusCallback: (status: VoiceStatus) => void = () => {};
18
+ let transcriptCallback: NonNullable<Parameters<NonNullable<VoiceProvider["onTranscript"]>>[0]> = () => {};
19
+ let active = false;
20
+ const provider: VoiceProvider = {
21
+ type: "runtype",
22
+ connect: vi.fn(async () => {}),
23
+ disconnect: vi.fn(async () => { active = false; }),
24
+ startListening: vi.fn(async () => { active = true; statusCallback("listening"); }),
25
+ stopListening: vi.fn(async () => { active = false; statusCallback("idle"); }),
26
+ onResult: vi.fn(),
27
+ onError: vi.fn(),
28
+ onStatusChange: callback => { statusCallback = callback; },
29
+ onTranscript: callback => { transcriptCallback = callback; },
30
+ getInterruptionMode: () => mode,
31
+ isBargeInActive: () => active,
32
+ stopPlayback: vi.fn(() => { statusCallback("listening"); }),
33
+ deactivateBargeIn: vi.fn(async () => { active = false; statusCallback("idle"); }),
34
+ };
35
+ const recognition = vi.fn();
36
+ vi.stubGlobal("SpeechRecognition", recognition);
37
+ const mount = document.createElement("div");
38
+ document.body.appendChild(mount);
39
+ const controller = createAgentExperience(mount, {
40
+ apiUrl: "https://api.example.test/chat",
41
+ launcher: { enabled: false }, persistState: false, suggestionChips: [],
42
+ voiceRecognition: { enabled: true, provider: { type: "custom", custom: () => provider } },
43
+ });
44
+ controllers.push(controller);
45
+ const mic = () => mount.querySelector<HTMLButtonElement>("[data-persona-composer-mic]")!;
46
+ return { provider, recognition, mic, controller, emit: (status: VoiceStatus) => statusCallback(status),
47
+ transcript: (role: "user" | "assistant", text: string, final = true, turnId?: string) => transcriptCallback(role, text, final, { turnId }) };
48
+ }
49
+
50
+ it("starts the custom provider from the microphone and cancels playback without hanging up", async () => {
51
+ const { provider, recognition, mic, emit } = fixture();
52
+ mic().click();
53
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
54
+ expect(recognition).not.toHaveBeenCalled();
55
+ emit("speaking");
56
+ expect(mic().getAttribute("aria-label")).toBe("Stop playback and re-record");
57
+ mic().click();
58
+ expect(provider.stopPlayback).toHaveBeenCalledOnce();
59
+ expect(provider.stopListening).not.toHaveBeenCalled();
60
+ expect(provider.deactivateBargeIn).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("respects none mode and cleans up the custom provider on destroy", async () => {
64
+ const { provider, mic, emit, controller } = fixture("none");
65
+ mic().click();
66
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
67
+ emit("speaking");
68
+ mic().click();
69
+ expect(provider.stopPlayback).not.toHaveBeenCalled();
70
+ expect(provider.stopListening).not.toHaveBeenCalled();
71
+ controller.destroy();
72
+ controllers.splice(controllers.indexOf(controller), 1);
73
+ expect(provider.disconnect).toHaveBeenCalled();
74
+ });
75
+
76
+ it("hangs up a barge-in custom call through the microphone control", async () => {
77
+ const { provider, mic, emit } = fixture("barge-in");
78
+ mic().click();
79
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
80
+ emit("speaking");
81
+ mic().click();
82
+ await vi.waitFor(() => expect(provider.deactivateBargeIn).toHaveBeenCalledOnce());
83
+ });
84
+
85
+ it("routes controller start and stop to the custom provider after a composer update", async () => {
86
+ const { provider, recognition, controller, mic, emit } = fixture();
87
+ controller.update({ voiceRecognition: { enabled: false } });
88
+ controller.update({ voiceRecognition: { enabled: true } });
89
+ expect(mic()).not.toBeNull();
90
+ await controller.startVoiceRecognition();
91
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
92
+ emit("speaking");
93
+ await controller.stopVoiceRecognition();
94
+ expect(provider.deactivateBargeIn).toHaveBeenCalledOnce();
95
+ expect(recognition).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it.each(["cancel", "barge-in"] as const)("settles a pending assistant placeholder after %s during thinking", async (mode) => {
99
+ const { provider, mic, emit, transcript, controller } = fixture(mode);
100
+ mic().click();
101
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
102
+ emit("processing");
103
+ transcript("user", "Tell me a story");
104
+ expect(controller.getMessages().some(message => message.streaming)).toBe(true);
105
+ if (mode === "cancel") mic().click();
106
+ else emit("listening");
107
+ expect(controller.getMessages().map(message => message.content)).toEqual(["Tell me a story"]);
108
+ expect(controller.getMessages().some(message => message.streaming || message.voiceProcessing)).toBe(false);
109
+ transcript("user", "Next question");
110
+ transcript("assistant", "Next answer");
111
+ expect(controller.getMessages().map(message => message.content)).toEqual(["Tell me a story", "Next question", "Next answer"]);
112
+ });
113
+
114
+ it("preserves partial voice content while clearing its streaming flags on hangup", async () => {
115
+ const { provider, mic, emit, transcript, controller } = fixture("barge-in");
116
+ mic().click();
117
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
118
+ transcript("user", "Explain");
119
+ transcript("assistant", "A partial answer", false);
120
+ emit("speaking");
121
+ mic().click();
122
+ expect(controller.getMessages().at(-1)).toMatchObject({ content: "A partial answer", streaming: false, voiceProcessing: false });
123
+ });
124
+
125
+ it("keeps the current user partial in one bubble when playback returns to listening", async () => {
126
+ const { provider, mic, emit, transcript, controller } = fixture();
127
+ mic().click();
128
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
129
+ transcript("user", "Hello");
130
+ transcript("assistant", "Hi");
131
+ transcript("user", "Next", false);
132
+ emit("listening");
133
+ transcript("user", "Next question");
134
+ transcript("assistant", "Next answer");
135
+ expect(controller.getMessages().filter(message => message.role === "user").map(message => message.content)).toEqual(["Hello", "Next question"]);
136
+ });
137
+
138
+ it("drops a cancelled reply arriving after a newer identified turn", async () => {
139
+ const { provider, mic, emit, transcript, controller } = fixture();
140
+ mic().click();
141
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
142
+ transcript("user", "Old question", true, "old");
143
+ emit("processing");
144
+ mic().click();
145
+ transcript("assistant", "Old partial", false, "old");
146
+ transcript("user", "New question", true, "new");
147
+ transcript("assistant", "Cancelled old answer", true, "old");
148
+ transcript("assistant", "New answer", true, "new");
149
+ expect(controller.getMessages().map(message => message.content)).toEqual(["Old question", "New question", "New answer"]);
150
+ });
151
+
152
+ it("ignores untagged cancelled output until the next user final without losing that next answer", async () => {
153
+ const { provider, mic, emit, transcript, controller } = fixture();
154
+ mic().click();
155
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
156
+ transcript("user", "Old question");
157
+ emit("processing");
158
+ mic().click();
159
+ transcript("assistant", "Cancelled answer");
160
+ transcript("user", "New question");
161
+ transcript("assistant", "New answer");
162
+ expect(controller.getMessages().map(message => message.content)).toEqual(["Old question", "New question", "New answer"]);
163
+ });
164
+
165
+ it("disconnects the old provider before constructing its replacement and ignores its stale callbacks", async () => {
166
+ const { provider, mic, controller, transcript, emit } = fixture();
167
+ mic().click();
168
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
169
+ let finishDisconnect!: () => void;
170
+ vi.mocked(provider.disconnect).mockImplementation(() => new Promise<void>(resolve => { finishDisconnect = resolve; }));
171
+ const replacement = { ...provider, connect: vi.fn(async () => {}), startListening: vi.fn(async () => {}), disconnect: vi.fn(async () => {}) };
172
+ const factory = vi.fn(() => replacement);
173
+ controller.update({ voiceRecognition: { enabled: true, provider: { type: "custom", custom: factory } } });
174
+ expect(provider.disconnect).toHaveBeenCalledOnce();
175
+ expect(controller.isVoiceActive()).toBe(false);
176
+ expect(factory).not.toHaveBeenCalled();
177
+ transcript("assistant", "Stale reply");
178
+ emit("speaking");
179
+ expect(controller.getMessages()).toEqual([]);
180
+ finishDisconnect();
181
+ await controller.startVoiceRecognition();
182
+ await vi.waitFor(() => expect(replacement.startListening).toHaveBeenCalledOnce());
183
+ expect(factory).toHaveBeenCalledOnce();
184
+ });
185
+
186
+ it("disconnects a live call when disabled and reinstalls the provider on reenable", async () => {
187
+ const { provider, mic, controller } = fixture();
188
+ mic().click();
189
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
190
+ controller.update({ voiceRecognition: { enabled: false } });
191
+ expect(provider.disconnect).toHaveBeenCalledOnce();
192
+ expect(controller.isVoiceActive()).toBe(false);
193
+ controller.update({ voiceRecognition: { enabled: true } });
194
+ await controller.startVoiceRecognition();
195
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledTimes(2));
196
+ });
197
+
198
+ it("sets up custom voice after starting with browser dictation and preserves it for cosmetic updates", async () => {
199
+ const { provider, controller } = fixture();
200
+ await controller.startVoiceRecognition();
201
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
202
+ controller.update({ voiceRecognition: { provider: { type: "browser" } } });
203
+ expect(provider.disconnect).toHaveBeenCalledOnce();
204
+ expect(controller.isVoiceActive()).toBe(false);
205
+ vi.mocked(provider.startListening).mockClear();
206
+ controller.update({ voiceRecognition: { provider: { type: "custom", custom: () => provider } } });
207
+ await controller.startVoiceRecognition();
208
+ await vi.waitFor(() => expect(provider.startListening).toHaveBeenCalledOnce());
209
+ controller.update({ voiceRecognition: { iconName: "mic" }, colorScheme: "dark" });
210
+ expect(provider.disconnect).toHaveBeenCalledOnce();
211
+ });
@@ -37,7 +37,7 @@ describe("EventStreamBuffer", () => {
37
37
 
38
38
  it("should push and retrieve events", () => {
39
39
  const buf = new EventStreamBuffer(10);
40
- const evt = makeEvent("step_chunk", 1);
40
+ const evt = makeEvent("text_delta", 1);
41
41
  buf.push(evt);
42
42
  expect(buf.getSize()).toBe(1);
43
43
  expect(buf.getAll()).toEqual([evt]);
@@ -85,30 +85,30 @@ describe("EventStreamBuffer", () => {
85
85
 
86
86
  it("should track unique event types", () => {
87
87
  const buf = new EventStreamBuffer(10);
88
- buf.push(makeEvent("step_chunk", 1));
89
- buf.push(makeEvent("flow_complete", 2));
90
- buf.push(makeEvent("step_chunk", 3));
88
+ buf.push(makeEvent("text_delta", 1));
89
+ buf.push(makeEvent("execution_complete", 2));
90
+ buf.push(makeEvent("text_delta", 3));
91
91
  const types = buf.getEventTypes();
92
- expect(types).toContain("step_chunk");
93
- expect(types).toContain("flow_complete");
92
+ expect(types).toContain("text_delta");
93
+ expect(types).toContain("execution_complete");
94
94
  expect(types).toHaveLength(2);
95
95
  });
96
96
 
97
97
  it("should preserve event types after eviction", () => {
98
98
  const buf = new EventStreamBuffer(3);
99
- buf.push(makeEvent("step_chunk", 1));
99
+ buf.push(makeEvent("text_delta", 1));
100
100
  buf.push(makeEvent("tool_start", 2));
101
- buf.push(makeEvent("flow_complete", 3));
102
- // Evict the step_chunk event
101
+ buf.push(makeEvent("execution_complete", 3));
102
+ // Evict the text_delta event
103
103
  buf.push(makeEvent("tool_end", 4));
104
104
  buf.push(makeEvent("tool_end", 5));
105
- // step_chunk is evicted from the buffer but still tracked in types
105
+ // text_delta is evicted from the buffer but still tracked in types
106
106
  const all = buf.getAll();
107
- expect(all.every(e => e.type !== "step_chunk")).toBe(true);
107
+ expect(all.every(e => e.type !== "text_delta")).toBe(true);
108
108
  const types = buf.getEventTypes();
109
- expect(types).toContain("step_chunk");
109
+ expect(types).toContain("text_delta");
110
110
  expect(types).toContain("tool_start");
111
- expect(types).toContain("flow_complete");
111
+ expect(types).toContain("execution_complete");
112
112
  expect(types).toContain("tool_end");
113
113
  expect(types).toHaveLength(4);
114
114
  });
@@ -45,10 +45,10 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
45
45
  });
46
46
  });
47
47
 
48
- it("should fire onSSEEvent for step_chunk events", async () => {
48
+ it("should fire onSSEEvent for text_delta events", async () => {
49
49
  global.fetch = createMockFetch([
50
50
  sseData({
51
- type: "step_chunk",
51
+ type: "text_delta",
52
52
  id: "step_1",
53
53
  name: "Prompt 1",
54
54
  executionType: "prompt",
@@ -56,7 +56,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
56
56
  text: "Hello world"
57
57
  }),
58
58
  sseData({
59
- type: "flow_complete",
59
+ type: "execution_complete",
60
60
  flowId: "flow_1",
61
61
  success: true,
62
62
  duration: 100
@@ -79,11 +79,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
79
79
 
80
80
  // Should capture both events
81
81
  const stepChunkEvents = capturedSSEEvents.filter(
82
- (e) => e.eventType === "step_chunk"
82
+ (e) => e.eventType === "text_delta"
83
83
  );
84
84
  expect(stepChunkEvents).toHaveLength(1);
85
85
  expect(stepChunkEvents[0].payload).toMatchObject({
86
- type: "step_chunk",
86
+ type: "text_delta",
87
87
  text: "Hello world"
88
88
  });
89
89
  });
@@ -106,7 +106,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
106
106
  completedAt: "2025-01-01T00:00:01.000Z"
107
107
  }),
108
108
  sseData({
109
- type: "flow_complete",
109
+ type: "execution_complete",
110
110
  flowId: "flow_1",
111
111
  success: true,
112
112
  duration: 200
@@ -148,10 +148,10 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
148
148
  });
149
149
  });
150
150
 
151
- it("should fire onSSEEvent for flow_complete events", async () => {
151
+ it("should fire onSSEEvent for execution_complete events", async () => {
152
152
  global.fetch = createMockFetch([
153
153
  sseData({
154
- type: "flow_complete",
154
+ type: "execution_complete",
155
155
  flowId: "flow_1",
156
156
  success: true,
157
157
  duration: 500,
@@ -175,11 +175,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
175
175
  );
176
176
 
177
177
  const flowCompleteEvents = capturedSSEEvents.filter(
178
- (e) => e.eventType === "flow_complete"
178
+ (e) => e.eventType === "execution_complete"
179
179
  );
180
180
  expect(flowCompleteEvents).toHaveLength(1);
181
181
  expect(flowCompleteEvents[0].payload).toMatchObject({
182
- type: "flow_complete",
182
+ type: "execution_complete",
183
183
  flowId: "flow_1",
184
184
  success: true,
185
185
  duration: 500
@@ -189,7 +189,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
189
189
  it("should fire onSSEEvent for every event in a multi-event stream", async () => {
190
190
  global.fetch = createMockFetch([
191
191
  sseData({
192
- type: "step_chunk",
192
+ type: "text_delta",
193
193
  id: "step_1",
194
194
  name: "Prompt 1",
195
195
  executionType: "prompt",
@@ -197,7 +197,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
197
197
  text: "Hello"
198
198
  }),
199
199
  sseData({
200
- type: "step_chunk",
200
+ type: "text_delta",
201
201
  id: "step_1",
202
202
  name: "Prompt 1",
203
203
  executionType: "prompt",
@@ -218,7 +218,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
218
218
  duration: 50
219
219
  }),
220
220
  sseData({
221
- type: "flow_complete",
221
+ type: "execution_complete",
222
222
  flowId: "flow_1",
223
223
  success: true,
224
224
  duration: 300
@@ -244,11 +244,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
244
244
 
245
245
  const types = capturedSSEEvents.map((e) => e.eventType);
246
246
  expect(types).toEqual([
247
- "step_chunk",
248
- "step_chunk",
247
+ "text_delta",
248
+ "text_delta",
249
249
  "tool_start",
250
250
  "tool_complete",
251
- "flow_complete"
251
+ "execution_complete"
252
252
  ]);
253
253
  });
254
254
 
@@ -257,11 +257,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
257
257
  // When SSE has no event prefix, payloadType = payload.type
258
258
  global.fetch = createMockFetch([
259
259
  sseData({
260
- type: "step_chunk",
260
+ type: "text_delta",
261
261
  text: "chunk1"
262
262
  }),
263
263
  sseData({
264
- type: "flow_complete",
264
+ type: "execution_complete",
265
265
  success: true
266
266
  })
267
267
  ]);
@@ -281,8 +281,8 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
281
281
  );
282
282
 
283
283
  // Event types should be resolved from payload.type
284
- expect(capturedSSEEvents[0].eventType).toBe("step_chunk");
285
- expect(capturedSSEEvents[1].eventType).toBe("flow_complete");
284
+ expect(capturedSSEEvents[0].eventType).toBe("text_delta");
285
+ expect(capturedSSEEvents[1].eventType).toBe("execution_complete");
286
286
  });
287
287
  });
288
288