@gmickel/gno 1.30.7 → 1.32.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 +6 -5
- package/assets/skill/SKILL.md +25 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +176 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +37 -17
- package/src/mcp/tools/sections.ts +512 -0
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +238 -116
- package/src/sdk/index.ts +12 -0
- package/src/sdk/types.ts +61 -3
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +395 -77
- package/src/serve/routes/api.ts +191 -104
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
LinkIcon,
|
|
14
14
|
Loader2Icon,
|
|
15
15
|
PencilIcon,
|
|
16
|
+
QuoteIcon,
|
|
16
17
|
Share2Icon,
|
|
17
18
|
SquareArrowOutUpRightIcon,
|
|
18
19
|
TextIcon,
|
|
@@ -30,6 +31,10 @@ import {
|
|
|
30
31
|
|
|
31
32
|
import type { PdfFallbackReason } from "../lib/pdf";
|
|
32
33
|
|
|
34
|
+
import {
|
|
35
|
+
FILE_REFACTOR_APPLY_CONFIRMATION,
|
|
36
|
+
type FileRefactorPreviewPlan,
|
|
37
|
+
} from "../../../core/file-refactor-contract";
|
|
33
38
|
import { extractSections } from "../../../core/sections";
|
|
34
39
|
import {
|
|
35
40
|
CodeBlock,
|
|
@@ -46,6 +51,7 @@ import {
|
|
|
46
51
|
OutgoingLinksPanel,
|
|
47
52
|
type OutgoingLink,
|
|
48
53
|
} from "../components/OutgoingLinksPanel";
|
|
54
|
+
import { RefactorImpactPreview } from "../components/RefactorImpactPreview";
|
|
49
55
|
import { RelatedNotesSidebar } from "../components/RelatedNotesSidebar";
|
|
50
56
|
import { TagInput } from "../components/TagInput";
|
|
51
57
|
import { Badge } from "../components/ui/badge";
|
|
@@ -83,6 +89,15 @@ import {
|
|
|
83
89
|
downloadPublishArtifactFile,
|
|
84
90
|
type PublishExportResponse,
|
|
85
91
|
} from "../lib/publish-export";
|
|
92
|
+
import {
|
|
93
|
+
buildReadableSectionUrl,
|
|
94
|
+
createCitationSectionUrl,
|
|
95
|
+
readSectionTargetLinkParam,
|
|
96
|
+
resolveSectionLinkNavigation,
|
|
97
|
+
SECTION_LINK_NOTICE_COPY,
|
|
98
|
+
stripSectionTargetLinkParam,
|
|
99
|
+
type SectionLinkNoticeKind,
|
|
100
|
+
} from "../lib/section-links";
|
|
86
101
|
import { subscribeWorkspaceActionRequest } from "../lib/workspace-events";
|
|
87
102
|
|
|
88
103
|
/** Lazy so pdfjs is never pulled for non-PDF documents. */
|
|
@@ -189,6 +204,9 @@ interface RenameDocResponse {
|
|
|
189
204
|
uri: string;
|
|
190
205
|
path: string;
|
|
191
206
|
relPath: string;
|
|
207
|
+
planDigest?: string;
|
|
208
|
+
status?: string;
|
|
209
|
+
warning?: string;
|
|
192
210
|
refactorWarnings?: {
|
|
193
211
|
warnings: string[];
|
|
194
212
|
};
|
|
@@ -199,6 +217,9 @@ interface MoveDocResponse {
|
|
|
199
217
|
uri: string;
|
|
200
218
|
path: string;
|
|
201
219
|
relPath: string;
|
|
220
|
+
planDigest?: string;
|
|
221
|
+
status?: string;
|
|
222
|
+
warning?: string;
|
|
202
223
|
refactorWarnings?: {
|
|
203
224
|
warnings: string[];
|
|
204
225
|
};
|
|
@@ -349,13 +370,23 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
349
370
|
const [renaming, setRenaming] = useState(false);
|
|
350
371
|
const [renameError, setRenameError] = useState<string | null>(null);
|
|
351
372
|
const [renameValue, setRenameValue] = useState("");
|
|
352
|
-
const [
|
|
373
|
+
const [renamePlan, setRenamePlan] = useState<FileRefactorPreviewPlan | null>(
|
|
374
|
+
null
|
|
375
|
+
);
|
|
376
|
+
const [renamePlanLoading, setRenamePlanLoading] = useState(false);
|
|
377
|
+
const [renameConfirmed, setRenameConfirmed] = useState(false);
|
|
378
|
+
const [renameOutcome, setRenameOutcome] = useState<string | null>(null);
|
|
353
379
|
const [moveDialogOpen, setMoveDialogOpen] = useState(false);
|
|
354
380
|
const [moving, setMoving] = useState(false);
|
|
355
381
|
const [moveError, setMoveError] = useState<string | null>(null);
|
|
356
382
|
const [moveFolderPath, setMoveFolderPath] = useState("");
|
|
357
383
|
const [moveName, setMoveName] = useState("");
|
|
358
|
-
const [
|
|
384
|
+
const [movePlan, setMovePlan] = useState<FileRefactorPreviewPlan | null>(
|
|
385
|
+
null
|
|
386
|
+
);
|
|
387
|
+
const [movePlanLoading, setMovePlanLoading] = useState(false);
|
|
388
|
+
const [moveConfirmed, setMoveConfirmed] = useState(false);
|
|
389
|
+
const [moveOutcome, setMoveOutcome] = useState<string | null>(null);
|
|
359
390
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
|
360
391
|
const [duplicating, setDuplicating] = useState(false);
|
|
361
392
|
const [duplicateError, setDuplicateError] = useState<string | null>(null);
|
|
@@ -389,9 +420,13 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
389
420
|
const [activeSectionAnchor, setActiveSectionAnchor] = useState<string | null>(
|
|
390
421
|
null
|
|
391
422
|
);
|
|
423
|
+
const [sectionLinkNotice, setSectionLinkNotice] =
|
|
424
|
+
useState<SectionLinkNoticeKind | null>(null);
|
|
425
|
+
const [blockHashNavigation, setBlockHashNavigation] = useState(false);
|
|
392
426
|
|
|
393
427
|
// Request sequencing - ignore stale responses on rapid navigation
|
|
394
428
|
const requestIdRef = useRef(0);
|
|
429
|
+
const sectionResolveRequestRef = useRef(0);
|
|
395
430
|
const latestDocEvent = useDocEvents();
|
|
396
431
|
|
|
397
432
|
// App remounts page on route/query changes, so URI is stable per render.
|
|
@@ -404,6 +439,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
404
439
|
() => window.location.hash.replace(/^#/u, ""),
|
|
405
440
|
[]
|
|
406
441
|
);
|
|
442
|
+
const encodedSectionTarget = useMemo(
|
|
443
|
+
() => readSectionTargetLinkParam(window.location.search),
|
|
444
|
+
[]
|
|
445
|
+
);
|
|
407
446
|
const highlightedLines = useMemo(() => {
|
|
408
447
|
if (!currentTarget.lineStart) return [];
|
|
409
448
|
const end = currentTarget.lineEnd ?? currentTarget.lineStart;
|
|
@@ -564,7 +603,78 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
564
603
|
}, []);
|
|
565
604
|
|
|
566
605
|
useEffect(() => {
|
|
567
|
-
if (!
|
|
606
|
+
if (!sectionLinkNotice) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const timer = window.setTimeout(() => setSectionLinkNotice(null), 3200);
|
|
610
|
+
return () => {
|
|
611
|
+
window.clearTimeout(timer);
|
|
612
|
+
};
|
|
613
|
+
}, [sectionLinkNotice]);
|
|
614
|
+
|
|
615
|
+
useEffect(() => {
|
|
616
|
+
if (!doc?.content || loading || !encodedSectionTarget) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const requestId = ++sectionResolveRequestRef.current;
|
|
621
|
+
const content = doc.content;
|
|
622
|
+
void resolveSectionLinkNavigation({
|
|
623
|
+
content,
|
|
624
|
+
uri: doc.uri,
|
|
625
|
+
encodedTarget: encodedSectionTarget,
|
|
626
|
+
hashAnchor: currentHash,
|
|
627
|
+
}).then((result) => {
|
|
628
|
+
if (requestId !== sectionResolveRequestRef.current) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
setBlockHashNavigation(result.blockHashNavigation);
|
|
632
|
+
if (result.notice) {
|
|
633
|
+
setSectionLinkNotice(result.notice);
|
|
634
|
+
}
|
|
635
|
+
if (result.cleanCitationParam) {
|
|
636
|
+
const cleanedSearch = stripSectionTargetLinkParam(
|
|
637
|
+
window.location.search
|
|
638
|
+
);
|
|
639
|
+
const nextHash = result.navigateAnchor
|
|
640
|
+
? `#${result.navigateAnchor}`
|
|
641
|
+
: window.location.hash;
|
|
642
|
+
window.history.replaceState(
|
|
643
|
+
{},
|
|
644
|
+
"",
|
|
645
|
+
`${window.location.pathname}${cleanedSearch}${nextHash}`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (result.blockHashNavigation || !result.navigateAnchor) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (showRawView) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
requestAnimationFrame(() => {
|
|
655
|
+
document
|
|
656
|
+
.getElementById(result.navigateAnchor ?? "")
|
|
657
|
+
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
658
|
+
setActiveSectionAnchor(result.navigateAnchor);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}, [
|
|
662
|
+
currentHash,
|
|
663
|
+
doc?.content,
|
|
664
|
+
doc?.uri,
|
|
665
|
+
encodedSectionTarget,
|
|
666
|
+
loading,
|
|
667
|
+
showRawView,
|
|
668
|
+
]);
|
|
669
|
+
|
|
670
|
+
useEffect(() => {
|
|
671
|
+
if (
|
|
672
|
+
blockHashNavigation ||
|
|
673
|
+
encodedSectionTarget ||
|
|
674
|
+
!currentHash ||
|
|
675
|
+
showRawView ||
|
|
676
|
+
loading
|
|
677
|
+
) {
|
|
568
678
|
return;
|
|
569
679
|
}
|
|
570
680
|
|
|
@@ -574,7 +684,13 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
574
684
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
575
685
|
setActiveSectionAnchor(currentHash);
|
|
576
686
|
});
|
|
577
|
-
}, [
|
|
687
|
+
}, [
|
|
688
|
+
blockHashNavigation,
|
|
689
|
+
currentHash,
|
|
690
|
+
encodedSectionTarget,
|
|
691
|
+
loading,
|
|
692
|
+
showRawView,
|
|
693
|
+
]);
|
|
578
694
|
|
|
579
695
|
const breadcrumbs = doc ? parseBreadcrumbs(doc.collection, doc.relPath) : [];
|
|
580
696
|
const sections = useMemo(
|
|
@@ -582,6 +698,79 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
582
698
|
[parsedContent.body]
|
|
583
699
|
);
|
|
584
700
|
|
|
701
|
+
const copyReadableSectionLink = useCallback(
|
|
702
|
+
(anchor: string) => {
|
|
703
|
+
if (!doc) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
void navigator.clipboard
|
|
707
|
+
.writeText(
|
|
708
|
+
buildReadableSectionUrl(window.location.origin, {
|
|
709
|
+
uri: doc.uri,
|
|
710
|
+
view: "rendered",
|
|
711
|
+
anchor,
|
|
712
|
+
})
|
|
713
|
+
)
|
|
714
|
+
.then(() => {
|
|
715
|
+
setSectionLinkNotice("copied_link");
|
|
716
|
+
})
|
|
717
|
+
.catch(() => {
|
|
718
|
+
setSectionLinkNotice("clipboard_unavailable");
|
|
719
|
+
});
|
|
720
|
+
},
|
|
721
|
+
[doc]
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const copyCitationSectionLink = useCallback(
|
|
725
|
+
async (anchor: string) => {
|
|
726
|
+
const content = doc?.content;
|
|
727
|
+
if (!doc || !content) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const citationUrl = await createCitationSectionUrl({
|
|
731
|
+
origin: window.location.origin,
|
|
732
|
+
uri: doc.uri,
|
|
733
|
+
content,
|
|
734
|
+
anchor,
|
|
735
|
+
view: "rendered",
|
|
736
|
+
});
|
|
737
|
+
if (!citationUrl) {
|
|
738
|
+
setSectionLinkNotice("citation_unavailable");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
await navigator.clipboard.writeText(citationUrl);
|
|
743
|
+
setSectionLinkNotice("copied_citation");
|
|
744
|
+
} catch {
|
|
745
|
+
setSectionLinkNotice("clipboard_unavailable");
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
[doc]
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const jumpToSection = useCallback(
|
|
752
|
+
(anchor: string) => {
|
|
753
|
+
setShowRawView(false);
|
|
754
|
+
setBlockHashNavigation(false);
|
|
755
|
+
requestAnimationFrame(() => {
|
|
756
|
+
document.getElementById(anchor)?.scrollIntoView({
|
|
757
|
+
behavior: "smooth",
|
|
758
|
+
block: "start",
|
|
759
|
+
});
|
|
760
|
+
window.history.replaceState(
|
|
761
|
+
{},
|
|
762
|
+
"",
|
|
763
|
+
`${buildDocDeepLink({
|
|
764
|
+
uri: doc?.uri ?? "",
|
|
765
|
+
view: "rendered",
|
|
766
|
+
})}#${anchor}`
|
|
767
|
+
);
|
|
768
|
+
setActiveSectionAnchor(anchor);
|
|
769
|
+
});
|
|
770
|
+
},
|
|
771
|
+
[doc?.uri]
|
|
772
|
+
);
|
|
773
|
+
|
|
585
774
|
useEffect(() => {
|
|
586
775
|
if (showRawView || sections.length === 0) {
|
|
587
776
|
setActiveSectionAnchor(sections[0]?.anchor ?? null);
|
|
@@ -707,7 +896,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
707
896
|
const filename = doc.relPath.split("/").pop() ?? doc.relPath;
|
|
708
897
|
setRenameValue(filename);
|
|
709
898
|
setRenameError(null);
|
|
710
|
-
|
|
899
|
+
setRenamePlan(null);
|
|
900
|
+
setRenameConfirmed(false);
|
|
901
|
+
setRenameOutcome(null);
|
|
711
902
|
setRenameDialogOpen(true);
|
|
712
903
|
}, [doc]);
|
|
713
904
|
|
|
@@ -715,7 +906,11 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
715
906
|
if (!renameDialogOpen || !doc || !renameValue.trim()) {
|
|
716
907
|
return;
|
|
717
908
|
}
|
|
718
|
-
|
|
909
|
+
let cancelled = false;
|
|
910
|
+
setRenamePlanLoading(true);
|
|
911
|
+
setRenamePlan(null);
|
|
912
|
+
setRenameConfirmed(false);
|
|
913
|
+
void apiFetch<FileRefactorPreviewPlan>(
|
|
719
914
|
`/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
|
|
720
915
|
{
|
|
721
916
|
method: "POST",
|
|
@@ -725,22 +920,42 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
725
920
|
uri: doc.uri,
|
|
726
921
|
}),
|
|
727
922
|
}
|
|
728
|
-
).then(({ data }) => {
|
|
729
|
-
|
|
923
|
+
).then(({ data, error: err }) => {
|
|
924
|
+
if (cancelled) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
setRenamePlanLoading(false);
|
|
928
|
+
if (err) {
|
|
929
|
+
setRenameError(err);
|
|
930
|
+
setRenamePlan(null);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
setRenameError(null);
|
|
934
|
+
setRenamePlan(data);
|
|
730
935
|
});
|
|
936
|
+
return () => {
|
|
937
|
+
cancelled = true;
|
|
938
|
+
};
|
|
731
939
|
}, [doc, renameDialogOpen, renameValue]);
|
|
732
940
|
|
|
733
941
|
const handleRename = useCallback(async () => {
|
|
734
|
-
if (!doc) {
|
|
942
|
+
if (!doc || !renamePlan?.canApply || !renameConfirmed) {
|
|
735
943
|
return;
|
|
736
944
|
}
|
|
737
945
|
setRenaming(true);
|
|
738
946
|
setRenameError(null);
|
|
947
|
+
setRenameOutcome(null);
|
|
739
948
|
const { data, error: err } = await apiFetch<RenameDocResponse>(
|
|
740
949
|
`/api/docs/${encodeURIComponent(doc.docid)}/rename`,
|
|
741
950
|
{
|
|
742
951
|
method: "POST",
|
|
743
|
-
body: JSON.stringify({
|
|
952
|
+
body: JSON.stringify({
|
|
953
|
+
name: renameValue,
|
|
954
|
+
uri: doc.uri,
|
|
955
|
+
planDigest: renamePlan.planDigest,
|
|
956
|
+
confirmation: FILE_REFACTOR_APPLY_CONFIRMATION,
|
|
957
|
+
schemaVersion: renamePlan.schemaVersion,
|
|
958
|
+
}),
|
|
744
959
|
}
|
|
745
960
|
);
|
|
746
961
|
setRenaming(false);
|
|
@@ -750,11 +965,19 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
750
965
|
return;
|
|
751
966
|
}
|
|
752
967
|
|
|
968
|
+
if (data?.status === "applied_with_sync_pending" || data?.warning) {
|
|
969
|
+
setRenameOutcome(
|
|
970
|
+
data.warning ??
|
|
971
|
+
"Files renamed, but index sync is still pending. Run Update All to finish."
|
|
972
|
+
);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
753
976
|
setRenameDialogOpen(false);
|
|
754
977
|
if (data?.uri) {
|
|
755
978
|
navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
|
|
756
979
|
}
|
|
757
|
-
}, [doc, navigate, renameValue]);
|
|
980
|
+
}, [doc, navigate, renameConfirmed, renamePlan, renameValue]);
|
|
758
981
|
|
|
759
982
|
const handleStartMove = useCallback(() => {
|
|
760
983
|
if (!doc) {
|
|
@@ -763,7 +986,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
763
986
|
setMoveFolderPath(getParentPath(doc.relPath));
|
|
764
987
|
setMoveName(doc.relPath.split("/").pop() ?? doc.relPath);
|
|
765
988
|
setMoveError(null);
|
|
766
|
-
|
|
989
|
+
setMovePlan(null);
|
|
990
|
+
setMoveConfirmed(false);
|
|
991
|
+
setMoveOutcome(null);
|
|
767
992
|
setMoveDialogOpen(true);
|
|
768
993
|
}, [doc]);
|
|
769
994
|
|
|
@@ -771,7 +996,11 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
771
996
|
if (!moveDialogOpen || !doc || !moveFolderPath.trim()) {
|
|
772
997
|
return;
|
|
773
998
|
}
|
|
774
|
-
|
|
999
|
+
let cancelled = false;
|
|
1000
|
+
setMovePlanLoading(true);
|
|
1001
|
+
setMovePlan(null);
|
|
1002
|
+
setMoveConfirmed(false);
|
|
1003
|
+
void apiFetch<FileRefactorPreviewPlan>(
|
|
775
1004
|
`/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
|
|
776
1005
|
{
|
|
777
1006
|
method: "POST",
|
|
@@ -782,17 +1011,31 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
782
1011
|
uri: doc.uri,
|
|
783
1012
|
}),
|
|
784
1013
|
}
|
|
785
|
-
).then(({ data }) => {
|
|
786
|
-
|
|
1014
|
+
).then(({ data, error: err }) => {
|
|
1015
|
+
if (cancelled) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
setMovePlanLoading(false);
|
|
1019
|
+
if (err) {
|
|
1020
|
+
setMoveError(err);
|
|
1021
|
+
setMovePlan(null);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
setMoveError(null);
|
|
1025
|
+
setMovePlan(data);
|
|
787
1026
|
});
|
|
1027
|
+
return () => {
|
|
1028
|
+
cancelled = true;
|
|
1029
|
+
};
|
|
788
1030
|
}, [doc, moveDialogOpen, moveFolderPath, moveName]);
|
|
789
1031
|
|
|
790
1032
|
const handleMove = useCallback(async () => {
|
|
791
|
-
if (!doc) {
|
|
1033
|
+
if (!doc || !movePlan?.canApply || !moveConfirmed) {
|
|
792
1034
|
return;
|
|
793
1035
|
}
|
|
794
1036
|
setMoving(true);
|
|
795
1037
|
setMoveError(null);
|
|
1038
|
+
setMoveOutcome(null);
|
|
796
1039
|
const { data, error: err } = await apiFetch<MoveDocResponse>(
|
|
797
1040
|
`/api/docs/${encodeURIComponent(doc.docid)}/move`,
|
|
798
1041
|
{
|
|
@@ -801,6 +1044,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
801
1044
|
folderPath: moveFolderPath,
|
|
802
1045
|
name: moveName,
|
|
803
1046
|
uri: doc.uri,
|
|
1047
|
+
planDigest: movePlan.planDigest,
|
|
1048
|
+
confirmation: FILE_REFACTOR_APPLY_CONFIRMATION,
|
|
1049
|
+
schemaVersion: movePlan.schemaVersion,
|
|
804
1050
|
}),
|
|
805
1051
|
}
|
|
806
1052
|
);
|
|
@@ -811,11 +1057,19 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
811
1057
|
return;
|
|
812
1058
|
}
|
|
813
1059
|
|
|
1060
|
+
if (data?.status === "applied_with_sync_pending" || data?.warning) {
|
|
1061
|
+
setMoveOutcome(
|
|
1062
|
+
data.warning ??
|
|
1063
|
+
"Files moved, but index sync is still pending. Run Update All to finish."
|
|
1064
|
+
);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
814
1068
|
setMoveDialogOpen(false);
|
|
815
1069
|
if (data?.uri) {
|
|
816
1070
|
navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
|
|
817
1071
|
}
|
|
818
|
-
}, [doc, moveFolderPath, moveName, navigate]);
|
|
1072
|
+
}, [doc, moveConfirmed, moveFolderPath, moveName, movePlan, navigate]);
|
|
819
1073
|
|
|
820
1074
|
const handleStartDuplicate = useCallback(() => {
|
|
821
1075
|
if (!doc) {
|
|
@@ -1170,8 +1424,19 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1170
1424
|
<>
|
|
1171
1425
|
<div className="mx-3 border-border/20 border-t" />
|
|
1172
1426
|
<div className="px-3 py-3">
|
|
1173
|
-
<div className="mb-2
|
|
1174
|
-
|
|
1427
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
1428
|
+
<div className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
1429
|
+
Outline
|
|
1430
|
+
</div>
|
|
1431
|
+
{sectionLinkNotice && (
|
|
1432
|
+
<div
|
|
1433
|
+
aria-live="polite"
|
|
1434
|
+
className="min-w-0 truncate font-mono text-[10px] text-muted-foreground/70"
|
|
1435
|
+
role="status"
|
|
1436
|
+
>
|
|
1437
|
+
{SECTION_LINK_NOTICE_COPY[sectionLinkNotice]}
|
|
1438
|
+
</div>
|
|
1439
|
+
)}
|
|
1175
1440
|
</div>
|
|
1176
1441
|
<div className="w-full min-w-0 max-w-full space-y-0.5 overflow-x-hidden">
|
|
1177
1442
|
{sections.map((section) => (
|
|
@@ -1185,25 +1450,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1185
1450
|
style={{ paddingLeft: `${section.level * 7}px` }}
|
|
1186
1451
|
>
|
|
1187
1452
|
<button
|
|
1188
|
-
className="flex w-full min-w-0 max-w-full cursor-pointer items-start gap-2 overflow-hidden rounded px-1 py-0.5 pr-
|
|
1453
|
+
className="flex w-full min-w-0 max-w-full cursor-pointer items-start gap-2 overflow-hidden rounded px-1 py-0.5 pr-12 text-left text-xs transition-colors hover:bg-muted/20 hover:text-foreground"
|
|
1189
1454
|
onClick={() => {
|
|
1190
|
-
|
|
1191
|
-
requestAnimationFrame(() => {
|
|
1192
|
-
document
|
|
1193
|
-
.getElementById(section.anchor)
|
|
1194
|
-
?.scrollIntoView({
|
|
1195
|
-
behavior: "smooth",
|
|
1196
|
-
block: "start",
|
|
1197
|
-
});
|
|
1198
|
-
window.history.replaceState(
|
|
1199
|
-
{},
|
|
1200
|
-
"",
|
|
1201
|
-
`${buildDocDeepLink({
|
|
1202
|
-
uri: doc?.uri ?? "",
|
|
1203
|
-
view: "rendered",
|
|
1204
|
-
})}#${section.anchor}`
|
|
1205
|
-
);
|
|
1206
|
-
});
|
|
1455
|
+
jumpToSection(section.anchor);
|
|
1207
1456
|
}}
|
|
1208
1457
|
type="button"
|
|
1209
1458
|
>
|
|
@@ -1221,20 +1470,40 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1221
1470
|
</TooltipContent>
|
|
1222
1471
|
</Tooltip>
|
|
1223
1472
|
</button>
|
|
1224
|
-
<
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1473
|
+
<div className="absolute top-1 right-1 flex items-center gap-0.5 opacity-0 transition-all focus-within:opacity-100 group-hover:opacity-100">
|
|
1474
|
+
<Tooltip>
|
|
1475
|
+
<TooltipTrigger asChild>
|
|
1476
|
+
<button
|
|
1477
|
+
aria-label={`Copy link to ${section.title}`}
|
|
1478
|
+
className="cursor-pointer rounded p-1 hover:bg-muted/20 hover:text-foreground"
|
|
1479
|
+
onClick={() => {
|
|
1480
|
+
copyReadableSectionLink(section.anchor);
|
|
1481
|
+
}}
|
|
1482
|
+
type="button"
|
|
1483
|
+
>
|
|
1484
|
+
<CopyIcon className="size-3" />
|
|
1485
|
+
</button>
|
|
1486
|
+
</TooltipTrigger>
|
|
1487
|
+
<TooltipContent side="left">Copy link</TooltipContent>
|
|
1488
|
+
</Tooltip>
|
|
1489
|
+
<Tooltip>
|
|
1490
|
+
<TooltipTrigger asChild>
|
|
1491
|
+
<button
|
|
1492
|
+
aria-label={`Copy local citation link to ${section.title}`}
|
|
1493
|
+
className="cursor-pointer rounded p-1 hover:bg-muted/20 hover:text-foreground"
|
|
1494
|
+
onClick={() => {
|
|
1495
|
+
void copyCitationSectionLink(section.anchor);
|
|
1496
|
+
}}
|
|
1497
|
+
type="button"
|
|
1498
|
+
>
|
|
1499
|
+
<QuoteIcon className="size-3" />
|
|
1500
|
+
</button>
|
|
1501
|
+
</TooltipTrigger>
|
|
1502
|
+
<TooltipContent side="left">
|
|
1503
|
+
Copy local citation link
|
|
1504
|
+
</TooltipContent>
|
|
1505
|
+
</Tooltip>
|
|
1506
|
+
</div>
|
|
1238
1507
|
</div>
|
|
1239
1508
|
))}
|
|
1240
1509
|
</div>
|
|
@@ -2024,39 +2293,61 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
2024
2293
|
</Dialog>
|
|
2025
2294
|
|
|
2026
2295
|
<Dialog onOpenChange={setRenameDialogOpen} open={renameDialogOpen}>
|
|
2027
|
-
<DialogContent>
|
|
2296
|
+
<DialogContent className="sm:max-w-lg">
|
|
2028
2297
|
<DialogHeader>
|
|
2029
2298
|
<DialogTitle>Rename document</DialogTitle>
|
|
2030
2299
|
<DialogDescription>
|
|
2031
|
-
Rename the file on disk inside its current folder.
|
|
2032
|
-
|
|
2300
|
+
Rename the file on disk inside its current folder. Reference
|
|
2301
|
+
rewrites follow the exact plan digest below.
|
|
2033
2302
|
</DialogDescription>
|
|
2034
2303
|
</DialogHeader>
|
|
2035
2304
|
<input
|
|
2305
|
+
aria-label="New file name"
|
|
2036
2306
|
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm"
|
|
2037
2307
|
onChange={(event) => setRenameValue(event.target.value)}
|
|
2038
2308
|
value={renameValue}
|
|
2039
2309
|
/>
|
|
2310
|
+
<RefactorImpactPreview
|
|
2311
|
+
confirmed={renameConfirmed}
|
|
2312
|
+
loading={renamePlanLoading}
|
|
2313
|
+
onConfirmedChange={setRenameConfirmed}
|
|
2314
|
+
outcomeMessage={renameOutcome}
|
|
2315
|
+
outcomeTone="warning"
|
|
2316
|
+
plan={renamePlan}
|
|
2317
|
+
/>
|
|
2040
2318
|
{renameError && (
|
|
2041
|
-
<div
|
|
2319
|
+
<div
|
|
2320
|
+
className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm"
|
|
2321
|
+
role="alert"
|
|
2322
|
+
>
|
|
2042
2323
|
{renameError}
|
|
2043
2324
|
</div>
|
|
2044
2325
|
)}
|
|
2045
|
-
{renameWarnings.length > 0 && (
|
|
2046
|
-
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
|
|
2047
|
-
{renameWarnings.map((warning) => (
|
|
2048
|
-
<div key={warning}>{warning}</div>
|
|
2049
|
-
))}
|
|
2050
|
-
</div>
|
|
2051
|
-
)}
|
|
2052
2326
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
2053
2327
|
<Button
|
|
2054
|
-
onClick={() =>
|
|
2328
|
+
onClick={() => {
|
|
2329
|
+
if (renameOutcome && renamePlan) {
|
|
2330
|
+
setRenameDialogOpen(false);
|
|
2331
|
+
navigate(
|
|
2332
|
+
`/doc?uri=${encodeURIComponent(renamePlan.target.uri)}`
|
|
2333
|
+
);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
setRenameDialogOpen(false);
|
|
2337
|
+
}}
|
|
2055
2338
|
variant="outline"
|
|
2056
2339
|
>
|
|
2057
|
-
Cancel
|
|
2340
|
+
{renameOutcome ? "Open note" : "Cancel"}
|
|
2058
2341
|
</Button>
|
|
2059
|
-
<Button
|
|
2342
|
+
<Button
|
|
2343
|
+
disabled={
|
|
2344
|
+
renaming ||
|
|
2345
|
+
!renamePlan?.canApply ||
|
|
2346
|
+
!renameConfirmed ||
|
|
2347
|
+
Boolean(renameOutcome)
|
|
2348
|
+
}
|
|
2349
|
+
onClick={() => void handleRename()}
|
|
2350
|
+
>
|
|
2060
2351
|
{renaming && (
|
|
2061
2352
|
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
2062
2353
|
)}
|
|
@@ -2067,42 +2358,69 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
2067
2358
|
</Dialog>
|
|
2068
2359
|
|
|
2069
2360
|
<Dialog onOpenChange={setMoveDialogOpen} open={moveDialogOpen}>
|
|
2070
|
-
<DialogContent>
|
|
2361
|
+
<DialogContent className="sm:max-w-lg">
|
|
2071
2362
|
<DialogHeader>
|
|
2072
2363
|
<DialogTitle>Move document</DialogTitle>
|
|
2073
2364
|
<DialogDescription>
|
|
2074
2365
|
Move the current note to another folder inside this collection.
|
|
2366
|
+
Reference rewrites follow the exact plan digest below.
|
|
2075
2367
|
</DialogDescription>
|
|
2076
2368
|
</DialogHeader>
|
|
2077
2369
|
<div className="space-y-3">
|
|
2078
2370
|
<Input
|
|
2371
|
+
aria-label="Destination folder path"
|
|
2079
2372
|
onChange={(event) => setMoveFolderPath(event.target.value)}
|
|
2080
2373
|
placeholder="projects/research"
|
|
2081
2374
|
value={moveFolderPath}
|
|
2082
2375
|
/>
|
|
2083
2376
|
<Input
|
|
2377
|
+
aria-label="File name"
|
|
2084
2378
|
onChange={(event) => setMoveName(event.target.value)}
|
|
2085
2379
|
placeholder="note.md"
|
|
2086
2380
|
value={moveName}
|
|
2087
2381
|
/>
|
|
2382
|
+
<RefactorImpactPreview
|
|
2383
|
+
confirmed={moveConfirmed}
|
|
2384
|
+
loading={movePlanLoading}
|
|
2385
|
+
onConfirmedChange={setMoveConfirmed}
|
|
2386
|
+
outcomeMessage={moveOutcome}
|
|
2387
|
+
outcomeTone="warning"
|
|
2388
|
+
plan={movePlan}
|
|
2389
|
+
/>
|
|
2088
2390
|
{moveError && (
|
|
2089
|
-
<div
|
|
2391
|
+
<div
|
|
2392
|
+
className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm"
|
|
2393
|
+
role="alert"
|
|
2394
|
+
>
|
|
2090
2395
|
{moveError}
|
|
2091
2396
|
</div>
|
|
2092
2397
|
)}
|
|
2093
|
-
{moveWarnings.length > 0 && (
|
|
2094
|
-
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
|
|
2095
|
-
{moveWarnings.map((warning) => (
|
|
2096
|
-
<div key={warning}>{warning}</div>
|
|
2097
|
-
))}
|
|
2098
|
-
</div>
|
|
2099
|
-
)}
|
|
2100
2398
|
</div>
|
|
2101
2399
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
2102
|
-
<Button
|
|
2103
|
-
|
|
2400
|
+
<Button
|
|
2401
|
+
onClick={() => {
|
|
2402
|
+
if (moveOutcome && movePlan) {
|
|
2403
|
+
setMoveDialogOpen(false);
|
|
2404
|
+
navigate(
|
|
2405
|
+
`/doc?uri=${encodeURIComponent(movePlan.target.uri)}`
|
|
2406
|
+
);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
setMoveDialogOpen(false);
|
|
2410
|
+
}}
|
|
2411
|
+
variant="outline"
|
|
2412
|
+
>
|
|
2413
|
+
{moveOutcome ? "Open note" : "Cancel"}
|
|
2104
2414
|
</Button>
|
|
2105
|
-
<Button
|
|
2415
|
+
<Button
|
|
2416
|
+
disabled={
|
|
2417
|
+
moving ||
|
|
2418
|
+
!movePlan?.canApply ||
|
|
2419
|
+
!moveConfirmed ||
|
|
2420
|
+
Boolean(moveOutcome)
|
|
2421
|
+
}
|
|
2422
|
+
onClick={() => void handleMove()}
|
|
2423
|
+
>
|
|
2106
2424
|
{moving && <Loader2Icon className="mr-1.5 size-4 animate-spin" />}
|
|
2107
2425
|
Move
|
|
2108
2426
|
</Button>
|