fix(ui): execute directly typed inline command args

This commit is contained in:
Jesse Merhi
2026-08-13 01:36:58 +10:00
parent ffcd4c08d4
commit ade6fb3517
3 changed files with 113 additions and 3 deletions
+28
View File
@@ -3620,6 +3620,34 @@ describe("chat slash menu accessibility", () => {
expect(onSend).not.toHaveBeenCalled();
});
it.each([
["think", "high"],
["verbose", "full"],
])(
"executes a directly typed inline /%s argument without requiring completion",
(command, argument) => {
let draft = "";
const onDraftChange = vi.fn((next: string) => {
draft = next;
});
const onSend = vi.fn();
const onSlashCommand = vi.fn();
const { container } = createReactiveDraftHarness({
onDraftChange,
onSend,
onSlashCommand,
});
inputDraftAtEnd(container, `hello /${command} ${argument}`);
keydownComposer(container, "Enter");
expect(onSlashCommand).toHaveBeenCalledExactlyOnceWith(`/${command} ${argument}`);
expect(draft).toBe("hello ");
expect(container.querySelector<HTMLTextAreaElement>("textarea")?.value).toBe(draft);
expect(onSend).not.toHaveBeenCalled();
},
);
it("preserves typed inline argument mode across command hydration", async () => {
let draft = "";
let resolveRefresh: (() => void) | undefined;
@@ -0,0 +1,55 @@
import {
getSlashCommandCompletions,
type InlineSlashCompletion,
type SlashCommandDef,
} from "../../../lib/chat/commands.ts";
export type InlineSlashArgumentInvocation = {
command: SlashCommandDef;
completion: InlineSlashCompletion;
};
export function findDirectInlineSlashArgumentInvocation(
text: string,
caret = text.length,
): InlineSlashArgumentInvocation | null {
const boundedCaret = Math.max(0, Math.min(caret, text.length));
const prefix = text.slice(0, boundedCaret);
const commandPattern = /(?:^|\s)\/([^\s/:]+)\s+/gu;
let invocation: InlineSlashArgumentInvocation | null = null;
for (const match of prefix.matchAll(commandPattern)) {
const typedName = match[1]?.toLowerCase();
if (!typedName || match.index === undefined) {
continue;
}
const command = getSlashCommandCompletions(typedName, {
showAll: true,
inlineOnly: true,
}).find(
(entry) =>
entry.name.toLowerCase() === typedName ||
entry.aliases?.some((alias) => alias.replace(/^\//u, "").toLowerCase() === typedName),
);
if (!command?.args || command.source === "skill") {
continue;
}
const start = match.index + match[0].indexOf("/");
const args = prefix.slice(match.index + match[0].length).trim();
if (!args) {
continue;
}
invocation = {
command,
completion: {
query: command.name,
start,
end: boundedCaret,
inline:
text.slice(0, start).trim().length > 0 || text.slice(boundedCaret).trim().length > 0,
},
};
}
return invocation?.completion.inline ? invocation : null;
}
@@ -12,6 +12,7 @@ import {
} from "../../../lib/chat/commands.ts";
import { exportChatMarkdown } from "../export.ts";
import { adjustTextareaHeight } from "./chat-composer-dom.ts";
import { findDirectInlineSlashArgumentInvocation } from "./chat-composer-inline-slash.ts";
import { commitComposerDraft, getChatComposerState } from "./chat-composer-state.ts";
import type { ChatComposerProps, ChatComposerState } from "./chat-composer-types.ts";
@@ -439,16 +440,36 @@ function submitInlineSlashArgument(props: ChatComposerProps, requestUpdate: () =
return true;
}
function beginDirectInlineSlashArgument(
props: ChatComposerProps,
state: ChatComposerState,
): boolean {
if (!props.onSlashCommand) {
return false;
}
const target = state.composerTextarea;
const current = target?.value ?? props.getDraft?.() ?? props.draft;
const caret = target?.selectionStart ?? current.length;
const invocation = findDirectInlineSlashArgumentInvocation(current, caret);
if (!invocation) {
return false;
}
state.slashMenuMode = "freeform-args";
state.slashMenuCommand = invocation.command;
state.slashMenuCompletion = invocation.completion;
return true;
}
export function handleInlineSlashArgumentKeyDown(
event: KeyboardEvent,
props: ChatComposerProps,
requestUpdate: () => void,
): boolean {
const state = getChatComposerState(props.paneId);
if (state.slashMenuMode !== "freeform-args" || !state.slashMenuCompletion?.inline) {
return false;
}
if (event.key === "Escape") {
if (state.slashMenuMode !== "freeform-args" || !state.slashMenuCompletion?.inline) {
return false;
}
event.preventDefault();
resetSlashMenuState(state);
requestUpdate();
@@ -457,6 +478,12 @@ export function handleInlineSlashArgumentKeyDown(
if (event.key !== "Enter") {
return false;
}
if (
(state.slashMenuMode !== "freeform-args" || !state.slashMenuCompletion?.inline) &&
!beginDirectInlineSlashArgument(props, state)
) {
return false;
}
event.preventDefault();
return submitInlineSlashArgument(props, requestUpdate);
}