@north-light/crouter 0.3.192 → 0.3.193
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/api/client.d.ts +31 -10
- package/dist/api/client.js +83 -24
- package/dist/api/dto/bash-jobs.d.ts +22 -0
- package/dist/api/dto/bash-jobs.js +9 -0
- package/dist/api/dto/human.d.ts +3 -4
- package/dist/api/dto/inbox.d.ts +106 -65
- package/dist/api/dto/inbox.js +2 -8
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/input/overlay-owner.js +1 -1
- package/dist/clients/attach/overlays/auth.js +34 -8
- package/dist/clients/attach/overlays/pickers.d.ts +5 -8
- package/dist/clients/attach/overlays/pickers.js +37 -43
- package/dist/clients/attach/viewer.js +643 -620
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
- package/dist/clients/inbox/controller.d.ts +2 -3
- package/dist/clients/inbox/controller.js +41 -42
- package/dist/clients/inbox/page-adapter.d.ts +33 -0
- package/dist/clients/inbox/page-adapter.js +54 -0
- package/dist/clients/inbox/resolve.d.ts +2 -2
- package/dist/clients/inbox/resolve.js +1 -1
- package/dist/clients/inbox/review-visibility.d.ts +1 -1
- package/dist/clients/inbox/review-visibility.js +1 -1
- package/dist/clients/inbox/tui/ansi.d.ts +1 -1
- package/dist/clients/inbox/tui/ansi.js +2 -2
- package/dist/clients/inbox/tui/input.d.ts +1 -3
- package/dist/clients/inbox/tui/input.js +242 -553
- package/dist/clients/inbox/tui/panel.d.ts +3 -1
- package/dist/clients/inbox/tui/panel.js +59 -187
- package/dist/clients/inbox/tui/render.d.ts +0 -25
- package/dist/clients/inbox/tui/render.js +165 -416
- package/dist/clients/inbox/tui/slots.d.ts +32 -0
- package/dist/clients/inbox/tui/slots.js +156 -0
- package/dist/clients/inbox/tui/types.d.ts +25 -13
- package/dist/clients/inbox/tui.js +5 -3
- package/dist/commands/attention.js +2 -2
- package/dist/commands/human/doc.js +66 -20
- package/dist/commands/human/prompts.d.ts +3 -3
- package/dist/commands/human/prompts.js +158 -112
- package/dist/commands/human/queue.d.ts +0 -1
- package/dist/commands/human/queue.js +34 -94
- package/dist/commands/human/shared.d.ts +10 -4
- package/dist/commands/human/shared.js +62 -13
- package/dist/commands/human.js +8 -4
- package/dist/commands/memory/read.js +13 -5
- package/dist/commands/node/bash.js +15 -28
- package/dist/commands/surface/node/focus.js +1 -1
- package/dist/core/__tests__/human-deliver.test.js +40 -46
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
- package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
- package/dist/core/auth-interaction.d.ts +2 -2
- package/dist/core/bash-jobs.d.ts +32 -0
- package/dist/core/bash-jobs.js +72 -1
- package/dist/core/canvas/__tests__/attention.test.js +16 -10
- package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
- package/dist/core/canvas/attention.js +1 -1
- package/dist/core/canvas/browse/app.js +1 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/remote-canvas-source.js +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-render.test.js +49 -0
- package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-tickets.test.js +112 -0
- package/dist/core/human/__tests__/page.test.d.ts +1 -0
- package/dist/core/human/__tests__/page.test.js +139 -0
- package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
- package/dist/core/human/claim.js +2 -2
- package/dist/core/human/convention.d.ts +4 -4
- package/dist/core/human/convention.js +6 -13
- package/dist/core/human/markdown-html.d.ts +8 -0
- package/dist/core/human/markdown-html.js +581 -0
- package/dist/core/human/page-catalog.d.ts +6 -0
- package/dist/core/human/page-catalog.js +43 -0
- package/dist/core/human/page-render.d.ts +3 -0
- package/dist/core/human/page-render.js +131 -0
- package/dist/core/human/page-schema.d.ts +312 -0
- package/dist/core/human/page-schema.js +291 -0
- package/dist/core/human/page-synth.d.ts +9 -0
- package/dist/core/human/page-synth.js +17 -0
- package/dist/core/human/page.d.ts +22 -0
- package/dist/core/human/page.js +427 -0
- package/dist/core/human/review-schema.d.ts +19 -0
- package/dist/core/human/review-schema.js +34 -0
- package/dist/core/human/scan.d.ts +12 -3
- package/dist/core/human/scan.js +69 -26
- package/dist/core/human/summary.d.ts +5 -6
- package/dist/core/human/summary.js +54 -37
- package/dist/core/human/tickets.d.ts +61 -11
- package/dist/core/human/tickets.js +100 -57
- package/dist/core/human/types.d.ts +21 -66
- package/dist/core/human/types.js +1 -5
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/ticket-filter.d.ts +1 -1
- package/dist/core/review/ticket-filter.js +2 -8
- package/dist/core/runtime/broker/read-ops.js +3 -0
- package/dist/core/runtime/broker.js +19 -10
- package/dist/core/runtime/package-health.js +1 -1
- package/dist/core/termrender/termrender.d.ts +1 -1
- package/dist/core/termrender/termrender.js +2 -2
- package/dist/core/tui/page-host.d.ts +1 -1
- package/dist/core/tui/page-host.js +2 -2
- package/dist/core/user-settings.d.ts +19 -0
- package/dist/core/user-settings.js +22 -0
- package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
- package/dist/daemon/api/handlers/bash-jobs.js +89 -0
- package/dist/daemon/api/handlers/human.js +16 -14
- package/dist/daemon/api/handlers/inbox.js +311 -227
- package/dist/daemon/api/router.d.ts +5 -1
- package/dist/daemon/api/router.js +5 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +23 -10
- package/dist/daemon/human/finish.d.ts +8 -3
- package/dist/daemon/human/finish.js +35 -15
- package/dist/pages/bundle.css +1 -0
- package/dist/pages/bundle.js +1347 -0
- package/dist/pages/comments.d.ts +34 -0
- package/dist/pages/comments.js +72 -0
- package/dist/pages/elements/cards.d.ts +17 -0
- package/dist/pages/elements/cards.js +785 -0
- package/dist/pages/elements/chart.d.ts +1 -0
- package/dist/pages/elements/chart.js +648 -0
- package/dist/pages/elements/options.d.ts +21 -0
- package/dist/pages/elements/options.js +604 -0
- package/dist/pages/elements/pages.d.ts +24 -0
- package/dist/pages/elements/pages.js +355 -0
- package/dist/pages/elements/slot.d.ts +21 -0
- package/dist/pages/elements/slot.js +105 -0
- package/dist/pages/elements/table.d.ts +19 -0
- package/dist/pages/elements/table.js +906 -0
- package/dist/pages/elements/text.d.ts +25 -0
- package/dist/pages/elements/text.js +951 -0
- package/dist/pages/entry.d.ts +8 -0
- package/dist/pages/entry.js +8 -0
- package/dist/pages/host.d.ts +132 -0
- package/dist/pages/host.js +149 -0
- package/dist/pages/register.d.ts +2 -0
- package/dist/pages/register.js +3 -0
- package/dist/pages/slot-config.d.ts +36 -0
- package/dist/pages/slot-config.js +50 -0
- package/dist/pages/types.d.ts +98 -0
- package/dist/pages/types.js +23 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +2 -0
- package/package.json +8 -8
- package/runtime.lock.json +156 -101
- package/dist/clients/inbox/deck-adapter.d.ts +0 -29
- package/dist/clients/inbox/deck-adapter.js +0 -63
- package/dist/core/human/deck-factories.d.ts +0 -8
- package/dist/core/human/deck-factories.js +0 -15
- package/dist/core/human/deck-schema.d.ts +0 -70
- package/dist/core/human/deck-schema.js +0 -92
|
@@ -1,95 +1,53 @@
|
|
|
1
1
|
import stringWidth from 'string-width';
|
|
2
|
+
import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
|
|
2
3
|
import { renderMarkdownBlockAwareLines } from '../../../core/termrender/termrender.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import { anchorLabel, describeSlot, pageComplete, slotActions, slotAnswered, summarizeSlotResponse, tableMarkdown, terminalAnswerable } from './slots.js';
|
|
5
|
+
import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, hardWrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
|
|
5
6
|
const PAN_STEP = 8;
|
|
6
|
-
|
|
7
|
+
const mdLines = (md, maxW, paneW) => renderMarkdownBlockAwareLines(md, maxW, paneW);
|
|
8
|
+
function proseLines(blocks, maxW, paneW) {
|
|
9
|
+
return blocks.flatMap((block, index) => [...(index > 0 ? [''] : []), ...mdLines(block, maxW, paneW)]);
|
|
10
|
+
}
|
|
7
11
|
export function renderOverview(state, cols, rows) {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
lines.push(` ${
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
13
|
+
const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)}${RESET}`];
|
|
14
|
+
if (state.manifest.subtitle)
|
|
15
|
+
lines.push(` ${DIM}${sanitize(state.manifest.subtitle)}${RESET}`);
|
|
16
|
+
lines.push(` ${DIM}${hline(maxW)}${RESET}`, '');
|
|
17
|
+
if (state.manifest.dialect === 'html')
|
|
18
|
+
lines.push(` ${DIM}rich page — open in Northlight${RESET}`, '');
|
|
19
|
+
else
|
|
20
|
+
for (const line of proseLines(state.overviewProse, maxW, Math.max(20, cols - 4)))
|
|
21
|
+
lines.push(` ${line}`);
|
|
22
|
+
let priorStep = -1;
|
|
23
|
+
for (let i = 0; i < state.slots.length; i++) {
|
|
24
|
+
const slot = state.slots[i];
|
|
25
|
+
if (state.manifest.steps > 1 && slot.step !== priorStep) {
|
|
26
|
+
if (priorStep !== -1)
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push(` ${BOLD}Step ${slot.step + 1}/${state.manifest.steps}${RESET}`);
|
|
29
|
+
priorStep = slot.step;
|
|
30
|
+
}
|
|
31
|
+
const response = slot.id === undefined ? undefined : state.responses.get(slot.id);
|
|
32
|
+
const icon = isResponseBearingSlot(slot) ? (slotAnswered(state, slot) ? `${GREEN}✓${RESET}` : `${DIM}○${RESET}`) : `${DIM}·${RESET}`;
|
|
24
33
|
const cursor = i === state.currentIndex ? `${CYAN}▸${RESET} ` : ' ';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
questionIndex: i,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const reserved = 4 + 3 + 2;
|
|
40
|
-
const available = Math.max(1, rows - reserved);
|
|
41
|
-
let scroll = state.scrollOffset || 0;
|
|
42
|
-
const focusRow = rowsBuf.findIndex((r) => r.questionIndex === state.currentIndex);
|
|
43
|
-
if (focusRow >= 0) {
|
|
44
|
-
if (focusRow < scroll)
|
|
45
|
-
scroll = focusRow;
|
|
46
|
-
if (focusRow >= scroll + available)
|
|
47
|
-
scroll = focusRow - available + 1;
|
|
48
|
-
}
|
|
49
|
-
scroll = Math.max(0, Math.min(scroll, Math.max(0, rowsBuf.length - available)));
|
|
50
|
-
if (scroll > 0) {
|
|
51
|
-
lines.push(` ${DIM}↑ ${scroll} more above${RESET}`);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
lines.push('');
|
|
55
|
-
}
|
|
56
|
-
const end = Math.min(rowsBuf.length, scroll + available);
|
|
57
|
-
for (let i = scroll; i < end; i++)
|
|
58
|
-
lines.push(rowsBuf[i].line);
|
|
59
|
-
if (end < rowsBuf.length) {
|
|
60
|
-
lines.push(` ${DIM}↓ ${rowsBuf.length - end} more below${RESET}`);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
lines.push('');
|
|
64
|
-
}
|
|
65
|
-
lines.push(` ${DIM}${hline(Math.min(cols - 4, 60))}${RESET}`);
|
|
66
|
-
lines.push(` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} finish`);
|
|
67
|
-
while (lines.length < rows)
|
|
68
|
-
lines.push('');
|
|
69
|
-
// Overview content extends roughly cols-16 wide for option labels; center
|
|
70
|
-
// against a 60-col cap (the divider width) when the terminal is much wider.
|
|
71
|
-
const centered = centerHorizontal(lines.slice(0, rows), cols, Math.min(cols, 60) + 2);
|
|
72
|
-
return centered;
|
|
34
|
+
lines.push(` ${cursor}${icon} ${truncate(singleLine(describeSlot(slot)), Math.max(10, cols - 16))}`);
|
|
35
|
+
if (response !== undefined)
|
|
36
|
+
lines.push(` ${DIM}${truncate(singleLine(summarizeSlotResponse(slot, response)), Math.max(10, cols - 10))}${RESET}`);
|
|
37
|
+
}
|
|
38
|
+
if (state.slots.length === 0)
|
|
39
|
+
lines.push(` ${DIM}No interactive slots${RESET}`);
|
|
40
|
+
lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
|
|
41
|
+
const display = !state.manifest.slots.some(isResponseBearingSlot);
|
|
42
|
+
lines.push(display ? ` ${DIM}q${RESET} dismiss` : ` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} close`);
|
|
43
|
+
return centeredWindow(lines, state, cols, rows, maxW + 2);
|
|
73
44
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Render a (possibly multiline) input buffer with a visible cursor at
|
|
76
|
-
* `cursor` (a code-point index). A private-use sentinel is inserted at the
|
|
77
|
-
* cursor position, the result is hard-wrapped (so `\n` and width-wrapping
|
|
78
|
-
* behave exactly as they would without a cursor), then the sentinel is
|
|
79
|
-
* swapped for a reverse-video block on whichever wrapped line it landed on.
|
|
80
|
-
* `stringWidth('\uE000') === 1`, so it occupies exactly one column and never
|
|
81
|
-
* throws off the wrap math.
|
|
82
|
-
*/
|
|
83
45
|
export function renderInputBuffer(buffer, cursor, maxWidth) {
|
|
84
46
|
const chars = [...buffer];
|
|
85
47
|
const at = Math.max(0, Math.min(cursor, chars.length));
|
|
86
48
|
const withCaret = [...chars.slice(0, at), '\uE000', ...chars.slice(at)].join('');
|
|
87
|
-
|
|
88
|
-
return wrapped.map((line) => (line.includes('\uE000') ? line.replace('\uE000', `${REVERSE} ${RESET}`) : line));
|
|
49
|
+
return hardWrap(withCaret, maxWidth).map((line) => line.includes('\uE000') ? line.replace('\uE000', `${REVERSE} ${RESET}`) : line);
|
|
89
50
|
}
|
|
90
|
-
/** Code-point bounds of every visual row `renderInputBuffer` paints, mirroring
|
|
91
|
-
* `hardWrap` (character-based, lossless) so a cursor index maps to the row the
|
|
92
|
-
* human actually sees. `end` is exclusive of the row's trailing '\n', if any. */
|
|
93
51
|
export function wrappedRowBounds(buffer, width) {
|
|
94
52
|
const chars = [...buffer];
|
|
95
53
|
const w = Math.max(1, width);
|
|
@@ -110,16 +68,12 @@ export function wrappedRowBounds(buffer, width) {
|
|
|
110
68
|
start = i;
|
|
111
69
|
rowW = cw;
|
|
112
70
|
}
|
|
113
|
-
else
|
|
71
|
+
else
|
|
114
72
|
rowW += cw;
|
|
115
|
-
}
|
|
116
73
|
}
|
|
117
74
|
rows.push({ start, end: chars.length });
|
|
118
75
|
return rows;
|
|
119
76
|
}
|
|
120
|
-
/** Vertical motion by *visual* row — what up/down mean in a wrapped input box,
|
|
121
|
-
* where one long soft-wrapped paragraph spans many rows. Returns the new
|
|
122
|
-
* code-point cursor, unchanged when there is no row in that direction. */
|
|
123
77
|
export function verticalCursor(buffer, cursor, delta, width) {
|
|
124
78
|
const rows = wrappedRowBounds(buffer, width);
|
|
125
79
|
const at = Math.max(0, Math.min(cursor, [...buffer].length));
|
|
@@ -128,349 +82,144 @@ export function verticalCursor(buffer, cursor, delta, width) {
|
|
|
128
82
|
if (rows[i].start <= at)
|
|
129
83
|
r = i;
|
|
130
84
|
const target = rows[r + delta];
|
|
131
|
-
|
|
132
|
-
return at;
|
|
133
|
-
return Math.min(target.start + (at - rows[r].start), target.end);
|
|
85
|
+
return target === undefined ? at : Math.min(target.start + (at - rows[r].start), target.end);
|
|
134
86
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
87
|
+
function responseComments(state, slot) {
|
|
88
|
+
if (slot.id === undefined)
|
|
89
|
+
return [];
|
|
90
|
+
const response = state.responses.get(slot.id);
|
|
91
|
+
return response?.comments ?? [];
|
|
92
|
+
}
|
|
93
|
+
function selected(state, slot, group, id) {
|
|
94
|
+
if (slot.id === undefined)
|
|
95
|
+
return false;
|
|
96
|
+
const r = state.responses.get(slot.id);
|
|
97
|
+
if (group === 'option')
|
|
98
|
+
return r?.selectedOptionIds?.includes(id) === true;
|
|
99
|
+
if (group === 'card')
|
|
100
|
+
return r?.selectedCardIds?.includes(id) === true;
|
|
101
|
+
if (group === 'row')
|
|
102
|
+
return r?.selectedRowIds?.includes(id) === true;
|
|
103
|
+
return r?.selectedColumnIds?.includes(id) === true;
|
|
104
|
+
}
|
|
105
|
+
function itemLines(state, cols) {
|
|
106
|
+
const slot = state.slots[state.currentIndex];
|
|
107
|
+
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
148
108
|
const paneW = Math.max(20, cols - 4);
|
|
149
|
-
const
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
if (interaction.body) {
|
|
180
|
-
bodyLines.push('');
|
|
181
|
-
for (const line of mdLines(interaction.body)) {
|
|
182
|
-
bodyLines.push(` ${line}`);
|
|
183
|
-
}
|
|
109
|
+
const lines = ['', ` ${BOLD}${CYAN}[${state.currentIndex + 1}/${state.slots.length}]${RESET} ${DIM}${sanitize(state.manifest.title)}${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, '', ` ${BOLD}${sanitize(describeSlot(slot))}${RESET}`];
|
|
110
|
+
// A single-slot page opens straight into item-review, so its leading prose has nowhere else to render.
|
|
111
|
+
const prose = state.slots.length === 1 ? [...state.overviewProse, ...(state.slotProse.get(0) ?? [])] : state.slotProse.get(state.currentIndex) ?? [];
|
|
112
|
+
if (state.manifest.dialect === 'html')
|
|
113
|
+
lines.push('', ` ${DIM}rich page — open in Northlight${RESET}`);
|
|
114
|
+
else
|
|
115
|
+
for (const line of proseLines(prose, maxW, paneW))
|
|
116
|
+
lines.push(` ${line}`);
|
|
117
|
+
if (!terminalAnswerable(slot))
|
|
118
|
+
lines.push('', ` ${DIM}${sanitize(slot.kind)} — cannot be answered in the terminal; open in Northlight${RESET}`);
|
|
119
|
+
else if (slot.kind === 'chart') {
|
|
120
|
+
const c = slot.config;
|
|
121
|
+
const title = c.title ?? `${c.x} vs ${(c.y ?? []).map((y) => y.label ?? y.field).join(', ')}`;
|
|
122
|
+
lines.push('', ` ${DIM}chart — ${sanitize(c.kind)}: ${sanitize(title)}${RESET}`);
|
|
123
|
+
}
|
|
124
|
+
else if (slot.kind === 'table') {
|
|
125
|
+
lines.push('');
|
|
126
|
+
for (const line of mdLines(tableMarkdown(slot.config), maxW, paneW))
|
|
127
|
+
lines.push(` ${line}`);
|
|
128
|
+
const caption = slot.config.caption;
|
|
129
|
+
if (caption)
|
|
130
|
+
lines.push(` ${DIM}${sanitize(caption)}${RESET}`);
|
|
131
|
+
}
|
|
132
|
+
else if (slot.kind === 'text') {
|
|
133
|
+
const response = slot.id === undefined ? undefined : state.responses.get(slot.id);
|
|
134
|
+
const text = response?.text ?? slot.config.initialText;
|
|
135
|
+
lines.push('');
|
|
136
|
+
for (const line of mdLines(text, maxW, paneW))
|
|
137
|
+
lines.push(` ${line}`);
|
|
184
138
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: state.inputMode.kind === 'comment' ? 'Comment' : 'Response';
|
|
193
|
-
// Show attached option in comment mode. For single-select the comment
|
|
194
|
-
// qualifies the pick; for multi-select an attached option means the
|
|
195
|
-
// comment is saved as a per-option note (and auto-checks the option).
|
|
196
|
-
let attachedLine;
|
|
197
|
-
if (state.inputMode.kind === 'comment') {
|
|
198
|
-
const attachedId = state.inputMode.selectedOptionId;
|
|
199
|
-
const opts = interaction.options;
|
|
200
|
-
if (opts.length > 0) {
|
|
201
|
-
const attached = attachedId !== undefined
|
|
202
|
-
? opts.find((o) => o.id === attachedId)
|
|
203
|
-
: undefined;
|
|
204
|
-
const valueText = attached !== undefined
|
|
205
|
-
? `${CYAN}${truncate(singleLine(attached.label), Math.max(10, maxW - 28))}${RESET}`
|
|
206
|
-
: `${DIM}none (overall)${RESET}`;
|
|
207
|
-
attachedLine = ` ${DIM}attached:${RESET} ${valueText} ${DIM}[tab to cycle]${RESET}`;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
for (const labelLine of wrap(`${singleLine(label)}:`, maxW)) {
|
|
211
|
-
postLines.push(` ${YELLOW}${labelLine}${RESET}`);
|
|
212
|
-
}
|
|
213
|
-
const bufLines = renderInputBuffer(state.inputMode.buffer, state.inputMode.cursor, maxW - 1);
|
|
214
|
-
for (const line of bufLines)
|
|
215
|
-
postLines.push(` ${line}`);
|
|
216
|
-
if (attachedLine !== undefined) {
|
|
217
|
-
postLines.push('');
|
|
218
|
-
postLines.push(attachedLine);
|
|
219
|
-
}
|
|
220
|
-
postLines.push('');
|
|
221
|
-
postLines.push(` ${DIM}enter${RESET} submit ${DIM}^J/⌥⏎${RESET} newline${state.editorAvailable ? ` ${DIM}^O${RESET} editor` : ''} ${DIM}esc${RESET} cancel`);
|
|
139
|
+
lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
|
|
140
|
+
if (state.inputMode !== null) {
|
|
141
|
+
const label = state.inputMode.kind === 'comment' ? `Comment on ${anchorLabel(slot, state.inputMode.anchor ?? { kind: 'whole' })}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
|
|
142
|
+
lines.push(` ${BOLD}${label}${RESET}`);
|
|
143
|
+
for (const line of renderInputBuffer(state.inputMode.buffer, state.inputMode.cursor, Math.max(10, maxW - 4)))
|
|
144
|
+
lines.push(` ${line}`);
|
|
145
|
+
lines.push(` ${DIM}enter save esc cancel${state.editorAvailable ? ' ^o editor' : ''}${RESET}`);
|
|
222
146
|
}
|
|
223
147
|
else {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
for (const line of visible)
|
|
247
|
-
widest = Math.max(widest, visibleWidth(line));
|
|
248
|
-
const bodyBoxW = widest > maxW + 2 ? paneW + 2 : maxW + 2;
|
|
249
|
-
return {
|
|
250
|
-
interaction, preLines, bodyLines, postLines,
|
|
251
|
-
maxW, bodyHeight, maxScroll, overflows,
|
|
252
|
-
bodyBoxW, maxHScroll: Math.max(0, widest - bodyBoxW),
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
/** Body rows as they are actually painted: vertical-scroll indicators replace
|
|
256
|
-
* their endpoint rows, so those hidden rows cannot make h/l available. */
|
|
257
|
-
function visibleItemBodyLines(bodyLines, scroll, bodyHeight) {
|
|
258
|
-
if (bodyLines.length <= bodyHeight)
|
|
259
|
-
return bodyLines;
|
|
260
|
-
const visible = bodyLines.slice(scroll, scroll + bodyHeight);
|
|
261
|
-
if (scroll > 0)
|
|
262
|
-
visible[0] = ` ${DIM}↑ ${scroll} more above${RESET}`;
|
|
263
|
-
const remainingBelow = bodyLines.length - (scroll + bodyHeight);
|
|
264
|
-
if (remainingBelow > 0)
|
|
265
|
-
visible[visible.length - 1] = ` ${DIM}↓ ${remainingBelow} more below${RESET}`;
|
|
266
|
-
return visible;
|
|
148
|
+
const actions = slotActions(slot);
|
|
149
|
+
actions.forEach((action, index) => {
|
|
150
|
+
const cursor = index === state.selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
151
|
+
if (action.kind === 'select') {
|
|
152
|
+
const mark = selected(state, slot, action.group, action.id) ? `${GREEN}✓${RESET}` : ' ';
|
|
153
|
+
lines.push(` ${cursor} [${mark}] ${action.shortcut ? `[${action.shortcut}] ` : ''}${sanitize(action.label)}`);
|
|
154
|
+
if (action.description)
|
|
155
|
+
lines.push(` ${DIM}${sanitize(action.description)}${RESET}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const key = action.kind === 'freetext' ? 'r' : action.kind === 'edit' ? 'e' : 'a';
|
|
159
|
+
lines.push(` ${cursor} [${key}] ${sanitize(action.label)}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
for (const comment of responseComments(state, slot))
|
|
163
|
+
lines.push(` ${DIM}attached: ${anchorLabel(slot, comment.anchor)} — ${sanitize(comment.text)}${RESET}`);
|
|
164
|
+
if (state.hint)
|
|
165
|
+
lines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
|
|
166
|
+
lines.push(` ${DIM}n/p next/previous j/k actions c comment u/d scroll h/l pan q close${RESET}`);
|
|
167
|
+
}
|
|
168
|
+
const maxLine = Math.max(0, ...lines.map(visibleWidth));
|
|
169
|
+
return { lines, maxW, maxH: Math.max(0, maxLine - Math.max(1, cols)) };
|
|
267
170
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Clamp state.scrollOffset to the current body's scroll bounds. The host runs
|
|
270
|
-
* this as a pre-render step (from the input path) so `renderItemReview` stays a
|
|
271
|
-
* pure function of state — the clamp keeps u/d scrolling responsive without the
|
|
272
|
-
* renderer mutating state mid-frame.
|
|
273
|
-
*/
|
|
274
171
|
export function clampItemReviewScroll(state, cols, rows) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
state.
|
|
172
|
+
if (state.slots[state.currentIndex] === undefined) {
|
|
173
|
+
state.scrollOffset = 0;
|
|
174
|
+
state.hscrollOffset = 0;
|
|
175
|
+
state.hscrollMax = 0;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const layout = itemLines(state, cols);
|
|
179
|
+
state.hscrollMax = layout.maxH;
|
|
180
|
+
state.hscrollOffset = Math.max(0, Math.min(state.hscrollOffset, state.hscrollMax));
|
|
181
|
+
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, Math.max(0, layout.lines.length - rows)));
|
|
283
182
|
}
|
|
284
|
-
/** Pan the card body horizontally. Clamping happens in the pre-render clamp
|
|
285
|
-
* the host already runs, exactly like vertical over-scroll. */
|
|
286
183
|
export function panItemReview(state, direction) {
|
|
287
|
-
state.hscrollOffset = Math.max(0, (state.hscrollOffset
|
|
184
|
+
state.hscrollOffset = Math.max(0, Math.min(state.hscrollMax, state.hscrollOffset + direction * PAN_STEP));
|
|
288
185
|
}
|
|
289
186
|
export function renderItemReview(state, cols, rows) {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// the content still off-screen.
|
|
298
|
-
const hscroll = Math.max(0, Math.min(state.hscrollOffset || 0, maxHScroll));
|
|
299
|
-
visibleBody = visibleBody.map((line) => panLine(line, hscroll, bodyBoxW));
|
|
300
|
-
// Footer hint — mention scroll keys when body overflows
|
|
301
|
-
const footerParts = interaction.multiSelect === true
|
|
302
|
-
? [
|
|
303
|
-
`${DIM}n/p${RESET} prev/next`,
|
|
304
|
-
`${DIM}space${RESET} toggle`,
|
|
305
|
-
`${DIM}enter${RESET} confirm`,
|
|
306
|
-
`${DIM}q${RESET} overview`,
|
|
307
|
-
]
|
|
308
|
-
: [
|
|
309
|
-
`${DIM}n/p${RESET} prev/next`,
|
|
310
|
-
`${DIM}q${RESET} overview`,
|
|
311
|
-
];
|
|
312
|
-
if (overflows) {
|
|
313
|
-
footerParts.unshift(state.inputMode ? `${DIM}pgup/pgdn${RESET} scroll` : `${DIM}u/d${RESET} scroll`);
|
|
314
|
-
}
|
|
315
|
-
if (maxHScroll > 0 && state.inputMode === null) {
|
|
316
|
-
footerParts.unshift(`${DIM}h/l${RESET} pan`);
|
|
317
|
-
}
|
|
318
|
-
const footer = ` ${footerParts.join(' ')}`;
|
|
319
|
-
// Assemble — pad to fill rows so post-body sits at the bottom
|
|
320
|
-
const lines = [...preLines, ...visibleBody, ...postLines];
|
|
321
|
-
while (lines.length < rows - 1)
|
|
322
|
-
lines.push('');
|
|
323
|
-
lines.push(footer);
|
|
324
|
-
// Final clamp (safety net for very small terminals)
|
|
325
|
-
const clamped = lines.length > rows
|
|
326
|
-
? [...lines.slice(0, rows - 1), footer]
|
|
327
|
-
: lines;
|
|
328
|
-
// Content occupies the body rectangle (prose column, or the full pane when a
|
|
329
|
-
// diagram claims it) — center the whole block when the terminal is wider,
|
|
330
|
-
// then contain every row so nothing can wrap past the card's rectangle.
|
|
331
|
-
return centerHorizontal(clamped, cols, bodyBoxW).map((line) => clipLine(line, cols));
|
|
332
|
-
}
|
|
333
|
-
/** The option list. Focus is always drawn so j/k and shortcuts show where they land. */
|
|
334
|
-
function renderActions(interaction, selectedAction, maxW, existing) {
|
|
335
|
-
const lines = [];
|
|
336
|
-
const opts = interaction.options;
|
|
337
|
-
// Prefix on first row: " X [s] " — 2 + 1 (cursor) + 1 + 3 ([s]) + 1 = 8 visible cols.
|
|
338
|
-
// Continuation rows align under the label so each option reads as a block.
|
|
339
|
-
const multi = interaction.multiSelect === true;
|
|
340
|
-
const checked = new Set(existing?.selectedOptionIds ?? []);
|
|
341
|
-
const prefixWidth = multi ? 12 : 8;
|
|
342
|
-
const indent = ' '.repeat(prefixWidth);
|
|
343
|
-
const contentMax = Math.max(20, maxW - prefixWidth);
|
|
344
|
-
const optionComments = existing !== undefined ? existing.optionComments : undefined;
|
|
345
|
-
for (let i = 0; i < opts.length; i++) {
|
|
346
|
-
const o = opts[i];
|
|
347
|
-
const cursor = i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
348
|
-
const sc = o.shortcut === undefined ? ' ' : o.shortcut;
|
|
349
|
-
const keyBadge = `${DIM}[${sc}]${RESET}`;
|
|
350
|
-
const box = multi
|
|
351
|
-
? (checked.has(o.id) ? `${GREEN}[x]${RESET}` : `${DIM}[ ]${RESET}`) + ' '
|
|
352
|
-
: '';
|
|
353
|
-
const labelLines = wrap(sanitize(o.label), contentMax);
|
|
354
|
-
for (let j = 0; j < labelLines.length; j++) {
|
|
355
|
-
const prefix = j === 0 ? ` ${cursor} ${box}${keyBadge} ` : indent;
|
|
356
|
-
lines.push(`${prefix}${labelLines[j]}`);
|
|
357
|
-
}
|
|
358
|
-
if (o.description) {
|
|
359
|
-
const descLines = wrap(`— ${sanitize(o.description)}`, contentMax);
|
|
360
|
-
for (const dl of descLines) {
|
|
361
|
-
lines.push(`${indent}${DIM}${dl}${RESET}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (multi && optionComments !== undefined) {
|
|
365
|
-
const note = optionComments[o.id];
|
|
366
|
-
if (typeof note === 'string' && note.length > 0) {
|
|
367
|
-
const noteLines = wrap(`✎ ${sanitize(note)}`, contentMax);
|
|
368
|
-
for (const nl of noteLines) {
|
|
369
|
-
lines.push(`${indent}${YELLOW}${nl}${RESET}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (interaction.allowFreetext && opts.length > 0) {
|
|
375
|
-
const cursor = opts.length === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
376
|
-
let label;
|
|
377
|
-
if (interaction.freetextLabel !== undefined)
|
|
378
|
-
label = interaction.freetextLabel;
|
|
379
|
-
else if (multi)
|
|
380
|
-
label = 'Add overall comment (c on an option for per-option)';
|
|
381
|
-
else
|
|
382
|
-
label = 'Add comment';
|
|
383
|
-
const ftLines = wrap(sanitize(label), contentMax);
|
|
384
|
-
for (let j = 0; j < ftLines.length; j++) {
|
|
385
|
-
const prefix = j === 0 ? ` ${cursor} ${DIM}[c]${RESET} ` : ' '.repeat(8);
|
|
386
|
-
lines.push(`${prefix}${ftLines[j]}`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
else if (interaction.allowFreetext && opts.length === 0) {
|
|
390
|
-
const ftLabel = interaction.freetextLabel !== undefined ? interaction.freetextLabel : 'Enter response';
|
|
391
|
-
const ftLines = wrap(sanitize(ftLabel), contentMax);
|
|
392
|
-
for (let j = 0; j < ftLines.length; j++) {
|
|
393
|
-
const prefix = j === 0 ? ` ${DIM}[r]${RESET} ` : ' '.repeat(6);
|
|
394
|
-
lines.push(`${prefix}${ftLines[j]}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (existing) {
|
|
398
|
-
lines.push('');
|
|
399
|
-
for (const curLine of wrap(`Current: ${responseSummary(existing, interaction)}`, maxW)) {
|
|
400
|
-
lines.push(` ${GREEN}${curLine}${RESET}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return lines;
|
|
187
|
+
const layout = itemLines(state, cols);
|
|
188
|
+
const start = state.scrollOffset;
|
|
189
|
+
const sliced = layout.lines.slice(start, start + rows);
|
|
190
|
+
while (sliced.length < rows)
|
|
191
|
+
sliced.push('');
|
|
192
|
+
const panned = state.hscrollOffset > 0 ? sliced.map((line) => panLine(line, state.hscrollOffset, cols)) : sliced;
|
|
193
|
+
return centerHorizontal(panned.map((line) => clipLine(line, cols)), cols, Math.min(cols, layout.maxW + 2));
|
|
404
194
|
}
|
|
405
195
|
export function renderFinal(state, cols, rows) {
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for (const interaction of state.interactions) {
|
|
425
|
-
const response = state.responses.get(interaction.id);
|
|
426
|
-
const preAnswered = state.preAnsweredIds.has(interaction.id);
|
|
427
|
-
const icon = response
|
|
428
|
-
? (preAnswered ? `${DIM}◆${RESET}` : `${GREEN}✓${RESET}`)
|
|
429
|
-
: `${YELLOW}○${RESET}`;
|
|
430
|
-
const label = singleLine(interaction.title);
|
|
431
|
-
questionRows.push(` ${icon} ${truncate(label, Math.max(10, maxW - 4))}`);
|
|
432
|
-
if (response) {
|
|
433
|
-
questionRows.push(` ${DIM}${truncate(singleLine(responseSummary(response, interaction)), Math.max(10, maxW - 6))}${RESET}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
const available = Math.max(1, rows - header.length - footer.length - 1);
|
|
437
|
-
let visible = questionRows;
|
|
438
|
-
if (questionRows.length > available) {
|
|
439
|
-
visible = [
|
|
440
|
-
...questionRows.slice(0, available - 1),
|
|
441
|
-
` ${DIM}… ${questionRows.length - (available - 1)} more rows omitted${RESET}`,
|
|
442
|
-
];
|
|
196
|
+
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
197
|
+
const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)} — Summary${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, ''];
|
|
198
|
+
for (const slot of state.slots) {
|
|
199
|
+
const response = slot.id === undefined ? undefined : state.responses.get(slot.id);
|
|
200
|
+
lines.push(` ${slotAnswered(state, slot) ? `${GREEN}✓${RESET}` : isResponseBearingSlot(slot) ? `${YELLOW}○${RESET}` : `${DIM}·${RESET}`} ${BOLD}${sanitize(describeSlot(slot))}${RESET} ${DIM}${sanitize(summarizeSlotResponse(slot, response))}${RESET}`);
|
|
201
|
+
}
|
|
202
|
+
if (state.manifest.dialect !== 'html')
|
|
203
|
+
for (const line of proseLines(state.finalProse, maxW, Math.max(20, cols - 4)))
|
|
204
|
+
lines.push(` ${line}`);
|
|
205
|
+
const complete = pageComplete(state);
|
|
206
|
+
if (!complete) {
|
|
207
|
+
lines.push('', ` ${YELLOW}Cannot submit yet.${RESET}`);
|
|
208
|
+
for (const slot of state.slots.filter(isResponseBearingSlot))
|
|
209
|
+
if (!slotAnswered(state, slot)) {
|
|
210
|
+
lines.push(` ${DIM}${slot.id ?? slot.kind} is unanswered${RESET}`);
|
|
211
|
+
if (!terminalAnswerable(slot))
|
|
212
|
+
lines.push(` ${DIM}${slot.id} must be answered in Northlight${RESET}`);
|
|
213
|
+
}
|
|
443
214
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
lines.push('');
|
|
447
|
-
return centerHorizontal(lines.slice(0, rows), cols, maxW + 2);
|
|
215
|
+
lines.push('', ` ${DIM}${hline(maxW)}${RESET}`, complete ? ` ${DIM}enter${RESET} submit ${DIM}q${RESET} back` : ` ${DIM}q${RESET} back`);
|
|
216
|
+
return centeredWindow(lines, state, cols, rows, maxW + 2);
|
|
448
217
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const note = oc !== undefined ? oc[o.id] : undefined;
|
|
457
|
-
return typeof note === 'string' && note.length > 0
|
|
458
|
-
? `${sanitize(o.label)} ("${sanitize(note)}")`
|
|
459
|
-
: sanitize(o.label);
|
|
460
|
-
});
|
|
461
|
-
const picks = parts.length > 0 ? parts.join(', ') : '(none)';
|
|
462
|
-
if (r.freetext)
|
|
463
|
-
return `${picks}: "${sanitize(r.freetext)}"`;
|
|
464
|
-
return picks;
|
|
465
|
-
}
|
|
466
|
-
const opt = r.selectedOptionId
|
|
467
|
-
? interaction.options.find((o) => o.id === r.selectedOptionId)
|
|
468
|
-
: undefined;
|
|
469
|
-
if (opt && r.freetext)
|
|
470
|
-
return `${sanitize(opt.label)}: "${sanitize(r.freetext)}"`;
|
|
471
|
-
if (opt)
|
|
472
|
-
return sanitize(opt.label);
|
|
473
|
-
if (r.freetext)
|
|
474
|
-
return sanitize(r.freetext);
|
|
475
|
-
return '(empty)';
|
|
218
|
+
function centeredWindow(lines, state, cols, rows, width) {
|
|
219
|
+
const max = Math.max(0, lines.length - rows);
|
|
220
|
+
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, max));
|
|
221
|
+
const out = lines.slice(state.scrollOffset, state.scrollOffset + rows);
|
|
222
|
+
while (out.length < rows)
|
|
223
|
+
out.push('');
|
|
224
|
+
return centerHorizontal(out.map((line) => clipLine(line, cols)), cols, Math.min(cols, width));
|
|
476
225
|
}
|