@gmickel/gno 1.46.0 → 2.1.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 (235) hide show
  1. package/README.md +17 -5
  2. package/THIRD_PARTY_NOTICES.md +46 -0
  3. package/assets/skill/SKILL.md +7 -6
  4. package/assets/spa-production.json.gz +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/browser-extension/dist/preview.html +1 -1
  10. package/browser-extension/dist/service-worker.js +32 -33
  11. package/bunfig.toml +2 -0
  12. package/package.json +40 -26
  13. package/spec/cli.md +29 -4
  14. package/spec/db/schema.sql +146 -1
  15. package/spec/mcp.md +26 -0
  16. package/src/app/context-runtime-types.ts +3 -0
  17. package/src/app/context-runtime.ts +2 -0
  18. package/src/cli/commands/ask.ts +6 -1
  19. package/src/cli/commands/daemon.ts +21 -8
  20. package/src/cli/commands/embed.ts +77 -41
  21. package/src/cli/detach.ts +3 -2
  22. package/src/config/types.ts +3 -3
  23. package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
  24. package/src/converters/adapters/officeparser/adapter.ts +1 -2
  25. package/src/converters/versions.ts +6 -8
  26. package/src/core/context-evidence.ts +8 -4
  27. package/src/core/job-manager.ts +95 -13
  28. package/src/core/network-boundary-inventory.ts +10 -0
  29. package/src/core/shutdown-budget.ts +45 -0
  30. package/src/embed/backlog.ts +107 -4
  31. package/src/embed/batch.ts +42 -2
  32. package/src/embed/fingerprint.ts +16 -0
  33. package/src/embed/retry.ts +113 -5
  34. package/src/embed/variant-backlog.ts +105 -0
  35. package/src/embed/variant-plan.ts +62 -0
  36. package/src/embed/variant-retry.ts +113 -0
  37. package/src/ingestion/graph-reconciliation.ts +327 -0
  38. package/src/ingestion/sync.ts +9 -272
  39. package/src/llm/http-inference.ts +6 -0
  40. package/src/llm/httpEmbedding.ts +37 -6
  41. package/src/llm/httpGeneration.ts +18 -3
  42. package/src/llm/httpRerank.ts +23 -5
  43. package/src/llm/inference-cancellation.ts +168 -0
  44. package/src/llm/inference-scope.ts +202 -0
  45. package/src/llm/lazy-ports.ts +115 -0
  46. package/src/llm/native-worker/client.ts +541 -0
  47. package/src/llm/native-worker/dispatcher.ts +228 -0
  48. package/src/llm/native-worker/embedding-identity.ts +33 -0
  49. package/src/llm/native-worker/entry.ts +173 -0
  50. package/src/llm/native-worker/errors.ts +32 -0
  51. package/src/llm/native-worker/evaluation.ts +16 -0
  52. package/src/llm/native-worker/owned-exit.ts +108 -0
  53. package/src/llm/native-worker/owner.ts +141 -0
  54. package/src/llm/native-worker/ports.ts +317 -0
  55. package/src/llm/native-worker/protocol.ts +442 -0
  56. package/src/llm/native-worker/runtime-config.ts +92 -0
  57. package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
  58. package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
  59. package/src/llm/nodeLlamaCpp/generation.ts +34 -5
  60. package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
  61. package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
  62. package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
  63. package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
  64. package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
  65. package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
  66. package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
  67. package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
  68. package/src/llm/types.ts +35 -5
  69. package/src/mcp/context.ts +27 -0
  70. package/src/mcp/http-transport.ts +12 -10
  71. package/src/mcp/server.ts +3 -0
  72. package/src/mcp/tool-profile.ts +30 -8
  73. package/src/mcp/tools/context.ts +8 -11
  74. package/src/mcp/tools/embed.ts +1 -1
  75. package/src/mcp/tools/index-cmd.ts +1 -1
  76. package/src/mcp/tools/index.ts +10 -8
  77. package/src/mcp/tools/query.ts +14 -30
  78. package/src/mcp/tools/vsearch.ts +1 -1
  79. package/src/pipeline/answer.ts +23 -3
  80. package/src/pipeline/claim-verifier.ts +6 -0
  81. package/src/pipeline/expansion.ts +43 -40
  82. package/src/pipeline/explain.ts +6 -2
  83. package/src/pipeline/filters.ts +63 -0
  84. package/src/pipeline/fusion.ts +29 -9
  85. package/src/pipeline/graph-retrieval.ts +29 -9
  86. package/src/pipeline/hybrid.ts +198 -55
  87. package/src/pipeline/hydration.ts +161 -0
  88. package/src/pipeline/owner-fusion.ts +87 -0
  89. package/src/pipeline/rerank.ts +35 -11
  90. package/src/pipeline/search.ts +13 -2
  91. package/src/pipeline/types.ts +5 -3
  92. package/src/pipeline/vsearch.ts +87 -7
  93. package/src/sdk/client.ts +47 -3
  94. package/src/sdk/embed.ts +63 -39
  95. package/src/serve/background-runtime.ts +1 -1
  96. package/src/serve/context.ts +41 -56
  97. package/src/serve/embed-scheduler.ts +58 -35
  98. package/src/serve/public/components/IndexingProgress.tsx +46 -60
  99. package/src/serve/public/components/PublishExportDialog.tsx +266 -0
  100. package/src/serve/public/globals.built.css +1 -1
  101. package/src/serve/public/globals.css +35 -0
  102. package/src/serve/public/lib/publish-export.ts +81 -1
  103. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  104. package/src/serve/public/pages/Collections.tsx +12 -46
  105. package/src/serve/public/pages/DocView.tsx +14 -52
  106. package/src/serve/resident-admission.ts +36 -36
  107. package/src/serve/resident-background-work.ts +20 -2
  108. package/src/serve/resident-request.ts +11 -5
  109. package/src/serve/resident-runtime.ts +97 -61
  110. package/src/serve/resident-shutdown.ts +153 -0
  111. package/src/serve/routes/api.ts +3 -1
  112. package/src/serve/server.ts +47 -26
  113. package/src/store/migrations/028-vector-variants.ts +54 -0
  114. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  115. package/src/store/migrations/index.ts +4 -0
  116. package/src/store/sqlite/adapter.ts +251 -183
  117. package/src/store/sqlite/eligibility.ts +174 -0
  118. package/src/store/sqlite/graph-edge-application.ts +66 -0
  119. package/src/store/sqlite/graph-reference-state.ts +194 -0
  120. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  121. package/src/store/types.ts +80 -12
  122. package/src/store/vector/eligibility.ts +36 -0
  123. package/src/store/vector/freshness.ts +33 -6
  124. package/src/store/vector/lazy.ts +81 -0
  125. package/src/store/vector/sqlite-vec.ts +106 -54
  126. package/src/store/vector/stats.ts +14 -3
  127. package/src/store/vector/types.ts +35 -2
  128. package/src/store/vector/variant-search.ts +192 -0
  129. package/src/store/vector/variants.ts +451 -0
  130. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  131. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  132. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  133. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  134. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  135. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  136. package/vendor/converters/markitdown-ts/package.json +77 -0
  137. package/vendor/converters/officeparser/LICENSE +21 -0
  138. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  139. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  140. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  141. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  142. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  143. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  144. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  145. package/vendor/converters/officeparser/dist/cli.js +381 -0
  146. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  147. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  148. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  149. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  150. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  151. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  152. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  153. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  154. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  155. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  156. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  157. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  158. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  159. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  160. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  161. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  162. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  163. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  164. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  165. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  166. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  167. package/vendor/converters/officeparser/dist/index.js +72 -0
  168. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  169. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  170. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  171. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  172. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  173. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  174. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  175. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  176. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  177. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  178. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  179. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  180. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  181. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  182. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  183. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  184. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  185. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  186. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  187. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  188. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  189. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  190. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  191. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  192. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  193. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  194. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  195. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  196. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  197. package/vendor/converters/officeparser/dist/types.js +107 -0
  198. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  199. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  200. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  201. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  202. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  203. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  204. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  205. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  206. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  207. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  208. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  209. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  210. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  211. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  212. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  213. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  214. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  215. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  216. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  217. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  218. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  219. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  220. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  221. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  222. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  223. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  224. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  225. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  226. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  227. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  228. package/vendor/converters/officeparser/package.json +147 -0
  229. package/vendor/converters/upstream-manifest.json +124 -0
  230. package/vendor/dependency-fixes/README.md +77 -0
  231. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  232. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
  233. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
  234. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  235. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
