From 27ceb474e2bd528b60829599d2c28e0fc7e6ff1a Mon Sep 17 00:00:00 2001 From: joelbrilliant Date: Sat, 18 Jul 2026 11:45:12 +1000 Subject: [PATCH 1/3] fix(desktop): derive ? quick-help commands from the slash-command spec table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composer's ? quick-help drawer hardcoded /clear, /details, /copy, and /quit as "common commands" — 4 of its 6 entries sit in NO_DESKTOP_SURFACE.terminal, so tapping any of them errored with "only available in the terminal interface". The drawer advertised commands the desktop refuses to run, and i18n even shipped '/quit': 'exit hermes'. Derive the advertised list from the spec table instead: - desktop-slash-commands.ts exports DESKTOP_COMMON_COMMANDS (/help, /new, /resume, /compress, /usage, /status), filtered through the spec table so a command without a real desktop surface can never be advertised - help-hint.tsx consumes it, with locale descriptions falling back to the spec-table English so partially translated locales degrade gracefully - retire the dead /clear, /details, /copy, /quit commandDescs entries in en, ja, zh, and zh-hant - tests pin the advertised list and assert every entry passes isDesktopSlashCommand() and isDesktopSlashSuggestion(), so the regression cannot return Co-Authored-By: Claude Fable 5 --- .../src/app/chat/composer/help-hint.tsx | 12 ++++++--- apps/desktop/src/i18n/en.ts | 8 +++--- apps/desktop/src/i18n/ja.ts | 6 +---- apps/desktop/src/i18n/zh-hant.ts | 6 +---- apps/desktop/src/i18n/zh.ts | 6 +---- .../src/lib/desktop-slash-commands.test.ts | 22 +++++++++++++++ .../desktop/src/lib/desktop-slash-commands.ts | 27 +++++++++++++++++++ 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/help-hint.tsx b/apps/desktop/src/app/chat/composer/help-hint.tsx index 8c0546ace3fa..4d2e8fa79421 100644 --- a/apps/desktop/src/app/chat/composer/help-hint.tsx +++ b/apps/desktop/src/app/chat/composer/help-hint.tsx @@ -2,11 +2,10 @@ import type { ReactNode } from 'react' import { KbdCombo } from '@/components/ui/kbd' import { useI18n } from '@/i18n' +import { DESKTOP_COMMON_COMMANDS } from '@/lib/desktop-slash-commands' import { COMPLETION_DRAWER_CLASS } from './completion-drawer' -const COMMON_COMMAND_KEYS = ['/help', '/clear', '/resume', '/details', '/copy', '/quit'] - /** Stable ids → i18n `hotkeyDescs` keys. Combos resolve mod labels per OS. */ const COMPOSER_HOTKEY_ROWS = [ { id: 'composer.mention', combos: ['@'] }, @@ -26,8 +25,13 @@ export function HelpHint() { return (
- {COMMON_COMMAND_KEYS.map(key => ( - + {DESKTOP_COMMON_COMMANDS.map(command => ( + ))}
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 12d15e9657c4..308aa9119bb7 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1928,11 +1928,11 @@ export const en: Translations = { helpFooter: 'opens the full panel · backspace dismisses', commandDescs: { '/help': 'full list of commands + hotkeys', - '/clear': 'start a new session', + '/new': 'start a new chat', '/resume': 'resume a prior session', - '/details': 'control transcript detail level', - '/copy': 'copy selection or last assistant message', - '/quit': 'exit hermes' + '/compress': 'compress this conversation context', + '/usage': 'token usage for this session', + '/status': 'current session status' }, hotkeyDescs: { 'composer.mention': 'reference files, folders, urls, git', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 90d65ad5901a..8c5e1bf34bb1 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -1794,11 +1794,7 @@ export const ja = defineLocale({ helpFooter: 'フルパネルを開く · Backspace で閉じる', commandDescs: { '/help': 'コマンドとホットキーの全リスト', - '/clear': '新しいセッションを開始', - '/resume': '以前のセッションを再開', - '/details': 'トランスクリプトの詳細レベルを制御', - '/copy': '選択または最後のアシスタントメッセージをコピー', - '/quit': 'hermes を終了' + '/resume': '以前のセッションを再開' }, hotkeyDescs: { 'composer.mention': 'ファイル、フォルダー、URL、Git を参照', diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 2772852eee51..e96ac76579c3 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -1740,11 +1740,7 @@ export const zhHant = defineLocale({ helpFooter: '開啟完整面板 · 退格鍵關閉', commandDescs: { '/help': '指令與快捷鍵的完整清單', - '/clear': '開始新工作階段', - '/resume': '繼續之前的工作階段', - '/details': '控制對話記錄的詳細程度', - '/copy': '複製所選內容或最後一條助手訊息', - '/quit': '結束 hermes' + '/resume': '繼續之前的工作階段' }, hotkeyDescs: { 'composer.mention': '參照檔案、資料夾、URL、git', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 68cbbb77ad4a..1f20ae0fc2d5 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -2117,11 +2117,7 @@ export const zh: Translations = { helpFooter: '打开完整面板 · 退格键关闭', commandDescs: { '/help': '命令与快捷键的完整列表', - '/clear': '开始新会话', - '/resume': '恢复之前的会话', - '/details': '控制对话记录的详细程度', - '/copy': '复制所选内容或最后一条助手消息', - '/quit': '退出 hermes' + '/resume': '恢复之前的会话' }, hotkeyDescs: { 'composer.mention': '引用文件、文件夹、URL、git', diff --git a/apps/desktop/src/lib/desktop-slash-commands.test.ts b/apps/desktop/src/lib/desktop-slash-commands.test.ts index cbca758e7e65..77c12a0a628a 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.test.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { + DESKTOP_COMMON_COMMANDS, desktopSkinSlashCompletions, desktopSlashDescription, desktopSlashUnavailableMessage, @@ -269,6 +270,27 @@ describe('desktop slash command curation', () => { expect(isModelPickerCommand('/resume')).toBe(false) }) + it('advertises only runnable commands in the ? quick-help drawer', () => { + const advertised = DESKTOP_COMMON_COMMANDS.map(command => command.name) + + // Pin the list so a typo'd entry fails loudly instead of being silently + // filtered out of the drawer. + expect(advertised).toEqual(['/help', '/new', '/resume', '/compress', '/usage', '/status']) + + // Regression guard: the drawer once hardcoded /clear, /details, /copy, and + // /quit — all terminal-only, so tapping them errored. Every advertised + // command must execute on desktop and appear in the slash popover. + expect(advertised.filter(name => isDesktopSlashCommand(name))).toEqual(advertised) + expect(advertised.filter(name => isDesktopSlashSuggestion(name))).toEqual(advertised) + }) + + it('carries a spec-table description for every advertised command (i18n fallback)', () => { + for (const command of DESKTOP_COMMON_COMMANDS) { + expect(command.description).toBeTruthy() + expect(resolveDesktopCommand(command.name)?.surface.kind).not.toBe('unavailable') + } + }) + it('resolves commands and aliases to their declared surface', () => { expect(resolveDesktopCommand('/new')?.surface).toEqual({ kind: 'action', action: 'new' }) expect(resolveDesktopCommand('/reset')?.surface).toEqual({ kind: 'action', action: 'new' }) diff --git a/apps/desktop/src/lib/desktop-slash-commands.ts b/apps/desktop/src/lib/desktop-slash-commands.ts index 22ae4e590e42..dec63055d314 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -22,6 +22,14 @@ export interface DesktopThemeCommandOption { name: string } +/** A command advertised in the composer's `?` quick-help drawer. */ +export interface DesktopCommonCommand { + /** Spec-table English description — locales may override, falling back here. */ + description: string + /** Canonical command, leading slash included. */ + name: string +} + /** * Local client action a command resolves to. Each id maps to exactly one * handler in the dispatcher (`use-prompt-actions`), so adding a command never @@ -302,6 +310,25 @@ const ALIAS_TO_CANONICAL = new Map( ALL_SPECS.flatMap(spec => (spec.aliases ?? []).map(alias => [alias, spec.name] as const)) ) +const COMMON_COMMAND_NAMES = ['/help', '/new', '/resume', '/compress', '/usage', '/status'] + +/** + * Commands advertised in the composer's `?` quick-help drawer. Derived from + * the spec table so the drawer can never advertise a command the desktop + * refuses to run: anything without a real desktop surface is filtered out + * here, and the test suite pins the expected list so a bad entry fails loudly + * instead of silently disappearing. + */ +export const DESKTOP_COMMON_COMMANDS: readonly DesktopCommonCommand[] = COMMON_COMMAND_NAMES.flatMap(name => { + const spec = SPEC_BY_NAME.get(name) + + if (!spec || spec.surface.kind === 'unavailable') { + return [] + } + + return [{ description: spec.description ?? '', name: spec.name }] +}) + const UNAVAILABLE_MESSAGE: Record string> = { advanced: command => `${command} is not shown in the desktop slash palette. Use the relevant desktop control or terminal interface instead.`, From de236640f17f63c629003fbf0118cc617cfdf97f Mon Sep 17 00:00:00 2001 From: joelbrilliant Date: Sat, 18 Jul 2026 11:52:19 +1000 Subject: [PATCH 2/3] test(desktop): render-level guard for the help-hint drawer Mounts the real HelpHint component and asserts it renders one row per DESKTOP_COMMON_COMMAND, that none of the retired terminal-only commands (/clear, /details, /copy, /quit) reach the DOM, and that every advertised command passes isDesktopSlashCommand(). Complements the unit test on the derived list with a DOM-level regression pin. Co-Authored-By: Claude Fable 5 --- .../src/app/chat/composer/help-hint.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 apps/desktop/src/app/chat/composer/help-hint.test.tsx diff --git a/apps/desktop/src/app/chat/composer/help-hint.test.tsx b/apps/desktop/src/app/chat/composer/help-hint.test.tsx new file mode 100644 index 000000000000..17ca4fd19ca8 --- /dev/null +++ b/apps/desktop/src/app/chat/composer/help-hint.test.tsx @@ -0,0 +1,54 @@ +import { act, cleanup, render, screen, within } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' + +import { I18nProvider } from '@/i18n/context' +import { DESKTOP_COMMON_COMMANDS, isDesktopSlashCommand } from '@/lib/desktop-slash-commands' + +import { HelpHint } from './help-hint' + +// The four commands the pre-fix drawer hardcoded that the desktop refuses to +// run (all in NO_DESKTOP_SURFACE.terminal). None may reach the DOM again. +const RETIRED_TERMINAL_COMMANDS = ['/clear', '/details', '/copy', '/quit'] + +async function renderHelpHint() { + let result: ReturnType + await act(async () => { + result = render( + ({}), saveConfig: async () => ({ ok: true }) }}> + + + ) + }) + return result! +} + +// Render-level guard for the #-quick-help regression: the drawer used to +// hardcode /clear, /details, /copy, /quit — all terminal-only — so tapping a +// row errored. The component must render exactly the spec-derived list and +// no terminal-only command may reach the DOM. +describe('HelpHint drawer', () => { + afterEach(cleanup) + + it('renders a row for every DESKTOP_COMMON_COMMAND', async () => { + const { container } = await renderHelpHint() + const drawer = container.querySelector('[data-slot="composer-completion-drawer"]')! + for (const command of DESKTOP_COMMON_COMMANDS) { + // getAllByText: /help also appears in the drawer footer, so >=1 row. + expect(within(drawer as HTMLElement).getAllByText(command.name).length).toBeGreaterThan(0) + } + }) + + it('never advertises a terminal-only command', async () => { + await renderHelpHint() + for (const name of RETIRED_TERMINAL_COMMANDS) { + expect(screen.queryByText(name)).toBeNull() + } + }) + + it('only advertises commands the desktop can actually run', async () => { + await renderHelpHint() + for (const command of DESKTOP_COMMON_COMMANDS) { + expect(isDesktopSlashCommand(command.name)).toBe(true) + } + }) +}) From aa29610a21f2104606abd7b16ae27e79a61612f6 Mon Sep 17 00:00:00 2001 From: joelbrilliant Date: Sat, 25 Jul 2026 15:58:41 +1000 Subject: [PATCH 3/3] i18n(desktop): drop terminal-only commands from the Arabic help drawer The Arabic locale predated this branch, so its commandDescs still advertised /clear, /details, /copy and /quit - the exact terminal-only commands the desktop refuses, which is the bug this PR exists to fix. Arabic users would have kept seeing the misleading drawer. Aligned with ja/zh/zh-hant: keep /help and /resume, fall back to English for the desktop-supported additions. tsc clean, help-hint + i18n suites 31 passed, lint clean. --- apps/desktop/src/i18n/ar.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts index 2d61c399998f..c1a752c58c95 100644 --- a/apps/desktop/src/i18n/ar.ts +++ b/apps/desktop/src/i18n/ar.ts @@ -1648,11 +1648,7 @@ export const ar = defineLocale({ helpFooter: 'استخدم الأسهم للتنقل و Enter للاختيار.', commandDescs: { '/help': 'قائمة كاملة بالأوامر + اختصارات لوحة المفاتيح', - '/clear': 'بدء جلسة جديدة', - '/resume': 'استئناف جلسة سابقة', - '/details': 'التحكم في مستوى تفاصيل النص', - '/copy': 'نسخ التحديد أو آخر رسالة من المساعد', - '/quit': 'الخروج من hermes' + '/resume': 'استئناف جلسة سابقة' }, hotkeyDescs: { 'composer.mention': 'الإشارة إلى الملفات والمجلدات والروابط وgit',