@agent-native/core 0.42.0 → 0.44.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 (139) hide show
  1. package/README.md +17 -56
  2. package/dist/chat-threads/store.d.ts.map +1 -1
  3. package/dist/chat-threads/store.js +71 -10
  4. package/dist/chat-threads/store.js.map +1 -1
  5. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  6. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  7. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  8. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  9. package/dist/cli/recap.d.ts.map +1 -1
  10. package/dist/cli/recap.js +13 -13
  11. package/dist/cli/recap.js.map +1 -1
  12. package/dist/cli/skills.d.ts +2 -6
  13. package/dist/cli/skills.d.ts.map +1 -1
  14. package/dist/cli/skills.js +21 -79
  15. package/dist/cli/skills.js.map +1 -1
  16. package/dist/client/AssistantChat.d.ts.map +1 -1
  17. package/dist/client/AssistantChat.js +76 -18
  18. package/dist/client/AssistantChat.js.map +1 -1
  19. package/dist/client/blocks/index.d.ts +9 -0
  20. package/dist/client/blocks/index.d.ts.map +1 -1
  21. package/dist/client/blocks/index.js +9 -0
  22. package/dist/client/blocks/index.js.map +1 -1
  23. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  24. package/dist/client/blocks/library/AnnotatedCodeBlock.js +3 -3
  25. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  26. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
  27. package/dist/client/blocks/library/ApiEndpointBlock.js +1 -1
  28. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
  29. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  30. package/dist/client/blocks/library/DiffBlock.js +128 -19
  31. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  32. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  33. package/dist/client/blocks/library/FileTreeBlock.js +31 -4
  34. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  35. package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
  36. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
  37. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  38. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  39. package/dist/client/blocks/library/callout.config.d.ts +29 -0
  40. package/dist/client/blocks/library/callout.config.d.ts.map +1 -0
  41. package/dist/client/blocks/library/callout.config.js +33 -0
  42. package/dist/client/blocks/library/callout.config.js.map +1 -0
  43. package/dist/client/blocks/library/callout.d.ts +20 -0
  44. package/dist/client/blocks/library/callout.d.ts.map +1 -0
  45. package/dist/client/blocks/library/callout.js +61 -0
  46. package/dist/client/blocks/library/callout.js.map +1 -0
  47. package/dist/client/blocks/library/checklist.d.ts.map +1 -1
  48. package/dist/client/blocks/library/checklist.js +3 -3
  49. package/dist/client/blocks/library/checklist.js.map +1 -1
  50. package/dist/client/blocks/library/code-tabs.js +1 -1
  51. package/dist/client/blocks/library/code-tabs.js.map +1 -1
  52. package/dist/client/blocks/library/diagram.config.d.ts +64 -0
  53. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -0
  54. package/dist/client/blocks/library/diagram.config.js +111 -0
  55. package/dist/client/blocks/library/diagram.config.js.map +1 -0
  56. package/dist/client/blocks/library/diagram.d.ts +16 -0
  57. package/dist/client/blocks/library/diagram.d.ts.map +1 -0
  58. package/dist/client/blocks/library/diagram.js +261 -0
  59. package/dist/client/blocks/library/diagram.js.map +1 -0
  60. package/dist/client/blocks/library/question-form.config.d.ts +69 -0
  61. package/dist/client/blocks/library/question-form.config.d.ts.map +1 -0
  62. package/dist/client/blocks/library/question-form.config.js +58 -0
  63. package/dist/client/blocks/library/question-form.config.js.map +1 -0
  64. package/dist/client/blocks/library/question-form.d.ts +20 -0
  65. package/dist/client/blocks/library/question-form.d.ts.map +1 -0
  66. package/dist/client/blocks/library/question-form.js +286 -0
  67. package/dist/client/blocks/library/question-form.js.map +1 -0
  68. package/dist/client/blocks/library/sanitize-html.d.ts +5 -0
  69. package/dist/client/blocks/library/sanitize-html.d.ts.map +1 -0
  70. package/dist/client/blocks/library/sanitize-html.js +240 -0
  71. package/dist/client/blocks/library/sanitize-html.js.map +1 -0
  72. package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
  73. package/dist/client/blocks/library/server-specs.js +49 -0
  74. package/dist/client/blocks/library/server-specs.js.map +1 -1
  75. package/dist/client/blocks/library/specs.d.ts.map +1 -1
  76. package/dist/client/blocks/library/specs.js +9 -0
  77. package/dist/client/blocks/library/specs.js.map +1 -1
  78. package/dist/client/blocks/library/tabs.d.ts.map +1 -1
  79. package/dist/client/blocks/library/tabs.js +12 -12
  80. package/dist/client/blocks/library/tabs.js.map +1 -1
  81. package/dist/client/blocks/library/wireframe-kit.d.ts +260 -0
  82. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -0
  83. package/dist/client/blocks/library/wireframe-kit.js +920 -0
  84. package/dist/client/blocks/library/wireframe-kit.js.map +1 -0
  85. package/dist/client/blocks/library/wireframe.config.d.ts +123 -0
  86. package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -0
  87. package/dist/client/blocks/library/wireframe.config.js +311 -0
  88. package/dist/client/blocks/library/wireframe.config.js.map +1 -0
  89. package/dist/client/blocks/library/wireframe.d.ts +15 -0
  90. package/dist/client/blocks/library/wireframe.d.ts.map +1 -0
  91. package/dist/client/blocks/library/wireframe.js +206 -0
  92. package/dist/client/blocks/library/wireframe.js.map +1 -0
  93. package/dist/client/blocks/mdx.d.ts.map +1 -1
  94. package/dist/client/blocks/mdx.js +11 -0
  95. package/dist/client/blocks/mdx.js.map +1 -1
  96. package/dist/client/blocks/registry.d.ts +9 -0
  97. package/dist/client/blocks/registry.d.ts.map +1 -1
  98. package/dist/client/blocks/registry.js +12 -5
  99. package/dist/client/blocks/registry.js.map +1 -1
  100. package/dist/client/blocks/server.d.ts +1 -0
  101. package/dist/client/blocks/server.d.ts.map +1 -1
  102. package/dist/client/blocks/server.js +1 -0
  103. package/dist/client/blocks/server.js.map +1 -1
  104. package/dist/client/blocks/types.d.ts +8 -0
  105. package/dist/client/blocks/types.d.ts.map +1 -1
  106. package/dist/client/blocks/types.js.map +1 -1
  107. package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
  108. package/dist/client/rich-markdown-editor/DragHandle.js +112 -84
  109. package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
  110. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
  111. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +1 -1
  112. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  113. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +9 -1
  114. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  115. package/dist/client/rich-markdown-editor/SharedRichEditor.js +3 -1
  116. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  117. package/dist/client/rich-markdown-editor/extensions.d.ts +13 -1
  118. package/dist/client/rich-markdown-editor/extensions.d.ts.map +1 -1
  119. package/dist/client/rich-markdown-editor/extensions.js +4 -2
  120. package/dist/client/rich-markdown-editor/extensions.js.map +1 -1
  121. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  122. package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -1
  123. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  124. package/dist/server/poll.d.ts.map +1 -1
  125. package/dist/server/poll.js +30 -14
  126. package/dist/server/poll.js.map +1 -1
  127. package/dist/styles/agent-native.css +1 -0
  128. package/dist/styles/blocks.css +1388 -0
  129. package/dist/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
  130. package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
  131. package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
  132. package/docs/content/plan-plugin.md +8 -8
  133. package/docs/content/pr-visual-recap.md +2 -2
  134. package/docs/content/template-plan.md +94 -17
  135. package/package.json +2 -1
  136. package/src/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
  137. package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
  138. package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
  139. package/docs/content/visual-plans.md +0 -82
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
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, AnnotationNoteRail, buildLineMarkerMap, hasRailAnnotations, resolveAnnotations, } from "./annotation-rail.js";
6
+ import { AnnotationGutterMarker, buildLineMarkerMap, hasRailAnnotations, rangeLabel, resolveAnnotations, } from "./annotation-rail.js";
7
7
  import { DevInput, DevLabel, DevTextarea, DevSelect } from "./dev-doc-ui.js";
