@gmickel/gno 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +14 -7
  2. package/assets/skill/SKILL.md +54 -12
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +2 -1
  6. package/spec/AGENTS.md +83 -0
  7. package/spec/CLAUDE.md +83 -0
  8. package/spec/bench-fixture.schema.json +137 -0
  9. package/spec/cli.md +2919 -0
  10. package/spec/db/schema.sql +442 -0
  11. package/spec/evals-agentic.md +592 -0
  12. package/spec/evals.md +1106 -0
  13. package/spec/mcp.md +2279 -0
  14. package/spec/output-schemas/activation-verification.schema.json +515 -0
  15. package/spec/output-schemas/ask.schema.json +564 -0
  16. package/spec/output-schemas/backlinks.schema.json +131 -0
  17. package/spec/output-schemas/bench-result.schema.json +120 -0
  18. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  19. package/spec/output-schemas/claim-verification.schema.json +291 -0
  20. package/spec/output-schemas/collection-list.schema.json +45 -0
  21. package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
  22. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  23. package/spec/output-schemas/context-list.schema.json +21 -0
  24. package/spec/output-schemas/doctor.schema.json +313 -0
  25. package/spec/output-schemas/error.schema.json +30 -0
  26. package/spec/output-schemas/expansion.schema.json +37 -0
  27. package/spec/output-schemas/get.schema.json +140 -0
  28. package/spec/output-schemas/graph-query.schema.json +99 -0
  29. package/spec/output-schemas/graph.schema.json +371 -0
  30. package/spec/output-schemas/links-list.schema.json +186 -0
  31. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  32. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  33. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  34. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  35. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  36. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  37. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  38. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  39. package/spec/output-schemas/models-list.schema.json +93 -0
  40. package/spec/output-schemas/multi-get.schema.json +103 -0
  41. package/spec/output-schemas/process-status.schema.json +119 -0
  42. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  43. package/spec/output-schemas/resident-status.schema.json +154 -0
  44. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  45. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  46. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  47. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  48. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  49. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  50. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  51. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  52. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  53. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  54. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  55. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  56. package/spec/output-schemas/search-result.schema.json +154 -0
  57. package/spec/output-schemas/search-results.schema.json +338 -0
  58. package/spec/output-schemas/similar.schema.json +84 -0
  59. package/spec/output-schemas/status.schema.json +676 -0
  60. package/spec/output-schemas/tags-list.schema.json +48 -0
  61. package/src/app/context-runtime-contract.ts +10 -5
  62. package/src/app/context-runtime-input.ts +29 -1
  63. package/src/app/context-runtime-types.ts +7 -0
  64. package/src/app/context-runtime.ts +20 -2
  65. package/src/app/context-surface.ts +4 -0
  66. package/src/app/verified-ask.ts +291 -0
  67. package/src/cli/commands/ask-format.ts +255 -0
  68. package/src/cli/commands/ask.ts +144 -183
  69. package/src/cli/commands/context-build.ts +56 -9
  70. package/src/cli/commands/get.ts +64 -3
  71. package/src/cli/commands/query.ts +62 -23
  72. package/src/cli/commands/replay.ts +140 -0
  73. package/src/cli/commands/search.ts +48 -3
  74. package/src/cli/commands/shared.ts +3 -1
  75. package/src/cli/commands/trace.ts +200 -0
  76. package/src/cli/commands/vsearch.ts +75 -53
  77. package/src/cli/program.ts +287 -1
  78. package/src/config/index.ts +9 -0
  79. package/src/config/retrieval-traces.ts +56 -0
  80. package/src/config/types.ts +4 -0
  81. package/src/core/context-budget.ts +6 -0
  82. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  83. package/src/core/context-capsule-schema.ts +17 -0
  84. package/src/core/context-capsule-validation.ts +3 -2
  85. package/src/core/context-capsule.ts +18 -0
  86. package/src/core/context-compiler.ts +44 -25
  87. package/src/core/context-evidence.ts +6 -0
  88. package/src/core/retrieval-qrels.ts +405 -0
  89. package/src/core/retrieval-replay-candidate.ts +368 -0
  90. package/src/core/retrieval-replay-types.ts +109 -0
  91. package/src/core/retrieval-replay-validation.ts +89 -0
  92. package/src/core/retrieval-replay.ts +441 -0
  93. package/src/core/retrieval-trace-evidence-origin.ts +178 -0
  94. package/src/core/retrieval-trace-export.ts +113 -0
  95. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  96. package/src/core/retrieval-trace-filters.ts +19 -0
  97. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  98. package/src/core/retrieval-trace-management-types.ts +132 -0
  99. package/src/core/retrieval-trace-management.ts +422 -0
  100. package/src/core/retrieval-trace-request.ts +141 -0
  101. package/src/core/retrieval-trace-session.ts +507 -0
  102. package/src/core/retrieval-trace.ts +472 -0
  103. package/src/llm/errors.ts +10 -1
  104. package/src/llm/httpGeneration.ts +11 -1
  105. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  106. package/src/llm/types.ts +6 -0
  107. package/src/mcp/tools/ask.ts +228 -0
  108. package/src/mcp/tools/context.ts +87 -15
  109. package/src/mcp/tools/get.ts +35 -1
  110. package/src/mcp/tools/index.ts +83 -0
  111. package/src/mcp/tools/query.ts +95 -64
  112. package/src/mcp/tools/search.ts +36 -13
  113. package/src/mcp/tools/trace.ts +143 -0
  114. package/src/mcp/tools/vsearch.ts +71 -38
  115. package/src/pipeline/answer.ts +167 -26
  116. package/src/pipeline/claim-verification-schema.ts +235 -0
  117. package/src/pipeline/claim-verification.ts +487 -0
  118. package/src/pipeline/claim-verifier.ts +474 -0
  119. package/src/pipeline/graph-retrieval.ts +15 -1
  120. package/src/pipeline/hybrid.ts +151 -43
  121. package/src/pipeline/search.ts +36 -3
  122. package/src/pipeline/trace-metadata.ts +47 -0
  123. package/src/pipeline/types.ts +68 -0
  124. package/src/pipeline/vsearch.ts +101 -38
  125. package/src/sdk/client.ts +415 -73
  126. package/src/sdk/documents.ts +48 -1
  127. package/src/sdk/index.ts +17 -0
  128. package/src/sdk/types.ts +28 -0
  129. package/src/serve/context-capsule.ts +67 -8
  130. package/src/serve/public/app.tsx +12 -1
  131. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  132. package/src/serve/public/globals.built.css +1 -1
  133. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  134. package/src/serve/public/pages/Ask.tsx +42 -4
  135. package/src/serve/public/pages/Dashboard.tsx +10 -0
  136. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  137. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  138. package/src/serve/retrieval-trace.ts +28 -0
  139. package/src/serve/routes/api.ts +508 -68
  140. package/src/serve/routes/traces.ts +156 -0
  141. package/src/serve/server.ts +87 -2
  142. package/src/store/index.ts +31 -0
  143. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  144. package/src/store/migrations/index.ts +2 -0
  145. package/src/store/retrieval-trace-codec.ts +384 -0
  146. package/src/store/sqlite/adapter.ts +153 -1
  147. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  148. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  149. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  150. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  151. package/src/store/types.ts +297 -0
  152. package/src/store/vector/sqlite-vec.ts +76 -1
  153. package/src/store/vector/types.ts +1 -1
