export interface CommandsCatalogSection { name: string pairs: [string, string][] } export interface CommandsCatalogLike { categories?: CommandsCatalogSection[] pairs?: [string, string][] skill_count?: number warning?: string } export interface DesktopSlashCompletion { display: string meta: string text: string } export interface DesktopThemeCommandOption { description: string label: string name: string } const DESKTOP_COMMAND_META = [ ['/agents', 'Show active desktop sessions and running tasks'], ['/background', 'Run a prompt in the background'], ['/branch', 'Branch the latest message into a new chat'], ['/compress', 'Compress this conversation context'], ['/debug', 'Create a debug report'], ['/goal', 'Manage the standing goal for this session'], ['/help', 'Show desktop slash commands'], ['/new', 'Start a new desktop chat'], ['/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'], ['/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'], ['/undo', 'Remove the last user/assistant exchange'], ['/usage', 'Show token usage for this session'], ['/version', 'Show Hermes Agent version'], ['/yolo', 'Toggle YOLO — auto-approve dangerous commands'] ] as const const DESKTOP_COMMANDS: ReadonlySet = new Set(DESKTOP_COMMAND_META.map(([command]) => command)) const DESKTOP_ALIASES = new Map([ ['/bg', '/background'], ['/btw', '/background'], ['/fork', '/branch'], ['/q', '/queue'], ['/reload_mcp', '/reload-mcp'], ['/reload_skills', '/reload-skills'], ['/reset', '/new'], ['/tasks', '/agents'] ]) const DESKTOP_COMMAND_DESCRIPTIONS: ReadonlyMap = new Map(DESKTOP_COMMAND_META) const PICKER_OWNED_COMMANDS = new Set(['/model']) const TERMINAL_ONLY_COMMANDS = new Set([ '/browser', '/busy', '/clear', '/commands', '/compact', '/config', '/copy', '/cron', '/details', '/exit', '/footer', '/gateway', '/gquota', '/history', '/image', '/indicator', '/logs', '/mouse', '/paste', '/platforms', '/plugins', '/quit', '/redraw', '/reload', '/restart', '/save', '/sb', '/set-home', '/sethome', '/snap', '/snapshot', '/statusbar', '/toolsets', '/tools', '/update', '/verbose' ]) const MESSAGING_ONLY_COMMANDS = new Set(['/approve', '/deny']) const SETTINGS_OWNED_COMMANDS = new Set(['/skills']) const ADVANCED_COMMANDS = new Set([ '/curator', '/fast', '/insights', '/kanban', '/personality', '/reasoning', '/reload-mcp', '/reload-skills', '/voice' ]) const BLOCKED_COMMANDS = new Set([ ...PICKER_OWNED_COMMANDS, ...TERMINAL_ONLY_COMMANDS, ...MESSAGING_ONLY_COMMANDS, ...SETTINGS_OWNED_COMMANDS, ...ADVANCED_COMMANDS ]) function normalizeCommand(command: string): string { const trimmed = command.trim() const base = (trimmed.startsWith('/') ? trimmed : `/${trimmed}`).split(/\s+/, 1)[0]?.toLowerCase() || '' return base } export function canonicalDesktopSlashCommand(command: string): string { const normalized = normalizeCommand(command) return DESKTOP_ALIASES.get(normalized) || normalized } export function isDesktopSlashCommand(command: string): boolean { const normalized = normalizeCommand(command) const canonical = canonicalDesktopSlashCommand(normalized) if (BLOCKED_COMMANDS.has(normalized) || BLOCKED_COMMANDS.has(canonical)) { return false } 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) } export function desktopSlashUnavailableMessage(command: string): string | null { const normalized = normalizeCommand(command) const canonical = canonicalDesktopSlashCommand(normalized) if (PICKER_OWNED_COMMANDS.has(canonical)) { return `/${canonical.slice(1)} uses the desktop model picker instead of a slash command.` } if (SETTINGS_OWNED_COMMANDS.has(canonical)) { return `/${canonical.slice(1)} is managed from the desktop sidebar.` } if (MESSAGING_ONLY_COMMANDS.has(canonical)) { return `/${canonical.slice(1)} is only used from messaging platforms.` } if (ADVANCED_COMMANDS.has(canonical)) { return `/${canonical.slice(1)} is not shown in the desktop slash palette. Use the relevant desktop control or terminal interface instead.` } if (TERMINAL_ONLY_COMMANDS.has(normalized) || TERMINAL_ONLY_COMMANDS.has(canonical)) { return `/${canonical.slice(1)} is only available in the terminal interface.` } return null } export function desktopSlashDescription(command: string, fallback = ''): string { const canonical = canonicalDesktopSlashCommand(command) return DESKTOP_COMMAND_DESCRIPTIONS.get(canonical) || fallback } export function desktopSkinSlashCompletions( themes: DesktopThemeCommandOption[], activeThemeName: string, argPrefix: string ): DesktopSlashCompletion[] { const prefix = argPrefix.trim().toLowerCase() const commands: DesktopSlashCompletion[] = [ { text: '/skin list', display: '/skin list', meta: 'Show available desktop themes' }, { text: '/skin next', display: '/skin next', meta: 'Cycle to the next desktop theme' }, ...themes.map(theme => ({ text: `/skin ${theme.name}`, display: `/skin ${theme.name}`, meta: `${theme.label}${theme.name === activeThemeName ? ' (current)' : ''} - ${theme.description}` })) ] if (!prefix) { return commands } return commands.filter(item => item.text.slice('/skin '.length).toLowerCase().startsWith(prefix)) } export function filterDesktopCommandsCatalog(catalog: CommandsCatalogLike): CommandsCatalogLike { const categories = catalog.categories ?.map(section => ({ ...section, pairs: section.pairs .filter(([command]) => isDesktopSlashSuggestion(command)) .map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string]) })) .filter(section => section.pairs.length > 0) const pairs = catalog.pairs ?.filter(([command]) => isDesktopSlashSuggestion(command)) .map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string]) return { ...catalog, ...(categories ? { categories } : {}), ...(pairs ? { pairs } : {}) } } function isKnownHermesSlashCommand(command: string): boolean { return DESKTOP_COMMANDS.has(command) || DESKTOP_ALIASES.has(command) || BLOCKED_COMMANDS.has(command) }