diff --git a/packages/views/editor/extensions/slash-command-suggestion.test.tsx b/packages/views/editor/extensions/slash-command-suggestion.test.tsx index a2b6799ba..66f94eb60 100644 --- a/packages/views/editor/extensions/slash-command-suggestion.test.tsx +++ b/packages/views/editor/extensions/slash-command-suggestion.test.tsx @@ -346,18 +346,39 @@ describe("buildBuiltinCommandItems", () => { expect(buildBuiltinCommandItems("")).toEqual(BUILTIN_COMMANDS); }); - it("includes /note while the query is a prefix of it", () => { + it("includes /note while the query is a prefix of the label", () => { expect(buildBuiltinCommandItems("no").map((c) => c.id)).toEqual(["note"]); expect(buildBuiltinCommandItems("NOTE").map((c) => c.id)).toEqual(["note"]); }); - it("matches against the description as well as the label", () => { - expect(buildBuiltinCommandItems("agent").map((c) => c.id)).toEqual([ - "note", - ]); + it("matches the label as a prefix only — not the description", () => { + // "agent" appears in the description but is not a label prefix. + expect(buildBuiltinCommandItems("agent")).toEqual([]); + // A non-prefix substring of the label does not match either. + expect(buildBuiltinCommandItems("ote")).toEqual([]); }); it("returns nothing for a query that matches no command", () => { expect(buildBuiltinCommandItems("deploy")).toEqual([]); }); }); + +describe("SlashCommandList built-in command rendering", () => { + it("renders the localized description for a built-in command", () => { + const { getByText } = render( + + + , + ); + + expect(getByText("/note")).toBeInTheDocument(); + expect( + getByText("Add a note — won't trigger the assigned agent"), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/views/editor/extensions/slash-command-suggestion.tsx b/packages/views/editor/extensions/slash-command-suggestion.tsx index 2e1acdb90..8eda201b4 100644 --- a/packages/views/editor/extensions/slash-command-suggestion.tsx +++ b/packages/views/editor/extensions/slash-command-suggestion.tsx @@ -23,10 +23,20 @@ import { createSuggestionPopupRender } from "./suggestion-popup"; const MAX_ITEMS = 20; +/** Known built-in command ids — the keys under editor `slash_command.commands`. */ +export type BuiltinCommandKey = "note"; + export interface SlashCommandItem { id: string; label: string; - description: string; + /** Raw description (skill picker). Built-in commands use descriptionKey. */ + description?: string; + /** + * For built-in commands: the i18n key under editor `slash_command.commands`. + * When set, the menu renders the translated copy instead of `description`, + * so the visible string stays localized (the typed `/label` does not). + */ + descriptionKey?: BuiltinCommandKey; } interface SlashCommandListProps { @@ -107,27 +117,37 @@ export const SlashCommandList = forwardRef< ); } + // Built-in commands carry an i18n key so the visible description stays + // localized; skills carry a raw description string from their config. + const describe = (item: SlashCommandItem): string | undefined => + item.descriptionKey === "note" + ? t(($) => $.slash_command.commands.note) + : item.description; + return (
- {items.map((item, index) => ( - - ))} + {items.map((item, index) => { + const description = describe(item); + return ( + + ); + })}
); }); @@ -227,21 +247,15 @@ export function createSlashCommandSuggestion(qc: QueryClient): Omit< * `noteCommentPrefix` in server/internal/handler/comment.go. */ export const BUILTIN_COMMANDS: SlashCommandItem[] = [ - { - id: "note", - label: "note", - description: "Add a note — won't trigger the assigned agent", - }, + { id: "note", label: "note", descriptionKey: "note" }, ]; +// Match on the command label as a prefix only — the description is for display, +// not search. With a single command this keeps the menu predictable (typing +// `/no` surfaces `note`; an unrelated `/deploy` shows nothing). export function buildBuiltinCommandItems(query: string): SlashCommandItem[] { const q = query.toLowerCase(); - return BUILTIN_COMMANDS.filter( - (c) => - !q || - c.label.toLowerCase().includes(q) || - c.description.toLowerCase().includes(q), - ); + return BUILTIN_COMMANDS.filter((c) => c.label.toLowerCase().startsWith(q)); } export function createBuiltinCommandSuggestion(): Omit< diff --git a/packages/views/locales/en/editor.json b/packages/views/locales/en/editor.json index 35c06c015..7b4af4376 100644 --- a/packages/views/locales/en/editor.json +++ b/packages/views/locales/en/editor.json @@ -64,7 +64,10 @@ }, "slash_command": { "no_skills_configured": "No skills configured", - "no_results": "No matching skills" + "no_results": "No matching skills", + "commands": { + "note": "Add a note — won't trigger the assigned agent" + } }, "code_block": { "copy_code": "Copy code", diff --git a/packages/views/locales/ja/editor.json b/packages/views/locales/ja/editor.json index cc5cd3ca9..09a787914 100644 --- a/packages/views/locales/ja/editor.json +++ b/packages/views/locales/ja/editor.json @@ -80,6 +80,9 @@ }, "slash_command": { "no_skills_configured": "設定済みスキルなし", - "no_results": "一致するスキルなし" + "no_results": "一致するスキルなし", + "commands": { + "note": "メモを追加 — 担当エージェントをトリガーしません" + } } } diff --git a/packages/views/locales/ko/editor.json b/packages/views/locales/ko/editor.json index 115d5fb2d..63d4fd225 100644 --- a/packages/views/locales/ko/editor.json +++ b/packages/views/locales/ko/editor.json @@ -80,6 +80,9 @@ }, "slash_command": { "no_skills_configured": "구성된 스킬 없음", - "no_results": "일치하는 스킬 없음" + "no_results": "일치하는 스킬 없음", + "commands": { + "note": "메모 추가 — 담당 에이전트를 트리거하지 않음" + } } } diff --git a/packages/views/locales/zh-Hans/editor.json b/packages/views/locales/zh-Hans/editor.json index f9714c20a..c30ff2410 100644 --- a/packages/views/locales/zh-Hans/editor.json +++ b/packages/views/locales/zh-Hans/editor.json @@ -64,7 +64,10 @@ }, "slash_command": { "no_skills_configured": "暂无配置的技能", - "no_results": "没有匹配的技能" + "no_results": "没有匹配的技能", + "commands": { + "note": "添加备注 — 不会触发已分配的 Agent" + } }, "code_block": { "copy_code": "复制代码",