fix(app): preserve dictation text during submit (#2745)

Dictation auto-send bypassed the controlled composer update, so preserved draft surfaces could lock while showing an empty prompt. Publish the transcript before invoking the existing submit lifecycle so pending and failed submissions retain the user's spoken input.
This commit is contained in:
Mohamed Boudra
2026-08-01 12:28:55 +02:00
committed by GitHub
parent 6177d85e99
commit a059af1eda
4 changed files with 258 additions and 36 deletions
+1 -36
View File
@@ -66,6 +66,7 @@ import {
resolveVoiceTooltipText,
} from "./labels";
import {
applyDictationTranscript,
computeCanStartDictation,
resolveComposerSurfacePresentation,
runAlternateSendAction,
@@ -778,42 +779,6 @@ function SendButtonTooltip({
);
}
interface DictationTranscriptContext {
value: string;
defaultSendBehavior: "interrupt" | "queue";
isAgentRunning: boolean;
onQueue: ((payload: MessagePayload) => void) | undefined;
onSubmit: (payload: MessagePayload) => void;
onChangeText: (text: string) => void;
attachments: ComposerAttachment[];
cwd: string;
autoSend: boolean;
}
function applyDictationTranscript(text: string, ctx: DictationTranscriptContext): void {
if (!text) return;
const shouldPad = ctx.value.length > 0 && !/\s$/.test(ctx.value);
const nextValue = `${ctx.value}${shouldPad ? " " : ""}${text}`;
if (!ctx.autoSend) {
ctx.onChangeText(nextValue);
return;
}
if (ctx.defaultSendBehavior === "queue" && ctx.isAgentRunning && ctx.onQueue) {
ctx.onQueue({ text: nextValue, attachments: ctx.attachments, cwd: ctx.cwd });
ctx.onChangeText("");
return;
}
ctx.onSubmit({
text: nextValue,
attachments: ctx.attachments,
cwd: ctx.cwd,
forceSend: ctx.isAgentRunning || undefined,
});
}
interface ToggleRealtimeVoiceContext {
voice:
| {
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import {
applyDictationTranscript,
computeCanStartDictation,
resolveComposerSurfacePresentation,
runAlternateSendAction,
@@ -163,6 +164,29 @@ describe("dictation keyboard behavior", () => {
});
});
describe("dictation transcript behavior", () => {
it("publishes an auto-sent transcript to the composer before submitting it", () => {
const actions: string[] = [];
applyDictationTranscript("spoken prompt", {
value: "typed context",
defaultSendBehavior: "interrupt",
isAgentRunning: false,
onQueue: undefined,
onChangeText: (text) => actions.push(`change:${text}`),
onSubmit: (payload) => actions.push(`submit:${payload.text}`),
attachments: [],
cwd: "/repo",
autoSend: true,
});
expect(actions).toEqual([
"change:typed context spoken prompt",
"submit:typed context spoken prompt",
]);
});
});
describe("composer send behavior", () => {
function actions() {
const calls: string[] = [];
+38
View File
@@ -46,6 +46,44 @@ interface SendActionContext {
handleQueueMessage: () => void;
}
interface DictationTranscriptContext {
value: string;
defaultSendBehavior: SendBehavior;
isAgentRunning: boolean;
onQueue: ((payload: MessagePayload) => void) | undefined;
onSubmit: (payload: MessagePayload) => void;
onChangeText: (text: string) => void;
attachments: MessagePayload["attachments"];
cwd: string;
autoSend: boolean;
}
export function applyDictationTranscript(text: string, ctx: DictationTranscriptContext): void {
if (!text) return;
const shouldPad = ctx.value.length > 0 && !/\s$/.test(ctx.value);
const nextValue = `${ctx.value}${shouldPad ? " " : ""}${text}`;
if (!ctx.autoSend) {
ctx.onChangeText(nextValue);
return;
}
ctx.onChangeText(nextValue);
if (ctx.defaultSendBehavior === "queue" && ctx.isAgentRunning && ctx.onQueue) {
ctx.onQueue({ text: nextValue, attachments: ctx.attachments, cwd: ctx.cwd });
ctx.onChangeText("");
return;
}
ctx.onSubmit({
text: nextValue,
attachments: ctx.attachments,
cwd: ctx.cwd,
forceSend: ctx.isAgentRunning || undefined,
});
}
interface MessageInputKeyboardActions {
focusInput: () => void;
isDictationRecording: () => boolean;