oxios 1.23.1

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { BookOpen, Clock, Inbox, Newspaper, Plus, ShoppingCart, Tv } from 'lucide-react'
import type { ReactNode } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useChatAppend, useChecklistAdd, useJournalAdd } from '@/hooks/use-knowledge'
import { matchScore, modePrimaryVerb } from './ranker'
import type { CommandProvider, PaletteItem, QueryContext } from './types'

/** A capture destination (checklist file or journal). */
interface CaptureRoute {
  key: string
  labelKey: string
  icon: ReactNode
  /** Checklist file path; absent for Journal (uses journal_add). */
  path?: string
}

/**
 * v1 capture destinations — ported verbatim from the legacy `CAPTURE_ROUTES`
 * for behavior parity. Data-driven detection of checklist files (design §6,
 * mechanism (a): sniff `- [ ]` headers) is a contained follow-up enhancement;
 * the structure here is the single place to swap in a tree-derived list.
 */
const ROUTES: CaptureRoute[] = [
  {
    key: 'Later',
    labelKey: 'knowledge.later',
    icon: <Clock className="h-4 w-4 text-info" />,
    path: 'Later.md',
  },
  {
    key: 'Read',
    labelKey: 'knowledge.read',
    icon: <Newspaper className="h-4 w-4 text-warning" />,
    path: 'Read.md',
  },
  {
    key: 'Shop',
    labelKey: 'knowledge.shop',
    icon: <ShoppingCart className="h-4 w-4 text-destructive" />,
    path: 'Shop.md',
  },
  {
    key: 'Watch',
    labelKey: 'knowledge.watch',
    icon: <Tv className="h-4 w-4 text-chart-4" />,
    path: 'Watch.md',
  },
  {
    key: 'Journal',
    labelKey: 'knowledge.toJournal',
    icon: <BookOpen className="h-4 w-4 text-success" />,
  },
]

/** kbd hint for a route shortcut, e.g. `/later`. */
function routeKbd(key: string) {
  return <kbd className="text-[10px] text-muted-foreground">/{key.toLowerCase()}</kbd>
}

/**
 * Capture provider — verb `capture`.
 *
 * Owns the `/route text` slash convention (a capture-specific parse of the
 * first word after `/`) and the knowledge-mode bare-text memo default. The
 * generic lexer only identifies the `/` verb; route-keyword matching lives here.
 */
export function useCaptureProvider(): CommandProvider {
  const { t } = useTranslation()
  const chatAppend = useChatAppend()
  const journalAdd = useJournalAdd()
  const checklistAdd = useChecklistAdd()

  const runCapture = useMemo(
    () => async (text: string, routeKey: string | null) => {
      const body = text.trim()
      if (!body) return
      try {
        if (routeKey === 'Journal') {
          await journalAdd.mutateAsync(body)
          toast.success(t('commandPalette.capturedJournal'))
        } else if (routeKey) {
          const target = ROUTES.find((r) => r.key === routeKey)
          if (target?.path) {
            await checklistAdd.mutateAsync({ path: target.path, item: body })
            toast.success(t('commandPalette.capturedRoute', { route: t(target.labelKey) }))
          }
        } else {
          await chatAppend.mutateAsync(body)
          toast.success(t('commandPalette.capturedInbox'))
        }
      } catch {
        toast.error(t('commandPalette.captureFailed'))
      }
    },
    [t, chatAppend, journalAdd, checklistAdd],
  )

  return useMemo(
    () => ({
      id: 'capture',
      verbs: ['capture'],
      resolve(ctx: QueryContext): PaletteItem[] {
        // `/` explicit capture verb: slash-route convention.
        if (ctx.verb === 'capture') {
          const m = ctx.text.match(/^(\w*)\s*(.*)$/s)
          const tok = (m?.[1] ?? '').toLowerCase()
          const remainder = m?.[2] ?? ''
          const route = ROUTES.find((r) => r.key.toLowerCase() === tok)
          if (route) {
            const tail = remainder.trim()
            return [
              {
                id: `capture-${route.key}`,
                verb: 'capture',
                icon: route.icon,
                title: t('commandPalette.captureTo', { route: t(route.labelKey) }),
                subtitle: tail || t('commandPalette.typeText'),
                score: 100,
                onSelect: () => runCapture(remainder, route.key),
              },
            ]
          }
          // No/unknown route → destination picker (compose sets the query).
          return ROUTES.map(
            (r): PaletteItem => ({
              id: `route-${r.key}`,
              verb: 'capture',
              icon: r.icon,
              title: t(r.labelKey),
              hint: routeKbd(r.key),
              compose: `/${r.key} `,
              score: 0,
              onSelect: () => {},
            }),
          )
        }

        // Bare text → mode-primary. CaptureProvider owns knowledge-mode memos.
        const trimmed = ctx.text.trim()
        if (ctx.verb === null && modePrimaryVerb(ctx.mode) === 'capture' && trimmed) {
          return [
            {
              id: 'capture-inbox',
              verb: 'capture',
              icon: <Plus className="h-4 w-4" />,
              title: t('commandPalette.captureMemo'),
              subtitle: trimmed,
              hint: <kbd className="text-[10px] text-muted-foreground">⏎</kbd>,
              score: matchScore(ctx, { label: trimmed }),
              onSelect: () => runCapture(trimmed, null),
            },
          ]
        }

        // Empty-state: capture route shortcuts + inbox hint.
        if (ctx.verb === null && !ctx.raw.trim()) {
          return [
            ...ROUTES.map(
              (r): PaletteItem => ({
                id: `route-${r.key}`,
                verb: 'capture',
                icon: r.icon,
                title: t('commandPalette.captureTo', { route: t(r.labelKey) }),
                hint: routeKbd(r.key),
                compose: `/${r.key} `,
                score: 0,
                onSelect: () => {},
              }),
            ),
            {
              id: 'capture-inbox-hint',
              verb: 'capture',
              icon: <Inbox className="h-4 w-4 text-muted-foreground" />,
              title: t('commandPalette.inboxHint'),
              score: 0,
              onSelect: () => {},
            },
          ]
        }

        return []
      },
    }),
    [t, runCapture],
  )
}