poly-weaver 0.12.2 → 0.12.4

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 (89) hide show
  1. package/dist/ansi.d.ts +12 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +334 -9
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/offscreen/artifact-pane.d.ts +5 -0
  6. package/dist/offscreen/artifact-pane.d.ts.map +1 -1
  7. package/dist/offscreen/artifact-pane.js +20 -1
  8. package/dist/offscreen/artifact-pane.js.map +1 -1
  9. package/dist/offscreen/controller.d.ts +1 -0
  10. package/dist/offscreen/controller.d.ts.map +1 -1
  11. package/dist/offscreen/controller.js +105 -0
  12. package/dist/offscreen/controller.js.map +1 -1
  13. package/dist/offscreen/inquiry-pane.d.ts +2 -0
  14. package/dist/offscreen/inquiry-pane.d.ts.map +1 -1
  15. package/dist/offscreen/inquiry-pane.js +19 -0
  16. package/dist/offscreen/inquiry-pane.js.map +1 -1
  17. package/dist/offscreen/inquiry-sender.js +1 -1
  18. package/dist/offscreen/inquiry-sender.js.map +1 -1
  19. package/dist/offscreen/panes.d.ts +2 -0
  20. package/dist/offscreen/panes.d.ts.map +1 -1
  21. package/dist/offscreen/split-inquiry.d.ts +17 -0
  22. package/dist/offscreen/split-inquiry.d.ts.map +1 -1
  23. package/dist/offscreen/split-inquiry.js +107 -1
  24. package/dist/offscreen/split-inquiry.js.map +1 -1
  25. package/dist/providers/codex/plan-mode-activator.js +0 -3
  26. package/dist/providers/codex/plan-mode-activator.js.map +1 -1
  27. package/dist/providers/option-catalog.d.ts.map +1 -1
  28. package/dist/providers/option-catalog.js +23 -0
  29. package/dist/providers/option-catalog.js.map +1 -1
  30. package/dist/session/locator.d.ts +2 -2
  31. package/dist/session/locator.d.ts.map +1 -1
  32. package/dist/session/locator.js +2 -2
  33. package/dist/session/locator.js.map +1 -1
  34. package/dist/status-bar.d.ts +5 -2
  35. package/dist/status-bar.d.ts.map +1 -1
  36. package/dist/status-bar.js +18 -20
  37. package/dist/status-bar.js.map +1 -1
  38. package/dist/terminal/clipboard-write.d.ts +17 -0
  39. package/dist/terminal/clipboard-write.d.ts.map +1 -0
  40. package/dist/terminal/clipboard-write.js +58 -0
  41. package/dist/terminal/clipboard-write.js.map +1 -0
  42. package/dist/terminal/host.d.ts +90 -0
  43. package/dist/terminal/host.d.ts.map +1 -1
  44. package/dist/terminal/host.js +858 -17
  45. package/dist/terminal/host.js.map +1 -1
  46. package/dist/terminal/input-router.d.ts +21 -6
  47. package/dist/terminal/input-router.d.ts.map +1 -1
  48. package/dist/terminal/input-router.js +30 -8
  49. package/dist/terminal/input-router.js.map +1 -1
  50. package/dist/terminal/mouse-mode-tracker.d.ts +1 -0
  51. package/dist/terminal/mouse-mode-tracker.d.ts.map +1 -1
  52. package/dist/terminal/mouse-mode-tracker.js +3 -0
  53. package/dist/terminal/mouse-mode-tracker.js.map +1 -1
  54. package/dist/terminal/osc52.d.ts +1 -0
  55. package/dist/terminal/osc52.d.ts.map +1 -1
  56. package/dist/terminal/osc52.js +3 -0
  57. package/dist/terminal/osc52.js.map +1 -1
  58. package/dist/terminal/paste-submit.d.ts +1 -1
  59. package/dist/terminal/paste-submit.d.ts.map +1 -1
  60. package/dist/terminal/paste-submit.js +2 -2
  61. package/dist/terminal/paste-submit.js.map +1 -1
  62. package/dist/terminal/render.d.ts +5 -0
  63. package/dist/terminal/render.d.ts.map +1 -1
  64. package/dist/terminal/render.js +14 -4
  65. package/dist/terminal/render.js.map +1 -1
  66. package/dist/terminal/selection.d.ts +101 -0
  67. package/dist/terminal/selection.d.ts.map +1 -0
  68. package/dist/terminal/selection.js +517 -0
  69. package/dist/terminal/selection.js.map +1 -0
  70. package/dist/terminal/virtual-terminal.d.ts +5 -1
  71. package/dist/terminal/virtual-terminal.d.ts.map +1 -1
  72. package/dist/terminal/virtual-terminal.js +10 -0
  73. package/dist/terminal/virtual-terminal.js.map +1 -1
  74. package/dist/user/host-curate-prompt.d.ts +2 -0
  75. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  76. package/dist/user/host-curate-prompt.js +239 -43
  77. package/dist/user/host-curate-prompt.js.map +1 -1
  78. package/dist/user/host-prompt.d.ts.map +1 -1
  79. package/dist/user/host-prompt.js +257 -14
  80. package/dist/user/host-prompt.js.map +1 -1
  81. package/dist/user/selectable-doc-pane.d.ts +13 -0
  82. package/dist/user/selectable-doc-pane.d.ts.map +1 -0
  83. package/dist/user/selectable-doc-pane.js +34 -0
  84. package/dist/user/selectable-doc-pane.js.map +1 -0
  85. package/dist/user/split-surface.d.ts +2 -0
  86. package/dist/user/split-surface.d.ts.map +1 -1
  87. package/dist/user/split-surface.js +17 -0
  88. package/dist/user/split-surface.js.map +1 -1
  89. package/package.json +1 -1
