@gmickel/gno 2.4.0 → 2.6.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.
- package/README.md +69 -6
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +98 -1
- package/assets/skill/cli-reference.md +140 -0
- package/assets/skill/examples.md +45 -0
- package/assets/skill/mcp-reference.md +62 -0
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +358 -24
- package/spec/compiled-context.md +68 -0
- package/spec/mcp.md +223 -2
- package/spec/output-schemas/capture-receipt.schema.json +3 -0
- package/spec/output-schemas/compiled-context-check.schema.json +44 -0
- package/spec/output-schemas/compiled-context-file.schema.json +165 -0
- package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
- package/spec/output-schemas/memory-remember.schema.json +8 -2
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/src/app/compiled-context-files.ts +361 -0
- package/src/app/compiled-context.ts +240 -0
- package/src/app/context-surface.ts +7 -2
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/audit.ts +4 -0
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/context-compiled.ts +136 -0
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- package/src/cli/errors.ts +10 -3
- package/src/cli/program.ts +458 -1
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +3 -0
- package/src/core/compiled-context.ts +254 -0
- package/src/core/context-budget.ts +2 -10
- package/src/core/file-lock.ts +22 -5
- package/src/core/folder-setup-planning.ts +2 -1
- package/src/core/memory-remember.ts +233 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +16 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/setup-receipt.ts +27 -20
- package/src/core/typed-metadata.ts +4 -0
- package/src/core/validation.ts +9 -2
- package/src/core/windows-private-path.ts +96 -0
- package/src/index.ts +11 -2
- package/src/ingestion/compiled-context.ts +15 -0
- package/src/ingestion/sync.ts +40 -8
- package/src/ingestion/walker.ts +5 -4
- package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
- package/src/mcp/context.ts +8 -0
- package/src/mcp/http-egress.ts +15 -3
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/retrieval-warnings.ts +24 -0
- package/src/mcp/tools/ask.ts +14 -1
- package/src/mcp/tools/capture.ts +87 -83
- package/src/mcp/tools/context.ts +46 -2
- package/src/mcp/tools/index.ts +119 -7
- package/src/mcp/tools/memory-remember.ts +7 -0
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/query.ts +3 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/search.ts +3 -1
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/vsearch.ts +3 -1
- package/src/sdk/client.ts +252 -89
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +82 -3
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/compiled-context.ts +84 -0
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/public/app.tsx +15 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/components/sessions/snippet.tsx +53 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +10 -2
- package/src/serve/public/lib/request-intent.ts +69 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +4 -0
- package/src/serve/public/pages/CompiledContext.tsx +364 -0
- package/src/serve/public/pages/Dashboard.tsx +17 -0
- package/src/serve/public/pages/DocView.tsx +15 -1
- package/src/serve/public/pages/DocumentEditor.tsx +139 -96
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +43 -3
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +248 -3
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/spa-production-build.ts +6 -5
- package/src/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -0
- package/src/store/sqlite/adapter.ts +68 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertTriangleIcon,
|
|
5
|
+
CheckCircle2Icon,
|
|
6
|
+
CircleSlashIcon,
|
|
7
|
+
EyeIcon,
|
|
8
|
+
XCircleIcon,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
|
|
11
|
+
import type { SessionImportReceipt, SessionUnitReceipt } from "./api";
|
|
12
|
+
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { Badge } from "../ui/badge";
|
|
15
|
+
|
|
16
|
+
const STATUS_COPY: Record<
|
|
17
|
+
SessionImportReceipt["status"],
|
|
18
|
+
{ label: string; tone: string; Icon: typeof CheckCircle2Icon }
|
|
19
|
+
> = {
|
|
20
|
+
complete: {
|
|
21
|
+
label: "Complete",
|
|
22
|
+
tone: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
|
23
|
+
Icon: CheckCircle2Icon,
|
|
24
|
+
},
|
|
25
|
+
partial: {
|
|
26
|
+
label: "Partial — some units were not fully read",
|
|
27
|
+
tone: "border-amber-500/50 bg-amber-500/10 text-amber-800 dark:text-amber-200",
|
|
28
|
+
Icon: AlertTriangleIcon,
|
|
29
|
+
},
|
|
30
|
+
failed: {
|
|
31
|
+
label: "Failed",
|
|
32
|
+
tone: "border-destructive/50 bg-destructive/10 text-destructive",
|
|
33
|
+
Icon: XCircleIcon,
|
|
34
|
+
},
|
|
35
|
+
nothing_to_do: {
|
|
36
|
+
label: "Nothing to do — everything is current",
|
|
37
|
+
tone: "border-border bg-muted/40 text-muted-foreground",
|
|
38
|
+
Icon: CircleSlashIcon,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Unit outcomes worth listing individually (unchanged units are noise). */
|
|
43
|
+
const NOTABLE_OUTCOMES = new Set<SessionUnitReceipt["outcome"]>([
|
|
44
|
+
"imported",
|
|
45
|
+
"updated",
|
|
46
|
+
"skipped_policy",
|
|
47
|
+
"unsupported",
|
|
48
|
+
"incomplete",
|
|
49
|
+
"failed",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const OUTCOME_TONE: Partial<Record<SessionUnitReceipt["outcome"], string>> = {
|
|
53
|
+
incomplete: "text-amber-700 dark:text-amber-300",
|
|
54
|
+
failed: "text-destructive",
|
|
55
|
+
unsupported: "text-destructive",
|
|
56
|
+
skipped_policy: "text-amber-700 dark:text-amber-300",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function Stat({ label, value }: { label: string; value: number | string }) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="min-w-0 rounded-md border border-border/50 bg-muted/30 p-2">
|
|
62
|
+
<dt className="break-words text-muted-foreground text-xs">{label}</dt>
|
|
63
|
+
<dd className="font-mono font-semibold text-sm">{value}</dd>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ImportReceiptProps {
|
|
69
|
+
receipt: SessionImportReceipt;
|
|
70
|
+
/** Focus target after a preview/import completes (region is tabIndex -1). */
|
|
71
|
+
ref?: Ref<HTMLElement>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function ImportReceipt({ receipt, ref }: ImportReceiptProps) {
|
|
75
|
+
const status = STATUS_COPY[receipt.status];
|
|
76
|
+
const { counts, turns } = receipt;
|
|
77
|
+
const notable = receipt.units.filter((unit) =>
|
|
78
|
+
NOTABLE_OUTCOMES.has(unit.outcome)
|
|
79
|
+
);
|
|
80
|
+
const destinations = [
|
|
81
|
+
...new Set(receipt.units.flatMap((unit) => unit.collections)),
|
|
82
|
+
].sort();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<section
|
|
86
|
+
aria-label={receipt.dryRun ? "Import preview" : "Import receipt"}
|
|
87
|
+
className="min-w-0 space-y-4 rounded-lg border border-border/60 bg-card/60 p-4 outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
88
|
+
ref={ref}
|
|
89
|
+
tabIndex={-1}
|
|
90
|
+
>
|
|
91
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
92
|
+
{receipt.dryRun && (
|
|
93
|
+
<Badge className="gap-1" variant="outline">
|
|
94
|
+
<EyeIcon />
|
|
95
|
+
Preview only — nothing was written
|
|
96
|
+
</Badge>
|
|
97
|
+
)}
|
|
98
|
+
<span
|
|
99
|
+
className={cn(
|
|
100
|
+
"inline-flex min-w-0 items-center gap-1.5 rounded-md border px-2 py-1 font-medium text-sm",
|
|
101
|
+
status.tone
|
|
102
|
+
)}
|
|
103
|
+
data-status={receipt.status}
|
|
104
|
+
role="status"
|
|
105
|
+
>
|
|
106
|
+
<status.Icon aria-hidden="true" className="size-4 shrink-0" />
|
|
107
|
+
<span className="break-words">{status.label}</span>
|
|
108
|
+
</span>
|
|
109
|
+
<span className="text-muted-foreground text-xs">
|
|
110
|
+
Sources: {receipt.sourceIds.join(", ") || "—"} · index{" "}
|
|
111
|
+
<span className="font-mono">{receipt.index}</span>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<h4 className="mb-1 font-medium text-sm">Threads and units</h4>
|
|
117
|
+
<dl className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
118
|
+
<Stat label="Imported" value={counts.imported} />
|
|
119
|
+
<Stat label="Updated" value={counts.updated} />
|
|
120
|
+
<Stat label="Unchanged" value={counts.unchanged} />
|
|
121
|
+
<Stat label="Skipped by policy" value={counts.skippedPolicy} />
|
|
122
|
+
<Stat label="Unsupported" value={counts.unsupported} />
|
|
123
|
+
<Stat label="Incomplete" value={counts.incomplete} />
|
|
124
|
+
<Stat label="Failed" value={counts.failed} />
|
|
125
|
+
<Stat label="Deferred units" value={receipt.deferredUnits} />
|
|
126
|
+
</dl>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div>
|
|
130
|
+
<h4 className="mb-1 font-medium text-sm">Turns and redaction</h4>
|
|
131
|
+
<dl className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
132
|
+
<Stat label="Human turns" value={turns.human} />
|
|
133
|
+
<Stat label="Assistant turns" value={turns.assistant} />
|
|
134
|
+
<Stat label="Redactions" value={turns.redactions} />
|
|
135
|
+
<Stat
|
|
136
|
+
label="Injected context skipped"
|
|
137
|
+
value={turns.injectedSkipped}
|
|
138
|
+
/>
|
|
139
|
+
<Stat
|
|
140
|
+
label="Copied history skipped"
|
|
141
|
+
value={turns.copiedHistorySkipped}
|
|
142
|
+
/>
|
|
143
|
+
<Stat label="Over limit" value={turns.overLimit} />
|
|
144
|
+
</dl>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<p className="text-sm">
|
|
148
|
+
<span className="text-muted-foreground">Destination collections: </span>
|
|
149
|
+
{destinations.length > 0 ? (
|
|
150
|
+
<span className="font-mono break-all">{destinations.join(", ")}</span>
|
|
151
|
+
) : (
|
|
152
|
+
<span className="text-muted-foreground">none in this run</span>
|
|
153
|
+
)}
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
{!receipt.dryRun && (
|
|
157
|
+
<p className="text-sm">
|
|
158
|
+
<span className="text-muted-foreground">Lexical search: </span>
|
|
159
|
+
<span
|
|
160
|
+
className={cn(
|
|
161
|
+
"font-medium",
|
|
162
|
+
receipt.lexical.status === "failed" && "text-destructive"
|
|
163
|
+
)}
|
|
164
|
+
>
|
|
165
|
+
{receipt.lexical.status}
|
|
166
|
+
</span>
|
|
167
|
+
{receipt.lexical.error && (
|
|
168
|
+
<span className="block break-words text-destructive text-xs">
|
|
169
|
+
{receipt.lexical.error}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="text-muted-foreground">
|
|
173
|
+
{" "}
|
|
174
|
+
· embedding backlog: {receipt.embedding.backlog ?? "n/a"}
|
|
175
|
+
</span>
|
|
176
|
+
</p>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{receipt.warnings.length > 0 && (
|
|
180
|
+
<ul className="list-disc space-y-1 pl-5 text-amber-800 text-sm dark:text-amber-200">
|
|
181
|
+
{receipt.warnings.map((warning) => (
|
|
182
|
+
<li className="break-words" key={warning}>
|
|
183
|
+
{warning}
|
|
184
|
+
</li>
|
|
185
|
+
))}
|
|
186
|
+
</ul>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{notable.length > 0 && (
|
|
190
|
+
<div className="min-w-0">
|
|
191
|
+
<h4 className="mb-1 font-medium text-sm">Unit outcomes</h4>
|
|
192
|
+
<ul className="divide-y divide-border/40 rounded-md border border-border/50">
|
|
193
|
+
{notable.map((unit) => (
|
|
194
|
+
<li
|
|
195
|
+
className="min-w-0 space-y-0.5 p-2 text-sm"
|
|
196
|
+
key={`${unit.sourceId}:${unit.locator}`}
|
|
197
|
+
>
|
|
198
|
+
<div className="flex flex-wrap items-baseline gap-x-2">
|
|
199
|
+
<span
|
|
200
|
+
className={cn(
|
|
201
|
+
"font-medium",
|
|
202
|
+
OUTCOME_TONE[unit.outcome] ?? "text-foreground"
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
{unit.outcome.replace("_", " ")}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="min-w-0 break-all font-mono text-xs">
|
|
208
|
+
{unit.locator}
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
<div className="text-muted-foreground text-xs">
|
|
212
|
+
{unit.threads} thread{unit.threads === 1 ? "" : "s"} ·{" "}
|
|
213
|
+
{unit.turns} turn{unit.turns === 1 ? "" : "s"}
|
|
214
|
+
{unit.collections.length > 0 &&
|
|
215
|
+
` → ${unit.collections.join(", ")}`}
|
|
216
|
+
{unit.reason && ` · reason: ${unit.reason}`}
|
|
217
|
+
</div>
|
|
218
|
+
{unit.warnings?.map((warning) => (
|
|
219
|
+
<div
|
|
220
|
+
className="break-words text-amber-800 text-xs dark:text-amber-200"
|
|
221
|
+
key={warning}
|
|
222
|
+
>
|
|
223
|
+
{warning}
|
|
224
|
+
</div>
|
|
225
|
+
))}
|
|
226
|
+
</li>
|
|
227
|
+
))}
|
|
228
|
+
</ul>
|
|
229
|
+
{receipt.unitsTruncated && (
|
|
230
|
+
<p className="mt-1 text-muted-foreground text-xs">
|
|
231
|
+
More units were processed than are listed here.
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</section>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Loader2Icon, SearchIcon } from "lucide-react";
|
|
2
|
+
import { type FormEvent, useEffect, useId, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SESSION_HARNESS_LABELS,
|
|
6
|
+
SESSION_HARNESSES,
|
|
7
|
+
type SessionHarness,
|
|
8
|
+
} from "../../../../sessions/types";
|
|
9
|
+
import { apiFetch } from "../../hooks/use-api";
|
|
10
|
+
import { buildDocDeepLink } from "../../lib/deep-links";
|
|
11
|
+
import { Badge } from "../ui/badge";
|
|
12
|
+
import { Button } from "../ui/button";
|
|
13
|
+
import { Input } from "../ui/input";
|
|
14
|
+
import { renderSessionSnippet } from "./snippet";
|
|
15
|
+
|
|
16
|
+
interface SessionSearchResult {
|
|
17
|
+
docid: string;
|
|
18
|
+
uri: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
snippet: string;
|
|
21
|
+
categories?: string[];
|
|
22
|
+
record?: {
|
|
23
|
+
author?: string;
|
|
24
|
+
categories?: string[];
|
|
25
|
+
dateFields?: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SearchResponse {
|
|
30
|
+
results: SessionSearchResult[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface TagsResponse {
|
|
34
|
+
tags: Array<{ tag: string; count: number }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Role = "human" | "assistant";
|
|
38
|
+
|
|
39
|
+
const SELECT_CLASS =
|
|
40
|
+
"h-9 w-full min-w-0 rounded-md border border-input bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50";
|
|
41
|
+
|
|
42
|
+
const tagValue = (tags: readonly string[], prefix: string): string | null =>
|
|
43
|
+
tags.find((tag) => tag.startsWith(`${prefix}/`))?.slice(prefix.length + 1) ??
|
|
44
|
+
null;
|
|
45
|
+
|
|
46
|
+
function resultRole(result: SessionSearchResult, tags: string[]): Role | null {
|
|
47
|
+
const author = result.record?.author?.toLowerCase();
|
|
48
|
+
if (author === "human" || author === "assistant") return author;
|
|
49
|
+
const role = tagValue(tags, "role");
|
|
50
|
+
if (role === "human" || role === "assistant") return role;
|
|
51
|
+
const title = result.title ?? "";
|
|
52
|
+
if (title.startsWith("Human")) return "human";
|
|
53
|
+
if (title.startsWith("Assistant")) return "assistant";
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function AuthorBadge({ role }: { role: Role | null }) {
|
|
58
|
+
if (role === "human") {
|
|
59
|
+
return (
|
|
60
|
+
<Badge className="bg-sky-600 text-white dark:bg-sky-500">Human</Badge>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (role === "assistant") {
|
|
64
|
+
return (
|
|
65
|
+
<Badge className="whitespace-normal text-left" variant="outline">
|
|
66
|
+
Assistant — suggestion, not a user decision
|
|
67
|
+
</Badge>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return <Badge variant="outline">Author unknown</Badge>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatRecorded(iso: string | undefined): string {
|
|
74
|
+
if (!iso) return "time unknown";
|
|
75
|
+
const date = new Date(iso);
|
|
76
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface SessionSearchProps {
|
|
80
|
+
collections: string[];
|
|
81
|
+
navigate: (to: string) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function SessionSearch({ collections, navigate }: SessionSearchProps) {
|
|
85
|
+
const id = useId();
|
|
86
|
+
const [query, setQuery] = useState("");
|
|
87
|
+
const [harness, setHarness] = useState<"" | SessionHarness>("");
|
|
88
|
+
const [project, setProject] = useState("");
|
|
89
|
+
const [role, setRole] = useState<"" | Role>("");
|
|
90
|
+
const [collection, setCollection] = useState("");
|
|
91
|
+
const [projects, setProjects] = useState<string[]>([]);
|
|
92
|
+
const [results, setResults] = useState<SessionSearchResult[] | null>(null);
|
|
93
|
+
const [error, setError] = useState<string | null>(null);
|
|
94
|
+
const [loading, setLoading] = useState(false);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
void apiFetch<TagsResponse>("/api/tags?prefix=project").then(({ data }) => {
|
|
98
|
+
setProjects(
|
|
99
|
+
(data?.tags ?? [])
|
|
100
|
+
.map((entry) => entry.tag)
|
|
101
|
+
.filter((tag) => tag.startsWith("project/"))
|
|
102
|
+
.map((tag) => tag.slice("project/".length))
|
|
103
|
+
.sort()
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
const trimmed = query.trim();
|
|
111
|
+
if (!trimmed) return;
|
|
112
|
+
const tagsAll = [
|
|
113
|
+
"session",
|
|
114
|
+
harness ? `harness/${harness}` : null,
|
|
115
|
+
project ? `project/${project}` : null,
|
|
116
|
+
role ? `role/${role}` : null,
|
|
117
|
+
].filter((tag): tag is string => tag !== null);
|
|
118
|
+
setLoading(true);
|
|
119
|
+
const { data, error: err } = await apiFetch<SearchResponse>("/api/search", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
query: trimmed,
|
|
123
|
+
limit: 20,
|
|
124
|
+
tagsAll: tagsAll.join(","),
|
|
125
|
+
...(collection ? { collection } : {}),
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
setLoading(false);
|
|
129
|
+
setError(err);
|
|
130
|
+
setResults(data?.results ?? null);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<section aria-labelledby={`${id}-heading`} className="min-w-0 space-y-4">
|
|
135
|
+
<h2 className="font-semibold text-xl" id={`${id}-heading`}>
|
|
136
|
+
Search sessions
|
|
137
|
+
</h2>
|
|
138
|
+
<form
|
|
139
|
+
className="grid min-w-0 gap-3 sm:grid-cols-2 lg:grid-cols-4"
|
|
140
|
+
onSubmit={(event) => void submit(event)}
|
|
141
|
+
role="search"
|
|
142
|
+
>
|
|
143
|
+
<div className="grid min-w-0 gap-1 text-sm sm:col-span-2 lg:col-span-4">
|
|
144
|
+
<label htmlFor={`${id}-query`}>Session search query</label>
|
|
145
|
+
<div className="flex min-w-0 gap-2">
|
|
146
|
+
<Input
|
|
147
|
+
id={`${id}-query`}
|
|
148
|
+
onChange={(event) => setQuery(event.currentTarget.value)}
|
|
149
|
+
placeholder="What was decided about…"
|
|
150
|
+
type="search"
|
|
151
|
+
value={query}
|
|
152
|
+
/>
|
|
153
|
+
<Button disabled={loading || !query.trim()} type="submit">
|
|
154
|
+
{loading ? (
|
|
155
|
+
<Loader2Icon className="animate-spin" />
|
|
156
|
+
) : (
|
|
157
|
+
<SearchIcon />
|
|
158
|
+
)}
|
|
159
|
+
Search
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<label className="grid gap-1 text-sm" htmlFor={`${id}-harness`}>
|
|
164
|
+
Harness
|
|
165
|
+
<select
|
|
166
|
+
className={SELECT_CLASS}
|
|
167
|
+
id={`${id}-harness`}
|
|
168
|
+
onChange={(event) =>
|
|
169
|
+
setHarness(event.currentTarget.value as "" | SessionHarness)
|
|
170
|
+
}
|
|
171
|
+
value={harness}
|
|
172
|
+
>
|
|
173
|
+
<option value="">All harnesses</option>
|
|
174
|
+
{SESSION_HARNESSES.map((value) => (
|
|
175
|
+
<option key={value} value={value}>
|
|
176
|
+
{SESSION_HARNESS_LABELS[value]}
|
|
177
|
+
</option>
|
|
178
|
+
))}
|
|
179
|
+
</select>
|
|
180
|
+
</label>
|
|
181
|
+
<label className="grid gap-1 text-sm" htmlFor={`${id}-project`}>
|
|
182
|
+
Project
|
|
183
|
+
<select
|
|
184
|
+
className={SELECT_CLASS}
|
|
185
|
+
id={`${id}-project`}
|
|
186
|
+
onChange={(event) => setProject(event.currentTarget.value)}
|
|
187
|
+
value={project}
|
|
188
|
+
>
|
|
189
|
+
<option value="">All projects</option>
|
|
190
|
+
{projects.map((value) => (
|
|
191
|
+
<option key={value} value={value}>
|
|
192
|
+
{value}
|
|
193
|
+
</option>
|
|
194
|
+
))}
|
|
195
|
+
</select>
|
|
196
|
+
</label>
|
|
197
|
+
<label className="grid gap-1 text-sm" htmlFor={`${id}-role`}>
|
|
198
|
+
Speaker
|
|
199
|
+
<select
|
|
200
|
+
className={SELECT_CLASS}
|
|
201
|
+
id={`${id}-role`}
|
|
202
|
+
onChange={(event) =>
|
|
203
|
+
setRole(event.currentTarget.value as "" | Role)
|
|
204
|
+
}
|
|
205
|
+
value={role}
|
|
206
|
+
>
|
|
207
|
+
<option value="">Human and assistant</option>
|
|
208
|
+
<option value="human">Human only</option>
|
|
209
|
+
<option value="assistant">Assistant only</option>
|
|
210
|
+
</select>
|
|
211
|
+
</label>
|
|
212
|
+
<label className="grid gap-1 text-sm" htmlFor={`${id}-collection`}>
|
|
213
|
+
Collection
|
|
214
|
+
<select
|
|
215
|
+
className={SELECT_CLASS}
|
|
216
|
+
id={`${id}-collection`}
|
|
217
|
+
onChange={(event) => setCollection(event.currentTarget.value)}
|
|
218
|
+
value={collection}
|
|
219
|
+
>
|
|
220
|
+
<option value="">All archive collections</option>
|
|
221
|
+
{collections.map((value) => (
|
|
222
|
+
<option key={value} value={value}>
|
|
223
|
+
{value}
|
|
224
|
+
</option>
|
|
225
|
+
))}
|
|
226
|
+
</select>
|
|
227
|
+
</label>
|
|
228
|
+
</form>
|
|
229
|
+
|
|
230
|
+
{error && (
|
|
231
|
+
<p className="break-words text-destructive text-sm" role="alert">
|
|
232
|
+
{error}
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
{results && results.length === 0 && (
|
|
236
|
+
<p className="text-muted-foreground text-sm">
|
|
237
|
+
No archived turns matched.
|
|
238
|
+
</p>
|
|
239
|
+
)}
|
|
240
|
+
{results && results.length > 0 && (
|
|
241
|
+
<ol aria-label="Session search results" className="space-y-3">
|
|
242
|
+
{results.map((result) => {
|
|
243
|
+
const tags = result.categories ?? result.record?.categories ?? [];
|
|
244
|
+
const resultHarness = tagValue(tags, "harness");
|
|
245
|
+
const resultProject = tagValue(tags, "project");
|
|
246
|
+
const href = buildDocDeepLink({ uri: result.uri });
|
|
247
|
+
return (
|
|
248
|
+
<li
|
|
249
|
+
className="min-w-0 space-y-2 rounded-lg border border-border/60 p-3"
|
|
250
|
+
key={result.docid}
|
|
251
|
+
>
|
|
252
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
253
|
+
<AuthorBadge role={resultRole(result, tags)} />
|
|
254
|
+
<Badge variant="secondary">
|
|
255
|
+
{resultHarness
|
|
256
|
+
? (SESSION_HARNESS_LABELS[
|
|
257
|
+
resultHarness as SessionHarness
|
|
258
|
+
] ?? resultHarness)
|
|
259
|
+
: "harness unknown"}
|
|
260
|
+
</Badge>
|
|
261
|
+
<span className="text-muted-foreground text-xs">
|
|
262
|
+
project {resultProject ?? "none"} ·{" "}
|
|
263
|
+
{formatRecorded(result.record?.dateFields?.recorded)}
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
<a
|
|
267
|
+
className="block break-words font-medium text-primary underline-offset-2 hover:underline"
|
|
268
|
+
href={href}
|
|
269
|
+
onClick={(event) => {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
navigate(href);
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
{result.title || result.uri}
|
|
275
|
+
</a>
|
|
276
|
+
<p className="line-clamp-3 break-words text-muted-foreground text-sm">
|
|
277
|
+
{renderSessionSnippet(result.snippet)}
|
|
278
|
+
</p>
|
|
279
|
+
</li>
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
282
|
+
</ol>
|
|
283
|
+
)}
|
|
284
|
+
</section>
|
|
285
|
+
);
|
|
286
|
+
}
|