md-redline 0.1.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/md-redline +255 -0
  4. package/bin/test-windows.ps1 +70 -0
  5. package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
  6. package/dist/assets/arc-DI2g9LXK.js +1 -0
  7. package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
  8. package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
  9. package/dist/assets/array-DOVTz2Mq.js +1 -0
  10. package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
  11. package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
  12. package/dist/assets/channel-DPCihw7y.js +1 -0
  13. package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
  14. package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
  15. package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
  16. package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
  17. package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
  18. package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
  19. package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
  20. package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
  21. package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
  22. package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
  23. package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
  24. package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
  25. package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
  26. package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
  27. package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
  28. package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
  29. package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
  30. package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
  31. package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
  32. package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
  33. package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
  34. package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
  35. package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
  36. package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
  37. package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
  38. package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
  39. package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
  40. package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
  41. package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
  42. package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
  43. package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
  44. package/dist/assets/clone-CI9aUwHe.js +1 -0
  45. package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
  46. package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
  47. package/dist/assets/dagre-CilMRazv.js +1 -0
  48. package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
  49. package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
  50. package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
  51. package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
  52. package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
  53. package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
  54. package/dist/assets/dist-C_eddq6m.js +1 -0
  55. package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
  56. package/dist/assets/flatten-B8XIuT0x.js +1 -0
  57. package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
  58. package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
  59. package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
  60. package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
  61. package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
  62. package/dist/assets/index-BoggyWS0.css +2 -0
  63. package/dist/assets/index-aLvjHQW4.js +104 -0
  64. package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
  65. package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
  66. package/dist/assets/init-Bft5Ffpj.js +1 -0
  67. package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
  68. package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
  69. package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
  70. package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
  71. package/dist/assets/katex-Uj9wLT16.js +265 -0
  72. package/dist/assets/line-CRxEwpOv.js +1 -0
  73. package/dist/assets/linear-PDPfFByd.js +1 -0
  74. package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
  75. package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
  76. package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
  77. package/dist/assets/ordinal-DIg8h6NI.js +1 -0
  78. package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
  79. package/dist/assets/path-DfRbCp9y.js +1 -0
  80. package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
  81. package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
  82. package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
  83. package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
  84. package/dist/assets/reduce-CV2X8n1a.js +1 -0
  85. package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
  86. package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
  87. package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
  88. package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
  89. package/dist/assets/src-PKe5NtkK.js +1 -0
  90. package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
  91. package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
  92. package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
  93. package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
  94. package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
  95. package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
  96. package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
  97. package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
  98. package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
  99. package/dist/favicon.svg +15 -0
  100. package/dist/index.html +14 -0
  101. package/dist/screenshot.png +0 -0
  102. package/index.html +13 -0
  103. package/package.json +105 -0
  104. package/public/favicon.svg +15 -0
  105. package/public/screenshot.png +0 -0
  106. package/server/index.test.ts +814 -0
  107. package/server/index.ts +736 -0
  108. package/server/preferences.test.ts +126 -0
  109. package/server/preferences.ts +76 -0
  110. package/src/App.tsx +1620 -0
  111. package/src/components/ActionButton.tsx +41 -0
  112. package/src/components/CommandPalette.tsx +191 -0
  113. package/src/components/CommentCard.tsx +556 -0
  114. package/src/components/CommentForm.tsx +285 -0
  115. package/src/components/CommentSidebar.tsx +428 -0
  116. package/src/components/ConfirmDialog.tsx +64 -0
  117. package/src/components/ContextMenu.tsx +220 -0
  118. package/src/components/DragHandles.tsx +48 -0
  119. package/src/components/FileExplorer.tsx +251 -0
  120. package/src/components/FileOpener.tsx +304 -0
  121. package/src/components/IconButton.tsx +32 -0
  122. package/src/components/KeyboardShortcutsPanel.tsx +136 -0
  123. package/src/components/MarkdownViewer.tsx +682 -0
  124. package/src/components/RawView.tsx +798 -0
  125. package/src/components/SearchBar.tsx +129 -0
  126. package/src/components/Separator.tsx +7 -0
  127. package/src/components/SettingsPanel.tsx +813 -0
  128. package/src/components/SplitIconButton.tsx +133 -0
  129. package/src/components/TabBar.tsx +594 -0
  130. package/src/components/TableOfContents.tsx +70 -0
  131. package/src/components/ThemeSelector.tsx +159 -0
  132. package/src/components/Toast.tsx +99 -0
  133. package/src/components/Toolbar.tsx +161 -0
  134. package/src/components/iconButtonVariants.ts +19 -0
  135. package/src/components/rawView.test.ts +291 -0
  136. package/src/contexts/SettingsContext.tsx +120 -0
  137. package/src/hooks/useAuthor.test.ts +58 -0
  138. package/src/hooks/useAuthor.ts +69 -0
  139. package/src/hooks/useAutoResize.ts +20 -0
  140. package/src/hooks/useCommentCardTriggers.ts +20 -0
  141. package/src/hooks/useComments.test.ts +773 -0
  142. package/src/hooks/useComments.ts +332 -0
  143. package/src/hooks/useContextMenu.ts +48 -0
  144. package/src/hooks/useContextMenuItems.ts +392 -0
  145. package/src/hooks/useDiffSnapshot.test.ts +130 -0
  146. package/src/hooks/useDiffSnapshot.ts +67 -0
  147. package/src/hooks/useDragHandles.ts +417 -0
  148. package/src/hooks/useFileWatcher.ts +45 -0
  149. package/src/hooks/useHeadingTracking.ts +84 -0
  150. package/src/hooks/useMermaidRenderer.ts +75 -0
  151. package/src/hooks/useModalState.ts +22 -0
  152. package/src/hooks/usePageVisible.test.ts +69 -0
  153. package/src/hooks/usePageVisible.ts +19 -0
  154. package/src/hooks/usePaneLayout.test.ts +108 -0
  155. package/src/hooks/usePaneLayout.ts +102 -0
  156. package/src/hooks/useRecentFiles.test.ts +103 -0
  157. package/src/hooks/useRecentFiles.ts +99 -0
  158. package/src/hooks/useResizablePanel.test.ts +84 -0
  159. package/src/hooks/useResizablePanel.ts +118 -0
  160. package/src/hooks/useSearch.test.ts +72 -0
  161. package/src/hooks/useSearch.ts +53 -0
  162. package/src/hooks/useSelection.ts +48 -0
  163. package/src/hooks/useSessionPersistence.test.ts +59 -0
  164. package/src/hooks/useSessionPersistence.ts +43 -0
  165. package/src/hooks/useTabs.test.ts +127 -0
  166. package/src/hooks/useTabs.ts +561 -0
  167. package/src/hooks/useThemePersistence.ts +41 -0
  168. package/src/hooks/useToast.ts +27 -0
  169. package/src/index.css +1047 -0
  170. package/src/lib/agent-prompts.test.ts +34 -0
  171. package/src/lib/agent-prompts.ts +68 -0
  172. package/src/lib/comment-editor-state.ts +6 -0
  173. package/src/lib/comment-parser.test.ts +1959 -0
  174. package/src/lib/comment-parser.ts +1021 -0
  175. package/src/lib/diff.test.ts +164 -0
  176. package/src/lib/diff.ts +139 -0
  177. package/src/lib/heading-slugs.test.ts +85 -0
  178. package/src/lib/heading-slugs.ts +44 -0
  179. package/src/lib/http.test.ts +43 -0
  180. package/src/lib/http.ts +29 -0
  181. package/src/lib/mermaid-highlights.test.ts +517 -0
  182. package/src/lib/mermaid-highlights.ts +936 -0
  183. package/src/lib/mermaid-renderer.test.ts +114 -0
  184. package/src/lib/mermaid-renderer.ts +89 -0
  185. package/src/lib/path-utils.test.ts +17 -0
  186. package/src/lib/path-utils.ts +7 -0
  187. package/src/lib/platform.test.ts +58 -0
  188. package/src/lib/platform.ts +14 -0
  189. package/src/lib/preferences-client.test.ts +177 -0
  190. package/src/lib/preferences-client.ts +94 -0
  191. package/src/lib/selection-resolver.test.ts +118 -0
  192. package/src/lib/selection-resolver.ts +37 -0
  193. package/src/lib/settings.test.ts +152 -0
  194. package/src/lib/settings.ts +78 -0
  195. package/src/lib/shortcut-label.tsx +18 -0
  196. package/src/lib/themes.ts +21 -0
  197. package/src/lib/visible-text.test.ts +86 -0
  198. package/src/lib/visible-text.ts +77 -0
  199. package/src/main.tsx +22 -0
  200. package/src/markdown/pipeline.test.ts +82 -0
  201. package/src/markdown/pipeline.ts +33 -0
  202. package/src/types.test.ts +43 -0
  203. package/src/types.ts +46 -0
  204. package/tsconfig.app.json +28 -0
  205. package/tsconfig.json +7 -0
  206. package/tsconfig.node.json +26 -0
  207. package/vite.config.ts +50 -0
