spool-memory 0.1.1

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
import * as React from 'react'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import type { DesktopLifecycleAction } from '@/lib/types/desktop'
import { Archive, CheckCircle2, Loader2, TrendingUp } from 'lucide-react'

type ActionMeta = {
  label: string
  title: (recordTitle?: string) => string
  description: string
  icon: typeof CheckCircle2
  variant: 'default' | 'outline' | 'destructive'
}

const ACTION_META: Record<DesktopLifecycleAction, ActionMeta> = {
  accept: {
    label: '采纳',
    title: (t) => (t ? `采纳 - ${t}` : '采纳记录'),
    description: '采纳后该记录变为 Accepted,将参与唤醒检索。可补充元数据用于审计与回溯。',
    icon: CheckCircle2,
    variant: 'default',
  },
  promote: {
    label: '晋升固化',
    title: (t) => (t ? `晋升固化 - ${t}` : '晋升固化'),
    description: '晋升后状态变为 Canonical,作为长期参考。请填写晋升理由便于追溯。',
    icon: TrendingUp,
    variant: 'outline',
  },
  archive: {
    label: '归档',
    title: (t) => (t ? `归档 - ${t}` : '归档记录'),
    description: '归档后该记录从队列中移除,但 ledger 历史保留。请说明归档原因。',
    icon: Archive,
    variant: 'destructive',
  },
}

export type ActionConfirmMetadata = {
  actor: string
  reason: string
  evidence_refs: string[]
}

type Props = {
  open: boolean
  action: DesktopLifecycleAction | null
  recordTitle?: string
  defaultActor?: string
  loading?: boolean
  countLabel?: string
  onConfirm: (metadata: ActionConfirmMetadata) => void
  onCancel: () => void
}

export function ActionConfirmDialog({
  open,
  action,
  recordTitle,
  defaultActor,
  loading,
  countLabel,
  onConfirm,
  onCancel,
}: Props) {
  const [actor, setActor] = React.useState('')
  const [reason, setReason] = React.useState('')
  const [evidenceRefs, setEvidenceRefs] = React.useState('')

  React.useEffect(() => {
    if (open) {
      setActor(defaultActor ?? '')
      setReason('')
      setEvidenceRefs('')
    }
  }, [open, defaultActor])

  if (!action) return null

  const meta = ACTION_META[action]
  const Icon = meta.icon

  function handleConfirm() {
    onConfirm({
      actor: actor.trim(),
      reason: reason.trim(),
      evidence_refs: evidenceRefs
        .split(/[\n,]/)
        .map((s) => s.trim())
        .filter(Boolean),
    })
  }

  return (
    <Dialog
      open={open}
      onOpenChange={(o) => {
        if (!o) onCancel()
      }}
    >
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            <Icon className="h-4 w-4" />
            {countLabel ? `${meta.label}${countLabel}` : meta.title(recordTitle)}
          </DialogTitle>
          <DialogDescription>{meta.description}</DialogDescription>
        </DialogHeader>

        <div className="space-y-3">
          <div className="grid gap-1">
            <Label className="text-xs" htmlFor="confirm-actor">
              actor
            </Label>
            <Input
              id="confirm-actor"
              value={actor}
              onChange={(e) => setActor(e.target.value)}
              placeholder="long"
              autoFocus
            />
          </div>
          <div className="grid gap-1">
            <Label className="text-xs" htmlFor="confirm-reason">
              reason
            </Label>
            <Input
              id="confirm-reason"
              value={reason}
              onChange={(e) => setReason(e.target.value)}
              placeholder={
                action === 'archive' ? 'duplicate / outdated / ...' : 'approved after review'
              }
            />
          </div>
          <div className="grid gap-1">
            <Label className="text-xs" htmlFor="confirm-evidence">
              evidence_refs
            </Label>
            <Textarea
              id="confirm-evidence"
              value={evidenceRefs}
              onChange={(e) => setEvidenceRefs(e.target.value)}
              rows={2}
              placeholder="session:1, obsidian://note"
            />
            <p className="text-[10px] text-muted-foreground">
              用逗号或换行分隔。可留空。
            </p>
          </div>
        </div>

        <DialogFooter>
          <Button variant="ghost" onClick={onCancel} disabled={loading}>
            取消
          </Button>
          <Button
            variant={meta.variant}
            onClick={handleConfirm}
            disabled={loading}
          >
            {loading ? (
              <Loader2 className="h-3.5 w-3.5 animate-spin" />
            ) : (
              <Icon className="h-3.5 w-3.5" />
            )}
            确认{meta.label}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}