@gmickel/gno 0.34.0 → 0.35.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 +1 -0
- package/src/serve/public/components/CaptureModal.tsx +135 -13
- package/src/serve/public/components/QuickSwitcher.tsx +228 -4
- package/src/serve/public/components/ShortcutHelpModal.tsx +54 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +58 -26
- package/src/serve/public/hooks/useCaptureModal.tsx +31 -5
- 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 +439 -0
- package/src/serve/public/pages/DocumentEditor.tsx +52 -0
- package/src/serve/routes/api.ts +712 -13
- package/src/serve/server.ts +74 -0
|
@@ -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">
|
|
@@ -8,8 +8,12 @@ import {
|
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
10
10
|
|
|
11
|
+
import type { DocumentSection } from "../../../core/sections";
|
|
12
|
+
import type { CaptureModalOpenOptions } from "../hooks/useCaptureModal";
|
|
13
|
+
|
|
14
|
+
import { NOTE_PRESETS } from "../../../core/note-presets";
|
|
11
15
|
import { apiFetch } from "../hooks/use-api";
|
|
12
|
-
import { buildDocDeepLink } from "../lib/deep-links";
|
|
16
|
+
import { buildDocDeepLink, parseDocumentDeepLink } from "../lib/deep-links";
|
|
13
17
|
import {
|
|
14
18
|
loadFavoriteCollections,
|
|
15
19
|
loadFavoriteDocuments,
|
|
@@ -18,6 +22,10 @@ import {
|
|
|
18
22
|
type FavoriteDoc,
|
|
19
23
|
type RecentDoc,
|
|
20
24
|
} from "../lib/navigation-state";
|
|
25
|
+
import {
|
|
26
|
+
getWorkspaceActions,
|
|
27
|
+
runWorkspaceAction,
|
|
28
|
+
} from "../lib/workspace-actions";
|
|
21
29
|
import {
|
|
22
30
|
CommandDialog,
|
|
23
31
|
CommandEmpty,
|
|
@@ -45,14 +53,25 @@ interface SearchResponse {
|
|
|
45
53
|
results: SearchResult[];
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
interface DocLookupResponse {
|
|
57
|
+
docid: string;
|
|
58
|
+
uri: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SectionsResponse {
|
|
62
|
+
sections: DocumentSection[];
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
export interface QuickSwitcherProps {
|
|
66
|
+
location: string;
|
|
49
67
|
open: boolean;
|
|
50
68
|
onOpenChange: (open: boolean) => void;
|
|
51
69
|
navigate: (to: string) => void;
|
|
52
|
-
onCreateNote: (
|
|
70
|
+
onCreateNote: (options?: string | CaptureModalOpenOptions) => void;
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
export function QuickSwitcher({
|
|
74
|
+
location,
|
|
56
75
|
open,
|
|
57
76
|
onOpenChange,
|
|
58
77
|
navigate,
|
|
@@ -65,6 +84,7 @@ export function QuickSwitcher({
|
|
|
65
84
|
const [favoriteCollections, setFavoriteCollections] = useState<
|
|
66
85
|
FavoriteCollection[]
|
|
67
86
|
>([]);
|
|
87
|
+
const [sections, setSections] = useState<DocumentSection[]>([]);
|
|
68
88
|
const [loading, setLoading] = useState(false);
|
|
69
89
|
const requestIdRef = useRef(0);
|
|
70
90
|
|
|
@@ -80,6 +100,35 @@ export function QuickSwitcher({
|
|
|
80
100
|
setFavoriteCollections(loadFavoriteCollections());
|
|
81
101
|
}, [open]);
|
|
82
102
|
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!open || !location.startsWith("/doc?")) {
|
|
105
|
+
setSections([]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const target = parseDocumentDeepLink(
|
|
110
|
+
location.includes("?") ? `?${location.split("?")[1] ?? ""}` : ""
|
|
111
|
+
);
|
|
112
|
+
if (!target.uri) {
|
|
113
|
+
setSections([]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
void apiFetch<DocLookupResponse>(
|
|
118
|
+
`/api/doc?uri=${encodeURIComponent(target.uri)}`
|
|
119
|
+
).then(({ data }) => {
|
|
120
|
+
if (!data?.docid) {
|
|
121
|
+
setSections([]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
void apiFetch<SectionsResponse>(
|
|
125
|
+
`/api/doc/${encodeURIComponent(data.docid)}/sections`
|
|
126
|
+
).then(({ data: sectionData }) => {
|
|
127
|
+
setSections(sectionData?.sections ?? []);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}, [location, open]);
|
|
131
|
+
|
|
83
132
|
useEffect(() => {
|
|
84
133
|
if (!open) return;
|
|
85
134
|
if (!query.trim()) {
|
|
@@ -118,6 +167,31 @@ export function QuickSwitcher({
|
|
|
118
167
|
() => favoriteCollections.slice(0, 6),
|
|
119
168
|
[favoriteCollections]
|
|
120
169
|
);
|
|
170
|
+
const workspaceActions = useMemo(
|
|
171
|
+
() => getWorkspaceActions({ location }),
|
|
172
|
+
[location]
|
|
173
|
+
);
|
|
174
|
+
const exactResult = useMemo(() => {
|
|
175
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
176
|
+
if (!normalizedQuery) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
results.find((result) => {
|
|
182
|
+
const normalizedTitle = (result.title ?? "").trim().toLowerCase();
|
|
183
|
+
const normalizedLeaf = decodeURIComponent(
|
|
184
|
+
result.uri.split("/").pop() ?? ""
|
|
185
|
+
)
|
|
186
|
+
.replace(/\.[^.]+$/u, "")
|
|
187
|
+
.toLowerCase();
|
|
188
|
+
return (
|
|
189
|
+
normalizedTitle === normalizedQuery ||
|
|
190
|
+
normalizedLeaf === normalizedQuery
|
|
191
|
+
);
|
|
192
|
+
}) ?? null
|
|
193
|
+
);
|
|
194
|
+
}, [query, results]);
|
|
121
195
|
|
|
122
196
|
const openTarget = useCallback(
|
|
123
197
|
(target: { uri: string; lineStart?: number; lineEnd?: number }) => {
|
|
@@ -134,7 +208,32 @@ export function QuickSwitcher({
|
|
|
134
208
|
[navigate, onOpenChange]
|
|
135
209
|
);
|
|
136
210
|
|
|
137
|
-
const showCreateAction =
|
|
211
|
+
const showCreateAction = true;
|
|
212
|
+
const actionHandlers = useMemo(
|
|
213
|
+
() => ({
|
|
214
|
+
navigate,
|
|
215
|
+
openCapture: onCreateNote,
|
|
216
|
+
closePalette: () => onOpenChange(false),
|
|
217
|
+
}),
|
|
218
|
+
[navigate, onCreateNote, onOpenChange]
|
|
219
|
+
);
|
|
220
|
+
const filteredPresetActions = useMemo(() => {
|
|
221
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
222
|
+
return NOTE_PRESETS.filter((preset) => preset.id !== "blank").filter(
|
|
223
|
+
(preset) =>
|
|
224
|
+
!normalizedQuery ||
|
|
225
|
+
preset.label.toLowerCase().includes(normalizedQuery) ||
|
|
226
|
+
preset.description.toLowerCase().includes(normalizedQuery)
|
|
227
|
+
);
|
|
228
|
+
}, [query]);
|
|
229
|
+
const filteredSections = useMemo(() => {
|
|
230
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
231
|
+
return sections.filter(
|
|
232
|
+
(section) =>
|
|
233
|
+
!normalizedQuery ||
|
|
234
|
+
section.title.toLowerCase().includes(normalizedQuery)
|
|
235
|
+
);
|
|
236
|
+
}, [query, sections]);
|
|
138
237
|
|
|
139
238
|
return (
|
|
140
239
|
<CommandDialog
|
|
@@ -214,10 +313,24 @@ export function QuickSwitcher({
|
|
|
214
313
|
{showCreateAction && (
|
|
215
314
|
<>
|
|
216
315
|
<CommandGroup heading="Actions">
|
|
316
|
+
{exactResult && (
|
|
317
|
+
<CommandItem
|
|
318
|
+
onSelect={() =>
|
|
319
|
+
openTarget({
|
|
320
|
+
uri: exactResult.uri,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
value={`open-exact-${query}`}
|
|
324
|
+
>
|
|
325
|
+
<FileTextIcon />
|
|
326
|
+
<span>Open exact match</span>
|
|
327
|
+
<CommandShortcut>Exact</CommandShortcut>
|
|
328
|
+
</CommandItem>
|
|
329
|
+
)}
|
|
217
330
|
<CommandItem
|
|
218
331
|
onSelect={() => {
|
|
219
332
|
onOpenChange(false);
|
|
220
|
-
onCreateNote(query.trim());
|
|
333
|
+
onCreateNote({ draftTitle: query.trim() });
|
|
221
334
|
}}
|
|
222
335
|
value={`create-${query}`}
|
|
223
336
|
>
|
|
@@ -225,11 +338,122 @@ export function QuickSwitcher({
|
|
|
225
338
|
<span>Create new note</span>
|
|
226
339
|
<CommandShortcut>{query.trim()}</CommandShortcut>
|
|
227
340
|
</CommandItem>
|
|
341
|
+
{workspaceActions
|
|
342
|
+
.filter((action) =>
|
|
343
|
+
[
|
|
344
|
+
"new-note-in-context",
|
|
345
|
+
"create-folder-here",
|
|
346
|
+
"rename-current-note",
|
|
347
|
+
"move-current-note",
|
|
348
|
+
"duplicate-current-note",
|
|
349
|
+
].includes(action.id)
|
|
350
|
+
)
|
|
351
|
+
.map((action) => (
|
|
352
|
+
<CommandItem
|
|
353
|
+
disabled={!action.available}
|
|
354
|
+
key={action.id}
|
|
355
|
+
onSelect={() =>
|
|
356
|
+
runWorkspaceAction(
|
|
357
|
+
action,
|
|
358
|
+
{ location },
|
|
359
|
+
actionHandlers,
|
|
360
|
+
query.trim()
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
value={`${action.label} ${action.keywords.join(" ")} ${query}`}
|
|
364
|
+
>
|
|
365
|
+
<FolderIcon />
|
|
366
|
+
<span>{action.label}</span>
|
|
367
|
+
<CommandShortcut>
|
|
368
|
+
{action.id === "new-note-in-context"
|
|
369
|
+
? "Context"
|
|
370
|
+
: "Action"}
|
|
371
|
+
</CommandShortcut>
|
|
372
|
+
</CommandItem>
|
|
373
|
+
))}
|
|
228
374
|
</CommandGroup>
|
|
229
375
|
<CommandSeparator />
|
|
230
376
|
</>
|
|
231
377
|
)}
|
|
232
378
|
|
|
379
|
+
<CommandGroup heading="Go To">
|
|
380
|
+
{workspaceActions
|
|
381
|
+
.filter((action) => action.group === "Go To" && action.available)
|
|
382
|
+
.map((action) => (
|
|
383
|
+
<CommandItem
|
|
384
|
+
key={action.id}
|
|
385
|
+
onSelect={() =>
|
|
386
|
+
runWorkspaceAction(
|
|
387
|
+
action,
|
|
388
|
+
{ location },
|
|
389
|
+
actionHandlers,
|
|
390
|
+
query.trim()
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
value={`${action.label} ${action.keywords.join(" ")}`}
|
|
394
|
+
>
|
|
395
|
+
<FolderIcon />
|
|
396
|
+
<span>{action.label}</span>
|
|
397
|
+
</CommandItem>
|
|
398
|
+
))}
|
|
399
|
+
</CommandGroup>
|
|
400
|
+
|
|
401
|
+
{filteredPresetActions.length > 0 && (
|
|
402
|
+
<CommandGroup heading="Presets">
|
|
403
|
+
{filteredPresetActions.map((preset) => (
|
|
404
|
+
<CommandItem
|
|
405
|
+
key={preset.id}
|
|
406
|
+
onSelect={() => {
|
|
407
|
+
onCreateNote({
|
|
408
|
+
draftTitle: query.trim() || undefined,
|
|
409
|
+
presetId: preset.id,
|
|
410
|
+
});
|
|
411
|
+
onOpenChange(false);
|
|
412
|
+
}}
|
|
413
|
+
value={`${preset.label} ${preset.description} preset`}
|
|
414
|
+
>
|
|
415
|
+
<FilePlusIcon />
|
|
416
|
+
<span>{preset.label}</span>
|
|
417
|
+
<CommandShortcut>Preset</CommandShortcut>
|
|
418
|
+
</CommandItem>
|
|
419
|
+
))}
|
|
420
|
+
</CommandGroup>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{filteredSections.length > 0 && (
|
|
424
|
+
<CommandGroup heading="Sections">
|
|
425
|
+
{filteredSections.map((section) => {
|
|
426
|
+
const target = parseDocumentDeepLink(
|
|
427
|
+
location.includes("?") ? `?${location.split("?")[1] ?? ""}` : ""
|
|
428
|
+
);
|
|
429
|
+
if (!target.uri) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
return (
|
|
433
|
+
<CommandItem
|
|
434
|
+
key={section.anchor}
|
|
435
|
+
onSelect={() => {
|
|
436
|
+
navigate(
|
|
437
|
+
`${buildDocDeepLink({
|
|
438
|
+
uri: target.uri,
|
|
439
|
+
view: "rendered",
|
|
440
|
+
})}#${section.anchor}`
|
|
441
|
+
);
|
|
442
|
+
onOpenChange(false);
|
|
443
|
+
}}
|
|
444
|
+
value={`${section.title} section heading outline`}
|
|
445
|
+
>
|
|
446
|
+
<SearchIcon />
|
|
447
|
+
<span>{section.title}</span>
|
|
448
|
+
<CommandShortcut>{`H${section.level}`}</CommandShortcut>
|
|
449
|
+
</CommandItem>
|
|
450
|
+
);
|
|
451
|
+
})}
|
|
452
|
+
</CommandGroup>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{!query.trim() && <CommandSeparator />}
|
|
456
|
+
|
|
233
457
|
{query.trim() && (
|
|
234
458
|
<CommandGroup heading="Documents">
|
|
235
459
|
{results.map((result) => (
|
|
@@ -29,12 +29,17 @@ interface ShortcutGroup {
|
|
|
29
29
|
shortcuts: ShortcutItem[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
interface CommandExampleGroup {
|
|
33
|
+
title: string;
|
|
34
|
+
commands: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
const shortcutGroups: ShortcutGroup[] = [
|
|
33
38
|
{
|
|
34
39
|
title: "Global",
|
|
35
40
|
shortcuts: [
|
|
36
41
|
{ keys: "N", description: "New note" },
|
|
37
|
-
{ keys: "Cmd+K", description: "
|
|
42
|
+
{ keys: "Cmd+K", description: "Command palette" },
|
|
38
43
|
{ keys: "/", description: "Focus search" },
|
|
39
44
|
{ keys: "T", description: "Cycle search depth" },
|
|
40
45
|
{ keys: "?", description: "Show shortcuts" },
|
|
@@ -59,6 +64,29 @@ const footerShortcut: ShortcutItem = {
|
|
|
59
64
|
description: "Submit form",
|
|
60
65
|
};
|
|
61
66
|
|
|
67
|
+
const commandExamples: CommandExampleGroup[] = [
|
|
68
|
+
{
|
|
69
|
+
title: "Create",
|
|
70
|
+
commands: [
|
|
71
|
+
"new note",
|
|
72
|
+
"new note in current location",
|
|
73
|
+
"create folder here",
|
|
74
|
+
"Project Note",
|
|
75
|
+
"Research Note",
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: "Current Note",
|
|
80
|
+
commands: [
|
|
81
|
+
"rename current note",
|
|
82
|
+
"move current note",
|
|
83
|
+
"duplicate current note",
|
|
84
|
+
"Intro",
|
|
85
|
+
"Details",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
62
90
|
function KeyCombo({ keys }: { keys: string }) {
|
|
63
91
|
const parts = keys.split("+");
|
|
64
92
|
|
|
@@ -202,6 +230,31 @@ export function ShortcutHelpModal({
|
|
|
202
230
|
</div>
|
|
203
231
|
<KeyCombo keys={footerShortcut.keys} />
|
|
204
232
|
</div>
|
|
233
|
+
|
|
234
|
+
<div className="mt-4 space-y-3 rounded-md border border-[hsl(var(--secondary)/0.12)] bg-[hsl(var(--secondary)/0.03)] p-3">
|
|
235
|
+
<div className="font-mono text-[10px] uppercase tracking-[0.15em] text-[hsl(var(--secondary)/0.7)]">
|
|
236
|
+
Command Palette Examples
|
|
237
|
+
</div>
|
|
238
|
+
<div className="grid grid-cols-2 gap-4">
|
|
239
|
+
{commandExamples.map((group) => (
|
|
240
|
+
<div className="space-y-1" key={group.title}>
|
|
241
|
+
<div className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground/60">
|
|
242
|
+
{group.title}
|
|
243
|
+
</div>
|
|
244
|
+
<div className="space-y-1">
|
|
245
|
+
{group.commands.map((command) => (
|
|
246
|
+
<div
|
|
247
|
+
className="rounded px-2 py-1 font-mono text-[11px] text-muted-foreground/80"
|
|
248
|
+
key={command}
|
|
249
|
+
>
|
|
250
|
+
{command}
|
|
251
|
+
</div>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
205
258
|
</DialogContent>
|
|
206
259
|
</Dialog>
|
|
207
260
|
);
|