@gmickel/gno 0.7.0 → 0.8.2

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 (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -50
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/assets/screenshots/webui-ask-answer.png +0 -0
  5. package/assets/screenshots/webui-collections.png +0 -0
  6. package/assets/screenshots/webui-editor.png +0 -0
  7. package/assets/screenshots/webui-home.png +0 -0
  8. package/assets/skill/SKILL.md +12 -12
  9. package/assets/skill/cli-reference.md +59 -57
  10. package/assets/skill/examples.md +8 -7
  11. package/assets/skill/mcp-reference.md +8 -4
  12. package/package.json +32 -25
  13. package/src/app/constants.ts +43 -42
  14. package/src/cli/colors.ts +1 -1
  15. package/src/cli/commands/ask.ts +44 -43
  16. package/src/cli/commands/cleanup.ts +9 -8
  17. package/src/cli/commands/collection/add.ts +12 -12
  18. package/src/cli/commands/collection/index.ts +4 -4
  19. package/src/cli/commands/collection/list.ts +26 -25
  20. package/src/cli/commands/collection/remove.ts +10 -10
  21. package/src/cli/commands/collection/rename.ts +10 -10
  22. package/src/cli/commands/context/add.ts +1 -1
  23. package/src/cli/commands/context/check.ts +17 -17
  24. package/src/cli/commands/context/index.ts +4 -4
  25. package/src/cli/commands/context/list.ts +11 -11
  26. package/src/cli/commands/context/rm.ts +1 -1
  27. package/src/cli/commands/doctor.ts +86 -84
  28. package/src/cli/commands/embed.ts +30 -28
  29. package/src/cli/commands/get.ts +27 -26
  30. package/src/cli/commands/index-cmd.ts +9 -9
  31. package/src/cli/commands/index.ts +16 -16
  32. package/src/cli/commands/init.ts +13 -12
  33. package/src/cli/commands/ls.ts +20 -19
  34. package/src/cli/commands/mcp/config.ts +30 -28
  35. package/src/cli/commands/mcp/index.ts +4 -4
  36. package/src/cli/commands/mcp/install.ts +17 -17
  37. package/src/cli/commands/mcp/paths.ts +133 -133
  38. package/src/cli/commands/mcp/status.ts +21 -21
  39. package/src/cli/commands/mcp/uninstall.ts +13 -13
  40. package/src/cli/commands/mcp.ts +2 -2
  41. package/src/cli/commands/models/clear.ts +12 -11
  42. package/src/cli/commands/models/index.ts +5 -5
  43. package/src/cli/commands/models/list.ts +31 -30
  44. package/src/cli/commands/models/path.ts +1 -1
  45. package/src/cli/commands/models/pull.ts +19 -18
  46. package/src/cli/commands/models/use.ts +4 -4
  47. package/src/cli/commands/multi-get.ts +38 -36
  48. package/src/cli/commands/query.ts +21 -20
  49. package/src/cli/commands/ref-parser.ts +10 -10
  50. package/src/cli/commands/reset.ts +40 -39
  51. package/src/cli/commands/search.ts +14 -13
  52. package/src/cli/commands/serve.ts +4 -4
  53. package/src/cli/commands/shared.ts +11 -10
  54. package/src/cli/commands/skill/index.ts +5 -5
  55. package/src/cli/commands/skill/install.ts +18 -17
  56. package/src/cli/commands/skill/paths-cmd.ts +11 -10
  57. package/src/cli/commands/skill/paths.ts +23 -23
  58. package/src/cli/commands/skill/show.ts +13 -12
  59. package/src/cli/commands/skill/uninstall.ts +16 -15
  60. package/src/cli/commands/status.ts +25 -24
  61. package/src/cli/commands/update.ts +3 -3
  62. package/src/cli/commands/vsearch.ts +17 -16
  63. package/src/cli/context.ts +5 -5
  64. package/src/cli/errors.ts +3 -3
  65. package/src/cli/format/search-results.ts +37 -37
  66. package/src/cli/options.ts +43 -43
  67. package/src/cli/program.ts +455 -459
  68. package/src/cli/progress.ts +1 -1
  69. package/src/cli/run.ts +24 -23
  70. package/src/collection/add.ts +9 -8
  71. package/src/collection/index.ts +3 -3
  72. package/src/collection/remove.ts +7 -6
  73. package/src/collection/types.ts +6 -6
  74. package/src/config/defaults.ts +1 -1
  75. package/src/config/index.ts +5 -5
  76. package/src/config/loader.ts +19 -18
  77. package/src/config/paths.ts +9 -8
  78. package/src/config/saver.ts +14 -13
  79. package/src/config/types.ts +53 -52
  80. package/src/converters/adapters/markitdownTs/adapter.ts +21 -19
  81. package/src/converters/adapters/officeparser/adapter.ts +18 -16
  82. package/src/converters/canonicalize.ts +12 -12
  83. package/src/converters/errors.ts +26 -22
  84. package/src/converters/index.ts +8 -8
  85. package/src/converters/mime.ts +25 -25
  86. package/src/converters/native/markdown.ts +10 -9
  87. package/src/converters/native/plaintext.ts +8 -7
  88. package/src/converters/path.ts +2 -2
  89. package/src/converters/pipeline.ts +11 -10
  90. package/src/converters/registry.ts +8 -8
  91. package/src/converters/types.ts +14 -14
  92. package/src/converters/versions.ts +4 -4
  93. package/src/index.ts +4 -4
  94. package/src/ingestion/chunker.ts +10 -9
  95. package/src/ingestion/index.ts +6 -6
  96. package/src/ingestion/language.ts +62 -62
  97. package/src/ingestion/sync.ts +50 -49
  98. package/src/ingestion/types.ts +10 -10
  99. package/src/ingestion/walker.ts +14 -13
  100. package/src/llm/cache.ts +51 -49
  101. package/src/llm/errors.ts +40 -36
  102. package/src/llm/index.ts +9 -9
  103. package/src/llm/lockfile.ts +6 -6
  104. package/src/llm/nodeLlamaCpp/adapter.ts +13 -12
  105. package/src/llm/nodeLlamaCpp/embedding.ts +9 -8
  106. package/src/llm/nodeLlamaCpp/generation.ts +7 -6
  107. package/src/llm/nodeLlamaCpp/lifecycle.ts +11 -10
  108. package/src/llm/nodeLlamaCpp/rerank.ts +6 -5
  109. package/src/llm/policy.ts +5 -5
  110. package/src/llm/registry.ts +6 -5
  111. package/src/llm/types.ts +2 -2
  112. package/src/mcp/resources/index.ts +15 -13
  113. package/src/mcp/server.ts +25 -23
  114. package/src/mcp/tools/get.ts +25 -23
  115. package/src/mcp/tools/index.ts +32 -29
  116. package/src/mcp/tools/multi-get.ts +34 -32
  117. package/src/mcp/tools/query.ts +29 -27
  118. package/src/mcp/tools/search.ts +14 -12
  119. package/src/mcp/tools/status.ts +12 -11
  120. package/src/mcp/tools/vsearch.ts +26 -24
  121. package/src/pipeline/answer.ts +9 -9
  122. package/src/pipeline/chunk-lookup.ts +1 -1
  123. package/src/pipeline/contextual.ts +4 -4
  124. package/src/pipeline/expansion.ts +23 -21
  125. package/src/pipeline/explain.ts +21 -21
  126. package/src/pipeline/fusion.ts +9 -9
  127. package/src/pipeline/hybrid.ts +41 -42
  128. package/src/pipeline/index.ts +10 -10
  129. package/src/pipeline/query-language.ts +39 -39
  130. package/src/pipeline/rerank.ts +8 -7
  131. package/src/pipeline/search.ts +22 -22
  132. package/src/pipeline/types.ts +8 -8
  133. package/src/pipeline/vsearch.ts +21 -24
  134. package/src/serve/CLAUDE.md +21 -15
  135. package/src/serve/config-sync.ts +9 -8
  136. package/src/serve/context.ts +19 -18
  137. package/src/serve/index.ts +1 -1
  138. package/src/serve/jobs.ts +7 -7
  139. package/src/serve/public/app.tsx +79 -25
  140. package/src/serve/public/components/AddCollectionDialog.tsx +382 -0
  141. package/src/serve/public/components/CaptureButton.tsx +60 -0
  142. package/src/serve/public/components/CaptureModal.tsx +365 -0
  143. package/src/serve/public/components/IndexingProgress.tsx +333 -0
  144. package/src/serve/public/components/ShortcutHelpModal.tsx +106 -0
  145. package/src/serve/public/components/ai-elements/code-block.tsx +42 -32
  146. package/src/serve/public/components/ai-elements/conversation.tsx +16 -14
  147. package/src/serve/public/components/ai-elements/inline-citation.tsx +33 -32
  148. package/src/serve/public/components/ai-elements/loader.tsx +5 -4
  149. package/src/serve/public/components/ai-elements/message.tsx +39 -37
  150. package/src/serve/public/components/ai-elements/prompt-input.tsx +97 -95
  151. package/src/serve/public/components/ai-elements/sources.tsx +12 -10
  152. package/src/serve/public/components/ai-elements/suggestion.tsx +10 -9
  153. package/src/serve/public/components/editor/CodeMirrorEditor.tsx +142 -0
  154. package/src/serve/public/components/editor/MarkdownPreview.tsx +311 -0
  155. package/src/serve/public/components/editor/index.ts +6 -0
  156. package/src/serve/public/components/preset-selector.tsx +29 -28
  157. package/src/serve/public/components/ui/badge.tsx +13 -12
  158. package/src/serve/public/components/ui/button-group.tsx +13 -12
  159. package/src/serve/public/components/ui/button.tsx +23 -22
  160. package/src/serve/public/components/ui/card.tsx +16 -16
  161. package/src/serve/public/components/ui/carousel.tsx +36 -35
  162. package/src/serve/public/components/ui/collapsible.tsx +1 -1
  163. package/src/serve/public/components/ui/command.tsx +17 -15
  164. package/src/serve/public/components/ui/dialog.tsx +13 -12
  165. package/src/serve/public/components/ui/dropdown-menu.tsx +13 -12
  166. package/src/serve/public/components/ui/hover-card.tsx +6 -5
  167. package/src/serve/public/components/ui/input-group.tsx +45 -43
  168. package/src/serve/public/components/ui/input.tsx +6 -6
  169. package/src/serve/public/components/ui/progress.tsx +5 -4
  170. package/src/serve/public/components/ui/scroll-area.tsx +11 -10
  171. package/src/serve/public/components/ui/select.tsx +19 -18
  172. package/src/serve/public/components/ui/separator.tsx +6 -5
  173. package/src/serve/public/components/ui/table.tsx +18 -18
  174. package/src/serve/public/components/ui/textarea.tsx +4 -4
  175. package/src/serve/public/components/ui/tooltip.tsx +5 -4
  176. package/src/serve/public/globals.css +27 -4
  177. package/src/serve/public/hooks/use-api.ts +8 -8
  178. package/src/serve/public/hooks/useCaptureModal.tsx +83 -0
  179. package/src/serve/public/hooks/useKeyboardShortcuts.ts +85 -0
  180. package/src/serve/public/index.html +4 -4
  181. package/src/serve/public/lib/utils.ts +6 -0
  182. package/src/serve/public/pages/Ask.tsx +27 -26
  183. package/src/serve/public/pages/Browse.tsx +28 -27
  184. package/src/serve/public/pages/Collections.tsx +439 -0
  185. package/src/serve/public/pages/Dashboard.tsx +166 -40
  186. package/src/serve/public/pages/DocView.tsx +258 -73
  187. package/src/serve/public/pages/DocumentEditor.tsx +510 -0
  188. package/src/serve/public/pages/Search.tsx +80 -58
  189. package/src/serve/routes/api.ts +272 -155
  190. package/src/serve/security.ts +4 -4
  191. package/src/serve/server.ts +66 -48
  192. package/src/store/index.ts +5 -5
  193. package/src/store/migrations/001-initial.ts +24 -23
  194. package/src/store/migrations/002-documents-fts.ts +7 -6
  195. package/src/store/migrations/index.ts +4 -4
  196. package/src/store/migrations/runner.ts +17 -15
  197. package/src/store/sqlite/adapter.ts +123 -121
  198. package/src/store/sqlite/fts5-snowball.ts +24 -23
  199. package/src/store/sqlite/index.ts +1 -1
  200. package/src/store/sqlite/setup.ts +12 -12
  201. package/src/store/sqlite/types.ts +4 -4
  202. package/src/store/types.ts +19 -19
  203. package/src/store/vector/index.ts +3 -3
  204. package/src/store/vector/sqlite-vec.ts +23 -20
  205. package/src/store/vector/stats.ts +10 -8
  206. package/src/store/vector/types.ts +2 -2
  207. package/vendor/fts5-snowball/README.md +6 -6
  208. package/assets/screenshots/webui-ask-answer.jpg +0 -0
  209. package/assets/screenshots/webui-home.jpg +0 -0
@@ -0,0 +1,510 @@
1
+ /**
2
+ * DocumentEditor - Split-view markdown editor with auto-save.
3
+ *
4
+ * Features:
5
+ * - CodeMirror 6 for editing
6
+ * - Live markdown preview
7
+ * - Debounced auto-save (2s)
8
+ * - Keyboard shortcuts (Cmd+S to save)
9
+ * - Unsaved changes warning
10
+ */
11
+
12
+ import {
13
+ AlertCircleIcon,
14
+ ArrowLeftIcon,
15
+ CheckIcon,
16
+ CloudIcon,
17
+ EyeIcon,
18
+ EyeOffIcon,
19
+ Loader2Icon,
20
+ PenIcon,
21
+ } from "lucide-react";
22
+ import { useCallback, useEffect, useRef, useState } from "react";
23
+
24
+ import { Loader } from "../components/ai-elements/loader";
25
+ import {
26
+ CodeMirrorEditor,
27
+ type CodeMirrorEditorRef,
28
+ MarkdownPreview,
29
+ } from "../components/editor";
30
+ import { Badge } from "../components/ui/badge";
31
+ import { Button } from "../components/ui/button";
32
+ import {
33
+ Dialog,
34
+ DialogContent,
35
+ DialogDescription,
36
+ DialogFooter,
37
+ DialogHeader,
38
+ DialogTitle,
39
+ } from "../components/ui/dialog";
40
+ import { Separator } from "../components/ui/separator";
41
+ import {
42
+ Tooltip,
43
+ TooltipContent,
44
+ TooltipProvider,
45
+ TooltipTrigger,
46
+ } from "../components/ui/tooltip";
47
+ import { apiFetch } from "../hooks/use-api";
48
+
49
+ interface PageProps {
50
+ navigate: (to: string | number) => void;
51
+ }
52
+
53
+ interface DocData {
54
+ docid: string;
55
+ uri: string;
56
+ title: string | null;
57
+ content: string | null;
58
+ contentAvailable: boolean;
59
+ collection: string;
60
+ relPath: string;
61
+ source: {
62
+ mime: string;
63
+ ext: string;
64
+ modifiedAt?: string;
65
+ sizeBytes?: number;
66
+ };
67
+ }
68
+
69
+ type SaveStatus = "saved" | "saving" | "unsaved" | "error";
70
+
71
+ function useDebouncedCallback<T extends unknown[]>(
72
+ callback: (...args: T) => void | Promise<void>,
73
+ delay: number
74
+ ) {
75
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
76
+
77
+ const debouncedFn = useCallback(
78
+ (...args: T) => {
79
+ if (timeoutRef.current) {
80
+ clearTimeout(timeoutRef.current);
81
+ }
82
+ timeoutRef.current = setTimeout(() => {
83
+ void callback(...args);
84
+ }, delay);
85
+ },
86
+ [callback, delay]
87
+ );
88
+
89
+ const cancel = useCallback(() => {
90
+ if (timeoutRef.current) {
91
+ clearTimeout(timeoutRef.current);
92
+ timeoutRef.current = null;
93
+ }
94
+ }, []);
95
+
96
+ const flush = useCallback(
97
+ (...args: T) => {
98
+ cancel();
99
+ void callback(...args);
100
+ },
101
+ [callback, cancel]
102
+ );
103
+
104
+ useEffect(() => {
105
+ return () => {
106
+ if (timeoutRef.current) {
107
+ clearTimeout(timeoutRef.current);
108
+ }
109
+ };
110
+ }, []);
111
+
112
+ return { debouncedFn, cancel, flush };
113
+ }
114
+
115
+ function formatTime(date: Date): string {
116
+ return date.toLocaleTimeString("en-US", {
117
+ hour: "2-digit",
118
+ minute: "2-digit",
119
+ });
120
+ }
121
+
122
+ export default function DocumentEditor({ navigate }: PageProps) {
123
+ const [doc, setDoc] = useState<DocData | null>(null);
124
+ const [error, setError] = useState<string | null>(null);
125
+ const [loading, setLoading] = useState(true);
126
+
127
+ const [content, setContent] = useState("");
128
+ const [originalContent, setOriginalContent] = useState("");
129
+ const [saveStatus, setSaveStatus] = useState<SaveStatus>("saved");
130
+ const [lastSaved, setLastSaved] = useState<Date | null>(null);
131
+ const [saveError, setSaveError] = useState<string | null>(null);
132
+
133
+ const [showPreview, setShowPreview] = useState(true);
134
+ const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
135
+ const editorRef = useRef<CodeMirrorEditorRef>(null);
136
+
137
+ const hasUnsavedChanges = content !== originalContent;
138
+
139
+ // Save function
140
+ const saveDocument = useCallback(
141
+ async (contentToSave: string) => {
142
+ if (!doc) return;
143
+
144
+ setSaveStatus("saving");
145
+ setSaveError(null);
146
+
147
+ const { error: err } = await apiFetch(
148
+ `/api/docs/${encodeURIComponent(doc.docid)}`,
149
+ {
150
+ method: "PUT",
151
+ body: JSON.stringify({ content: contentToSave }),
152
+ }
153
+ );
154
+
155
+ if (err) {
156
+ setSaveStatus("error");
157
+ setSaveError(err);
158
+ } else {
159
+ setSaveStatus("saved");
160
+ setOriginalContent(contentToSave);
161
+ setLastSaved(new Date());
162
+ }
163
+ },
164
+ [doc]
165
+ );
166
+
167
+ // Debounced auto-save
168
+ const { debouncedFn: debouncedSave, flush: flushSave } = useDebouncedCallback(
169
+ saveDocument,
170
+ 2000
171
+ );
172
+
173
+ // Handle content changes
174
+ const handleContentChange = useCallback(
175
+ (newContent: string) => {
176
+ setContent(newContent);
177
+ if (newContent !== originalContent) {
178
+ setSaveStatus("unsaved");
179
+ debouncedSave(newContent);
180
+ }
181
+ },
182
+ [originalContent, debouncedSave]
183
+ );
184
+
185
+ // Force save (Cmd+S)
186
+ const handleForceSave = useCallback(() => {
187
+ if (hasUnsavedChanges) {
188
+ flushSave(content);
189
+ }
190
+ }, [hasUnsavedChanges, flushSave, content]);
191
+
192
+ // Load document
193
+ useEffect(() => {
194
+ const params = new URLSearchParams(window.location.search);
195
+ const uri = params.get("uri");
196
+
197
+ if (!uri) {
198
+ setError("No document URI provided");
199
+ setLoading(false);
200
+ return;
201
+ }
202
+
203
+ void apiFetch<DocData>(`/api/doc?uri=${encodeURIComponent(uri)}`).then(
204
+ ({ data, error: err }) => {
205
+ setLoading(false);
206
+ if (err) {
207
+ setError(err);
208
+ } else if (data) {
209
+ setDoc(data);
210
+ const docContent = data.content ?? "";
211
+ setContent(docContent);
212
+ setOriginalContent(docContent);
213
+ // Ensure CodeMirror reflects content after async load
214
+ requestAnimationFrame(() => {
215
+ editorRef.current?.setValue(docContent);
216
+ });
217
+ }
218
+ }
219
+ );
220
+ }, []);
221
+
222
+ // Keyboard shortcuts
223
+ useEffect(() => {
224
+ const handleKeyDown = (e: KeyboardEvent) => {
225
+ const isMeta = e.metaKey || e.ctrlKey;
226
+
227
+ // Cmd+S / Ctrl+S - Save
228
+ if (isMeta && e.key === "s") {
229
+ e.preventDefault();
230
+ handleForceSave();
231
+ return;
232
+ }
233
+
234
+ // Editor-specific shortcuts - only when focus is in CodeMirror
235
+ const target = e.target as HTMLElement;
236
+ const inEditor = !!target.closest(".cm-editor");
237
+
238
+ // Cmd+B - Bold (editor only)
239
+ if (isMeta && e.key === "b" && inEditor) {
240
+ e.preventDefault();
241
+ editorRef.current?.wrapSelection("**", "**");
242
+ return;
243
+ }
244
+
245
+ // Cmd+I - Italic (editor only)
246
+ if (isMeta && e.key === "i" && inEditor) {
247
+ e.preventDefault();
248
+ editorRef.current?.wrapSelection("*", "*");
249
+ return;
250
+ }
251
+
252
+ // Cmd+K - Link (editor only)
253
+ if (isMeta && e.key === "k" && inEditor) {
254
+ e.preventDefault();
255
+ editorRef.current?.wrapSelection("[", "](url)");
256
+ return;
257
+ }
258
+
259
+ // Escape - Close (with warning if unsaved)
260
+ if (e.key === "Escape") {
261
+ e.preventDefault();
262
+ if (hasUnsavedChanges) {
263
+ setShowUnsavedDialog(true);
264
+ } else {
265
+ navigate(-1);
266
+ }
267
+ }
268
+ };
269
+
270
+ window.addEventListener("keydown", handleKeyDown);
271
+ return () => window.removeEventListener("keydown", handleKeyDown);
272
+ }, [handleForceSave, hasUnsavedChanges, navigate]);
273
+
274
+ // Warn before leaving with unsaved changes
275
+ useEffect(() => {
276
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
277
+ if (hasUnsavedChanges) {
278
+ e.preventDefault();
279
+ e.returnValue = ""; // Required for modern browsers
280
+ }
281
+ };
282
+
283
+ window.addEventListener("beforeunload", handleBeforeUnload);
284
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
285
+ }, [hasUnsavedChanges]);
286
+
287
+ // Handle close/back
288
+ const handleClose = () => {
289
+ if (hasUnsavedChanges) {
290
+ setShowUnsavedDialog(true);
291
+ } else {
292
+ navigate(-1);
293
+ }
294
+ };
295
+
296
+ // Discard and close
297
+ const handleDiscardAndClose = () => {
298
+ setShowUnsavedDialog(false);
299
+ navigate(-1);
300
+ };
301
+
302
+ // Save and close
303
+ const handleSaveAndClose = async () => {
304
+ await saveDocument(content);
305
+ setShowUnsavedDialog(false);
306
+ navigate(-1);
307
+ };
308
+
309
+ // Save status indicator
310
+ const SaveStatusIndicator = () => {
311
+ const statusConfig = {
312
+ saved: {
313
+ icon: CheckIcon,
314
+ text: lastSaved ? `Saved at ${formatTime(lastSaved)}` : "Saved",
315
+ className: "text-green-500",
316
+ },
317
+ saving: {
318
+ icon: Loader2Icon,
319
+ text: "Saving...",
320
+ className: "text-muted-foreground animate-spin",
321
+ },
322
+ unsaved: {
323
+ icon: CloudIcon,
324
+ text: "Unsaved changes",
325
+ className: "text-yellow-500",
326
+ },
327
+ error: {
328
+ icon: AlertCircleIcon,
329
+ text: saveError ?? "Save failed",
330
+ className: "text-destructive",
331
+ },
332
+ };
333
+
334
+ const { icon: Icon, text, className } = statusConfig[saveStatus];
335
+
336
+ return (
337
+ <TooltipProvider>
338
+ <Tooltip>
339
+ <TooltipTrigger asChild>
340
+ <div className="flex items-center gap-1.5 text-sm">
341
+ <Icon className={`size-4 ${className}`} />
342
+ <span className="hidden text-muted-foreground sm:inline">
343
+ {saveStatus === "saved" && lastSaved
344
+ ? formatTime(lastSaved)
345
+ : text}
346
+ </span>
347
+ </div>
348
+ </TooltipTrigger>
349
+ <TooltipContent>
350
+ <p>{text}</p>
351
+ </TooltipContent>
352
+ </Tooltip>
353
+ </TooltipProvider>
354
+ );
355
+ };
356
+
357
+ // Loading state
358
+ if (loading) {
359
+ return (
360
+ <div className="flex min-h-screen items-center justify-center">
361
+ <div className="flex flex-col items-center gap-4">
362
+ <Loader className="text-primary" size={32} />
363
+ <p className="text-muted-foreground">Loading document...</p>
364
+ </div>
365
+ </div>
366
+ );
367
+ }
368
+
369
+ // Error state
370
+ if (error || !doc) {
371
+ return (
372
+ <div className="flex min-h-screen items-center justify-center">
373
+ <div className="flex flex-col items-center gap-4 text-center">
374
+ <AlertCircleIcon className="size-12 text-destructive" />
375
+ <h2 className="font-semibold text-xl">Failed to load document</h2>
376
+ <p className="text-muted-foreground">{error ?? "Unknown error"}</p>
377
+ <Button onClick={() => navigate(-1)} variant="outline">
378
+ <ArrowLeftIcon className="mr-2 size-4" />
379
+ Go Back
380
+ </Button>
381
+ </div>
382
+ </div>
383
+ );
384
+ }
385
+
386
+ return (
387
+ <div className="flex h-screen flex-col overflow-hidden">
388
+ {/* Toolbar */}
389
+ <header className="glass shrink-0 border-border/50 border-b">
390
+ <div className="flex items-center gap-3 px-4 py-2">
391
+ {/* Back button */}
392
+ <Button
393
+ className="gap-1.5"
394
+ onClick={handleClose}
395
+ size="sm"
396
+ variant="ghost"
397
+ >
398
+ <ArrowLeftIcon className="size-4" />
399
+ <span className="hidden sm:inline">Back</span>
400
+ </Button>
401
+
402
+ <Separator className="h-5" orientation="vertical" />
403
+
404
+ {/* Document info */}
405
+ <div className="flex min-w-0 flex-1 items-center gap-2">
406
+ <PenIcon className="size-4 shrink-0 text-muted-foreground" />
407
+ <h1 className="truncate font-medium">{doc.title || doc.relPath}</h1>
408
+ {hasUnsavedChanges && (
409
+ <Badge
410
+ className="shrink-0 bg-yellow-500/20 text-yellow-500"
411
+ variant="outline"
412
+ >
413
+ Unsaved
414
+ </Badge>
415
+ )}
416
+ </div>
417
+
418
+ {/* Save status */}
419
+ <SaveStatusIndicator />
420
+
421
+ <Separator className="h-5" orientation="vertical" />
422
+
423
+ {/* Preview toggle */}
424
+ <TooltipProvider>
425
+ <Tooltip>
426
+ <TooltipTrigger asChild>
427
+ <Button
428
+ onClick={() => setShowPreview(!showPreview)}
429
+ size="sm"
430
+ variant={showPreview ? "secondary" : "ghost"}
431
+ >
432
+ {showPreview ? (
433
+ <EyeIcon className="size-4" />
434
+ ) : (
435
+ <EyeOffIcon className="size-4" />
436
+ )}
437
+ </Button>
438
+ </TooltipTrigger>
439
+ <TooltipContent>
440
+ <p>{showPreview ? "Hide preview" : "Show preview"}</p>
441
+ </TooltipContent>
442
+ </Tooltip>
443
+ </TooltipProvider>
444
+
445
+ {/* Save button */}
446
+ <Button
447
+ disabled={!hasUnsavedChanges || saveStatus === "saving"}
448
+ onClick={handleForceSave}
449
+ size="sm"
450
+ >
451
+ {saveStatus === "saving" ? (
452
+ <Loader2Icon className="mr-1.5 size-4 animate-spin" />
453
+ ) : (
454
+ <CloudIcon className="mr-1.5 size-4" />
455
+ )}
456
+ Save
457
+ </Button>
458
+ </div>
459
+ </header>
460
+
461
+ {/* Editor area */}
462
+ <div className="flex min-h-0 flex-1">
463
+ {/* Editor pane */}
464
+ <div
465
+ className={`min-h-0 overflow-hidden ${showPreview ? "w-1/2 border-border/30 border-r" : "w-full"}`}
466
+ >
467
+ <CodeMirrorEditor
468
+ className="h-full"
469
+ initialContent={content}
470
+ onChange={handleContentChange}
471
+ ref={editorRef}
472
+ />
473
+ </div>
474
+
475
+ {/* Preview pane */}
476
+ {showPreview && (
477
+ <div className="w-1/2 min-h-0 overflow-auto bg-background p-6">
478
+ <div className="mx-auto max-w-3xl">
479
+ <MarkdownPreview content={content} />
480
+ </div>
481
+ </div>
482
+ )}
483
+ </div>
484
+
485
+ {/* Unsaved changes dialog */}
486
+ <Dialog onOpenChange={setShowUnsavedDialog} open={showUnsavedDialog}>
487
+ <DialogContent>
488
+ <DialogHeader>
489
+ <DialogTitle>Unsaved changes</DialogTitle>
490
+ <DialogDescription>
491
+ You have unsaved changes. What would you like to do?
492
+ </DialogDescription>
493
+ </DialogHeader>
494
+ <DialogFooter className="gap-2 sm:gap-0">
495
+ <Button
496
+ onClick={() => setShowUnsavedDialog(false)}
497
+ variant="outline"
498
+ >
499
+ Cancel
500
+ </Button>
501
+ <Button onClick={handleDiscardAndClose} variant="destructive">
502
+ Discard
503
+ </Button>
504
+ <Button onClick={handleSaveAndClose}>Save & Close</Button>
505
+ </DialogFooter>
506
+ </DialogContent>
507
+ </Dialog>
508
+ </div>
509
+ );
510
+ }