@plannotator/ui 0.52.1 → 0.53.1

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 (76) hide show
  1. package/HANDOFF.md +284 -29
  2. package/README.md +45 -5
  3. package/components/AnnotationPanel.tsx +195 -118
  4. package/components/AnnotationToolbar.tsx +17 -14
  5. package/components/AnnotationToolstrip.tsx +54 -40
  6. package/components/BlockRenderer.tsx +20 -14
  7. package/components/CodeFilePopout.tsx +132 -69
  8. package/components/CommentPopover.tsx +8 -349
  9. package/components/ComposerTokenTextarea.tsx +379 -0
  10. package/components/ExportModal.tsx +6 -1
  11. package/components/HostTitle.tsx +108 -0
  12. package/components/HtmlSurfaceControls.tsx +2 -1
  13. package/components/ImageLightbox.tsx +288 -17
  14. package/components/ImagePeek.tsx +255 -0
  15. package/components/ImageResolverContext.tsx +31 -0
  16. package/components/InlineMarkdown.tsx +123 -15
  17. package/components/LocalLinkContext.tsx +49 -0
  18. package/components/MentionPicker.tsx +51 -0
  19. package/components/MenuVersionSection.tsx +51 -25
  20. package/components/OpenInAppButton.tsx +18 -49
  21. package/components/PlanHeaderMenu.tsx +2 -8
  22. package/components/ProviderIcons.tsx +10 -4
  23. package/components/RenderedMarkdown.tsx +40 -4
  24. package/components/Settings.tsx +33 -5
  25. package/components/SkillReferenceMenu.tsx +67 -18
  26. package/components/Tooltip.tsx +51 -4
  27. package/components/Viewer.tsx +220 -49
  28. package/components/ai/AIProviderBar.tsx +11 -0
  29. package/components/ai/AskSessionSetupHint.tsx +12 -0
  30. package/components/ai/DocumentAIChatPanel.tsx +17 -5
  31. package/components/ai/SessionAskNotice.tsx +4 -3
  32. package/components/blocks/BlockquoteBlock.tsx +159 -0
  33. package/components/blocks/HtmlBlock.tsx +121 -9
  34. package/components/blocks/QuestionBlock.tsx +183 -96
  35. package/components/blocks/QuestionContext.tsx +3 -0
  36. package/components/diagram/DiagramViewer.tsx +41 -4
  37. package/components/diagram/index.ts +1 -1
  38. package/components/html-viewer/HtmlViewer.tsx +95 -3
  39. package/components/html-viewer/bridge-script.asset.js +413 -7
  40. package/components/html-viewer/bridge-script.lite.ts +1 -1
  41. package/components/html-viewer/bridge-script.ts +454 -7
  42. package/components/html-viewer/useHtmlAnnotation.ts +51 -3
  43. package/components/icons/AgentIcons.tsx +9 -7
  44. package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
  45. package/components/plan-diff/PlanDiffViewer.tsx +6 -1
  46. package/configure.ts +27 -1
  47. package/hooks/useAgentJobs.ts +8 -3
  48. package/hooks/useAnnotationHighlighter.ts +128 -23
  49. package/hooks/useExternalAnnotations.ts +6 -0
  50. package/hooks/useMentionAutocomplete.ts +1 -1
  51. package/hooks/useOpenInApps.ts +88 -0
  52. package/hooks/useSkillReferenceAutocomplete.ts +7 -3
  53. package/hooks/useUpdateCheck.ts +38 -98
  54. package/hooks/useUpdateCheckSetting.ts +43 -0
  55. package/hooks/useUpdateToast.ts +101 -0
  56. package/hooks/useVimSelection.ts +9 -0
  57. package/package.json +2 -2
  58. package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
  59. package/shortcuts/index.ts +1 -0
  60. package/shortcuts/runtime.ts +10 -3
  61. package/styles.css +1 -1
  62. package/types.ts +48 -0
  63. package/utils/agentClosed.ts +26 -0
  64. package/utils/agentToolSetting.ts +16 -9
  65. package/utils/aiProvider.ts +7 -0
  66. package/utils/askSessionSetupHint.ts +27 -0
  67. package/utils/blockTargeting.ts +14 -3
  68. package/utils/mentions.ts +34 -5
  69. package/utils/parser.ts +65 -51
  70. package/utils/pasteInsert.ts +234 -0
  71. package/utils/planDiffEngine.ts +272 -2
  72. package/utils/skillCatalog.ts +15 -2
  73. package/utils/skillReferences.ts +50 -13
  74. package/utils/storage.ts +5 -0
  75. package/utils/textCut.ts +41 -0
  76. package/utils/updateNotice.ts +120 -0
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useLayoutEffect, useEffect } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import type { EditorMode, InputMethod } from '../types';
3
3
  import { TaterSpritePullup } from './TaterSpritePullup';