@@ -65,6 +65,8 @@ function getLocationLabel(location: string): string {
65
65
  return "Connectors";
66
66
  case "/graph":
67
67
  return "Graph";
68
+ case "/traces":
69
+ return "Trace history";
68
70
  case "/doc":
69
71
  case "/edit": {
70
72
  const uri = params.get("uri") ?? "";
@@ -21,6 +21,10 @@ import {
21
21
  SourcesTrigger,
22
22
  } from "../components/ai-elements/sources";
23
23
  import { AIModelSelector } from "../components/AIModelSelector";
24
+ import {
25
+ type AskVerification,
26
+ AskVerificationPanel,
27
+ } from "../components/AskVerificationPanel";
24
28
  import {
25
29
  ThoroughnessSelector,
26
30
  type Thoroughness,
@@ -57,6 +61,7 @@ interface PageProps {
57
61
  }
58
62
 
59
63
  interface Citation {
64
+ evidenceId?: string;
60
65
  docid: string;
61
66
  uri: string;
62
67
  startLine?: number;
@@ -88,12 +93,15 @@ interface AskResponse {
88
93
  vectorsUsed: boolean;
89
94
  answerGenerated: boolean;
90
95
  totalResults: number;
96
+ verificationRequested?: boolean;
97
+ abstained?: boolean;
91
98
  queryModes?: {
92
99
  term: number;
93
100
  intent: number;
94
101
  hyde: boolean;
95
102
  };
96
103
  };
104
+ verification?: AskVerification;
97
105
  }
98
106
 
99
107
  interface Capabilities {
@@ -139,7 +147,7 @@ function renderAnswer(
139
147
  const parts: React.ReactNode[] = [];
140
148
  let key = 0;
141
149
 
142
- const citationRegex = /\[(\d+)\]/g;
150
+ const citationRegex = /\[(?:evidence:([a-f0-9]{64})|(\d+))\]/g;
143
151
  let lastIndex = 0;
144
152
  let match: RegExpExecArray | null;
145
153
 
@@ -149,8 +157,13 @@ function renderAnswer(
149
157
  parts.push(answer.slice(lastIndex, match.index));
150
158
  }
151
159
 
152
- const citationNum = Number(match[1]);
153
- const citation = citations[citationNum - 1];
160
+ const evidenceId = match[1];
161
+ const numericCitation = match[2] ? Number(match[2]) : undefined;
162
+ const citationIndex = evidenceId
163
+ ? citations.findIndex((citation) => citation.evidenceId === evidenceId)
164
+ : (numericCitation ?? 0) - 1;
165
+ const citationNum = citationIndex + 1;
166
+ const citation = citations[citationIndex];
154
167
 
155
168
  if (citation) {
156
169
  parts.push(
@@ -186,6 +199,7 @@ export default function Ask({ navigate }: PageProps) {
186
199
  const [collections, setCollections] = useState<Collection[]>([]);
187
200
  const [thoroughness, setThoroughness] = useState<Thoroughness>("balanced");
188
201
  const [activePreset, setActivePreset] = useState("slim-tuned");
202
+ const [verify, setVerify] = useState(false);
189
203
 
190
204
  const [showAdvanced, setShowAdvanced] = useState(false);
191
205
  const [selectedCollection, setSelectedCollection] = useState("");
@@ -318,6 +332,7 @@ export default function Ask({ navigate }: PageProps) {
318
332
  const requestBody: Record<string, unknown> = {
319
333
  query: currentQuery,
320
334
  limit: 5,
335
+ verify,
321
336
  };
322
337
 
323
338
  if (selectedCollection) {
@@ -394,6 +409,7 @@ export default function Ask({ navigate }: PageProps) {
394
409
  tagsInput,
395
410
  thoroughness,
396
411
  until,
412
+ verify,
397
413
  ]
398
414
  );
399
415
 
@@ -443,6 +459,7 @@ export default function Ask({ navigate }: PageProps) {
443
459
  parseTagsCsv(tagsInput).length > 0
444
460
  ? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
445
461
  : null,
462
+ verify ? "verified" : null,
446
463
  ].filter((pill): pill is string => Boolean(pill));
447
464
 
448
465
  return (
@@ -475,6 +492,16 @@ export default function Ask({ navigate }: PageProps) {
475
492
  value={thoroughness}
476
493
  />
477
494
 
495
+ <Button
496
+ aria-pressed={verify}
497
+ onClick={() => setVerify((value) => !value)}
498
+ size="sm"
499
+ type="button"
500
+ variant={verify ? "default" : "outline"}
501
+ >
502
+ Verify
503
+ </Button>
504
+
478
505
  <div className="h-6 w-px bg-border/40" />
479
506
 
480
507
  <AIModelSelector onPresetChange={setActivePreset} />
@@ -878,8 +905,19 @@ export default function Ask({ navigate }: PageProps) {
878
905
  </Sources>
879
906
  )}
880
907
 
908
+ {entry.response.verification && (
909
+ <AskVerificationPanel
910
+ navigate={navigate}
911
+ verification={entry.response.verification}
912
+ />
913
+ )}
914
+
881
915
  <div className="flex items-center gap-2 text-muted-foreground/60 text-xs">
882
- <span>{entry.response.results.length} results</span>
916
+ <span>
917
+ {entry.response.verification
918
+ ? `${entry.response.verification.capsule.evidence.length} evidence spans`
919
+ : `${entry.response.results.length} results`}
920
+ </span>
883
921
  {entry.response.meta.vectorsUsed && (
884
922
  <Badge
885
923
  className="font-mono text-[9px]"
@@ -7,6 +7,7 @@ import {
7
7
  FolderHeartIcon,
8
8
  FolderIcon,
9
9
  GitForkIcon,
10
+ HistoryIcon,
10
11
  Layers,
11
12
  Loader2Icon,
12
13
  MessageSquare,
@@ -499,6 +500,15 @@ export default function Dashboard({ navigate }: PageProps) {
499
500
  <GitForkIcon className="size-4" />
500
501
  Graph
501
502
  </Button>
503
+ <Button
504
+ className="gap-2"
505
+ onClick={() => navigate("/traces")}
506
+ size="lg"
507
+ variant="outline"
508
+ >
509
+ <HistoryIcon className="size-4" />
510
+ Trace history
511
+ </Button>
502
512
  </nav>
503
513
 
504
514
  {error && (
@@ -0,0 +1,478 @@
1
+ import {
2
+ ArrowLeftIcon,
3
+ DownloadIcon,
4
+ Loader2Icon,
5
+ RefreshCwIcon,
6
+ ShieldCheckIcon,
7
+ Trash2Icon,
8
+ } from "lucide-react";
9
+ import { useCallback, useEffect, useMemo, useState } from "react";
10
+
11
+ import type {
12
+ RetrievalTraceDetail,
13
+ RetrievalTraceExportResult,
14
+ RetrievalTraceLabelResult,
15
+ RetrievalTraceListResult,
16
+ RetrievalTracePurgeResult,
17
+ } from "../../../core/retrieval-trace-management";
18
+
19
+ import { Badge } from "../components/ui/badge";
20
+ import { Button } from "../components/ui/button";
21
+ import {
22
+ Card,
23
+ CardContent,
24
+ CardHeader,
25
+ CardTitle,
26
+ } from "../components/ui/card";
27
+ import {
28
+ Dialog,
29
+ DialogContent,
30
+ DialogDescription,
31
+ DialogFooter,
32
+ DialogHeader,
33
+ DialogTitle,
34
+ } from "../components/ui/dialog";
35
+ import {
36
+ Table,
37
+ TableBody,
38
+ TableCell,
39
+ TableHead,
40
+ TableHeader,
41
+ TableRow,
42
+ } from "../components/ui/table";
43
+ import { apiFetch } from "../hooks/use-api";
44
+ import {
45
+ TraceEvidenceList,
46
+ type TraceEvidenceSelection,
47
+ TraceLabelForm,
48
+ TracePurgeNotice,
49
+ } from "./trace-history-detail";
50
+
51
+ interface PageProps {
52
+ navigate: (to: string | number) => void;
53
+ }
54
+
55
+ type ConfirmAction =
56
+ | { kind: "delete"; traceId: string }
57
+ | { kind: "purge" }
58
+ | null;
59
+
60
+ const statusVariant = (
61
+ status: string
62
+ ): "default" | "destructive" | "outline" | "secondary" => {
63
+ if (status === "completed") return "default";
64
+ if (status === "failed" || status === "cancelled") return "destructive";
65
+ if (status === "partial") return "secondary";
66
+ return "outline";
67
+ };
68
+
69
+ const downloadJson = (value: unknown, filename: string): void => {
70
+ const blob = new Blob([`${JSON.stringify(value, null, 2)}\n`], {
71
+ type: "application/json",
72
+ });
73
+ const url = URL.createObjectURL(blob);
74
+ const anchor = document.createElement("a");
75
+ anchor.href = url;
76
+ anchor.download = filename;
77
+ anchor.click();
78
+ URL.revokeObjectURL(url);
79
+ };
80
+
81
+ export default function TraceHistory({ navigate }: PageProps) {
82
+ const [history, setHistory] = useState<RetrievalTraceListResult | null>(null);
83
+ const [detail, setDetail] = useState<RetrievalTraceDetail | null>(null);
84
+ const [selected, setSelected] = useState<Set<string>>(new Set());
85
+ const [busy, setBusy] = useState<string | null>("list");
86
+ const [error, setError] = useState<string | null>(null);
87
+ const [confirmAction, setConfirmAction] = useState<ConfirmAction>(null);
88
+ const [label, setLabel] = useState<
89
+ "relevant" | "irrelevant" | "missing_expected"
90
+ >("relevant");
91
+ const [targetRef, setTargetRef] = useState("");
92
+ const [sourceHash, setSourceHash] = useState("");
93
+ const [selectedEvidence, setSelectedEvidence] =
94
+ useState<TraceEvidenceSelection | null>(null);
95
+ const [purgeReceipt, setPurgeReceipt] =
96
+ useState<RetrievalTracePurgeResult | null>(null);
97
+
98
+ const loadHistory = useCallback(async (): Promise<void> => {
99
+ setBusy("list");
100
+ setError(null);
101
+ const result = await apiFetch<RetrievalTraceListResult>(
102
+ "/api/traces?limit=100"
103
+ );
104
+ if (result.error) {
105
+ setError(result.error);
106
+ } else {
107
+ setHistory(result.data);
108
+ const ids = new Set(result.data?.traces.map((trace) => trace.traceId));
109
+ setSelected(
110
+ (current) => new Set([...current].filter((traceId) => ids.has(traceId)))
111
+ );
112
+ }
113
+ setBusy(null);
114
+ }, []);
115
+
116
+ useEffect(() => {
117
+ void loadHistory();
118
+ }, [loadHistory]);
119
+
120
+ const inspect = useCallback(async (traceId: string): Promise<void> => {
121
+ setBusy(`show:${traceId}`);
122
+ setError(null);
123
+ const result = await apiFetch<RetrievalTraceDetail>(
124
+ `/api/traces/${encodeURIComponent(traceId)}?detailLimit=500`
125
+ );
126
+ setSelectedEvidence(null);
127
+ setTargetRef("");
128
+ setSourceHash("");
129
+ setDetail(result.data);
130
+ setError(result.error);
131
+ setBusy(null);
132
+ }, []);
133
+
134
+ const submitLabel = async (): Promise<void> => {
135
+ if (!detail || !targetRef.trim()) return;
136
+ setBusy("label");
137
+ setError(null);
138
+ const result = await apiFetch<RetrievalTraceLabelResult>(
139
+ `/api/traces/${encodeURIComponent(detail.trace.traceId)}/judgments`,
140
+ {
141
+ method: "POST",
142
+ body: JSON.stringify({
143
+ label,
144
+ targetRef: targetRef.trim(),
145
+ ...(label !== "missing_expected" &&
146
+ selectedEvidence?.ref === targetRef.trim()
147
+ ? {
148
+ targetKind: selectedEvidence.targetKind,
149
+ startLine: selectedEvidence.startLine,
150
+ endLine: selectedEvidence.endLine,
151
+ sourceHash: selectedEvidence.sourceHash,
152
+ docid: selectedEvidence.docid,
153
+ }
154
+ : {}),
155
+ ...(label === "missing_expected" && sourceHash.trim()
156
+ ? { sourceHash: sourceHash.trim() }
157
+ : {}),
158
+ }),
159
+ }
160
+ );
161
+ if (result.error) {
162
+ setError(result.error);
163
+ } else {
164
+ setTargetRef("");
165
+ setSourceHash("");
166
+ setSelectedEvidence(null);
167
+ await inspect(detail.trace.traceId);
168
+ }
169
+ setBusy(null);
170
+ };
171
+
172
+ const exportSelected = async (): Promise<void> => {
173
+ if (selected.size === 0) return;
174
+ setBusy("export");
175
+ setError(null);
176
+ const result = await apiFetch<RetrievalTraceExportResult>(
177
+ "/api/traces/export",
178
+ {
179
+ method: "POST",
180
+ body: JSON.stringify({
181
+ traceIds: [...selected],
182
+ format: "agentic-receipt",
183
+ }),
184
+ }
185
+ );
186
+ if (result.data) {
187
+ downloadJson(
188
+ result.data.artifact,
189
+ `gno-traces-${result.data.manifest.artifactHash.slice(0, 12)}.json`
190
+ );
191
+ }
192
+ setError(result.error);
193
+ setBusy(null);
194
+ };
195
+
196
+ const runConfirmedAction = async (): Promise<void> => {
197
+ if (!confirmAction) return;
198
+ const action = confirmAction;
199
+ setConfirmAction(null);
200
+ setBusy(action.kind);
201
+ setError(null);
202
+ const endpoint =
203
+ action.kind === "purge"
204
+ ? "/api/traces"
205
+ : `/api/traces/${encodeURIComponent(action.traceId)}`;
206
+ const result = await apiFetch<RetrievalTracePurgeResult>(endpoint, {
207
+ method: "DELETE",
208
+ });
209
+ if (result.error) {
210
+ setError(result.error);
211
+ } else {
212
+ if (action.kind === "purge") setPurgeReceipt(result.data);
213
+ setDetail(null);
214
+ setSelected(new Set());
215
+ await loadHistory();
216
+ }
217
+ setBusy(null);
218
+ };
219
+
220
+ const terminalSelected = useMemo(() => {
221
+ const byId = new Map(
222
+ history?.traces.map((trace) => [trace.traceId, trace]) ?? []
223
+ );
224
+ return [...selected].every(
225
+ (traceId) => byId.get(traceId)?.status !== "open"
226
+ );
227
+ }, [history, selected]);
228
+
229
+ return (
230
+ <div className="min-h-screen bg-background">
231
+ <header className="border-b border-border/50 bg-card/50 backdrop-blur-sm">
232
+ <div className="mx-auto flex max-w-7xl items-center justify-between p-6">
233
+ <div className="flex items-center gap-4">
234
+ <Button
235
+ aria-label="Back"
236
+ onClick={() => navigate(-1)}
237
+ size="icon"
238
+ variant="ghost"
239
+ >
240
+ <ArrowLeftIcon className="size-4" />
241
+ </Button>
242
+ <div>
243
+ <h1 className="font-display text-2xl font-semibold">
244
+ Trace history
245
+ </h1>
246
+ <p className="text-sm text-muted-foreground">
247
+ Private receipts. Local evidence. Explicit feedback only.
248
+ </p>
249
+ </div>
250
+ </div>
251
+ <div className="flex gap-2">
252
+ <Button
253
+ disabled={busy !== null}
254
+ onClick={() => void loadHistory()}
255
+ variant="outline"
256
+ >
257
+ <RefreshCwIcon className="mr-2 size-4" />
258
+ Refresh
259
+ </Button>
260
+ <Button
261
+ disabled={
262
+ busy !== null || selected.size === 0 || !terminalSelected
263
+ }
264
+ onClick={() => void exportSelected()}
265
+ >
266
+ <DownloadIcon className="mr-2 size-4" />
267
+ Export {selected.size || ""}
268
+ </Button>
269
+ <Button
270
+ disabled={busy !== null || !history?.traces.length}
271
+ onClick={() => setConfirmAction({ kind: "purge" })}
272
+ variant="destructive"
273
+ >
274
+ <Trash2Icon className="mr-2 size-4" />
275
+ Purge all
276
+ </Button>
277
+ </div>
278
+ </div>
279
+ </header>
280
+
281
+ <main className="mx-auto grid max-w-7xl gap-6 p-6 lg:grid-cols-[1.35fr_1fr]">
282
+ <div className="lg:col-span-2">
283
+ <TracePurgeNotice receipt={purgeReceipt} />
284
+ </div>
285
+ <Card>
286
+ <CardHeader>
287
+ <CardTitle className="flex items-center gap-2">
288
+ <ShieldCheckIcon className="size-5 text-primary" />
289
+ Local receipts
290
+ </CardTitle>
291
+ </CardHeader>
292
+ <CardContent>
293
+ {error && (
294
+ <div className="mb-4 rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
295
+ {error}
296
+ </div>
297
+ )}
298
+ {busy === "list" && !history ? (
299
+ <div className="flex items-center justify-center py-16 text-muted-foreground">
300
+ <Loader2Icon className="mr-2 size-5 animate-spin" />
301
+ Loading traces
302
+ </div>
303
+ ) : (
304
+ <Table>
305
+ <TableHeader>
306
+ <TableRow>
307
+ <TableHead className="w-10">Use</TableHead>
308
+ <TableHead>Trace</TableHead>
309
+ <TableHead>Status</TableHead>
310
+ <TableHead>Mode</TableHead>
311
+ <TableHead>Created</TableHead>
312
+ </TableRow>
313
+ </TableHeader>
314
+ <TableBody>
315
+ {history?.traces.map((trace) => (
316
+ <TableRow
317
+ className="cursor-pointer"
318
+ key={trace.traceId}
319
+ onClick={() => void inspect(trace.traceId)}
320
+ >
321
+ <TableCell>
322
+ <input
323
+ aria-label={`Select ${trace.traceId}`}
324
+ checked={selected.has(trace.traceId)}
325
+ onChange={(event) => {
326
+ event.stopPropagation();
327
+ setSelected((current) => {
328
+ const next = new Set(current);
329
+ if (next.has(trace.traceId)) {
330
+ next.delete(trace.traceId);
331
+ } else {
332
+ next.add(trace.traceId);
333
+ }
334
+ return next;
335
+ });
336
+ }}
337
+ onClick={(event) => event.stopPropagation()}
338
+ type="checkbox"
339
+ />
340
+ </TableCell>
341
+ <TableCell className="max-w-48 truncate font-mono text-xs">
342
+ {trace.traceId}
343
+ </TableCell>
344
+ <TableCell>
345
+ <Badge variant={statusVariant(trace.status)}>
346
+ {trace.status}
347
+ </Badge>
348
+ </TableCell>
349
+ <TableCell>{trace.redactionMode}</TableCell>
350
+ <TableCell className="text-xs text-muted-foreground">
351
+ {new Date(trace.createdAtMs).toLocaleString()}
352
+ </TableCell>
353
+ </TableRow>
354
+ ))}
355
+ </TableBody>
356
+ </Table>
357
+ )}
358
+ </CardContent>
359
+ </Card>
360
+
361
+ <Card>
362
+ <CardHeader>
363
+ <CardTitle>Receipt detail</CardTitle>
364
+ </CardHeader>
365
+ <CardContent>
366
+ {!detail ? (
367
+ <p className="py-12 text-center text-sm text-muted-foreground">
368
+ Select a trace to inspect bounded evidence and add explicit
369
+ feedback.
370
+ </p>
371
+ ) : (
372
+ <div className="space-y-5">
373
+ <div className="space-y-1 text-sm">
374
+ <div className="break-all font-mono text-xs">
375
+ {detail.trace.traceId}
376
+ </div>
377
+ <div className="flex gap-2">
378
+ <Badge variant={statusVariant(detail.trace.status)}>
379
+ {detail.trace.status}
380
+ </Badge>
381
+ <Badge variant="outline">
382
+ {detail.trace.redactionMode}
383
+ </Badge>
384
+ </div>
385
+ </div>
386
+
387
+ <div className="grid grid-cols-4 gap-2 text-center text-xs">
388
+ {(
389
+ [
390
+ ["Runs", detail.runs.length],
391
+ ["Events", detail.events.length],
392
+ ["Labels", detail.judgments.length],
393
+ ["Exports", detail.exports.length],
394
+ ] as const
395
+ ).map(([name, count]) => (
396
+ <div className="rounded-md bg-muted p-2" key={name}>
397
+ <div className="font-semibold">{count}</div>
398
+ <div className="text-muted-foreground">{name}</div>
399
+ </div>
400
+ ))}
401
+ </div>
402
+
403
+ <TraceEvidenceList
404
+ detail={detail}
405
+ onSelect={(selection) => {
406
+ setSelectedEvidence(selection);
407
+ setTargetRef(selection.ref);
408
+ setSourceHash(selection.sourceHash ?? "");
409
+ }}
410
+ />
411
+
412
+ <TraceLabelForm
413
+ busy={busy === "label"}
414
+ label={label}
415
+ onLabelChange={setLabel}
416
+ onSourceHashChange={setSourceHash}
417
+ onSubmit={() => void submitLabel()}
418
+ onTargetRefChange={(value) => {
419
+ setTargetRef(value);
420
+ setSelectedEvidence(null);
421
+ }}
422
+ sourceHash={sourceHash}
423
+ targetRef={targetRef}
424
+ />
425
+
426
+ <Button
427
+ className="w-full"
428
+ onClick={() =>
429
+ setConfirmAction({
430
+ kind: "delete",
431
+ traceId: detail.trace.traceId,
432
+ })
433
+ }
434
+ variant="destructive"
435
+ >
436
+ <Trash2Icon className="mr-2 size-4" />
437
+ Delete this trace
438
+ </Button>
439
+ </div>
440
+ )}
441
+ </CardContent>
442
+ </Card>
443
+ </main>
444
+
445
+ <Dialog
446
+ onOpenChange={(open) => {
447
+ if (!open) setConfirmAction(null);
448
+ }}
449
+ open={confirmAction !== null}
450
+ >
451
+ <DialogContent>
452
+ <DialogHeader>
453
+ <DialogTitle>
454
+ {confirmAction?.kind === "purge"
455
+ ? "Purge every retrieval trace?"
456
+ : "Delete this retrieval trace?"}
457
+ </DialogTitle>
458
+ <DialogDescription>
459
+ This removes local receipt data and cannot be undone. Source
460
+ documents are not changed.
461
+ </DialogDescription>
462
+ </DialogHeader>
463
+ <DialogFooter>
464
+ <Button onClick={() => setConfirmAction(null)} variant="outline">
465
+ Cancel
466
+ </Button>
467
+ <Button
468
+ onClick={() => void runConfirmedAction()}
469
+ variant="destructive"
470
+ >
471
+ Delete local receipts
472
+ </Button>
473
+ </DialogFooter>
474
+ </DialogContent>
475
+ </Dialog>
476
+ </div>
477
+ );
478
+ }