@agent-native/core 0.41.1 → 0.43.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 (181) hide show
  1. package/README.md +17 -56
  2. package/dist/action.d.ts +13 -1
  3. package/dist/action.d.ts.map +1 -1
  4. package/dist/action.js.map +1 -1
  5. package/dist/agent/production-agent.d.ts +8 -0
  6. package/dist/agent/production-agent.d.ts.map +1 -1
  7. package/dist/agent/production-agent.js +93 -0
  8. package/dist/agent/production-agent.js.map +1 -1
  9. package/dist/cli/app-skill.d.ts +16 -0
  10. package/dist/cli/app-skill.d.ts.map +1 -1
  11. package/dist/cli/app-skill.js +33 -3
  12. package/dist/cli/app-skill.js.map +1 -1
  13. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  14. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  15. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  16. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  17. package/dist/cli/recap.d.ts.map +1 -1
  18. package/dist/cli/recap.js +38 -16
  19. package/dist/cli/recap.js.map +1 -1
  20. package/dist/cli/skills.d.ts +30 -3
  21. package/dist/cli/skills.d.ts.map +1 -1
  22. package/dist/cli/skills.js +180 -114
  23. package/dist/cli/skills.js.map +1 -1
  24. package/dist/client/AssistantChat.d.ts.map +1 -1
  25. package/dist/client/AssistantChat.js +2 -2
  26. package/dist/client/AssistantChat.js.map +1 -1
  27. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  28. package/dist/client/agent-chat-adapter.js +172 -5
  29. package/dist/client/agent-chat-adapter.js.map +1 -1
  30. package/dist/client/blocks/index.d.ts +11 -0
  31. package/dist/client/blocks/index.d.ts.map +1 -1
  32. package/dist/client/blocks/index.js +11 -0
  33. package/dist/client/blocks/index.js.map +1 -1
  34. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts +19 -0
  35. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  36. package/dist/client/blocks/library/AnnotatedCodeBlock.js +6 -58
  37. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  38. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
  39. package/dist/client/blocks/library/ApiEndpointBlock.js +116 -7
  40. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
  41. package/dist/client/blocks/library/DataModelBlock.d.ts.map +1 -1
  42. package/dist/client/blocks/library/DataModelBlock.js +75 -9
  43. package/dist/client/blocks/library/DataModelBlock.js.map +1 -1
  44. package/dist/client/blocks/library/DiffBlock.d.ts +1 -1
  45. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  46. package/dist/client/blocks/library/DiffBlock.js +265 -39
  47. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  48. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  49. package/dist/client/blocks/library/FileTreeBlock.js +27 -4
  50. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  51. package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
  52. package/dist/client/blocks/library/HighlightedCode.js +1 -1
  53. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  54. package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
  55. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
  56. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  57. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  58. package/dist/client/blocks/library/annotation-rail.d.ts +115 -0
  59. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -0
  60. package/dist/client/blocks/library/annotation-rail.js +139 -0
  61. package/dist/client/blocks/library/annotation-rail.js.map +1 -0
  62. package/dist/client/blocks/library/api-endpoint.config.d.ts +31 -6
  63. package/dist/client/blocks/library/api-endpoint.config.d.ts.map +1 -1
  64. package/dist/client/blocks/library/api-endpoint.config.js +30 -6
  65. package/dist/client/blocks/library/api-endpoint.config.js.map +1 -1
  66. package/dist/client/blocks/library/callout.config.d.ts +29 -0
  67. package/dist/client/blocks/library/callout.config.d.ts.map +1 -0
  68. package/dist/client/blocks/library/callout.config.js +33 -0
  69. package/dist/client/blocks/library/callout.config.js.map +1 -0
  70. package/dist/client/blocks/library/callout.d.ts +20 -0
  71. package/dist/client/blocks/library/callout.d.ts.map +1 -0
  72. package/dist/client/blocks/library/callout.js +61 -0
  73. package/dist/client/blocks/library/callout.js.map +1 -0
  74. package/dist/client/blocks/library/checklist.d.ts.map +1 -1
  75. package/dist/client/blocks/library/checklist.js +3 -3
  76. package/dist/client/blocks/library/checklist.js.map +1 -1
  77. package/dist/client/blocks/library/code.d.ts.map +1 -1
  78. package/dist/client/blocks/library/code.js +32 -15
  79. package/dist/client/blocks/library/code.js.map +1 -1
  80. package/dist/client/blocks/library/columns.d.ts.map +1 -1
  81. package/dist/client/blocks/library/columns.js +56 -35
  82. package/dist/client/blocks/library/columns.js.map +1 -1
  83. package/dist/client/blocks/library/data-model.config.d.ts +17 -0
  84. package/dist/client/blocks/library/data-model.config.d.ts.map +1 -1
  85. package/dist/client/blocks/library/data-model.config.js +15 -0
  86. package/dist/client/blocks/library/data-model.config.js.map +1 -1
  87. package/dist/client/blocks/library/decision.config.d.ts +37 -0
  88. package/dist/client/blocks/library/decision.config.d.ts.map +1 -0
  89. package/dist/client/blocks/library/decision.config.js +32 -0
  90. package/dist/client/blocks/library/decision.config.js.map +1 -0
  91. package/dist/client/blocks/library/decision.d.ts +19 -0
  92. package/dist/client/blocks/library/decision.d.ts.map +1 -0
  93. package/dist/client/blocks/library/decision.js +119 -0
  94. package/dist/client/blocks/library/decision.js.map +1 -0
  95. package/dist/client/blocks/library/diagram.config.d.ts +64 -0
  96. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -0
  97. package/dist/client/blocks/library/diagram.config.js +111 -0
  98. package/dist/client/blocks/library/diagram.config.js.map +1 -0
  99. package/dist/client/blocks/library/diagram.d.ts +16 -0
  100. package/dist/client/blocks/library/diagram.d.ts.map +1 -0
  101. package/dist/client/blocks/library/diagram.js +261 -0
  102. package/dist/client/blocks/library/diagram.js.map +1 -0
  103. package/dist/client/blocks/library/diff.config.d.ts +28 -6
  104. package/dist/client/blocks/library/diff.config.d.ts.map +1 -1
  105. package/dist/client/blocks/library/diff.config.js +30 -6
  106. package/dist/client/blocks/library/diff.config.js.map +1 -1
  107. package/dist/client/blocks/library/question-form.config.d.ts +69 -0
  108. package/dist/client/blocks/library/question-form.config.d.ts.map +1 -0
  109. package/dist/client/blocks/library/question-form.config.js +58 -0
  110. package/dist/client/blocks/library/question-form.config.js.map +1 -0
  111. package/dist/client/blocks/library/question-form.d.ts +20 -0
  112. package/dist/client/blocks/library/question-form.d.ts.map +1 -0
  113. package/dist/client/blocks/library/question-form.js +286 -0
  114. package/dist/client/blocks/library/question-form.js.map +1 -0
  115. package/dist/client/blocks/library/sanitize-html.d.ts +5 -0
  116. package/dist/client/blocks/library/sanitize-html.d.ts.map +1 -0
  117. package/dist/client/blocks/library/sanitize-html.js +240 -0
  118. package/dist/client/blocks/library/sanitize-html.js.map +1 -0
  119. package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
  120. package/dist/client/blocks/library/server-specs.js +59 -0
  121. package/dist/client/blocks/library/server-specs.js.map +1 -1
  122. package/dist/client/blocks/library/specs.d.ts.map +1 -1
  123. package/dist/client/blocks/library/specs.js +11 -0
  124. package/dist/client/blocks/library/specs.js.map +1 -1
  125. package/dist/client/blocks/library/tabs.d.ts.map +1 -1
  126. package/dist/client/blocks/library/tabs.js +12 -12
  127. package/dist/client/blocks/library/tabs.js.map +1 -1
  128. package/dist/client/blocks/library/wireframe-kit.d.ts +260 -0
  129. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -0
  130. package/dist/client/blocks/library/wireframe-kit.js +920 -0
  131. package/dist/client/blocks/library/wireframe-kit.js.map +1 -0
  132. package/dist/client/blocks/library/wireframe.config.d.ts +123 -0
  133. package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -0
  134. package/dist/client/blocks/library/wireframe.config.js +294 -0
  135. package/dist/client/blocks/library/wireframe.config.js.map +1 -0
  136. package/dist/client/blocks/library/wireframe.d.ts +15 -0
  137. package/dist/client/blocks/library/wireframe.d.ts.map +1 -0
  138. package/dist/client/blocks/library/wireframe.js +206 -0
  139. package/dist/client/blocks/library/wireframe.js.map +1 -0
  140. package/dist/client/blocks/registry.d.ts +9 -0
  141. package/dist/client/blocks/registry.d.ts.map +1 -1
  142. package/dist/client/blocks/registry.js +12 -5
  143. package/dist/client/blocks/registry.js.map +1 -1
  144. package/dist/client/blocks/server.d.ts +1 -0
  145. package/dist/client/blocks/server.d.ts.map +1 -1
  146. package/dist/client/blocks/server.js +1 -0
  147. package/dist/client/blocks/server.js.map +1 -1
  148. package/dist/client/blocks/types.d.ts +10 -2
  149. package/dist/client/blocks/types.d.ts.map +1 -1
  150. package/dist/client/blocks/types.js.map +1 -1
  151. package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
  152. package/dist/client/rich-markdown-editor/DragHandle.js +152 -21
  153. package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
  154. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts +25 -1
  155. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
  156. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +29 -6
  157. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  158. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +8 -1
  159. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  160. package/dist/client/rich-markdown-editor/SharedRichEditor.js +5 -1
  161. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  162. package/dist/extensions/actions.d.ts.map +1 -1
  163. package/dist/extensions/actions.js +159 -12
  164. package/dist/extensions/actions.js.map +1 -1
  165. package/dist/extensions/store.d.ts +21 -0
  166. package/dist/extensions/store.d.ts.map +1 -1
  167. package/dist/extensions/store.js +33 -1
  168. package/dist/extensions/store.js.map +1 -1
  169. package/dist/server/recap-image-route.d.ts.map +1 -1
  170. package/dist/server/recap-image-route.js +12 -3
  171. package/dist/server/recap-image-route.js.map +1 -1
  172. package/dist/styles/agent-native.css +1 -0
  173. package/dist/styles/blocks.css +1380 -0
  174. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +30 -5
  175. package/docs/content/plan-plugin.md +107 -0
  176. package/docs/content/pr-visual-recap.md +2 -2
  177. package/docs/content/skills-guide.md +8 -0
  178. package/docs/content/template-plan.md +94 -17
  179. package/package.json +2 -1
  180. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +30 -5
  181. package/docs/content/visual-plans.md +0 -80
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
3
- import { IconChevronRight, IconColumns, IconDotsVertical, IconFileDiff, IconList, } from "@tabler/icons-react";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { IconChevronRight, IconColumns, IconDotsVertical, IconFileDiff, IconList, IconPlus, IconTrash, } from "@tabler/icons-react";
4
4
  import { common, createLowlight } from "lowlight";
