spool-memory 0.1.1

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import type { MemoryScope } from '@/lib/types/desktop'
import type { DraftForm } from '@/lib/drafts'

const SCOPES: MemoryScope[] = ['user', 'project', 'workspace', 'team', 'agent']

export function DraftFormFields({
  value,
  onChange,
}: {
  value: DraftForm
  onChange: (next: DraftForm) => void
}) {
  function patch(fields: Partial<DraftForm>) {
    onChange({ ...value, ...fields })
  }

  return (
    <div className="space-y-4">
      <div className="grid gap-2">
        <Label>标题</Label>
        <Input
          value={value.title}
          onChange={(e) => patch({ title: e.target.value })}
          placeholder="简洁输出"
        />
      </div>
      <div className="grid gap-2">
        <Label>摘要</Label>
        <Textarea
          value={value.summary}
          onChange={(e) => patch({ summary: e.target.value })}
          placeholder="偏好简洁输出"
          rows={3}
        />
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="grid gap-2">
          <Label>类型</Label>
          <Input
            value={value.memory_type}
            onChange={(e) => patch({ memory_type: e.target.value })}
            placeholder="preference / workflow / decision ..."
          />
        </div>
        <div className="grid gap-2">
          <Label>作用域</Label>
          <Select
            value={value.scope}
            onValueChange={(v) => patch({ scope: v as MemoryScope })}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {SCOPES.map((s) => (
                <SelectItem key={s} value={s}>
                  {s}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      </div>
      <div className="grid gap-2">
        <Label>source_ref</Label>
        <Input
          value={value.source_ref}
          onChange={(e) => patch({ source_ref: e.target.value })}
          placeholder="manual:desktop / session:<id>"
        />
      </div>
      <div className="grid grid-cols-3 gap-3">
        <div className="grid gap-2">
          <Label>project_id</Label>
          <Input
            value={value.project_id}
            onChange={(e) => patch({ project_id: e.target.value })}
          />
        </div>
        <div className="grid gap-2">
          <Label>user_id</Label>
          <Input
            value={value.user_id}
            onChange={(e) => patch({ user_id: e.target.value })}
          />
        </div>
        <div className="grid gap-2">
          <Label>sensitivity</Label>
          <Input
            value={value.sensitivity}
            onChange={(e) => patch({ sensitivity: e.target.value })}
          />
        </div>
      </div>
      <Separator />
      <p className="text-xs text-muted-foreground">
        结构化检索信号(可选)— 逗号分隔,提升检索精度。
      </p>
      <div className="grid grid-cols-2 gap-3">
        <div className="grid gap-2">
          <Label>entities(实体)</Label>
          <Input
            value={value.entities}
            onChange={(e) => patch({ entities: e.target.value })}
            placeholder="LifecycleService, scorer"
          />
        </div>
        <div className="grid gap-2">
          <Label>tags(标签)</Label>
          <Input
            value={value.tags}
            onChange={(e) => patch({ tags: e.target.value })}
            placeholder="retrieval, lifecycle"
          />
        </div>
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="grid gap-2">
          <Label>triggers(触发词)</Label>
          <Input
            value={value.triggers}
            onChange={(e) => patch({ triggers: e.target.value })}
            placeholder="routing, scorer"
          />
        </div>
        <div className="grid gap-2">
          <Label>applies_to(适用项目)</Label>
          <Input
            value={value.applies_to}
            onChange={(e) => patch({ applies_to: e.target.value })}
            placeholder="spool, other-project"
          />
        </div>
      </div>
      <div className="grid gap-2">
        <Label>related_files(关联文件)</Label>
        <Textarea
          value={value.related_files}
          onChange={(e) => patch({ related_files: e.target.value })}
          placeholder="src/engine/scorer.rs, src/mcp.rs"
          rows={2}
        />
      </div>
      <div className="grid gap-2">
        <Label>related_records(关联记忆 ID)</Label>
        <Input
          value={value.related_records}
          onChange={(e) => patch({ related_records: e.target.value })}
          placeholder="rec-001, rec-002"
        />
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="grid gap-2">
          <Label>supersedes(替代记忆 ID)</Label>
          <Input
            value={value.supersedes}
            onChange={(e) => patch({ supersedes: e.target.value })}
            placeholder="rec-old-001"
          />
        </div>
        <div className="grid gap-2">
          <Label>valid_until(有效期)</Label>
          <Input
            value={value.valid_until}
            onChange={(e) => patch({ valid_until: e.target.value })}
            placeholder="2026-12-31"
          />
        </div>
      </div>
      <Separator />
      <p className="text-xs text-muted-foreground">
        Provenance 元数据(可选)— 记录谁在什么原因下写入,便于回放审计。
      </p>
      <div className="grid grid-cols-2 gap-3">
        <div className="grid gap-2">
          <Label>actor</Label>
          <Input
            value={value.actor}
            onChange={(e) => patch({ actor: e.target.value })}
            placeholder="long"
          />
        </div>
        <div className="grid gap-2">
          <Label>reason</Label>
          <Input
            value={value.reason}
            onChange={(e) => patch({ reason: e.target.value })}
            placeholder="approved after review"
          />
        </div>
      </div>
      <div className="grid gap-2">
        <Label>evidence_refs(逗号/换行分隔)</Label>
        <Textarea
          value={value.evidence_refs}
          onChange={(e) => patch({ evidence_refs: e.target.value })}
          placeholder="session:1, obsidian://note"
          rows={2}
        />
      </div>
    </div>
  )
}