mergestorm 0.3.13 → 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 +14 -15
- package/dist/api.d.ts +82 -5
- package/dist/api.js +166 -7
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- 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 +7 -10
- 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 +4 -3
- 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.js +93 -18
- package/dist/commands/review-client.d.ts +13 -0
- package/dist/commands/review-client.js +110 -0
- package/dist/commands/review.d.ts +6 -0
- package/dist/commands/review.js +18 -7
- 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 +2 -0
- package/dist/commands/stack.js +91 -57
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +30 -15
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.js +21 -7
- package/dist/errors.d.ts +1 -1
- package/dist/shell.js +39 -31
- package/dist/stack-dto.d.ts +82 -1
- 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 +163 -79
- package/dist/ui/findings.d.ts +2 -1
- package/dist/ui/findings.js +13 -7
- 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 +58 -1
- package/dist/ui/prompt.js +225 -42
- 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 +4 -0
- package/dist/ui/usage.js +22 -2
- package/package.json +2 -1
- package/skill/SKILL.md +98 -0
- package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/ui/prompt.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface AskLineOptions {
|
|
|
17
17
|
/** Streams (defaults: process.stdin / process.stdout). Injectable for tests. */
|
|
18
18
|
input?: NodeJS.ReadStream;
|
|
19
19
|
output?: NodeJS.WriteStream;
|
|
20
|
+
/**
|
|
21
|
+
* Welcome-panel rows sitting directly above this prompt, reflowed per
|
|
22
|
+
* width. Painted on every frame from the pane origin (`ESC[H`) so a
|
|
23
|
+
* resize cannot desync CUU counts. Pass `true` for compact, `"mini"` /
|
|
24
|
+
* `"nano"` for a short pane that still shows the mark.
|
|
25
|
+
*/
|
|
26
|
+
header?: (columns?: number, variant?: boolean | "mini" | "nano") => string[];
|
|
20
27
|
}
|
|
21
28
|
/** Thrown when the user asks to close the prompt (Ctrl+D empty, or Ctrl+C twice). */
|
|
22
29
|
export declare class PromptClosedError extends Error {
|
|
@@ -34,6 +41,12 @@ export declare function promptRedrawPrefix(inputRow: number): string;
|
|
|
34
41
|
* (`down(2)`). Plain chrome is the content line (`down(1)`).
|
|
35
42
|
*/
|
|
36
43
|
export declare function promptClearOverlay(boxed: boolean): string;
|
|
44
|
+
/**
|
|
45
|
+
* Cursor home + ED 0: clears the whole screen from row 1 and leaves the
|
|
46
|
+
* cursor there. Used to redraw the welcome panel on resize, where the old
|
|
47
|
+
* frame's row count is unknowable after the terminal rewraps.
|
|
48
|
+
*/
|
|
49
|
+
export declare const HEADER_REDRAW_PREFIX = "\u001B[H\u001B[0J";
|
|
37
50
|
export declare const DROPDOWN_MAX_COMMAND_ROWS = 8;
|
|
38
51
|
export declare function formatSlashLabel(spec: CommandSpec): string;
|
|
39
52
|
export declare function specMatchesTerm(spec: CommandSpec, term: string): boolean;
|
|
@@ -51,6 +64,39 @@ export type OverlayRow = {
|
|
|
51
64
|
selected?: boolean;
|
|
52
65
|
};
|
|
53
66
|
/** Overlay rows (no invert). At most `maxRows` commands plus edge markers. */
|
|
67
|
+
/** Prompt label + input chrome + footer hint. */
|
|
68
|
+
export declare function promptChromeRows(boxed: boolean, hasPrompt: boolean, hasHint?: boolean): number;
|
|
69
|
+
/**
|
|
70
|
+
* Rows available above the input bar for the slash list. Idle home
|
|
71
|
+
* passes 0 — do not pad a blank gap between header and prompt.
|
|
72
|
+
*/
|
|
73
|
+
export declare function overlayReserve(terminalRows: number | undefined, headerRows: number, boxed: boolean, hasPrompt: boolean): number;
|
|
74
|
+
/** Pad so the slash list sits just above the input bar (Claude-style). */
|
|
75
|
+
export declare function padOverlayRows(rows: OverlayRow[], reserve: number): OverlayRow[];
|
|
76
|
+
export interface HomeFrameFit {
|
|
77
|
+
header: string[];
|
|
78
|
+
reserve: number;
|
|
79
|
+
boxed: boolean;
|
|
80
|
+
hasPrompt: boolean;
|
|
81
|
+
hasHint: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Pick a header density and input chrome that fit the pane.
|
|
85
|
+
* Never drops the mark when a mini/nano header is provided — a short
|
|
86
|
+
* pane unboxes the input instead of painting an overflowing frame.
|
|
87
|
+
* `dockBottom` uses leftover rows so the prompt sits on the last lines.
|
|
88
|
+
*/
|
|
89
|
+
export declare function fitHomeFrame(params: {
|
|
90
|
+
fullHeader: string[];
|
|
91
|
+
compactHeader: string[];
|
|
92
|
+
miniHeader?: string[];
|
|
93
|
+
nanoHeader?: string[];
|
|
94
|
+
terminalRows: number | undefined;
|
|
95
|
+
boxed: boolean;
|
|
96
|
+
hasPrompt: boolean;
|
|
97
|
+
dropdownCount: number;
|
|
98
|
+
dockBottom?: boolean;
|
|
99
|
+
}): HomeFrameFit;
|
|
54
100
|
export declare function dropdownOverlayRows(dropdown: CommandSpec[], index: number, maxRows: number): OverlayRow[];
|
|
55
101
|
/** Input prefix glyph (U+203A), kept as an escape so editors can't mangle it. */
|
|
56
102
|
export declare const INPUT_PREFIX_GLYPH = "\u203A";
|
|
@@ -73,7 +119,7 @@ export interface InputWindow {
|
|
|
73
119
|
*/
|
|
74
120
|
export declare function inputWindow(buffer: string, cursor: number, cells: number, prevStart?: number): InputWindow;
|
|
75
121
|
/** Cells available to the buffer (after the prefix) on a content row. */
|
|
76
|
-
export declare function inputCells(columns: number | undefined): {
|
|
122
|
+
export declare function inputCells(columns: number | undefined, boxedOverride?: boolean): {
|
|
77
123
|
boxed: boolean;
|
|
78
124
|
cells: number;
|
|
79
125
|
};
|
|
@@ -86,6 +132,17 @@ export interface PromptFrameInput {
|
|
|
86
132
|
/** Previous scroll origin, so left/right only move the window at the edges. */
|
|
87
133
|
scrollStart?: number;
|
|
88
134
|
overlay?: OverlayRow[];
|
|
135
|
+
/**
|
|
136
|
+
* When set, the overlay is padded to this many rows and drawn **above**
|
|
137
|
+
* the input bar. Height stays constant so `/` cannot scroll the header.
|
|
138
|
+
*/
|
|
139
|
+
overlayReserve?: number;
|
|
140
|
+
/** Trailing footer row (empty or the Ctrl+C hint). */
|
|
141
|
+
hint?: string;
|
|
142
|
+
/** Welcome-panel rows painted with this frame (idle home screen). */
|
|
143
|
+
header?: string[];
|
|
144
|
+
/** Force boxed / plain input. Default follows {@link preferBoxedUi}. */
|
|
145
|
+
boxed?: boolean;
|
|
89
146
|
}
|
|
90
147
|
export interface PromptFrame {
|
|
91
148
|
rows: string[];
|
package/dist/ui/prompt.js
CHANGED
|
@@ -31,6 +31,12 @@ export function promptRedrawPrefix(inputRow) {
|
|
|
31
31
|
export function promptClearOverlay(boxed) {
|
|
32
32
|
return `${down(boxed ? 2 : 1)}\r${clearDown}`;
|
|
33
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}`;
|
|
34
40
|
export const DROPDOWN_MAX_COMMAND_ROWS = 8;
|
|
35
41
|
export function formatSlashLabel(spec) {
|
|
36
42
|
const extra = spec.aliases?.length
|
|
@@ -85,12 +91,108 @@ export function dropdownViewport(count, index, maxRows) {
|
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
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
|
+
}
|
|
88
182
|
export function dropdownOverlayRows(dropdown, index, maxRows) {
|
|
89
|
-
const
|
|
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);
|
|
90
192
|
const rows = [];
|
|
91
193
|
if (view.moreAbove > 0)
|
|
92
194
|
rows.push({ text: ` \u2191 ${view.moreAbove} more` });
|
|
93
|
-
const end = Math.min(dropdown.length, view.start +
|
|
195
|
+
const end = Math.min(dropdown.length, view.start + cmdSlots);
|
|
94
196
|
for (let i = view.start; i < end; i++) {
|
|
95
197
|
const spec = dropdown[i];
|
|
96
198
|
rows.push({
|
|
@@ -100,7 +202,7 @@ export function dropdownOverlayRows(dropdown, index, maxRows) {
|
|
|
100
202
|
}
|
|
101
203
|
if (view.moreBelow > 0)
|
|
102
204
|
rows.push({ text: ` \u2193 ${view.moreBelow} more` });
|
|
103
|
-
return rows;
|
|
205
|
+
return rows.slice(0, max);
|
|
104
206
|
}
|
|
105
207
|
/** Input prefix glyph (U+203A), kept as an escape so editors can't mangle it. */
|
|
106
208
|
export const INPUT_PREFIX_GLYPH = "\u203a";
|
|
@@ -133,8 +235,8 @@ export function inputWindow(buffer, cursor, cells, prevStart = 0) {
|
|
|
133
235
|
};
|
|
134
236
|
}
|
|
135
237
|
/** Cells available to the buffer (after the prefix) on a content row. */
|
|
136
|
-
export function inputCells(columns) {
|
|
137
|
-
const boxed = preferBoxedUi(columns);
|
|
238
|
+
export function inputCells(columns, boxedOverride) {
|
|
239
|
+
const boxed = boxedOverride ?? preferBoxedUi(columns);
|
|
138
240
|
const contentCells = boxed
|
|
139
241
|
? frameWidth(80, columns) - 2 - INPUT_BOX_PADDING * 2
|
|
140
242
|
: terminalColumns(80, columns) - 1;
|
|
@@ -146,16 +248,37 @@ export function inputCells(columns) {
|
|
|
146
248
|
* CUU count on the next redraw stays honest.
|
|
147
249
|
*/
|
|
148
250
|
export function buildPromptFrame(params) {
|
|
149
|
-
const { boxed, cells } = inputCells(params.columns);
|
|
251
|
+
const { boxed, cells } = inputCells(params.columns, params.boxed);
|
|
150
252
|
const cols = terminalColumns(80, params.columns);
|
|
151
253
|
const win = inputWindow(params.buffer, params.cursor, cells, params.scrollStart ?? 0);
|
|
152
|
-
const
|
|
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);
|
|
153
262
|
// Nothing we emit may wrap: a wrapped row is one the CUU count can't see.
|
|
154
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
|
+
};
|
|
155
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
|
+
}
|
|
156
279
|
if (params.prompt)
|
|
157
|
-
rows.push(
|
|
158
|
-
const inputLine = `${ansi.
|
|
280
|
+
rows.push(clip(params.prompt));
|
|
281
|
+
const inputLine = `${ansi.brightGreen(INPUT_PREFIX_GLYPH)} ${win.text}`;
|
|
159
282
|
if (boxed) {
|
|
160
283
|
// Full terminal width input frame, not content-sized.
|
|
161
284
|
rows.push(...roundedBox([inputLine], {
|
|
@@ -167,9 +290,12 @@ export function buildPromptFrame(params) {
|
|
|
167
290
|
// Plain prompt on narrow TTYs (avoids wrapped borders).
|
|
168
291
|
rows.push(inputLine);
|
|
169
292
|
}
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
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));
|
|
173
299
|
}
|
|
174
300
|
// Boxed: border col + padding; plain: column 1.
|
|
175
301
|
const targetCol = (boxed ? 1 + INPUT_BOX_PADDING : 0) + INPUT_PREFIX_CELLS + win.cursorCol + 1;
|
|
@@ -207,9 +333,9 @@ export async function askLine(opts) {
|
|
|
207
333
|
// final overlay clear must use *these*, not a fresh measurement, because
|
|
208
334
|
// the cursor is parked relative to what was actually drawn.
|
|
209
335
|
let drawnInputRow = 0;
|
|
210
|
-
let drawnBoxed = true;
|
|
211
336
|
let scrollStart = 0;
|
|
212
337
|
let finished = false;
|
|
338
|
+
let exitHint = false;
|
|
213
339
|
const ctrlCExit = new CtrlCExitGate();
|
|
214
340
|
const wasRaw = Boolean(input.isRaw);
|
|
215
341
|
readline.emitKeypressEvents(input);
|
|
@@ -223,44 +349,97 @@ export async function askLine(opts) {
|
|
|
223
349
|
dropdownDismissed = false;
|
|
224
350
|
buffer = next;
|
|
225
351
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
output.write(promptRedrawPrefix(drawnInputRow));
|
|
229
|
-
}
|
|
230
|
-
// Re-measured every frame: the terminal may have been resized since
|
|
231
|
-
// the last draw, flipping boxed/plain or changing the frame width.
|
|
352
|
+
let lastRoom = 24;
|
|
353
|
+
function paint() {
|
|
232
354
|
const columns = opts.columns ?? output.columns;
|
|
233
355
|
const boxedNow = preferBoxedUi(columns);
|
|
234
|
-
const
|
|
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") : [];
|
|
235
365
|
const dropdown = currentDropdown();
|
|
236
|
-
let overlay = [];
|
|
237
366
|
if (dropdown.length) {
|
|
238
367
|
if (dropdownIndex >= dropdown.length)
|
|
239
368
|
dropdownIndex = dropdown.length - 1;
|
|
240
|
-
const maxRows = dropdownMaxRows(opts.rows ?? output.rows, inputRowNow);
|
|
241
|
-
overlay = dropdownOverlayRows(dropdown, dropdownIndex, maxRows);
|
|
242
369
|
}
|
|
243
370
|
else {
|
|
244
371
|
dropdownIndex = 0;
|
|
245
372
|
}
|
|
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
|
+
: [];
|
|
246
396
|
const frame = buildPromptFrame({
|
|
247
|
-
prompt: opts.prompt,
|
|
397
|
+
prompt: fit.hasPrompt ? opts.prompt : undefined,
|
|
248
398
|
buffer,
|
|
249
399
|
cursor,
|
|
250
400
|
columns,
|
|
251
401
|
scrollStart,
|
|
252
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,
|
|
253
407
|
});
|
|
254
|
-
|
|
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"));
|
|
255
430
|
linesDrawn = frame.rows.length;
|
|
256
431
|
drawnInputRow = frame.inputRow;
|
|
257
|
-
drawnBoxed = frame.boxed;
|
|
258
432
|
scrollStart = frame.scrollStart;
|
|
259
|
-
|
|
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();
|
|
260
438
|
}
|
|
261
439
|
function onResize() {
|
|
262
|
-
if (
|
|
263
|
-
|
|
440
|
+
if (finished)
|
|
441
|
+
return;
|
|
442
|
+
paint();
|
|
264
443
|
}
|
|
265
444
|
function cleanup() {
|
|
266
445
|
input.removeListener("keypress", onKeypress);
|
|
@@ -275,9 +454,19 @@ export async function askLine(opts) {
|
|
|
275
454
|
return;
|
|
276
455
|
finished = true;
|
|
277
456
|
cleanup();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
}
|
|
281
470
|
if (err)
|
|
282
471
|
reject(err);
|
|
283
472
|
else
|
|
@@ -292,23 +481,17 @@ export async function askLine(opts) {
|
|
|
292
481
|
try {
|
|
293
482
|
if (finished || !key)
|
|
294
483
|
return;
|
|
295
|
-
if (!(key.ctrl && key.name === "c"))
|
|
484
|
+
if (!(key.ctrl && key.name === "c")) {
|
|
296
485
|
ctrlCExit.reset();
|
|
486
|
+
exitHint = false;
|
|
487
|
+
}
|
|
297
488
|
const dropdown = currentDropdown();
|
|
298
489
|
if (key.ctrl && key.name === "c") {
|
|
299
490
|
if (ctrlCExit.press()) {
|
|
300
491
|
finish(null, new PromptClosedError());
|
|
301
492
|
return;
|
|
302
493
|
}
|
|
303
|
-
|
|
304
|
-
cursor = 0;
|
|
305
|
-
scrollStart = 0;
|
|
306
|
-
historyIndex = history.length;
|
|
307
|
-
dropdownIndex = 0;
|
|
308
|
-
if (linesDrawn > 0)
|
|
309
|
-
output.write(promptClearOverlay(drawnBoxed));
|
|
310
|
-
output.write(`\n${ansi.dim(CTRL_C_EXIT_HINT)}\n`);
|
|
311
|
-
linesDrawn = 0;
|
|
494
|
+
exitHint = true;
|
|
312
495
|
render();
|
|
313
496
|
return;
|
|
314
497
|
}
|
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
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type UsagePanelInput } from "./usage.js";
|
|
2
|
+
export type TabId = "status" | "usage" | "jobs" | "config";
|
|
3
|
+
export declare const TAB_ORDER: readonly TabId[];
|
|
4
|
+
export type TabsJob = {
|
|
5
|
+
id: string;
|
|
6
|
+
cells: {
|
|
7
|
+
job: string;
|
|
8
|
+
status: string;
|
|
9
|
+
verdict: string;
|
|
10
|
+
thread: string;
|
|
11
|
+
when: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/** One Config-tab row: a writable on/off toggle or a read-only connection. */
|
|
15
|
+
export type TabsConfigRow = {
|
|
16
|
+
/** Settings key sent on PATCH (writable rows only). */
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
value: boolean;
|
|
20
|
+
writable: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type TabsData = {
|
|
23
|
+
/** Prebuilt whoami + credits lines for the Status tab. */
|
|
24
|
+
statusLines: string[];
|
|
25
|
+
/** Right-aligned chrome note (reachability). Lives on the hint row. */
|
|
26
|
+
footerNote?: string;
|
|
27
|
+
/** Usage panel input minus width concerns; null when the API degraded. */
|
|
28
|
+
usage: Omit<UsagePanelInput, "columns" | "color"> | null;
|
|
29
|
+
/** Usage-tab body when the panel is unavailable (overrides the default). */
|
|
30
|
+
usageEmptyText?: string;
|
|
31
|
+
jobs: TabsJob[];
|
|
32
|
+
/** Jobs-tab body when the list is empty (e.g. the list call failed). */
|
|
33
|
+
jobsEmptyText?: string;
|
|
34
|
+
/** Config tab rows; null when GET /api/v1/settings degraded. */
|
|
35
|
+
config: TabsConfigRow[] | null;
|
|
36
|
+
/** Config-tab body when settings are unavailable (overrides the default). */
|
|
37
|
+
configEmptyText?: string;
|
|
38
|
+
/** Transient Config-tab status line (PATCH in flight or rejected). */
|
|
39
|
+
configNote?: string;
|
|
40
|
+
color?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type TabsDetail = {
|
|
43
|
+
jobId: string;
|
|
44
|
+
rows: string[];
|
|
45
|
+
};
|
|
46
|
+
export type TabsState = {
|
|
47
|
+
tab: TabId;
|
|
48
|
+
/** Body scroll per scrollable view; jobs and config scroll by selection. */
|
|
49
|
+
scroll: {
|
|
50
|
+
status: number;
|
|
51
|
+
usage: number;
|
|
52
|
+
detail: number;
|
|
53
|
+
};
|
|
54
|
+
jobsIndex: number;
|
|
55
|
+
configIndex: number;
|
|
56
|
+
/** In-place `status <id>` view on the Jobs tab; Esc returns to the list. */
|
|
57
|
+
detail: TabsDetail | null;
|
|
58
|
+
};
|
|
59
|
+
export declare function initialTabsState(tab?: TabId): TabsState;
|
|
60
|
+
export type TabsKeyName = "left" | "right" | "up" | "down" | "return" | "space" | "escape" | "q";
|
|
61
|
+
export type TabsStep = {
|
|
62
|
+
kind: "state";
|
|
63
|
+
state: TabsState;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "close";
|
|
66
|
+
} | {
|
|
67
|
+
kind: "open-job";
|
|
68
|
+
index: number;
|
|
69
|
+
state: TabsState;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "toggle-setting";
|
|
72
|
+
key: string;
|
|
73
|
+
value: boolean;
|
|
74
|
+
state: TabsState;
|
|
75
|
+
};
|
|
76
|
+
export type TabsReduceCtx = {
|
|
77
|
+
jobsCount: number;
|
|
78
|
+
/** Max scroll offset of the active view (content rows minus body rows). */
|
|
79
|
+
maxScroll: number;
|
|
80
|
+
/** Config rows so Space knows which key to flip (read-only rows no-op). */
|
|
81
|
+
config?: readonly TabsConfigRow[];
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* One keypress against the browser state. Pure so tests can drive the whole
|
|
85
|
+
* key map without a TTY. Tabs clamp at the ends (no wrap-around, so "which
|
|
86
|
+
* tab am I on" never gets ambiguous); Esc backs out of a job detail before
|
|
87
|
+
* it closes the browser; `q` always closes.
|
|
88
|
+
*/
|
|
89
|
+
export declare function reduceTabsKey(state: TabsState, key: TabsKeyName, ctx: TabsReduceCtx): TabsStep;
|
|
90
|
+
/** Body rows available inside the frame: total rows minus chrome + hint. */
|
|
91
|
+
export declare function tabsBodyRows(rows: number | undefined): number;
|
|
92
|
+
export type TabsLayout = {
|
|
93
|
+
/** Full frame (border, body, hint); no line wider than `columns - 1`. */
|
|
94
|
+
lines: string[];
|
|
95
|
+
bodyRows: number;
|
|
96
|
+
/** Content rows of the active view before clipping (drives max scroll). */
|
|
97
|
+
contentRows: number;
|
|
98
|
+
};
|
|
99
|
+
export declare function tabsMaxScroll(layout: TabsLayout): number;
|
|
100
|
+
/** Key hint on the left, optional reachability note on the right. */
|
|
101
|
+
export declare function tabsHintLine(left: string, right: string | undefined, columns: number, color: boolean): string;
|
|
102
|
+
/**
|
|
103
|
+
* The whole browser frame for one render: tab strip in the top border, the
|
|
104
|
+
* active tab's body clipped to `rows - 4`, and a dim key hint. Pure; the
|
|
105
|
+
* raw-mode loop and tests both consume it.
|
|
106
|
+
*/
|
|
107
|
+
export declare function layoutTabs(state: TabsState, data: TabsData, columns?: number, rows?: number): TabsLayout;
|
|
108
|
+
export type TabsBrowserOptions = {
|
|
109
|
+
initial?: TabId;
|
|
110
|
+
data: TabsData;
|
|
111
|
+
/** Fetches `status <id>` rows for the Jobs tab's Enter action. */
|
|
112
|
+
loadJobDetail?: (jobId: string) => Promise<string[]>;
|
|
113
|
+
/** PATCHes one setting for the Config tab's Space action; resolves to the refreshed rows. */
|
|
114
|
+
toggleSetting?: (key: string, value: boolean) => Promise<TabsConfigRow[]>;
|
|
115
|
+
input?: NodeJS.ReadStream;
|
|
116
|
+
output?: NodeJS.WriteStream;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Raw-mode tabbed browser (Status / Usage / Jobs / Config) on the select.ts
|
|
120
|
+
* loop: Left/Right switch tabs, Up/Down scroll or select, Enter opens a job
|
|
121
|
+
* in place, Space toggles a Config setting, `q`/Esc close, resize re-renders.
|
|
122
|
+
* Non-TTY streams fall back to the static panels (callers normally gate on
|
|
123
|
+
* TTY already).
|
|
124
|
+
*/
|
|
125
|
+
export declare function runTabsBrowser(opts: TabsBrowserOptions): Promise<void>;
|