5
5
  import { cn } from "../../utils.js";
6
+ import { AnnotationGutterMarker, InlineAnnotationNote, buildLineMarkerMap, hasRailAnnotations, resolveAnnotations, } from "./annotation-rail.js";
6
7
  import { DevInput, DevLabel, DevTextarea, DevSelect } from "./dev-doc-ui.js";
7
8
  /**
8
9
  * Split text into lines, each KEEPING its trailing newline (so the change
@@ -228,6 +229,19 @@ function SyntaxHighlightedLine({ code, language, }) {
228
229
  }, [code, language]);
229
230
  return _jsx(_Fragment, { children: highlighted ?? code });
230
231
  }
232
+ /** The default side an annotation targets when `side` is omitted. */
233
+ function annotationSide(annotation) {
234
+ return annotation.side === "before" ? "before" : "after";
235
+ }
236
+ /**
237
+ * Count 1-based source lines in a side's text, matching how `buildRows` numbers
238
+ * `oldNo`/`newNo`: a trailing newline does not add a phantom final line.
239
+ */
240
+ function countLines(text) {
241
+ if (text === "")
242
+ return 0;
243
+ return splitLines(text).length;
244
+ }
231
245
  /** Number of context lines above which an unchanged run is collapsed. */
232
246
  const COLLAPSE_THRESHOLD = 6;
