poly-weaver 0.9.3 → 0.10.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 (99) hide show
  1. package/dist/agents/reviewers/prompts.d.ts.map +1 -1
  2. package/dist/agents/reviewers/prompts.js +7 -2
  3. package/dist/agents/reviewers/prompts.js.map +1 -1
  4. package/dist/auto-update/check-agent-updates.d.ts +28 -0
  5. package/dist/auto-update/check-agent-updates.d.ts.map +1 -0
  6. package/dist/auto-update/check-agent-updates.js +130 -0
  7. package/dist/auto-update/check-agent-updates.js.map +1 -0
  8. package/dist/auto-update/detect-agents.d.ts +77 -0
  9. package/dist/auto-update/detect-agents.d.ts.map +1 -0
  10. package/dist/auto-update/detect-agents.js +333 -0
  11. package/dist/auto-update/detect-agents.js.map +1 -0
  12. package/dist/auto-update/gate.d.ts +20 -0
  13. package/dist/auto-update/gate.d.ts.map +1 -0
  14. package/dist/auto-update/gate.js +85 -0
  15. package/dist/auto-update/gate.js.map +1 -0
  16. package/dist/auto-update/install-agents.d.ts +12 -0
  17. package/dist/auto-update/install-agents.d.ts.map +1 -0
  18. package/dist/auto-update/install-agents.js +75 -0
  19. package/dist/auto-update/install-agents.js.map +1 -0
  20. package/dist/auto-update/install-method.d.ts +2 -0
  21. package/dist/auto-update/install-method.d.ts.map +1 -0
  22. package/dist/auto-update/install-method.js +16 -0
  23. package/dist/auto-update/install-method.js.map +1 -0
  24. package/dist/auto-update/semver.d.ts +9 -0
  25. package/dist/auto-update/semver.d.ts.map +1 -0
  26. package/dist/auto-update/semver.js +72 -0
  27. package/dist/auto-update/semver.js.map +1 -0
  28. package/dist/auto-update/timeouts.d.ts +2 -0
  29. package/dist/auto-update/timeouts.d.ts.map +1 -0
  30. package/dist/auto-update/timeouts.js +2 -0
  31. package/dist/auto-update/timeouts.js.map +1 -0
  32. package/dist/auto-update/tui.d.ts +38 -0
  33. package/dist/auto-update/tui.d.ts.map +1 -0
  34. package/dist/auto-update/tui.js +140 -0
  35. package/dist/auto-update/tui.js.map +1 -0
  36. package/dist/auto-update/winget-retry.d.ts +22 -0
  37. package/dist/auto-update/winget-retry.d.ts.map +1 -0
  38. package/dist/auto-update/winget-retry.js +40 -0
  39. package/dist/auto-update/winget-retry.js.map +1 -0
  40. package/dist/cli.js +37 -25
  41. package/dist/cli.js.map +1 -1
  42. package/dist/flow/built-in/default-factory.d.ts.map +1 -1
  43. package/dist/flow/built-in/default-factory.js +1 -9
  44. package/dist/flow/built-in/default-factory.js.map +1 -1
  45. package/dist/flow/custom/AUTHORING.md +8 -19
  46. package/dist/flow-editor/tui.d.ts.map +1 -1
  47. package/dist/flow-editor/tui.js +22 -3
  48. package/dist/flow-editor/tui.js.map +1 -1
  49. package/dist/preflight.d.ts +1 -40
  50. package/dist/preflight.d.ts.map +1 -1
  51. package/dist/preflight.js +1 -135
  52. package/dist/preflight.js.map +1 -1
  53. package/dist/preview-panel.d.ts +2 -0
  54. package/dist/preview-panel.d.ts.map +1 -1
  55. package/dist/preview-panel.js +42 -19
  56. package/dist/preview-panel.js.map +1 -1
  57. package/dist/providers/option-catalog.d.ts.map +1 -1
  58. package/dist/providers/option-catalog.js +42 -1
  59. package/dist/providers/option-catalog.js.map +1 -1
  60. package/dist/resume-tui.d.ts +3 -3
  61. package/dist/resume-tui.d.ts.map +1 -1
  62. package/dist/resume-tui.js +4 -14
  63. package/dist/resume-tui.js.map +1 -1
  64. package/dist/startup-tui.d.ts +5 -11
  65. package/dist/startup-tui.d.ts.map +1 -1
  66. package/dist/startup-tui.js +23 -35
  67. package/dist/startup-tui.js.map +1 -1
  68. package/dist/terminal/win32-key-translator.d.ts.map +1 -1
  69. package/dist/terminal/win32-key-translator.js +5 -4
  70. package/dist/terminal/win32-key-translator.js.map +1 -1
  71. package/dist/terminal-input.d.ts.map +1 -1
  72. package/dist/terminal-input.js +12 -3
  73. package/dist/terminal-input.js.map +1 -1
  74. package/dist/text-editing/emacs-input.d.ts +1 -1
  75. package/dist/text-editing/emacs-input.d.ts.map +1 -1
  76. package/dist/text-editing/emacs-input.js +2 -4
  77. package/dist/text-editing/emacs-input.js.map +1 -1
  78. package/dist/text-editing/soft-wrap.d.ts +16 -0
  79. package/dist/text-editing/soft-wrap.d.ts.map +1 -0
  80. package/dist/text-editing/soft-wrap.js +134 -0
  81. package/dist/text-editing/soft-wrap.js.map +1 -0
  82. package/dist/update-check.d.ts.map +1 -1
  83. package/dist/update-check.js +20 -6
  84. package/dist/update-check.js.map +1 -1
  85. package/dist/user/curate-handler.d.ts +16 -11
  86. package/dist/user/curate-handler.d.ts.map +1 -1
  87. package/dist/user/curate-handler.js +33 -22
  88. package/dist/user/curate-handler.js.map +1 -1
  89. package/dist/user/curate-prompt.d.ts +3 -0
  90. package/dist/user/curate-prompt.d.ts.map +1 -1
  91. package/dist/user/curate-prompt.js +4 -1
  92. package/dist/user/curate-prompt.js.map +1 -1
  93. package/dist/user/host-curate-prompt.d.ts +5 -6
  94. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  95. package/dist/user/host-curate-prompt.js +261 -103
  96. package/dist/user/host-curate-prompt.js.map +1 -1
  97. package/dist/user/host-prompt.js +47 -36
  98. package/dist/user/host-prompt.js.map +1 -1
  99. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