@@ -7,10 +7,11 @@ import { noteFocus } from "../text-editing/kill-ring.js";
7
7
  import { clipToWidth, tagFor, userAddedTag, wrapToWidth, } from "./curate-prompt.js";
8
8
  import { buildPlanPane } from "./plan-pane.js";
9
9
  import { buildIssuesArtifactPane, issueMetaToken } from "./issues-pane.js";
10
- import { buildSplitSurface, computeSplitMetrics, SPLIT_COLLAPSE_AT_COLS, } from "./split-surface.js";
10
+ import { buildSplitSurface, computeSplitMetrics, sideBySidePaneRegion, SPLIT_COLLAPSE_AT_COLS, } from "./split-surface.js";
11
11
  import { renderShortcutHints } from "./shortcut-hints.js";
12
12
  import { OffscreenController, } from "../offscreen/controller.js";
13
13
  import { composeIssueInquiry } from "./issue-inquiry.js";
14
+ import { createSelectableDocumentPane, } from "./selectable-doc-pane.js";
14
15
  import { AttachmentManager } from "../paste-image/attachment-manager.js";
15
16
  import { ALT_V_PASTE_IMAGE_HINT, handleBracketedPaste, handleImagePasteIntent, PASTING_IMAGE_HINT, } from "../paste-image/intents.js";
16
17
  import { applyEditingBufferEvent, applyEditInsertedText, bufferFromText, emptyBuffer, finalizeEditText, renderMultilineEditor, } from "./feedback-editing.js";
@@ -64,6 +65,8 @@ export class HostBackedCuratePrompt {
64
65
  userAdded: [],
65
66
  editing: undefined,
66
67
  planScrollTop: 0,
68
+ issueScrollTop: 0,
69
+ issueScrollPinnedToCursor: true,
67
70
  nextCycle: issues.map(() => 0),
68
71
  };
69
72
  const parser = new EmacsInputParser();
@@ -114,6 +117,7 @@ export class HostBackedCuratePrompt {
114
117
  if (!editing)
115
118
  return;
116
119
  state.editing = undefined;
120
+ state.issueScrollPinnedToCursor = true;
117
121
  if (editing.target.kind === "reviewer-revise" &&
118
122
  !state.comments[editing.target.index]) {
119
123
  state.decisions[editing.target.index] = "keep";
@@ -127,6 +131,7 @@ export class HostBackedCuratePrompt {
127
131
  state.comments[i] = text.length > 0 ? text : undefined;
128
132
  if (!state.comments[i])
129
133
  state.decisions[i] = "keep";
134
+ state.issueScrollPinnedToCursor = true;
130
135
  return;
131
136
  }
132
137
  if (editing.target.kind === "user-edit") {
@@ -138,12 +143,14 @@ export class HostBackedCuratePrompt {
138
143
  state.userAdded.splice(i, 1);
139
144
  clampCursor(issues.length, state);
140
145
  }
146
+ state.issueScrollPinnedToCursor = true;
141
147
  return;
142
148
  }
143
149
  if (text.length > 0) {
144
150
  state.userAdded.push(text);
145
151
  state.cursor = issues.length + state.userAdded.length - 1;
146
152
  }
153
+ state.issueScrollPinnedToCursor = true;
147
154
  clampCursor(issues.length, state);
148
155
  };
149
156
  const submitEdit = () => {
@@ -170,6 +177,7 @@ export class HostBackedCuratePrompt {
170
177
  });
171
178
  };
