@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.
Files changed (71) hide show
  1. package/README.md +6 -5
  2. package/assets/skill/SKILL.md +25 -0
  3. package/assets/skill/mcp-reference.md +6 -0
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +1 -1
  8. package/spec/cli.md +19 -0
  9. package/spec/db/schema.sql +55 -0
  10. package/spec/mcp.md +176 -11
  11. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  12. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  13. package/spec/output-schemas/section-target-create-result.schema.json +20 -0
  14. package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
  15. package/spec/output-schemas/section-target.schema.json +118 -0
  16. package/spec/output-schemas/section.schema.json +113 -0
  17. package/src/core/document-capabilities.ts +13 -0
  18. package/src/core/file-ops.ts +129 -1
  19. package/src/core/file-refactor-adapter.ts +329 -0
  20. package/src/core/file-refactor-apply-edits.ts +61 -0
  21. package/src/core/file-refactor-apply-fs.ts +512 -0
  22. package/src/core/file-refactor-apply-safety.ts +340 -0
  23. package/src/core/file-refactor-apply-validate.ts +401 -0
  24. package/src/core/file-refactor-contract.ts +486 -0
  25. package/src/core/file-refactor-destination.ts +123 -0
  26. package/src/core/file-refactor-from-snapshot.ts +148 -0
  27. package/src/core/file-refactor-journal-port.ts +150 -0
  28. package/src/core/file-refactor-journal.ts +347 -0
  29. package/src/core/file-refactor-paths.ts +60 -0
  30. package/src/core/file-refactor-plan-classify.ts +208 -0
  31. package/src/core/file-refactor-plan-validate.ts +169 -0
  32. package/src/core/file-refactor-planner-types.ts +62 -0
  33. package/src/core/file-refactor-planner.ts +423 -0
  34. package/src/core/file-refactor-resolve.ts +280 -0
  35. package/src/core/file-refactor-service.ts +468 -0
  36. package/src/core/file-refactors.ts +84 -56
  37. package/src/core/link-destination-parse.ts +275 -0
  38. package/src/core/link-inventory-markdown.ts +454 -0
  39. package/src/core/link-inventory-opaque.ts +244 -0
  40. package/src/core/link-inventory-types.ts +47 -0
  41. package/src/core/link-inventory.ts +182 -0
  42. package/src/core/link-relevance.ts +150 -0
  43. package/src/core/section-parse.ts +187 -0
  44. package/src/core/section-target-link.ts +154 -0
  45. package/src/core/section-target-resolve.ts +351 -0
  46. package/src/core/section-target-transport.ts +519 -0
  47. package/src/core/section-target.ts +263 -0
  48. package/src/core/sections.ts +60 -115
  49. package/src/mcp/AGENTS.md +1 -0
  50. package/src/mcp/CLAUDE.md +1 -0
  51. package/src/mcp/http-egress.ts +1 -0
  52. package/src/mcp/tools/index.ts +37 -17
  53. package/src/mcp/tools/sections.ts +512 -0
  54. package/src/mcp/tools/workspace-write.ts +215 -97
  55. package/src/sdk/client.ts +238 -116
  56. package/src/sdk/index.ts +12 -0
  57. package/src/sdk/types.ts +61 -3
  58. package/src/serve/file-refactor-http.ts +239 -0
  59. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  60. package/src/serve/public/globals.built.css +1 -1
  61. package/src/serve/public/lib/section-links.ts +189 -0
  62. package/src/serve/public/pages/DocView.tsx +395 -77
  63. package/src/serve/routes/api.ts +191 -104
  64. package/src/serve/routes/section-targets.ts +221 -0
  65. package/src/serve/server.ts +34 -0
  66. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  67. package/src/store/migrations/index.ts +2 -0
  68. package/src/store/sqlite/adapter.ts +452 -0
  69. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  70. package/src/store/types.ts +84 -0
  71. 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 [renameWarnings, setRenameWarnings] = useState<string[]>([]);
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 [moveWarnings, setMoveWarnings] = useState<string[]>([]);
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 (!currentHash || showRawView || loading) {
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
- }, [currentHash, loading, showRawView]);
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
- setRenameWarnings([]);
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
- void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
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
- setRenameWarnings(data?.refactorWarnings?.warnings ?? []);
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({ name: renameValue, uri: doc.uri }),
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
- setMoveWarnings([]);
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
- void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
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
- setMoveWarnings(data?.refactorWarnings?.warnings ?? []);
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 font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
1174
- Outline
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-7 text-left text-xs transition-colors hover:bg-muted/20 hover:text-foreground"
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
- setShowRawView(false);
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
- <button
1225
- className="absolute top-1 right-1 cursor-pointer rounded p-1 opacity-0 transition-all hover:bg-muted/20 hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100"
1226
- onClick={() => {
1227
- void navigator.clipboard.writeText(
1228
- `${window.location.origin}${buildDocDeepLink({
1229
- uri: doc?.uri ?? "",
1230
- view: "rendered",
1231
- })}#${section.anchor}`
1232
- );
1233
- }}
1234
- type="button"
1235
- >
1236
- <CopyIcon className="size-3" />
1237
- </button>
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. This does not
2032
- move it to another collection yet.
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 className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
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={() => setRenameDialogOpen(false)}
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 disabled={renaming} onClick={() => void handleRename()}>
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 className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
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 onClick={() => setMoveDialogOpen(false)} variant="outline">
2103
- Cancel
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 disabled={moving} onClick={() => void handleMove()}>
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>