- import { applyBlockCursor, bold, cyan, dim, reset, visibleLength, } from "../ansi.js";
1
+ import { bold, cyan, dim, reset, visibleLength, } from "../ansi.js";
2
2
  import { AgentInterruptedError } from "../agents/runner.js";
3
3
  import { renderMarkdown } from "../markdown.js";
4
4
  import { EmacsInputParser } from "./prompt.js";
5
- import { applySingleLineTextBufferEvent, isTextBufferEvent, } from "../text-editing/emacs-input.js";
6
- import { clipToWidth, tagFor, wrapToWidth, } from "./curate-prompt.js";
5
+ import { applyTextBufferEvent, isTextBufferEvent, } from "../text-editing/emacs-input.js";
6
+ import { cursorToVisual, renderWrappedLine, softWrapLine } from "../text-editing/soft-wrap.js";
7
+ import { clipToWidth, tagFor, userAddedTag, wrapToWidth, } from "./curate-prompt.js";
7
8
  import { buildPlanPane } from "./plan-pane.js";
8
9
  import { buildSplitSurface, computeSplitMetrics } from "./split-surface.js";
9
10
  const DECISION_CYCLE = ["keep", "drop", "revise"];
@@ -22,16 +23,15 @@ const CITATION_MARKER = "\x1b[1;95m▶\x1b[0m";
22
23
  * Up/Down cursor movement (the issues pane auto-scrolls to keep the
23
24
  * active issue visible).
24
25
  *
25
- * Issues panel: title + summary + per-issue rows. Each issue's full
26
- * description is wrapped inline; when the cursor row is in revise mode and
27
- * the editor is open, an inline buffer line renders directly under the
28
- * issue.
26
+ * Issues panel: title + summary + reviewer issue rows, user-added rows, and
27
+ * a trailing add-feedback row. Descriptions are wrapped inline; open editors
28
+ * render as inline multi-line buffers.
29
29
  *
