PR #523: sync slash commands with gateway registry (mwaxman1)
This commit is contained in:
@@ -31,15 +31,50 @@ export type SlashCommandMenuHandle = {
|
||||
}
|
||||
|
||||
export const DEFAULT_SLASH_COMMANDS: Array<SlashCommandDefinition> = [
|
||||
// Session control
|
||||
{ command: '/new', description: 'Start new session' },
|
||||
{ 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: '/save', description: 'Save the current conversation' },
|
||||
{ command: '/skills', description: 'Browse and manage skills' },
|
||||
{ command: '/plugins', description: 'List installed plugins and their status' },
|
||||
{ command: '/mcp', description: 'Manage MCP servers' },
|
||||
{ command: '/reasoning', description: 'Set reasoning level (none/minimal/low/medium/high/xhigh)' },
|
||||
{ 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(
|
||||
@@ -148,18 +183,20 @@ const SlashCommandMenu = forwardRef(function SlashCommandMenu(
|
||||
<CommandItem
|
||||
key={item.command}
|
||||
value={item.command}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onMouseMove={() => setActiveIndex(index)}
|
||||
onClick={() => onSelect(item)}
|
||||
onSelect={() => onSelect(item)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
onSelect(item)
|
||||
}}
|
||||
className={cn(
|
||||
'flex flex-col items-start gap-0.5 rounded-md px-3 py-2',
|
||||
index === activeIndex && 'bg-primary-100 text-primary-900',
|
||||
'flex items-center gap-2 px-3 py-2 text-sm transition-colors',
|
||||
index === activeIndex && 'bg-neutral-100 dark:bg-neutral-800',
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-semibold">{item.command}</span>
|
||||
<span className="text-xs text-primary-600">
|
||||
{item.description}
|
||||
<span className="font-mono text-[var(--color-accent,#6366f1)]">
|
||||
{item.command}
|
||||
</span>
|
||||
<span className="text-primary-600">{item.description}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
@@ -170,8 +207,5 @@ const SlashCommandMenu = forwardRef(function SlashCommandMenu(
|
||||
)
|
||||
})
|
||||
|
||||
export {
|
||||
SlashCommandMenu,
|
||||
type SlashCommandDefinition,
|
||||
type SlashCommandMenuHandle,
|
||||
}
|
||||
export { SlashCommandMenu }
|
||||
export default SlashCommandMenu
|
||||
|
||||
42
src/routes/api/commands.ts
Normal file
42
src/routes/api/commands.ts
Normal 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 },
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1671,10 +1671,26 @@ function ChatComposerComponent({
|
||||
const promptPlaceholder = isMobileViewport
|
||||
? 'Message...'
|
||||
: '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(
|
||||
() =>
|
||||
mergeSlashCommands(
|
||||
DEFAULT_SLASH_COMMANDS,
|
||||
mergeSlashCommands(DEFAULT_SLASH_COMMANDS, serverCommands),
|
||||
(installedSkillsQuery.data ?? [])
|
||||
.filter((skill) => skill.installed && skill.enabled)
|
||||
.map((skill) => ({
|
||||
@@ -1682,7 +1698,7 @@ function ChatComposerComponent({
|
||||
description: skill.description || `Run ${skill.name}`,
|
||||
})),
|
||||
),
|
||||
[installedSkillsQuery.data],
|
||||
[serverCommands, installedSkillsQuery.data],
|
||||
)
|
||||
const slashCommandQuery = useMemo(() => readSlashCommandQuery(value), [value])
|
||||
const isSlashMenuOpen =
|
||||
|
||||
Reference in New Issue
Block a user