233
247
  /** Context lines kept visible at each edge of a collapsed run. */
@@ -272,9 +286,32 @@ function buildRows(changes) {
272
286
  * Group rows into segments, collapsing interior runs of >COLLAPSE_THRESHOLD
273
287
  * context rows (keeping CONTEXT_EDGE visible at each side). Leading/trailing runs
274
288
  * collapse too, but keep only the inner edge visible.
289
+ *
290
+ * `isAnchored` marks context rows that carry an annotation (or sit adjacent to
291
+ * one): an anchored row is NEVER hidden inside a collapsed run, so a note that
292
+ * targets an unchanged line stays reachable. An anchor splits its run into the
293
+ * separately-collapsible spans on either side of it, with CONTEXT_EDGE rows kept
294
+ * visible around the anchor.
275
295
  */
276
- function segmentRows(rows) {
296
+ function segmentRows(rows, isAnchored) {
277
297
  const segments = [];
298
+ // Collapse one contiguous context run [from, to) that contains NO anchors.
299
+ const collapseRun = (run, atStart, atEnd) => {
300
+ if (run.length <= COLLAPSE_THRESHOLD) {
301
+ for (const row of run)
302
+ segments.push(row);
303
+ return;
304
+ }
305
+ const head = atStart ? [] : run.slice(0, CONTEXT_EDGE);
306
+ const tail = atEnd ? [] : run.slice(run.length - CONTEXT_EDGE);
307
+ const hidden = run.slice(head.length, run.length - tail.length);
308
+ for (const row of head)
309
+ segments.push(row);
310
+ if (hidden.length > 0)
311
+ segments.push({ collapsed: true, rows: hidden });
312
+ for (const row of tail)
313
+ segments.push(row);
314
+ };
278
315
  let i = 0;
279
316
  while (i < rows.length) {
280
317
  if (rows[i].kind !== "context") {
@@ -286,23 +323,29 @@ function segmentRows(rows) {
286
323
  let j = i;
287
324
  while (j < rows.length && rows[j].kind === "context")
288
325
  j += 1;
289
- const run = rows.slice(i, j);
290
- if (run.length <= COLLAPSE_THRESHOLD) {
291
- for (const row of run)
292
- segments.push(row);
326
+ const fullRun = rows.slice(i, j);
327
+ const runAtStart = i === 0;
328
+ const runAtEnd = j === rows.length;
329
+ if (!isAnchored || !fullRun.some(isAnchored)) {
330
+ collapseRun(fullRun, runAtStart, runAtEnd);
293
331
  }
294
332
  else {
295
- const atStart = i === 0;
296
- const atEnd = j === rows.length;
297
- const head = atStart ? [] : run.slice(0, CONTEXT_EDGE);
298
- const tail = atEnd ? [] : run.slice(run.length - CONTEXT_EDGE);
299
- const hidden = run.slice(head.length, run.length - tail.length);
300
- for (const row of head)
301
- segments.push(row);
302
- if (hidden.length > 0)
303
- segments.push({ collapsed: true, rows: hidden });
304
- for (const row of tail)
305
- segments.push(row);
333
+ // Walk the run, emitting anchored rows verbatim and collapsing the
334
+ // unanchored spans between them. An anchored row is always visible; the
335
+ // spans on each side of it collapse independently.
336
+ let spanStart = 0;
337
+ for (let k = 0; k <= fullRun.length; k += 1) {
338
+ const atAnchor = k < fullRun.length && isAnchored(fullRun[k]);
339
+ if (atAnchor || k === fullRun.length) {
340
+ const span = fullRun.slice(spanStart, k);
341
+ if (span.length > 0) {
342
+ collapseRun(span, runAtStart && spanStart === 0, runAtEnd && k === fullRun.length);
343
+ }
344
+ if (k < fullRun.length)
345
+ segments.push(fullRun[k]);
346
+ spanStart = k + 1;
347
+ }
348
+ }
306
349
  }
307
350
  i = j;
308
351
  }
@@ -329,10 +372,42 @@ const SIGN = {
329
372
  removed: "−",
330
373
  context: " ",
331
374
  };
332
- const LINE_NO_CLASS = "select-none px-2 py-0 text-right font-mono text-[12px] leading-5 text-muted-foreground tabular-nums";
333
- const DIFF_LINE_CLASS = "block min-w-max flex-1 whitespace-pre px-2 py-0 font-mono text-[12px] leading-5 text-foreground";
375
+ const LINE_NO_CLASS = "select-none px-2 py-0 text-right font-mono [font-size:var(--plan-code-size)] leading-5 text-muted-foreground tabular-nums";
376
+ const DIFF_LINE_CLASS = "block min-w-max flex-1 whitespace-pre px-2 py-0 font-mono [font-size:var(--plan-code-size)] leading-5 text-foreground";
334
377
  const DEFAULT_VISIBLE_DIFF_LINES = 15;
335
378
  const MAX_DIFF_LCS_CELLS = 1_000_000;
379
+ /**
380
+ * Below this rendered container width (px) a diff drops side-by-side `split`
381
+ * mode and falls back to `unified`: split's two line-number gutters double the
382
+ * width the code needs, exactly where space is tightest (e.g. a diff nested in a
383
+ * vertical-tabs content column). Measured from the block's own box, not the
384
+ * viewport, so the fallback fires by available width at any nesting depth.
385
+ */
386
+ const SPLIT_MIN_WIDTH = 560;
387
+ /**
388
+ * Observe the rendered inline width of an element. Returns a ref to attach plus
389
+ * the measured content-box width (null until the first ResizeObserver tick).
390
+ * Lets the diff pick an effective mode from the width it was actually handed — a
391
+ * measurement CSS container queries can't drive, since switching split↔unified
392
+ * is a structural (DOM) change, not a style toggle.
393
+ */
394
+ function useContainerWidth() {
395
+ const ref = useRef(null);
396
+ const [width, setWidth] = useState(null);
397
+ useEffect(() => {
398
+ const el = ref.current;
399
+ if (!el || typeof ResizeObserver === "undefined")
400
+ return;
401
+ const observer = new ResizeObserver((entries) => {
402
+ const entry = entries[0];
403
+ if (entry)
404
+ setWidth(entry.contentRect.width);
405
+ });
406
+ observer.observe(el);
407
+ return () => observer.disconnect();
408
+ }, []);
409
+ return [ref, width];
410
+ }
336
411
  function splitDiffFilename(filename) {
337
412
  const value = filename?.trim() || "diff";
338
413
  const segments = value.split("/").filter(Boolean);
@@ -345,21 +420,81 @@ function DiffLineText({ language, text }) {
345
420
  return (_jsx("span", { className: DIFF_LINE_CLASS, children: _jsx(SyntaxHighlightedLine, { code: code, language: language }) }));
346
421
  }
347
422
  /* ── Read ──────────────────────────────────────────────────────────────────── */
348
- function DiffRead({ data, blockId, title, summary }) {
423
+ function DiffRead({ data, blockId, title, summary, ctx, }) {
349
424
  const [mode, setMode] = useState(data.mode ?? "unified");
350
425
  const [expanded, setExpanded] = useState(() => new Set());
351
426
  const [showAllRows, setShowAllRows] = useState(false);
427
+ const [activeIndex, setActiveIndex] = useState(null);
428
+ const [containerRef, containerWidth] = useContainerWidth();
352
429
  const rows = useMemo(() => buildRows(diffLines(data.before, data.after)), [data.before, data.after]);
353
430
  const language = useMemo(() => resolveDiffLanguage(data), [data.filename, data.language]);
354
431
  const fileParts = useMemo(() => splitDiffFilename(data.filename), [data.filename]);
355
432
  const splitLineCount = useMemo(() => pairSplitRows(rows).length, [rows]);
433
+ // Resolve annotations against the side they target. A `before` annotation's
434
+ // `lines` ref is clamped to the OLD file's line count and matched on `oldNo`;
435
+ // an `after` (default) ref to the NEW file and matched on `newNo`. Markers are
436
+ // authoring-order across BOTH sides so a note ↔ row ↔ rail card share one id.
437
+ const beforeLineCount = useMemo(() => countLines(data.before), [data.before]);
438
+ const afterLineCount = useMemo(() => countLines(data.after), [data.after]);
439
+ const resolved = useMemo(() => resolveAnnotations(data.annotations, (annotation) => annotationSide(annotation) === "before"
440
+ ? beforeLineCount
441
+ : afterLineCount), [data.annotations, beforeLineCount, afterLineCount]);
442
+ const hasAnnotations = hasRailAnnotations(resolved);
443
+ // Effective render mode. Annotated diffs always render unified so the inline
444
+ // notes read in flow (there is no side rail); and any diff in a container
445
+ // narrower than SPLIT_MIN_WIDTH falls back to unified so split's doubled
446
+ // gutters never crush the code. `canSplit` also gates the mode toggle so it is
447
+ // hidden whenever split is unavailable.
448
+ const narrow = containerWidth != null && containerWidth < SPLIT_MIN_WIDTH;
449
+ const canSplit = !hasAnnotations && !narrow;
450
+ const effectiveMode = canSplit ? mode : "unified";
451
+ // Side-scoped line → markers maps so a row only lights from its own side.
452
+ const beforeMarkers = useMemo(() => buildLineMarkerMap(resolved.filter((r) => annotationSide(r.annotation) === "before")), [resolved]);
453
+ const afterMarkers = useMemo(() => buildLineMarkerMap(resolved.filter((r) => annotationSide(r.annotation) !== "before")), [resolved]);
454
+ const markersForRow = useMemo(() => {
455
+ return (row, side) => {
456
+ const out = [];
457
+ if ((side === undefined || side === "old") &&
458
+ row.oldNo != null &&
459
+ row.kind !== "added") {
460
+ out.push(...(beforeMarkers.get(row.oldNo) ?? []));
461
+ }
462
+ if ((side === undefined || side === "new") &&
463
+ row.newNo != null &&
464
+ row.kind !== "removed") {
465
+ out.push(...(afterMarkers.get(row.newNo) ?? []));
466
+ }
467
+ return out;
468
+ };
469
+ }, [beforeMarkers, afterMarkers]);
470
+ // A context row that carries a marker is an anchor: never collapse it away.
471
+ const anchoredRow = useMemo(() => {
472
+ if (!hasAnnotations)
473
+ return undefined;
474
+ return (row) => markersForRow(row).length > 0;
475
+ }, [hasAnnotations, markersForRow]);
356
476
  const added = rows.filter((r) => r.kind === "added").length;
357
477
  const removed = rows.filter((r) => r.kind === "removed").length;
358
478
  const unchanged = data.before === data.after;
359
- const totalVisibleLineCount = mode === "split" ? splitLineCount : rows.length;
479
+ const totalVisibleLineCount = effectiveMode === "split" ? splitLineCount : rows.length;
360
480
  const shouldLimitRows = totalVisibleLineCount > DEFAULT_VISIBLE_DIFF_LINES;
361
- const rowLimit = !showAllRows && shouldLimitRows ? DEFAULT_VISIBLE_DIFF_LINES : undefined;
362
- const displayedRows = mode === "unified" && rowLimit ? rows.slice(0, rowLimit) : rows;
481
+ // Never truncate away an annotated row: extend the window past the last one.
482
+ const effectiveRowLimit = useMemo(() => {
483
+ if (showAllRows || !shouldLimitRows)
484
+ return undefined;
485
+ let limit = DEFAULT_VISIBLE_DIFF_LINES;
486
+ if (hasAnnotations) {
487
+ for (let idx = rows.length - 1; idx >= limit; idx -= 1) {
488
+ if (markersForRow(rows[idx]).length > 0) {
489
+ limit = idx + 1;
490
+ break;
491
+ }
492
+ }
493
+ }
494
+ return limit;
495
+ }, [showAllRows, shouldLimitRows, hasAnnotations, rows, markersForRow]);
496
+ const rowLimit = effectiveRowLimit;
497
+ const displayedRows = effectiveMode === "unified" && rowLimit ? rows.slice(0, rowLimit) : rows;
363
498
  const toggleRun = (index) => setExpanded((prev) => {
364
499
  const next = new Set(prev);
365
500
  if (next.has(index))
@@ -368,31 +503,93 @@ function DiffRead({ data, blockId, title, summary }) {
368
503
  next.add(index);
369
504
  return next;
370
505
  });
371
- return (_jsxs("section", { className: "plan-block group/diff-block", "data-block-id": blockId, children: [title && _jsx("div", { className: "plan-block-label", children: title }), _jsxs("div", { className: "overflow-hidden rounded-md border border-border bg-background", children: [_jsxs("div", { className: "flex min-h-10 flex-wrap items-center gap-2 border-b border-border bg-muted/60 px-3 py-1.5", children: [_jsx(IconFileDiff, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 items-baseline gap-1.5 font-mono", title: data.filename || undefined, children: [_jsx("span", { className: "min-w-0 max-w-[16rem] truncate text-[13px] font-semibold leading-5 text-foreground", children: fileParts.basename }), fileParts.directory && (_jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] leading-5 text-muted-foreground/70", children: fileParts.directory }))] }), _jsxs("span", { className: "ml-1 flex shrink-0 items-center gap-2 font-mono text-xs", children: [_jsxs("span", { className: "text-emerald-700 dark:text-emerald-300", children: ["+", added] }), _jsxs("span", { className: "text-destructive", children: ["\u2212", removed] })] }), _jsxs("div", { className: "pointer-events-none ml-auto flex shrink-0 items-center overflow-hidden rounded-md border border-border bg-background opacity-0 transition-opacity group-hover/diff-block:pointer-events-auto group-hover/diff-block:opacity-100 group-focus-within/diff-block:pointer-events-auto group-focus-within/diff-block:opacity-100", children: [_jsx(ModeButton, { active: mode === "unified", onClick: () => setMode("unified"), icon: _jsx(IconList, { className: "size-3.5" }), label: "Unified" }), _jsx(ModeButton, { active: mode === "split", onClick: () => setMode("split"), icon: _jsx(IconColumns, { className: "size-3.5" }), label: "Split" })] })] }), unchanged ? (_jsx("div", { className: "px-4 py-6 text-center font-mono text-sm text-muted-foreground", children: "No changes" })) : mode === "split" ? (_jsx(SplitView, { rows: rows, language: language, rowLimit: rowLimit })) : (_jsx(UnifiedView, { rows: displayedRows, language: language, expanded: expanded, onToggleRun: toggleRun })), !unchanged && shouldLimitRows && (_jsxs("button", { type: "button", "data-plan-interactive": true, "aria-expanded": showAllRows, onClick: () => setShowAllRows((current) => !current), className: "flex h-7 w-full items-center justify-center gap-1.5 border-t border-border bg-background px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground", children: [_jsx(IconChevronRight, { className: cn("size-3 shrink-0 transition-transform", showAllRows ? "-rotate-90" : "rotate-90") }), showAllRows
506
+ return (_jsxs("section", { ref: containerRef, className: "plan-block group/diff-block", "data-block-id": blockId, children: [title && _jsx("div", { className: "plan-block-label", children: title }), summary && (_jsx("p", { className: "mb-3 text-sm leading-relaxed text-plan-muted", children: summary })), _jsxs("div", { className: "overflow-hidden rounded-md border border-border bg-background", children: [_jsxs("div", { className: "flex min-h-10 flex-wrap items-center gap-2 border-b border-border bg-muted/60 px-3 py-1.5", children: [_jsx(IconFileDiff, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 items-baseline gap-1.5 font-mono", title: data.filename || undefined, children: [_jsx("span", { className: "min-w-0 max-w-[16rem] truncate text-[13px] font-semibold leading-5 text-foreground", children: fileParts.basename }), fileParts.directory && (_jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] leading-5 text-muted-foreground/70", children: fileParts.directory }))] }), _jsxs("span", { className: "ml-1 flex shrink-0 items-center gap-2 font-mono text-xs", children: [_jsxs("span", { className: "text-emerald-700 dark:text-emerald-300", children: ["+", added] }), _jsxs("span", { className: "text-destructive", children: ["\u2212", removed] })] }), canSplit && (_jsxs("div", { className: "pointer-events-none ml-auto flex shrink-0 items-center overflow-hidden rounded-md border border-border bg-background opacity-0 transition-opacity group-hover/diff-block:pointer-events-auto group-hover/diff-block:opacity-100 group-focus-within/diff-block:pointer-events-auto group-focus-within/diff-block:opacity-100", children: [_jsx(ModeButton, { active: mode === "unified", onClick: () => setMode("unified"), icon: _jsx(IconList, { className: "size-3.5" }), label: "Unified" }), _jsx(ModeButton, { active: mode === "split", onClick: () => setMode("split"), icon: _jsx(IconColumns, { className: "size-3.5" }), label: "Split" })] }))] }), unchanged ? (_jsx("div", { className: "px-4 py-6 text-center font-mono text-sm text-muted-foreground", children: "No changes" })) : effectiveMode === "split" ? (_jsx(SplitView, { rows: rows, language: language, rowLimit: rowLimit, markersForRow: markersForRow, activeIndex: activeIndex, onActiveChange: setActiveIndex })) : (_jsx(UnifiedView, { rows: displayedRows, language: language, expanded: expanded, onToggleRun: toggleRun, markersForRow: markersForRow, anchoredRow: anchoredRow, activeIndex: activeIndex, onActiveChange: setActiveIndex, ctx: ctx })), !unchanged && shouldLimitRows && (_jsxs("button", { type: "button", "data-plan-interactive": true, "aria-expanded": showAllRows, onClick: () => setShowAllRows((current) => !current), className: "flex h-7 w-full items-center justify-center gap-1.5 border-t border-border bg-background px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground", children: [_jsx(IconChevronRight, { className: cn("size-3 shrink-0 transition-transform", showAllRows ? "-rotate-90" : "rotate-90") }), showAllRows
372
507
  ? "Show fewer"
373
- : `Show all ${totalVisibleLineCount} lines`] }))] }), summary && _jsx("p", { className: "mt-5 text-plan-muted", children: summary })] }));
508
+ : `Show all ${totalVisibleLineCount} lines`] }))] })] }));
374
509
  }
