mirror of
https://github.com/JohnRiceML/clawport-ui.git
synced 2026-08-14 08:51:58 +00:00
Add slash commands to agent chat UI
Introduce client-side slash commands (/clear, /help, /info, /soul, /tools, /crons) with an autocomplete dropdown in the chat input. - New lib/slash-commands.ts with command registry, parser, matcher, and executor - Autocomplete dropdown appears when typing "/" with keyboard navigation (arrow keys, Enter/Tab to select, Escape to dismiss) - System messages render as accent-bordered cards, filtered from API calls so they never reach the gateway - 35 new tests covering all parser and executor paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a829e32859
commit
9efebfdac0
@@ -7,6 +7,8 @@ import { parseMedia, addMessage, updateLastMessage } from '@/lib/conversations'
|
||||
import { buildApiContent } from '@/lib/multimodal'
|
||||
import { generateId } from '@/lib/id'
|
||||
import { useSettings } from '@/app/settings-provider'
|
||||
import { isSlashInput, matchCommands, parseSlashCommand, executeCommand } from '@/lib/slash-commands'
|
||||
import type { SlashCommand } from '@/lib/slash-commands'
|
||||
import { FileAttachment } from './FileAttachment'
|
||||
import { MediaPreview } from './MediaPreview'
|
||||
import { AgentAvatar } from '@/components/AgentAvatar'
|
||||
@@ -293,6 +295,9 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const [pendingAttachments, setPendingAttachments] = useState<MediaAttachment[]>([])
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
const [slashMatches, setSlashMatches] = useState<SlashCommand[]>([])
|
||||
const [slashIndex, setSlashIndex] = useState(0)
|
||||
const slashMenuOpen = slashMatches.length > 0
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -355,10 +360,12 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
setIsStreaming(true)
|
||||
|
||||
// Use ref to read latest messages (avoids stale closure)
|
||||
const apiMessages = [...messagesRef.current, userMsg].map(m => ({
|
||||
role: m.role,
|
||||
content: buildApiContent(m),
|
||||
}))
|
||||
const apiMessages = [...messagesRef.current, userMsg]
|
||||
.filter(m => m.role !== 'system')
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: buildApiContent(m),
|
||||
}))
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/chat/${agent.id}`, {
|
||||
@@ -404,7 +411,52 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
}
|
||||
}, [input, pendingAttachments, isStreaming, agent.id, onUpdate])
|
||||
|
||||
function runSlashCommand(command: string) {
|
||||
const result = executeCommand(command, agent)
|
||||
if (result.action === 'clear') {
|
||||
clearChat()
|
||||
} else {
|
||||
const sysMsg: Message = {
|
||||
id: generateId(),
|
||||
role: 'system',
|
||||
content: result.content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
onUpdate(agent.id, prev => addMessage(prev, agent.id, sysMsg))
|
||||
}
|
||||
setInput('')
|
||||
setSlashMatches([])
|
||||
if (textareaRef.current) textareaRef.current.style.height = 'auto'
|
||||
}
|
||||
|
||||
function handleSlashSelect(cmd: SlashCommand) {
|
||||
runSlashCommand(cmd.name)
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (slashMenuOpen) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setSlashIndex(i => (i + 1) % slashMatches.length)
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
setSlashIndex(i => (i - 1 + slashMatches.length) % slashMatches.length)
|
||||
return
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
handleSlashSelect(slashMatches[slashIndex])
|
||||
return
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
setSlashMatches([])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
textareaRef.current?.blur()
|
||||
@@ -412,6 +464,11 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
const parsed = parseSlashCommand(input)
|
||||
if (parsed) {
|
||||
runSlashCommand(parsed.command)
|
||||
return
|
||||
}
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
@@ -747,7 +804,7 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
const isUser = msg.role === 'user'
|
||||
const showAvatar = shouldShowAvatar(messages, i)
|
||||
const showTimestamp = shouldShowTimestamp(messages, i)
|
||||
const isLastAssistant = !isUser && i === messages.length - 1 && (isStreaming || msg.isStreaming)
|
||||
const isLastAssistant = msg.role === 'assistant' && i === messages.length - 1 && (isStreaming || msg.isStreaming)
|
||||
const showTypingDots = isLastAssistant && !msg.content
|
||||
const media = msg.media || parseMedia(msg.content)
|
||||
|
||||
@@ -817,8 +874,30 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System message (slash command result) */}
|
||||
{msg.role === 'system' && (
|
||||
<div style={{
|
||||
padding: '0 var(--space-4)',
|
||||
marginBottom: 'var(--space-1)',
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '85%',
|
||||
margin: '0 auto',
|
||||
padding: 'var(--space-3) var(--space-4)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
background: 'var(--fill-tertiary)',
|
||||
borderLeft: '3px solid var(--accent)',
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: 'var(--text-footnote)',
|
||||
lineHeight: 'var(--leading-relaxed)',
|
||||
}}>
|
||||
{formatMessage(msg.content)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assistant message */}
|
||||
{!isUser && (
|
||||
{msg.role === 'assistant' && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
@@ -898,6 +977,64 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
background: 'var(--material-regular)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{/* Slash command autocomplete dropdown */}
|
||||
{slashMenuOpen && (
|
||||
<div
|
||||
className="animate-slide-down"
|
||||
style={{
|
||||
marginBottom: 'var(--space-2)',
|
||||
background: 'var(--material-thick)',
|
||||
border: '1px solid var(--separator)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
boxShadow: 'var(--shadow-overlay)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{slashMatches.map((cmd, i) => (
|
||||
<button
|
||||
key={cmd.name}
|
||||
onMouseDown={e => {
|
||||
e.preventDefault() // prevent textarea blur
|
||||
handleSlashSelect(cmd)
|
||||
}}
|
||||
onMouseEnter={() => setSlashIndex(i)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-3)',
|
||||
width: '100%',
|
||||
padding: 'var(--space-2) var(--space-3)',
|
||||
background: i === slashIndex ? 'var(--fill-secondary)' : 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 'var(--text-subheadline)',
|
||||
transition: 'background 100ms',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
color: 'var(--accent)',
|
||||
fontWeight: 'var(--weight-semibold)',
|
||||
fontFamily: '"SF Mono", Menlo, monospace',
|
||||
fontSize: 'var(--text-footnote)',
|
||||
minWidth: 60,
|
||||
}}>
|
||||
{cmd.name}
|
||||
</span>
|
||||
<span style={{
|
||||
color: 'var(--text-tertiary)',
|
||||
fontSize: 'var(--text-caption1)',
|
||||
}}>
|
||||
{cmd.description}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pending attachments preview */}
|
||||
{pendingAttachments.length > 0 && (
|
||||
<div style={{ marginBottom: 'var(--space-2)' }}>
|
||||
@@ -948,7 +1085,17 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onChange={e => {
|
||||
const val = e.target.value
|
||||
setInput(val)
|
||||
if (isSlashInput(val) && !val.includes(' ')) {
|
||||
const matches = matchCommands(val)
|
||||
setSlashMatches(matches)
|
||||
setSlashIndex(0)
|
||||
} else {
|
||||
setSlashMatches([])
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder={`Message ${agent.name}...`}
|
||||
@@ -1012,7 +1159,7 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
||||
textAlign: 'center',
|
||||
marginTop: 'var(--space-1)',
|
||||
}}>
|
||||
Enter to send · Shift+Enter for newline
|
||||
Enter to send · Shift+Enter for newline · / for commands
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface MediaAttachment {
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
timestamp: number
|
||||
media?: MediaAttachment[]
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
isSlashInput,
|
||||
parseSlashCommand,
|
||||
matchCommands,
|
||||
executeCommand,
|
||||
COMMANDS,
|
||||
} from './slash-commands'
|
||||
import type { Agent } from './types'
|
||||
|
||||
function makeAgent(overrides: Partial<Agent> = {}): Agent {
|
||||
return {
|
||||
id: 'test-agent',
|
||||
name: 'TestBot',
|
||||
title: 'Test Agent',
|
||||
reportsTo: null,
|
||||
directReports: [],
|
||||
soulPath: null,
|
||||
soul: null,
|
||||
voiceId: null,
|
||||
color: '#ff0000',
|
||||
emoji: '🤖',
|
||||
tools: [],
|
||||
crons: [],
|
||||
memoryPath: null,
|
||||
description: 'A test agent for unit tests.',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/* ── isSlashInput ─────────────────────────────────────── */
|
||||
|
||||
describe('isSlashInput', () => {
|
||||
it('returns true for input starting with /', () => {
|
||||
expect(isSlashInput('/help')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for input with leading whitespace before /', () => {
|
||||
expect(isSlashInput(' /clear')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for bare /', () => {
|
||||
expect(isSlashInput('/')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for empty string', () => {
|
||||
expect(isSlashInput('')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for regular text', () => {
|
||||
expect(isSlashInput('hello world')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for slash in middle of text', () => {
|
||||
expect(isSlashInput('use /help command')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
/* ── parseSlashCommand ────────────────────────────────── */
|
||||
|
||||
describe('parseSlashCommand', () => {
|
||||
it('parses a known command', () => {
|
||||
expect(parseSlashCommand('/help')).toEqual({ command: '/help', args: '' })
|
||||
})
|
||||
|
||||
it('parses a command with args', () => {
|
||||
expect(parseSlashCommand('/help extra stuff')).toEqual({ command: '/help', args: 'extra stuff' })
|
||||
})
|
||||
|
||||
it('is case insensitive', () => {
|
||||
expect(parseSlashCommand('/CLEAR')).toEqual({ command: '/clear', args: '' })
|
||||
})
|
||||
|
||||
it('handles leading whitespace', () => {
|
||||
expect(parseSlashCommand(' /soul')).toEqual({ command: '/soul', args: '' })
|
||||
})
|
||||
|
||||
it('returns null for unknown command', () => {
|
||||
expect(parseSlashCommand('/unknown')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for empty string', () => {
|
||||
expect(parseSlashCommand('')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for non-slash input', () => {
|
||||
expect(parseSlashCommand('hello')).toBeNull()
|
||||
})
|
||||
|
||||
it('parses all registered commands', () => {
|
||||
for (const cmd of COMMANDS) {
|
||||
const result = parseSlashCommand(cmd.name)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.command).toBe(cmd.name)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/* ── matchCommands ────────────────────────────────────── */
|
||||
|
||||
describe('matchCommands', () => {
|
||||
it('returns all commands for bare /', () => {
|
||||
const matches = matchCommands('/')
|
||||
expect(matches).toHaveLength(COMMANDS.length)
|
||||
})
|
||||
|
||||
it('filters by partial match', () => {
|
||||
const matches = matchCommands('/cl')
|
||||
expect(matches).toHaveLength(1)
|
||||
expect(matches[0].name).toBe('/clear')
|
||||
})
|
||||
|
||||
it('returns multiple matches when prefix matches several', () => {
|
||||
const matches = matchCommands('/c')
|
||||
expect(matches.length).toBeGreaterThanOrEqual(2) // /clear, /crons
|
||||
expect(matches.map(m => m.name)).toContain('/clear')
|
||||
expect(matches.map(m => m.name)).toContain('/crons')
|
||||
})
|
||||
|
||||
it('returns exact match', () => {
|
||||
const matches = matchCommands('/help')
|
||||
expect(matches).toHaveLength(1)
|
||||
expect(matches[0].name).toBe('/help')
|
||||
})
|
||||
|
||||
it('returns empty for non-matching input', () => {
|
||||
expect(matchCommands('/xyz')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty for non-slash input', () => {
|
||||
expect(matchCommands('hello')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty for empty string', () => {
|
||||
expect(matchCommands('')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('is case insensitive', () => {
|
||||
const matches = matchCommands('/HEL')
|
||||
expect(matches).toHaveLength(1)
|
||||
expect(matches[0].name).toBe('/help')
|
||||
})
|
||||
})
|
||||
|
||||
/* ── executeCommand ───────────────────────────────────── */
|
||||
|
||||
describe('executeCommand', () => {
|
||||
const agent = makeAgent({
|
||||
name: 'VERA',
|
||||
title: 'Chief Strategy Officer',
|
||||
description: 'Oversees strategic planning.',
|
||||
tools: ['web-search', 'file-read'],
|
||||
soul: '# VERA\nI am the strategy lead.',
|
||||
memoryPath: '/memory/vera',
|
||||
crons: [
|
||||
{
|
||||
id: 'daily-report',
|
||||
name: 'Daily Report',
|
||||
schedule: '0 8 * * *',
|
||||
scheduleDescription: 'Daily at 8 AM',
|
||||
timezone: 'US/Eastern',
|
||||
status: 'ok',
|
||||
lastRun: '2025-01-01',
|
||||
nextRun: '2025-01-02',
|
||||
lastError: null,
|
||||
agentId: 'vera',
|
||||
description: 'Generate daily report',
|
||||
enabled: true,
|
||||
delivery: null,
|
||||
lastDurationMs: 5000,
|
||||
consecutiveErrors: 0,
|
||||
lastDeliveryStatus: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
it('/clear returns action', () => {
|
||||
const result = executeCommand('/clear', agent)
|
||||
expect(result.content).toBe('Conversation cleared.')
|
||||
expect(result.action).toBe('clear')
|
||||
})
|
||||
|
||||
it('/help lists all commands', () => {
|
||||
const result = executeCommand('/help', agent)
|
||||
for (const cmd of COMMANDS) {
|
||||
expect(result.content).toContain(cmd.name)
|
||||
expect(result.content).toContain(cmd.description)
|
||||
}
|
||||
})
|
||||
|
||||
it('/info shows agent profile', () => {
|
||||
const result = executeCommand('/info', agent)
|
||||
expect(result.content).toContain('VERA')
|
||||
expect(result.content).toContain('Chief Strategy Officer')
|
||||
expect(result.content).toContain('web-search')
|
||||
expect(result.content).toContain('Memory: /memory/vera')
|
||||
})
|
||||
|
||||
it('/info shows "none" when agent has no tools', () => {
|
||||
const bare = makeAgent()
|
||||
const result = executeCommand('/info', bare)
|
||||
expect(result.content).toContain('Tools: none')
|
||||
})
|
||||
|
||||
it('/info shows "not configured" when no memory path', () => {
|
||||
const bare = makeAgent()
|
||||
const result = executeCommand('/info', bare)
|
||||
expect(result.content).toContain('Memory: not configured')
|
||||
})
|
||||
|
||||
it('/soul shows SOUL.md content', () => {
|
||||
const result = executeCommand('/soul', agent)
|
||||
expect(result.content).toBe('# VERA\nI am the strategy lead.')
|
||||
})
|
||||
|
||||
it('/soul handles missing SOUL.md', () => {
|
||||
const bare = makeAgent()
|
||||
const result = executeCommand('/soul', bare)
|
||||
expect(result.content).toContain('No SOUL.md found')
|
||||
})
|
||||
|
||||
it('/tools lists tools', () => {
|
||||
const result = executeCommand('/tools', agent)
|
||||
expect(result.content).toContain('web-search')
|
||||
expect(result.content).toContain('file-read')
|
||||
})
|
||||
|
||||
it('/tools handles no tools', () => {
|
||||
const bare = makeAgent()
|
||||
const result = executeCommand('/tools', bare)
|
||||
expect(result.content).toContain('no tools configured')
|
||||
})
|
||||
|
||||
it('/crons lists cron jobs', () => {
|
||||
const result = executeCommand('/crons', agent)
|
||||
expect(result.content).toContain('Daily Report')
|
||||
expect(result.content).toContain('Daily at 8 AM')
|
||||
expect(result.content).toContain('ok')
|
||||
})
|
||||
|
||||
it('/crons shows disabled status for disabled jobs', () => {
|
||||
const agentWithDisabled = makeAgent({
|
||||
crons: [{
|
||||
id: 'test',
|
||||
name: 'Test Job',
|
||||
schedule: '0 0 * * *',
|
||||
scheduleDescription: 'Daily at midnight',
|
||||
timezone: null,
|
||||
status: 'ok',
|
||||
lastRun: null,
|
||||
nextRun: null,
|
||||
lastError: null,
|
||||
agentId: null,
|
||||
description: null,
|
||||
enabled: false,
|
||||
delivery: null,
|
||||
lastDurationMs: null,
|
||||
consecutiveErrors: 0,
|
||||
lastDeliveryStatus: null,
|
||||
}],
|
||||
})
|
||||
const result = executeCommand('/crons', agentWithDisabled)
|
||||
expect(result.content).toContain('disabled')
|
||||
})
|
||||
|
||||
it('/crons handles no cron jobs', () => {
|
||||
const bare = makeAgent()
|
||||
const result = executeCommand('/crons', bare)
|
||||
expect(result.content).toContain('no cron jobs')
|
||||
})
|
||||
|
||||
it('unknown command returns error message', () => {
|
||||
const result = executeCommand('/bogus', agent)
|
||||
expect(result.content).toContain('Unknown command')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { Agent } from './types'
|
||||
|
||||
export interface SlashCommand {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const COMMANDS: SlashCommand[] = [
|
||||
{ name: '/clear', description: 'Clear conversation history' },
|
||||
{ name: '/help', description: 'Show available commands' },
|
||||
{ name: '/info', description: 'Show agent profile summary' },
|
||||
{ name: '/soul', description: "Show agent's SOUL.md persona" },
|
||||
{ name: '/tools', description: "List agent's available tools" },
|
||||
{ name: '/crons', description: "Show agent's scheduled jobs" },
|
||||
]
|
||||
|
||||
export interface ParsedCommand {
|
||||
command: string
|
||||
args: string
|
||||
}
|
||||
|
||||
/** Returns true if input looks like the start of a slash command (leading `/`). */
|
||||
export function isSlashInput(input: string): boolean {
|
||||
return input.trimStart().startsWith('/')
|
||||
}
|
||||
|
||||
/** Parse a complete slash command from input. Returns null if not a valid command. */
|
||||
export function parseSlashCommand(input: string): ParsedCommand | null {
|
||||
const trimmed = input.trimStart()
|
||||
if (!trimmed.startsWith('/')) return null
|
||||
|
||||
const spaceIdx = trimmed.indexOf(' ')
|
||||
const command = spaceIdx === -1 ? trimmed.toLowerCase() : trimmed.slice(0, spaceIdx).toLowerCase()
|
||||
const args = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1).trim()
|
||||
|
||||
const match = COMMANDS.find(c => c.name === command)
|
||||
if (!match) return null
|
||||
|
||||
return { command: match.name, args }
|
||||
}
|
||||
|
||||
/** Return commands matching a partial input (e.g. "/cl" matches "/clear"). */
|
||||
export function matchCommands(partial: string): SlashCommand[] {
|
||||
const trimmed = partial.trimStart().toLowerCase()
|
||||
if (!trimmed.startsWith('/')) return []
|
||||
|
||||
// Show all commands for bare "/"
|
||||
if (trimmed === '/') return [...COMMANDS]
|
||||
|
||||
return COMMANDS.filter(c => c.name.startsWith(trimmed))
|
||||
}
|
||||
|
||||
/** Execute a slash command and return the formatted content string for a system message. */
|
||||
export function executeCommand(command: string, agent: Agent): { content: string; action?: 'clear' } {
|
||||
switch (command) {
|
||||
case '/clear':
|
||||
return { content: 'Conversation cleared.', action: 'clear' }
|
||||
|
||||
case '/help':
|
||||
return {
|
||||
content: [
|
||||
'**Available commands**',
|
||||
'',
|
||||
...COMMANDS.map(c => `\`${c.name}\` -- ${c.description}`),
|
||||
'',
|
||||
'Type `/` to see the command menu.',
|
||||
].join('\n'),
|
||||
}
|
||||
|
||||
case '/info':
|
||||
return {
|
||||
content: [
|
||||
`**${agent.name}**`,
|
||||
agent.title,
|
||||
'',
|
||||
agent.description,
|
||||
'',
|
||||
`Tools: ${agent.tools.length > 0 ? agent.tools.join(', ') : 'none'}`,
|
||||
`Cron jobs: ${agent.crons.length}`,
|
||||
agent.memoryPath ? `Memory: ${agent.memoryPath}` : 'Memory: not configured',
|
||||
].join('\n'),
|
||||
}
|
||||
|
||||
case '/soul': {
|
||||
if (!agent.soul) {
|
||||
return { content: `No SOUL.md found for ${agent.name}.` }
|
||||
}
|
||||
return { content: agent.soul }
|
||||
}
|
||||
|
||||
case '/tools': {
|
||||
if (agent.tools.length === 0) {
|
||||
return { content: `${agent.name} has no tools configured.` }
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
`**${agent.name}'s tools**`,
|
||||
'',
|
||||
...agent.tools.map(t => `- ${t}`),
|
||||
].join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
case '/crons': {
|
||||
if (agent.crons.length === 0) {
|
||||
return { content: `${agent.name} has no cron jobs.` }
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
`**${agent.name}'s cron jobs**`,
|
||||
'',
|
||||
...agent.crons.map(c => {
|
||||
const status = c.enabled ? c.status : 'disabled'
|
||||
return `- **${c.name}** (${c.scheduleDescription}) -- ${status}`
|
||||
}),
|
||||
].join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return { content: `Unknown command: ${command}` }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user