fix(webui): finalize turn-ending slash commands

maintainer edit: keep /new and manually submitted /stop from leaving stale WebUI streaming state after they cancel or reset the active turn.
This commit is contained in:
chengyongru
2026-07-07 15:42:04 +08:00
committed by Xubin Ren
parent ef5318ebdc
commit 8a231b6e4d
4 changed files with 152 additions and 7 deletions
+42 -4
View File
@@ -109,8 +109,21 @@ const FALLBACK_SIDE_CHANNEL_COMMANDS = new Set([
]);
type VoiceShortcutPlatform = "apple" | "chromeos" | "linux" | "other" | "windows";
function slashCommandName(content: string): string {
return content.split(/\s+/, 1)[0];
}
function isExactSlashCommand(content: string, commandName: string): boolean {
if (slashCommandName(content) !== commandName) return false;
return content.slice(commandName.length).trim().length === 0;
}
function shouldFinalizeActiveTurn(content: string): boolean {
return isExactSlashCommand(content, "/new");
}
function isSlashCommandSideChannel(content: string, visibleSlashCommands: SlashCommand[]): boolean {
const commandName = content.split(/\s+/, 1)[0];
const commandName = slashCommandName(content);
if (!commandName.startsWith("/")) return false;
if (commandName === "/goal" && content.slice(commandName.length).trim().length > 0) {
return false;
@@ -1509,15 +1522,37 @@ export function ThreadComposer({
...(attachedMcpPresets.length > 0 ? { mcpPresets: attachedMcpPresets } : {}),
}
: undefined;
const isSlashSideChannel =
const hasPlainTextCommandPayload =
payload === undefined
&& attachedCliApps.length === 0
&& attachedMcpPresets.length === 0
&& attachedMcpPresets.length === 0;
if (
hasPlainTextCommandPayload
&& isStreaming
&& onStop
&& isExactSlashCommand(content, "/stop")
) {
handleStop();
setQueuedPrompts([]);
clear();
clearComposerText();
return;
}
const isSlashSideChannel =
hasPlainTextCommandPayload
&& isSlashCommandSideChannel(content, visibleSlashCommands);
const finalizeActiveTurn =
isSlashSideChannel && shouldFinalizeActiveTurn(content);
onSend(
content,
payload,
isSlashSideChannel ? { ...options, sideChannel: true } : options,
isSlashSideChannel
? {
...options,
sideChannel: true,
...(finalizeActiveTurn ? { finalizeActiveTurn } : {}),
}
: options,
);
setQueuedPrompts([]);
// Bubble owns the data URL copy; safe to revoke every staged blob
@@ -1530,9 +1565,12 @@ export function ThreadComposer({
canSend,
clear,
clearComposerText,
handleStop,
isStreaming,
modelNeedsSetup,
onModelBadgeClick,
onSend,
onStop,
readyImages,
value,
visibleSlashCommands,