375
510
  function ModeButton({ active, onClick, icon, label, }) {
376
511
  return (_jsxs("button", { type: "button", "data-plan-interactive": true, onClick: onClick, "aria-pressed": active, className: cn("flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-medium transition-colors", active
377
512
  ? "bg-accent text-accent-foreground"
378
513
  : "text-muted-foreground hover:bg-muted/80 hover:text-foreground"), children: [icon, label] }));
379
514
  }
515
+ /**
516
+ * The numbered marker pip(s) for a row plus the active-state it derives. Returns
517
+ * `null` when the row carries no annotation so unannotated diffs render an empty
518
+ * marker column (or no column at all when the whole diff is unannotated).
519
+ */
520
+ function rowMarkerInfo(markers, activeIndex) {
521
+ if (markers.length === 0)
522
+ return null;
523
+ const isActive = markers.some((m) => m.index === activeIndex);
524
+ return { isActive, primaryIndex: markers[0].index };
525
+ }
526
+ /** Shared amber wash for an annotated row, brighter when active. */
527
+ function annotatedRowBg(info) {
528
+ if (!info)
529
+ return null;
530
+ return info.isActive
531
+ ? "bg-amber-400/20 dark:bg-amber-300/15"
532
+ : "bg-amber-400/[0.07] dark:bg-amber-300/[0.07]";
533
+ }
534
+ /**
535
+ * Whether `row` is the FIRST line of `marker`'s resolved range. The numbered pip
536
+ * renders only on this line, so a multi-line annotation shows a single marker at
537
+ * the top of its span instead of repeating the same number down every line it
538
+ * covers. The amber band still washes the whole range (via `annotatedRowBg`), so
539
+ * the span stays visually grouped without the column of duplicate numbers.
540
+ */
541
+ function isMarkerRangeStart(row, marker) {
542
+ if (!marker.range)
543
+ return false;
544
+ const lineNo = annotationSide(marker.annotation) === "before" ? row.oldNo : row.newNo;
545
+ return lineNo === marker.range.start;
546
+ }
380
547
  /* ── Unified view ──────────────────────────────────────────────────────────── */