30
30
  * - k/d/r toggles the cursor row's decision.
31
31
  * - Tab cycles keep → drop → revise → keep.
32
- * - Enter on a revise row begins or finishes the inline edit; Esc cancels.
32
+ * - Enter on editable rows opens an inline editor; Esc cancels.
33
33
  * - PageUp/PageDown/Home/End/wheel scrolls the plan.
34
- * - Up/Down moves the cursor among issues.
34
+ * - Up/Down moves the cursor among all navigable rows.
35
35
  * - Ctrl+D submits; Ctrl+C quits poly-weaver (exit code 130). Inside the
36
36
  * inline revise editor (`state.editing`), Ctrl+C cancels the edit only.
37
37
  * - Ctrl+] is consumed by the host's `InputRouter` for dump capture.
@@ -53,6 +53,7 @@ export class HostBackedCuratePrompt {
53
53
  cursor: 0,
54
54
  decisions: issues.map(() => "keep"),
55
55
  comments: issues.map(() => undefined),
56
+ userAdded: [],
56
57
  editing: undefined,
57
58
  planScrollTop: 0,
58
59
  nextCycle: issues.map(() => 0),
@@ -76,7 +77,7 @@ export class HostBackedCuratePrompt {
76
77
  state.decisions[i] = "keep";
77
78
  }
78
79
  }
