@runtypelabs/persona 4.22.0 → 4.23.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.
Files changed (95) 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 +172 -27
  29. package/dist/index.d.ts +172 -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/launcher.global.js.map +1 -1
  35. package/dist/session-reconnect.d.cts +148 -14
  36. package/dist/session-reconnect.d.ts +148 -14
  37. package/dist/smart-dom-reader.d.cts +148 -14
  38. package/dist/smart-dom-reader.d.ts +148 -14
  39. package/dist/testing.cjs +3 -3
  40. package/dist/testing.d.cts +3 -4
  41. package/dist/testing.d.ts +3 -4
  42. package/dist/testing.js +3 -3
  43. package/dist/theme-editor-preview.cjs +71 -71
  44. package/dist/theme-editor-preview.d.cts +154 -14
  45. package/dist/theme-editor-preview.d.ts +154 -14
  46. package/dist/theme-editor-preview.js +48 -48
  47. package/dist/theme-editor.d.cts +148 -14
  48. package/dist/theme-editor.d.ts +148 -14
  49. package/dist/voice-runtime.d.cts +6 -2
  50. package/dist/voice-runtime.d.ts +6 -2
  51. package/package.json +1 -1
  52. package/src/ask-user-question-tool.ts +1 -1
  53. package/src/client-history-rest.test.ts +2 -1
  54. package/src/client-visitor-history.test.ts +106 -4
  55. package/src/client.composer-options.test.ts +1 -1
  56. package/src/client.interrupt.test.ts +2 -2
  57. package/src/client.test.ts +477 -915
  58. package/src/client.ts +134 -494
  59. package/src/client.unified-tools.test.ts +76 -0
  60. package/src/components/event-stream-badge-theming.test.ts +3 -0
  61. package/src/components/event-stream-view.test.ts +76 -76
  62. package/src/components/event-stream-view.ts +5 -0
  63. package/src/components/tool-bubble.test.ts +12 -0
  64. package/src/components/tool-bubble.ts +13 -2
  65. package/src/event-stream-view-bundle.test.ts +2 -2
  66. package/src/generated/runtype-openapi-contract.ts +129 -1
  67. package/src/internal/runtype-history-provider.test.ts +2 -2
  68. package/src/launcher-global.ts +1 -1
  69. package/src/session-history.test.ts +1 -1
  70. package/src/session.quote.test.ts +1 -1
  71. package/src/session.resubmit-from.test.ts +1 -1
  72. package/src/session.test.ts +5 -5
  73. package/src/session.ts +95 -36
  74. package/src/suggest-replies-tool.ts +1 -1
  75. package/src/testing/mock-stream.test.ts +37 -16
  76. package/src/testing/mock-stream.ts +14 -9
  77. package/src/types.ts +19 -14
  78. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  79. package/src/ui.composer-defer-one.test.ts +6 -34
  80. package/src/ui.composer-draft-persistence.test.ts +1 -1
  81. package/src/ui.composer-quote.test.ts +1 -1
  82. package/src/ui.message-history-actions.test.ts +1 -1
  83. package/src/ui.streaming-coalescing.test.ts +10 -13
  84. package/src/ui.ts +80 -32
  85. package/src/ui.typing-indicator.test.ts +216 -0
  86. package/src/ui.voice-custom-provider.test.ts +211 -0
  87. package/src/utils/event-stream-buffer.test.ts +13 -13
  88. package/src/utils/event-stream-capture.test.ts +20 -20
  89. package/src/utils/throughput-tracker.test.ts +100 -142
  90. package/src/utils/throughput-tracker.ts +18 -132
  91. package/src/utils/voice-support.ts +18 -1
  92. package/src/voice/voice-turn-tracker.ts +18 -0
  93. package/src/webmcp-bridge.ts +1 -1
  94. package/dist/event-stream-view-FKWDY3UB.js +0 -1
  95. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
