Tesseract / studio /src /hooks /useSlashCommands.js
rkbosamia's picture
Tesseract major version upgrade - v2
9c49532
Raw
History Blame Contribute Delete
5.62 kB
/**
* 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])
}