@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
- package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +136 -6
- package/dist/animations-extra.d.ts +136 -6
- package/dist/{approval-ui-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +221 -16
- package/dist/approval-ui.d.ts +221 -16
- package/dist/approval-ui.js +2 -2
- package/dist/artifacts-ui-F4N6VMKF.js +7 -0
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +221 -16
- package/dist/artifacts-ui.d.ts +221 -16
- package/dist/artifacts-ui.js +1 -1
- package/dist/chunk-NQLDCJAP.js +4 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +221 -16
- package/dist/context-mentions.d.ts +221 -16
- package/dist/event-stream-view-GL3ADRY2.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +221 -16
- package/dist/event-stream-view.d.ts +221 -16
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +234 -25
- package/dist/forms-ui.d.ts +234 -25
- package/dist/history-view.d.cts +136 -6
- package/dist/history-view.d.ts +136 -6
- package/dist/index.cjs +70 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +302 -32
- package/dist/index.d.ts +302 -32
- package/dist/index.global.js +54 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +221 -16
- package/dist/session-reconnect.d.ts +221 -16
- package/dist/smart-dom-reader.d.cts +221 -16
- package/dist/smart-dom-reader.d.ts +221 -16
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +3 -4
- package/dist/testing.d.ts +3 -4
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +73 -73
- package/dist/theme-editor-preview.d.cts +221 -16
- package/dist/theme-editor-preview.d.ts +221 -16
- package/dist/theme-editor-preview.js +51 -51
- package/dist/theme-editor.cjs +4 -4
- package/dist/theme-editor.d.cts +221 -16
- package/dist/theme-editor.d.ts +221 -16
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +65 -1
- package/dist/theme-reference.d.ts +65 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime.d.cts +6 -2
- package/dist/voice-runtime.d.ts +6 -2
- package/dist/webmcp-polyfill.js +1 -4
- package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +34 -2
- package/dist/webmcp-runtime.d.ts +34 -2
- package/dist/webmcp-runtime.js +2 -2
- package/package.json +2 -2
- package/src/ask-user-question-tool.ts +1 -1
- package/src/client-history-rest.test.ts +2 -1
- package/src/client-visitor-history.test.ts +20 -4
- package/src/client.composer-options.test.ts +1 -1
- package/src/client.interrupt.test.ts +2 -2
- package/src/client.test.ts +477 -915
- package/src/client.ts +134 -494
- package/src/client.unified-tools.test.ts +76 -0
- package/src/components/artifact-preview.ts +5 -0
- package/src/components/event-stream-badge-theming.test.ts +162 -0
- package/src/components/event-stream-view.test.ts +76 -76
- package/src/components/event-stream-view.ts +55 -20
- package/src/components/tool-bubble.test.ts +12 -0
- package/src/components/tool-bubble.ts +13 -2
- package/src/event-stream-view-bundle.test.ts +2 -2
- package/src/generated/runtype-openapi-contract.ts +137 -1
- package/src/index-core.ts +2 -0
- package/src/internal/runtype-history-provider.test.ts +2 -2
- package/src/runtime/init.registry.test.ts +56 -0
- package/src/runtime/init.ts +17 -0
- package/src/session-history.test.ts +1 -1
- package/src/session.quote.test.ts +1 -1
- package/src/session.resubmit-from.test.ts +1 -1
- package/src/session.test.ts +5 -5
- package/src/session.ts +95 -36
- package/src/suggest-replies-tool.ts +1 -1
- package/src/testing/mock-stream.test.ts +37 -16
- package/src/testing/mock-stream.ts +14 -9
- package/src/types/theme.ts +41 -1
- package/src/types.ts +57 -15
- package/src/ui.ask-user-question-plugin.test.ts +7 -7
- package/src/ui.composer-defer-one.test.ts +6 -34
- package/src/ui.composer-draft-persistence.test.ts +1 -1
- package/src/ui.composer-quote.test.ts +1 -1
- package/src/ui.message-history-actions.test.ts +1 -1
- package/src/ui.reasoning-display.test.ts +136 -2
- package/src/ui.streaming-coalescing.test.ts +10 -13
- package/src/ui.ts +101 -35
- package/src/ui.typing-indicator.test.ts +216 -0
- package/src/ui.voice-custom-provider.test.ts +211 -0
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
- package/src/utils/event-stream-buffer.test.ts +13 -13
- package/src/utils/event-stream-capture.test.ts +20 -20
- package/src/utils/formatting.ts +22 -0
- package/src/utils/theme.ts +16 -0
- package/src/utils/throughput-tracker.test.ts +100 -142
- package/src/utils/throughput-tracker.ts +18 -132
- package/src/utils/tokens.ts +59 -0
- package/src/utils/voice-support.ts +18 -1
- package/src/voice/voice-turn-tracker.ts +18 -0
- package/src/webmcp-bridge.test.ts +132 -23
- package/src/webmcp-bridge.ts +27 -3
- package/src/webmcp-runtime-entry.ts +71 -11
- package/dist/artifacts-ui-J6AKUGTS.js +0 -7
- package/dist/chunk-HBE7LFNT.js +0 -4
- package/dist/event-stream-view-73NWKWVQ.js +0 -1
- package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
- 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 (
|
|
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
|
|
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"
|
|
6301
|
-
//
|
|
6302
|
-
//
|
|
6303
|
-
|
|
6304
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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
|
-
|
|
6983
|
-
|
|
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 =
|
|
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
|
|
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?.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14281
|
-
config.voiceRecognition?.provider
|
|
14282
|
-
const hasVoiceInput = hasSpeechRecognition ||
|
|
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
|
|
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
|
|
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
|
|
15010
|
-
if (!session.isVoiceActive()) return false;
|
|
15011
|
-
session.
|
|
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
|
+
});
|