381
- function UnifiedView({ rows, language, expanded, onToggleRun, }) {
382
- const segments = useMemo(() => segmentRows(rows), [rows]);
548
+ function UnifiedView({ rows, language, expanded, onToggleRun, markersForRow, anchoredRow, activeIndex, onActiveChange, ctx, }) {
549
+ const segments = useMemo(() => segmentRows(rows, anchoredRow), [rows, anchoredRow]);
550
+ // Any annotation present ⇒ reserve the marker column so rows stay aligned.
551
+ const showMarkerColumn = useMemo(() => rows.some((row) => markersForRow(row).length > 0), [rows, markersForRow]);
552
+ const rowProps = {
553
+ language,
554
+ markersForRow,
555
+ activeIndex,
556
+ onActiveChange,
557
+ showMarkerColumn,
558
+ };
559
+ // A diff row plus any notes anchored to it: each annotation renders ONE
560
+ // full-width inline note directly under the FIRST line of its range (matched
561
+ // by `isMarkerRangeStart`), so the row's numbered pip and its note read as a
562
+ // single GitHub-style unit and the note never repeats down a multi-line span.
563
+ const renderRow = (row, key) => {
564
+ const notes = markersForRow(row).filter((marker) => isMarkerRangeStart(row, marker));
565
+ return (_jsxs("div", { children: [_jsx(UnifiedRow, { row: row, ...rowProps }), notes.map((item) => (_jsx(InlineAnnotationNote, { item: item, active: item.index === activeIndex, onActiveChange: onActiveChange, ctx: ctx }, `note-${item.index}`)))] }, key));
566
+ };
383
567
  let runIndex = 0;
384
- return (_jsx("div", { className: "overflow-x-auto", children: _jsx("div", { className: "w-max min-w-full font-mono text-[13px] leading-5", children: segments.map((segment, idx) => {
568
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsx("div", { className: "w-max min-w-full font-mono [font-size:var(--plan-code-size)] leading-5", children: segments.map((segment, idx) => {
385
569
  if ("collapsed" in segment) {
386
570
  const key = runIndex++;
387
571
  const open = expanded.has(key);
388
572
  return (_jsxs("div", { children: [_jsx(CollapsedRow, { count: segment.rows.length, open: open, onClick: () => onToggleRun(key) }), open &&
389
- segment.rows.map((row, ri) => (_jsx(UnifiedRow, { row: row, language: language }, `run-${key}-${ri}`)))] }, `run-${key}`));
573
+ segment.rows.map((row, ri) => renderRow(row, `run-${key}-${ri}`))] }, `run-${key}`));
390
574
  }
391
- return _jsx(UnifiedRow, { row: segment, language: language }, idx);
575
+ return renderRow(segment, String(idx));
392
576
  }) }) }));
393
577
  }
394
- function UnifiedRow({ language, row }) {
395
- return (_jsxs("div", { className: cn("flex min-h-5 min-w-full", ROW_BG[row.kind]), children: [_jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]"), children: row.oldNo ?? "" }), _jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]"), children: row.newNo ?? "" }), _jsx("span", { className: cn("w-6 shrink-0 select-none py-0 text-center font-semibold leading-5", GUTTER_BG[row.kind], SIGN_COLOR[row.kind]), children: SIGN[row.kind] }), _jsx(DiffLineText, { text: row.text, language: language })] }));
578
+ function UnifiedRow({ language, row, markersForRow, activeIndex, onActiveChange, showMarkerColumn, }) {
579
+ const markers = markersForRow(row);
580
+ const info = rowMarkerInfo(markers, activeIndex);
581
+ const startMarker = markers.find((marker) => isMarkerRangeStart(row, marker));
582
+ return (_jsxs("div", { className: cn("flex min-h-5 min-w-full", ROW_BG[row.kind], annotatedRowBg(info)), onMouseEnter: info ? () => onActiveChange(info.primaryIndex) : undefined, onMouseLeave: info ? () => onActiveChange(null) : undefined, children: [_jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]"), children: row.oldNo ?? "" }), _jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]"), children: row.newNo ?? "" }), _jsx("span", { className: cn("w-6 shrink-0 select-none py-0 text-center font-semibold leading-5", GUTTER_BG[row.kind], SIGN_COLOR[row.kind]), children: SIGN[row.kind] }), showMarkerColumn && (_jsx(MarkerCell, { startMarker: startMarker, active: startMarker != null && startMarker.index === activeIndex })), _jsx(DiffLineText, { text: row.text, language: language })] }));
583
+ }
584
+ /**
585
+ * The fixed-width marker column rendered between the sign gutter and the code.
586
+ * `startMarker` is set only on the FIRST line of an annotation's range, so the
587
+ * numbered pip appears once at the top of a span; every other row (the rest of a
588
+ * span, and every unannotated row in a diff that has annotations) renders an
589
+ * empty spacer so the code text stays aligned.
590
+ */
591
+ function MarkerCell({ startMarker, active, }) {
592
+ return (_jsx("span", { className: "flex w-6 shrink-0 select-none items-center justify-center py-0", children: startMarker != null && (_jsx(AnnotationGutterMarker, { marker: startMarker.marker, active: active })) }));
396
593
  }
