PR #523: sync slash commands with gateway registry (mwaxman1)

This commit is contained in:
Aurora
2026-06-05 04:25:16 -04:00
parent bac192d834
commit d05113b3cd
3 changed files with 112 additions and 20 deletions

View File

@@ -31,15 +31,50 @@ export type SlashCommandMenuHandle = {
} }
export const DEFAULT_SLASH_COMMANDS: Array<SlashCommandDefinition> = [ export const DEFAULT_SLASH_COMMANDS: Array<SlashCommandDefinition> = [
// Session control
{ command: '/new', description: 'Start new session' }, { command: '/new', description: 'Start new session' },
{ command: '/clear', description: 'Clear screen and start fresh' }, { command: '/clear', description: 'Clear screen and start fresh' },
{ command: '/retry', description: 'Resend the last message' },
{ command: '/undo', description: 'Remove the last exchange' },
{ command: '/title', description: 'Name the current session' },
{ command: '/compress', description: 'Manually compress context' },
// Persistent goals (Ralph loop)
{ command: '/goal <text>', description: 'Set standing goal across turns' },
{ command: '/goal status', description: 'Check active goal status' },
{ command: '/goal pause', description: 'Pause active goal' },
{ command: '/goal resume', description: 'Resume paused goal' },
{ command: '/goal clear', description: 'Clear active goal' },
{ command: '/subgoal <text>', description: 'Add extra success criteria to active goal' },
// Model & config
{ command: '/model', description: 'Show or change the current model' }, { command: '/model', description: 'Show or change the current model' },
{ command: '/save', description: 'Save the current conversation' }, { command: '/reasoning', description: 'Set reasoning level (none/minimal/low/medium/high/xhigh)' },
{ command: '/skills', description: 'Browse and manage skills' },
{ command: '/plugins', description: 'List installed plugins and their status' },
{ command: '/mcp', description: 'Manage MCP servers' },
{ command: '/skin', description: 'Change the display theme' }, { command: '/skin', description: 'Change the display theme' },
{ command: '/help', description: 'Show available commands' }, { command: '/config', description: 'Show session config' },
{ command: '/profile', description: 'Show active Hermes profile info' },
// Tools & skills
{ command: '/skills', description: 'Browse and manage skills' },
{ command: '/skill <name>', description: 'Load a skill into session' },
{ command: '/plugins', description: 'List installed plugins' },
{ command: '/mcp', description: 'Manage MCP servers' },
{ command: '/cron', description: 'Manage cron jobs' },
{ command: '/kanban', description: 'Kanban collaboration board' },
// Session management
{ command: '/save', description: 'Save the current conversation' },
{ command: '/history', description: 'Show conversation history' },
{ command: '/agents', description: 'Show active agents and running tasks' },
{ command: '/resume', description: 'Resume a named session' },
{ command: '/branch', description: 'Branch the current session' },
{ command: '/fork', description: 'Fork the current session' },
// Info
{ command: '/help', description: 'Show all available commands' },
{ command: '/usage', description: 'View token usage' },
{ command: '/status', description: 'Show session info' },
{ command: '/debug', description: 'Upload debug report' },
] ]
export function mergeSlashCommands( export function mergeSlashCommands(
@@ -148,18 +183,20 @@ const SlashCommandMenu = forwardRef(function SlashCommandMenu(
<CommandItem <CommandItem
key={item.command} key={item.command}
value={item.command} value={item.command}
onMouseDown={(event) => event.preventDefault()} onSelect={() => onSelect(item)}
onMouseMove={() => setActiveIndex(index)} onMouseDown={(e) => {
onClick={() => onSelect(item)} e.preventDefault()
onSelect(item)
}}
className={cn( className={cn(
'flex flex-col items-start gap-0.5 rounded-md px-3 py-2', 'flex items-center gap-2 px-3 py-2 text-sm transition-colors',
index === activeIndex && 'bg-primary-100 text-primary-900', index === activeIndex && 'bg-neutral-100 dark:bg-neutral-800',
)} )}
> >
<span className="text-sm font-semibold">{item.command}</span> <span className="font-mono text-[var(--color-accent,#6366f1)]">
<span className="text-xs text-primary-600"> {item.command}
{item.description}
</span> </span>
<span className="text-primary-600">{item.description}</span>
</CommandItem> </CommandItem>
))} ))}
</CommandList> </CommandList>
@@ -170,8 +207,5 @@ const SlashCommandMenu = forwardRef(function SlashCommandMenu(
) )
}) })
export { export { SlashCommandMenu }
SlashCommandMenu, export default SlashCommandMenu
type SlashCommandDefinition,
type SlashCommandMenuHandle,
}

View File

@@ -0,0 +1,42 @@
/**
* Server-side proxy for /v1/commands on the gateway.
*
* The gateway exposes a list of slash commands at /v1/commands.
* This route proxies that call server-side (with gateway auth) so the
* browser never needs to reach the gateway port directly.
*/
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
import { isAuthenticated } from '../../server/auth-middleware'
import { gatewayFetch } from '../../server/gateway-capabilities'
export const Route = createFileRoute('/api/commands')({
server: {
handlers: {
GET: async ({ request }) => {
if (!isAuthenticated(request)) {
return json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const res = await gatewayFetch('/v1/commands')
if (!res.ok) {
return json(
{ error: `Gateway responded with status ${res.status}` },
{ status: res.status },
)
}
const body = await res.json()
return Response.json(body)
} catch {
return json(
{ error: 'Gateway is unreachable' },
{ status: 500 },
)
}
},
},
},
})

View File

@@ -1671,10 +1671,26 @@ function ChatComposerComponent({
const promptPlaceholder = isMobileViewport const promptPlaceholder = isMobileViewport
? 'Message...' ? 'Message...'
: 'Ask anything... (↵ to send · ⇧↵ new line · ⌘⇧M switch model)' : 'Ask anything... (↵ to send · ⇧↵ new line · ⌘⇧M switch model)'
const [serverCommands, setServerCommands] = useState<SlashCommandDefinition[]>([])
useEffect(() => {
fetch('/api/commands')
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data: { commands?: Array<{ command: string; description: string }> }) => {
setServerCommands(data.commands ?? [])
})
.catch(() => {
// fall back to DEFAULT_SLASH_COMMANDS only
})
}, [])
const slashCommands = useMemo( const slashCommands = useMemo(
() => () =>
mergeSlashCommands( mergeSlashCommands(
DEFAULT_SLASH_COMMANDS, mergeSlashCommands(DEFAULT_SLASH_COMMANDS, serverCommands),
(installedSkillsQuery.data ?? []) (installedSkillsQuery.data ?? [])
.filter((skill) => skill.installed && skill.enabled) .filter((skill) => skill.installed && skill.enabled)
.map((skill) => ({ .map((skill) => ({
@@ -1682,7 +1698,7 @@ function ChatComposerComponent({
description: skill.description || `Run ${skill.name}`, description: skill.description || `Run ${skill.name}`,
})), })),
), ),
[installedSkillsQuery.data], [serverCommands, installedSkillsQuery.data],
) )
const slashCommandQuery = useMemo(() => readSlashCommandQuery(value), [value]) const slashCommandQuery = useMemo(() => readSlashCommandQuery(value), [value])
const isSlashMenuOpen = const isSlashMenuOpen =