package/src/types.ts CHANGED
@@ -991,15 +991,15 @@ export type AgentMessageMetadata = {
991
991
  */
992
992
  parentStepId?: string;
993
993
  /**
994
- * Set to `true` on a tool-variant message produced from a `step_await`
994
+ * Set to `true` on a tool-variant message produced from an `await`
995
995
  * event (`awaitReason: "local_tool_required"`). Signals to UI code that
996
996
  * the tool call is a LOCAL tool and the server is paused waiting for a
997
997
  * `POST /v1/dispatch/resume` with the user's answer keyed by tool name.
998
998
  */
999
999
  awaitingLocalTool?: boolean;
1000
1000
  /**
1001
- * The provider per-call id (`toolu_…`) carried on the `step_await` /
1002
- * `flow_await` events for a LOCAL tool (core#3878). Present only when the
1001
+ * The provider per-call id (`toolu_…`) carried on the `await`
1002
+ * event for a LOCAL tool (core#3878). Present only when the
1003
1003
  * server emits it. Two PARALLEL calls to the same tool in one turn share a
1004
1004
  * `toolName` (and a collapsed `toolId`) but get DISTINCT `webMcpToolCallId`s,
1005
1005
  * so this is the key the widget batches a single `/resume` on: preferred
@@ -3418,7 +3418,7 @@ export type EventStreamConfig = {
3418
3418
  /**
3419
3419
  * Custom badge color mappings by event type prefix or exact type.
3420
3420
  * Keys are matched as exact match first, then prefix match (keys ending with "_").
3421
- * @example { "flow_": { bg: "#dcfce7", text: "#166534" }, "error": { bg: "#fecaca", text: "#991b1b" } }
3421
+ * @example { "execution_": { bg: "#dcfce7", text: "#166534" }, "error": { bg: "#fecaca", text: "#991b1b" } }
3422
3422
  */
3423
3423
  badgeColors?: Record<string, EventStreamBadgeColor>;
3424
3424
  /**
@@ -4983,10 +4983,12 @@ export interface VoiceProvider {
4983
4983
  * `isFinal=false` is a live interim update (user partials, or assistant deltas
4984
4984
  * on providers that stream them); `isFinal=true` finalizes that role's text.
4985
4985
  * On the realtime `runtype` path, interim updates fire for the `user` only and
4986
- * the `assistant` arrives as a single final.
4986
+ * the `assistant` arrives as a single final. Providers with overlapping turns
4987
+ * must include the same turnId on user and assistant finals; without IDs,
4988
+ * providers must discard cancelled output before emitting the next user final.
4987
4989
  */
4988
4990
  onTranscript?(
4989
- callback: (role: 'user' | 'assistant', text: string, isFinal: boolean) => void,
4991
+ callback: (role: 'user' | 'assistant', text: string, isFinal: boolean, metadata?: { turnId?: string }) => void,
4990
4992
  ): void;
4991
4993
 
4992
4994
  /** Register a callback for per-turn latency metrics (realtime path). */
@@ -7561,16 +7563,16 @@ export type AgentWidgetConfig = {
7561
7563
  *
7562
7564
  * @example
7563
7565
  * ```typescript
7564
- * // For Runtype API format
7566
+ * // For a custom backend (Runtype unified events are handled natively)
7565
7567
  * config: {
7566
7568
  * parseSSEEvent: (data) => {
7567
- * if ((data.type === 'step_delta' || data.type === 'step_chunk') && (data.delta || data.chunk)) {
7568
- * return { text: data.delta ?? data.chunk };
7569
+ * if (data.type === 'token' && typeof data.value === 'string') {
7570
+ * return { text: data.value };
7569
7571
  * }
7570
- * if (data.type === 'flow_complete') {
7572
+ * if (data.type === 'done') {
7571
7573
  * return { done: true };
7572
7574
  * }
7573
- * if (data.type === 'step_error') {
7575
+ * if (data.type === 'failure') {
7574
7576
  * return { error: data.error };
7575
7577
  * }
7576
7578
  * return null; // Ignore other events
@@ -7787,7 +7789,7 @@ export type AgentWidgetReasoning = {
7787
7789
  /**
7788
7790
  * Reasoning channel scope (wire spec). `"turn"` is ordinary per-turn
7789
7791
  * thinking; `"loop"` is a cross-iteration agent reflection (the fold that
7790
- * replaced the legacy `agent_reflection` event). Absent for legacy streams.
7792
+ * replaced the legacy `agent_reflection` event). Optional when the stream does not specify a scope.
7791
7793
  */
7792
7794
  scope?: "turn" | "loop";
7793
7795
  startedAt?: number;
@@ -7802,6 +7804,9 @@ export type AgentWidgetToolCall = {
7802
7804
  args?: unknown;
7803
7805
  chunks?: string[];
7804
7806
  result?: unknown;
7807
+ /** Whether the tool succeeded; failure does not necessarily end the execution. */
7808
+ success?: boolean;
7809
+ error?: string;
7805
7810
  duration?: number;
7806
7811
  startedAt?: number;
7807
7812
  completedAt?: number;
@@ -7835,7 +7840,7 @@ export type AgentWidgetMessageVariant = "assistant" | "reasoning" | "tool" | "ap
7835
7840
 
7836
7841
  /**
7837
7842
  * Per-turn / per-step stop reason emitted by the runtime on
7838
- * `agent_turn_complete` and `step_complete` SSE events. The vocabulary is
7843
+ * `turn_complete` and `step_complete` SSE events. The vocabulary is
7839
7844
  * owned by the upstream Runtype API: do not extend without coordination.
7840
7845
  *
7841
7846
  * - `end_turn`: natural completion (no affordance needed)
@@ -7967,7 +7972,7 @@ export type AgentWidgetMessage = {
7967
7972
  */
7968
7973
  agentMetadata?: AgentMessageMetadata;
7969
7974
  /**
7970
- * Per-turn stop reason reported by the runtime on `agent_turn_complete`
7975
+ * Per-turn stop reason reported by the runtime on `turn_complete`
7971
7976
  * (agent-loop path) or the last `step_complete` for a prompt step
7972
7977
  * (dispatch / flow path). Absent when the API did not report a value.
7973
7978
  *
@@ -174,7 +174,7 @@ describe("renderAskUserQuestion plugin hook", () => {
174
174
  ok: true,
175
175
  body: new ReadableStream({
176
176
  start(c) {
177
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
177
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
178
178
  c.close();
179
179
  },
180
180
  }),
@@ -217,7 +217,7 @@ describe("renderAskUserQuestion plugin hook", () => {
217
217
  ok: true,
218
218
  body: new ReadableStream({
219
219
  start(c) {
220
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
220
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
221
221
  c.close();
222
222
  },
223
223
  }),
@@ -284,7 +284,7 @@ describe("renderAskUserQuestion plugin hook", () => {
284
284
  ok: true,
285
285
  body: new ReadableStream({
286
286
  start(c) {
287
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
287
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
288
288
  c.close();
289
289
  },
290
290
  }),
@@ -366,7 +366,7 @@ describe("renderAskUserQuestion plugin hook", () => {
366
366
  ok: true,
367
367
  body: new ReadableStream({
368
368
  start(c) {
369
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
369
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
370
370
  c.close();
371
371
  },
372
372
  }),
@@ -434,7 +434,7 @@ describe("renderAskUserQuestion plugin hook", () => {
434
434
  ok: true,
435
435
  body: new ReadableStream({
436
436
  start(c) {
437
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
437
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
438
438
  c.close();
439
439
  },
440
440
  }),
@@ -631,7 +631,7 @@ describe("renderAskUserQuestion plugin hook", () => {
631
631
  ok: true,
632
632
  body: new ReadableStream({
633
633
  start(c) {
634
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
634
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
635
635
  c.close();
636
636
  },
637
637
  }),
@@ -645,7 +645,7 @@ describe("renderAskUserQuestion plugin hook", () => {
645
645
  ok: true,
646
646
  body: new ReadableStream({
647
647
  start(c) {
648
- c.enqueue(new TextEncoder().encode('data: {"type":"flow_complete","success":true}\n\n'));
648
+ c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
649
649
  c.close();
650
650
  },
651
651
  }),
@@ -3,42 +3,14 @@
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
4
 
5
5
  import { createAgentExperience } from "./ui";
6
- import { createUnifiedEventWrite } from "./utils/__fixtures__/unified-translator.oracle";
7
-
8
- const legacyEvent = (type: string, data: Record<string, unknown>) =>
9
- `event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
6
+ import { buildAssistantTurnFrames } from "./testing/mock-stream";
10
7
 
11
8
  /** One complete assistant turn, in the same wire shape the widget expects. */
12
- const unifiedFrames = (): string => {
13
- let out = "";
14
- const write = createUnifiedEventWrite((chunk) => {
15
- out += chunk;
16
- });
17
- write(legacyEvent("flow_start", { flowId: "flow_1", flowName: "T", totalSteps: 1 }));
18
- write(
19
- legacyEvent("step_start", {
20
- id: "step_1",
21
- name: "Prompt",
22
- stepType: "prompt",
23
- index: 0,
24
- totalSteps: 1,
25
- })
26
- );
27
- write(legacyEvent("text_start", { messageId: "message_1" }));
28
- write(legacyEvent("step_delta", { id: "step_1", text: "answer" }));
29
- write(legacyEvent("text_end", { messageId: "message_1" }));
30
- write(
31
- legacyEvent("step_complete", {
32
- id: "step_1",
33
- name: "Prompt",
34
- stepType: "prompt",
35
- success: true,
36
- result: { response: "answer" },
37
- })
38
- );
39
- write(legacyEvent("flow_complete", { flowId: "flow_1", success: true }));
40
- return out;
41
- };
9
+ const unifiedFrames = (): string => [
10
+ { type: "execution_start", executionId: "exec-1", kind: "agent" },
11
+ ...buildAssistantTurnFrames({ executionId: "exec-1", text: "answer" }),
12
+ { type: "execution_complete", executionId: "exec-1", kind: "agent", success: true },
13
+ ].map((frame) => `data: ${JSON.stringify(frame)}\n\n`).join("");
42
14
 
43
15
  const mounts: HTMLElement[] = [];
44
16
  const controllers: ReturnType<typeof createAgentExperience>[] = [];
@@ -17,7 +17,7 @@ const streamResponse = () => {
17
17
  body: new ReadableStream({
18
18
  start(controller) {
19
19
  controller.enqueue(
20
- encoder.encode('data: {"type":"flow_complete","success":true}\n\n')
20
+ encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
21
21
  );
22
22
  controller.close();
23
23
  },
@@ -17,7 +17,7 @@ const streamResponse = () => {
17
17
  body: new ReadableStream({
18
18
  start(controller) {
19
19
  controller.enqueue(
20
- encoder.encode('data: {"type":"flow_complete","success":true}\n\n')
20
+ encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
21
21
  );
22
22
  controller.close();
23
23
  },
@@ -17,7 +17,7 @@ const streamResponse = () => {
17
17
  body: new ReadableStream({
18
18
  start(controller) {
19
19
  controller.enqueue(
20
- encoder.encode('data: {"type":"flow_complete","success":true}\n\n')
20
+ encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
21
21
  );
22
22
  controller.close();
23
23
  },
@@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
4
 
5
5
  import { createAgentExperience } from "./ui";
6
6
  import type { AgentWidgetStoredState } from "./types";
7
- import { createUnifiedEventWrite } from "./utils/__fixtures__/unified-translator.oracle";
8
7
 
9
8
  // The approval UI ships in a lazy chunk; provide it eagerly so this file's
10
9
  // synchronous render assertions hold. Per-file module isolation keeps the
@@ -48,7 +47,7 @@ const flushMicrotasks = async (times = 20) => {
48
47
  }
49
48
  };
50
49
 
51
- const legacyEvent = (type: string, data: Record<string, unknown>) =>
50
+ const sseEvent = (type: string, data: Record<string, unknown>) =>
52
51
  `event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
53
52
 
54
53
  const createStreamHarness = () => {
@@ -59,9 +58,7 @@ const createStreamHarness = () => {
59
58
  new Response(new ReadableStream<Uint8Array>({
60
59
  start(streamController) {
61
60
  controller = streamController;
62
- write = createUnifiedEventWrite((chunk) => {
63
- streamController.enqueue(encoder.encode(chunk));
64
- });
61
+ write = (chunk) => streamController.enqueue(encoder.encode(chunk));
65
62
  },
66
63
  }))
67
64
  );
@@ -69,7 +66,7 @@ const createStreamHarness = () => {
69
66
  fetchMock,
70
67
  send(type: string, data: Record<string, unknown>) {
71
68
  if (!write) throw new Error("stream not started");
72
- write(legacyEvent(type, data));
69
+ write(sseEvent(type, data));
73
70
  },
74
71
  close() {
75
72
  controller?.close();
@@ -85,7 +82,7 @@ const startTurn = async (mount: HTMLElement) => {
85
82
  };
86
83
 
87
84
  const startText = (stream: ReturnType<typeof createStreamHarness>, deltas: string[]) => {
88
- stream.send("flow_start", { flowId: "flow_1", flowName: "Test", totalSteps: 1 });
85
+ stream.send("execution_start", { kind: "flow", executionId: "exec-1", flowId: "flow_1", flowName: "Test", totalSteps: 1 });
89
86
  stream.send("step_start", {
90
87
  id: "step_1",
91
88
  name: "Prompt",
@@ -93,12 +90,12 @@ const startText = (stream: ReturnType<typeof createStreamHarness>, deltas: strin
93
90
  index: 0,
94
91
  totalSteps: 1,
95
92
  });
96
- stream.send("text_start", { messageId: "message_1" });
97
- deltas.forEach((text) => stream.send("step_delta", { id: "step_1", text }));
93
+ stream.send("text_start", { id: "message_1" });
94
+ deltas.forEach((text) => stream.send("text_delta", { id: "message_1", delta: text }));
98
95
  };
99
96
 
100
97
  const finishText = (stream: ReturnType<typeof createStreamHarness>) => {
101
- stream.send("text_end", { messageId: "message_1" });
98
+ stream.send("text_complete", { id: "message_1" });
102
99
  stream.send("step_complete", {
103
100
  id: "step_1",
104
101
  name: "Prompt",
@@ -106,7 +103,7 @@ const finishText = (stream: ReturnType<typeof createStreamHarness>) => {
106
103
  success: true,
107
104
  result: { response: "Hello world" },
108
105
  });
109
- stream.send("flow_complete", { flowId: "flow_1", success: true });
106
+ stream.send("execution_complete", { kind: "flow", executionId: "exec-1", flowId: "flow_1", success: true });
110
107
  stream.close();
111
108
  };
112
109
 
@@ -145,8 +142,8 @@ describe("pure streaming text update coalescing", () => {
145
142
  startText(stream, ["Hel"]);
146
143
  await flushMicrotasks();
147
144
  const afterFirstDelta = saves.length;
148
- stream.send("step_delta", { id: "step_1", text: "lo" });
149
- stream.send("step_delta", { id: "step_1", text: " world" });
145
+ stream.send("text_delta", { id: "message_1", delta: "lo" });
146
+ stream.send("text_delta", { id: "message_1", delta: " world" });
150
147
  await flushMicrotasks();
151
148
 
152
149
  expect(saves).toHaveLength(afterFirstDelta);
package/src/ui.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { usesSessionVoice } from "./utils/voice-support";
1
2
  import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
2
3
  import { resolveSanitizer } from "./utils/sanitize";
3
4
  import { stabilizeStreamingTables } from "./utils/streaming-table";
@@ -730,6 +731,12 @@ type Controller = {
730
731
  * Required host logout wiring.
731
732
  */
732
733
  resetHistoryIdentity: () => Promise<{ remoteRevocationConfirmed: boolean }>;
734
+ /**
735
+ * Read the current browser visitor credential for a trusted voice integration.
736
+ * Returns null before minting, outside client-token mode, or after teardown.
737
+ * Never put this bearer credential in logs, URLs, or model context.
738
+ */
739
+ getVisitorToken: () => Promise<string | null>;
733
740
  /** Latest sanitized identity state; carries no token, proof, or identity id. */
734
741
  getHistoryIdentityStatus: () => HistoryIdentityStatus;
735
742
  /** `focus: false` keeps focus where it is (programmatic/preview opens). */
@@ -3272,7 +3279,7 @@ export const createAgentExperience = (
3272
3279
 
3273
3280
  removeAskUserQuestionSheet(askUserOverlay, toolCallId);
3274
3281
 
3275
- // Branch: LOCAL-tool pause (step_await) resumes via /resume with structured
3282
+ // Branch: LOCAL-tool pause (await) resumes via /resume with structured
3276
3283
  // toolOutputs; legacy path sends as a plain user message.
3277
3284
  const sourceMessage = sessionRef.current
3278
3285
  .getMessages()
@@ -5245,7 +5252,7 @@ export const createAgentExperience = (
5245
5252
  setTimeout(() => {
5246
5253
  if (!voiceState.active) {
5247
5254
  voiceState.manuallyDeactivated = false;
5248
- if (config.voiceRecognition?.provider?.type === 'runtype') {
5255
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
5249
5256
  session.toggleVoice().then(() => {
5250
5257
  voiceState.active = session.isVoiceActive();
5251
5258
  emitVoiceState("restore");
@@ -6319,20 +6326,23 @@ export const createAgentExperience = (
6319
6326
  // reasoning removal, no threshold variant.
6320
6327
  const dropCompletedToolCalls =
6321
6328
  config.features?.toolCallDisplay?.completedVisibility === "removed";
6322
-
6323
- messages.forEach((message) => {
6329
+ /** A finished reasoning/tool row that `completedVisibility` removes from the transcript. */
6330
+ const isDroppedCompletedRow = (message: AgentWidgetMessage): boolean => {
6324
6331
  if (
6325
6332
  message.variant === "reasoning" &&
6326
- message.reasoning?.status === "complete" &&
6327
- dropReasoningRow(message.reasoning)
6333
+ message.reasoning?.status === "complete"
6328
6334
  ) {
6329
- return;
6335
+ return dropReasoningRow(message.reasoning);
6330
6336
  }
6331
- if (
6337
+ return (
6332
6338
  dropCompletedToolCalls &&
6333
6339
  message.variant === "tool" &&
6334
6340
  message.toolCall?.status === "complete"
6335
- ) {
6341
+ );
6342
+ };
6343
+
6344
+ messages.forEach((message) => {
6345
+ if (isDroppedCompletedRow(message)) {
6336
6346
  return;
6337
6347
  }
6338
6348
  activeMessageIds.add(message.id);
@@ -6408,11 +6418,11 @@ export const createAgentExperience = (
6408
6418
  if (cachedWrapper) {
6409
6419
  tempContainer.appendChild(cachedWrapper.cloneNode(true));
6410
6420
  // Keep the overlay sheet alive only while the server is actively
6411
- // waiting on the user (awaitingLocalTool === true). Before step_await
6421
+ // waiting on the user (awaitingLocalTool === true). Before await
6412
6422
  // fires, or after the answer resumes the flow, omit from
6413
6423
  // liveAskToolIds so the prune loop below removes any stale DOM sheet.
6414
6424
  // Guards against lingering skeleton sheets from tool_start events
6415
- // that never get a matching step_await (e.g. LLM-hallucinated trailing
6425
+ // that never get a matching await (e.g. LLM-hallucinated trailing
6416
6426
  // ask_user_question calls at end-of-turn).
6417
6427
  if (
6418
6428
  isAskUserQuestionMessage(message) &&
@@ -7002,16 +7012,40 @@ export const createAgentExperience = (
7002
7012
  // Add standalone typing indicator only if streaming but no assistant message is streaming yet
7003
7013
  // (This shows while waiting for the stream to start)
7004
7014
  // Check for ANY streaming assistant message, even if empty (to avoid duplicate bubbles)
7015
+ //
7016
+ // Tool and reasoning rows hidden by `features.showToolCalls: false` /
7017
+ // `features.showReasoning: false`, or removed once complete by their
7018
+ // `completedVisibility` setting, never render, so they must not count as
7019
+ // "the assistant is already visibly responding": otherwise the transcript
7020
+ // goes blank for the whole tool phase. When those rows are shown, their own
7021
+ // bubble carries the loading animation and the standalone dots stay hidden.
7022
+ const isHiddenActivityRow = (msg: AgentWidgetMessage): boolean =>
7023
+ (msg.variant === "tool" && !showToolCalls) ||
7024
+ (msg.variant === "reasoning" && !showReasoning) ||
7025
+ isDroppedCompletedRow(msg);
7005
7026
  const hasStreamingAssistantMessage = messages.some(
7006
- (msg) => msg.role === "assistant" && msg.streaming
7027
+ (msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
7007
7028
  );
7008
7029
 
7009
7030
  // Also check if there's a recently completed assistant message (streaming just ended)
7010
7031
  // This prevents flicker when the message completes but isStreaming hasn't updated yet
7011
7032
  // Approval-variant messages are UI controls, not content: exclude them so the typing
7012
- // indicator still shows while the agent resumes after approval
7013
- const lastMessage = messages[messages.length - 1];
7014
- const hasRecentAssistantResponse = lastMessage?.role === "assistant" && !lastMessage.streaming && lastMessage.variant !== "approval";
7033
+ // indicator still shows while the agent resumes after approval. Hidden tool/reasoning
7034
+ // rows are skipped for the same reason (the gap between a tool finishing and the
7035
+ // model's next text would otherwise show nothing), and a hidden row *after* the
7036
+ // last visible message means the agent has moved on to more work, so that visible
7037
+ // message is not the turn's final response.
7038
+ let lastVisibleIndex = messages.length - 1;
7039
+ while (lastVisibleIndex >= 0 && isHiddenActivityRow(messages[lastVisibleIndex])) {
7040
+ lastVisibleIndex--;
7041
+ }
7042
+ const lastMessage = lastVisibleIndex >= 0 ? messages[lastVisibleIndex] : undefined;
7043
+ const hasHiddenActivityAfterLastVisible = lastVisibleIndex < messages.length - 1;
7044
+ const hasRecentAssistantResponse =
7045
+ lastMessage?.role === "assistant" &&
7046
+ !lastMessage.streaming &&
7047
+ lastMessage.variant !== "approval" &&
7048
+ !hasHiddenActivityAfterLastVisible;
7015
7049
 
7016
7050
  if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
7017
7051
  // Get loading indicator using priority chain: plugin -> config -> default
@@ -7925,6 +7959,10 @@ export const createAgentExperience = (
7925
7959
  };
7926
7960
 
7927
7961
  const isComposerInputDisabled = (): boolean => composerLock.inputDisabled;
7962
+ const isActiveSessionVoice = (): boolean =>
7963
+ usesSessionVoice(config.voiceRecognition?.provider) &&
7964
+ !!session && (session.isBargeInActive() ||
7965
+ ["listening", "processing", "speaking"].includes(session.getVoiceStatus()));
7928
7966
  /** Any submission path is blocked; stop is not a submission. */
7929
7967
  const isComposerSendBlocked = (): boolean =>
7930
7968
  composerLock.inputDisabled || composerLock.sendDisabled;
@@ -8033,7 +8071,7 @@ export const createAgentExperience = (
8033
8071
  button.disabled = locked;
8034
8072
  });
8035
8073
  }
8036
- if (micButton) micButton.disabled = blocked;
8074
+ if (micButton) micButton.disabled = locked || (streaming && !isActiveSessionVoice());
8037
8075
 
8038
8076
  suggestionManagers.forEach((manager) => {
8039
8077
  manager.elements.forEach((element) => {
@@ -8860,7 +8898,7 @@ export const createAgentExperience = (
8860
8898
  // per-state UI (e.g. a listening/speaking status dock). Fires for every
8861
8899
  // provider; the mic-button styling below is runtype-specific.
8862
8900
  eventBus.emit("voice:status", { status, timestamp: Date.now() });
8863
- if (config.voiceRecognition?.provider?.type !== 'runtype') return;
8901
+ if (status !== 'disconnected' && !usesSessionVoice(config.voiceRecognition?.provider)) return;
8864
8902
 
8865
8903
  switch (status) {
8866
8904
  case 'listening':
@@ -8950,6 +8988,7 @@ export const createAgentExperience = (
8950
8988
  destroyCallbacks.push(() => {
8951
8989
  session.destroy();
8952
8990
  visitorStore?.destroy();
8991
+ visitorStore = null;
8953
8992
  });
8954
8993
 
8955
8994
  // Mirror read-aloud playback state into the action buttons, and surface it as
@@ -8983,7 +9022,7 @@ export const createAgentExperience = (
8983
9022
  scrollSendSeeded = true;
8984
9023
 
8985
9024
  // Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
8986
- if (config.voiceRecognition?.provider?.type === 'runtype') {
9025
+ if (config.voiceRecognition?.enabled === true && usesSessionVoice(config.voiceRecognition.provider)) {
8987
9026
  try {
8988
9027
  session.setupVoice();
8989
9028
  } catch (err) {
@@ -12622,7 +12661,7 @@ export const createAgentExperience = (
12622
12661
  // Dictation is composition: a locked input takes none of it.
12623
12662
  if (isComposerInputDisabled()) return;
12624
12663
  // Runtype provider: use session.toggleVoice() (WebSocket-based STT)
12625
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12664
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12626
12665
  const voiceStatus = session.getVoiceStatus();
12627
12666
  const interruptionMode = session.getVoiceInterruptionMode();
12628
12667
 
@@ -12693,7 +12732,7 @@ export const createAgentExperience = (
12693
12732
  // The click listener itself is registered by `wireComposerSurface`, so a
12694
12733
  // composer rebuild unwires it with the rest of the surface.
12695
12734
  destroyCallbacks.push(() => {
12696
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12735
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12697
12736
  if (session.isVoiceActive()) session.toggleVoice();
12698
12737
  removeRuntypeMicStateStyles();
12699
12738
  } else {
@@ -12722,7 +12761,7 @@ export const createAgentExperience = (
12722
12761
  }
12723
12762
  setTimeout(() => {
12724
12763
  if (!voiceState.active && !voiceState.manuallyDeactivated) {
12725
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12764
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12726
12765
  session.toggleVoice().then(() => {
12727
12766
  voiceState.active = session.isVoiceActive();
12728
12767
  emitVoiceState("auto");
@@ -14308,9 +14347,9 @@ export const createAgentExperience = (
14308
14347
  typeof window !== 'undefined' &&
14309
14348
  (typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
14310
14349
  typeof (window as any).SpeechRecognition !== 'undefined');
14311
- const hasRuntypeProvider =
14312
- config.voiceRecognition?.provider?.type === 'runtype';
14313
- const hasVoiceInput = hasSpeechRecognition || hasRuntypeProvider;
14350
+ const hasSessionProvider =
14351
+ usesSessionVoice(config.voiceRecognition?.provider);
14352
+ const hasVoiceInput = hasSpeechRecognition || hasSessionProvider;
14314
14353
 
14315
14354
  if (voiceRecognitionEnabled && hasVoiceInput) {
14316
14355
  // Create or update mic button
@@ -14343,7 +14382,7 @@ export const createAgentExperience = (
14343
14382
  );
14344
14383
 
14345
14384
  // Set disabled state
14346
- micButton.disabled = session.isStreaming();
14385
+ micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
14347
14386
  }
14348
14387
  } else {
14349
14388
  // Update existing mic button with new config
@@ -14439,14 +14478,14 @@ export const createAgentExperience = (
14439
14478
 
14440
14479
  // Show and update disabled state
14441
14480
  micButtonWrapper.style.display = "";
14442
- micButton.disabled = session.isStreaming();
14481
+ micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
14443
14482
  }
14444
14483
  } else {
14445
14484
  // Hide mic button
14446
14485
  if (micButton && micButtonWrapper) {
14447
14486
  micButtonWrapper.style.display = "none";
14448
14487
  // Stop any active recording if disabling
14449
- if (config.voiceRecognition?.provider?.type === 'runtype') {
14488
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
14450
14489
  if (session.isVoiceActive()) session.toggleVoice();
14451
14490
  } else if (isRecording) {
14452
14491
  stopVoiceRecognition();
@@ -15015,8 +15054,8 @@ export const createAgentExperience = (
15015
15054
  },
15016
15055
  startVoiceRecognition(): boolean {
15017
15056
  if (session.isStreaming()) return false;
15018
- if (config.voiceRecognition?.provider?.type === 'runtype') {
15019
- if (session.isVoiceActive()) return true;
15057
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
15058
+ if (session.isVoiceActive() || session.isBargeInActive()) return true;
15020
15059
  if (!open && isPanelToggleable()) setOpenState(true, "system");
15021
15060
  voiceState.manuallyDeactivated = false;
15022
15061
  persistVoiceMetadata();
@@ -15037,9 +15076,12 @@ export const createAgentExperience = (
15037
15076
  return true;
15038
15077
  },
15039
15078
  stopVoiceRecognition(): boolean {
15040
- if (config.voiceRecognition?.provider?.type === 'runtype') {
15041
- if (!session.isVoiceActive()) return false;
15042
- session.toggleVoice().then(() => {
15079
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
15080
+ if (!session.isVoiceActive() && !session.isBargeInActive()) return false;
15081
+ const stop = session.isBargeInActive()
15082
+ ? session.deactivateBargeIn()
15083
+ : session.toggleVoice();
15084
+ stop.then(() => {
15043
15085
  voiceState.active = false;
15044
15086
  voiceState.manuallyDeactivated = true;
15045
15087
  persistVoiceMetadata();
@@ -15180,6 +15222,12 @@ export const createAgentExperience = (
15180
15222
  resetHistoryIdentity(): Promise<{ remoteRevocationConfirmed: boolean }> {
15181
15223
  return resetHistoryIdentity();
15182
15224
  },
15225
+ async getVisitorToken(): Promise<string | null> {
15226
+ const store = visitorStore;
15227
+ if (!store) return null;
15228
+ const token = await store.get();
15229
+ return store === visitorStore ? token : null;
15230
+ },
15183
15231
  getHistoryIdentityStatus(): HistoryIdentityStatus {
15184
15232
  if (!historyProvider) {
15185
15233
  return historyFeatureEnabled()