4
4
 
@@ -36,6 +36,12 @@ export interface AnnotationToolstripProps {
36
36
  * retain the complete action-mode group.
37
37
  */
38
38
  hideQuickLabel?: boolean;
39
+ /**
40
+ * Omit only the Redline action. Defaults to false. For a host whose
41
+ * composer is its own (the Inbox surface on a phone draws Select, Pinpoint,
42
+ * Markup and Comment).
43
+ */
44
+ hideRedline?: boolean;
39
45
  }
40
46
 
41
47
  /** Render the shared input-method and annotation-mode controls. */
@@ -50,6 +56,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
50
56
  iconOnly = false,
51
57
  hideInputMethodSwitch = false,
52
58
  hideQuickLabel = false,
59
+ hideRedline = false,
53
60
  }) => {
54
61
  const [showHelp, setShowHelp] = useState(false);
55
62
  const [helpTab, setHelpTab] = useState<'selection' | 'plannotator'>('selection');
@@ -136,6 +143,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
136
143
  </svg>
137
144
  }
138
145
  />
146
+ {!hideRedline && (
139
147
  <ToolstripButton
140
148
  active={mode === 'redline'}
141
149
  onClick={() => onModeChange('redline')}
@@ -150,6 +158,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
150
158
  </svg>
151
159
  }
152
160
  />
161
+ )}
153
162
  {!hideQuickLabel && (
154
163
  <ToolstripButton
155
164
  active={mode === 'quickLabel'}
@@ -282,10 +291,26 @@ const ICON_SIZE = 28; // collapsed button width (px)
282
291
  const H_PAD = 10; // horizontal padding when expanded (px) — matches px-2.5
283
292
  const GAP = 6; // gap between icon and label (px) — matches gap-1.5
284
293
  const ICON_INNER = 14; // icon element width (px)
294
+ const COLLAPSED_PAD = (ICON_SIZE - ICON_INNER) / 2;
285
295
  const DURATION = 180; // transition ms
296
+ const EASE = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
286
297
 
287
298
  /* ─── Button ─── */
288
299
 
300
+ /**
301
+ * One toolstrip button: the icon, and a label that slides out beside it.
302
+ *
303
+ * The width comes from layout, never from a JS measurement. A hidden copy of
304
+ * the label sits in a one-column grid whose column animates `0fr` -> `1fr`, so
305
+ * the button's shrink-to-fit width runs from the icon-only 28 px to the icon,
306
+ * the gap and the label; the side padding animates 7 -> 10 px with it. The
307
+ * width at every frame matches the explicit-width version this replaced
308
+ * (28 + (12 + label) x progress, same 180 ms curve). The visible label is
309
+ * positioned beside the icon and clipped by the button's own edge, as before:
310
+ * a grid track laid out at a fraction of its width reveals its content more
311
+ * slowly than the button grows, so the label is not drawn inside it.
312
+ * Reading `offsetWidth` per button forced a layout on every document open.
313
+ */
289
314
  const ToolstripButton: React.FC<{
290
315
  active: boolean;
291
316
  onClick: () => void;
@@ -297,18 +322,9 @@ const ToolstripButton: React.FC<{
297
322
  iconOnly?: boolean;
298
323
  }> = ({ active, onClick, icon, label, color, mounted, compact = false, iconOnly = false }) => {
299
324
  const [hovered, setHovered] = useState(false);
300
- const [labelWidth, setLabelWidth] = useState(0);
301
- const measureRef = useRef<HTMLSpanElement>(null);
302
325
  const styles = colorStyles[color];
303
326
  const [isTouchDevice] = useState(() => 'ontouchstart' in window || navigator.maxTouchPoints > 0);
304
327
 
305
- // Measure label text width synchronously before first paint
306
- useLayoutEffect(() => {
307
- if (measureRef.current) {
308
- setLabelWidth(measureRef.current.offsetWidth);
309
- }
310
- }, [label]);
311
-
312
328
  // iconOnly: never expand (mobile sticky lane).
313
329
  // compact: only active expands (sm+ sticky lane — shows current mode).
314
330
  // default: active or hovered expands (top-of-doc full toolstrip).
@@ -317,8 +333,7 @@ const ToolstripButton: React.FC<{
317
333
  : compact
318
334
  ? active
319
335
  : (active || hovered || isTouchDevice);
320
- const expandedWidth = H_PAD + ICON_INNER + GAP + labelWidth + H_PAD;
321
- const currentWidth = expanded ? expandedWidth : ICON_SIZE;
336
+ const pad = expanded ? H_PAD : COLLAPSED_PAD;
322
337
 
323
338
  const colorClass = active
324
339
  ? styles.active
@@ -326,12 +341,9 @@ const ToolstripButton: React.FC<{
326
341
  ? styles.hover
327
342
  : styles.inactive;
328
343
 
344
+ const slide = (property: string) => `${property} ${DURATION}ms ${EASE}`;
329
345
  const transition = mounted
330
- ? `width ${DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94), background-color ${DURATION}ms ease, color ${DURATION}ms ease, box-shadow ${DURATION}ms ease`
331
- : 'none';
332
-
333
- const innerTransition = mounted
334
- ? `padding-left ${DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
346
+ ? `${slide('padding-inline-start')}, ${slide('padding-inline-end')}, background-color ${DURATION}ms ease, color ${DURATION}ms ease, box-shadow ${DURATION}ms ease`
335
347
  : 'none';
336
348
 
337
349
  return (
@@ -340,34 +352,36 @@ const ToolstripButton: React.FC<{
340
352
  onMouseEnter={() => setHovered(true)}
341
353
  onMouseLeave={() => setHovered(false)}
342
354
  aria-pressed={active}
343
- className={`relative flex items-center h-7 rounded-md overflow-hidden ${colorClass}`}
344
- style={{ width: currentWidth, transition }}
355
+ className={`relative flex items-center h-7 rounded-md overflow-hidden whitespace-nowrap ${colorClass}`}
356
+ style={{ paddingInlineStart: pad, paddingInlineEnd: pad, transition }}
345
357
  >
346
- {/* Inner flex container — fixed layout, no layout-shifting properties animated */}
347
- <div
348
- className="flex items-center whitespace-nowrap"
349
- style={{ paddingLeft: expanded ? H_PAD : (ICON_SIZE - ICON_INNER) / 2, gap: GAP, transition: innerTransition }}
358
+ {icon}
359
+ {/* The label people see, beside the icon, clipped at the button's edge. */}
360
+ <span
361
+ className="absolute inset-y-0 flex items-center text-xs font-medium"
362
+ style={{
363
+ insetInlineStart: pad + ICON_INNER + GAP,
364
+ opacity: expanded ? 1 : 0,
365
+ transition: mounted
366
+ ? `${slide('inset-inline-start')}, opacity ${expanded ? DURATION : DURATION * 0.6}ms ease ${expanded ? '60ms' : '0ms'}`
367
+ : 'none',
368
+ }}
350
369
  >
351
- {icon}
352
- <span
353
- className="text-xs font-medium"
354
- style={{
355
- opacity: expanded ? 1 : 0,
356
- transition: mounted ? `opacity ${expanded ? DURATION : DURATION * 0.6}ms ease ${expanded ? '60ms' : '0ms'}` : 'none',
357
- }}
358
- >
359
- {label}
360
- </span>
361
- </div>
362
-
363
- {/* Hidden measurement span — rendered offscreen to get label pixel width */}
370
+ {label}
371
+ </span>
372
+ {/* The width: a hidden copy of the gap and label in a column that
373
+ animates 0fr -> 1fr. min-width 0 and overflow hidden let it reach 0. */}
364
374
  <span
365
- ref={measureRef}
366
- className="text-xs font-medium absolute pointer-events-none"
367
- style={{ visibility: 'hidden', position: 'absolute', left: -9999 }}
368
375
  aria-hidden
376
+ style={{
377
+ display: 'grid',
378
+ gridTemplateColumns: expanded ? '1fr' : '0fr',
379
+ transition: mounted ? slide('grid-template-columns') : 'none',
380
+ }}
369
381
  >
370
- {label}
382
+ <span style={{ minWidth: 0, overflow: 'hidden', visibility: 'hidden' }}>
383
+ <span className="block text-xs font-medium" style={{ paddingInlineStart: GAP }}>{label}</span>
384
+ </span>
371
385
  </span>
372
386
  </button>
373
387
  );
@@ -6,6 +6,7 @@ import { CodeBlock } from "./blocks/CodeBlock";
6
6
  import { HtmlBlock } from "./blocks/HtmlBlock";
7
7
  import { Callout } from "./blocks/Callout";
8
8
  import { AlertBlock } from "./blocks/AlertBlock";
9
+ import { BlockquoteBlock } from "./blocks/BlockquoteBlock";
9
10
  import { TableBlock } from "./blocks/TableBlock";
10
11
  import { MathBlock } from "./blocks/MathBlock";
11
12
  import { QuestionBlock, type QuestionBlockProps } from "./blocks/QuestionBlock";
@@ -61,7 +62,11 @@ export const BlockRenderer: React.FC<{
61
62
  onOpenQuestionDecision?: QuestionBlockProps['onOpenDecision'];
62
63
  /** `'none'` hides the card's own status tag (see `QuestionBlock`). */
63
64
  questionStatusTag?: QuestionBlockProps['statusTag'];
64
- }> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor, question, questionTotal, questionAnswer, onAnswerQuestion, onSaveQuestionAnswer, saveQuestionAnswerLabel, renderQuestionFooter, questionDecisionRecording, questionDecisionScope, onToggleQuestionDecisionRecording, onOpenQuestionDecision, questionStatusTag }) => {
65
+ /** The host's words for a question's status tag (see `QuestionBlock`). */
66
+ questionStatusLabel?: QuestionBlockProps['statusLabel'];
67
+ /** Host content under a question's prompt (see `QuestionBlock`). */
68
+ renderQuestionBelowPrompt?: QuestionBlockProps['renderBelowPrompt'];
69
+ }> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor, question, questionTotal, questionAnswer, onAnswerQuestion, onSaveQuestionAnswer, saveQuestionAnswerLabel, renderQuestionFooter, questionDecisionRecording, questionDecisionScope, onToggleQuestionDecisionRecording, onOpenQuestionDecision, questionStatusTag, questionStatusLabel, renderQuestionBelowPrompt }) => {
65
70
  switch (block.type) {
66
71
  case 'heading': {
67
72
  const Tag = `h${block.level || 1}` as React.ElementType;
@@ -99,20 +104,19 @@ export const BlockRenderer: React.FC<{
99
104
  />
100
105
  );
101
106
  }
102
- // Content may span multiple merged `>` lines. Split on blank-line
103
- // paragraph breaks so `> a\n>\n> b` renders as two <p> children.
104
- const paragraphs = block.content.split(/\n\n+/);
107
+ // One quote whatever it holds: its content is the quote's own markdown
108
+ // (paragraphs, code fences, lists, nested quotes), see BlockquoteBlock.
105
109
  return (
106
- <blockquote
107
- className="border-l-2 border-primary/50 pl-4 my-4 text-muted-foreground italic"
108
- data-block-id={block.id}
109
- >
110
- {paragraphs.map((para, i) => (
111
- <p key={i} className={i > 0 ? 'mt-2' : ''}>
112
- <InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={para} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} repoHost={repoHost} onNavigateAnchor={onNavigateAnchor} />
113
- </p>
114
- ))}
115
- </blockquote>
110
+ <BlockquoteBlock
111
+ block={block}
112
+ onOpenLinkedDoc={onOpenLinkedDoc}
113
+ onOpenCodeFile={onOpenCodeFile}
114
+ imageBaseDir={imageBaseDir}
115
+ onImageClick={onImageClick}
116
+ githubRepo={githubRepo}
117
+ repoHost={repoHost}
118
+ onNavigateAnchor={onNavigateAnchor}
119
+ />
116
120
  );
117
121
  }
118
122
 
@@ -193,6 +197,8 @@ export const BlockRenderer: React.FC<{
193
197
  onToggleDecisionRecording={onToggleQuestionDecisionRecording}
194
198
  onOpenDecision={onOpenQuestionDecision}
195
199
  statusTag={questionStatusTag}
200
+ statusLabel={questionStatusLabel}
201
+ renderBelowPrompt={renderQuestionBelowPrompt}
196
202
  onOpenLinkedDoc={onOpenLinkedDoc}
197
203
  onOpenCodeFile={onOpenCodeFile}
198
204
  imageBaseDir={imageBaseDir}
@@ -92,7 +92,7 @@ function buildPierreCSS(mode: 'dark' | 'light', bg: string, fg: string): string
92
92
  pre, code { background-color: ${bg} !important; }
93
93
  [data-column-number] { background-color: ${bg} !important; }
94
94
  [data-file] { height: 100% !important; }
95
- [data-code] { height: 100% !important; overflow-y: auto !important; }
95
+ [data-code] { height: 100% !important; overflow-y: auto !important; align-content: start; }
96
96
  [data-line] { cursor: pointer; }
97
97
  `;
98
98
  }
@@ -180,7 +180,7 @@ const CodeInlineAnnotation: React.FC<{
180
180
  <span className="rounded bg-muted px-1.5 py-0.5 font-mono normal-case text-foreground">
181
181
  {lineLabel(annotation.lineStart, annotation.lineEnd)}
182
182
  </span>
183
- {annotation.author && <span className="truncate normal-case">by {annotation.author}</span>}
183
+ {annotation.author && <span className="truncate normal-case">by <bdi>{annotation.author}</bdi></span>}
184
184
  <div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 [@media(hover:none)]:opacity-100">
185
185
  {onEdit && !isEditing && (
186
186
  <button
@@ -283,21 +283,46 @@ const CodeInlineAnnotation: React.FC<{
283
283
  );
284
284
  };
285
285
 
286
- export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
287
- open,
288
- onClose,
286
+ export interface CodeFileViewProps {
287
+ filepath: string;
288
+ contents: string;
289
+ prerenderedHTML?: string;
290
+ annotations?: CodeAnnotation[];
291
+ selectedAnnotationId?: string | null;
292
+ onAddAnnotation?: (annotation: CodeFileAnnotationInput) => void;
293
+ onEditAnnotation?: (id: string, updates: Partial<CodeAnnotation>) => void;
294
+ onDeleteAnnotation?: (id: string) => void;
295
+ onSelectAnnotation?: (id: string | null) => void;
296
+ /** Whether the comment composer offers image attachments. Default true (the popout's behavior). */
297
+ allowImages?: boolean;
298
+ /** The file area's class. Default: fills the rest of a flex column. */
299
+ className?: string;
300
+ /** The highlighting language, instead of Pierre's inference from the name (`'text'`: no highlighting, for a very large file). */
301
+ lang?: string;
302
+ /** Long lines: `'scroll'` sideways (the default) or `'wrap'` onto the next row, for a minified file. */
303
+ overflow?: 'scroll' | 'wrap';
304
+ }
305
+
306
+ /**
307
+ * One code file drawn by Pierre's `File`, with line and range comments: the
308
+ * gutter `+`, a click on a line, a dragged range or a text selection opens
309
+ * the comment composer, and saved comments render inline under their lines.
310
+ * The body of `CodeFilePopout`, usable on its own (the Inbox's file pane).
311
+ */
312
+ export const CodeFileView: React.FC<CodeFileViewProps> = ({
289
313
  filepath,
290
314
  contents,
291
315
  prerenderedHTML,
292
- error,
293
- requestedPath,
294
316
  annotations = [],
295
317
  selectedAnnotationId,
296
318
  onAddAnnotation,
297
319
  onEditAnnotation,
298
320
  onDeleteAnnotation,
299
321
  onSelectAnnotation,
300
- container,
322
+ allowImages = true,
323
+ className = 'flex-1 min-h-0',
324
+ lang,
325
+ overflow = 'scroll',
301
326
  }) => {
302
327
  const { resolvedMode } = useTheme();
303
328
  const mode = resolvedMode ?? 'dark';
@@ -306,7 +331,6 @@ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
306
331
  type: mode as 'dark' | 'light',
307
332
  css: buildPierreCSS(mode, colors.bg, colors.fg),
308
333
  }));
309
- const [copied, setCopied] = useState(false);
310
334
  const [pendingComment, setPendingComment] = useState<PendingComment | null>(null);
311
335
  const fileAreaRef = useRef<HTMLDivElement>(null);
312
336
  const lastPointerRectRef = useRef<DOMRect | null>(null);
@@ -328,7 +352,6 @@ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
328
352
 
329
353
  const displayName = filepath.split('/').pop() || filepath;
330
354
  const relativePath = filepath.replace(/.*\/(?=.*\/)/, '');
331
- const lineCount = useMemo(() => contents.split('\n').length, [contents]);
332
355
  const selectedCodeAnnotation = useMemo(
333
356
  () => annotations.find((ann) => ann.id === selectedAnnotationId),
334
357
  [annotations, selectedAnnotationId],
@@ -457,6 +480,94 @@ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
457
480
  );
458
481
  }, [onAddAnnotation, openCommentForRange]);
459
482
 
483
+ return (
484
+ <>
485
+ <div
486
+ ref={fileAreaRef}
487
+ className={className}
488
+ onPointerMove={(e) => {
489
+ lastPointerRectRef.current = new DOMRect(e.clientX, e.clientY, 0, 0);
490
+ }}
491
+ onMouseUp={() => {
492
+ requestAnimationFrame(openCommentForBrowserSelection);
493
+ }}
494
+ >
495
+ <File
496
+ key={filepath}
497
+ file={lang ? { name: displayName, contents, lang } : { name: displayName, contents }}
498
+ prerenderedHTML={effectivePrerenderedHTML}
499
+ className="h-full"
500
+ lineAnnotations={lineAnnotations}
501
+ selectedLines={selectedLines}
502
+ renderAnnotation={renderAnnotation}
503
+ renderGutterUtility={renderGutterUtility}
504
+ style={{
505
+ '--diffs-dark-bg': colors.bg,
506
+ '--diffs-light-bg': colors.bg,
507
+ '--diffs-dark': colors.fg,
508
+ '--diffs-light': colors.fg,
509
+ } as React.CSSProperties}
510
+ options={{
511
+ themeType: pierreTheme.type,
512
+ unsafeCSS: pierreTheme.css,
513
+ overflow,
514
+ disableFileHeader: true,
515
+ enableLineSelection: true,
516
+ enableGutterUtility: !!onAddAnnotation,
517
+ lineHoverHighlight: onAddAnnotation ? 'line' : 'disabled',
518
+ onLineClick: handleLineClick,
519
+ onLineSelectionEnd: handleLineSelectionEnd,
520
+ }}
521
+ />
522
+ </div>
523
+ {pendingComment && onAddAnnotation && (
524
+ <CommentPopover
525
+ anchorEl={pendingComment.anchorEl}
526
+ anchorRect={pendingComment.anchorRect}
527
+ contextText={pendingComment.contextText}
528
+ isGlobal={false}
529
+ skillReferences
530
+ allowImages={allowImages}
531
+ onSubmit={(text, images) => {
532
+ onAddAnnotation({
533
+ filePath: filepath,
534
+ lineStart: pendingComment.range.start,
535
+ lineEnd: pendingComment.range.end,
536
+ text,
537
+ images,
538
+ originalCode: pendingComment.originalCode,
539
+ });
540
+ setPendingComment(null);
541
+ }}
542
+ onClose={() => setPendingComment(null)}
543
+ />
544
+ )}
545
+ </>
546
+ );
547
+ };
548
+
549
+ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
550
+ open,
551
+ onClose,
552
+ filepath,
553
+ contents,
554
+ prerenderedHTML,
555
+ error,
556
+ requestedPath,
557
+ annotations = [],
558
+ selectedAnnotationId,
559
+ onAddAnnotation,
560
+ onEditAnnotation,
561
+ onDeleteAnnotation,
562
+ onSelectAnnotation,
563
+ container,
564
+ }) => {
565
+ const [copied, setCopied] = useState(false);
566
+
567
+ const displayName = filepath.split('/').pop() || filepath;
568
+ const relativePath = filepath.replace(/.*\/(?=.*\/)/, '');
569
+ const lineCount = useMemo(() => contents.split('\n').length, [contents]);
570
+
460
571
  const handleCopy = async () => {
461
572
  if (await copyTextToClipboard(contents)) {
462
573
  setCopied(true);
@@ -539,65 +650,17 @@ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
539
650
  </div>
540
651
 
541
652
  <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
- skillReferences
587
- onSubmit={(text, images) => {
588
- onAddAnnotation({
589
- filePath: filepath,
590
- lineStart: pendingComment.range.start,
591
- lineEnd: pendingComment.range.end,
592
- text,
593
- images,
594
- originalCode: pendingComment.originalCode,
595
- });
596
- setPendingComment(null);
597
- }}
598
- onClose={() => setPendingComment(null)}
599
- />
600
- )}
653
+ <CodeFileView
654
+ filepath={filepath}
655
+ contents={contents}
656
+ prerenderedHTML={prerenderedHTML}
657
+ annotations={annotations}
658
+ selectedAnnotationId={selectedAnnotationId}
659
+ onAddAnnotation={onAddAnnotation}
660
+ onEditAnnotation={onEditAnnotation}
661
+ onDeleteAnnotation={onDeleteAnnotation}
662
+ onSelectAnnotation={onSelectAnnotation}
663
+ />
601
664
  </PopoutDialog>
602
665
  );
603
666
  };