@@ -15,7 +15,7 @@ import {
15
15
  Loader2Icon,
16
16
  TimerIcon,
17
17
  } from "lucide-react";
18
- import { useCallback, useEffect, useRef, useState } from "react";
18
+ import { useEffect, useState } from "react";
19
19
 
20
20
  import { apiFetch } from "../hooks/use-api";
21
21
  import { cn } from "../lib/utils";
@@ -103,77 +103,63 @@ export function IndexingProgress({
103
103
  const [status, setStatus] = useState<JobStatus | null>(null);
104
104
  const [elapsed, setElapsed] = useState(0);
105
105
  const [fetchError, setFetchError] = useState<string | null>(null);
106
- const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
107
- const elapsedIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
108
- null
109
- );
110
- const completedRef = useRef(false);
106
+ useEffect(() => {
107
+ // Each effect owns its requests and timers, including across quick restarts.
108
+ const controller = new AbortController();
109
+ let pollTimeout: ReturnType<typeof setTimeout> | undefined;
110
+ const startTime = Date.now();
111
+ const elapsedInterval = setInterval(() => {
112
+ setElapsed(Date.now() - startTime);
113
+ }, 1000);
111
114
 
112
- // Poll job status
113
- const poll = useCallback(async () => {
114
- const { data, error: err } = await apiFetch<JobStatus>(
115
- `/api/jobs/${encodeURIComponent(jobId)}`
116
- );
115
+ setStatus(null);
116
+ setFetchError(null);
117
+ setElapsed(0);
117
118
 
118
- if (err) {
119
- setFetchError(err);
120
- return;
121
- }
119
+ const poll = async () => {
120
+ if (controller.signal.aborted) {
121
+ return;
122
+ }
123
+ const { data, error: err } = await apiFetch<JobStatus>(
124
+ `/api/jobs/${encodeURIComponent(jobId)}`,
125
+ { signal: controller.signal }
126
+ );
122
127
 
123
- if (!data) {
124
- setFetchError("No response from server");
125
- return;
126
- }
128
+ // Abort may race with response parsing; never publish or reschedule it.
129
+ if (controller.signal.aborted) {
130
+ return;
131
+ }
132
+ if (err || !data) {
133
+ setFetchError(err ?? "No response from server");
134
+ clearInterval(elapsedInterval);
135
+ return;
136
+ }
127
137
 
128
- setStatus(data);
129
- setFetchError(null);
138
+ setStatus(data);
139
+ setFetchError(null);
130
140
 
131
- // Handle terminal states
132
- if (data.status === "completed") {
133
- if (!completedRef.current && data.result) {
134
- completedRef.current = true;
135
- onComplete?.(data.result);
141
+ if (data.status === "running") {
142
+ pollTimeout = setTimeout(() => {
143
+ void poll();
144
+ }, 1000);
145
+ return;
136
146
  }
137
- } else if (data.status === "failed") {
138
- if (!completedRef.current) {
139
- completedRef.current = true;
147
+
148
+ clearInterval(elapsedInterval);
149
+ if (data.status === "completed" && data.result) {
150
+ onComplete?.(data.result);
151
+ } else if (data.status === "failed") {
140
152
  onError?.(data.error ?? "Unknown error");
141
153
  }
142
- } else if (data.status === "running") {
143
- // Continue polling
144
- pollTimeoutRef.current = setTimeout(() => {
145
- void poll();
146
- }, 1000);
147
- }
148
- }, [jobId, onComplete, onError]);
149
-
150
- // Start polling on mount
151
- useEffect(() => {
152
- completedRef.current = false;
154
+ };
153
155
  void poll();
154
156
 
155
- // Track elapsed time
156
- const startTime = Date.now();
157
- elapsedIntervalRef.current = setInterval(() => {
158
- setElapsed(Date.now() - startTime);
159
- }, 1000);
160
-
161
157
  return () => {
162
- if (pollTimeoutRef.current) {
163
- clearTimeout(pollTimeoutRef.current);
164
- }
165
- if (elapsedIntervalRef.current) {
166
- clearInterval(elapsedIntervalRef.current);
167
- }
158
+ controller.abort();
159
+ clearTimeout(pollTimeout);
160
+ clearInterval(elapsedInterval);
168
161
  };
169
- }, [poll]);
170
-
171
- // Stop elapsed timer when complete
172
- useEffect(() => {
173
- if (status?.status !== "running" && elapsedIntervalRef.current) {
174
- clearInterval(elapsedIntervalRef.current);
175
- }
176
- }, [status?.status]);
162
+ }, [jobId, onComplete, onError]);
177
163
 
178
164
  // Loading/error before first status
179
165
  if (!status && !fetchError) {
@@ -0,0 +1,266 @@
1
+ import { Loader2Icon } from "lucide-react";
2
+ import { useRef, useState } from "react";
3
+
4
+ import type { PublishVisibility } from "../../../publish/artifact";
5
+
6
+ import { apiFetch } from "../hooks/use-api";
7
+ import {
8
+ buildPublishExportRequest,
9
+ downloadPublishArtifactFile,
10
+ PUBLISH_ACCESS_OPTIONS,
11
+ type PublishExportResponse,
12
+ } from "../lib/publish-export";
13
+ import { Button } from "./ui/button";
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ } from "./ui/dialog";
22
+ import { Input } from "./ui/input";
23
+
24
+ interface PublishExportDialogProps {
25
+ onClose: () => void;
26
+ target: string;
27
+ title: string;
28
+ }
29
+
30
+ /** Mount a fresh dialog for each export so access and secrets never carry over. */
31
+ export function PublishExportDialog({
32
+ onClose,
33
+ target,
34
+ title,
35
+ }: PublishExportDialogProps) {
36
+ const [visibility, setVisibility] = useState<PublishVisibility | null>(null);
37
+ const [passphrase, setPassphrase] = useState("");
38
+ const [passphraseConfirmation, setPassphraseConfirmation] = useState("");
39
+ const [audienceConfirmed, setAudienceConfirmed] = useState(false);
40
+ const [error, setError] = useState<string | null>(null);
41
+ const [busy, setBusy] = useState(false);
42
+ const exporting = useRef(false);
43
+ const [returnFocus] = useState(() => {
44
+ const active = document.activeElement;
45
+ const menuTriggerId = active
46
+ ?.closest('[role="menu"]')
47
+ ?.getAttribute("aria-labelledby");
48
+ const trigger = menuTriggerId
49
+ ? document.getElementById(menuTriggerId)
50
+ : active;
51
+ return trigger instanceof HTMLElement ? trigger : null;
52
+ });
53
+ const selected = PUBLISH_ACCESS_OPTIONS.find(
54
+ ({ value }) => value === visibility
55
+ );
56
+ const encryptionReady =
57
+ visibility !== "encrypted" ||
58
+ (passphrase.trim().length > 0 && passphrase === passphraseConfirmation);
59
+
60
+ const handleExport = async () => {
61
+ if (exporting.current) return;
62
+ setError(null);
63
+ try {
64
+ const body = buildPublishExportRequest({
65
+ target,
66
+ visibility,
67
+ passphrase,
68
+ passphraseConfirmation,
69
+ audienceConfirmed,
70
+ });
71
+ exporting.current = true;
72
+ setBusy(true);
73
+ const result = await apiFetch<PublishExportResponse>(
74
+ "/api/publish/export",
75
+ {
76
+ method: "POST",
77
+ body: JSON.stringify(body),
78
+ }
79
+ );
80
+ if (result.error || !result.data) {
81
+ setError(
82
+ result.error ?? "The export did not return an artifact. Try again."
83
+ );
84
+ return;
85
+ }
86
+ // Refuse a mismatched response rather than downloading a broader artifact.
87
+ if (
88
+ result.data.artifact.spaces.length !== 1 ||
89
+ result.data.artifact.spaces[0]?.visibility !== visibility ||
90
+ result.data.artifact.version !== (visibility === "encrypted" ? 2 : 1)
91
+ ) {
92
+ setError(
93
+ "The returned artifact does not match the reviewed access. Nothing was downloaded."
94
+ );
95
+ return;
96
+ }
97
+ downloadPublishArtifactFile(result.data);
98
+ setPassphrase("");
99
+ setPassphraseConfirmation("");
100
+ onClose();
101
+ } catch (cause) {
102
+ setError(
103
+ cause instanceof Error ? cause.message : "Failed to export. Try again."
104
+ );
105
+ } finally {
106
+ exporting.current = false;
107
+ setBusy(false);
108
+ }
109
+ };
110
+
111
+ return (
112
+ <Dialog
113
+ open
114
+ onOpenChange={(open) => {
115
+ if (!open && !exporting.current) onClose();
116
+ }}
117
+ >
118
+ <DialogContent
119
+ className="publish-export-dialog bg-card"
120
+ onCloseAutoFocus={(event) => {
121
+ if (returnFocus?.isConnected) {
122
+ event.preventDefault();
123
+ returnFocus.focus();
124
+ }
125
+ }}
126
+ showCloseButton={!busy}
127
+ >
128
+ <DialogHeader className="publish-export-dialog-header">
129
+ <DialogTitle>Export for gno.sh</DialogTitle>
130
+ <DialogDescription>
131
+ Review who can read “{title}”. This downloads a local file; upload
132
+ and publish it separately in Studio.
133
+ </DialogDescription>
134
+ </DialogHeader>
135
+ <div className="publish-export-dialog-body">
136
+ <fieldset className="space-y-2" disabled={busy}>
137
+ <legend className="mb-2 font-medium text-sm">
138
+ Who can read this?
139
+ </legend>
140
+ {PUBLISH_ACCESS_OPTIONS.map((option) => (
141
+ <label
142
+ className="flex cursor-pointer items-start gap-3 rounded-md border border-border p-3 transition-colors hover:bg-muted/40 has-[:checked]:border-primary has-[:checked]:bg-primary/10 focus-within:ring-2 focus-within:ring-primary/50"
143
+ key={option.value}
144
+ >
145
+ <input
146
+ checked={visibility === option.value}
147
+ className="mt-1 accent-primary"
148
+ name="publish-access"
149
+ onChange={() => {
150
+ setVisibility(option.value);
151
+ setAudienceConfirmed(false);
152
+ setPassphrase("");
153
+ setPassphraseConfirmation("");
154
+ setError(null);
155
+ }}
156
+ type="radio"
157
+ value={option.value}
158
+ />
159
+ <span className="min-w-0 space-y-1">
160
+ <span className="block font-medium text-sm">
161
+ {option.label}
162
+ </span>
163
+ <span className="block text-muted-foreground text-sm">
164
+ {option.audience}
165
+ </span>
166
+ <span className="block font-mono text-muted-foreground text-xs">
167
+ {option.availability}
168
+ </span>
169
+ </span>
170
+ </label>
171
+ ))}
172
+ </fieldset>
173
+ <p className="text-muted-foreground text-xs">
174
+ Local GNO cannot check your hosted account. Studio checks plan
175
+ availability and Egress Policy before publishing. See{" "}
176
+ <a
177
+ className="text-primary underline"
178
+ href="https://gno.sh/pricing"
179
+ rel="noopener noreferrer"
180
+ target="_blank"
181
+ >
182
+ current plans
183
+ </a>
184
+ .
185
+ </p>
186
+ {visibility === "encrypted" && (
187
+ <fieldset className="space-y-3" disabled={busy}>
188
+ <legend className="mb-2 font-medium text-sm">
189
+ Local encryption
190
+ </legend>
191
+ <p className="text-muted-foreground text-sm">
192
+ Your local GNO server encrypts the notes and bundled assets. The
193
+ passphrase is never sent to gno.sh or included in the downloaded
194
+ file. Keep it safe: lost passphrases cannot be recovered.
195
+ Switching into or out of encrypted sharing requires a new local
196
+ export.
197
+ </p>
198
+ <label className="block space-y-1 text-sm">
199
+ <span>Passphrase</span>
200
+ <Input
201
+ autoComplete="new-password"
202
+ onChange={(event) => setPassphrase(event.target.value)}
203
+ type="password"
204
+ value={passphrase}
205
+ />
206
+ </label>
207
+ <label className="block space-y-1 text-sm">
208
+ <span>Confirm passphrase</span>
209
+ <Input
210
+ autoComplete="new-password"
211
+ onChange={(event) =>
212
+ setPassphraseConfirmation(event.target.value)
213
+ }
214
+ type="password"
215
+ value={passphraseConfirmation}
216
+ />
217
+ </label>
218
+ {passphraseConfirmation &&
219
+ passphrase !== passphraseConfirmation && (
220
+ <p className="text-destructive text-sm" role="status">
221
+ The passphrases do not match.
222
+ </p>
223
+ )}
224
+ </fieldset>
225
+ )}
226
+ {selected && (
227
+ <label className="flex cursor-pointer items-start gap-2 text-sm">
228
+ <input
229
+ checked={audienceConfirmed}
230
+ className="mt-1 accent-primary"
231
+ disabled={busy}
232
+ onChange={(event) => setAudienceConfirmed(event.target.checked)}
233
+ type="checkbox"
234
+ />
235
+ <span>
236
+ I reviewed the {selected.label.toLowerCase()} audience and want
237
+ to export with this access.
238
+ </span>
239
+ </label>
240
+ )}
241
+ {error && (
242
+ <p className="text-destructive text-sm" role="alert">
243
+ {error}
244
+ </p>
245
+ )}
246
+ </div>
247
+ <DialogFooter className="publish-export-dialog-footer">
248
+ <Button disabled={busy} onClick={onClose} variant="outline">
249
+ Cancel
250
+ </Button>
251
+ <Button
252
+ disabled={
253
+ busy || !selected || !audienceConfirmed || !encryptionReady
254
+ }
255
+ onClick={() => {
256
+ void handleExport();
257
+ }}
258
+ >
259
+ {busy && <Loader2Icon className="mr-2 size-4 animate-spin" />}
260
+ {busy ? "Exporting…" : "Download export"}
261
+ </Button>
262
+ </DialogFooter>
263
+ </DialogContent>
264
+ </Dialog>
265
+ );
266
+ }