@plannotator/ui 0.22.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 (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. package/utils/wideMode.ts +48 -0
@@ -0,0 +1,602 @@
1
+ import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
2
+ import { File, type LineAnnotation } from '@pierre/diffs/react';
3
+ import type { SelectedLineRange as PierreSelectedLineRange, LineEventBaseProps } from '@pierre/diffs';
4
+ import { PopoutDialog } from './PopoutDialog';
5
+ import { useTheme } from './ThemeProvider';
6
+ import { CommentPopover } from './CommentPopover';
7
+ import { ImageThumbnail } from './ImageThumbnail';
8
+ import type { CodeAnnotation, ImageAttachment } from '../types';
9
+
10
+ export interface CodeFileAnnotationInput {
11
+ filePath: string;
12
+ lineStart: number;
13
+ lineEnd: number;
14
+ text: string;
15
+ images?: ImageAttachment[];
16
+ originalCode: string;
17
+ }
18
+
19
+ interface CodeFilePopoutProps {
20
+ open: boolean;
21
+ onClose: () => void;
22
+ filepath: string;
23
+ contents: string;
24
+ prerenderedHTML?: string;
25
+ error?: string;
26
+ requestedPath?: string;
27
+ annotations?: CodeAnnotation[];
28
+ selectedAnnotationId?: string | null;
29
+ onAddAnnotation?: (annotation: CodeFileAnnotationInput) => void;
30
+ onEditAnnotation?: (id: string, updates: Partial<CodeAnnotation>) => void;
31
+ onDeleteAnnotation?: (id: string) => void;
32
+ onSelectAnnotation?: (id: string | null) => void;
33
+ container?: HTMLElement | null;
34
+ }
35
+
36
+ interface PendingComment {
37
+ range: { start: number; end: number };
38
+ contextText: string;
39
+ originalCode: string;
40
+ anchorEl?: HTMLElement;
41
+ anchorRect?: DOMRect;
42
+ }
43
+
44
+ const gutterButtonStyle: React.CSSProperties = {
45
+ appearance: 'none',
46
+ display: 'flex',
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ width: '1lh',
50
+ height: '1lh',
51
+ fontSize: 'var(--diffs-font-size, 13px)',
52
+ lineHeight: 'var(--diffs-line-height, 20px)',
53
+ border: 'none',
54
+ borderRadius: 4,
55
+ backgroundColor: 'var(--diffs-modified-base)',
56
+ color: 'var(--diffs-bg)',
57
+ cursor: 'pointer',
58
+ position: 'relative',
59
+ zIndex: 4,
60
+ padding: 0,
61
+ marginRight: 'calc(1ch - 1lh)',
62
+ };
63
+
64
+ function getThemeColors(): { bg: string; fg: string } {
65
+ try {
66
+ const styles = getComputedStyle(document.documentElement);
67
+ return {
68
+ bg: styles.getPropertyValue('--background').trim(),
69
+ fg: styles.getPropertyValue('--foreground').trim(),
70
+ };
71
+ } catch {
72
+ return { bg: '', fg: '' };
73
+ }
74
+ }
75
+
76
+ function buildPierreCSS(mode: 'dark' | 'light', bg: string, fg: string): string {
77
+ if (!bg || !fg) return '';
78
+ return `
79
+ :host {
80
+ color-scheme: ${mode};
81
+ height: 100% !important;
82
+ }
83
+ :host, [data-diff], [data-file], [data-diffs-header], [data-error-wrapper], [data-virtualizer-buffer] {
84
+ --diffs-bg: ${bg} !important;
85
+ --diffs-fg: ${fg} !important;
86
+ --diffs-dark-bg: ${bg};
87
+ --diffs-light-bg: ${bg};
88
+ --diffs-dark: ${fg};
89
+ --diffs-light: ${fg};
90
+ }
91
+ pre, code { background-color: ${bg} !important; }
92
+ [data-column-number] { background-color: ${bg} !important; }
93
+ [data-file] { height: 100% !important; }
94
+ [data-code] { height: 100% !important; overflow-y: auto !important; }
95
+ [data-line] { cursor: pointer; }
96
+ `;
97
+ }
98
+
99
+ function getLineSlice(contents: string, start: number, end: number): string {
100
+ return contents
101
+ .split('\n')
102
+ .slice(Math.max(0, start - 1), Math.max(0, end))
103
+ .join('\n');
104
+ }
105
+
106
+ function lineLabel(start: number, end: number): string {
107
+ return start === end ? `line ${start}` : `lines ${start}-${end}`;
108
+ }
109
+
110
+ function getLineNumberFromSelectionNode(node: Node | null): number | null {
111
+ let current: Node | null = node;
112
+ if (current?.nodeType === Node.TEXT_NODE) current = current.parentNode;
113
+
114
+ while (current) {
115
+ if (current instanceof HTMLElement) {
116
+ const line = current.closest('[data-line]')?.getAttribute('data-line');
117
+ if (line) {
118
+ const parsed = Number(line);
119
+ return Number.isFinite(parsed) ? parsed : null;
120
+ }
121
+ }
122
+ current = current.parentNode;
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ function getPierreSelection(root: HTMLElement | null): Selection | null {
129
+ const shadowRoot = root?.querySelector('diffs-container')?.shadowRoot;
130
+ const shadowSelection = (shadowRoot as (ShadowRoot & { getSelection?: () => Selection | null }) | null)
131
+ ?.getSelection?.();
132
+ return shadowSelection && !shadowSelection.isCollapsed
133
+ ? shadowSelection
134
+ : window.getSelection();
135
+ }
136
+
137
+ const CodeInlineAnnotation: React.FC<{
138
+ annotation: CodeAnnotation;
139
+ isSelected: boolean;
140
+ onSelect?: (id: string | null) => void;
141
+ onEdit?: (id: string, updates: Partial<CodeAnnotation>) => void;
142
+ onDelete?: (id: string) => void;
143
+ }> = ({ annotation, isSelected, onSelect, onEdit, onDelete }) => {
144
+ const [isEditing, setIsEditing] = useState(false);
145
+ const [editText, setEditText] = useState(annotation.text ?? '');
146
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
147
+
148
+ useEffect(() => {
149
+ if (!isEditing) setEditText(annotation.text ?? '');
150
+ }, [annotation.text, isEditing]);
151
+
152
+ useEffect(() => {
153
+ if (isEditing) {
154
+ textareaRef.current?.focus();
155
+ textareaRef.current?.select();
156
+ }
157
+ }, [isEditing]);
158
+
159
+ const save = () => {
160
+ onEdit?.(annotation.id, { text: editText });
161
+ setIsEditing(false);
162
+ };
163
+
164
+ return (
165
+ <div
166
+ data-code-annotation-id={annotation.id}
167
+ onClick={() => onSelect?.(annotation.id)}
168
+ className={`group my-2 mx-3 rounded-lg border px-3 py-2 text-xs shadow-sm cursor-pointer transition-colors ${
169
+ isSelected ? 'border-primary/50' : 'border-border hover:border-border/80'
170
+ }`}
171
+ style={{
172
+ backgroundColor: isSelected ? 'color-mix(in oklab, var(--primary) 12%, var(--popover))' : 'var(--popover)',
173
+ color: 'var(--foreground)',
174
+ opacity: 1,
175
+ }}
176
+ >
177
+ <div className="flex items-center gap-2 text-[10px] uppercase tracking-wide text-muted-foreground">
178
+ <span className="font-semibold text-primary">Comment</span>
179
+ <span className="rounded bg-muted px-1.5 py-0.5 font-mono normal-case text-foreground">
180
+ {lineLabel(annotation.lineStart, annotation.lineEnd)}
181
+ </span>
182
+ {annotation.author && <span className="truncate normal-case">by {annotation.author}</span>}
183
+ <div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 [@media(hover:none)]:opacity-100">
184
+ {onEdit && !isEditing && (
185
+ <button
186
+ type="button"
187
+ onClick={(e) => {
188
+ e.stopPropagation();
189
+ setIsEditing(true);
190
+ }}
191
+ className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
192
+ title="Edit comment"
193
+ >
194
+ <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
195
+ <path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
196
+ </svg>
197
+ </button>
198
+ )}
199
+ {onDelete && (
200
+ <button
201
+ type="button"
202
+ onClick={(e) => {
203
+ e.stopPropagation();
204
+ onDelete(annotation.id);
205
+ }}
206
+ className="rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
207
+ title="Delete comment"
208
+ >
209
+ <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
210
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
211
+ </svg>
212
+ </button>
213
+ )}
214
+ </div>
215
+ </div>
216
+
217
+ {isEditing ? (
218
+ <div className="mt-2 space-y-2">
219
+ <textarea
220
+ ref={textareaRef}
221
+ value={editText}
222
+ onChange={(e) => setEditText(e.target.value)}
223
+ onClick={(e) => e.stopPropagation()}
224
+ onKeyDown={(e) => {
225
+ if (e.key === 'Escape') {
226
+ e.preventDefault();
227
+ setIsEditing(false);
228
+ setEditText(annotation.text ?? '');
229
+ } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
230
+ e.preventDefault();
231
+ save();
232
+ }
233
+ }}
234
+ rows={Math.min(editText.split('\n').length + 1, 8)}
235
+ className="w-full resize-none rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
236
+ />
237
+ <div className="flex items-center gap-2">
238
+ <button
239
+ type="button"
240
+ onClick={(e) => {
241
+ e.stopPropagation();
242
+ save();
243
+ }}
244
+ className="rounded bg-primary px-2 py-1 text-[10px] font-medium text-primary-foreground hover:opacity-90"
245
+ >
246
+ Save
247
+ </button>
248
+ <button
249
+ type="button"
250
+ onClick={(e) => {
251
+ e.stopPropagation();
252
+ setIsEditing(false);
253
+ setEditText(annotation.text ?? '');
254
+ }}
255
+ className="rounded bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground hover:bg-muted/80"
256
+ >
257
+ Cancel
258
+ </button>
259
+ </div>
260
+ </div>
261
+ ) : (
262
+ annotation.text && (
263
+ <div className="mt-1.5 whitespace-pre-wrap border-l-2 border-primary/50 pl-2 text-foreground/90">
264
+ {annotation.text}
265
+ </div>
266
+ )
267
+ )}
268
+
269
+ {annotation.images && annotation.images.length > 0 && (
270
+ <div className="mt-2 flex flex-wrap gap-1.5">
271
+ {annotation.images.map((img) => (
272
+ <div key={img.path} className="text-center">
273
+ <ImageThumbnail path={img.path} size="sm" showRemove={false} />
274
+ <div className="max-w-[3rem] truncate text-[9px] text-muted-foreground" title={img.name}>
275
+ {img.name}
276
+ </div>
277
+ </div>
278
+ ))}
279
+ </div>
280
+ )}
281
+ </div>
282
+ );
283
+ };
284
+
285
+ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
286
+ open,
287
+ onClose,
288
+ filepath,
289
+ contents,
290
+ prerenderedHTML,
291
+ error,
292
+ requestedPath,
293
+ annotations = [],
294
+ selectedAnnotationId,
295
+ onAddAnnotation,
296
+ onEditAnnotation,
297
+ onDeleteAnnotation,
298
+ onSelectAnnotation,
299
+ container,
300
+ }) => {
301
+ const { resolvedMode } = useTheme();
302
+ const mode = resolvedMode ?? 'dark';
303
+ const colors = getThemeColors();
304
+ const [pierreTheme, setPierreTheme] = useState(() => ({
305
+ type: mode as 'dark' | 'light',
306
+ css: buildPierreCSS(mode, colors.bg, colors.fg),
307
+ }));
308
+ const [copied, setCopied] = useState(false);
309
+ const [pendingComment, setPendingComment] = useState<PendingComment | null>(null);
310
+ const fileAreaRef = useRef<HTMLDivElement>(null);
311
+ const lastPointerRectRef = useRef<DOMRect | null>(null);
312
+ const suppressLineClickUntilRef = useRef(0);
313
+
314
+ useEffect(() => {
315
+ requestAnimationFrame(() => {
316
+ const c = getThemeColors();
317
+ setPierreTheme({
318
+ type: mode,
319
+ css: buildPierreCSS(mode, c.bg, c.fg),
320
+ });
321
+ });
322
+ }, [mode]);
323
+
324
+ useEffect(() => {
325
+ setPendingComment(null);
326
+ }, [filepath]);
327
+
328
+ const displayName = filepath.split('/').pop() || filepath;
329
+ const relativePath = filepath.replace(/.*\/(?=.*\/)/, '');
330
+ const lineCount = useMemo(() => contents.split('\n').length, [contents]);
331
+ const selectedCodeAnnotation = useMemo(
332
+ () => annotations.find((ann) => ann.id === selectedAnnotationId),
333
+ [annotations, selectedAnnotationId],
334
+ );
335
+
336
+ // TODO: add token-level annotation support (charStart/charEnd) — for now only line-scope
337
+ const lineAnnotations = useMemo((): LineAnnotation<CodeAnnotation>[] => {
338
+ return annotations
339
+ .filter((ann) => (ann.scope ?? 'line') === 'line')
340
+ .map((ann) => ({
341
+ lineNumber: ann.lineEnd,
342
+ metadata: ann,
343
+ }));
344
+ }, [annotations]);
345
+
346
+ const selectedLines = useMemo((): PierreSelectedLineRange | null => {
347
+ if (pendingComment) {
348
+ return { start: pendingComment.range.start, end: pendingComment.range.end };
349
+ }
350
+ if (selectedCodeAnnotation) {
351
+ return { start: selectedCodeAnnotation.lineStart, end: selectedCodeAnnotation.lineEnd };
352
+ }
353
+ return null;
354
+ }, [pendingComment, selectedCodeAnnotation]);
355
+ const effectivePrerenderedHTML = lineAnnotations.length === 0 ? prerenderedHTML : undefined;
356
+
357
+ useEffect(() => {
358
+ if (!selectedAnnotationId || !fileAreaRef.current) return;
359
+ const timer = setTimeout(() => {
360
+ fileAreaRef.current
361
+ ?.querySelector(`[data-code-annotation-id="${selectedAnnotationId}"]`)
362
+ ?.scrollIntoView({ behavior: 'smooth', block: 'center' });
363
+ }, 100);
364
+ return () => clearTimeout(timer);
365
+ }, [selectedAnnotationId, filepath]);
366
+
367
+ const openCommentForRange = useCallback((
368
+ range: { start: number; end: number },
369
+ anchorEl?: HTMLElement,
370
+ anchorRect?: DOMRect,
371
+ ) => {
372
+ const start = Math.min(range.start, range.end);
373
+ const end = Math.max(range.start, range.end);
374
+ setPendingComment({
375
+ range: { start, end },
376
+ anchorEl,
377
+ anchorRect: anchorRect ?? anchorEl?.getBoundingClientRect() ?? lastPointerRectRef.current ?? undefined,
378
+ contextText: `${relativePath} ${lineLabel(start, end)}`,
379
+ originalCode: getLineSlice(contents, start, end),
380
+ });
381
+ }, [contents, relativePath]);
382
+
383
+ const openCommentForBrowserSelection = useCallback(() => {
384
+ if (!onAddAnnotation) return;
385
+
386
+ const selection = getPierreSelection(fileAreaRef.current);
387
+ const selectedText = selection?.toString();
388
+ if (!selection || selection.isCollapsed || !selectedText?.trim()) return;
389
+
390
+ const anchorLine = getLineNumberFromSelectionNode(selection.anchorNode);
391
+ const focusLine = getLineNumberFromSelectionNode(selection.focusNode);
392
+ if (anchorLine == null || focusLine == null) return;
393
+
394
+ openCommentForRange(
395
+ { start: anchorLine, end: focusLine },
396
+ undefined,
397
+ lastPointerRectRef.current
398
+ ?? (selection.rangeCount > 0 ? selection.getRangeAt(0).getBoundingClientRect() : undefined),
399
+ );
400
+ selection.removeAllRanges();
401
+ }, [onAddAnnotation, openCommentForRange]);
402
+
403
+ const renderAnnotation = useCallback((annotation: LineAnnotation<CodeAnnotation>) => {
404
+ if (!annotation.metadata) return null;
405
+ return (
406
+ <CodeInlineAnnotation
407
+ annotation={annotation.metadata}
408
+ isSelected={selectedAnnotationId === annotation.metadata.id}
409
+ onSelect={onSelectAnnotation}
410
+ onEdit={onEditAnnotation}
411
+ onDelete={onDeleteAnnotation}
412
+ />
413
+ );
414
+ }, [onDeleteAnnotation, onEditAnnotation, onSelectAnnotation, selectedAnnotationId]);
415
+
416
+ const renderGutterUtility = useCallback((getHoveredLine: () => { lineNumber: number } | undefined) => {
417
+ return (
418
+ <button
419
+ type="button"
420
+ style={gutterButtonStyle}
421
+ title="Add code comment"
422
+ onMouseEnter={(e) => {
423
+ e.currentTarget.style.filter = 'brightness(1.2)';
424
+ }}
425
+ onMouseLeave={(e) => {
426
+ e.currentTarget.style.filter = '';
427
+ }}
428
+ onClick={(e) => {
429
+ e.stopPropagation();
430
+ const line = getHoveredLine();
431
+ if (!line) return;
432
+ openCommentForRange({ start: line.lineNumber, end: line.lineNumber }, e.currentTarget);
433
+ }}
434
+ >
435
+ +
436
+ </button>
437
+ );
438
+ }, [openCommentForRange]);
439
+
440
+ const handleLineSelectionEnd = useCallback((range: PierreSelectedLineRange | null) => {
441
+ if (!onAddAnnotation) return;
442
+ if (!range) return;
443
+ if (range.start !== range.end) {
444
+ suppressLineClickUntilRef.current = Date.now() + 300;
445
+ }
446
+ openCommentForRange({ start: range.start, end: range.end }, undefined, lastPointerRectRef.current ?? undefined);
447
+ }, [onAddAnnotation, openCommentForRange]);
448
+
449
+ const handleLineClick = useCallback((props: LineEventBaseProps & { event: PointerEvent }) => {
450
+ if (!onAddAnnotation) return;
451
+ if (Date.now() < suppressLineClickUntilRef.current) return;
452
+ openCommentForRange(
453
+ { start: props.lineNumber, end: props.lineNumber },
454
+ undefined,
455
+ props.lineElement.getBoundingClientRect(),
456
+ );
457
+ }, [onAddAnnotation, openCommentForRange]);
458
+
459
+ const handleCopy = async () => {
460
+ try {
461
+ await navigator.clipboard.writeText(contents);
462
+ setCopied(true);
463
+ setTimeout(() => setCopied(false), 1500);
464
+ } catch (err) {
465
+ console.error('Failed to copy:', err);
466
+ }
467
+ };
468
+
469
+ if (error) {
470
+ // The server's error string distinguishes "File not found", "Ambiguous
471
+ // path '…'", and other failures (e.g. permission). Earlier this dialog
472
+ // hardcoded "File not found in repo" regardless of cause, which was
473
+ // misleading when an optimistic-link click hit an ambiguous response
474
+ // before validation completed.
475
+ const isNotFound = /^file not found/i.test(error);
476
+ return (
477
+ <PopoutDialog
478
+ open={open}
479
+ onClose={onClose}
480
+ title={requestedPath ?? displayName}
481
+ container={container}
482
+ className="w-[min(520px,calc(100vw-4rem))]"
483
+ >
484
+ <div className="flex flex-col gap-2 px-5 py-6 text-sm">
485
+ <div className="font-medium text-foreground">{error}</div>
486
+ <code className="text-xs font-mono text-muted-foreground break-all">
487
+ {requestedPath ?? filepath}
488
+ </code>
489
+ {isNotFound && (
490
+ <p className="text-xs text-muted-foreground mt-1">
491
+ The path was referenced in the document but no matching file was found
492
+ in this project. It may describe a planned/future file.
493
+ </p>
494
+ )}
495
+ </div>
496
+ </PopoutDialog>
497
+ );
498
+ }
499
+
500
+ return (
501
+ <PopoutDialog
502
+ open={open}
503
+ onClose={onClose}
504
+ title={displayName}
505
+ container={container}
506
+ className="w-[calc(100vw-4rem)] max-w-[min(calc(100vw-4rem),1500px)] h-[calc(100vh-4rem)]"
507
+ >
508
+ <div className="flex items-center gap-3 px-5 pt-4 pb-3 pr-12">
509
+ <div className="flex items-center gap-2 min-w-0">
510
+ <svg className="w-4 h-4 flex-shrink-0 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
511
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
512
+ </svg>
513
+ <span className="text-sm font-medium text-foreground truncate" title={filepath}>
514
+ {relativePath}
515
+ </span>
516
+ </div>
517
+ <div className="ml-auto flex items-center gap-2">
518
+ <span className="text-xs text-muted-foreground tabular-nums">
519
+ {lineCount} lines
520
+ </span>
521
+ <button
522
+ onClick={handleCopy}
523
+ title={copied ? 'Copied!' : 'Copy file contents'}
524
+ className={`p-1.5 rounded-md transition-colors ${
525
+ copied ? 'text-success' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
526
+ }`}
527
+ >
528
+ {copied ? (
529
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
530
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
531
+ </svg>
532
+ ) : (
533
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
534
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
535
+ </svg>
536
+ )}
537
+ </button>
538
+ </div>
539
+ </div>
540
+
541
+ <div className="border-t border-border/30" />
542
+ <div
543
+ ref={fileAreaRef}
544
+ className="flex-1 min-h-0"
545
+ onPointerMove={(e) => {
546
+ lastPointerRectRef.current = new DOMRect(e.clientX, e.clientY, 0, 0);
547
+ }}
548
+ onMouseUp={() => {
549
+ requestAnimationFrame(openCommentForBrowserSelection);
550
+ }}
551
+ >
552
+ <File
553
+ key={filepath}
554
+ file={{ name: displayName, contents }}
555
+ prerenderedHTML={effectivePrerenderedHTML}
556
+ className="h-full"
557
+ lineAnnotations={lineAnnotations}
558
+ selectedLines={selectedLines}
559
+ renderAnnotation={renderAnnotation}
560
+ renderGutterUtility={renderGutterUtility}
561
+ style={{
562
+ '--diffs-dark-bg': colors.bg,
563
+ '--diffs-light-bg': colors.bg,
564
+ '--diffs-dark': colors.fg,
565
+ '--diffs-light': colors.fg,
566
+ } as React.CSSProperties}
567
+ options={{
568
+ themeType: pierreTheme.type,
569
+ unsafeCSS: pierreTheme.css,
570
+ overflow: 'scroll',
571
+ disableFileHeader: true,
572
+ enableLineSelection: true,
573
+ enableGutterUtility: !!onAddAnnotation,
574
+ lineHoverHighlight: onAddAnnotation ? 'line' : 'disabled',
575
+ onLineClick: handleLineClick,
576
+ onLineSelectionEnd: handleLineSelectionEnd,
577
+ }}
578
+ />
579
+ </div>
580
+ {pendingComment && onAddAnnotation && (
581
+ <CommentPopover
582
+ anchorEl={pendingComment.anchorEl}
583
+ anchorRect={pendingComment.anchorRect}
584
+ contextText={pendingComment.contextText}
585
+ isGlobal={false}
586
+ onSubmit={(text, images) => {
587
+ onAddAnnotation({
588
+ filePath: filepath,
589
+ lineStart: pendingComment.range.start,
590
+ lineEnd: pendingComment.range.end,
591
+ text,
592
+ images,
593
+ originalCode: pendingComment.originalCode,
594
+ });
595
+ setPendingComment(null);
596
+ }}
597
+ onClose={() => setPendingComment(null)}
598
+ />
599
+ )}
600
+ </PopoutDialog>
601
+ );
602
+ };
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { ValidatedMap } from "../hooks/useValidatedCodePaths";
3
+
4
+ export interface CodePathValidationContextValue {
5
+ validated: ValidatedMap;
6
+ ready: boolean;
7
+ }
8
+
9
+ export const CodePathValidationContext =
10
+ createContext<CodePathValidationContextValue | null>(null);
11
+
12
+ export function useCodePathValidation(): CodePathValidationContextValue | null {
13
+ return useContext(CodePathValidationContext);
14
+ }