diff --git a/AGENTS.md b/AGENTS.md index d6b600715..15cd7536e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -283,6 +283,21 @@ The dashboard embeds the real `hermes --tui` — **not** a rewrite. See `hermes **Structured React UI around the TUI is allowed when it is not a second chat surface.** Sidebar widgets, inspectors, summaries, status panels, and similar supporting views (e.g. `ChatSidebar`, `ModelPickerDialog`, `ToolCall`) are fine when they complement the embedded TUI rather than replacing the transcript / composer / terminal. Keep their state independent of the PTY child's session and surface their failures non-destructively so the terminal pane keeps working unimpaired. +### Electron Desktop Chat App (`apps/desktop/`) + +A **separate** chat surface from both the classic CLI and the dashboard's embedded TUI. It is an Electron + React + nanostore renderer (`@assistant-ui/react`) that talks to a `tui_gateway` backend over JSON-RPC (`requestGateway(method, params)`). It does NOT embed `hermes --tui` — it has its own composer, transcript, and slash-command pipeline. Route desktop bugs to the `hermes-desktop-app-work` skill, not `hermes-dashboard-work`. + +**Slash commands in the desktop app are curated client-side, then dispatched to the backend.** The pipeline: + +- **Backend already provides everything.** `tui_gateway/server.py` `commands.catalog` (empty-query list) and `complete.slash` (typed-query completions) both include built-in commands, user `quick_commands`, AND skill-derived commands (`scan_skill_commands()` / `get_skill_commands()`). The desktop app does not need a new RPC to see skills. +- **The renderer curates via `apps/desktop/src/lib/desktop-slash-commands.ts`.** This is the load-bearing file. It holds `DESKTOP_COMMANDS` (the ~19 built-ins shown in the palette) plus block-lists for terminal-only / messaging-only / picker-owned / settings-owned / advanced commands that should NOT clutter the desktop popover. + - `isDesktopSlashCommand(name)` — gates **execution**. Returns true for built-ins AND for any non-built-in (skill / quick command), so typed extension commands run. + - `isDesktopSlashSuggestion(name)` — gates **discovery/completion**. Used by BOTH completion paths in `app/chat/composer/hooks/use-slash-completions.ts` (empty-query catalog filter + typed-query `complete.slash` filter) and by `filterDesktopCommandsCatalog`. + - `isDesktopSlashExtensionCommand(name)` — true when the command is NOT a known Hermes built-in (i.e. a skill or user quick command). Both suggestion and catalog-filter paths allow extensions through so skill commands surface in the palette. (Added when fixing "skill commands missing from the desktop slash palette" — the curated allow-list was silently dropping every skill/quick command from completions even though they executed fine when typed.) +- **Dispatch** lives in `app/session/hooks/use-prompt-actions.ts` (`runSlash`): built-ins that the desktop owns (`/skin`, `/help`, `/new`, …) are handled locally or via `commands.catalog`; everything else goes to `slash.exec`, falling back to `command.dispatch` (which the gateway resolves into skill / alias / exec directives). A skill command resolves to `{type: "skill", message}` and is submitted as a normal prompt. + +**Rule:** the desktop slash palette's curation is about hiding noise (terminal-only / messaging-only built-ins), NOT about hiding user-activated extensions. Skill commands and `quick_commands` are extensions the backend surfaces — they belong in completions. If you tighten `desktop-slash-commands.ts`, keep `isDesktopSlashExtensionCommand` flowing into both the suggestion and catalog-filter paths. Tests: `apps/desktop/src/lib/desktop-slash-commands.test.ts` (run via the repo-root `vitest`, since `apps/desktop` resolves deps from the root workspace install). + --- ## Adding New Tools diff --git a/apps/desktop/src/lib/desktop-slash-commands.test.ts b/apps/desktop/src/lib/desktop-slash-commands.test.ts index 8b3666e22..b9390791d 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.test.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.test.ts @@ -17,8 +17,9 @@ describe('desktop slash command curation', () => { expect(isDesktopSlashSuggestion('/usage')).toBe(true) }) - it('lets explicitly typed extension commands run without suggesting them', () => { - expect(isDesktopSlashSuggestion('/my-skill')).toBe(false) + it('surfaces skill and quick commands (extensions) in suggestions and lets them run', () => { + expect(isDesktopSlashSuggestion('/my-skill')).toBe(true) + expect(isDesktopSlashSuggestion('/gif-search')).toBe(true) expect(isDesktopSlashCommand('/my-skill')).toBe(true) }) @@ -38,7 +39,7 @@ describe('desktop slash command curation', () => { expect(isDesktopSlashCommand('/reset')).toBe(true) }) - it('filters command catalogs down to core desktop commands', () => { + it('filters built-in catalog noise but keeps skill / quick-command extensions', () => { const filtered = filterDesktopCommandsCatalog({ categories: [ { @@ -61,8 +62,14 @@ describe('desktop slash command curation', () => { skill_count: 2 }) - expect(filtered.categories).toEqual([{ name: 'Session', pairs: [['/new', 'Start a new desktop chat']] }]) - expect(filtered.pairs).toEqual([['/new', 'Start a new desktop chat']]) + expect(filtered.categories).toEqual([ + { name: 'Session', pairs: [['/new', 'Start a new desktop chat']] }, + { name: 'User commands', pairs: [['/ship-it', 'Run release checklist']] } + ]) + expect(filtered.pairs).toEqual([ + ['/new', 'Start a new desktop chat'], + ['/ship-it', 'Run release checklist'] + ]) expect(filtered.skill_count).toBe(2) }) diff --git a/apps/desktop/src/lib/desktop-slash-commands.ts b/apps/desktop/src/lib/desktop-slash-commands.ts index bcf8be585..d6c1437c6 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -150,10 +150,35 @@ export function isDesktopSlashCommand(command: string): boolean { return DESKTOP_COMMANDS.has(canonical) || !isKnownHermesSlashCommand(normalized) } +/** + * An "extension" command is anything the backend surfaces that is NOT one of + * Hermes' built-in slash commands — i.e. skill commands (`/gif-search`, + * `/codex`, …) and user-defined quick commands. These are user-activated, so + * they should appear in the desktop slash palette even though they aren't in + * the curated `DESKTOP_COMMANDS` allow-list. This mirrors the predicate in + * `isDesktopSlashCommand` that already lets them EXECUTE when typed. + */ +export function isDesktopSlashExtensionCommand(command: string): boolean { + const normalized = normalizeCommand(command) + + if (!normalized || normalized === '/') { + return false + } + + return !isKnownHermesSlashCommand(normalized) +} + export function isDesktopSlashSuggestion(command: string): boolean { const normalized = normalizeCommand(command) const canonical = canonicalDesktopSlashCommand(normalized) + // Surface skill / quick commands (extensions the backend provides) alongside + // the curated built-ins. Built-in aliases stay hidden so the popover isn't + // cluttered with duplicates. + if (isDesktopSlashExtensionCommand(normalized)) { + return true + } + return DESKTOP_COMMANDS.has(canonical) && !DESKTOP_ALIASES.has(normalized) }