import { useQuery } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
import { FolderKanban, MessageSquare, Zap } from 'lucide-react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useProjects } from '@/hooks/use-projects'
import { api } from '@/lib/api-client'
import { useChatStore } from '@/stores/chat'
import type { Skill } from '@/types'
import { modePrimaryVerb } from './ranker'
import type { CommandProvider, PaletteItem, QueryContext } from './types'
/**
* Run provider — verb `run` (prefix `>`).
*
* v1 = frontend message synthesis (design §8). Composes an intent over the
* existing per-message slots (`project_id`, `mount_ids`) and sends via the chat
* WS. `@persona` is deferred to v2 (no per-message slot yet); persona switching
* is owned by `~ @persona` (SwitchProvider).
*
* Also owns chat-mode bare-text (mode-primary `run`): typing plain text in chat
* mode sends it as a message.
*/
export function useRunProvider(): CommandProvider {
const { t } = useTranslation()
const router = useRouter()
const setActiveProject = useChatStore((s) => s.setActiveProject)
const setActiveMountIds = useChatStore((s) => s.setActiveMountIds)
const sendMessage = useChatStore((s) => s.sendMessage)
const projectsQ = useProjects()
const skillsQ = useQuery({
queryKey: ['skills'],
queryFn: () => api.get<{ skills: Skill[] }>('/api/skills'),
staleTime: 60_000,
})
const projects = projectsQ.data?.items ?? []
const skills = skillsQ.data?.skills ?? []
const startChat = useCallback(
(content: string) => {
router.history.push('/chat')
sendMessage(content)
},
[router, sendMessage],
)
return useMemo(
() => ({
id: 'run',
verbs: ['run'],
resolve(ctx: QueryContext): PaletteItem[] {
const isRun = ctx.verb === 'run'
const isChatBare = ctx.verb === null && modePrimaryVerb(ctx.mode) === 'run'
if (!isRun && !isChatBare) return []
const text = ctx.text.trim()
const ent = ctx.entity
// Entity-targeted run.
if (ent) {
const name = ent.name.toLowerCase()
const wantsSkill = ent.type === 'skill'
const wantsProject = ent.type === 'project'
const skill = !wantsProject
? skills.find(
(s) => s.name.toLowerCase() === name || s.name.toLowerCase().includes(name),
)
: undefined
const project = !wantsSkill
? projects.find(
(p) => p.name.toLowerCase() === name || p.name.toLowerCase().includes(name),
)
: undefined
// Explicit @skill:… with no match → nothing (avoid a dead action).
if (wantsSkill && !skill) return []
if (wantsProject && !project) return []
if (skill && (wantsSkill || !project)) {
const intent = text
const composed = intent ? `[skill: ${skill.name}] ${intent}` : `[skill: ${skill.name}]`
return [
{
id: `run-skill-${skill.name}`,
verb: 'run',
icon: <Zap className="h-4 w-4" />,
title: t('commandPalette.runSkill', { name: skill.name }),
subtitle: intent || t('commandPalette.composed'),
score: 100,
onSelect: () => startChat(composed),
},
]
}
if (project) {
const intent = text
// Undefined intent → set project context, navigate, let the user type.
if (!intent) {
return [
{
id: `run-project-${project.id}`,
verb: 'run',
icon: <FolderKanban className="h-4 w-4" />,
title: t('commandPalette.runProject', { name: project.name }),
subtitle: t('commandPalette.typeIntent'),
score: 100,
onSelect: () => {
setActiveProject(project.id)
setActiveMountIds(project.mount_ids ?? null)
router.history.push('/chat')
},
},
]
}
return [
{
id: `run-project-${project.id}`,
verb: 'run',
icon: <FolderKanban className="h-4 w-4" />,
title: t('commandPalette.runProject', { name: project.name }),
subtitle: intent,
score: 100,
onSelect: () => {
setActiveProject(project.id)
setActiveMountIds(project.mount_ids ?? null)
startChat(intent)
},
},
]
}
// Bare @name matched neither skill nor project.
return []
}
// No entity: run the text as a chat intent.
if (text) {
return [
{
id: 'run-intent',
verb: 'run',
icon: <MessageSquare className="h-4 w-4" />,
title: t('commandPalette.runIntent'),
subtitle: text,
hint: <kbd className="text-[10px] text-muted-foreground">⏎</kbd>,
score: 100,
onSelect: () => startChat(text),
},
]
}
// `>` with nothing yet → hint.
if (isRun) {
return [
{
id: 'run-hint',
verb: 'run',
icon: <MessageSquare className="h-4 w-4" />,
title: t('commandPalette.runHint'),
score: 0,
onSelect: () => {},
},
]
}
return []
},
}),
[t, skills, projects, startChat, setActiveProject, setActiveMountIds, router],
)
}