Skip to content

Commit f587afc

Browse files
committed
fix(tui): lowercase slash command names once at dispatch
Case-insensitive routing matched /CLEAR but forwarded the typed case, so the exact registry lookup missed and reported Unknown command. The router now returns the canonical lowercase id; the ignores-case test asserts the lowered name and a keeper test drives /CLEAR through the real registry.
1 parent 96ca656 commit f587afc

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

‎src/tui/runner/submit.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ function hasKnownCommand(known: KnownCommandNames, name: string): boolean {
9191
* stays empty. Callers that omit `knownCommands` (tests and non-registry
9292
* surfaces) keep the legacy any-leading-slash-is-a-command rule; every
9393
* product call site passes the registry set.
94+
*
95+
* Case is lowered once here: the returned command name is the canonical
96+
* lowercase registry id, so the downstream exact-`Map.get` lookup
97+
* (`getCommand`) hits for mixed-case input like `/CLEAR`. Matching stays
98+
* case-insensitive via `hasKnownCommand`.
9499
*/
95100
export function routeSubmission(
96101
raw: string,
@@ -107,7 +112,7 @@ export function routeSubmission(
107112
if (knownCommands !== undefined && !hasKnownCommand(knownCommands, name)) {
108113
return { kind: "prompt", text: trimmed };
109114
}
110-
return { kind: "command", name, args };
115+
return { kind: "command", name: name.toLowerCase(), args };
111116
}
112117

113118
export interface SubmitHandlerDeps {

‎src/tui/submit-handler.test.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
isFeedbackCapturePending,
2222
resetFeedbackStateForTests,
2323
} from "../telemetry/feedback.js";
24+
import { registerBuiltInCommands } from "./commands/built-in.js";
25+
import { getCommand, listCommands } from "./commands/registry.js";
2426

2527
afterEach(() => {
2628
resetFeedbackStateForTests();
@@ -124,10 +126,30 @@ describe("composer submit handler", () => {
124126
test("command matching ignores case", () => {
125127
const h = harness();
126128
expect(h.submit("/CLEAR")).toBe("local");
127-
expect(h.dispatched).toEqual([{ name: "CLEAR", args: "" }]);
129+
expect(h.dispatched).toEqual([{ name: "clear", args: "" }]);
128130
expect(h.prompts).toEqual([]);
129131
});
130132

133+
test("/CLEAR dispatches clear through the real registry", () => {
134+
registerBuiltInCommands();
135+
const dispatched: Dispatched[] = [];
136+
const resolved: (string | undefined)[] = [];
137+
const submit = createSubmitHandler({
138+
dispatchCommand: (name, args) => {
139+
dispatched.push({ name, args });
140+
resolved.push(getCommand(name)?.name);
141+
},
142+
sendPrompt: () => {
143+
throw new Error("/CLEAR must dispatch, not prompt");
144+
},
145+
onPromptSubmitted: () => undefined,
146+
knownCommands: () => listCommands().map((c) => c.name),
147+
});
148+
expect(submit("/CLEAR")).toBe("local");
149+
expect(dispatched).toEqual([{ name: "clear", args: "" }]);
150+
expect(resolved).toEqual(["clear"]);
151+
});
152+
131153
test("a leading-slash path is captured as feedback while armed", () => {
132154
const feedbackTexts: string[] = [];
133155
const h = harness({

0 commit comments

Comments
 (0)