@gmickel/gno 0.25.2 → 0.27.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 +5 -3
- package/assets/skill/SKILL.md +5 -0
- package/assets/skill/cli-reference.md +8 -6
- package/package.json +1 -1
- package/src/cli/commands/get.ts +21 -0
- package/src/cli/commands/skill/install.ts +2 -2
- package/src/cli/commands/skill/paths.ts +26 -4
- package/src/cli/commands/skill/uninstall.ts +2 -2
- package/src/cli/program.ts +18 -12
- package/src/core/document-capabilities.ts +113 -0
- package/src/mcp/tools/get.ts +10 -0
- package/src/mcp/tools/index.ts +434 -110
- package/src/sdk/documents.ts +12 -0
- package/src/serve/doc-events.ts +69 -0
- package/src/serve/public/app.tsx +81 -24
- package/src/serve/public/components/CaptureModal.tsx +138 -3
- package/src/serve/public/components/QuickSwitcher.tsx +248 -0
- package/src/serve/public/components/ShortcutHelpModal.tsx +1 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +74 -26
- package/src/serve/public/components/editor/CodeMirrorEditor.tsx +51 -0
- package/src/serve/public/components/ui/command.tsx +2 -2
- package/src/serve/public/hooks/use-doc-events.ts +34 -0
- package/src/serve/public/hooks/useCaptureModal.tsx +12 -3
- package/src/serve/public/hooks/useKeyboardShortcuts.ts +2 -2
- package/src/serve/public/lib/deep-links.ts +68 -0
- package/src/serve/public/lib/document-availability.ts +22 -0
- package/src/serve/public/lib/local-history.ts +44 -0
- package/src/serve/public/lib/wiki-link.ts +36 -0
- package/src/serve/public/pages/Browse.tsx +11 -0
- package/src/serve/public/pages/Dashboard.tsx +2 -2
- package/src/serve/public/pages/DocView.tsx +241 -18
- package/src/serve/public/pages/DocumentEditor.tsx +399 -9
- package/src/serve/public/pages/Search.tsx +20 -1
- package/src/serve/routes/api.ts +359 -28
- package/src/serve/server.ts +48 -1
- package/src/serve/watch-service.ts +149 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface LocalHistoryEntry {
|
|
2
|
+
savedAt: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const HISTORY_PREFIX = "gno.doc-history.";
|
|
7
|
+
const MAX_ENTRIES = 10;
|
|
8
|
+
|
|
9
|
+
function getHistoryKey(docId: string): string {
|
|
10
|
+
return `${HISTORY_PREFIX}${docId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function loadLocalHistory(docId: string): LocalHistoryEntry[] {
|
|
14
|
+
try {
|
|
15
|
+
const raw = localStorage.getItem(getHistoryKey(docId));
|
|
16
|
+
if (!raw) return [];
|
|
17
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
18
|
+
if (!Array.isArray(parsed)) return [];
|
|
19
|
+
return parsed.filter((entry): entry is LocalHistoryEntry => {
|
|
20
|
+
if (!entry || typeof entry !== "object") return false;
|
|
21
|
+
const candidate = entry as Record<string, unknown>;
|
|
22
|
+
return (
|
|
23
|
+
typeof candidate.savedAt === "string" &&
|
|
24
|
+
typeof candidate.content === "string"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function appendLocalHistory(docId: string, content: string): void {
|
|
33
|
+
const next = [
|
|
34
|
+
{ savedAt: new Date().toISOString(), content },
|
|
35
|
+
...loadLocalHistory(docId).filter((entry) => entry.content !== content),
|
|
36
|
+
].slice(0, MAX_ENTRIES);
|
|
37
|
+
localStorage.setItem(getHistoryKey(docId), JSON.stringify(next));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadLatestLocalHistory(
|
|
41
|
+
docId: string
|
|
42
|
+
): LocalHistoryEntry | undefined {
|
|
43
|
+
return loadLocalHistory(docId)[0];
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface WikiLinkQuery {
|
|
2
|
+
query: string;
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getActiveWikiLinkQuery(
|
|
8
|
+
content: string,
|
|
9
|
+
cursorPos: number
|
|
10
|
+
): WikiLinkQuery | null {
|
|
11
|
+
if (cursorPos < 2) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const prefix = content.slice(0, cursorPos);
|
|
16
|
+
const start = prefix.lastIndexOf("[[");
|
|
17
|
+
if (start === -1) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const closing = prefix.indexOf("]]", start);
|
|
22
|
+
if (closing !== -1) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const query = prefix.slice(start + 2);
|
|
27
|
+
if (query.includes("\n")) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
query,
|
|
33
|
+
start,
|
|
34
|
+
end: cursorPos,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
TableRow,
|
|
28
28
|
} from "../components/ui/table";
|
|
29
29
|
import { apiFetch } from "../hooks/use-api";
|
|
30
|
+
import { useDocEvents } from "../hooks/use-doc-events";
|
|
30
31
|
|
|
31
32
|
interface PageProps {
|
|
32
33
|
navigate: (to: string | number) => void;
|
|
@@ -77,6 +78,7 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
77
78
|
const [syncTarget, setSyncTarget] = useState<SyncTarget>(null);
|
|
78
79
|
const [syncError, setSyncError] = useState<string | null>(null);
|
|
79
80
|
const [refreshToken, setRefreshToken] = useState(0);
|
|
81
|
+
const latestDocEvent = useDocEvents();
|
|
80
82
|
const limit = 25;
|
|
81
83
|
|
|
82
84
|
// Parse collection from URL on mount
|
|
@@ -134,6 +136,15 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
134
136
|
setDocs([]);
|
|
135
137
|
}, [availableDateFields, sortField]);
|
|
136
138
|
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!latestDocEvent?.changedAt) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setOffset(0);
|
|
144
|
+
setDocs([]);
|
|
145
|
+
setRefreshToken((current) => current + 1);
|
|
146
|
+
}, [latestDocEvent?.changedAt]);
|
|
147
|
+
|
|
137
148
|
const handleCollectionChange = (value: string) => {
|
|
138
149
|
const newSelected = value === "all" ? "" : value;
|
|
139
150
|
setSelected(newSelected);
|
|
@@ -271,7 +271,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
271
271
|
{/* Quick Capture Card */}
|
|
272
272
|
<Card
|
|
273
273
|
className="group stagger-3 animate-fade-in cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-secondary/50 hover:bg-secondary/5 hover:shadow-lg"
|
|
274
|
-
onClick={openCapture}
|
|
274
|
+
onClick={() => openCapture()}
|
|
275
275
|
>
|
|
276
276
|
<CardHeader className="pb-2">
|
|
277
277
|
<CardDescription className="flex items-center gap-2">
|
|
@@ -350,7 +350,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
350
350
|
</main>
|
|
351
351
|
|
|
352
352
|
{/* Floating Action Button */}
|
|
353
|
-
<CaptureButton onClick={openCapture} />
|
|
353
|
+
<CaptureButton onClick={() => openCapture()} />
|
|
354
354
|
</div>
|
|
355
355
|
);
|
|
356
356
|
}
|
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
FolderOpen,
|
|
10
10
|
HardDrive,
|
|
11
11
|
HomeIcon,
|
|
12
|
+
LinkIcon,
|
|
12
13
|
Loader2Icon,
|
|
13
14
|
PencilIcon,
|
|
15
|
+
SquareArrowOutUpRightIcon,
|
|
14
16
|
TagIcon,
|
|
15
17
|
TextIcon,
|
|
16
18
|
TrashIcon,
|
|
@@ -49,6 +51,13 @@ import {
|
|
|
49
51
|
} from "../components/ui/dialog";
|
|
50
52
|
import { Separator } from "../components/ui/separator";
|
|
51
53
|
import { apiFetch } from "../hooks/use-api";
|
|
54
|
+
import { useDocEvents } from "../hooks/use-doc-events";
|
|
55
|
+
import {
|
|
56
|
+
buildDocDeepLink,
|
|
57
|
+
buildEditDeepLink,
|
|
58
|
+
parseDocumentDeepLink,
|
|
59
|
+
} from "../lib/deep-links";
|
|
60
|
+
import { waitForDocumentAvailability } from "../lib/document-availability";
|
|
52
61
|
|
|
53
62
|
interface PageProps {
|
|
54
63
|
navigate: (to: string | number) => void;
|
|
@@ -64,10 +73,40 @@ interface DocData {
|
|
|
64
73
|
relPath: string;
|
|
65
74
|
tags: string[];
|
|
66
75
|
source: {
|
|
76
|
+
absPath?: string;
|
|
67
77
|
mime: string;
|
|
68
78
|
ext: string;
|
|
69
79
|
modifiedAt?: string;
|
|
70
80
|
sizeBytes?: number;
|
|
81
|
+
sourceHash?: string;
|
|
82
|
+
};
|
|
83
|
+
capabilities: {
|
|
84
|
+
editable: boolean;
|
|
85
|
+
tagsEditable: boolean;
|
|
86
|
+
tagsWriteback: boolean;
|
|
87
|
+
canCreateEditableCopy: boolean;
|
|
88
|
+
mode: "editable" | "read_only";
|
|
89
|
+
reason?: string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface CreateEditableCopyResponse {
|
|
94
|
+
uri: string;
|
|
95
|
+
path: string;
|
|
96
|
+
jobId: string | null;
|
|
97
|
+
note?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface UpdateDocResponse {
|
|
101
|
+
success: boolean;
|
|
102
|
+
docId: string;
|
|
103
|
+
uri: string;
|
|
104
|
+
path: string;
|
|
105
|
+
jobId: string | null;
|
|
106
|
+
writeBack?: "applied" | "skipped_unsupported";
|
|
107
|
+
version: {
|
|
108
|
+
sourceHash: string;
|
|
109
|
+
modifiedAt?: string;
|
|
71
110
|
};
|
|
72
111
|
}
|
|
73
112
|
|
|
@@ -181,6 +220,11 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
181
220
|
const [deleting, setDeleting] = useState(false);
|
|
182
221
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
183
222
|
const [showRawView, setShowRawView] = useState(false);
|
|
223
|
+
const [creatingCopy, setCreatingCopy] = useState(false);
|
|
224
|
+
const [copyError, setCopyError] = useState<string | null>(null);
|
|
225
|
+
const [externalChangeNotice, setExternalChangeNotice] = useState<
|
|
226
|
+
string | null
|
|
227
|
+
>(null);
|
|
184
228
|
|
|
185
229
|
// Tag editing state
|
|
186
230
|
const [editingTags, setEditingTags] = useState(false);
|
|
@@ -191,25 +235,32 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
191
235
|
|
|
192
236
|
// Request sequencing - ignore stale responses on rapid navigation
|
|
193
237
|
const requestIdRef = useRef(0);
|
|
238
|
+
const latestDocEvent = useDocEvents();
|
|
194
239
|
|
|
195
240
|
// App remounts page on route/query changes, so URI is stable per render.
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
241
|
+
const currentTarget = useMemo(
|
|
242
|
+
() => parseDocumentDeepLink(window.location.search),
|
|
243
|
+
[]
|
|
244
|
+
);
|
|
245
|
+
const currentUri = currentTarget.uri;
|
|
246
|
+
const highlightedLines = useMemo(() => {
|
|
247
|
+
if (!currentTarget.lineStart) return [];
|
|
248
|
+
const end = currentTarget.lineEnd ?? currentTarget.lineStart;
|
|
249
|
+
const lines: number[] = [];
|
|
250
|
+
for (let line = currentTarget.lineStart; line <= end; line += 1) {
|
|
251
|
+
lines.push(line);
|
|
252
|
+
}
|
|
253
|
+
return lines;
|
|
254
|
+
}, [currentTarget.lineEnd, currentTarget.lineStart]);
|
|
200
255
|
|
|
201
|
-
|
|
202
|
-
useEffect(() => {
|
|
256
|
+
const loadDocument = useCallback(() => {
|
|
203
257
|
if (!currentUri) {
|
|
204
258
|
setError("No document URI provided");
|
|
205
259
|
setLoading(false);
|
|
206
260
|
return;
|
|
207
261
|
}
|
|
208
262
|
|
|
209
|
-
// Increment request ID to ignore stale responses
|
|
210
263
|
const currentRequestId = ++requestIdRef.current;
|
|
211
|
-
|
|
212
|
-
// Reset state for new document
|
|
213
264
|
setLoading(true);
|
|
214
265
|
setError(null);
|
|
215
266
|
setDoc(null);
|
|
@@ -232,6 +283,25 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
232
283
|
});
|
|
233
284
|
}, [currentUri]);
|
|
234
285
|
|
|
286
|
+
// Fetch document when URI changes
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
loadDocument();
|
|
289
|
+
}, [loadDocument]);
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (latestDocEvent?.uri !== currentUri) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
setExternalChangeNotice(
|
|
296
|
+
"This document changed on disk. Reload to see the latest content."
|
|
297
|
+
);
|
|
298
|
+
}, [currentUri, latestDocEvent?.changedAt, latestDocEvent?.uri]);
|
|
299
|
+
|
|
300
|
+
const reloadDocument = useCallback(() => {
|
|
301
|
+
setExternalChangeNotice(null);
|
|
302
|
+
loadDocument();
|
|
303
|
+
}, [loadDocument]);
|
|
304
|
+
|
|
235
305
|
const isMarkdown =
|
|
236
306
|
doc?.source.ext &&
|
|
237
307
|
[".md", ".markdown"].includes(doc.source.ext.toLowerCase());
|
|
@@ -267,14 +337,54 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
267
337
|
|
|
268
338
|
const hasFrontmatter = Object.keys(parsedContent.data).length > 0;
|
|
269
339
|
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (currentTarget.view === "source" || currentTarget.lineStart) {
|
|
342
|
+
setShowRawView(true);
|
|
343
|
+
}
|
|
344
|
+
}, [currentTarget.lineStart, currentTarget.view]);
|
|
345
|
+
|
|
270
346
|
const breadcrumbs = doc ? parseBreadcrumbs(doc.collection, doc.relPath) : [];
|
|
271
347
|
|
|
272
348
|
const handleEdit = () => {
|
|
273
|
-
if (doc) {
|
|
274
|
-
navigate(
|
|
349
|
+
if (doc?.capabilities.editable) {
|
|
350
|
+
navigate(
|
|
351
|
+
buildEditDeepLink({
|
|
352
|
+
uri: doc.uri,
|
|
353
|
+
lineStart: currentTarget.lineStart,
|
|
354
|
+
lineEnd: currentTarget.lineEnd,
|
|
355
|
+
})
|
|
356
|
+
);
|
|
275
357
|
}
|
|
276
358
|
};
|
|
277
359
|
|
|
360
|
+
const handleCreateEditableCopy = useCallback(async () => {
|
|
361
|
+
if (!doc?.capabilities.canCreateEditableCopy) return;
|
|
362
|
+
|
|
363
|
+
setCreatingCopy(true);
|
|
364
|
+
setCopyError(null);
|
|
365
|
+
const { data, error: err } = await apiFetch<CreateEditableCopyResponse>(
|
|
366
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/editable-copy`,
|
|
367
|
+
{ method: "POST" }
|
|
368
|
+
);
|
|
369
|
+
setCreatingCopy(false);
|
|
370
|
+
|
|
371
|
+
if (err) {
|
|
372
|
+
setCopyError(err);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (data) {
|
|
377
|
+
const ready = await waitForDocumentAvailability(data.uri);
|
|
378
|
+
if (!ready) {
|
|
379
|
+
setCopyError(
|
|
380
|
+
"Created the markdown copy, but it is still indexing. Try again in a moment."
|
|
381
|
+
);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
navigate(`/edit?uri=${encodeURIComponent(data.uri)}`);
|
|
385
|
+
}
|
|
386
|
+
}, [doc, navigate]);
|
|
387
|
+
|
|
278
388
|
const handleDelete = async () => {
|
|
279
389
|
if (!doc) return;
|
|
280
390
|
|
|
@@ -322,11 +432,15 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
322
432
|
setTagSaveError(null);
|
|
323
433
|
setTagSaveSuccess(false);
|
|
324
434
|
|
|
325
|
-
const { error: err } = await apiFetch(
|
|
435
|
+
const { data, error: err } = await apiFetch<UpdateDocResponse>(
|
|
326
436
|
`/api/docs/${encodeURIComponent(doc.docid)}`,
|
|
327
437
|
{
|
|
328
438
|
method: "PUT",
|
|
329
|
-
body: JSON.stringify({
|
|
439
|
+
body: JSON.stringify({
|
|
440
|
+
tags: editedTags,
|
|
441
|
+
expectedSourceHash: doc.source.sourceHash,
|
|
442
|
+
expectedModifiedAt: doc.source.modifiedAt,
|
|
443
|
+
}),
|
|
330
444
|
}
|
|
331
445
|
);
|
|
332
446
|
|
|
@@ -338,7 +452,15 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
338
452
|
}
|
|
339
453
|
|
|
340
454
|
// Update doc with new tags
|
|
341
|
-
setDoc({
|
|
455
|
+
setDoc({
|
|
456
|
+
...doc,
|
|
457
|
+
tags: editedTags,
|
|
458
|
+
source: {
|
|
459
|
+
...doc.source,
|
|
460
|
+
sourceHash: data?.version.sourceHash ?? doc.source.sourceHash,
|
|
461
|
+
modifiedAt: data?.version.modifiedAt ?? doc.source.modifiedAt,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
342
464
|
setEditingTags(false);
|
|
343
465
|
setTagSaveSuccess(true);
|
|
344
466
|
|
|
@@ -377,6 +499,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
377
499
|
{doc?.title || "Document"}
|
|
378
500
|
</h1>
|
|
379
501
|
</div>
|
|
502
|
+
{doc?.capabilities.mode === "read_only" && (
|
|
503
|
+
<Badge variant="secondary">Read-only</Badge>
|
|
504
|
+
)}
|
|
380
505
|
{doc?.source.ext && (
|
|
381
506
|
<Badge className="shrink-0 font-mono" variant="outline">
|
|
382
507
|
{doc.source.ext}
|
|
@@ -386,10 +511,44 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
386
511
|
<>
|
|
387
512
|
<Separator className="h-6" orientation="vertical" />
|
|
388
513
|
<div className="flex items-center gap-2">
|
|
389
|
-
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
514
|
+
{doc.capabilities.editable ? (
|
|
515
|
+
<Button className="gap-1.5" onClick={handleEdit} size="sm">
|
|
516
|
+
<PencilIcon className="size-4" />
|
|
517
|
+
Edit
|
|
518
|
+
</Button>
|
|
519
|
+
) : (
|
|
520
|
+
<>
|
|
521
|
+
{doc.capabilities.canCreateEditableCopy && (
|
|
522
|
+
<Button
|
|
523
|
+
className="gap-1.5"
|
|
524
|
+
disabled={creatingCopy}
|
|
525
|
+
onClick={() => {
|
|
526
|
+
void handleCreateEditableCopy();
|
|
527
|
+
}}
|
|
528
|
+
size="sm"
|
|
529
|
+
>
|
|
530
|
+
{creatingCopy ? (
|
|
531
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
532
|
+
) : (
|
|
533
|
+
<PencilIcon className="size-4" />
|
|
534
|
+
)}
|
|
535
|
+
Create editable copy
|
|
536
|
+
</Button>
|
|
537
|
+
)}
|
|
538
|
+
{doc.source.absPath && (
|
|
539
|
+
<Button asChild size="sm" variant="outline">
|
|
540
|
+
<a
|
|
541
|
+
href={`file://${doc.source.absPath}`}
|
|
542
|
+
rel="noopener noreferrer"
|
|
543
|
+
target="_blank"
|
|
544
|
+
>
|
|
545
|
+
<SquareArrowOutUpRightIcon className="mr-1.5 size-4" />
|
|
546
|
+
Open original
|
|
547
|
+
</a>
|
|
548
|
+
</Button>
|
|
549
|
+
)}
|
|
550
|
+
</>
|
|
551
|
+
)}
|
|
393
552
|
<Button
|
|
394
553
|
className="gap-1.5 text-muted-foreground hover:text-destructive"
|
|
395
554
|
onClick={() => setDeleteDialogOpen(true)}
|
|
@@ -431,6 +590,29 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
431
590
|
{/* Document */}
|
|
432
591
|
{doc && (
|
|
433
592
|
<div className="animate-fade-in space-y-6 opacity-0">
|
|
593
|
+
{externalChangeNotice && (
|
|
594
|
+
<Card className="border-amber-500/40 bg-amber-500/10">
|
|
595
|
+
<CardContent className="flex flex-wrap items-center justify-between gap-3 py-3">
|
|
596
|
+
<p className="text-amber-500 text-sm">
|
|
597
|
+
{externalChangeNotice}
|
|
598
|
+
</p>
|
|
599
|
+
<Button
|
|
600
|
+
onClick={reloadDocument}
|
|
601
|
+
size="sm"
|
|
602
|
+
variant="outline"
|
|
603
|
+
>
|
|
604
|
+
Reload
|
|
605
|
+
</Button>
|
|
606
|
+
</CardContent>
|
|
607
|
+
</Card>
|
|
608
|
+
)}
|
|
609
|
+
{copyError && (
|
|
610
|
+
<Card className="border-amber-500/40 bg-amber-500/10">
|
|
611
|
+
<CardContent className="py-3 text-amber-500 text-sm">
|
|
612
|
+
{copyError}
|
|
613
|
+
</CardContent>
|
|
614
|
+
</Card>
|
|
615
|
+
)}
|
|
434
616
|
{/* Breadcrumbs */}
|
|
435
617
|
{breadcrumbs.length > 0 && (
|
|
436
618
|
<nav className="flex items-center gap-1 text-sm">
|
|
@@ -507,6 +689,43 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
507
689
|
<code className="break-all font-mono text-muted-foreground text-sm">
|
|
508
690
|
{doc.uri}
|
|
509
691
|
</code>
|
|
692
|
+
{doc.capabilities.reason && (
|
|
693
|
+
<p className="mt-2 text-amber-500 text-xs">
|
|
694
|
+
{doc.capabilities.reason}
|
|
695
|
+
</p>
|
|
696
|
+
)}
|
|
697
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
698
|
+
{currentTarget.lineStart && (
|
|
699
|
+
<Badge className="font-mono" variant="outline">
|
|
700
|
+
L{currentTarget.lineStart}
|
|
701
|
+
{currentTarget.lineEnd &&
|
|
702
|
+
currentTarget.lineEnd !== currentTarget.lineStart
|
|
703
|
+
? `-${currentTarget.lineEnd}`
|
|
704
|
+
: ""}
|
|
705
|
+
</Badge>
|
|
706
|
+
)}
|
|
707
|
+
<Button
|
|
708
|
+
onClick={() => {
|
|
709
|
+
void navigator.clipboard.writeText(
|
|
710
|
+
`${window.location.origin}${buildDocDeepLink({
|
|
711
|
+
uri: doc.uri,
|
|
712
|
+
view:
|
|
713
|
+
currentTarget.view === "source" ||
|
|
714
|
+
currentTarget.lineStart
|
|
715
|
+
? "source"
|
|
716
|
+
: "rendered",
|
|
717
|
+
lineStart: currentTarget.lineStart,
|
|
718
|
+
lineEnd: currentTarget.lineEnd,
|
|
719
|
+
})}`
|
|
720
|
+
);
|
|
721
|
+
}}
|
|
722
|
+
size="sm"
|
|
723
|
+
variant="outline"
|
|
724
|
+
>
|
|
725
|
+
<LinkIcon className="mr-1.5 size-4" />
|
|
726
|
+
Copy link
|
|
727
|
+
</Button>
|
|
728
|
+
</div>
|
|
510
729
|
</div>
|
|
511
730
|
</CardContent>
|
|
512
731
|
</Card>
|
|
@@ -659,7 +878,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
659
878
|
{doc.contentAvailable && isMarkdown && showRawView && (
|
|
660
879
|
<CodeBlock
|
|
661
880
|
code={doc.content ?? ""}
|
|
881
|
+
highlightedLines={highlightedLines}
|
|
662
882
|
language={"markdown" as BundledLanguage}
|
|
883
|
+
scrollToLine={currentTarget.lineStart}
|
|
663
884
|
showLineNumbers
|
|
664
885
|
>
|
|
665
886
|
<CodeBlockCopyButton />
|
|
@@ -668,9 +889,11 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
668
889
|
{doc.contentAvailable && isCodeFile && !isMarkdown && (
|
|
669
890
|
<CodeBlock
|
|
670
891
|
code={doc.content ?? ""}
|
|
892
|
+
highlightedLines={highlightedLines}
|
|
671
893
|
language={
|
|
672
894
|
getLanguageFromExt(doc.source.ext) as BundledLanguage
|
|
673
895
|
}
|
|
896
|
+
scrollToLine={currentTarget.lineStart}
|
|
674
897
|
showLineNumbers
|
|
675
898
|
>
|
|
676
899
|
<CodeBlockCopyButton />
|