@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.
Files changed (136) hide show
  1. package/README.md +69 -6
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +98 -1
  4. package/assets/skill/cli-reference.md +140 -0
  5. package/assets/skill/examples.md +45 -0
  6. package/assets/skill/mcp-reference.md +62 -0
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +358 -24
  17. package/spec/compiled-context.md +68 -0
  18. package/spec/mcp.md +223 -2
  19. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  20. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  21. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  22. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  23. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  24. package/spec/output-schemas/memory-remember.schema.json +8 -2
  25. package/spec/output-schemas/request-status.schema.json +113 -0
  26. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  27. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  28. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  29. package/spec/output-schemas/sessions-status.schema.json +432 -0
  30. package/src/app/compiled-context-files.ts +361 -0
  31. package/src/app/compiled-context.ts +240 -0
  32. package/src/app/context-surface.ts +7 -2
  33. package/src/cli/commands/ask.ts +14 -2
  34. package/src/cli/commands/audit.ts +4 -0
  35. package/src/cli/commands/capture.ts +55 -96
  36. package/src/cli/commands/context-compiled.ts +136 -0
  37. package/src/cli/commands/daemon.ts +41 -0
  38. package/src/cli/commands/ls.ts +3 -0
  39. package/src/cli/commands/memory.ts +12 -3
  40. package/src/cli/commands/request-status.ts +59 -0
  41. package/src/cli/commands/reset.ts +39 -5
  42. package/src/cli/commands/sessions.ts +713 -0
  43. package/src/cli/commands/shared.ts +14 -1
  44. package/src/cli/errors.ts +10 -3
  45. package/src/cli/program.ts +458 -1
  46. package/src/cli/session-binding.ts +49 -0
  47. package/src/config/types.ts +8 -0
  48. package/src/core/capture-publish.ts +239 -0
  49. package/src/core/capture-sync.ts +3 -0
  50. package/src/core/compiled-context.ts +254 -0
  51. package/src/core/context-budget.ts +2 -10
  52. package/src/core/file-lock.ts +22 -5
  53. package/src/core/folder-setup-planning.ts +2 -1
  54. package/src/core/memory-remember.ts +233 -122
  55. package/src/core/memory-types.ts +11 -0
  56. package/src/core/network-boundary-inventory.ts +16 -0
  57. package/src/core/request-receipts.ts +671 -0
  58. package/src/core/setup-receipt.ts +27 -20
  59. package/src/core/typed-metadata.ts +4 -0
  60. package/src/core/validation.ts +9 -2
  61. package/src/core/windows-private-path.ts +96 -0
  62. package/src/index.ts +11 -2
  63. package/src/ingestion/compiled-context.ts +15 -0
  64. package/src/ingestion/sync.ts +40 -8
  65. package/src/ingestion/walker.ts +5 -4
  66. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  67. package/src/mcp/context.ts +8 -0
  68. package/src/mcp/http-egress.ts +15 -3
  69. package/src/mcp/http-transport.ts +2 -0
  70. package/src/mcp/retrieval-warnings.ts +24 -0
  71. package/src/mcp/tools/ask.ts +14 -1
  72. package/src/mcp/tools/capture.ts +87 -83
  73. package/src/mcp/tools/context.ts +46 -2
  74. package/src/mcp/tools/index.ts +119 -7
  75. package/src/mcp/tools/memory-remember.ts +7 -0
  76. package/src/mcp/tools/memory-shared.ts +7 -1
  77. package/src/mcp/tools/query.ts +3 -1
  78. package/src/mcp/tools/request-status.ts +73 -0
  79. package/src/mcp/tools/search.ts +3 -1
  80. package/src/mcp/tools/sessions.ts +208 -0
  81. package/src/mcp/tools/vsearch.ts +3 -1
  82. package/src/sdk/client.ts +252 -89
  83. package/src/sdk/index.ts +13 -0
  84. package/src/sdk/types.ts +82 -3
  85. package/src/serve/capture-service.ts +98 -32
  86. package/src/serve/compiled-context.ts +84 -0
  87. package/src/serve/config-sync.ts +3 -2
  88. package/src/serve/public/app.tsx +15 -1
  89. package/src/serve/public/components/CaptureModal.tsx +26 -8
  90. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  91. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  92. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  93. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  94. package/src/serve/public/components/sessions/api.ts +40 -0
  95. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  96. package/src/serve/public/globals.built.css +1 -1
  97. package/src/serve/public/hooks/use-api.ts +10 -2
  98. package/src/serve/public/lib/request-intent.ts +69 -0
  99. package/src/serve/public/lib/workspace-actions.ts +12 -1
  100. package/src/serve/public/lib/workspace-tabs.ts +4 -0
  101. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  102. package/src/serve/public/pages/Dashboard.tsx +17 -0
  103. package/src/serve/public/pages/DocView.tsx +15 -1
  104. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  105. package/src/serve/public/pages/Sessions.tsx +350 -0
  106. package/src/serve/resident-runtime.ts +43 -3
  107. package/src/serve/routes/api.ts +476 -147
  108. package/src/serve/routes/sessions.ts +766 -0
  109. package/src/serve/security.ts +9 -0
  110. package/src/serve/server.ts +248 -3
  111. package/src/serve/session-automation.ts +146 -0
  112. package/src/serve/spa-production-build.ts +6 -5
  113. package/src/sessions/archive.ts +348 -0
  114. package/src/sessions/automation-state.ts +444 -0
  115. package/src/sessions/automation-status.ts +239 -0
  116. package/src/sessions/automation.ts +1169 -0
  117. package/src/sessions/binding.ts +105 -0
  118. package/src/sessions/claude-hook.ts +240 -0
  119. package/src/sessions/config.ts +176 -0
  120. package/src/sessions/format.ts +191 -0
  121. package/src/sessions/import-child-env.ts +8 -0
  122. package/src/sessions/import-child.ts +152 -0
  123. package/src/sessions/parsers/claude-code.ts +259 -0
  124. package/src/sessions/parsers/codex.ts +303 -0
  125. package/src/sessions/parsers/hermes.ts +248 -0
  126. package/src/sessions/parsers/openclaw.ts +496 -0
  127. package/src/sessions/parsers/shared.ts +184 -0
  128. package/src/sessions/sanitize.ts +222 -0
  129. package/src/sessions/service.ts +1533 -0
  130. package/src/sessions/setup.ts +477 -0
  131. package/src/sessions/sources.ts +518 -0
  132. package/src/sessions/state.ts +118 -0
  133. package/src/sessions/types.ts +457 -0
  134. package/src/store/sqlite/adapter.ts +68 -16
  135. package/src/store/sqlite/scoped-index.ts +9 -0
  136. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
