fix(webui): classify builtin slash commands without metadata
maintainer edit: keep builtin shortcut commands on the side-channel path before async command metadata loads, while preserving /goal task text as a normal agent turn.
This commit is contained in:
@@ -90,8 +90,37 @@ import { cn } from "@/lib/utils";
|
|||||||
const ACCEPT_ATTR = "image/png,image/jpeg,image/webp,image/gif";
|
const ACCEPT_ATTR = "image/png,image/jpeg,image/webp,image/gif";
|
||||||
const VOICE_SHORTCUT_CODE = "KeyD";
|
const VOICE_SHORTCUT_CODE = "KeyD";
|
||||||
const VOICE_SHORTCUT_ARIA = "Control+Shift+D";
|
const VOICE_SHORTCUT_ARIA = "Control+Shift+D";
|
||||||
|
const FALLBACK_SIDE_CHANNEL_COMMANDS = new Set([
|
||||||
|
"/new",
|
||||||
|
"/stop",
|
||||||
|
"/restart",
|
||||||
|
"/status",
|
||||||
|
"/model",
|
||||||
|
"/history",
|
||||||
|
"/goal",
|
||||||
|
"/trigger",
|
||||||
|
"/dream",
|
||||||
|
"/dream-log",
|
||||||
|
"/dream-restore",
|
||||||
|
"/dream-prompt",
|
||||||
|
"/skill",
|
||||||
|
"/help",
|
||||||
|
"/pairing",
|
||||||
|
]);
|
||||||
type VoiceShortcutPlatform = "apple" | "chromeos" | "linux" | "other" | "windows";
|
type VoiceShortcutPlatform = "apple" | "chromeos" | "linux" | "other" | "windows";
|
||||||
|
|
||||||
|
function isSlashCommandSideChannel(content: string, visibleSlashCommands: SlashCommand[]): boolean {
|
||||||
|
const commandName = content.split(/\s+/, 1)[0];
|
||||||
|
if (!commandName.startsWith("/")) return false;
|
||||||
|
if (commandName === "/goal" && content.slice(commandName.length).trim().length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
FALLBACK_SIDE_CHANNEL_COMMANDS.has(commandName)
|
||||||
|
|| visibleSlashCommands.some((command) => command.command === commandName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatBytes(n: number): string {
|
function formatBytes(n: number): string {
|
||||||
if (n < 1024) return `${n} B`;
|
if (n < 1024) return `${n} B`;
|
||||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||||
@@ -1480,12 +1509,11 @@ export function ThreadComposer({
|
|||||||
...(attachedMcpPresets.length > 0 ? { mcpPresets: attachedMcpPresets } : {}),
|
...(attachedMcpPresets.length > 0 ? { mcpPresets: attachedMcpPresets } : {}),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
const commandName = content.split(/\s+/, 1)[0];
|
|
||||||
const isSlashSideChannel =
|
const isSlashSideChannel =
|
||||||
payload === undefined
|
payload === undefined
|
||||||
&& attachedCliApps.length === 0
|
&& attachedCliApps.length === 0
|
||||||
&& attachedMcpPresets.length === 0
|
&& attachedMcpPresets.length === 0
|
||||||
&& visibleSlashCommands.some((command) => command.command === commandName);
|
&& isSlashCommandSideChannel(content, visibleSlashCommands);
|
||||||
onSend(
|
onSend(
|
||||||
content,
|
content,
|
||||||
payload,
|
payload,
|
||||||
|
|||||||
@@ -1329,6 +1329,51 @@ describe("ThreadComposer", () => {
|
|||||||
expect(onSend).toHaveBeenCalledWith("/history", undefined, { sideChannel: true });
|
expect(onSend).toHaveBeenCalledWith("/history", undefined, { sideChannel: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks builtin slash commands as side-channel sends before command metadata loads", () => {
|
||||||
|
const onSend = vi.fn();
|
||||||
|
render(
|
||||||
|
<ThreadComposer
|
||||||
|
onSend={onSend}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText("Message input");
|
||||||
|
fireEvent.change(input, { target: { value: "/status" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||||
|
|
||||||
|
expect(onSend).toHaveBeenCalledWith("/status", undefined, { sideChannel: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps goal task commands on the normal agent turn path", () => {
|
||||||
|
const onSend = vi.fn();
|
||||||
|
render(
|
||||||
|
<ThreadComposer
|
||||||
|
onSend={onSend}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
slashCommands={[
|
||||||
|
{
|
||||||
|
command: "/goal",
|
||||||
|
title: "Start long-running goal",
|
||||||
|
description: "Tell the agent to treat the request as a long-running goal.",
|
||||||
|
icon: "activity",
|
||||||
|
argHint: "<goal>",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText("Message input");
|
||||||
|
fireEvent.change(input, { target: { value: "/goal fix the release blocker" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||||
|
|
||||||
|
expect(onSend).toHaveBeenCalledWith(
|
||||||
|
"/goal fix the release blocker",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows a stop button while streaming", () => {
|
it("shows a stop button while streaming", () => {
|
||||||
const onStop = vi.fn();
|
const onStop = vi.fn();
|
||||||
render(
|
render(
|
||||||
|
|||||||
Reference in New Issue
Block a user