/** * Slash-command registry. * * Two flavors of command: * * Action commands (`run` field). Fire deterministic studio actions — * `/run`, `/stop`, `/restart`, `/reload`, `/clear-logs`, `/snapshot`. * Never reach Groq. Composer input is cleared after dispatch. * * Prompt commands (`fill` field). Drop a templated request into the * composer for the user to review/edit before pressing Send — `/explain`, * `/fix`, `/refactor`, `/test`, `/style`. These DO reach Groq, but only * after the user confirms by hitting Enter. * * `useSlashCommands({ activeFile })` returns the live list — entries depend * on whether there's an active file (prompt commands need a target) and on * dev-server state (action commands depend on the run lifecycle). * * VibePanel reads the `fill` vs `run` discriminator in `runSlashAt`. */ import { useMemo } from 'react' import useAppStore from '../store/appStore' import { useProcesses } from './useProcesses' export function useSlashCommands({ activeFile = null } = {}) { const { start, stop, reload, status } = useProcesses() const clearLogs = useAppStore((s) => s.clearLogs) const showToast = useAppStore((s) => s.showToast) const setSnapshotsOpen = useAppStore((s) => s.setSnapshotsOpen) return useMemo(() => { const isRunning = status === 'running' const isTransitional = ['installing', 'starting', 'stopping'].includes(status) const fileShort = activeFile ? activeFile.split('/').pop() : null return [ // ── Action commands — execute immediately, clear composer ────────── { id: 'run', label: '/run', hint: 'Start the dev servers', disabled: isRunning || isTransitional, run: () => { start(); showToast('Starting dev servers…') }, }, { id: 'stop', label: '/stop', hint: 'Stop the dev servers', disabled: status === 'idle' || status === 'stopping', run: () => { stop(); showToast('Stopping dev servers…') }, }, { id: 'restart', label: '/restart', hint: 'Stop, then start again', disabled: status === 'stopping', run: async () => { showToast('Restarting dev servers…') if (status !== 'idle') { await stop() await new Promise((r) => setTimeout(r, 400)) } await start() }, }, { id: 'reload', label: '/reload', hint: 'Reload the preview iframe', disabled: !isRunning, run: () => { reload(); showToast('Preview reloaded') }, }, { id: 'clear-logs', label: '/clear-logs', hint: 'Empty the logs panel', disabled: false, run: () => { clearLogs(); showToast('Logs cleared') }, }, { id: 'snapshot', label: '/snapshot', hint: 'Open snapshots — save or restore project state', disabled: false, run: () => { setSnapshotsOpen(true) }, }, // ── Prompt commands — prefill composer, do not auto-send ─────────── // Each templates a request scoped to the active file and auto-pins it. // User reviews, optionally edits, then presses Enter. { id: 'explain', label: '/explain', hint: fileShort ? `Explain ${fileShort}` : 'Open a file first', disabled: !activeFile, fill: () => ({ text: `Walk me through \`${activeFile}\` — what it does, how it's wired into the rest of the codebase, and any non-obvious decisions.`, pinnedFiles: [activeFile], }), }, { id: 'fix', label: '/fix', hint: fileShort ? `Fix errors in ${fileShort}` : 'Open a file first', disabled: !activeFile, fill: () => ({ text: `There's a bug in \`${activeFile}\`. Read it, check the dev-server logs with \`read_logs\` if useful, and fix the root cause. Avoid bandaid patches.`, pinnedFiles: [activeFile], }), }, { id: 'refactor', label: '/refactor', hint: fileShort ? `Refactor ${fileShort}` : 'Open a file first', disabled: !activeFile, fill: () => ({ text: `Refactor \`${activeFile}\` for readability and clarity. Keep behavior identical. Don't introduce new dependencies, don't widen abstractions, don't change file boundaries unless the win is obvious.`, pinnedFiles: [activeFile], }), }, { id: 'test', label: '/test', hint: fileShort ? `Write tests for ${fileShort}` : 'Open a file first', disabled: !activeFile, fill: () => ({ text: `Write tests for \`${activeFile}\`. Cover the happy path plus the edge cases that would actually break in practice. Use whatever test framework the project already has — check package.json first.`, pinnedFiles: [activeFile], }), }, { id: 'style', label: '/style', hint: fileShort ? `Polish UI/UX of ${fileShort}` : 'Open a file first', disabled: !activeFile, fill: () => ({ text: `Polish the visual design and UX of \`${activeFile}\` — spacing, typography, color, hover states, motion. Match the rest of the app. Don't change behavior, just appearance.`, pinnedFiles: [activeFile], }), }, ] }, [status, start, stop, reload, clearLogs, showToast, setSnapshotsOpen, activeFile]) }