@runtypelabs/persona 4.21.1 → 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 (129) 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-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +221 -16
  12. package/dist/approval-ui.d.ts +221 -16
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/artifacts-ui-F4N6VMKF.js +7 -0
  15. package/dist/artifacts-ui.cjs +1 -1
  16. package/dist/artifacts-ui.d.cts +221 -16
  17. package/dist/artifacts-ui.d.ts +221 -16
  18. package/dist/artifacts-ui.js +1 -1
  19. package/dist/chunk-NQLDCJAP.js +4 -0
  20. package/dist/codegen.cjs +1 -1
  21. package/dist/codegen.js +1 -1
  22. package/dist/context-mentions.d.cts +221 -16
  23. package/dist/context-mentions.d.ts +221 -16
  24. package/dist/event-stream-view-GL3ADRY2.js +1 -0
  25. package/dist/event-stream-view.cjs +1 -1
  26. package/dist/event-stream-view.d.cts +221 -16
  27. package/dist/event-stream-view.d.ts +221 -16
  28. package/dist/event-stream-view.js +1 -1
  29. package/dist/forms-ui.d.cts +234 -25
  30. package/dist/forms-ui.d.ts +234 -25
  31. package/dist/history-view.d.cts +136 -6
  32. package/dist/history-view.d.ts +136 -6
  33. package/dist/index.cjs +70 -70
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +302 -32
  36. package/dist/index.d.ts +302 -32
  37. package/dist/index.global.js +54 -54
  38. package/dist/index.global.js.map +1 -1
  39. package/dist/index.js +69 -69
  40. package/dist/index.js.map +1 -1
  41. package/dist/launcher.global.js +2 -2
  42. package/dist/launcher.global.js.map +1 -1
  43. package/dist/session-reconnect.d.cts +221 -16
  44. package/dist/session-reconnect.d.ts +221 -16
  45. package/dist/smart-dom-reader.d.cts +221 -16
  46. package/dist/smart-dom-reader.d.ts +221 -16
  47. package/dist/testing.cjs +3 -3
  48. package/dist/testing.d.cts +3 -4
  49. package/dist/testing.d.ts +3 -4
  50. package/dist/testing.js +3 -3
  51. package/dist/theme-editor-preview.cjs +73 -73
  52. package/dist/theme-editor-preview.d.cts +221 -16
  53. package/dist/theme-editor-preview.d.ts +221 -16
  54. package/dist/theme-editor-preview.js +51 -51
  55. package/dist/theme-editor.cjs +4 -4
  56. package/dist/theme-editor.d.cts +221 -16
  57. package/dist/theme-editor.d.ts +221 -16
  58. package/dist/theme-editor.js +2 -2
  59. package/dist/theme-reference.cjs +1 -1
  60. package/dist/theme-reference.d.cts +65 -1
  61. package/dist/theme-reference.d.ts +65 -1
  62. package/dist/theme-reference.js +1 -1
  63. package/dist/voice-runtime.d.cts +6 -2
  64. package/dist/voice-runtime.d.ts +6 -2
  65. package/dist/webmcp-polyfill.js +1 -4
  66. package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
  67. package/dist/webmcp-runtime.cjs +2 -2
  68. package/dist/webmcp-runtime.d.cts +34 -2
  69. package/dist/webmcp-runtime.d.ts +34 -2
  70. package/dist/webmcp-runtime.js +2 -2
  71. package/package.json +2 -2
  72. package/src/ask-user-question-tool.ts +1 -1
  73. package/src/client-history-rest.test.ts +2 -1
  74. package/src/client-visitor-history.test.ts +20 -4
  75. package/src/client.composer-options.test.ts +1 -1
  76. package/src/client.interrupt.test.ts +2 -2
  77. package/src/client.test.ts +477 -915
  78. package/src/client.ts +134 -494
  79. package/src/client.unified-tools.test.ts +76 -0
  80. package/src/components/artifact-preview.ts +5 -0
  81. package/src/components/event-stream-badge-theming.test.ts +162 -0
  82. package/src/components/event-stream-view.test.ts +76 -76
  83. package/src/components/event-stream-view.ts +55 -20
  84. package/src/components/tool-bubble.test.ts +12 -0
  85. package/src/components/tool-bubble.ts +13 -2
  86. package/src/event-stream-view-bundle.test.ts +2 -2
  87. package/src/generated/runtype-openapi-contract.ts +137 -1
  88. package/src/index-core.ts +2 -0
  89. package/src/internal/runtype-history-provider.test.ts +2 -2
  90. package/src/runtime/init.registry.test.ts +56 -0
  91. package/src/runtime/init.ts +17 -0
  92. package/src/session-history.test.ts +1 -1
  93. package/src/session.quote.test.ts +1 -1
  94. package/src/session.resubmit-from.test.ts +1 -1
  95. package/src/session.test.ts +5 -5
  96. package/src/session.ts +95 -36
  97. package/src/suggest-replies-tool.ts +1 -1
  98. package/src/testing/mock-stream.test.ts +37 -16
  99. package/src/testing/mock-stream.ts +14 -9
  100. package/src/types/theme.ts +41 -1
  101. package/src/types.ts +57 -15
  102. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  103. package/src/ui.composer-defer-one.test.ts +6 -34
  104. package/src/ui.composer-draft-persistence.test.ts +1 -1
  105. package/src/ui.composer-quote.test.ts +1 -1
  106. package/src/ui.message-history-actions.test.ts +1 -1
  107. package/src/ui.reasoning-display.test.ts +136 -2
  108. package/src/ui.streaming-coalescing.test.ts +10 -13
  109. package/src/ui.ts +101 -35
  110. package/src/ui.typing-indicator.test.ts +216 -0
  111. package/src/ui.voice-custom-provider.test.ts +211 -0
  112. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
  113. package/src/utils/event-stream-buffer.test.ts +13 -13
  114. package/src/utils/event-stream-capture.test.ts +20 -20
  115. package/src/utils/formatting.ts +22 -0
  116. package/src/utils/theme.ts +16 -0
  117. package/src/utils/throughput-tracker.test.ts +100 -142
  118. package/src/utils/throughput-tracker.ts +18 -132
  119. package/src/utils/tokens.ts +59 -0
  120. package/src/utils/voice-support.ts +18 -1
  121. package/src/voice/voice-turn-tracker.ts +18 -0
  122. package/src/webmcp-bridge.test.ts +132 -23
  123. package/src/webmcp-bridge.ts +27 -3
  124. package/src/webmcp-runtime-entry.ts +71 -11
  125. package/dist/artifacts-ui-J6AKUGTS.js +0 -7
  126. package/dist/chunk-HBE7LFNT.js +0 -4
  127. package/dist/event-stream-view-73NWKWVQ.js +0 -1
  128. package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
  129. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
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";
@@ -61,7 +62,8 @@ import {
61
62
  ComposerQuote,
62
63
  ComposerState,
63
64
  ComposerStreamingSubmitBehavior,
64
- ComposerSubmissionSnapshot
65
+ ComposerSubmissionSnapshot,
66
+ AgentWidgetReasoning
65
67
  } from "./types";