172
179
  const openReviewerEditor = (index, text = "") => {
180
+ state.issueScrollPinnedToCursor = true;
173
181
  state.editing = {
174
182
  target: { kind: "reviewer-revise", index },
175
183
  buffer: bufferFromText(text),
@@ -178,6 +186,7 @@ export class HostBackedCuratePrompt {
178
186
  };
179
187
  };
180
188
  const openUserAddEditor = () => {
189
+ state.issueScrollPinnedToCursor = true;
181
190
  state.editing = {
182
191
  target: { kind: "user-add" },
183
192
  buffer: emptyBuffer(),
@@ -191,6 +200,7 @@ export class HostBackedCuratePrompt {
191
200
  editing.transientHint = outcome.hint ?? null;
192
201
  if (outcome.inserted !== undefined)
193
202
  applyEditInsertedText(editing, outcome.inserted);
203
+ state.issueScrollPinnedToCursor = true;
194
204
  this.host.scheduleParentUiRender();
195
205
  };
196
206
  const launchEditPasteIntent = (editing, task) => {
@@ -248,6 +258,7 @@ export class HostBackedCuratePrompt {
248
258
  if (event.type === "ctrl-j") {
249
259
  dirty = applyEditingBufferEvent(state.editing, { type: "newline" }) || dirty;
250
260
  dirty = true;
261
+ state.issueScrollPinnedToCursor = true;
251
262
  continue;
252
263
  }
253
264
  if (event.type === "arrow") {
@@ -261,10 +272,14 @@ export class HostBackedCuratePrompt {
261
272
  type: event.direction === "up" ? "ctrl-p" : "ctrl-n",
262
273
  }) || dirty;
263
274
  }
275
+ if (dirty)
276
+ state.issueScrollPinnedToCursor = true;
264
277
  continue;
265
278
  }
266
279
  if (isTextBufferEvent(event)) {
267
280
  dirty = applyEditingBufferEvent(state.editing, event) || dirty;
281
+ if (dirty)
282
+ state.issueScrollPinnedToCursor = true;
268
283
  continue;
269
284
  }
270
285
  continue;
@@ -285,12 +300,14 @@ export class HostBackedCuratePrompt {
285
300
  state.cursor = (state.cursor - 1 + total) % total;
286
301
  if (state.cursor < issues.length)
287
302
  state.nextCycle[state.cursor] = 0;
303
+ state.issueScrollPinnedToCursor = true;
288
304
  dirty = true;
289
305
  }
290
306
  else if (event.direction === "down") {
291
307
  state.cursor = (state.cursor + 1) % total;
292
308
  if (state.cursor < issues.length)
293
309
  state.nextCycle[state.cursor] = 0;
310
+ state.issueScrollPinnedToCursor = true;
294
311
  dirty = true;
295
312
  }
296
313
  continue;
@@ -341,6 +358,7 @@ export class HostBackedCuratePrompt {
341
358
  attachments: new AttachmentManager(),
342
359
  transientHint: null,
343
360
  };
361
+ state.issueScrollPinnedToCursor = true;
344
362
  dirty = true;
345
363
  }
346
364
  else {
@@ -358,6 +376,7 @@ export class HostBackedCuratePrompt {
358
376
  if (next === "revise") {
359
377
  openReviewerEditor(state.cursor);
360
378
  }
379
+ state.issueScrollPinnedToCursor = true;
361
380
  dirty = true;
362
381
  continue;
363
382
  }
@@ -398,6 +417,7 @@ export class HostBackedCuratePrompt {
398
417
  if (event.text.toLowerCase() === "d") {
399
418
  state.userAdded.splice(state.cursor - issues.length, 1);
400
419
  clampCursor(issues.length, state);
420
+ state.issueScrollPinnedToCursor = true;
401
421
  dirty = true;
402
422
  }
403
423
  continue;
@@ -416,6 +436,7 @@ export class HostBackedCuratePrompt {
416
436
  if (openedEditor) {
417
437
  openReviewerEditor(state.cursor);
418
438
  }
439
+ state.issueScrollPinnedToCursor = true;
419
440
  if (state.decisions[state.cursor] !== prev || openedEditor)
420
441
  dirty = true;
421
442
  }
@@ -424,6 +445,7 @@ export class HostBackedCuratePrompt {
424
445
  this.host.scheduleParentUiRender();
425
446
  return finished;
426
447
  };
448
+ const selectionPanes = createCurateSelectionPanes(verdict, plan, state, context, () => this.host.getLayout(), () => offscreenController);
427
449
  const baseMode = {
428
450
  getSurface: () => {
429
451
  const layout = this.host.getLayout();
@@ -431,6 +453,15 @@ export class HostBackedCuratePrompt {
431
453
  const rows = Math.max(1, layout.childRows);
432
454
  return buildSurface(verdict, plan, state, cols, rows, context, offscreenController);
433
455
  },
456
+ selectionRegionAt: (column, row) => {
457
+ const layout = this.host.getLayout();
458
+ const cols = Math.max(20, layout.columns);
459
+ const rows = Math.max(1, layout.childRows);
460
+ return curateSelectionRegionAt(column, row, cols, rows);
461
+ },
462
+ selectionTargetAt: (column, row) => selectionPanes.selectionTargetAt(column, row),
463
+ docSurfaceSelectionPlacementAt: (column, row) => selectionPanes.placementAt(column, row),
464
+ docSurfaceSelectionPlacements: () => selectionPanes.placements(),
434
465
  onEvents: (keys) => {
435
466
  processEvents(parser.mapKeys(keys));
436
467
  return true;
@@ -488,15 +519,7 @@ function navigableCount(issueCount, state) {
488
519
  function clampCursor(issueCount, state) {
489
520
  state.cursor = Math.max(0, Math.min(state.cursor, navigableCount(issueCount, state) - 1));
490
521
  }
491
- function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive, issueInquiry) {
492
- const pinnedHint = clipToWidth(globalHintText, width);
493
- const scrollRows = Math.max(0, rows - 1);
494
- if (scrollRows === 0) {
495
- return {
496
- lines: [pinnedHint],
497
- cursor: { row: 0, col: 0 },
498
- };
499
- }
522
+ function buildIssueListRows(verdict, state, width, citationLive, issueInquiry) {
500
523
  const issues = verdict.issues;
501
524
  const lines = [];
502
525
  const n = issues.length;
@@ -508,9 +531,6 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
508
531
  }
509
532
  }
510
533
  lines.push("");
511
- // Every navigable row gets an explicit anchor so scrolling and prompt
512
- // cursor placement work for reviewer issues, user-added rows, and the
513
- // trailing add row uniformly.
514
534
  const rowStarts = [];
515
535
  const rowEnds = [];
516
536
  let editorRow;
@@ -543,8 +563,6 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
543
563
  if (metaToken) {
544
564
  const lastIdx = lines.length - 1;
545
565
  const lastVis = visibleLength(lines[lastIdx]);
546
- // Append meta to the last rendered row if it fits with a separating
547
- // space; otherwise put it on a new indented row.
548
566
  if (lastVis + 1 + metaWidth <= width) {
549
567
  lines[lastIdx] = `${lines[lastIdx]} ${metaToken}`;
550
568
  }
@@ -553,7 +571,6 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
553
571
  }
554
572
  }
555
573
  }
556
- // Inline revise row(s).
557
574
  if (state.decisions[i] === "revise") {
558
575
  const prefix = " └─ Revise: ";
559
576
  const reviseAvail = Math.max(1, width - visibleLength(prefix));
@@ -631,29 +648,43 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
631
648
  for (const l of wrapToWidth(renderShortcutHints(curateHelpTokens(state, issues.length, citationLive, issueInquiry)), width)) {
632
649
  lines.push(l);
633
650
  }
651
+ return { lines, rowStarts, rowEnds, editorRow, editorCol };
652
+ }
653
+ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive, issueInquiry) {
654
+ const pinnedHint = clipToWidth(globalHintText, width);
655
+ const scrollRows = Math.max(0, rows - 1);
656
+ if (scrollRows === 0) {
657
+ return {
658
+ lines: [pinnedHint],
659
+ cursor: { row: 0, col: 0 },
660
+ };
661
+ }
662
+ const { lines, rowStarts, rowEnds, editorRow, editorCol } = buildIssueListRows(verdict, state, width, citationLive, issueInquiry);
634
663
  const activeStart = rowStarts[state.cursor] ?? 0;
635
664
  const activeEndExclusive = rowEnds[state.cursor] ?? lines.length;
636
665
  // Prefer to show the entire active block; if it is taller than `rows`,
637
666
  // fall back to the editor row (when editing) or the header row.
638
- let scrollTop = 0;
639
667
  const total = lines.length;
668
+ let scrollTop = Math.min(Math.max(0, state.issueScrollTop), Math.max(0, total - scrollRows));
640
669
  if (total > scrollRows) {
641
- // Default: anchor to the start of the active issue.
642
- scrollTop = activeStart;
643
- // If the block fits and there is room above, pull the window up so we
644
- // also include some preceding context rather than leaving blank rows
645
- // below.
646
- if (activeEndExclusive - scrollTop < scrollRows) {
647
- scrollTop = Math.max(0, total - scrollRows);
648
- if (scrollTop > activeStart)
649
- scrollTop = activeStart;
650
- }
651
- // If editing, prefer keeping the editor row visible.
652
- if (editorRow !== undefined) {
653
- if (editorRow < scrollTop)
654
- scrollTop = editorRow;
655
- if (editorRow >= scrollTop + scrollRows)
656
- scrollTop = editorRow - scrollRows + 1;
670
+ if (state.issueScrollPinnedToCursor) {
671
+ // Default: anchor to the start of the active issue.
672
+ scrollTop = activeStart;
673
+ // If the block fits and there is room above, pull the window up so we
674
+ // also include some preceding context rather than leaving blank rows
675
+ // below.
676
+ if (activeEndExclusive - scrollTop < scrollRows) {
677
+ scrollTop = Math.max(0, total - scrollRows);
678
+ if (scrollTop > activeStart)
679
+ scrollTop = activeStart;
680
+ }
681
+ // If editing, prefer keeping the editor row visible.
682
+ if (editorRow !== undefined) {
683
+ if (editorRow < scrollTop)
684
+ scrollTop = editorRow;
685
+ if (editorRow >= scrollTop + scrollRows)
686
+ scrollTop = editorRow - scrollRows + 1;
687
+ }
657
688
  }
658
689
  // Clamp.
659
690
  if (scrollTop < 0)
@@ -661,6 +692,7 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
661
692
  if (scrollTop + scrollRows > total)
662
693
  scrollTop = total - scrollRows;
663
694
  }
695
+ state.issueScrollTop = scrollTop;
664
696
  let cursorRow;
665
697
  let cursorCol;
666
698
  if (editorRow !== undefined) {
@@ -674,13 +706,9 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
674
706
  }
675
707
  // Slice / pad to exactly `scrollRows`; the global hint row is pinned below
676
708
  // this scrollable issue window.
677
- if (scrollTop > 0) {
678
- lines.splice(0, scrollTop);
679
- }
680
- while (lines.length < scrollRows)
681
- lines.push("");
682
- if (lines.length > scrollRows)
683
- lines.splice(scrollRows, lines.length - scrollRows);
709
+ const visibleLines = lines.slice(scrollTop, scrollTop + scrollRows);
710
+ while (visibleLines.length < scrollRows)
711
+ visibleLines.push("");
684
712
  if (cursorRow < 0)
685
713
  cursorRow = 0;
686
714
  if (cursorRow >= scrollRows)
@@ -689,9 +717,9 @@ function buildLeftPane(verdict, state, width, rows, globalHintText, citationLive
689
717
  cursorCol = 0;
690
718
  if (cursorCol >= width)
691
719
  cursorCol = Math.max(0, width - 1);
692
- lines.push(pinnedHint);
720
+ visibleLines.push(pinnedHint);
693
721
  return {
694
- lines: lines.map((l) => clipToWidth(l, width)),
722
+ lines: visibleLines.map((l) => clipToWidth(l, width)),
695
723
  cursor: { row: cursorRow, col: cursorCol },
696
724
  };
697
725
  }
@@ -719,6 +747,174 @@ function computeNarrowPlanRows(rows) {
719
747
  // visible.
720
748
  return Math.max(3, Math.floor((rows - 1) / 2));
721
749
  }
750
+ function computeCurateCollapsedLayout(rows) {
751
+ const visibleRows = Math.max(1, rows);
752
+ const planRows = computeNarrowPlanRows(visibleRows);
753
+ return {
754
+ planRows,
755
+ planEnd: Math.min(planRows, visibleRows),
756
+ separatorRow: planRows + 1,
757
+ bodyStart: planRows + 2,
758
+ bodyEnd: visibleRows,
759
+ };
760
+ }
761
+ function curateSelectionRegionAt(column, row, cols, rows) {
762
+ if (column < 1 || column > cols || row < 1 || row > rows)
763
+ return null;
764
+ const geometry = curateSelectionGeometry(cols, rows);
765
+ if (pointInRegion(column, row, geometry.plan?.region))
766
+ return geometry.plan.region;
767
+ if (pointInRegion(column, row, geometry.issueList?.region)) {
768
+ return geometry.issueList.region;
769
+ }
770
+ if (pointInRegion(column, row, geometry.issueChrome))
771
+ return geometry.issueChrome;
772
+ const split = computeSplitMetrics({ cols, rows, collapseAt: COLLAPSE_AT_COLS });
773
+ if (!split.collapsed && geometry.issueChrome === undefined) {
774
+ return sideBySidePaneRegion(column, rows, split);
775
+ }
776
+ return null;
777
+ }
778
+ function curateSelectionGeometry(cols, rows) {
779
+ const split = computeSplitMetrics({ cols, rows, collapseAt: COLLAPSE_AT_COLS });
780
+ if (!split.collapsed) {
781
+ const issueRows = Math.max(0, rows - 1);
782
+ return {
783
+ plan: {
784
+ region: {
785
+ rowStart: 1,
786
+ rowEnd: rows,
787
+ colStart: split.leftWidth + 2,
788
+ colEnd: cols,
789
+ },
790
+ width: split.rightWidth,
791
+ visibleRows: rows,
792
+ },
793
+ issueList: issueRows > 0
794
+ ? {
795
+ region: { rowStart: 1, rowEnd: issueRows, colStart: 1, colEnd: split.leftWidth },
796
+ width: split.leftWidth,
797
+ visibleRows: issueRows,
798
+ }
799
+ : undefined,
800
+ issueChrome: issueRows < rows
801
+ ? { rowStart: issueRows + 1, rowEnd: rows, colStart: 1, colEnd: split.leftWidth }
802
+ : undefined,
803
+ };
804
+ }
805
+ const layout = computeCurateCollapsedLayout(rows);
806
+ const issueRows = Math.max(0, layout.bodyEnd - layout.bodyStart + 1);
807
+ const issueListRows = Math.max(0, issueRows - 1);
808
+ return {
809
+ plan: layout.planEnd >= 1
810
+ ? {
811
+ region: { rowStart: 1, rowEnd: layout.planEnd, colStart: 1, colEnd: cols },
812
+ width: cols,
813
+ visibleRows: layout.planEnd,
814
+ }
815
+ : undefined,
816
+ issueList: issueListRows > 0
817
+ ? {
818
+ region: {
819
+ rowStart: layout.bodyStart,
820
+ rowEnd: layout.bodyStart + issueListRows - 1,
821
+ colStart: 1,
822
+ colEnd: cols,
823
+ },
824
+ width: cols,
825
+ visibleRows: issueListRows,
826
+ }
827
+ : undefined,
828
+ issueChrome: issueRows > issueListRows
829
+ ? { rowStart: layout.bodyStart + issueListRows, rowEnd: layout.bodyEnd, colStart: 1, colEnd: cols }
830
+ : undefined,
831
+ };
832
+ }
833
+ function createCurateSelectionPanes(verdict, plan, state, context, getLayout, getController) {
834
+ const current = () => {
835
+ const layout = getLayout();
836
+ const cols = Math.max(20, layout.columns);
837
+ const rows = Math.max(1, layout.childRows);
838
+ return { cols, rows, geometry: curateSelectionGeometry(cols, rows) };
839
+ };
840
+ const issueLines = (width, planWidth) => {
841
+ const controller = getController();
842
+ const issueInquiry = (context.offscreen?.slots ?? []).map((slot) => ({
843
+ binding: slot.binding,
844
+ ready: controller?.isIssueInquiryReady(slot.binding) ?? true,
845
+ }));
846
+ const citationLive = !state.editing &&
847
+ state.cursor < verdict.issues.length &&
848
+ findCitationTargets(verdict.issues[state.cursor]?.citations, buildCuratePlanPane(plan, planWidth).sources).length > 0;
849
+ return buildIssueListRows(verdict, state, width, citationLive, issueInquiry).lines;
850
+ };
851
+ const planPane = createSelectableDocumentPane({
852
+ lines: () => {
853
+ const { geometry } = current();
854
+ return geometry.plan ? buildCuratePlanPane(plan, geometry.plan.width).lines : [];
855
+ },
856
+ getScrollTop: () => state.planScrollTop,
857
+ setScrollTop: (value) => {
858
+ const { geometry } = current();
859
+ const pane = geometry.plan;
860
+ if (!pane)
861
+ return;
862
+ const max = Math.max(0, buildCuratePlanPane(plan, pane.width).lines.length - pane.visibleRows);
863
+ state.planScrollTop = Math.min(max, Math.max(0, Math.trunc(value)));
864
+ },
865
+ getViewportRows: () => current().geometry.plan?.visibleRows ?? 1,
866
+ });
867
+ const issuePane = createSelectableDocumentPane({
868
+ lines: () => {
869
+ const { geometry } = current();
870
+ const planWidth = geometry.plan?.width ?? geometry.issueList?.width ?? 1;
871
+ return geometry.issueList ? issueLines(geometry.issueList.width, planWidth) : [];
872
+ },
873
+ getScrollTop: () => state.issueScrollTop,
874
+ setScrollTop: (value) => {
875
+ const { geometry } = current();
876
+ const pane = geometry.issueList;
877
+ if (!pane)
878
+ return;
879
+ const planWidth = geometry.plan?.width ?? pane.width;
880
+ const max = Math.max(0, issueLines(pane.width, planWidth).length - pane.visibleRows);
881
+ state.issueScrollTop = Math.min(max, Math.max(0, Math.trunc(value)));
882
+ },
883
+ getViewportRows: () => current().geometry.issueList?.visibleRows ?? 1,
884
+ onManualScroll: () => {
885
+ state.issueScrollPinnedToCursor = false;
886
+ },
887
+ });
888
+ const placements = () => {
889
+ const { geometry } = current();
890
+ const result = [];
891
+ if (geometry.plan &&
892
+ buildCuratePlanPane(plan, geometry.plan.width).lines.length > geometry.plan.visibleRows) {
893
+ result.push({ region: geometry.plan.region, pane: planPane });
894
+ }
895
+ if (geometry.issueList) {
896
+ const planWidth = geometry.plan?.width ?? geometry.issueList.width;
897
+ if (issueLines(geometry.issueList.width, planWidth).length > geometry.issueList.visibleRows) {
898
+ result.push({ region: geometry.issueList.region, pane: issuePane });
899
+ }
900
+ }
901
+ return result;
902
+ };
903
+ return {
904
+ selectionTargetAt: (column, row) => placements().some((placement) => pointInRegion(column, row, placement.region))
905
+ ? "parent-doc-surface"
906
+ : null,
907
+ placementAt: (column, row) => placements().find((placement) => pointInRegion(column, row, placement.region)),
908
+ placements,
909
+ };
910
+ }
911
+ function pointInRegion(column, row, region) {
912
+ return !!region &&
913
+ column >= region.colStart &&
914
+ column <= region.colEnd &&
915
+ row >= region.rowStart &&
916
+ row <= region.rowEnd;
917
+ }
722
918
  function buildSurface(verdict, plan, state, cols, rows, context, offscreenController) {
723
919
  const split = computeSplitMetrics({ cols, rows, collapseAt: COLLAPSE_AT_COLS });
724
920
  const primaryReady = offscreenController?.isIssueInquiryReady("primary") ?? true;
@@ -735,7 +931,7 @@ function buildSurface(verdict, plan, state, cols, rows, context, offscreenContro
735
931
  // Narrow stack: plan on top, divider rule, issues below. Both panes are
736
932
  // independently scrollable (plan via PageUp/PageDown/Home/End/wheel,
737
933
  // issues via Up/Down cursor movement).
738
- const planRows = computeNarrowPlanRows(rows);
934
+ const { planRows } = computeCurateCollapsedLayout(rows);
739
935
  const issueRows = Math.max(1, rows - 1 - planRows);
740
936
  const right = buildRightPane(verdict, plan, state, cols, planRows);
741
937
  const left = buildLeftPane(verdict, state, cols, issueRows, hint, citationLive, issueInquiry);