From cde41f31409aa3978dfafc1ad44760ed3ef62d4a Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 7 Jun 2026 18:59:02 -0500 Subject: [PATCH] desktop: surface /tools, /save, /personality and fix /help skill count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move /tools and /save out of TERMINAL_ONLY_COMMANDS and /personality out of ADVANCED_COMMANDS so they appear in the desktop slash palette and execute via the existing slash.exec → command.dispatch fallback. The backend gateway already accepts these through slash.exec (none are in _PENDING_INPUT_COMMANDS or the skill list), so no backend change is required. Recompute skill_count in filterDesktopCommandsCatalog from the filtered pairs. Previously the /help footer echoed the unfiltered backend total — e.g. "60 skill commands available" while only ~29 actually appeared in the rendered list, because the desktop hides terminal-only, picker-owned, and advanced commands. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/desktop-slash-commands.test.ts | 30 +++++++++++++++++++ .../desktop/src/lib/desktop-slash-commands.ts | 30 ++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/lib/desktop-slash-commands.test.ts b/apps/desktop/src/lib/desktop-slash-commands.test.ts index de0e72ec28b4..01379d23a115 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.test.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.test.ts @@ -38,6 +38,18 @@ describe('desktop slash command curation', () => { expect(isDesktopSlashSuggestion('/curator')).toBe(false) }) + it('surfaces /tools, /save, and /personality on the desktop', () => { + expect(isDesktopSlashSuggestion('/tools')).toBe(true) + expect(isDesktopSlashSuggestion('/save')).toBe(true) + expect(isDesktopSlashSuggestion('/personality')).toBe(true) + expect(isDesktopSlashCommand('/tools')).toBe(true) + expect(isDesktopSlashCommand('/save')).toBe(true) + expect(isDesktopSlashCommand('/personality')).toBe(true) + expect(desktopSlashUnavailableMessage('/tools')).toBeNull() + expect(desktopSlashUnavailableMessage('/save')).toBeNull() + expect(desktopSlashUnavailableMessage('/personality')).toBeNull() + }) + it('allows aliases to execute without cluttering the popover', () => { expect(isDesktopSlashSuggestion('/reset')).toBe(false) expect(isDesktopSlashCommand('/reset')).toBe(true) @@ -74,6 +86,24 @@ describe('desktop slash command curation', () => { ['/new', 'Start a new desktop chat'], ['/ship-it', 'Run release checklist'] ]) + // skill_count is recomputed from the filtered output (only /ship-it is an + // extension command — /new is a built-in) so the /help footer matches what + // the user actually sees rather than echoing the unfiltered backend total. + expect(filtered.skill_count).toBe(1) + }) + + it('recomputes skill_count to reflect only extensions surfaced on desktop', () => { + const filtered = filterDesktopCommandsCatalog({ + pairs: [ + ['/new', 'Start a new session'], + ['/clear', 'Clear terminal screen'], + ['/gif-search', 'Search for a gif'], + ['/ship-it', 'Run release checklist'] + ], + skill_count: 12 + }) + + expect(filtered.pairs?.map(([cmd]) => cmd)).toEqual(['/new', '/gif-search', '/ship-it']) 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 e373ac94317f..f11151fd8197 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -31,16 +31,19 @@ const DESKTOP_COMMAND_META = [ ['/goal', 'Manage the standing goal for this session'], ['/help', 'Show desktop slash commands'], ['/new', 'Start a new desktop chat'], + ['/personality', 'Switch personality for this session'], ['/profile', 'Switch the active Hermes profile'], ['/queue', 'Queue a prompt for the next turn'], ['/resume', 'Resume a saved session'], ['/retry', 'Retry the last user message'], ['/rollback', 'List or restore filesystem checkpoints'], + ['/save', 'Save the current transcript to JSON'], ['/skin', 'Switch desktop theme or cycle to the next one'], ['/status', 'Show current session status'], ['/steer', 'Steer the current run after the next tool call'], ['/stop', 'Stop running background processes'], ['/title', 'Rename the current session'], + ['/tools', 'List or toggle tools available to the agent'], ['/undo', 'Remove the last user/assistant exchange'], ['/usage', 'Show token usage for this session'], ['/version', 'Show Hermes Agent version'], @@ -90,7 +93,6 @@ const TERMINAL_ONLY_COMMANDS = new Set([ '/redraw', '/reload', '/restart', - '/save', '/sb', '/set-home', '/sethome', @@ -98,7 +100,6 @@ const TERMINAL_ONLY_COMMANDS = new Set([ '/snapshot', '/statusbar', '/toolsets', - '/tools', '/update', '/verbose' ]) @@ -112,7 +113,6 @@ const ADVANCED_COMMANDS = new Set([ '/fast', '/insights', '/kanban', - '/personality', '/reasoning', '/reload-mcp', '/reload-skills', @@ -274,10 +274,32 @@ export function filterDesktopCommandsCatalog(catalog: CommandsCatalogLike): Comm ?.filter(([command]) => isDesktopSlashSuggestion(command)) .map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string]) + // Recount skill commands from the filtered output so /help's footer reflects + // what the user actually sees. Backend's skill_count includes commands the + // desktop hides (terminal-only, picker-owned, advanced), producing a footer + // like "60 skill commands available" while only ~29 appear in the list. + const filteredCommands = new Set() + for (const section of categories ?? []) { + for (const [command] of section.pairs) { + filteredCommands.add(canonicalDesktopSlashCommand(command)) + } + } + for (const [command] of pairs ?? []) { + filteredCommands.add(canonicalDesktopSlashCommand(command)) + } + let skillCount = 0 + for (const command of filteredCommands) { + if (isDesktopSlashExtensionCommand(command)) { + skillCount += 1 + } + } + const hasSkillCount = catalog.skill_count !== undefined || skillCount > 0 + return { ...catalog, ...(categories ? { categories } : {}), - ...(pairs ? { pairs } : {}) + ...(pairs ? { pairs } : {}), + ...(hasSkillCount ? { skill_count: skillCount } : {}) } }