8
8
  /**
9
9
  * Split text into lines, each KEEPING its trailing newline (so the change
@@ -372,10 +372,42 @@ const SIGN = {
372
372
  removed: "−",
373
373
  context: " ",
374
374
  };
375
- const LINE_NO_CLASS = "select-none px-2 py-0 text-right font-mono text-[12px] 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 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";
377
377
  const DEFAULT_VISIBLE_DIFF_LINES = 15;
378
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
+ }
379
411
  function splitDiffFilename(filename) {
380
412
  const value = filename?.trim() || "diff";
381
413
  const segments = value.split("/").filter(Boolean);
@@ -393,6 +425,7 @@ function DiffRead({ data, blockId, title, summary, ctx, }) {
393
425
  const [expanded, setExpanded] = useState(() => new Set());
394
426
  const [showAllRows, setShowAllRows] = useState(false);
395
427
  const [activeIndex, setActiveIndex] = useState(null);
428
+ const [containerRef, containerWidth] = useContainerWidth();
396
429
  const rows = useMemo(() => buildRows(diffLines(data.before, data.after)), [data.before, data.after]);
397
430
  const language = useMemo(() => resolveDiffLanguage(data), [data.filename, data.language]);
398
431
  const fileParts = useMemo(() => splitDiffFilename(data.filename), [data.filename]);
@@ -407,6 +440,56 @@ function DiffRead({ data, blockId, title, summary, ctx, }) {
407
440
  ? beforeLineCount
408
441
  : afterLineCount), [data.annotations, beforeLineCount, afterLineCount]);
409
442
  const hasAnnotations = hasRailAnnotations(resolved);
443
+ // Effective render mode. The right-margin popover works in both unified and
444
+ // split, so annotations no longer force a mode; only a container narrower than
445
+ // SPLIT_MIN_WIDTH falls back to unified so split's doubled gutters never crush
446
+ // the code. `canSplit` also gates the mode toggle (hidden when unavailable).
447
+ const narrow = containerWidth != null && containerWidth < SPLIT_MIN_WIDTH;
448
+ const canSplit = !narrow;
449
+ const effectiveMode = canSplit ? mode : "unified";
450
+ // Annotation popover (diff): a marked line shows its note as a hover popover
451
+ // pinned in the diff's right margin and aligned to that line — no inline notes
452
+ // and no permanent side rail, so the code keeps its full width. A short
453
+ // hover-intent delay lets the pointer travel from the line into the popover.
454
+ const clearTimer = useRef(null);
455
+ const handleActiveChange = useCallback((index) => {
456
+ if (clearTimer.current) {
457
+ clearTimeout(clearTimer.current);
458
+ clearTimer.current = null;
459
+ }
460
+ if (index == null) {
461
+ clearTimer.current = setTimeout(() => setActiveIndex(null), 120);
462
+ }
463
+ else {
464
+ setActiveIndex(index);
465
+ }
466
+ }, []);
467
+ useEffect(() => () => {
468
+ if (clearTimer.current)
469
+ clearTimeout(clearTimer.current);
470
+ }, []);
471
+ const activeAnnotation = activeIndex == null
472
+ ? null
473
+ : (resolved.find((r) => r.index === activeIndex && r.range) ?? null);
474
+ // Vertical center of the active marker's anchor row within the section, so the
475
+ // popover sits beside that line. Rows are in page flow (no inner vertical
476
+ // scroll), so a DOM measure on activeIndex change is stable.
477
+ const [popoverTop, setPopoverTop] = useState(null);
478
+ useLayoutEffect(() => {
479
+ const host = containerRef.current;
480
+ if (activeIndex == null || !host) {
481
+ setPopoverTop(null);
482
+ return;
483
+ }
484
+ const rowEl = host.querySelector(`[data-annot-row="${activeIndex}"]`);
485
+ if (!rowEl) {
486
+ setPopoverTop(null);
487
+ return;
488
+ }
489
+ const hostRect = host.getBoundingClientRect();
490
+ const rowRect = rowEl.getBoundingClientRect();
491
+ setPopoverTop(rowRect.top - hostRect.top + rowRect.height / 2);
492
+ }, [activeIndex, containerRef]);
410
493
  // Side-scoped line → markers maps so a row only lights from its own side.
411
494
  const beforeMarkers = useMemo(() => buildLineMarkerMap(resolved.filter((r) => annotationSide(r.annotation) === "before")), [resolved]);
412
495
  const afterMarkers = useMemo(() => buildLineMarkerMap(resolved.filter((r) => annotationSide(r.annotation) !== "before")), [resolved]);
@@ -435,7 +518,7 @@ function DiffRead({ data, blockId, title, summary, ctx, }) {
435
518
  const added = rows.filter((r) => r.kind === "added").length;
436
519
  const removed = rows.filter((r) => r.kind === "removed").length;
437
520
  const unchanged = data.before === data.after;
438
- const totalVisibleLineCount = mode === "split" ? splitLineCount : rows.length;
521
+ const totalVisibleLineCount = effectiveMode === "split" ? splitLineCount : rows.length;
439
522
  const shouldLimitRows = totalVisibleLineCount > DEFAULT_VISIBLE_DIFF_LINES;
440
523
  // Never truncate away an annotated row: extend the window past the last one.
441
524
  const effectiveRowLimit = useMemo(() => {
@@ -453,7 +536,7 @@ function DiffRead({ data, blockId, title, summary, ctx, }) {
453
536
  return limit;
454
537
  }, [showAllRows, shouldLimitRows, hasAnnotations, rows, markersForRow]);
455
538
  const rowLimit = effectiveRowLimit;
456
- const displayedRows = mode === "unified" && rowLimit ? rows.slice(0, rowLimit) : rows;
539
+ const displayedRows = effectiveMode === "unified" && rowLimit ? rows.slice(0, rowLimit) : rows;
457
540
  const toggleRun = (index) => setExpanded((prev) => {
458
541
  const next = new Set(prev);
459
542
  if (next.has(index))
@@ -462,10 +545,9 @@ function DiffRead({ data, blockId, title, summary, ctx, }) {
462
545
  next.add(index);
463
546
  return next;
464
547
  });
465
- return (_jsxs("section", { 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: cn(hasAnnotations &&
466
- "grid items-start gap-3 md:grid-cols-[minmax(0,1fr)_minmax(190px,250px)]"), children: [_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, markersForRow: markersForRow, activeIndex: activeIndex, onActiveChange: setActiveIndex })) : (_jsx(UnifiedView, { rows: displayedRows, language: language, expanded: expanded, onToggleRun: toggleRun, markersForRow: markersForRow, anchoredRow: anchoredRow, activeIndex: activeIndex, onActiveChange: setActiveIndex })), !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
467
- ? "Show fewer"
468
- : `Show all ${totalVisibleLineCount} lines`] }))] }), hasAnnotations && (_jsx(AnnotationNoteRail, { items: resolved, activeIndex: activeIndex, onActiveChange: setActiveIndex, ctx: ctx, showMarker: true }))] })] }));
548
+ return (_jsxs("section", { ref: containerRef, className: "relative 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: handleActiveChange })) : (_jsx(UnifiedView, { rows: displayedRows, language: language, expanded: expanded, onToggleRun: toggleRun, markersForRow: markersForRow, anchoredRow: anchoredRow, activeIndex: activeIndex, onActiveChange: handleActiveChange })), !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
549
+ ? "Show fewer"
550
+ : `Show all ${totalVisibleLineCount} lines`] }))] }), activeAnnotation && popoverTop != null && (_jsx(AnnotationPopover, { item: activeAnnotation, top: popoverTop, onActiveChange: handleActiveChange, ctx: ctx }))] }));
469
551
  }
470
552
  function ModeButton({ active, onClick, icon, label, }) {
471
553
  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
@@ -491,6 +573,19 @@ function annotatedRowBg(info) {
491
573
  ? "bg-amber-400/20 dark:bg-amber-300/15"
492
574
  : "bg-amber-400/[0.07] dark:bg-amber-300/[0.07]";
493
575
  }
576
+ /**
577
+ * Whether `row` is the FIRST line of `marker`'s resolved range. The numbered pip
578
+ * renders only on this line, so a multi-line annotation shows a single marker at
579
+ * the top of its span instead of repeating the same number down every line it
580
+ * covers. The amber band still washes the whole range (via `annotatedRowBg`), so
581
+ * the span stays visually grouped without the column of duplicate numbers.
582
+ */
583
+ function isMarkerRangeStart(row, marker) {
584
+ if (!marker.range)
585
+ return false;
586
+ const lineNo = annotationSide(marker.annotation) === "before" ? row.oldNo : row.newNo;
587
+ return lineNo === marker.range.start;
588
+ }
494
589
  /* ── Unified view ──────────────────────────────────────────────────────────── */