@@ -81,7 +81,12 @@ export function useApi<T>() {
81
81
  export async function apiFetch<T>(
82
82
  endpoint: string,
83
83
  options?: RequestInit
84
- ): Promise<{ data: T | null; error: string | null }> {
84
+ ): Promise<{
85
+ data: T | null;
86
+ error: string | null;
87
+ /** `error.details` of the JSON error envelope, when the server sent one. */
88
+ details?: Record<string, unknown>;
89
+ }> {
85
90
  try {
86
91
  const res = await fetch(endpoint, {
87
92
  headers: { "Content-Type": "application/json" },
@@ -95,10 +100,13 @@ export async function apiFetch<T>(
95
100
  }
96
101
 
97
102
  if (!res.ok) {
98
- const apiError = json as { error?: { message?: string } };
103
+ const apiError = json as {
104
+ error?: { message?: string; details?: Record<string, unknown> };
105
+ };
99
106
  return {
100
107
  data: null,
101
108
  error: apiError.error?.message || `Request failed: ${res.status}`,
109
+ details: apiError.error?.details,
102
110
  };
103
111
  }
104
112
 
@@ -0,0 +1,69 @@
1
+ /**
2
+ * One request ID per submitted write intent.
3
+ *
4
+ * Retrying the same payload (after a lost response, an error, or a page
5
+ * refresh) reuses the ID, so the server replays the committed outcome instead
6
+ * of writing twice. A changed payload is a new intent and gets a new ID. The
7
+ * unresolved intent is kept in sessionStorage so it survives a refresh of
8
+ * this tab; it is cleared once the write is confirmed.
9
+ */
10
+ export interface RequestIntent {
11
+ key: string;
12
+ requestId: string;
13
+ }
14
+
15
+ /** Compact payload fingerprint (cyrb53); a collision only yields a conflict error. */
16
+ function fingerprint(text: string): string {
17
+ let h1 = 0xde_ad_be_ef;
18
+ let h2 = 0x41_c6_ce_57;
19
+ for (let index = 0; index < text.length; index += 1) {
20
+ const code = text.charCodeAt(index);
21
+ h1 = Math.imul(h1 ^ code, 2_654_435_761);
22
+ h2 = Math.imul(h2 ^ code, 1_597_334_677);
23
+ }
24
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507);
25
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909);
26
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507);
27
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909);
28
+ return (4_294_967_296 * (2_097_151 & h2) + (h1 >>> 0)).toString(36);
29
+ }
30
+
31
+ function readStored(storageKey: string): RequestIntent | null {
32
+ try {
33
+ const raw = sessionStorage.getItem(storageKey);
34
+ return raw ? (JSON.parse(raw) as RequestIntent) : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export function requestIdForIntent(
41
+ slot: { current: RequestIntent | null },
42
+ payload: string,
43
+ storageKey: string
44
+ ): string {
45
+ const key = fingerprint(payload);
46
+ slot.current ??= readStored(storageKey);
47
+ if (slot.current?.key !== key) {
48
+ slot.current = { key, requestId: crypto.randomUUID() };
49
+ try {
50
+ sessionStorage.setItem(storageKey, JSON.stringify(slot.current));
51
+ } catch {
52
+ // Storage unavailable: the ID still covers retries in this page.
53
+ }
54
+ }
55
+ return slot.current.requestId;
56
+ }
57
+
58
+ /** The intent's write is confirmed: the next submit is a new intent. */
59
+ export function clearRequestIntent(
60
+ slot: { current: RequestIntent | null },
61
+ storageKey: string
62
+ ): void {
63
+ slot.current = null;
64
+ try {
65
+ sessionStorage.removeItem(storageKey);
66
+ } catch {
67
+ // Nothing persisted to clear.
68
+ }
69
+ }
@@ -14,7 +14,8 @@ export type WorkspaceActionId =
14
14
  | "go-ask"
15
15
  | "go-graph"
16
16
  | "go-collections"
17
- | "go-connectors";
17
+ | "go-connectors"
18
+ | "go-sessions";
18
19
 
19
20
  export interface WorkspaceAction {
20
21
  id: WorkspaceActionId;
@@ -155,6 +156,13 @@ export function getWorkspaceActions(
155
156
  keywords: ["connectors", "mcp", "skills", "agents"],
156
157
  available: true,
157
158
  },
159
+ {
160
+ id: "go-sessions",
161
+ group: "Go To",
162
+ label: "Agent sessions",
163
+ keywords: ["sessions", "archive", "transcripts", "import", "agents"],
164
+ available: true,
165
+ },
158
166
  ];
159
167
  }
160
168
 
@@ -220,6 +228,9 @@ export function runWorkspaceAction(
220
228
  case "go-connectors":
221
229
  handlers.navigate("/connectors");
222
230
  break;
231
+ case "go-sessions":
232
+ handlers.navigate("/sessions");
233
+ break;
223
234
  }
224
235
 
225
236
  handlers.closePalette();
@@ -65,8 +65,12 @@ function getLocationLabel(location: string): string {
65
65
  return "Connectors";
66
66
  case "/graph":
67
67
  return "Graph";
68
+ case "/context/compiled":
69
+ return "Compiled context";
68
70
  case "/traces":
69
71
  return "Trace history";
72
+ case "/sessions":
73
+ return "Agent sessions";
70
74
  case "/doc":
71
75
  case "/edit": {
72
76
  const uri = params.get("uri") ?? "";
@@ -0,0 +1,364 @@
1
+ import { ArrowLeftIcon, DownloadIcon } from "lucide-react";
2
+ import { useState } from "react";
3
+
4
+ import type {
5
+ CompiledContextPreview as Preview,
6
+ CompiledContextCheck as Check,
7
+ } from "../../../core/compiled-context";
8
+
9
+ import { Button } from "../components/ui/button";
10
+ import {
11
+ Card,
12
+ CardContent,
13
+ CardHeader,
14
+ CardTitle,
15
+ } from "../components/ui/card";
16
+ import { Input } from "../components/ui/input";
17
+ import { Textarea } from "../components/ui/textarea";
18
+ import { apiFetch } from "../hooks/use-api";
19
+
20
+ const MAX_BYTES = 4 * 1024 * 1024;
21
+ const byteLength = (value: string): number =>
22
+ new TextEncoder().encode(value).length;
23
+
24
+ export default function CompiledContext({
25
+ navigate,
26
+ }: {
27
+ navigate: (to: string | number) => void;
28
+ }) {
29
+ const [capsule, setCapsule] = useState("");
30
+ const [markdown, setMarkdown] = useState("");
31
+ const [tokens, setTokens] = useState("12000");
32
+ const [bytes, setBytes] = useState("");
33
+ const [preview, setPreview] = useState<Preview | null>(null);
34
+ const [check, setCheck] = useState<Check | null>(null);
35
+ const [busy, setBusy] = useState(false);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ const invalidate = () => {
39
+ setPreview(null);
40
+ setCheck(null);
41
+ setError(null);
42
+ };
43
+ const upload = async (
44
+ file: File | undefined,
45
+ kind: "capsule" | "markdown"
46
+ ) => {
47
+ if (!file) return;
48
+ invalidate();
49
+ if (kind === "capsule") setCapsule("");
50
+ else setMarkdown("");
51
+ if (file.size > MAX_BYTES) {
52
+ setError("Files must be 4 MiB or smaller.");
53
+ return;
54
+ }
55
+ setBusy(true);
56
+ try {
57
+ const text = await file.text();
58
+ if (kind === "capsule") setCapsule(text);
59
+ else setMarkdown(text);
60
+ } catch {
61
+ setError("Could not read this file.");
62
+ } finally {
63
+ setBusy(false);
64
+ }
65
+ };
66
+ const run = async (kind: "preview" | "check") => {
67
+ invalidate();
68
+ setBusy(true);
69
+ try {
70
+ if (byteLength(capsule) > MAX_BYTES || byteLength(markdown) > MAX_BYTES)
71
+ throw new Error("Inputs must be 4 MiB or smaller.");
72
+ const parsed: unknown = JSON.parse(capsule);
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
74
+ throw new Error("Supply a Capsule JSON object.");
75
+ if (kind === "preview") {
76
+ const budgetTokens = Number(tokens);
77
+ const budgetBytes = bytes === "" ? undefined : Number(bytes);
78
+ if (
79
+ !Number.isInteger(budgetTokens) ||
80
+ budgetTokens < 1 ||
81
+ budgetTokens > 1_000_000 ||
82
+ (budgetBytes !== undefined &&
83
+ (!Number.isInteger(budgetBytes) ||
84
+ budgetBytes < 1 ||
85
+ budgetBytes > MAX_BYTES))
86
+ )
87
+ throw new Error(
88
+ "Choose a positive budget: at most 1,000,000 tokens and 4 MiB."
89
+ );
90
+ const result = await apiFetch<Preview>(
91
+ "/api/context/compiled/preview",
92
+ {
93
+ method: "POST",
94
+ body: JSON.stringify({
95
+ capsule: parsed,
96
+ budgetTokens,
97
+ budgetBytes,
98
+ }),
99
+ }
100
+ );
101
+ if (result.error || !result.data)
102
+ throw new Error(result.error ?? "No preview returned.");
103
+ setPreview(result.data);
104
+ } else {
105
+ if (!markdown.trim())
106
+ throw new Error("Supply the compiled Markdown to check.");
107
+ const result = await apiFetch<Check>("/api/context/compiled/check", {
108
+ method: "POST",
109
+ body: JSON.stringify({ capsule: parsed, markdown }),
110
+ });
111
+ if (result.error || !result.data)
112
+ throw new Error(result.error ?? "No check returned.");
113
+ setCheck(result.data);
114
+ }
115
+ } catch (cause) {
116
+ setError(cause instanceof Error ? cause.message : "Request failed.");
117
+ } finally {
118
+ setBusy(false);
119
+ }
120
+ };
121
+ const download = () => {
122
+ if (!preview) return;
123
+ const url = URL.createObjectURL(
124
+ new Blob([preview.markdown], { type: "text/markdown;charset=utf-8" })
125
+ );
126
+ const link = document.createElement("a");
127
+ link.href = url;
128
+ link.download = "project.gno-context.md";
129
+ link.click();
130
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
131
+ };
132
+
133
+ return (
134
+ <main className="container mx-auto max-w-5xl space-y-6 px-4 py-8">
135
+ <Button onClick={() => navigate("/")} variant="ghost">
136
+ <ArrowLeftIcon className="size-4" /> Dashboard
137
+ </Button>
138
+ <header className="space-y-2">
139
+ <h1 className="font-serif text-3xl">Compiled project context</h1>
140
+ <p className="text-muted-foreground">
141
+ Turn a verified Context Capsule into a compact, cited Markdown file.
142
+ Source passages remain untrusted evidence, not agent instructions.
143
+ </p>
144
+ </header>
145
+ <form
146
+ onSubmit={(event) => {
147
+ event.preventDefault();
148
+ void run("preview");
149
+ }}
150
+ >
151
+ <fieldset className="min-w-0 space-y-4" disabled={busy}>
152
+ <legend className="sr-only">Compile a Context Capsule</legend>
153
+ <Card>
154
+ <CardHeader>
155
+ <CardTitle>1. Supply a Capsule</CardTitle>
156
+ </CardHeader>
157
+ <CardContent className="space-y-3">
158
+ <p className="text-sm text-muted-foreground">
159
+ Use a Capsule from this GNO instance. Verification checks
160
+ current sources and access before returning evidence. Inputs are
161
+ limited to 4 MiB each.
162
+ </p>
163
+ <label className="block space-y-2" htmlFor="capsule-file">
164
+ <span>Upload Capsule JSON</span>
165
+ <Input
166
+ accept=".json,application/json"
167
+ id="capsule-file"
168
+ onChange={(event) =>
169
+ void upload(event.target.files?.[0], "capsule")
170
+ }
171
+ type="file"
172
+ />
173
+ </label>
174
+ <label className="block space-y-2" htmlFor="capsule-json">
175
+ <span>Or paste Capsule JSON</span>
176
+ <Textarea
177
+ className="h-40 min-w-0 field-sizing-fixed font-mono text-xs"
178
+ id="capsule-json"
179
+ onChange={(event) => {
180
+ invalidate();
181
+ setCapsule(event.target.value);
182
+ }}
183
+ spellCheck={false}
184
+ value={capsule}
185
+ />
186
+ </label>
187
+ <div className="grid gap-4 sm:grid-cols-2">
188
+ <label className="space-y-2" htmlFor="context-tokens">
189
+ <span>Token budget</span>
190
+ <Input
191
+ id="context-tokens"
192
+ max={1_000_000}
193
+ min={1}
194
+ onChange={(event) => {
195
+ invalidate();
196
+ setTokens(event.target.value);
197
+ }}
198
+ required
199
+ type="number"
200
+ value={tokens}
201
+ />
202
+ </label>
203
+ <label className="space-y-2" htmlFor="context-bytes">
204
+ <span>Byte budget (optional)</span>
205
+ <Input
206
+ id="context-bytes"
207
+ max={MAX_BYTES}
208
+ min={1}
209
+ onChange={(event) => {
210
+ invalidate();
211
+ setBytes(event.target.value);
212
+ }}
213
+ type="number"
214
+ value={bytes}
215
+ />
216
+ </label>
217
+ </div>
218
+ <Button type="submit">
219
+ {busy ? "Verifying…" : "Preview verified context"}
220
+ </Button>
221
+ </CardContent>
222
+ </Card>
223
+ <Card>
224
+ <CardHeader>
225
+ <CardTitle>2. Check an existing artifact</CardTitle>
226
+ </CardHeader>
227
+ <CardContent className="space-y-3">
228
+ <label className="block space-y-2" htmlFor="context-file">
229
+ <span>Upload compiled Markdown</span>
230
+ <Input
231
+ accept=".md,text/markdown"
232
+ id="context-file"
233
+ onChange={(event) =>
234
+ void upload(event.target.files?.[0], "markdown")
235
+ }
236
+ type="file"
237
+ />
238
+ </label>
239
+ <label className="block space-y-2" htmlFor="context-markdown">
240
+ <span>Or paste compiled Markdown</span>
241
+ <Textarea
242
+ className="h-32 min-w-0 field-sizing-fixed font-mono text-xs"
243
+ id="context-markdown"
244
+ onChange={(event) => {
245
+ invalidate();
246
+ setMarkdown(event.target.value);
247
+ }}
248
+ spellCheck={false}
249
+ value={markdown}
250
+ />
251
+ </label>
252
+ <Button
253
+ onClick={() => void run("check")}
254
+ type="button"
255
+ variant="outline"
256
+ >
257
+ Check freshness
258
+ </Button>
259
+ </CardContent>
260
+ </Card>
261
+ </fieldset>
262
+ </form>
263
+ {error && (
264
+ <p className="break-words text-destructive" role="alert">
265
+ {error}
266
+ </p>
267
+ )}
268
+ {check && (
269
+ <Card>
270
+ <CardHeader>
271
+ <CardTitle>Artifact: {check.status}</CardTitle>
272
+ </CardHeader>
273
+ <CardContent aria-live="polite">
274
+ <ul className="list-inside list-disc">
275
+ {check.reasons.map((reason) => (
276
+ <li key={reason}>{reason}</li>
277
+ ))}
278
+ </ul>
279
+ <p className="mt-2 text-sm text-muted-foreground">
280
+ Check is read-only. Conflicts require inspecting manual edits;
281
+ stale evidence requires an explicit local refresh.
282
+ </p>
283
+ </CardContent>
284
+ </Card>
285
+ )}
286
+ {preview && (
287
+ <Card>
288
+ <CardHeader>
289
+ <CardTitle>Verified preview</CardTitle>
290
+ </CardHeader>
291
+ <CardContent className="min-w-0 space-y-4 break-words">
292
+ <p>
293
+ {preview.budget.usedTokens.toLocaleString()} tokens ·{" "}
294
+ {preview.budget.usedBytes.toLocaleString()} bytes ·{" "}
295
+ {preview.budget.estimator}
296
+ </p>
297
+ <p>
298
+ {preview.coverage.complete
299
+ ? "Required facets covered"
300
+ : "Incomplete coverage"}
301
+ </p>
302
+ <p>
303
+ Covered facets:{" "}
304
+ {preview.coverage.coveredFacets.join(", ") || "None"}
305
+ </p>
306
+ <p>
307
+ Unresolved facets:{" "}
308
+ {preview.coverage.unresolvedFacets.join(", ") || "None"}
309
+ </p>
310
+ <details>
311
+ <summary className="cursor-pointer">
312
+ Evidence and verification
313
+ </summary>
314
+ <div className="space-y-2 break-all font-mono text-xs">
315
+ <p>Output digest: {preview.digest}</p>
316
+ <p>Verification digest: {preview.verificationDigest}</p>
317
+ <p>Evidence: {preview.evidenceIds.join(", ") || "None"}</p>
318
+ <ul>
319
+ {preview.omissions.map((item) => (
320
+ <li key={item.evidenceId}>
321
+ {item.evidenceId}: {item.reason}
322
+ </li>
323
+ ))}
324
+ </ul>
325
+ </div>
326
+ </details>
327
+ <pre
328
+ aria-label="Compiled Markdown preview"
329
+ className="max-h-[32rem] overflow-auto whitespace-pre-wrap break-all rounded-md border bg-muted/30 p-4 font-mono text-xs"
330
+ >
331
+ {preview.markdown}
332
+ </pre>
333
+ <Button onClick={download}>
334
+ <DownloadIcon className="size-4" /> Download Markdown
335
+ </Button>
336
+ <p className="text-sm text-muted-foreground">
337
+ Download contains exactly the preview bytes. It does not create a
338
+ local refresh sidecar or update your agent instructions.
339
+ </p>
340
+ </CardContent>
341
+ </Card>
342
+ )}
343
+ <Card>
344
+ <CardHeader>
345
+ <CardTitle>Refresh local files explicitly</CardTitle>
346
+ </CardHeader>
347
+ <CardContent className="space-y-3">
348
+ <p className="text-sm text-muted-foreground">
349
+ For managed refresh, first compile locally with an explicit Capsule
350
+ file. Run gno update after source edits: checks use indexed state.
351
+ Choose a fresh Capsule output filename for each stale refresh.
352
+ Refresh preserves hand edits by reporting a conflict. Nothing
353
+ refreshes in the background.
354
+ </p>
355
+ <pre className="overflow-auto whitespace-pre-wrap break-all rounded-md bg-muted/30 p-4 text-xs">
356
+ {
357
+ "gno context compiled compile --capsule capsule.json --budget 12000 --output project.gno-context.md\ngno context compiled check project.gno-context.md\ngno context compiled refresh project.gno-context.md --capsule-output project.gno-context.capsule.json"
358
+ }
359
+ </pre>
360
+ </CardContent>
361
+ </Card>
362
+ </main>
363
+ );
364
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ArchiveIcon,
2
3
  BookOpen,
3
4
  CheckCircle2Icon,
4
5
  CpuIcon,
@@ -540,6 +541,22 @@ export default function Dashboard({ navigate }: PageProps) {
540
541
  <HistoryIcon className="size-4" />
541
542
  Trace history
542
543
  </Button>
544
+ <Button
545
+ className="gap-2"
546
+ onClick={() => navigate("/sessions")}
547
+ size="lg"
548
+ variant="outline"
549
+ >
550
+ <ArchiveIcon className="size-4" />
551
+ Agent sessions
552
+ </Button>
553
+ <Button
554
+ onClick={() => navigate("/context/compiled")}
555
+ size="lg"
556
+ variant="outline"
557
+ >
558
+ Compiled context
559
+ </Button>
543
560
  </nav>
544
561
 
545
562
  {error && (
@@ -83,6 +83,13 @@ import {
83
83
  isPdfDocument,
84
84
  } from "../lib/doc-asset-url";
85
85
  import { waitForDocumentAvailability } from "../lib/document-availability";
86
+ import {
87
+ clearRequestIntent,
88
+ type RequestIntent,
89
+ requestIdForIntent,
90
+ } from "../lib/request-intent";
91
+
92
+ const TAG_INTENT_KEY = "gno:tag-intent";
86
93
  import {
87
94
  buildReadableSectionUrl,
88
95
  createCitationSectionUrl,
@@ -1201,7 +1208,8 @@ export default function DocView({ navigate }: PageProps) {
1201
1208
  setTagSaveError(null);
1202
1209
  }, []);
1203
1210
 
1204
- // Save tags
1211
+ // Save tags (one request ID per tag-set intent, reused on retry)
1212
+ const tagIntentRef = useRef<RequestIntent | null>(null);
1205
1213
  const handleSaveTags = useCallback(async () => {
1206
1214
  if (!doc) return;
1207
1215
 
@@ -1218,6 +1226,11 @@ export default function DocView({ navigate }: PageProps) {
1218
1226
  expectedSourceHash: doc.source.sourceHash,
1219
1227
  expectedModifiedAt: doc.source.modifiedAt,
1220
1228
  uri: doc.uri,
1229
+ requestId: requestIdForIntent(
1230
+ tagIntentRef,
1231
+ `${doc.uri}\u0000${doc.source.sourceHash}\u0000${editedTags.join(",")}`,
1232
+ TAG_INTENT_KEY
1233
+ ),
1221
1234
  }),
1222
1235
  }
1223
1236
  );
@@ -1229,6 +1242,7 @@ export default function DocView({ navigate }: PageProps) {
1229
1242
  return;
1230
1243
  }
1231
1244
 
1245
+ clearRequestIntent(tagIntentRef, TAG_INTENT_KEY);
1232
1246
  // Update doc with new tags
1233
1247
  setDoc({
1234
1248
  ...doc,