79
- return {
80
+ const result = {
80
81
  decisions: state.decisions.map((dec, i) => {
81
82
  const rec = { index: i, decision: dec };
82
83
  if (dec === "revise" && state.comments[i]) {
@@ -85,6 +86,60 @@ export class HostBackedCuratePrompt {
85
86
  return rec;
86
87
  }),
87
88
  };
89
+ if (state.userAdded.length > 0) {
90
+ result.userAdded = [...state.userAdded];
91
+ }
92
+ return result;
93
+ };
94
+ const cancelEdit = () => {
95
+ const editing = state.editing;
96
+ if (!editing)
97
+ return;
98
+ state.editing = undefined;
99
+ if (editing.target.kind === "reviewer-revise" &&
100
+ !state.comments[editing.target.index]) {
101
+ state.decisions[editing.target.index] = "keep";
102
+ }
103
+ clampCursor(issues.length, state);
104
+ };
105
+ const submitEdit = () => {
106
+ const editing = state.editing;
107
+ if (!editing)
108
+ return;
109
+ const text = editing.buffer.lines.join("\n").trim();
110
+ state.editing = undefined;
111
+ if (editing.target.kind === "reviewer-revise") {
112
+ const i = editing.target.index;
113
+ state.comments[i] = text.length > 0 ? text : undefined;
114
+ if (!state.comments[i])
115
+ state.decisions[i] = "keep";
116
+ return;
117
+ }
118
+ if (editing.target.kind === "user-edit") {
119
+ const i = editing.target.index;
120
+ if (text.length > 0) {
121
+ state.userAdded[i] = text;
122
+ }
123
+ else {
124
+ state.userAdded.splice(i, 1);
125
+ clampCursor(issues.length, state);
126
+ }
127
+ return;
128
+ }
129
+ if (text.length > 0) {
130
+ state.userAdded.push(text);
131
+ state.cursor = issues.length + state.userAdded.length - 1;
132
+ }
133
+ clampCursor(issues.length, state);
134
+ };
135
+ const openReviewerEditor = (index, text = "") => {
136
+ state.editing = {
137
+ target: { kind: "reviewer-revise", index },
138
+ buffer: bufferFromText(text),
139
+ };
140
+ };
141
+ const openUserAddEditor = () => {
142
+ state.editing = { target: { kind: "user-add" }, buffer: emptyBuffer() };
88
143
  };
89
144
  const processEvents = (events) => {
90
145
  let dirty = false;
@@ -93,25 +148,23 @@ export class HostBackedCuratePrompt {
93
148
  if (finished)
94
149
  break;
95
150
  if (state.editing) {
96
- const i = state.editing.index;
97
151
  if (event.type === "ctrl-c" || event.type === "escape") {
98
- state.editing = undefined;
99
- if (!state.comments[i])
100
- state.decisions[i] = "keep";
152
+ cancelEdit();
101
153
  dirty = true;
102
154
  continue;
103
155
  }
104
156
  if (event.type === "ctrl-d" || event.type === "enter") {
105
- const text = state.editing.buffer.trim();
106
- state.comments[i] = text.length > 0 ? text : undefined;
107
- state.editing = undefined;
108
- if (!state.comments[i])
109
- state.decisions[i] = "keep";
157
+ submitEdit();
158
+ dirty = true;
159
+ continue;
160
+ }
161
+ if (event.type === "ctrl-j") {
162
+ dirty = applyCurateTextBufferEvent(state.editing, { type: "newline" }) || dirty;
110
163
  dirty = true;
111
164
  continue;
112
165
  }
113
166
  if (isTextBufferEvent(event)) {
114
- dirty = applySingleLineTextBufferEvent(state.editing, event) || dirty;
167
+ dirty = applyCurateTextBufferEvent(state.editing, event) || dirty;
115
168
  continue;
116
169
  }
117
170
  continue;
@@ -127,14 +180,17 @@ export class HostBackedCuratePrompt {
127
180
  continue;
128
181
  }
129
182
  if (event.type === "arrow") {
183
+ const total = navigableCount(issues.length, state);
130
184
  if (event.direction === "up") {
131
- state.cursor = (state.cursor - 1 + issues.length) % issues.length;
132
- state.nextCycle[state.cursor] = 0;
185
+ state.cursor = (state.cursor - 1 + total) % total;
186
+ if (state.cursor < issues.length)
187
+ state.nextCycle[state.cursor] = 0;
133
188
  dirty = true;
134
189
  }
135
190
  else if (event.direction === "down") {
136
- state.cursor = (state.cursor + 1) % issues.length;
137
- state.nextCycle[state.cursor] = 0;
191
+ state.cursor = (state.cursor + 1) % total;
192
+ if (state.cursor < issues.length)
193
+ state.nextCycle[state.cursor] = 0;
138
194
  dirty = true;
139
195
  }
140
196
  continue;
@@ -171,28 +227,47 @@ export class HostBackedCuratePrompt {
171
227
  continue;
172
228
  }
173
229
  if (event.type === "enter") {
174
- if (state.decisions[state.cursor] === "revise") {
230
+ if (state.cursor < issues.length) {
231
+ if (state.decisions[state.cursor] === "revise") {
232
+ openReviewerEditor(state.cursor, state.comments[state.cursor] ?? "");
233
+ dirty = true;
234
+ }
235
+ }
236
+ else if (state.cursor < issues.length + state.userAdded.length) {
237
+ const index = state.cursor - issues.length;
175
238
  state.editing = {
176
- index: state.cursor,
177
- buffer: state.comments[state.cursor] ?? "",
178
- cursor: (state.comments[state.cursor] ?? "").length,
239
+ target: { kind: "user-edit", index },
240
+ buffer: bufferFromText(state.userAdded[index] ?? ""),
179
241
  };
180
242
  dirty = true;
181
243
  }
244
+ else {
245
+ openUserAddEditor();
246
+ dirty = true;
247
+ }
182
248
  continue;
183
249
  }
184
250
  if (event.type === "tab") {
251
+ if (state.cursor >= issues.length)
252
+ continue;
185
253
  const cur = state.decisions[state.cursor];
186
254
  const next = DECISION_CYCLE[(DECISION_CYCLE.indexOf(cur) + 1) % DECISION_CYCLE.length];
187
255
  state.decisions[state.cursor] = next;
188
- if (next === "revise" && !state.comments[state.cursor]) {
189
- state.editing = { index: state.cursor, buffer: "", cursor: 0 };
256
+ if (next === "revise") {
257
+ openReviewerEditor(state.cursor);
190
258
  }
191
259
  dirty = true;
192
260
  continue;
193
261
  }
194
262
  if (event.type === "insert") {
263
+ if (event.text.toLowerCase() === "a") {
264
+ openUserAddEditor();
265
+ dirty = true;
266
+ continue;
267
+ }
195
268
  if (event.text === "n") {
269
+ if (state.cursor >= issues.length)
270
+ continue;
196
271
  const m = planMetrics(plan, this.host);
197
272
  const citations = issues[state.cursor]?.citations;
198
273
  const targets = findCitationTargets(citations, m.sources);
@@ -215,6 +290,16 @@ export class HostBackedCuratePrompt {
215
290
  dirty = true;
216
291
  continue;
217
292
  }
293
+ if (state.cursor === issues.length + state.userAdded.length)
294
+ continue;
295
+ if (state.cursor >= issues.length) {
296
+ if (event.text === "d") {
297
+ state.userAdded.splice(state.cursor - issues.length, 1);
298
+ clampCursor(issues.length, state);
299
+ dirty = true;
300
+ }
301
+ continue;
302
+ }
218
303
  const ch = event.text.toLowerCase();
219
304
  const prev = state.decisions[state.cursor];
220
305
  if (ch === "k")
@@ -225,10 +310,11 @@ export class HostBackedCuratePrompt {
225
310
  state.decisions[state.cursor] = "revise";
226
311
  else
227
312
  continue;
228
- if (state.decisions[state.cursor] === "revise" && !state.comments[state.cursor]) {
229
- state.editing = { index: state.cursor, buffer: "", cursor: 0 };
313
+ const openedEditor = state.decisions[state.cursor] === "revise";
314
+ if (openedEditor) {
315
+ openReviewerEditor(state.cursor);
230
316
  }
231
- if (state.decisions[state.cursor] !== prev)
317
+ if (state.decisions[state.cursor] !== prev || openedEditor)
232
318
  dirty = true;
233
319
  }
234
320
  }
@@ -252,45 +338,65 @@ export class HostBackedCuratePrompt {
252
338
  });
253
339
  }
254
340
  }
255
- function nextCodePointIndex(text, index) {
256
- if (index >= text.length)
257
- return text.length;
258
- const cp = text.codePointAt(index);
259
- return index + (cp !== undefined && cp > 0xffff ? 2 : 1);
341
+ function emptyBuffer() {
342
+ return { lines: [""], cursorRow: 0, cursorCol: 0 };
260
343
  }
261
- function displayWindowForCursor(buffer, cursor, inputWidth) {
262
- const clampedCursor = Math.max(0, Math.min(cursor, buffer.length));
263
- if (visibleLength(buffer) <= inputWidth) {
264
- return {
265
- display: buffer,
266
- cursorCol: visibleLength(buffer.slice(0, clampedCursor)),
267
- };
268
- }
269
- let start = 0;
270
- let prefixWidth = 0;
271
- while (start < clampedCursor
272
- && visibleLength(buffer.slice(start, clampedCursor)) > inputWidth - prefixWidth) {
273
- start = nextCodePointIndex(buffer, start);
274
- prefixWidth = start > 0 ? 1 : 0;
275
- }
276
- if (start > 0)
277
- prefixWidth = 1;
278
- const bodyWidth = Math.max(0, inputWidth - prefixWidth);
279
- while (start < clampedCursor
280
- && visibleLength(buffer.slice(start, clampedCursor)) > bodyWidth) {
281
- start = nextCodePointIndex(buffer, start);
344
+ function bufferFromText(text) {
345
+ const lines = text.length > 0 ? text.split("\n") : [""];
346
+ const cursorRow = Math.max(0, lines.length - 1);
347
+ return {
348
+ lines,
349
+ cursorRow,
350
+ cursorCol: lines[cursorRow]?.length ?? 0,
351
+ };
352
+ }
353
+ function normalizeBuffer(buffer) {
354
+ if (buffer.lines.length === 0)
355
+ buffer.lines.push("");
356
+ buffer.cursorRow = Math.max(0, Math.min(buffer.cursorRow, buffer.lines.length - 1));
357
+ buffer.cursorCol = Math.max(0, Math.min(buffer.cursorCol, buffer.lines[buffer.cursorRow].length));
358
+ }
359
+ function applyCurateTextBufferEvent(editing, event) {
360
+ normalizeBuffer(editing.buffer);
361
+ const changed = applyTextBufferEvent(editing.buffer, event);
362
+ normalizeBuffer(editing.buffer);
363
+ return changed;
364
+ }
365
+ function navigableCount(issueCount, state) {
366
+ return issueCount + state.userAdded.length + 1;
367
+ }
368
+ function clampCursor(issueCount, state) {
369
+ state.cursor = Math.max(0, Math.min(state.cursor, navigableCount(issueCount, state) - 1));
370
+ }
371
+ function renderMultilineEditor(buffer, width, firstPrefix, continuationPrefix) {
372
+ normalizeBuffer(buffer);
373
+ const lines = [];
374
+ const firstPrefixWidth = visibleLength(firstPrefix);
375
+ const continuationPrefixWidth = visibleLength(continuationPrefix);
376
+ const bodyWidth = Math.max(1, width - firstPrefixWidth);
377
+ for (let row = 0; row < buffer.lines.length; row++) {
378
+ const raw = buffer.lines[row] ?? "";
379
+ const rendered = renderWrappedLine(raw, bodyWidth, row === buffer.cursorRow ? buffer.cursorCol : null);
380
+ for (let subRow = 0; subRow < rendered.length; subRow++) {
381
+ const prefix = row === 0 && subRow === 0 ? firstPrefix : continuationPrefix;
382
+ lines.push(clipToWidth(`${prefix}${rendered[subRow]}`, width));
383
+ }
282
384
  }
283
- let end = start;
284
- while (end < buffer.length) {
285
- const next = nextCodePointIndex(buffer, end);
286
- if (visibleLength(buffer.slice(start, next)) > bodyWidth)
287
- break;
288
- end = next;
385
+ const cursorLine = buffer.lines[buffer.cursorRow] ?? "";
386
+ let cursorRowOffset = 0;
387
+ for (let row = 0; row < buffer.cursorRow; row++) {
388
+ cursorRowOffset += softWrapLine(buffer.lines[row] ?? "", bodyWidth).length;
289
389
  }
290
- const prefix = start > 0 ? "…" : "";
390
+ const cursorSegments = softWrapLine(cursorLine, bodyWidth);
391
+ const cursor = cursorToVisual(cursorSegments, buffer.cursorCol, cursorLine.length, bodyWidth);
392
+ cursorRowOffset += cursor.subRow;
393
+ const cursorPrefixWidth = buffer.cursorRow === 0 && cursor.subRow === 0
394
+ ? firstPrefixWidth
395
+ : continuationPrefixWidth;
291
396
  return {
292
- display: prefix + buffer.slice(start, end),
293
- cursorCol: prefixWidth + visibleLength(buffer.slice(start, clampedCursor)),
397
+ lines,
398
+ cursorRowOffset,
399
+ cursorCol: Math.min(Math.max(0, width - 1), cursorPrefixWidth + cursor.visCol),
294
400
  };
295
401
  }
296
402
  function buildLeftPane(verdict, state, width, rows) {
@@ -305,14 +411,18 @@ function buildLeftPane(verdict, state, width, rows) {
305
411
  }
306
412
  }
307
413
  lines.push("");
308
- // Track each issue's header row so we can scroll the left pane to keep
309
- // the active issue on screen when the natural surface is taller than
310
- // `rows`. Editor placement also feeds into the anchor.
311
- const issueStartRows = [];
414
+ // Every navigable row gets an explicit anchor so scrolling and prompt
415
+ // cursor placement work for reviewer issues, user-added rows, and the
416
+ // trailing add row uniformly.
417
+ const rowStarts = [];
418
+ const rowEnds = [];
312
419
  let editorRow;
313
420
  let editorCol = 0;
314
421
  for (let i = 0; i < issues.length; i++) {
315
- const isCursor = i === state.cursor;
422
+ if (i > 0)
423
+ lines.push("");
424
+ const rowIndex = i;
425
+ const isCursor = rowIndex === state.cursor;
316
426
  const arrow = isCursor ? `${bold}❯${reset}` : " ";
317
427
  const tag = tagFor(state.decisions[i]);
318
428
  const issue = issues[i];
@@ -327,7 +437,7 @@ function buildLeftPane(verdict, state, width, rows) {
327
437
  const metaToken = meta.length ? `${dim}[${meta.join("/")}]${reset}` : "";
328
438
  const metaWidth = meta.length ? visibleLength(metaToken) : 0;
329
439
  const rendered = renderMarkdown(issue.description, avail);
330
- issueStartRows.push(lines.length);
440
+ rowStarts[rowIndex] = lines.length;
331
441
  if (rendered.length === 0) {
332
442
  const headRow = metaToken ? `${head}${metaToken}` : head;
333
443
  lines.push(clipToWidth(headRow, width));
@@ -353,51 +463,96 @@ function buildLeftPane(verdict, state, width, rows) {
353
463
  }
354
464
  // Inline revise row(s).
355
465
  if (state.decisions[i] === "revise") {
356
- const reviseAvail = Math.max(1, width - 4);
357
- if (state.editing && state.editing.index === i) {
358
- const buf = state.editing.buffer;
359
- const prefix = " └─ Revise: ";
360
- const prefixWidth = visibleLength(prefix);
361
- const inputWidth = Math.max(1, width - prefixWidth - 1);
362
- const window = displayWindowForCursor(buf, state.editing.cursor, inputWidth);
363
- const display = applyBlockCursor(window.display, window.cursorCol);
364
- const row = `${dim}${prefix}${reset}${display}`;
365
- lines.push(clipToWidth(row, width));
366
- editorRow = lines.length - 1;
367
- editorCol = Math.min(width - 1, prefixWidth + window.cursorCol);
466
+ const prefix = " └─ Revise: ";
467
+ const reviseAvail = Math.max(1, width - visibleLength(prefix));
468
+ if (state.editing?.target.kind === "reviewer-revise" &&
469
+ state.editing.target.index === i) {
470
+ const renderedEditor = renderMultilineEditor(state.editing.buffer, width, `${dim}${prefix}${reset}`, `${dim}${" ".repeat(visibleLength(prefix))}${reset}`);
471
+ editorRow = lines.length + renderedEditor.cursorRowOffset;
472
+ editorCol = renderedEditor.cursorCol;
473
+ lines.push(...renderedEditor.lines);
368
474
  }
369
475
  else if (state.comments[i]) {
370
- const prefix = " └─ Revise: ";
371
476
  for (const l of wrapToWidth(state.comments[i], reviseAvail)) {
372
- lines.push(`${dim}${prefix}${l}${reset}`);
477
+ lines.push(clipToWidth(`${dim}${prefix}${l}${reset}`, width));
373
478
  }
374
479
  }
375
480
  }
376
- if (i < issues.length - 1)
481
+ rowEnds[rowIndex] = lines.length;
482
+ }
483
+ for (let i = 0; i < state.userAdded.length; i++) {
484
+ const rowIndex = issues.length + i;
485
+ if (rowIndex > 0)
377
486
  lines.push("");
487
+ const isCursor = rowIndex === state.cursor;
488
+ const arrow = isCursor ? `${bold}❯${reset}` : " ";
489
+ const head = `${arrow} ${userAddedTag()} `;
490
+ const headWidth = visibleLength(head);
491
+ const avail = Math.max(1, width - headWidth);
492
+ const indent = " ".repeat(headWidth);
493
+ rowStarts[rowIndex] = lines.length;
494
+ if (state.editing?.target.kind === "user-edit" &&
495
+ state.editing.target.index === i) {
496
+ const renderedEditor = renderMultilineEditor(state.editing.buffer, width, head, indent);
497
+ editorRow = lines.length + renderedEditor.cursorRowOffset;
498
+ editorCol = renderedEditor.cursorCol;
499
+ lines.push(...renderedEditor.lines);
500
+ }
501
+ else {
502
+ const rendered = renderMarkdown(state.userAdded[i] ?? "", avail);
503
+ if (rendered.length === 0) {
504
+ lines.push(clipToWidth(head, width));
505
+ }
506
+ else {
507
+ lines.push(clipToWidth(`${head}${rendered[0]}`, width));
508
+ for (let j = 1; j < rendered.length; j++) {
509
+ lines.push(clipToWidth(`${indent}${rendered[j]}`, width));
510
+ }
511
+ }
512
+ }
513
+ rowEnds[rowIndex] = lines.length;
514
+ }
515
+ {
516
+ const rowIndex = issues.length + state.userAdded.length;
517
+ if (rowIndex > 0)
518
+ lines.push("");
519
+ const isCursor = rowIndex === state.cursor;
520
+ const arrow = isCursor ? `${bold}❯${reset}` : " ";
521
+ rowStarts[rowIndex] = lines.length;
522
+ if (state.editing?.target.kind === "user-add") {
523
+ const head = `${arrow} ${userAddedTag()} `;
524
+ const headWidth = visibleLength(head);
525
+ const renderedEditor = renderMultilineEditor(state.editing.buffer, width, head, " ".repeat(headWidth));
526
+ editorRow = lines.length + renderedEditor.cursorRowOffset;
527
+ editorCol = renderedEditor.cursorCol;
528
+ lines.push(...renderedEditor.lines);
529
+ }
530
+ else {
531
+ lines.push(clipToWidth(`${arrow} ${dim}+ Add feedback${reset}`, width));
532
+ }
533
+ rowEnds[rowIndex] = lines.length;
378
534
  }
379
535
  lines.push("");
380
- const helpStartRow = lines.length;
381
536
  const helpRows = [];
382
537
  if (state.editing) {
383
- helpRows.push("Enter Submit · Esc Cancel");
538
+ helpRows.push("Ctrl+J Newline · Enter Submit · Esc Cancel");
539
+ }
540
+ else if (state.cursor < issues.length) {
541
+ helpRows.push("↑/↓ Navigate · k/d/r Set · a Add · n Next Citation · Tab Cycle · Enter Edit · Ctrl+D Submit · Ctrl+C Quit");
542
+ }
543
+ else if (state.cursor < issues.length + state.userAdded.length) {
544
+ helpRows.push("↑/↓ Navigate · a Add · Enter Edit · d Delete · Ctrl+D Submit · Ctrl+C Quit");
384
545
  }
385
546
  else {
386
- helpRows.push("↑/↓ Navigate · k/d/r Set · n Next Citation · Tab Cycle · Enter Edit · Ctrl+D Submit · Ctrl+C Quit");
547
+ helpRows.push("↑/↓ Navigate · Enter/a Add · Ctrl+D Submit · Ctrl+C Quit");
387
548
  }
388
549
  for (const row of helpRows) {
389
550
  for (const l of wrapToWidth(row, width)) {
390
551
  lines.push(`${dim}${l}${reset}`);
391
552
  }
392
553
  }
393
- // Scroll the left pane so the active issue stays on screen. Anchor to the
394
- // active issue's header row; when the editor is open, also keep the editor
395
- // row visible (preferring the editor when both can't fit). Falls back to
396
- // scrollTop=0 when the natural surface fits in `rows`.
397
- const activeStart = issueStartRows[state.cursor] ?? 0;
398
- const activeEndExclusive = state.cursor + 1 < issueStartRows.length
399
- ? issueStartRows[state.cursor + 1] - 1 // one before the spacer
400
- : helpStartRow - 1; // last issue ends just before the spacer
554
+ const activeStart = rowStarts[state.cursor] ?? 0;
555
+ const activeEndExclusive = rowEnds[state.cursor] ?? lines.length;
401
556
  // Prefer to show the entire active block; if it is taller than `rows`,
402
557
  // fall back to the editor row (when editing) or the header row.
403
558
  let scrollTop = 0;
@@ -408,14 +563,17 @@ function buildLeftPane(verdict, state, width, rows) {
408
563
  // If the block fits and there is room above, pull the window up so we
409
564
  // also include some preceding context rather than leaving blank rows
410
565
  // below.
411
- if (activeEndExclusive - scrollTop + 1 < rows) {
566
+ if (activeEndExclusive - scrollTop < rows) {
412
567
  scrollTop = Math.max(0, total - rows);
413
568
  if (scrollTop > activeStart)
414
569
  scrollTop = activeStart;
415
570
  }
416
571
  // If editing, prefer keeping the editor row visible.
417
- if (editorRow !== undefined && editorRow >= scrollTop + rows) {
418
- scrollTop = editorRow - rows + 1;
572
+ if (editorRow !== undefined) {
573
+ if (editorRow < scrollTop)
574
+ scrollTop = editorRow;
575
+ if (editorRow >= scrollTop + rows)
576
+ scrollTop = editorRow - rows + 1;
419
577
  }
420
578
  // Clamp.
421
579
  if (scrollTop < 0)