mergestorm 0.3.9 → 0.3.15
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/README.md +67 -24
- package/dist/api.d.ts +106 -40
- package/dist/api.js +245 -9
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/commands/branches.d.ts +6 -2
- package/dist/commands/branches.js +29 -14
- package/dist/commands/browse.d.ts +39 -0
- package/dist/commands/browse.js +259 -0
- package/dist/commands/credits.js +30 -12
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/jobs.d.ts +11 -2
- package/dist/commands/jobs.js +64 -15
- package/dist/commands/login.js +32 -10
- package/dist/commands/logout.js +2 -1
- package/dist/commands/pr.d.ts +22 -0
- package/dist/commands/pr.js +127 -0
- package/dist/commands/queue.d.ts +12 -0
- package/dist/commands/queue.js +160 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +115 -33
- package/dist/commands/review-client.d.ts +106 -0
- package/dist/commands/review-client.js +399 -0
- package/dist/commands/review.d.ts +36 -6
- package/dist/commands/review.js +395 -145
- package/dist/commands/settings.d.ts +17 -0
- package/dist/commands/settings.js +89 -0
- package/dist/commands/skill.d.ts +20 -0
- package/dist/commands/skill.js +145 -0
- package/dist/commands/stack.d.ts +36 -2
- package/dist/commands/stack.js +341 -113
- package/dist/commands/status.d.ts +18 -2
- package/dist/commands/status.js +236 -21
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.js +21 -7
- package/dist/errors.d.ts +18 -2
- package/dist/errors.js +22 -1
- package/dist/git-stack.d.ts +2 -0
- package/dist/git-stack.js +4 -0
- package/dist/git.d.ts +12 -1
- package/dist/git.js +52 -7
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +39 -28
- package/dist/stack-dto.d.ts +266 -0
- package/dist/stack-dto.js +7 -0
- package/dist/submit-pr-base.d.ts +31 -0
- package/dist/submit-pr-base.js +69 -0
- package/dist/ui/ansi.d.ts +2 -0
- package/dist/ui/ansi.js +2 -0
- package/dist/ui/banner.d.ts +34 -2
- package/dist/ui/banner.js +170 -79
- package/dist/ui/box.d.ts +13 -9
- package/dist/ui/box.js +28 -44
- package/dist/ui/findings.d.ts +4 -1
- package/dist/ui/findings.js +14 -7
- package/dist/ui/job-envelope.d.ts +68 -0
- package/dist/ui/job-envelope.js +35 -0
- package/dist/ui/line-tabs.d.ts +27 -0
- package/dist/ui/line-tabs.js +170 -0
- package/dist/ui/logo.d.ts +10 -2
- package/dist/ui/logo.js +13 -9
- package/dist/ui/present.d.ts +10 -0
- package/dist/ui/present.js +13 -0
- package/dist/ui/prompt.d.ts +148 -0
- package/dist/ui/prompt.js +433 -71
- package/dist/ui/select.js +1 -1
- package/dist/ui/tabs.d.ts +125 -0
- package/dist/ui/tabs.js +480 -0
- package/dist/ui/usage.d.ts +29 -1
- package/dist/ui/usage.js +71 -2
- package/dist/ui/width.d.ts +26 -0
- package/dist/ui/width.js +262 -0
- package/package.json +6 -1
- package/skill/SKILL.md +98 -0
- package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/ui/prompt.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
-
import { stdin
|
|
3
|
+
import { stdin, stdout } from "node:process";
|
|
4
4
|
import { ansi } from "./ansi.js";
|
|
5
|
-
import { frameWidth, preferBoxedUi, roundedBox, visibleWidth } from "./box.js";
|
|
5
|
+
import { frameWidth, preferBoxedUi, roundedBox, sliceVisible, terminalColumns, visibleWidth, } from "./box.js";
|
|
6
6
|
import { CTRL_C_EXIT_HINT, CtrlCExitGate } from "./ctrl-c-exit.js";
|
|
7
7
|
/** Thrown when the user asks to close the prompt (Ctrl+D empty, or Ctrl+C twice). */
|
|
8
8
|
export class PromptClosedError extends Error {
|
|
@@ -11,11 +11,297 @@ export class PromptClosedError extends Error {
|
|
|
11
11
|
this.name = "PromptClosedError";
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
const up = (n) =>
|
|
15
|
-
const down = (n) =>
|
|
16
|
-
const col = (n) =>
|
|
17
|
-
const clearDown = "
|
|
18
|
-
|
|
14
|
+
const up = (n) => `\u001b[${n}A`;
|
|
15
|
+
const down = (n) => `\u001b[${n}B`;
|
|
16
|
+
const col = (n) => `\u001b[${n}G`;
|
|
17
|
+
const clearDown = "\u001b[0J";
|
|
18
|
+
/**
|
|
19
|
+
* Redraw prefix from the parked content-row cursor.
|
|
20
|
+
* CUU keeps the column, so CR must come before ED 0 or leftover
|
|
21
|
+
* prompt cells survive (`merg` + `mergestorm` -> `mergmmmmmergestorm`).
|
|
22
|
+
*/
|
|
23
|
+
export function promptRedrawPrefix(inputRow) {
|
|
24
|
+
return `${up(inputRow)}\r${clearDown}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* From the content row, move to the first row after the input chrome
|
|
28
|
+
* and erase the slash overlay. Boxed chrome is top + content + bottom
|
|
29
|
+
* (`down(2)`). Plain chrome is the content line (`down(1)`).
|
|
30
|
+
*/
|
|
31
|
+
export function promptClearOverlay(boxed) {
|
|
32
|
+
return `${down(boxed ? 2 : 1)}\r${clearDown}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Cursor home + ED 0: clears the whole screen from row 1 and leaves the
|
|
36
|
+
* cursor there. Used to redraw the welcome panel on resize, where the old
|
|
37
|
+
* frame's row count is unknowable after the terminal rewraps.
|
|
38
|
+
*/
|
|
39
|
+
export const HEADER_REDRAW_PREFIX = `\u001b[H${clearDown}`;
|
|
40
|
+
export const DROPDOWN_MAX_COMMAND_ROWS = 8;
|
|
41
|
+
export function formatSlashLabel(spec) {
|
|
42
|
+
const extra = spec.aliases?.length
|
|
43
|
+
? ` \u00b7 ${spec.aliases.join(" \u00b7 ")}`
|
|
44
|
+
: "";
|
|
45
|
+
return `/${spec.name}${extra}`;
|
|
46
|
+
}
|
|
47
|
+
export function specMatchesTerm(spec, term) {
|
|
48
|
+
if (spec.name.toLowerCase().startsWith(term))
|
|
49
|
+
return true;
|
|
50
|
+
return (spec.aliases ?? []).some((alias) => alias.toLowerCase().startsWith(term));
|
|
51
|
+
}
|
|
52
|
+
export function slashToken(buffer) {
|
|
53
|
+
if (!buffer.startsWith("/"))
|
|
54
|
+
return null;
|
|
55
|
+
return buffer.slice(1).split(/\s/, 1)[0].toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
export function matchSlashCommands(buffer, specs, dismissed = false) {
|
|
58
|
+
if (dismissed)
|
|
59
|
+
return [];
|
|
60
|
+
const token = slashToken(buffer);
|
|
61
|
+
if (token == null)
|
|
62
|
+
return [];
|
|
63
|
+
return specs.filter((spec) => specMatchesTerm(spec, token));
|
|
64
|
+
}
|
|
65
|
+
export function isExactSlashCommand(buffer, specs) {
|
|
66
|
+
const token = slashToken(buffer);
|
|
67
|
+
if (!token)
|
|
68
|
+
return false;
|
|
69
|
+
return specs.some((spec) => spec.name === token || (spec.aliases ?? []).includes(token));
|
|
70
|
+
}
|
|
71
|
+
export function dropdownMaxRows(terminalRows, inputRow) {
|
|
72
|
+
const rows = typeof terminalRows === "number" && Number.isFinite(terminalRows) && terminalRows >= 1
|
|
73
|
+
? terminalRows
|
|
74
|
+
: 24;
|
|
75
|
+
return Math.max(1, Math.min(DROPDOWN_MAX_COMMAND_ROWS, rows - inputRow - 3));
|
|
76
|
+
}
|
|
77
|
+
export function dropdownViewport(count, index, maxRows) {
|
|
78
|
+
const max = Math.max(1, maxRows);
|
|
79
|
+
if (count <= max)
|
|
80
|
+
return { start: 0, moreAbove: 0, moreBelow: 0 };
|
|
81
|
+
const clampedIndex = Math.max(0, Math.min(index, count - 1));
|
|
82
|
+
let start = 0;
|
|
83
|
+
if (clampedIndex >= max)
|
|
84
|
+
start = clampedIndex - max + 1;
|
|
85
|
+
if (start + max > count)
|
|
86
|
+
start = Math.max(0, count - max);
|
|
87
|
+
return {
|
|
88
|
+
start,
|
|
89
|
+
moreAbove: start,
|
|
90
|
+
moreBelow: Math.max(0, count - start - max),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Overlay rows (no invert). At most `maxRows` commands plus edge markers. */
|
|
94
|
+
/** Prompt label + input chrome + footer hint. */
|
|
95
|
+
export function promptChromeRows(boxed, hasPrompt, hasHint = true) {
|
|
96
|
+
return (hasPrompt ? 1 : 0) + (boxed ? 3 : 1) + (hasHint ? 1 : 0);
|
|
97
|
+
}
|
|
98
|
+
function resolvedRows(terminalRows) {
|
|
99
|
+
return typeof terminalRows === "number" && Number.isFinite(terminalRows) && terminalRows >= 1
|
|
100
|
+
? terminalRows
|
|
101
|
+
: 24;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Rows available above the input bar for the slash list. Idle home
|
|
105
|
+
* passes 0 — do not pad a blank gap between header and prompt.
|
|
106
|
+
*/
|
|
107
|
+
export function overlayReserve(terminalRows, headerRows, boxed, hasPrompt) {
|
|
108
|
+
const available = resolvedRows(terminalRows) - Math.max(0, headerRows) - promptChromeRows(boxed, hasPrompt);
|
|
109
|
+
return Math.max(0, Math.min(DROPDOWN_MAX_COMMAND_ROWS, available));
|
|
110
|
+
}
|
|
111
|
+
/** Pad so the slash list sits just above the input bar (Claude-style). */
|
|
112
|
+
export function padOverlayRows(rows, reserve) {
|
|
113
|
+
const max = Math.max(0, reserve);
|
|
114
|
+
if (max === 0)
|
|
115
|
+
return [];
|
|
116
|
+
const visible = rows.slice(0, max);
|
|
117
|
+
const pad = max - visible.length;
|
|
118
|
+
return [...Array.from({ length: pad }, () => ({ text: " " })), ...visible];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Pick a header density and input chrome that fit the pane.
|
|
122
|
+
* Never drops the mark when a mini/nano header is provided — a short
|
|
123
|
+
* pane unboxes the input instead of painting an overflowing frame.
|
|
124
|
+
* `dockBottom` uses leftover rows so the prompt sits on the last lines.
|
|
125
|
+
*/
|
|
126
|
+
export function fitHomeFrame(params) {
|
|
127
|
+
const rows = resolvedRows(params.terminalRows);
|
|
128
|
+
const want = params.dropdownCount > 0
|
|
129
|
+
? Math.min(DROPDOWN_MAX_COMMAND_ROWS, Math.max(1, params.dropdownCount))
|
|
130
|
+
: 0;
|
|
131
|
+
const headers = [
|
|
132
|
+
params.fullHeader,
|
|
133
|
+
params.compactHeader,
|
|
134
|
+
params.miniHeader ?? [],
|
|
135
|
+
params.nanoHeader ?? [],
|
|
136
|
+
].filter((h, i, all) => h.length > 0 && all.findIndex((x) => x === h) === i);
|
|
137
|
+
const chromes = [];
|
|
138
|
+
if (params.boxed)
|
|
139
|
+
chromes.push({ boxed: true, hasPrompt: params.hasPrompt, hasHint: true });
|
|
140
|
+
chromes.push({ boxed: false, hasPrompt: params.hasPrompt, hasHint: true });
|
|
141
|
+
chromes.push({ boxed: false, hasPrompt: false, hasHint: true });
|
|
142
|
+
chromes.push({ boxed: false, hasPrompt: false, hasHint: false });
|
|
143
|
+
const tryFit = (header, chrome) => {
|
|
144
|
+
const chromeRows = promptChromeRows(chrome.boxed, chrome.hasPrompt, chrome.hasHint);
|
|
145
|
+
const avail = rows - header.length - chromeRows;
|
|
146
|
+
if (avail < 0)
|
|
147
|
+
return null;
|
|
148
|
+
if (params.dockBottom) {
|
|
149
|
+
return { header, reserve: avail, ...chrome };
|
|
150
|
+
}
|
|
151
|
+
if (want === 0) {
|
|
152
|
+
return { header, reserve: 0, ...chrome };
|
|
153
|
+
}
|
|
154
|
+
const maxList = Math.max(0, Math.min(DROPDOWN_MAX_COMMAND_ROWS, avail));
|
|
155
|
+
if (maxList < 1)
|
|
156
|
+
return null;
|
|
157
|
+
return { header, reserve: Math.min(want, maxList), ...chrome };
|
|
158
|
+
};
|
|
159
|
+
// Prefer keeping the input boxed: try every header density at the
|
|
160
|
+
// current chrome before unboxing. Otherwise a 12-row pane would pick
|
|
161
|
+
// the full banner + a plain prompt over compact + boxed.
|
|
162
|
+
for (const chrome of chromes) {
|
|
163
|
+
for (const header of headers) {
|
|
164
|
+
const hit = tryFit(header, chrome);
|
|
165
|
+
if (hit)
|
|
166
|
+
return hit;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const fallback = params.nanoHeader?.length
|
|
170
|
+
? params.nanoHeader
|
|
171
|
+
: params.miniHeader?.length
|
|
172
|
+
? params.miniHeader
|
|
173
|
+
: [];
|
|
174
|
+
return {
|
|
175
|
+
header: fallback,
|
|
176
|
+
reserve: params.dockBottom ? Math.max(0, rows - fallback.length - 1) : 0,
|
|
177
|
+
boxed: false,
|
|
178
|
+
hasPrompt: false,
|
|
179
|
+
hasHint: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function dropdownOverlayRows(dropdown, index, maxRows) {
|
|
183
|
+
const max = Math.max(1, maxRows);
|
|
184
|
+
if (dropdown.length <= max) {
|
|
185
|
+
return dropdown.map((spec, i) => ({
|
|
186
|
+
text: ` ${formatSlashLabel(spec).padEnd(22)}${spec.summary}`,
|
|
187
|
+
selected: i === index,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
const cmdSlots = Math.max(1, max - 2);
|
|
191
|
+
const view = dropdownViewport(dropdown.length, index, cmdSlots);
|
|
192
|
+
const rows = [];
|
|
193
|
+
if (view.moreAbove > 0)
|
|
194
|
+
rows.push({ text: ` \u2191 ${view.moreAbove} more` });
|
|
195
|
+
const end = Math.min(dropdown.length, view.start + cmdSlots);
|
|
196
|
+
for (let i = view.start; i < end; i++) {
|
|
197
|
+
const spec = dropdown[i];
|
|
198
|
+
rows.push({
|
|
199
|
+
text: ` ${formatSlashLabel(spec).padEnd(22)}${spec.summary}`,
|
|
200
|
+
selected: i === index,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (view.moreBelow > 0)
|
|
204
|
+
rows.push({ text: ` \u2193 ${view.moreBelow} more` });
|
|
205
|
+
return rows.slice(0, max);
|
|
206
|
+
}
|
|
207
|
+
/** Input prefix glyph (U+203A), kept as an escape so editors can't mangle it. */
|
|
208
|
+
export const INPUT_PREFIX_GLYPH = "\u203a";
|
|
209
|
+
/** Cells the `> ` prefix occupies before the buffer. */
|
|
210
|
+
export const INPUT_PREFIX_CELLS = 2;
|
|
211
|
+
/** Padding inside the boxed input frame. */
|
|
212
|
+
export const INPUT_BOX_PADDING = 1;
|
|
213
|
+
/**
|
|
214
|
+
* Horizontal scroll for a one-row editor: pick the `cells`-wide window of
|
|
215
|
+
* `buffer` that keeps the cursor on screen, moving the previous window only
|
|
216
|
+
* when the cursor leaves it. Never wraps onto a second physical row.
|
|
217
|
+
*/
|
|
218
|
+
export function inputWindow(buffer, cursor, cells, prevStart = 0) {
|
|
219
|
+
if (cells <= 0)
|
|
220
|
+
return { start: 0, text: "", cursorCol: 0 };
|
|
221
|
+
const cursorCells = visibleWidth(buffer.slice(0, Math.max(0, cursor)));
|
|
222
|
+
const totalCells = visibleWidth(buffer);
|
|
223
|
+
// The cursor needs a cell of its own at end-of-line, so a full window
|
|
224
|
+
// shows at most `cells - 1` glyphs past the scroll origin.
|
|
225
|
+
const maxStart = Math.max(0, totalCells - (cells - 1));
|
|
226
|
+
let start = Math.min(Math.max(0, prevStart), maxStart);
|
|
227
|
+
if (cursorCells < start)
|
|
228
|
+
start = cursorCells;
|
|
229
|
+
if (cursorCells > start + cells - 1)
|
|
230
|
+
start = cursorCells - (cells - 1);
|
|
231
|
+
return {
|
|
232
|
+
start,
|
|
233
|
+
text: sliceVisible(buffer, start, cells),
|
|
234
|
+
cursorCol: cursorCells - start,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** Cells available to the buffer (after the prefix) on a content row. */
|
|
238
|
+
export function inputCells(columns, boxedOverride) {
|
|
239
|
+
const boxed = boxedOverride ?? preferBoxedUi(columns);
|
|
240
|
+
const contentCells = boxed
|
|
241
|
+
? frameWidth(80, columns) - 2 - INPUT_BOX_PADDING * 2
|
|
242
|
+
: terminalColumns(80, columns) - 1;
|
|
243
|
+
return { boxed, cells: Math.max(0, contentCells - INPUT_PREFIX_CELLS) };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Everything one prompt redraw writes, computed from the current terminal
|
|
247
|
+
* width. Every row fits in `columns - 1` cells so nothing wraps and the
|
|
248
|
+
* CUU count on the next redraw stays honest.
|
|
249
|
+
*/
|
|
250
|
+
export function buildPromptFrame(params) {
|
|
251
|
+
const { boxed, cells } = inputCells(params.columns, params.boxed);
|
|
252
|
+
const cols = terminalColumns(80, params.columns);
|
|
253
|
+
const win = inputWindow(params.buffer, params.cursor, cells, params.scrollStart ?? 0);
|
|
254
|
+
const header = params.header ?? [];
|
|
255
|
+
const reserve = params.overlayReserve;
|
|
256
|
+
const overlay = params.overlay ?? [];
|
|
257
|
+
const fitted = reserve != null ? padOverlayRows(overlay, reserve) : overlay;
|
|
258
|
+
const inputRow = header.length +
|
|
259
|
+
(reserve ?? 0) +
|
|
260
|
+
(params.prompt ? 1 : 0) +
|
|
261
|
+
(boxed ? 1 : 0);
|
|
262
|
+
// Nothing we emit may wrap: a wrapped row is one the CUU count can't see.
|
|
263
|
+
const clip = (text) => visibleWidth(text) > cols - 1 ? sliceVisible(text, 0, cols - 1) : text;
|
|
264
|
+
const paintOverlay = (row) => {
|
|
265
|
+
const text = clip(row.text);
|
|
266
|
+
return row.selected ? ansi.invert(text) : ansi.gray(text);
|
|
267
|
+
};
|
|
268
|
+
const rows = [];
|
|
269
|
+
if (reserve != null) {
|
|
270
|
+
for (const line of header)
|
|
271
|
+
rows.push(clip(line));
|
|
272
|
+
for (const row of fitted)
|
|
273
|
+
rows.push(paintOverlay(row));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
for (const line of header)
|
|
277
|
+
rows.push(clip(line));
|
|
278
|
+
}
|
|
279
|
+
if (params.prompt)
|
|
280
|
+
rows.push(clip(params.prompt));
|
|
281
|
+
const inputLine = `${ansi.brightGreen(INPUT_PREFIX_GLYPH)} ${win.text}`;
|
|
282
|
+
if (boxed) {
|
|
283
|
+
// Full terminal width input frame, not content-sized.
|
|
284
|
+
rows.push(...roundedBox([inputLine], {
|
|
285
|
+
padding: INPUT_BOX_PADDING,
|
|
286
|
+
width: frameWidth(80, params.columns),
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Plain prompt on narrow TTYs (avoids wrapped borders).
|
|
291
|
+
rows.push(inputLine);
|
|
292
|
+
}
|
|
293
|
+
if (reserve == null) {
|
|
294
|
+
for (const row of overlay)
|
|
295
|
+
rows.push(paintOverlay(row));
|
|
296
|
+
}
|
|
297
|
+
if (params.hint != null) {
|
|
298
|
+
rows.push(clip(params.hint === "" ? " " : params.hint));
|
|
299
|
+
}
|
|
300
|
+
// Boxed: border col + padding; plain: column 1.
|
|
301
|
+
const targetCol = (boxed ? 1 + INPUT_BOX_PADDING : 0) + INPUT_PREFIX_CELLS + win.cursorCol + 1;
|
|
302
|
+
return { rows, boxed, inputRow, targetCol, scrollStart: win.start };
|
|
303
|
+
}
|
|
304
|
+
async function fallbackAskLine(promptLabel, input, output) {
|
|
19
305
|
const rl = createInterface({ input, output, terminal: false });
|
|
20
306
|
try {
|
|
21
307
|
return await rl.question(promptLabel ? `${promptLabel}> ` : "> ");
|
|
@@ -31,68 +317,133 @@ async function fallbackAskLine(promptLabel) {
|
|
|
31
317
|
export async function askLine(opts) {
|
|
32
318
|
const commands = opts.commands ?? [];
|
|
33
319
|
const history = opts.history ?? [];
|
|
320
|
+
const input = opts.input ?? stdin;
|
|
321
|
+
const output = opts.output ?? stdout;
|
|
34
322
|
if (!input.isTTY || !output.isTTY) {
|
|
35
|
-
return fallbackAskLine(opts.prompt);
|
|
323
|
+
return fallbackAskLine(opts.prompt, input, output);
|
|
36
324
|
}
|
|
37
325
|
return new Promise((resolve, reject) => {
|
|
38
326
|
let buffer = "";
|
|
39
327
|
let cursor = 0;
|
|
40
328
|
let historyIndex = history.length;
|
|
41
329
|
let dropdownIndex = 0;
|
|
330
|
+
let dropdownDismissed = false;
|
|
42
331
|
let linesDrawn = 0;
|
|
332
|
+
// Geometry of the frame currently on screen. The redraw prefix and the
|
|
333
|
+
// final overlay clear must use *these*, not a fresh measurement, because
|
|
334
|
+
// the cursor is parked relative to what was actually drawn.
|
|
335
|
+
let drawnInputRow = 0;
|
|
336
|
+
let scrollStart = 0;
|
|
43
337
|
let finished = false;
|
|
338
|
+
let exitHint = false;
|
|
44
339
|
const ctrlCExit = new CtrlCExitGate();
|
|
45
340
|
const wasRaw = Boolean(input.isRaw);
|
|
46
341
|
readline.emitKeypressEvents(input);
|
|
47
342
|
input.setRawMode(true);
|
|
48
343
|
input.resume();
|
|
49
|
-
const boxed = preferBoxedUi();
|
|
50
|
-
// Row (0-based) of the "› buffer" content line among the rows we draw each frame.
|
|
51
|
-
// Boxed: optional prompt + top border + content. Plain: optional prompt + content.
|
|
52
|
-
const inputRow = (opts.prompt ? 1 : 0) + (boxed ? 1 : 0);
|
|
53
344
|
function currentDropdown() {
|
|
54
|
-
|
|
55
|
-
return [];
|
|
56
|
-
const term = buffer.slice(1).toLowerCase();
|
|
57
|
-
return commands.filter((c) => c.name.toLowerCase().startsWith(term));
|
|
345
|
+
return matchSlashCommands(buffer, commands, dropdownDismissed);
|
|
58
346
|
}
|
|
59
|
-
function
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
347
|
+
function setBuffer(next) {
|
|
348
|
+
if (next !== buffer)
|
|
349
|
+
dropdownDismissed = false;
|
|
350
|
+
buffer = next;
|
|
351
|
+
}
|
|
352
|
+
let lastRoom = 24;
|
|
353
|
+
function paint() {
|
|
354
|
+
const columns = opts.columns ?? output.columns;
|
|
355
|
+
const boxedNow = preferBoxedUi(columns);
|
|
356
|
+
const rawRoom = opts.rows ?? output.rows;
|
|
357
|
+
const room = typeof rawRoom === "number" && Number.isFinite(rawRoom) && rawRoom >= 1
|
|
358
|
+
? rawRoom
|
|
359
|
+
: lastRoom;
|
|
360
|
+
lastRoom = room;
|
|
361
|
+
const fullHeader = opts.header ? opts.header(columns, false) : [];
|
|
362
|
+
const compactHeader = opts.header ? opts.header(columns, true) : [];
|
|
363
|
+
const miniHeader = opts.header ? opts.header(columns, "mini") : [];
|
|
364
|
+
const nanoHeader = opts.header ? opts.header(columns, "nano") : [];
|
|
75
365
|
const dropdown = currentDropdown();
|
|
76
366
|
if (dropdown.length) {
|
|
77
367
|
if (dropdownIndex >= dropdown.length)
|
|
78
368
|
dropdownIndex = dropdown.length - 1;
|
|
79
|
-
dropdown.forEach((c, i) => {
|
|
80
|
-
const label = ` /${c.name.padEnd(12)}${c.summary}`;
|
|
81
|
-
rows.push(i === dropdownIndex ? ansi.invert(label) : ansi.gray(label));
|
|
82
|
-
});
|
|
83
369
|
}
|
|
84
370
|
else {
|
|
85
371
|
dropdownIndex = 0;
|
|
86
372
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
373
|
+
const roomRows = resolvedRows(room);
|
|
374
|
+
const fit = opts.header
|
|
375
|
+
? fitHomeFrame({
|
|
376
|
+
fullHeader,
|
|
377
|
+
compactHeader,
|
|
378
|
+
miniHeader,
|
|
379
|
+
nanoHeader,
|
|
380
|
+
terminalRows: room,
|
|
381
|
+
boxed: boxedNow,
|
|
382
|
+
hasPrompt: Boolean(opts.prompt),
|
|
383
|
+
dropdownCount: dropdown.length,
|
|
384
|
+
dockBottom: true,
|
|
385
|
+
})
|
|
386
|
+
: {
|
|
387
|
+
header: [],
|
|
388
|
+
reserve: overlayReserve(room, 0, boxedNow, Boolean(opts.prompt)),
|
|
389
|
+
boxed: boxedNow,
|
|
390
|
+
hasPrompt: Boolean(opts.prompt),
|
|
391
|
+
hasHint: true,
|
|
392
|
+
};
|
|
393
|
+
const overlay = dropdown.length
|
|
394
|
+
? dropdownOverlayRows(dropdown, dropdownIndex, Math.max(1, Math.min(DROPDOWN_MAX_COMMAND_ROWS, fit.reserve)))
|
|
395
|
+
: [];
|
|
396
|
+
const frame = buildPromptFrame({
|
|
397
|
+
prompt: fit.hasPrompt ? opts.prompt : undefined,
|
|
398
|
+
buffer,
|
|
399
|
+
cursor,
|
|
400
|
+
columns,
|
|
401
|
+
scrollStart,
|
|
402
|
+
overlay,
|
|
403
|
+
overlayReserve: opts.header || dropdown.length ? fit.reserve : undefined,
|
|
404
|
+
header: fit.header,
|
|
405
|
+
hint: fit.hasHint ? (exitHint ? ansi.dim(CTRL_C_EXIT_HINT) : " ") : undefined,
|
|
406
|
+
boxed: fit.boxed,
|
|
407
|
+
});
|
|
408
|
+
// Never write more rows than the pane — that scrolls the welcome
|
|
409
|
+
// tail (tips / what's new) into view and looks like a broken frame.
|
|
410
|
+
if (frame.rows.length > roomRows) {
|
|
411
|
+
const keep = frame.rows.slice(frame.rows.length - roomRows);
|
|
412
|
+
const shift = frame.rows.length - keep.length;
|
|
413
|
+
frame.rows = keep;
|
|
414
|
+
frame.inputRow = Math.max(0, frame.inputRow - shift);
|
|
415
|
+
}
|
|
416
|
+
// Home screen owns the pane: always origin + ED 0. Relative CUU after
|
|
417
|
+
// a split-drag is what tore the previous layout (cursor home vs
|
|
418
|
+
// includeHeader flipping mid-resize).
|
|
419
|
+
if (opts.header) {
|
|
420
|
+
output.write(HEADER_REDRAW_PREFIX);
|
|
421
|
+
}
|
|
422
|
+
else if (linesDrawn > 0) {
|
|
423
|
+
output.write(promptRedrawPrefix(drawnInputRow));
|
|
424
|
+
}
|
|
425
|
+
// A full-pane write plus a trailing newline scrolls row 1 into
|
|
426
|
+
// scrollback — that was the missing top border. Stop on the last
|
|
427
|
+
// cell of the last row and CUU from there.
|
|
428
|
+
const fillsPane = Boolean(opts.header) && frame.rows.length >= roomRows;
|
|
429
|
+
output.write(frame.rows.join("\r\n") + (fillsPane ? "" : "\r\n"));
|
|
430
|
+
linesDrawn = frame.rows.length;
|
|
431
|
+
drawnInputRow = frame.inputRow;
|
|
432
|
+
scrollStart = frame.scrollStart;
|
|
433
|
+
const fromRow = fillsPane ? linesDrawn - 1 : linesDrawn;
|
|
434
|
+
output.write(`${up(Math.max(0, fromRow - frame.inputRow))}${col(frame.targetCol)}`);
|
|
435
|
+
}
|
|
436
|
+
function render() {
|
|
437
|
+
paint();
|
|
438
|
+
}
|
|
439
|
+
function onResize() {
|
|
440
|
+
if (finished)
|
|
441
|
+
return;
|
|
442
|
+
paint();
|
|
93
443
|
}
|
|
94
444
|
function cleanup() {
|
|
95
445
|
input.removeListener("keypress", onKeypress);
|
|
446
|
+
output.removeListener("resize", onResize);
|
|
96
447
|
input.setRawMode(wasRaw);
|
|
97
448
|
// emitKeypressEvents leaves stdin flowing; pause it so an idle shell
|
|
98
449
|
// (no command running) doesn't keep the process alive after exit/EOF.
|
|
@@ -103,17 +454,26 @@ export async function askLine(opts) {
|
|
|
103
454
|
return;
|
|
104
455
|
finished = true;
|
|
105
456
|
cleanup();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
output.
|
|
457
|
+
// Home chrome already fills the pane. down() + newline scrolls that
|
|
458
|
+
// frame (cursor to the bottom, input jumps up) before /usage paints.
|
|
459
|
+
// Leave the idle screen in place; the next TUI / askLine homes over it.
|
|
460
|
+
// Bare prompts still need the newline so oneshot output starts below.
|
|
461
|
+
// Leaving the shell: one newline so the OS prompt is not on the box.
|
|
462
|
+
if (!opts.header || err instanceof PromptClosedError) {
|
|
463
|
+
if (linesDrawn > 0) {
|
|
464
|
+
const below = linesDrawn - drawnInputRow - 1;
|
|
465
|
+
if (below > 0)
|
|
466
|
+
output.write(down(below));
|
|
467
|
+
}
|
|
468
|
+
output.write("\n");
|
|
469
|
+
}
|
|
110
470
|
if (err)
|
|
111
471
|
reject(err);
|
|
112
472
|
else
|
|
113
473
|
resolve(value ?? "");
|
|
114
474
|
}
|
|
115
475
|
function acceptDropdown(dropdown) {
|
|
116
|
-
|
|
476
|
+
setBuffer(`/${dropdown[dropdownIndex].name} `);
|
|
117
477
|
cursor = buffer.length;
|
|
118
478
|
dropdownIndex = 0;
|
|
119
479
|
}
|
|
@@ -121,22 +481,17 @@ export async function askLine(opts) {
|
|
|
121
481
|
try {
|
|
122
482
|
if (finished || !key)
|
|
123
483
|
return;
|
|
124
|
-
if (!(key.ctrl && key.name === "c"))
|
|
484
|
+
if (!(key.ctrl && key.name === "c")) {
|
|
125
485
|
ctrlCExit.reset();
|
|
486
|
+
exitHint = false;
|
|
487
|
+
}
|
|
126
488
|
const dropdown = currentDropdown();
|
|
127
489
|
if (key.ctrl && key.name === "c") {
|
|
128
490
|
if (ctrlCExit.press()) {
|
|
129
491
|
finish(null, new PromptClosedError());
|
|
130
492
|
return;
|
|
131
493
|
}
|
|
132
|
-
|
|
133
|
-
cursor = 0;
|
|
134
|
-
historyIndex = history.length;
|
|
135
|
-
const rowsBelow = Math.max(0, linesDrawn - inputRow - 1);
|
|
136
|
-
if (rowsBelow > 0)
|
|
137
|
-
output.write(down(rowsBelow));
|
|
138
|
-
output.write(`\n${ansi.dim(CTRL_C_EXIT_HINT)}\n`);
|
|
139
|
-
linesDrawn = 0;
|
|
494
|
+
exitHint = true;
|
|
140
495
|
render();
|
|
141
496
|
return;
|
|
142
497
|
}
|
|
@@ -145,9 +500,18 @@ export async function askLine(opts) {
|
|
|
145
500
|
finish(null, new PromptClosedError());
|
|
146
501
|
return;
|
|
147
502
|
}
|
|
148
|
-
if (key.name === "
|
|
503
|
+
if (key.name === "escape") {
|
|
149
504
|
if (dropdown.length)
|
|
150
|
-
|
|
505
|
+
dropdownDismissed = true;
|
|
506
|
+
render();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (key.name === "return") {
|
|
510
|
+
if (dropdown.length &&
|
|
511
|
+
!dropdownDismissed &&
|
|
512
|
+
!isExactSlashCommand(buffer, dropdown)) {
|
|
513
|
+
setBuffer(`/${dropdown[dropdownIndex].name}`);
|
|
514
|
+
}
|
|
151
515
|
finish(buffer);
|
|
152
516
|
return;
|
|
153
517
|
}
|
|
@@ -158,10 +522,7 @@ export async function askLine(opts) {
|
|
|
158
522
|
return;
|
|
159
523
|
}
|
|
160
524
|
if (key.name === "right") {
|
|
161
|
-
|
|
162
|
-
acceptDropdown(dropdown);
|
|
163
|
-
else
|
|
164
|
-
cursor = Math.min(buffer.length, cursor + 1);
|
|
525
|
+
cursor = Math.min(buffer.length, cursor + 1);
|
|
165
526
|
render();
|
|
166
527
|
return;
|
|
167
528
|
}
|
|
@@ -172,11 +533,11 @@ export async function askLine(opts) {
|
|
|
172
533
|
}
|
|
173
534
|
if (key.name === "up") {
|
|
174
535
|
if (dropdown.length) {
|
|
175
|
-
dropdownIndex = (dropdownIndex - 1
|
|
536
|
+
dropdownIndex = Math.max(0, dropdownIndex - 1);
|
|
176
537
|
}
|
|
177
538
|
else if (history.length) {
|
|
178
539
|
historyIndex = Math.max(0, historyIndex - 1);
|
|
179
|
-
|
|
540
|
+
setBuffer(history[historyIndex] ?? "");
|
|
180
541
|
cursor = buffer.length;
|
|
181
542
|
}
|
|
182
543
|
render();
|
|
@@ -184,11 +545,11 @@ export async function askLine(opts) {
|
|
|
184
545
|
}
|
|
185
546
|
if (key.name === "down") {
|
|
186
547
|
if (dropdown.length) {
|
|
187
|
-
dropdownIndex = (dropdownIndex + 1)
|
|
548
|
+
dropdownIndex = Math.min(dropdown.length - 1, dropdownIndex + 1);
|
|
188
549
|
}
|
|
189
550
|
else if (history.length) {
|
|
190
551
|
historyIndex = Math.min(history.length, historyIndex + 1);
|
|
191
|
-
|
|
552
|
+
setBuffer(historyIndex >= history.length ? "" : history[historyIndex] ?? "");
|
|
192
553
|
cursor = buffer.length;
|
|
193
554
|
}
|
|
194
555
|
render();
|
|
@@ -205,31 +566,31 @@ export async function askLine(opts) {
|
|
|
205
566
|
return;
|
|
206
567
|
}
|
|
207
568
|
if (key.ctrl && key.name === "u") {
|
|
208
|
-
buffer
|
|
569
|
+
setBuffer(buffer.slice(cursor));
|
|
209
570
|
cursor = 0;
|
|
210
571
|
render();
|
|
211
572
|
return;
|
|
212
573
|
}
|
|
213
574
|
if (key.ctrl && key.name === "k") {
|
|
214
|
-
buffer
|
|
575
|
+
setBuffer(buffer.slice(0, cursor));
|
|
215
576
|
render();
|
|
216
577
|
return;
|
|
217
578
|
}
|
|
218
579
|
if (key.name === "backspace") {
|
|
219
580
|
if (cursor > 0) {
|
|
220
|
-
buffer
|
|
581
|
+
setBuffer(buffer.slice(0, cursor - 1) + buffer.slice(cursor));
|
|
221
582
|
cursor -= 1;
|
|
222
583
|
}
|
|
223
584
|
render();
|
|
224
585
|
return;
|
|
225
586
|
}
|
|
226
587
|
if (key.name === "delete") {
|
|
227
|
-
buffer
|
|
588
|
+
setBuffer(buffer.slice(0, cursor) + buffer.slice(cursor + 1));
|
|
228
589
|
render();
|
|
229
590
|
return;
|
|
230
591
|
}
|
|
231
592
|
if (str && !key.ctrl && !key.meta) {
|
|
232
|
-
buffer
|
|
593
|
+
setBuffer(buffer.slice(0, cursor) + str + buffer.slice(cursor));
|
|
233
594
|
cursor += str.length;
|
|
234
595
|
render();
|
|
235
596
|
}
|
|
@@ -239,6 +600,7 @@ export async function askLine(opts) {
|
|
|
239
600
|
}
|
|
240
601
|
}
|
|
241
602
|
input.on("keypress", onKeypress);
|
|
603
|
+
output.on("resize", onResize);
|
|
242
604
|
render();
|
|
243
605
|
});
|
|
244
606
|
}
|
package/dist/ui/select.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function selectFromList(opts) {
|
|
|
38
38
|
rows.push(ansi.bold(opts.title));
|
|
39
39
|
items.forEach((item, i) => {
|
|
40
40
|
const body = opts.render(item, i);
|
|
41
|
-
const marker = i === index ? ansi.
|
|
41
|
+
const marker = i === index ? ansi.brightGreen("›") : " ";
|
|
42
42
|
const line = ` ${marker} ${body}`;
|
|
43
43
|
rows.push(i === index ? ansi.invert(line) : line);
|
|
44
44
|
});
|