@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
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";
@@ -3272,7 +3273,7 @@ export const createAgentExperience = (
3272
3273
 
3273
3274
  removeAskUserQuestionSheet(askUserOverlay, toolCallId);
3274
3275
 
3275
- // Branch: LOCAL-tool pause (step_await) resumes via /resume with structured
3276
+ // Branch: LOCAL-tool pause (await) resumes via /resume with structured
3276
3277
  // toolOutputs; legacy path sends as a plain user message.
3277
3278
  const sourceMessage = sessionRef.current
3278
3279
  .getMessages()
@@ -5245,7 +5246,7 @@ export const createAgentExperience = (
5245
5246
  setTimeout(() => {
5246
5247
  if (!voiceState.active) {
5247
5248
  voiceState.manuallyDeactivated = false;
5248
- if (config.voiceRecognition?.provider?.type === 'runtype') {
5249
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
5249
5250
  session.toggleVoice().then(() => {
5250
5251
  voiceState.active = session.isVoiceActive();
5251
5252
  emitVoiceState("restore");
@@ -6319,20 +6320,23 @@ export const createAgentExperience = (
6319
6320
  // reasoning removal, no threshold variant.
6320
6321
  const dropCompletedToolCalls =
6321
6322
  config.features?.toolCallDisplay?.completedVisibility === "removed";
6322
-
6323
- messages.forEach((message) => {
6323
+ /** A finished reasoning/tool row that `completedVisibility` removes from the transcript. */
6324
+ const isDroppedCompletedRow = (message: AgentWidgetMessage): boolean => {
6324
6325
  if (
6325
6326
  message.variant === "reasoning" &&
6326
- message.reasoning?.status === "complete" &&
6327
- dropReasoningRow(message.reasoning)
6327
+ message.reasoning?.status === "complete"
6328
6328
  ) {
6329
- return;
6329
+ return dropReasoningRow(message.reasoning);
6330
6330
  }
6331
- if (
6331
+ return (
6332
6332
  dropCompletedToolCalls &&
6333
6333
  message.variant === "tool" &&
6334
6334
  message.toolCall?.status === "complete"
6335
- ) {
6335
+ );
6336
+ };
6337
+
6338
+ messages.forEach((message) => {
6339
+ if (isDroppedCompletedRow(message)) {
6336
6340
  return;
6337
6341
  }
6338
6342
  activeMessageIds.add(message.id);
@@ -6408,11 +6412,11 @@ export const createAgentExperience = (
6408
6412
  if (cachedWrapper) {
6409
6413
  tempContainer.appendChild(cachedWrapper.cloneNode(true));
6410
6414
  // Keep the overlay sheet alive only while the server is actively
6411
- // waiting on the user (awaitingLocalTool === true). Before step_await
6415
+ // waiting on the user (awaitingLocalTool === true). Before await
6412
6416
  // fires, or after the answer resumes the flow, omit from
6413
6417
  // liveAskToolIds so the prune loop below removes any stale DOM sheet.
6414
6418
  // Guards against lingering skeleton sheets from tool_start events
6415
- // that never get a matching step_await (e.g. LLM-hallucinated trailing
6419
+ // that never get a matching await (e.g. LLM-hallucinated trailing
6416
6420
  // ask_user_question calls at end-of-turn).
6417
6421
  if (
6418
6422
  isAskUserQuestionMessage(message) &&
@@ -7002,16 +7006,40 @@ export const createAgentExperience = (
7002
7006
  // Add standalone typing indicator only if streaming but no assistant message is streaming yet
7003
7007
  // (This shows while waiting for the stream to start)
7004
7008
  // Check for ANY streaming assistant message, even if empty (to avoid duplicate bubbles)
7009
+ //
7010
+ // Tool and reasoning rows hidden by `features.showToolCalls: false` /
7011
+ // `features.showReasoning: false`, or removed once complete by their
7012
+ // `completedVisibility` setting, never render, so they must not count as
7013
+ // "the assistant is already visibly responding": otherwise the transcript
7014
+ // goes blank for the whole tool phase. When those rows are shown, their own
7015
+ // bubble carries the loading animation and the standalone dots stay hidden.
7016
+ const isHiddenActivityRow = (msg: AgentWidgetMessage): boolean =>
7017
+ (msg.variant === "tool" && !showToolCalls) ||
7018
+ (msg.variant === "reasoning" && !showReasoning) ||
7019
+ isDroppedCompletedRow(msg);
7005
7020
  const hasStreamingAssistantMessage = messages.some(
7006
- (msg) => msg.role === "assistant" && msg.streaming
7021
+ (msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
7007
7022
  );
7008
7023
 
7009
7024
  // Also check if there's a recently completed assistant message (streaming just ended)
7010
7025
  // This prevents flicker when the message completes but isStreaming hasn't updated yet
7011
7026
  // 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";
7027
+ // indicator still shows while the agent resumes after approval. Hidden tool/reasoning
7028
+ // rows are skipped for the same reason (the gap between a tool finishing and the
7029
+ // model's next text would otherwise show nothing), and a hidden row *after* the
7030
+ // last visible message means the agent has moved on to more work, so that visible
7031
+ // message is not the turn's final response.
7032
+ let lastVisibleIndex = messages.length - 1;
7033
+ while (lastVisibleIndex >= 0 && isHiddenActivityRow(messages[lastVisibleIndex])) {
7034
+ lastVisibleIndex--;
7035
+ }
7036
+ const lastMessage = lastVisibleIndex >= 0 ? messages[lastVisibleIndex] : undefined;
7037
+ const hasHiddenActivityAfterLastVisible = lastVisibleIndex < messages.length - 1;
7038
+ const hasRecentAssistantResponse =
7039
+ lastMessage?.role === "assistant" &&
7040
+ !lastMessage.streaming &&
7041
+ lastMessage.variant !== "approval" &&
7042
+ !hasHiddenActivityAfterLastVisible;
7015
7043
 
7016
7044
  if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
7017
7045
  // Get loading indicator using priority chain: plugin -> config -> default
@@ -7925,6 +7953,10 @@ export const createAgentExperience = (
7925
7953
  };
7926
7954
 
7927
7955
  const isComposerInputDisabled = (): boolean => composerLock.inputDisabled;
7956
+ const isActiveSessionVoice = (): boolean =>
7957
+ usesSessionVoice(config.voiceRecognition?.provider) &&
7958
+ !!session && (session.isBargeInActive() ||
7959
+ ["listening", "processing", "speaking"].includes(session.getVoiceStatus()));
7928
7960
  /** Any submission path is blocked; stop is not a submission. */
7929
7961
  const isComposerSendBlocked = (): boolean =>
7930
7962
  composerLock.inputDisabled || composerLock.sendDisabled;
@@ -8033,7 +8065,7 @@ export const createAgentExperience = (
8033
8065
  button.disabled = locked;
8034
8066
  });
8035
8067
  }
8036
- if (micButton) micButton.disabled = blocked;
8068
+ if (micButton) micButton.disabled = locked || (streaming && !isActiveSessionVoice());
8037
8069
 
8038
8070
  suggestionManagers.forEach((manager) => {
8039
8071
  manager.elements.forEach((element) => {
@@ -8860,7 +8892,7 @@ export const createAgentExperience = (
8860
8892
  // per-state UI (e.g. a listening/speaking status dock). Fires for every
8861
8893
  // provider; the mic-button styling below is runtype-specific.
8862
8894
  eventBus.emit("voice:status", { status, timestamp: Date.now() });
8863
- if (config.voiceRecognition?.provider?.type !== 'runtype') return;
8895
+ if (status !== 'disconnected' && !usesSessionVoice(config.voiceRecognition?.provider)) return;
8864
8896
 
8865
8897
  switch (status) {
8866
8898
  case 'listening':
@@ -8983,7 +9015,7 @@ export const createAgentExperience = (
8983
9015
  scrollSendSeeded = true;
8984
9016
 
8985
9017
  // Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
8986
- if (config.voiceRecognition?.provider?.type === 'runtype') {
9018
+ if (config.voiceRecognition?.enabled === true && usesSessionVoice(config.voiceRecognition.provider)) {
8987
9019
  try {
8988
9020
  session.setupVoice();
8989
9021
  } catch (err) {
@@ -12622,7 +12654,7 @@ export const createAgentExperience = (
12622
12654
  // Dictation is composition: a locked input takes none of it.
12623
12655
  if (isComposerInputDisabled()) return;
12624
12656
  // Runtype provider: use session.toggleVoice() (WebSocket-based STT)
12625
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12657
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12626
12658
  const voiceStatus = session.getVoiceStatus();
12627
12659
  const interruptionMode = session.getVoiceInterruptionMode();
12628
12660
 
@@ -12693,7 +12725,7 @@ export const createAgentExperience = (
12693
12725
  // The click listener itself is registered by `wireComposerSurface`, so a
12694
12726
  // composer rebuild unwires it with the rest of the surface.
12695
12727
  destroyCallbacks.push(() => {
12696
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12728
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12697
12729
  if (session.isVoiceActive()) session.toggleVoice();
12698
12730
  removeRuntypeMicStateStyles();
12699
12731
  } else {
@@ -12722,7 +12754,7 @@ export const createAgentExperience = (
12722
12754
  }
12723
12755
  setTimeout(() => {
12724
12756
  if (!voiceState.active && !voiceState.manuallyDeactivated) {
12725
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12757
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12726
12758
  session.toggleVoice().then(() => {
12727
12759
  voiceState.active = session.isVoiceActive();
12728
12760
  emitVoiceState("auto");
@@ -14308,9 +14340,9 @@ export const createAgentExperience = (
14308
14340
  typeof window !== 'undefined' &&
14309
14341
  (typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
14310
14342
  typeof (window as any).SpeechRecognition !== 'undefined');
14311
- const hasRuntypeProvider =
14312
- config.voiceRecognition?.provider?.type === 'runtype';
14313
- const hasVoiceInput = hasSpeechRecognition || hasRuntypeProvider;
14343
+ const hasSessionProvider =
14344
+ usesSessionVoice(config.voiceRecognition?.provider);
14345
+ const hasVoiceInput = hasSpeechRecognition || hasSessionProvider;
14314
14346
 
14315
14347
  if (voiceRecognitionEnabled && hasVoiceInput) {
14316
14348
  // Create or update mic button
@@ -14343,7 +14375,7 @@ export const createAgentExperience = (
14343
14375
  );
14344
14376
 
14345
14377
  // Set disabled state
14346
- micButton.disabled = session.isStreaming();
14378
+ micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
14347
14379
  }
14348
14380
  } else {
14349
14381
  // Update existing mic button with new config
@@ -14439,14 +14471,14 @@ export const createAgentExperience = (
14439
14471
 
14440
14472
  // Show and update disabled state
14441
14473
  micButtonWrapper.style.display = "";
14442
- micButton.disabled = session.isStreaming();
14474
+ micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
14443
14475
  }
14444
14476
  } else {
14445
14477
  // Hide mic button
14446
14478
  if (micButton && micButtonWrapper) {
14447
14479
  micButtonWrapper.style.display = "none";
14448
14480
  // Stop any active recording if disabling
14449
- if (config.voiceRecognition?.provider?.type === 'runtype') {
14481
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
14450
14482
  if (session.isVoiceActive()) session.toggleVoice();
14451
14483
  } else if (isRecording) {
14452
14484
  stopVoiceRecognition();
@@ -15015,8 +15047,8 @@ export const createAgentExperience = (
15015
15047
  },
15016
15048
  startVoiceRecognition(): boolean {
15017
15049
  if (session.isStreaming()) return false;
15018
- if (config.voiceRecognition?.provider?.type === 'runtype') {
15019
- if (session.isVoiceActive()) return true;
15050
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
15051
+ if (session.isVoiceActive() || session.isBargeInActive()) return true;
15020
15052
  if (!open && isPanelToggleable()) setOpenState(true, "system");
15021
15053
  voiceState.manuallyDeactivated = false;
15022
15054
  persistVoiceMetadata();
@@ -15037,9 +15069,12 @@ export const createAgentExperience = (
15037
15069
  return true;
15038
15070
  },
15039
15071
  stopVoiceRecognition(): boolean {
15040
- if (config.voiceRecognition?.provider?.type === 'runtype') {
15041
- if (!session.isVoiceActive()) return false;
15042
- session.toggleVoice().then(() => {
15072
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
15073
+ if (!session.isVoiceActive() && !session.isBargeInActive()) return false;
15074
+ const stop = session.isBargeInActive()
15075
+ ? session.deactivateBargeIn()
15076
+ : session.toggleVoice();
15077
+ stop.then(() => {
15043
15078
  voiceState.active = false;
15044
15079
  voiceState.manuallyDeactivated = true;
15045
15080
  persistVoiceMetadata();