66
68
  import { AttachmentManager, type PendingAttachment } from "./utils/attachment-manager";
67
69
  import { createComposerStore } from "./utils/composer-store";
@@ -282,7 +284,7 @@ import {
282
284
  latestAgentSuggestions,
283
285
  resolveFollowUpsFeature,
284
286
  } from "./suggest-replies-tool";
285
- import { formatElapsedMs } from "./utils/formatting";
287
+ import { formatElapsedMs, isShortReasoning } from "./utils/formatting";
286
288
  import { loadApprovalUi, getApprovalUiSync, type ApprovalUiModule } from "./approval-ui-loader";
287
289
  import { getWebMcpToolDisplayTitle } from "./webmcp-bridge";
288
290
  import type { AgentWidgetPlugin } from "./plugins/types";
@@ -3271,7 +3273,7 @@ export const createAgentExperience = (
3271
3273
 
3272
3274
  removeAskUserQuestionSheet(askUserOverlay, toolCallId);
3273
3275
 
3274
- // Branch: LOCAL-tool pause (step_await) resumes via /resume with structured
3276
+ // Branch: LOCAL-tool pause (await) resumes via /resume with structured
3275
3277
  // toolOutputs; legacy path sends as a plain user message.
3276
3278
  const sourceMessage = sessionRef.current
3277
3279
  .getMessages()
@@ -5244,7 +5246,7 @@ export const createAgentExperience = (
5244
5246
  setTimeout(() => {
5245
5247
  if (!voiceState.active) {
5246
5248
  voiceState.manuallyDeactivated = false;
5247
- if (config.voiceRecognition?.provider?.type === 'runtype') {
5249
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
5248
5250
  session.toggleVoice().then(() => {
5249
5251
  voiceState.active = session.isVoiceActive();
5250
5252
  emitVoiceState("restore");
@@ -6297,18 +6299,44 @@ export const createAgentExperience = (
6297
6299
  /** The live editor, re-grafted into its morphed wrapper after the pass. */
6298
6300
  let editHydrate: { messageId: string; element: HTMLElement } | null = null;
6299
6301
 
6300
- // `reasoningDisplay.completedVisibility: "removed"`: no row for a finished
6301
- // trace, so the transcript gap closes with it. Checked before the row is
6302
- // registered, so the cached wrapper is pruned too.
6303
- const dropCompletedReasoning =
6304
- config.features?.reasoningDisplay?.completedVisibility === "removed";
6302
+ // `reasoningDisplay.completedVisibility: "removed"` (or "removed-when-short"
6303
+ // for a short completed trace): no row for the finished trace, so the
6304
+ // transcript gap closes with it. Checked before the row is registered, so
6305
+ // the cached wrapper is pruned too.
6306
+ const reasoningCompletedVisibility =
6307
+ config.features?.reasoningDisplay?.completedVisibility;
6308
+ const dropReasoningRow = (reasoning: AgentWidgetReasoning): boolean => {
6309
+ if (reasoningCompletedVisibility === "removed") return true;
6310
+ if (reasoningCompletedVisibility === "removed-when-short") {
6311
+ return isShortReasoning(
6312
+ reasoning,
6313
+ config.features?.reasoningDisplay?.shortThinkThreshold
6314
+ );
6315
+ }
6316
+ return false;
6317
+ };
6305
6318
 
6306
- messages.forEach((message) => {
6319
+ // `toolCallDisplay.completedVisibility: "removed"`: same mechanism as
6320
+ // reasoning removal, no threshold variant.
6321
+ const dropCompletedToolCalls =
6322
+ config.features?.toolCallDisplay?.completedVisibility === "removed";
6323
+ /** A finished reasoning/tool row that `completedVisibility` removes from the transcript. */
6324
+ const isDroppedCompletedRow = (message: AgentWidgetMessage): boolean => {
6307
6325
  if (
6308
- dropCompletedReasoning &&
6309
6326
  message.variant === "reasoning" &&
6310
6327
  message.reasoning?.status === "complete"
6311
6328
  ) {
6329
+ return dropReasoningRow(message.reasoning);
6330
+ }
6331
+ return (
6332
+ dropCompletedToolCalls &&
6333
+ message.variant === "tool" &&
6334
+ message.toolCall?.status === "complete"
6335
+ );
6336
+ };
6337
+
6338
+ messages.forEach((message) => {
6339
+ if (isDroppedCompletedRow(message)) {
6312
6340
  return;
6313
6341
  }
6314
6342
  activeMessageIds.add(message.id);
@@ -6384,11 +6412,11 @@ export const createAgentExperience = (
6384
6412
  if (cachedWrapper) {
6385
6413
  tempContainer.appendChild(cachedWrapper.cloneNode(true));
6386
6414
  // Keep the overlay sheet alive only while the server is actively
6387
- // waiting on the user (awaitingLocalTool === true). Before step_await
6415
+ // waiting on the user (awaitingLocalTool === true). Before await
6388
6416
  // fires, or after the answer resumes the flow, omit from
6389
6417
  // liveAskToolIds so the prune loop below removes any stale DOM sheet.
6390
6418
  // Guards against lingering skeleton sheets from tool_start events
6391
- // that never get a matching step_await (e.g. LLM-hallucinated trailing
6419
+ // that never get a matching await (e.g. LLM-hallucinated trailing
6392
6420
  // ask_user_question calls at end-of-turn).
6393
6421
  if (
6394
6422
  isAskUserQuestionMessage(message) &&
@@ -6862,6 +6890,12 @@ export const createAgentExperience = (
6862
6890
 
6863
6891
  messages.forEach((message) => {
6864
6892
  if (message.variant === "tool" && message.toolCall && showToolCalls) {
6893
+ // A tool call hidden by `completedVisibility: "removed"` does not
6894
+ // create a visible break in the transcript, so it should not split
6895
+ // an otherwise contiguous group.
6896
+ if (dropCompletedToolCalls && message.toolCall.status === "complete") {
6897
+ return;
6898
+ }
6865
6899
  currentGroup.push(message);
6866
6900
  return;
6867
6901
  }
@@ -6871,7 +6905,8 @@ export const createAgentExperience = (
6871
6905
  if (
6872
6906
  message.variant === "reasoning" &&
6873
6907
  (!showReasoning ||
6874
- (dropCompletedReasoning && message.reasoning?.status === "complete"))
6908
+ (message.reasoning?.status === "complete" &&
6909
+ dropReasoningRow(message.reasoning)))
6875
6910
  ) {
6876
6911
  return;
6877
6912
  }
@@ -6971,16 +7006,40 @@ export const createAgentExperience = (
6971
7006
  // Add standalone typing indicator only if streaming but no assistant message is streaming yet
6972
7007
  // (This shows while waiting for the stream to start)
6973
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);
6974
7020
  const hasStreamingAssistantMessage = messages.some(
6975
- (msg) => msg.role === "assistant" && msg.streaming
7021
+ (msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
6976
7022
  );
6977
7023
 
6978
7024
  // Also check if there's a recently completed assistant message (streaming just ended)
6979
7025
  // This prevents flicker when the message completes but isStreaming hasn't updated yet
6980
7026
  // Approval-variant messages are UI controls, not content: exclude them so the typing
6981
- // indicator still shows while the agent resumes after approval
6982
- const lastMessage = messages[messages.length - 1];
6983
- 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;
6984
7043
 
6985
7044
  if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
6986
7045
  // Get loading indicator using priority chain: plugin -> config -> default
@@ -7894,6 +7953,10 @@ export const createAgentExperience = (
7894
7953
  };
7895
7954
 
7896
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()));
7897
7960
  /** Any submission path is blocked; stop is not a submission. */
7898
7961
  const isComposerSendBlocked = (): boolean =>
7899
7962
  composerLock.inputDisabled || composerLock.sendDisabled;
@@ -8002,7 +8065,7 @@ export const createAgentExperience = (
8002
8065
  button.disabled = locked;
8003
8066
  });
8004
8067
  }
8005
- if (micButton) micButton.disabled = blocked;
8068
+ if (micButton) micButton.disabled = locked || (streaming && !isActiveSessionVoice());
8006
8069
 
8007
8070
  suggestionManagers.forEach((manager) => {
8008
8071
  manager.elements.forEach((element) => {
@@ -8829,7 +8892,7 @@ export const createAgentExperience = (
8829
8892
  // per-state UI (e.g. a listening/speaking status dock). Fires for every
8830
8893
  // provider; the mic-button styling below is runtype-specific.
8831
8894
  eventBus.emit("voice:status", { status, timestamp: Date.now() });
8832
- if (config.voiceRecognition?.provider?.type !== 'runtype') return;
8895
+ if (status !== 'disconnected' && !usesSessionVoice(config.voiceRecognition?.provider)) return;
8833
8896
 
8834
8897
  switch (status) {
8835
8898
  case 'listening':
@@ -8952,7 +9015,7 @@ export const createAgentExperience = (
8952
9015
  scrollSendSeeded = true;
8953
9016
 
8954
9017
  // Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
8955
- if (config.voiceRecognition?.provider?.type === 'runtype') {
9018
+ if (config.voiceRecognition?.enabled === true && usesSessionVoice(config.voiceRecognition.provider)) {
8956
9019
  try {
8957
9020
  session.setupVoice();
8958
9021
  } catch (err) {
@@ -12591,7 +12654,7 @@ export const createAgentExperience = (
12591
12654
  // Dictation is composition: a locked input takes none of it.
12592
12655
  if (isComposerInputDisabled()) return;
12593
12656
  // Runtype provider: use session.toggleVoice() (WebSocket-based STT)
12594
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12657
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12595
12658
  const voiceStatus = session.getVoiceStatus();
12596
12659
  const interruptionMode = session.getVoiceInterruptionMode();
12597
12660
 
@@ -12662,7 +12725,7 @@ export const createAgentExperience = (
12662
12725
  // The click listener itself is registered by `wireComposerSurface`, so a
12663
12726
  // composer rebuild unwires it with the rest of the surface.
12664
12727
  destroyCallbacks.push(() => {
12665
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12728
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12666
12729
  if (session.isVoiceActive()) session.toggleVoice();
12667
12730
  removeRuntypeMicStateStyles();
12668
12731
  } else {
@@ -12691,7 +12754,7 @@ export const createAgentExperience = (
12691
12754
  }
12692
12755
  setTimeout(() => {
12693
12756
  if (!voiceState.active && !voiceState.manuallyDeactivated) {
12694
- if (config.voiceRecognition?.provider?.type === 'runtype') {
12757
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
12695
12758
  session.toggleVoice().then(() => {
12696
12759
  voiceState.active = session.isVoiceActive();
12697
12760
  emitVoiceState("auto");
@@ -14277,9 +14340,9 @@ export const createAgentExperience = (
14277
14340
  typeof window !== 'undefined' &&
14278
14341
  (typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
14279
14342
  typeof (window as any).SpeechRecognition !== 'undefined');
14280
- const hasRuntypeProvider =
14281
- config.voiceRecognition?.provider?.type === 'runtype';
14282
- const hasVoiceInput = hasSpeechRecognition || hasRuntypeProvider;
14343
+ const hasSessionProvider =
14344
+ usesSessionVoice(config.voiceRecognition?.provider);
14345
+ const hasVoiceInput = hasSpeechRecognition || hasSessionProvider;
14283
14346
 
14284
14347
  if (voiceRecognitionEnabled && hasVoiceInput) {
14285
14348
  // Create or update mic button
@@ -14312,7 +14375,7 @@ export const createAgentExperience = (
14312
14375
  );
14313
14376
 
14314
14377
  // Set disabled state
14315
- micButton.disabled = session.isStreaming();
14378
+ micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
14316
14379
  }
14317
14380
  } else {
14318
14381
  // Update existing mic button with new config
@@ -14408,14 +14471,14 @@ export const createAgentExperience = (
14408
14471
 
14409
14472
  // Show and update disabled state
14410
14473
  micButtonWrapper.style.display = "";
14411
- micButton.disabled = session.isStreaming();
14474
+ micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
14412
14475
  }
14413
14476
  } else {
14414
14477
  // Hide mic button
14415
14478
  if (micButton && micButtonWrapper) {
14416
14479
  micButtonWrapper.style.display = "none";
14417
14480
  // Stop any active recording if disabling
14418
- if (config.voiceRecognition?.provider?.type === 'runtype') {
14481
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
14419
14482
  if (session.isVoiceActive()) session.toggleVoice();
14420
14483
  } else if (isRecording) {
14421
14484
  stopVoiceRecognition();
@@ -14984,8 +15047,8 @@ export const createAgentExperience = (
14984
15047
  },
14985
15048
  startVoiceRecognition(): boolean {
14986
15049
  if (session.isStreaming()) return false;
14987
- if (config.voiceRecognition?.provider?.type === 'runtype') {
14988
- if (session.isVoiceActive()) return true;
15050
+ if (usesSessionVoice(config.voiceRecognition?.provider)) {
15051
+ if (session.isVoiceActive() || session.isBargeInActive()) return true;
14989
15052
  if (!open && isPanelToggleable()) setOpenState(true, "system");
14990
15053
  voiceState.manuallyDeactivated = false;
14991
15054
  persistVoiceMetadata();
@@ -15006,9 +15069,12 @@ export const createAgentExperience = (
15006
15069
  return true;
15007
15070
  },
15008
15071
  stopVoiceRecognition(): boolean {
15009
- if (config.voiceRecognition?.provider?.type === 'runtype') {
15010
- if (!session.isVoiceActive()) return false;
15011
- 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(() => {
15012
15078
  voiceState.active = false;
15013
15079
  voiceState.manuallyDeactivated = true;
15014
15080
  persistVoiceMetadata();
@@ -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
+ });