397
594
  function CollapsedRow({ count, open, onClick, }) {
398
595
  return (_jsxs("button", { type: "button", "data-plan-interactive": true, onClick: onClick, className: "flex w-full cursor-pointer items-center gap-2 border-y border-border bg-muted/70 px-3 py-1 text-left text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: [_jsx(IconDotsVertical, { className: "size-3.5 shrink-0" }), _jsxs("span", { children: [open ? "Hide" : "Show", " ", count, " unchanged line", count === 1 ? "" : "s"] })] }));
@@ -426,28 +623,57 @@ function pairSplitRows(rows) {
426
623
  }
427
624
  return out;
428
625
  }
429
- function SplitView({ language, rowLimit, rows, }) {
626
+ function SplitView({ language, rowLimit, rows, markersForRow, activeIndex, onActiveChange, }) {
430
627
  const pairs = useMemo(() => pairSplitRows(rows), [rows]);
431
628
  const displayedPairs = rowLimit ? pairs.slice(0, rowLimit) : pairs;
432
- return (_jsxs("div", { className: "flex w-full bg-background font-mono text-[12px] leading-5", children: [_jsx("div", { className: "min-w-0 flex-1 overflow-x-auto border-r border-border", children: _jsx("div", { className: "inline-block min-w-full", children: displayedPairs.map((pair, idx) => (_jsx(SplitCell, { row: pair.left, side: "old", language: language }, `old-${idx}`))) }) }), _jsx("div", { className: "min-w-0 flex-1 overflow-x-auto", children: _jsx("div", { className: "inline-block min-w-full", children: displayedPairs.map((pair, idx) => (_jsx(SplitCell, { row: pair.right, side: "new", language: language }, `new-${idx}`))) }) })] }));
629
+ // Reserve the marker column on a side only if any visible row there has one.
630
+ const showOldMarkers = useMemo(() => displayedPairs.some((pair) => pair.left && markersForRow(pair.left, "old").length > 0), [displayedPairs, markersForRow]);
631
+ const showNewMarkers = useMemo(() => displayedPairs.some((pair) => pair.right && markersForRow(pair.right, "new").length > 0), [displayedPairs, markersForRow]);
632
+ const cellProps = { language, markersForRow, activeIndex, onActiveChange };
633
+ return (_jsxs("div", { className: "flex w-full bg-background font-mono [font-size:var(--plan-code-size)] leading-5", children: [_jsx("div", { className: "min-w-0 flex-1 overflow-x-auto border-r border-border", children: _jsx("div", { className: "inline-block min-w-full", children: displayedPairs.map((pair, idx) => (_jsx(SplitCell, { row: pair.left, side: "old", showMarkerColumn: showOldMarkers, ...cellProps }, `old-${idx}`))) }) }), _jsx("div", { className: "min-w-0 flex-1 overflow-x-auto", children: _jsx("div", { className: "inline-block min-w-full", children: displayedPairs.map((pair, idx) => (_jsx(SplitCell, { row: pair.right, side: "new", showMarkerColumn: showNewMarkers, ...cellProps }, `new-${idx}`))) }) })] }));
433
634
  }
434
- function SplitCell({ language, row, side, }) {
635
+ function SplitCell({ language, row, side, markersForRow, activeIndex, onActiveChange, showMarkerColumn, }) {
435
636
  if (!row) {
436
- return (_jsxs("div", { className: "flex min-h-5 min-w-full bg-muted/40 opacity-70", children: [_jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]") }), _jsx("span", { className: "w-6 shrink-0 bg-muted/60" }), _jsx("span", { className: DIFF_LINE_CLASS, children: " " })] }));
637
+ return (_jsxs("div", { className: "flex min-h-5 min-w-full bg-muted/40 opacity-70", children: [_jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]") }), _jsx("span", { className: "w-6 shrink-0 bg-muted/60" }), showMarkerColumn && _jsx("span", { className: "w-6 shrink-0" }), _jsx("span", { className: DIFF_LINE_CLASS, children: " " })] }));
437
638
  }
438
639
  const sign = side === "old" ? "−" : "+";
439
640
  const showSign = row.kind !== "context";
440
- return (_jsxs("div", { className: cn("flex min-h-5 min-w-full", ROW_BG[row.kind]), children: [_jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]"), children: side === "old" ? (row.oldNo ?? "") : (row.newNo ?? "") }), _jsx("span", { className: cn("w-6 shrink-0 select-none py-0 text-center font-semibold leading-5", GUTTER_BG[row.kind], SIGN_COLOR[row.kind]), children: showSign ? sign : " " }), _jsx(DiffLineText, { text: row.text, language: language })] }));
641
+ const markers = markersForRow(row, side);
642
+ const info = rowMarkerInfo(markers, activeIndex);
643
+ const startMarker = markers.find((marker) => isMarkerRangeStart(row, marker));
644
+ return (_jsxs("div", { className: cn("flex min-h-5 min-w-full", ROW_BG[row.kind], annotatedRowBg(info)), onMouseEnter: info ? () => onActiveChange(info.primaryIndex) : undefined, onMouseLeave: info ? () => onActiveChange(null) : undefined, children: [_jsx("span", { className: cn(LINE_NO_CLASS, "w-[52px]"), children: side === "old" ? (row.oldNo ?? "") : (row.newNo ?? "") }), _jsx("span", { className: cn("w-6 shrink-0 select-none py-0 text-center font-semibold leading-5", GUTTER_BG[row.kind], SIGN_COLOR[row.kind]), children: showSign ? sign : " " }), showMarkerColumn && (_jsx(MarkerCell, { startMarker: startMarker, active: startMarker != null && startMarker.index === activeIndex })), _jsx(DiffLineText, { text: row.text, language: language })] }));
441
645
  }
442
646
  /* ── Edit (panel) ──────────────────────────────────────────────────────────── */
443
647
  const codeAreaClass = "min-h-[140px] font-mono text-xs leading-5";
444
648
  function DiffEdit({ data, onChange, editable }) {
445
649
  const patch = (next) => onChange({ ...data, ...next });
446
650
  const mode = data.mode ?? "unified";
651
+ const annotations = data.annotations ?? [];
652
+ const updateAnnotation = (index, next) => patch({
653
+ annotations: annotations.map((annotation, i) => i === index ? { ...annotation, ...next } : annotation),
654
+ });
655
+ const removeAnnotation = (index) => patch({ annotations: annotations.filter((_, i) => i !== index) });
656
+ const addAnnotation = () => {
657
+ if (annotations.length >= 80)
658
+ return; // schema max
659
+ patch({
660
+ annotations: [
661
+ ...annotations,
662
+ { side: "after", lines: "1", label: "", note: "" },
663
+ ],
664
+ });
665
+ };
447
666
  return (_jsxs("div", { className: "flex flex-col gap-3", "data-plan-interactive": true, children: [_jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { htmlFor: "diff-filename", className: "text-xs", children: "Filename" }), _jsx(DevInput, { id: "diff-filename", value: data.filename ?? "", placeholder: "src/add.ts", disabled: !editable, onChange: (event) => patch({ filename: event.target.value || undefined }) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { htmlFor: "diff-language", className: "text-xs", children: "Language" }), _jsx(DevInput, { id: "diff-language", value: data.language ?? "", placeholder: "ts", disabled: !editable, onChange: (event) => patch({ language: event.target.value || undefined }) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { className: "text-xs", children: "Layout" }), _jsx(DevSelect, { value: mode, disabled: !editable, onValueChange: (value) => patch({ mode: value }), options: [
448
667
  { value: "unified", label: "Unified" },
449
668
  { value: "split", label: "Split (side-by-side)" },
450
- ] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { htmlFor: "diff-before", className: "text-xs", children: "Before" }), _jsx(DevTextarea, { id: "diff-before", spellCheck: false, className: codeAreaClass, value: data.before, disabled: !editable, onChange: (event) => patch({ before: event.target.value }) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { htmlFor: "diff-after", className: "text-xs", children: "After" }), _jsx(DevTextarea, { id: "diff-after", spellCheck: false, className: codeAreaClass, value: data.after, disabled: !editable, onChange: (event) => patch({ after: event.target.value }) })] })] }));
669
+ ] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { htmlFor: "diff-before", className: "text-xs", children: "Before" }), _jsx(DevTextarea, { id: "diff-before", spellCheck: false, className: codeAreaClass, value: data.before, disabled: !editable, onChange: (event) => patch({ before: event.target.value }) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DevLabel, { htmlFor: "diff-after", className: "text-xs", children: "After" }), _jsx(DevTextarea, { id: "diff-after", spellCheck: false, className: codeAreaClass, value: data.after, disabled: !editable, onChange: (event) => patch({ after: event.target.value }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(DevLabel, { className: "text-xs", children: "Annotations" }), editable && annotations.length < 80 && (_jsxs("button", { type: "button", "data-plan-interactive": true, onClick: addAnnotation, className: "flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-plan-muted transition-colors hover:bg-plan-block/60 hover:text-plan-text", children: [_jsx(IconPlus, { className: "size-3.5" }), "Add annotation"] }))] }), annotations.length === 0 && (_jsx("p", { className: "text-xs text-plan-muted", children: "No annotations yet. Add one to anchor a note to a line range on the before or after side." })), annotations.map((annotation, index) => (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-plan-line bg-plan-block/30 p-2", children: [_jsxs("div", { className: "grid gap-2 sm:grid-cols-[110px_110px_minmax(0,1fr)_auto]", children: [_jsx(DevSelect, { "aria-label": `Annotation ${index + 1} side`, value: annotation.side ?? "after", disabled: !editable, onValueChange: (value) => updateAnnotation(index, {
670
+ side: value,
671
+ }), options: [
672
+ { value: "after", label: "After" },
673
+ { value: "before", label: "Before" },
674
+ ] }), _jsx(DevInput, { "aria-label": `Annotation ${index + 1} lines`, value: annotation.lines, placeholder: "3-5", disabled: !editable, onChange: (event) => updateAnnotation(index, { lines: event.target.value }) }), _jsx(DevInput, { "aria-label": `Annotation ${index + 1} label`, value: annotation.label ?? "", placeholder: "Label (optional)", disabled: !editable, onChange: (event) => updateAnnotation(index, {
675
+ label: event.target.value || undefined,
676
+ }) }), editable && (_jsx("button", { type: "button", "data-plan-interactive": true, "aria-label": `Remove annotation ${index + 1}`, onClick: () => removeAnnotation(index), className: "flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-md text-plan-muted transition-colors hover:bg-muted hover:text-foreground", children: _jsx(IconTrash, { className: "size-4" }) }))] }), _jsx(DevTextarea, { "aria-label": `Annotation ${index + 1} note`, className: "min-h-[60px] text-sm", value: annotation.note, placeholder: "Explain what these lines do\u2026", disabled: !editable, onChange: (event) => updateAnnotation(index, { note: event.target.value }) })] }, index)))] })] }));
451
677
  }
452
678
  export { DiffRead, DiffEdit };
453
679
  //# sourceMappingURL=DiffBlock.js.map