@@ -0,0 +1,332 @@
1
+ import {
2
+ useState,
3
+ useMemo,
4
+ useCallback,
5
+ type RefObject,
6
+ type Dispatch,
7
+ type SetStateAction,
8
+ } from 'react';
9
+ import {
10
+ parseComments,
11
+ insertComment,
12
+ removeComment,
13
+ editComment,
14
+ editReply,
15
+ updateCommentAnchor,
16
+ resolveComment,
17
+ unresolveComment,
18
+ addReply,
19
+ removeReply,
20
+ removeAllComments,
21
+ resolveAllComments,
22
+ removeResolvedComments,
23
+ detectMissingAnchors,
24
+ } from '../lib/comment-parser';
25
+ import { getEffectiveStatus } from '../types';
26
+ import { renderMarkdown } from '../markdown/pipeline';
27
+ import type { MarkdownViewerHandle } from '../components/MarkdownViewer';
28
+ import type { RawViewHandle } from '../components/RawView';
29
+ import { buildAddressCommentsPrompt } from '../lib/agent-prompts';
30
+
31
+ interface TabInfo {
32
+ filePath: string;
33
+ rawMarkdown: string;
34
+ }
35
+
36
+ export interface UseCommentsParams {
37
+ rawMarkdown: string | undefined;
38
+ rawMarkdownRef: RefObject<string | undefined>;
39
+ setRawMarkdown: (content: string) => void;
40
+ saveFile: (content: string) => void;
41
+ author: string;
42
+ enableResolve: boolean;
43
+ tabs: TabInfo[];
44
+ activeFilePath: string | null;
45
+ viewerRef: RefObject<MarkdownViewerHandle | null>;
46
+ rawViewRef: RefObject<RawViewHandle | null>;
47
+ showToast: (msg: string) => void;
48
+ clearSelection: () => void;
49
+ setAutoExpandForm: Dispatch<SetStateAction<boolean>>;
50
+ requestCommentFocus: (commentId: string) => void;
51
+ }
52
+
53
+ export function useComments(params: UseCommentsParams) {
54
+ const {
55
+ rawMarkdown,
56
+ rawMarkdownRef,
57
+ setRawMarkdown,
58
+ saveFile,
59
+ author,
60
+ enableResolve,
61
+ tabs,
62
+ activeFilePath,
63
+ viewerRef,
64
+ rawViewRef,
65
+ showToast,
66
+ clearSelection,
67
+ setAutoExpandForm,
68
+ requestCommentFocus,
69
+ } = params;
70
+
71
+ const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
72
+
73
+ // Parse comments from raw markdown
74
+ const { cleanMarkdown, comments } = useMemo(
75
+ () => parseComments(rawMarkdown ?? ''),
76
+ [rawMarkdown],
77
+ );
78
+
79
+ // Render markdown to HTML
80
+ const html = useMemo(() => (cleanMarkdown ? renderMarkdown(cleanMarkdown) : ''), [cleanMarkdown]);
81
+
82
+ // Detect missing anchors
83
+ const missingAnchors = useMemo(
84
+ () => detectMissingAnchors(cleanMarkdown, comments),
85
+ [cleanMarkdown, comments],
86
+ );
87
+
88
+ // Comment counts per tab (for badges)
89
+ const { commentCounts, resolvedCommentCounts } = useMemo(() => {
90
+ const counts = new Map<string, number>();
91
+ const resolvedCounts = new Map<string, number>();
92
+ for (const tab of tabs) {
93
+ if (tab.filePath === activeFilePath) {
94
+ const count = enableResolve
95
+ ? comments.filter((c) => getEffectiveStatus(c) !== 'resolved').length
96
+ : comments.length;
97
+ counts.set(tab.filePath, count);
98
+ if (enableResolve) {
99
+ resolvedCounts.set(
100
+ tab.filePath,
101
+ comments.filter((c) => getEffectiveStatus(c) === 'resolved').length,
102
+ );
103
+ }
104
+ } else {
105
+ try {
106
+ const { comments: tabComments } = parseComments(tab.rawMarkdown);
107
+ const count = enableResolve
108
+ ? tabComments.filter((c) => getEffectiveStatus(c) !== 'resolved').length
109
+ : tabComments.length;
110
+ counts.set(tab.filePath, count);
111
+ if (enableResolve) {
112
+ resolvedCounts.set(
113
+ tab.filePath,
114
+ tabComments.filter((c) => getEffectiveStatus(c) === 'resolved').length,
115
+ );
116
+ }
117
+ } catch {
118
+ counts.set(tab.filePath, 0);
119
+ }
120
+ }
121
+ }
122
+ return { commentCounts: counts, resolvedCommentCounts: resolvedCounts };
123
+ }, [tabs, activeFilePath, comments, enableResolve]);
124
+
125
+ const commentCount = enableResolve
126
+ ? comments.filter((c) => getEffectiveStatus(c) !== 'resolved').length
127
+ : comments.length;
128
+
129
+ // Core update helper — synchronously updates the ref so back-to-back
130
+ // mutations (e.g. rapid keyboard shortcuts) each read the latest state.
131
+ const updateAndSave = useCallback(
132
+ (newRaw: string) => {
133
+ rawMarkdownRef.current = newRaw;
134
+ setRawMarkdown(newRaw);
135
+ saveFile(newRaw);
136
+ },
137
+ [setRawMarkdown, saveFile, rawMarkdownRef],
138
+ );
139
+
140
+ const handleAddComment = useCallback(
141
+ (
142
+ anchor: string,
143
+ text: string,
144
+ contextBefore?: string,
145
+ contextAfter?: string,
146
+ hintOffset?: number,
147
+ ) => {
148
+ const newCommentId = crypto.randomUUID();
149
+ const newRaw = insertComment(
150
+ rawMarkdownRef.current ?? '',
151
+ anchor,
152
+ text,
153
+ author,
154
+ contextBefore,
155
+ contextAfter,
156
+ hintOffset,
157
+ newCommentId,
158
+ );
159
+ updateAndSave(newRaw);
160
+ setActiveCommentId(newCommentId);
161
+ requestCommentFocus(newCommentId);
162
+ clearSelection();
163
+ setAutoExpandForm(false);
164
+ },
165
+ [updateAndSave, clearSelection, author, rawMarkdownRef, requestCommentFocus, setAutoExpandForm],
166
+ );
167
+
168
+ const handleResolve = useCallback(
169
+ (id: string) => {
170
+ updateAndSave(resolveComment(rawMarkdownRef.current ?? '', id));
171
+ },
172
+ [updateAndSave, rawMarkdownRef],
173
+ );
174
+
175
+ const handleUnresolve = useCallback(
176
+ (id: string) => {
177
+ updateAndSave(unresolveComment(rawMarkdownRef.current ?? '', id));
178
+ },
179
+ [updateAndSave, rawMarkdownRef],
180
+ );
181
+
182
+ const handleDelete = useCallback(
183
+ (id: string) => {
184
+ updateAndSave(removeComment(rawMarkdownRef.current ?? '', id));
185
+ setActiveCommentId((prev) => (prev === id ? null : prev));
186
+ },
187
+ [updateAndSave, rawMarkdownRef],
188
+ );
189
+
190
+ const handleEdit = useCallback(
191
+ (id: string, newText: string) => {
192
+ updateAndSave(editComment(rawMarkdownRef.current ?? '', id, newText));
193
+ },
194
+ [updateAndSave, rawMarkdownRef],
195
+ );
196
+
197
+ const handleReply = useCallback(
198
+ (id: string, text: string) => {
199
+ updateAndSave(addReply(rawMarkdownRef.current ?? '', id, text, author));
200
+ },
201
+ [updateAndSave, author, rawMarkdownRef],
202
+ );
203
+
204
+ const handleEditReply = useCallback(
205
+ (commentId: string, replyId: string, newText: string) => {
206
+ updateAndSave(editReply(rawMarkdownRef.current ?? '', commentId, replyId, newText));
207
+ },
208
+ [updateAndSave, rawMarkdownRef],
209
+ );
210
+
211
+ const handleDeleteReply = useCallback(
212
+ (commentId: string, replyId: string) => {
213
+ updateAndSave(removeReply(rawMarkdownRef.current ?? '', commentId, replyId));
214
+ },
215
+ [updateAndSave, rawMarkdownRef],
216
+ );
217
+
218
+ const handleBulkDelete = useCallback(() => {
219
+ updateAndSave(removeAllComments(rawMarkdownRef.current ?? ''));
220
+ }, [updateAndSave, rawMarkdownRef]);
221
+
222
+ const handleBulkResolve = useCallback(() => {
223
+ updateAndSave(resolveAllComments(rawMarkdownRef.current ?? ''));
224
+ }, [updateAndSave, rawMarkdownRef]);
225
+
226
+ const handleBulkDeleteResolved = useCallback(() => {
227
+ updateAndSave(removeResolvedComments(rawMarkdownRef.current ?? ''));
228
+ }, [updateAndSave, rawMarkdownRef]);
229
+
230
+ const handleCopyAgentPrompt = useCallback(
231
+ (filePaths: string[]) => {
232
+ if (filePaths.length === 0) return;
233
+ const prompt = buildAddressCommentsPrompt({
234
+ filePaths,
235
+ commentCounts,
236
+ enableResolve,
237
+ });
238
+
239
+ const fileCount = filePaths.length;
240
+ navigator.clipboard.writeText(prompt).then(
241
+ () =>
242
+ showToast(
243
+ `Copied agent instructions for ${fileCount} file${fileCount !== 1 ? 's' : ''} (snapshot saved)`,
244
+ ),
245
+ () => showToast("Couldn't copy to clipboard. Try from localhost."),
246
+ );
247
+ },
248
+ [commentCounts, showToast, enableResolve],
249
+ );
250
+
251
+ const handleHighlightClick = useCallback((commentId: string) => {
252
+ setActiveCommentId(commentId);
253
+ }, []);
254
+
255
+ const handleSidebarActivate = useCallback(
256
+ (commentId: string) => {
257
+ setActiveCommentId(commentId);
258
+ viewerRef.current?.scrollToComment(commentId);
259
+ rawViewRef.current?.scrollToComment(commentId);
260
+ },
261
+ [viewerRef, rawViewRef],
262
+ );
263
+
264
+ const handleAnchorChange = useCallback(
265
+ (commentIds: string[], newAnchor: string) => {
266
+ let newRaw = rawMarkdownRef.current ?? '';
267
+ for (const id of commentIds) {
268
+ newRaw = updateCommentAnchor(newRaw, id, newAnchor);
269
+ }
270
+ updateAndSave(newRaw);
271
+ },
272
+ [updateAndSave, rawMarkdownRef],
273
+ );
274
+
275
+ const handleJumpToNext = useCallback(() => {
276
+ const navigable = enableResolve
277
+ ? comments.filter((c) => getEffectiveStatus(c) === 'open')
278
+ : comments;
279
+ if (navigable.length === 0) return;
280
+
281
+ const currentIdx = activeCommentId ? navigable.findIndex((c) => c.id === activeCommentId) : -1;
282
+ const nextIdx = (currentIdx + 1) % navigable.length;
283
+ const next = navigable[nextIdx];
284
+ setActiveCommentId(next.id);
285
+ viewerRef.current?.scrollToComment(next.id);
286
+ rawViewRef.current?.scrollToComment(next.id);
287
+ }, [comments, activeCommentId, enableResolve, viewerRef, rawViewRef]);
288
+
289
+ const handleJumpToPrev = useCallback(() => {
290
+ const navigable = enableResolve
291
+ ? comments.filter((c) => getEffectiveStatus(c) === 'open')
292
+ : comments;
293
+ if (navigable.length === 0) return;
294
+
295
+ const currentIdx = activeCommentId ? navigable.findIndex((c) => c.id === activeCommentId) : -1;
296
+ const prevIdx = currentIdx <= 0 ? navigable.length - 1 : currentIdx - 1;
297
+ const prev = navigable[prevIdx];
298
+ setActiveCommentId(prev.id);
299
+ viewerRef.current?.scrollToComment(prev.id);
300
+ rawViewRef.current?.scrollToComment(prev.id);
301
+ }, [comments, activeCommentId, enableResolve, viewerRef, rawViewRef]);
302
+
303
+ return {
304
+ activeCommentId,
305
+ setActiveCommentId,
306
+ comments,
307
+ cleanMarkdown,
308
+ html,
309
+ missingAnchors,
310
+ commentCounts,
311
+ resolvedCommentCounts,
312
+ commentCount,
313
+ updateAndSave,
314
+ handleAddComment,
315
+ handleResolve,
316
+ handleUnresolve,
317
+ handleDelete,
318
+ handleEdit,
319
+ handleReply,
320
+ handleEditReply,
321
+ handleDeleteReply,
322
+ handleBulkDelete,
323
+ handleBulkResolve,
324
+ handleBulkDeleteResolved,
325
+ handleCopyAgentPrompt,
326
+ handleHighlightClick,
327
+ handleSidebarActivate,
328
+ handleAnchorChange,
329
+ handleJumpToNext,
330
+ handleJumpToPrev,
331
+ };
332
+ }
@@ -0,0 +1,48 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+
3
+ export interface ContextMenuState {
4
+ isOpen: boolean;
5
+ position: { x: number; y: number };
6
+ }
7
+
8
+ export function useContextMenu() {
9
+ const [state, setState] = useState<ContextMenuState>({
10
+ isOpen: false,
11
+ position: { x: 0, y: 0 },
12
+ });
13
+
14
+ const open = useCallback((x: number, y: number) => {
15
+ setState({ isOpen: true, position: { x, y } });
16
+ }, []);
17
+
18
+ const close = useCallback(() => {
19
+ setState((prev) => ({ ...prev, isOpen: false }));
20
+ }, []);
21
+
22
+ // Close on Escape
23
+ useEffect(() => {
24
+ if (!state.isOpen) return;
25
+
26
+ const handleKeyDown = (e: KeyboardEvent) => {
27
+ if (e.key === 'Escape') {
28
+ e.preventDefault();
29
+ close();
30
+ }
31
+ };
32
+
33
+ const handleScroll = () => {
34
+ close();
35
+ };
36
+
37
+ document.addEventListener('keydown', handleKeyDown);
38
+ // Close on any scroll event (capture phase to catch scrollable containers)
39
+ document.addEventListener('scroll', handleScroll, true);
40
+
41
+ return () => {
42
+ document.removeEventListener('keydown', handleKeyDown);
43
+ document.removeEventListener('scroll', handleScroll, true);
44
+ };
45
+ }, [state.isOpen, close]);
46
+
47
+ return { ...state, open, close };
48
+ }