poly-weaver 0.9.4 → 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.
- package/dist/agents/reviewers/prompts.d.ts.map +1 -1
- package/dist/agents/reviewers/prompts.js +7 -2
- package/dist/agents/reviewers/prompts.js.map +1 -1
- package/dist/flow/built-in/default-factory.d.ts.map +1 -1
- package/dist/flow/built-in/default-factory.js +1 -9
- package/dist/flow/built-in/default-factory.js.map +1 -1
- package/dist/flow/custom/AUTHORING.md +8 -19
- package/dist/flow-editor/tui.d.ts.map +1 -1
- package/dist/flow-editor/tui.js +22 -3
- package/dist/flow-editor/tui.js.map +1 -1
- package/dist/preview-panel.d.ts.map +1 -1
- package/dist/preview-panel.js +14 -12
- package/dist/preview-panel.js.map +1 -1
- package/dist/startup-tui.d.ts.map +1 -1
- package/dist/startup-tui.js +15 -7
- package/dist/startup-tui.js.map +1 -1
- package/dist/terminal/win32-key-translator.d.ts.map +1 -1
- package/dist/terminal/win32-key-translator.js +5 -4
- package/dist/terminal/win32-key-translator.js.map +1 -1
- package/dist/terminal-input.d.ts.map +1 -1
- package/dist/terminal-input.js +12 -3
- package/dist/terminal-input.js.map +1 -1
- package/dist/text-editing/emacs-input.d.ts +1 -1
- package/dist/text-editing/emacs-input.d.ts.map +1 -1
- package/dist/text-editing/emacs-input.js +2 -4
- package/dist/text-editing/emacs-input.js.map +1 -1
- package/dist/text-editing/soft-wrap.d.ts +16 -0
- package/dist/text-editing/soft-wrap.d.ts.map +1 -0
- package/dist/text-editing/soft-wrap.js +134 -0
- package/dist/text-editing/soft-wrap.js.map +1 -0
- package/dist/user/curate-handler.d.ts +16 -11
- package/dist/user/curate-handler.d.ts.map +1 -1
- package/dist/user/curate-handler.js +33 -22
- package/dist/user/curate-handler.js.map +1 -1
- package/dist/user/curate-prompt.d.ts +3 -0
- package/dist/user/curate-prompt.d.ts.map +1 -1
- package/dist/user/curate-prompt.js +4 -1
- package/dist/user/curate-prompt.js.map +1 -1
- package/dist/user/host-curate-prompt.d.ts +5 -6
- package/dist/user/host-curate-prompt.d.ts.map +1 -1
- package/dist/user/host-curate-prompt.js +261 -103
- package/dist/user/host-curate-prompt.js.map +1 -1
- package/dist/user/host-prompt.js +47 -36
- package/dist/user/host-prompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
6
|
-
import {
|
|
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 +
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 =
|
|
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 +
|
|
132
|
-
state.
|
|
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) %
|
|
137
|
-
state.
|
|
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.
|
|
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
|
-
|
|
177
|
-
buffer: state.
|
|
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"
|
|
189
|
-
state.
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
256
|
-
|
|
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
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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 ·
|
|
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
|
-
|
|
394
|
-
|
|
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
|
|
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
|
|
418
|
-
|
|
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)
|