@gmickel/gno 0.34.1 → 0.36.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 +37 -26
- package/assets/screenshots/webui-collections.jpg +0 -0
- package/assets/screenshots/webui-graph.jpg +0 -0
- package/package.json +1 -1
- package/src/core/file-ops.ts +12 -1
- package/src/core/file-refactors.ts +180 -0
- package/src/core/note-creation.ts +137 -0
- package/src/core/note-presets.ts +183 -0
- package/src/core/sections.ts +62 -0
- package/src/core/validation.ts +4 -3
- package/src/mcp/tools/capture.ts +71 -12
- package/src/mcp/tools/index.ts +82 -1
- package/src/mcp/tools/workspace-write.ts +321 -0
- package/src/sdk/client.ts +341 -0
- package/src/sdk/types.ts +67 -0
- package/src/serve/CLAUDE.md +3 -1
- package/src/serve/browse-tree.ts +60 -12
- package/src/serve/public/app.tsx +8 -3
- package/src/serve/public/components/BrowseDetailPane.tsx +18 -1
- package/src/serve/public/components/CaptureModal.tsx +135 -13
- package/src/serve/public/components/QuickSwitcher.tsx +408 -46
- package/src/serve/public/components/ShortcutHelpModal.tsx +54 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +58 -26
- package/src/serve/public/components/ui/command.tsx +19 -9
- package/src/serve/public/components/ui/dialog.tsx +1 -1
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/globals.css +47 -0
- package/src/serve/public/hooks/useCaptureModal.tsx +31 -5
- package/src/serve/public/lib/browse.ts +1 -0
- package/src/serve/public/lib/workspace-actions.ts +226 -0
- package/src/serve/public/lib/workspace-events.ts +39 -0
- package/src/serve/public/pages/Browse.tsx +154 -3
- package/src/serve/public/pages/DocView.tsx +472 -10
- package/src/serve/public/pages/DocumentEditor.tsx +52 -0
- package/src/serve/routes/api.ts +712 -13
- package/src/serve/server.ts +74 -0
- package/src/store/sqlite/adapter.ts +19 -19
|
@@ -19,6 +19,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
19
19
|
|
|
20
20
|
import type { WikiLinkDoc } from "./WikiLinkAutocomplete";
|
|
21
21
|
|
|
22
|
+
import {
|
|
23
|
+
getNotePreset,
|
|
24
|
+
NOTE_PRESETS,
|
|
25
|
+
resolveNotePreset,
|
|
26
|
+
} from "../../../core/note-presets";
|
|
22
27
|
import { apiFetch } from "../hooks/use-api";
|
|
23
28
|
import { getActiveWikiLinkQuery } from "../lib/wiki-link";
|
|
24
29
|
import { IndexingProgress } from "./IndexingProgress";
|
|
@@ -48,6 +53,12 @@ export interface CaptureModalProps {
|
|
|
48
53
|
open: boolean;
|
|
49
54
|
/** Prefill title when opening from another surface */
|
|
50
55
|
draftTitle?: string;
|
|
56
|
+
/** Default collection from current workspace context */
|
|
57
|
+
defaultCollection?: string;
|
|
58
|
+
/** Default folder path from current workspace context */
|
|
59
|
+
defaultFolderPath?: string;
|
|
60
|
+
/** Optional preset id */
|
|
61
|
+
presetId?: string;
|
|
51
62
|
/** Callback when open state changes */
|
|
52
63
|
onOpenChange: (open: boolean) => void;
|
|
53
64
|
/** Callback when document created successfully */
|
|
@@ -62,8 +73,11 @@ interface Collection {
|
|
|
62
73
|
interface CreateDocResponse {
|
|
63
74
|
uri: string;
|
|
64
75
|
path: string;
|
|
65
|
-
jobId: string;
|
|
76
|
+
jobId: string | null;
|
|
66
77
|
note: string;
|
|
78
|
+
openedExisting?: boolean;
|
|
79
|
+
created?: boolean;
|
|
80
|
+
relPath?: string;
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
interface CollectionsResponse {
|
|
@@ -91,8 +105,11 @@ type ModalState = "form" | "submitting" | "success" | "error";
|
|
|
91
105
|
export function CaptureModal({
|
|
92
106
|
open,
|
|
93
107
|
draftTitle = "",
|
|
108
|
+
defaultCollection = "",
|
|
109
|
+
defaultFolderPath = "",
|
|
94
110
|
onOpenChange,
|
|
95
111
|
onSuccess,
|
|
112
|
+
presetId = "",
|
|
96
113
|
}: CaptureModalProps) {
|
|
97
114
|
// Form state
|
|
98
115
|
const [title, setTitle] = useState("");
|
|
@@ -100,6 +117,9 @@ export function CaptureModal({
|
|
|
100
117
|
const [collection, setCollection] = useState("");
|
|
101
118
|
const [collections, setCollections] = useState<Collection[]>([]);
|
|
102
119
|
const [tags, setTags] = useState<string[]>([]);
|
|
120
|
+
const [selectedPresetId, setSelectedPresetId] = useState<string>(presetId);
|
|
121
|
+
const [contentTouched, setContentTouched] = useState(false);
|
|
122
|
+
const [lastGeneratedContent, setLastGeneratedContent] = useState("");
|
|
103
123
|
const [wikiLinkDocs, setWikiLinkDocs] = useState<WikiLinkDoc[]>([]);
|
|
104
124
|
const [wikiLinkOpen, setWikiLinkOpen] = useState(false);
|
|
105
125
|
const [wikiLinkQuery, setWikiLinkQuery] = useState("");
|
|
@@ -123,6 +143,7 @@ export function CaptureModal({
|
|
|
123
143
|
if (draftTitle.trim()) {
|
|
124
144
|
setTitle(draftTitle);
|
|
125
145
|
}
|
|
146
|
+
setSelectedPresetId(presetId || "blank");
|
|
126
147
|
|
|
127
148
|
void apiFetch<CollectionsResponse>("/api/status").then(({ data }) => {
|
|
128
149
|
if (data?.collections) {
|
|
@@ -130,9 +151,17 @@ export function CaptureModal({
|
|
|
130
151
|
data.collections.map((c) => ({ name: c.name, path: c.path }))
|
|
131
152
|
);
|
|
132
153
|
|
|
133
|
-
|
|
154
|
+
const requestedCollection = defaultCollection.trim();
|
|
134
155
|
const lastUsed = localStorage.getItem(STORAGE_KEY);
|
|
135
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
requestedCollection &&
|
|
158
|
+
data.collections.some((c) => c.name === requestedCollection)
|
|
159
|
+
) {
|
|
160
|
+
setCollection(requestedCollection);
|
|
161
|
+
} else if (
|
|
162
|
+
lastUsed &&
|
|
163
|
+
data.collections.some((c) => c.name === lastUsed)
|
|
164
|
+
) {
|
|
136
165
|
setCollection(lastUsed);
|
|
137
166
|
} else {
|
|
138
167
|
const firstCollection = data.collections.at(0);
|
|
@@ -142,7 +171,7 @@ export function CaptureModal({
|
|
|
142
171
|
}
|
|
143
172
|
}
|
|
144
173
|
});
|
|
145
|
-
}, [draftTitle, open]);
|
|
174
|
+
}, [defaultCollection, draftTitle, open, presetId]);
|
|
146
175
|
|
|
147
176
|
// Reset form when modal closes
|
|
148
177
|
useEffect(() => {
|
|
@@ -152,6 +181,9 @@ export function CaptureModal({
|
|
|
152
181
|
setTitle("");
|
|
153
182
|
setContent("");
|
|
154
183
|
setTags([]);
|
|
184
|
+
setContentTouched(false);
|
|
185
|
+
setLastGeneratedContent("");
|
|
186
|
+
setSelectedPresetId("blank");
|
|
155
187
|
setState("form");
|
|
156
188
|
setError(null);
|
|
157
189
|
setJobId(null);
|
|
@@ -168,6 +200,44 @@ export function CaptureModal({
|
|
|
168
200
|
// Validate form
|
|
169
201
|
const isValid = title.trim() && content.trim() && collection;
|
|
170
202
|
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!open) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const resolved = resolveNotePreset({
|
|
209
|
+
presetId: selectedPresetId || "blank",
|
|
210
|
+
title: title.trim() || draftTitle.trim() || "Untitled",
|
|
211
|
+
tags,
|
|
212
|
+
});
|
|
213
|
+
const generatedContent = resolved?.content ?? "";
|
|
214
|
+
const shouldApply =
|
|
215
|
+
!contentTouched || !content.trim() || content === lastGeneratedContent;
|
|
216
|
+
|
|
217
|
+
setLastGeneratedContent(generatedContent);
|
|
218
|
+
if (shouldApply) {
|
|
219
|
+
setContent(generatedContent);
|
|
220
|
+
if (resolved?.tags) {
|
|
221
|
+
const nextTags = resolved.tags;
|
|
222
|
+
const sameTags =
|
|
223
|
+
nextTags.length === tags.length &&
|
|
224
|
+
nextTags.every((tag, index) => tags[index] === tag);
|
|
225
|
+
if (!sameTags) {
|
|
226
|
+
setTags(nextTags);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}, [
|
|
231
|
+
content,
|
|
232
|
+
contentTouched,
|
|
233
|
+
draftTitle,
|
|
234
|
+
lastGeneratedContent,
|
|
235
|
+
open,
|
|
236
|
+
selectedPresetId,
|
|
237
|
+
tags,
|
|
238
|
+
title,
|
|
239
|
+
]);
|
|
240
|
+
|
|
171
241
|
// Submit form
|
|
172
242
|
const handleSubmit = useCallback(async () => {
|
|
173
243
|
if (!isValid) return;
|
|
@@ -175,9 +245,6 @@ export function CaptureModal({
|
|
|
175
245
|
setState("submitting");
|
|
176
246
|
setError(null);
|
|
177
247
|
|
|
178
|
-
const filename = sanitizeFilename(title) || "untitled";
|
|
179
|
-
const relPath = `${filename}.md`;
|
|
180
|
-
|
|
181
248
|
// Include tags in the POST request (server writes to frontmatter)
|
|
182
249
|
const { data, error: err } = await apiFetch<CreateDocResponse>(
|
|
183
250
|
"/api/docs",
|
|
@@ -185,8 +252,11 @@ export function CaptureModal({
|
|
|
185
252
|
method: "POST",
|
|
186
253
|
body: JSON.stringify({
|
|
187
254
|
collection,
|
|
188
|
-
|
|
255
|
+
title,
|
|
256
|
+
folderPath: defaultFolderPath || undefined,
|
|
189
257
|
content,
|
|
258
|
+
presetId: selectedPresetId || undefined,
|
|
259
|
+
collisionPolicy: "create_with_suffix",
|
|
190
260
|
...(tags.length > 0 && { tags }),
|
|
191
261
|
}),
|
|
192
262
|
}
|
|
@@ -207,7 +277,16 @@ export function CaptureModal({
|
|
|
207
277
|
setCreatedUri(data.uri);
|
|
208
278
|
onSuccess?.(data.uri);
|
|
209
279
|
}
|
|
210
|
-
}, [
|
|
280
|
+
}, [
|
|
281
|
+
collection,
|
|
282
|
+
content,
|
|
283
|
+
defaultFolderPath,
|
|
284
|
+
isValid,
|
|
285
|
+
onSuccess,
|
|
286
|
+
selectedPresetId,
|
|
287
|
+
tags,
|
|
288
|
+
title,
|
|
289
|
+
]);
|
|
211
290
|
|
|
212
291
|
// Handle keyboard submit
|
|
213
292
|
const handleKeyDown = useCallback(
|
|
@@ -249,16 +328,16 @@ export function CaptureModal({
|
|
|
249
328
|
|
|
250
329
|
const handleCreateLinkedNote = useCallback(
|
|
251
330
|
async (linkedTitle: string) => {
|
|
252
|
-
const filename = sanitizeFilename(linkedTitle) || "untitled";
|
|
253
|
-
const relPath = `${filename}.md`;
|
|
254
331
|
const { data, error: err } = await apiFetch<CreateDocResponse>(
|
|
255
332
|
"/api/docs",
|
|
256
333
|
{
|
|
257
334
|
method: "POST",
|
|
258
335
|
body: JSON.stringify({
|
|
259
336
|
collection,
|
|
260
|
-
|
|
337
|
+
title: linkedTitle,
|
|
338
|
+
folderPath: defaultFolderPath || undefined,
|
|
261
339
|
content: `# ${linkedTitle}\n`,
|
|
340
|
+
collisionPolicy: "open_existing",
|
|
262
341
|
}),
|
|
263
342
|
}
|
|
264
343
|
);
|
|
@@ -279,10 +358,11 @@ export function CaptureModal({
|
|
|
279
358
|
]);
|
|
280
359
|
}
|
|
281
360
|
},
|
|
282
|
-
[collection, insertWikiLink]
|
|
361
|
+
[collection, defaultFolderPath, insertWikiLink]
|
|
283
362
|
);
|
|
284
363
|
|
|
285
364
|
const handleContentInput = useCallback((nextContent: string) => {
|
|
365
|
+
setContentTouched(true);
|
|
286
366
|
setContent(nextContent);
|
|
287
367
|
const cursorPos = contentRef.current?.selectionStart ?? nextContent.length;
|
|
288
368
|
const activeQuery = getActiveWikiLinkQuery(nextContent, cursorPos);
|
|
@@ -414,6 +494,48 @@ export function CaptureModal({
|
|
|
414
494
|
)}
|
|
415
495
|
</div>
|
|
416
496
|
|
|
497
|
+
{/* Preset */}
|
|
498
|
+
<div>
|
|
499
|
+
<label
|
|
500
|
+
className="mb-1.5 block font-medium text-sm"
|
|
501
|
+
htmlFor="capture-preset"
|
|
502
|
+
>
|
|
503
|
+
Preset
|
|
504
|
+
</label>
|
|
505
|
+
<Select
|
|
506
|
+
disabled={state === "submitting"}
|
|
507
|
+
onValueChange={setSelectedPresetId}
|
|
508
|
+
value={selectedPresetId}
|
|
509
|
+
>
|
|
510
|
+
<SelectTrigger className="w-full" id="capture-preset">
|
|
511
|
+
<SelectValue placeholder="Select a preset" />
|
|
512
|
+
</SelectTrigger>
|
|
513
|
+
<SelectContent>
|
|
514
|
+
{NOTE_PRESETS.map((preset) => (
|
|
515
|
+
<SelectItem key={preset.id} value={preset.id}>
|
|
516
|
+
{preset.label}
|
|
517
|
+
</SelectItem>
|
|
518
|
+
))}
|
|
519
|
+
</SelectContent>
|
|
520
|
+
</Select>
|
|
521
|
+
{getNotePreset(selectedPresetId)?.description && (
|
|
522
|
+
<p className="mt-1 text-muted-foreground text-xs">
|
|
523
|
+
{getNotePreset(selectedPresetId)?.description}
|
|
524
|
+
</p>
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
{defaultFolderPath && (
|
|
529
|
+
<div className="rounded-lg border border-border/50 bg-muted/20 p-3">
|
|
530
|
+
<div className="mb-1 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.14em]">
|
|
531
|
+
Target location
|
|
532
|
+
</div>
|
|
533
|
+
<div className="font-mono text-xs text-muted-foreground">
|
|
534
|
+
{collection || defaultCollection} / {defaultFolderPath}
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
|
|
417
539
|
{/* Tags */}
|
|
418
540
|
<div>
|
|
419
541
|
<span className="mb-1.5 block font-medium text-sm">
|