495
590
  function UnifiedView({ rows, language, expanded, onToggleRun, markersForRow, anchoredRow, activeIndex, onActiveChange, }) {
496
591
  const segments = useMemo(() => segmentRows(rows, anchoredRow), [rows, anchoredRow]);
@@ -504,7 +599,7 @@ function UnifiedView({ rows, language, expanded, onToggleRun, markersForRow, anc
504
599
  showMarkerColumn,
505
600
  };
506
601
  let runIndex = 0;
507
- 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) => {
602
+ 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) => {
508
603
  if ("collapsed" in segment) {
509
604
  const key = runIndex++;
510
605
  const open = expanded.has(key);
@@ -517,15 +612,28 @@ function UnifiedView({ rows, language, expanded, onToggleRun, markersForRow, anc
517
612
  function UnifiedRow({ language, row, markersForRow, activeIndex, onActiveChange, showMarkerColumn, }) {
518
613
  const markers = markersForRow(row);
519
614
  const info = rowMarkerInfo(markers, activeIndex);
520
- 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, { marker: markers[0]?.marker, info: info })), _jsx(DiffLineText, { text: row.text, language: language })] }));
615
+ const startMarker = markers.find((marker) => isMarkerRangeStart(row, marker));
616
+ return (_jsxs("div", { "data-annot-row": startMarker ? startMarker.index : undefined, 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 })] }));
521
617
  }
522
618
  /**
523
619
  * The fixed-width marker column rendered between the sign gutter and the code.
524
- * When `marker` is undefined the cell is an empty spacer so every row in a diff
525
- * with annotations keeps its code text aligned.
620
+ * `startMarker` is set only on the FIRST line of an annotation's range, so the
621
+ * numbered pip appears once at the top of a span; every other row (the rest of a
622
+ * span, and every unannotated row in a diff that has annotations) renders an
623
+ * empty spacer so the code text stays aligned.
624
+ */
625
+ function MarkerCell({ startMarker, active, }) {
626
+ 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 })) }));
627
+ }
628
+ /**
629
+ * The hover popover for a diff annotation, pinned in the diff's right margin and
630
+ * vertically centered on the annotated line (`top` is measured by the caller).
631
+ * It shows the marker pip, line range, optional label, and the markdown note —
632
+ * beside the code instead of interrupting the diff flow — and keeps the
633
+ * annotation active while hovered so the pointer can travel from line to note.
526
634
  */
527
- function MarkerCell({ marker, info, }) {
528
- return (_jsx("span", { className: "flex w-6 shrink-0 select-none items-center justify-center py-0", children: marker != null && info != null && (_jsx(AnnotationGutterMarker, { marker: marker, active: info.isActive })) }));
635
+ function AnnotationPopover({ item, top, onActiveChange, ctx, }) {
636
+ return (_jsxs("div", { role: "tooltip", style: { top }, onMouseEnter: () => onActiveChange(item.index), onMouseLeave: () => onActiveChange(null), className: "absolute right-2 z-20 w-[min(17rem,calc(100%-3rem))] -translate-y-1/2 rounded-lg border border-amber-400/70 bg-background px-3 py-2.5 shadow-xl dark:border-amber-300/45", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5", children: [_jsx(AnnotationGutterMarker, { marker: item.marker, active: true }), _jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: rangeLabel(item) }), item.annotation.label && (_jsx("span", { className: "text-[13px] font-semibold text-foreground", children: item.annotation.label }))] }), _jsx("div", { className: "plan-annotation-note mt-1 text-[13px] leading-relaxed text-foreground/85", children: ctx.renderMarkdown ? (ctx.renderMarkdown(item.annotation.note)) : (_jsx("p", { children: item.annotation.note })) })] }));
529
637
  }
530
638
  function CollapsedRow({ count, open, onClick, }) {
531
639
  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"] })] }));
@@ -566,7 +674,7 @@ function SplitView({ language, rowLimit, rows, markersForRow, activeIndex, onAct
566
674
  const showOldMarkers = useMemo(() => displayedPairs.some((pair) => pair.left && markersForRow(pair.left, "old").length > 0), [displayedPairs, markersForRow]);
567
675
  const showNewMarkers = useMemo(() => displayedPairs.some((pair) => pair.right && markersForRow(pair.right, "new").length > 0), [displayedPairs, markersForRow]);
568
676
  const cellProps = { language, markersForRow, activeIndex, onActiveChange };
569
- 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", 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}`))) }) })] }));
677
+ 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}`))) }) })] }));
570
678
  }
571
679
  function SplitCell({ language, row, side, markersForRow, activeIndex, onActiveChange, showMarkerColumn, }) {
572
680
  if (!row) {
@@ -576,10 +684,11 @@ function SplitCell({ language, row, side, markersForRow, activeIndex, onActiveCh
576
684
  const showSign = row.kind !== "context";
577
685
  const markers = markersForRow(row, side);
578
686
  const info = rowMarkerInfo(markers, activeIndex);
579
- 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, { marker: markers[0]?.marker, info: info })), _jsx(DiffLineText, { text: row.text, language: language })] }));
687
+ const startMarker = markers.find((marker) => isMarkerRangeStart(row, marker));
688
+ return (_jsxs("div", { "data-annot-row": startMarker ? startMarker.index : undefined, 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 })] }));
580
689
  }
581
690
  /* ── Edit (panel) ──────────────────────────────────────────────────────────── */
582
- const codeAreaClass = "min-h-[140px] font-mono text-xs leading-5";
691
+ const codeAreaClass = "min-h-[140px] font-mono [font-size:var(--plan-code-size)] leading-5";
583
692
  function DiffEdit({ data, onChange, editable }) {
584
693
  const patch = (next) => onChange({ ...data, ...next });
585
694
  const mode = data.mode ?? "unified";