triflux 8.6.0 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hub/team/ansi.mjs +134 -11
- package/hub/team/tui.mjs +288 -53
- package/package.json +1 -1
package/hub/team/ansi.mjs
CHANGED
|
@@ -65,16 +65,92 @@ export function color(text, fg, bg) {
|
|
|
65
65
|
export function bold(text) { return `${BOLD}${text}${RESET}`; }
|
|
66
66
|
export function dim(text) { return `${DIM}${text}${RESET}`; }
|
|
67
67
|
|
|
68
|
+
export function lerpRgb(a, b, t) {
|
|
69
|
+
return {
|
|
70
|
+
r: Math.round(a.r + (b.r - a.r) * t),
|
|
71
|
+
g: Math.round(a.g + (b.g - a.g) * t),
|
|
72
|
+
b: Math.round(a.b + (b.b - a.b) * t),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function rgbSeq(rgb, mode = 38) {
|
|
77
|
+
return `${ESC}[${mode};2;${rgb.r};${rgb.g};${rgb.b}m`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function brightenRgb(rgb, amount = 0.3) {
|
|
81
|
+
return lerpRgb(rgb, { r: 255, g: 255, b: 255 }, amount);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseRgbSeq(seq) {
|
|
85
|
+
const match = typeof seq === "string"
|
|
86
|
+
? seq.match(/\x1b\[(?:38|48);2;(\d+);(\d+);(\d+)m/)
|
|
87
|
+
: null;
|
|
88
|
+
if (!match) return null;
|
|
89
|
+
return {
|
|
90
|
+
r: Number.parseInt(match[1], 10),
|
|
91
|
+
g: Number.parseInt(match[2], 10),
|
|
92
|
+
b: Number.parseInt(match[3], 10),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function reapplyBackground(text, bgSeq) {
|
|
97
|
+
if (!bgSeq) return text;
|
|
98
|
+
return `${bgSeq}${String(text).replaceAll(RESET, `${RESET}${bgSeq}`)}${RESET}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
// ── 박스 그리기 (유니코드 테두리) ──
|
|
69
102
|
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
70
103
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
function borderHighlightCell(width, totalRows, highlightPos) {
|
|
105
|
+
if (!Number.isFinite(highlightPos)) return null;
|
|
106
|
+
const perimeter = 2 * (width - 2) + 2 * totalRows;
|
|
107
|
+
if (perimeter <= 0) return null;
|
|
108
|
+
let pos = Math.floor(highlightPos) % perimeter;
|
|
109
|
+
if (pos < 0) pos += perimeter;
|
|
110
|
+
|
|
111
|
+
if (pos < width - 2) return { row: 0, col: pos + 1 };
|
|
112
|
+
pos -= width - 2;
|
|
113
|
+
if (pos < totalRows) return { row: pos, col: width - 1 };
|
|
114
|
+
pos -= totalRows;
|
|
115
|
+
if (pos < width - 2) return { row: totalRows - 1, col: width - 2 - pos };
|
|
116
|
+
pos -= width - 2;
|
|
117
|
+
return { row: totalRows - 1 - pos, col: 0 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderBorderChar(glyph, row, col, highlightCell, borderSeq, highlightSeq) {
|
|
121
|
+
if (highlightCell && highlightCell.row === row && highlightCell.col === col) {
|
|
122
|
+
return `${highlightSeq}${glyph}${RESET}`;
|
|
123
|
+
}
|
|
124
|
+
return borderSeq ? `${borderSeq}${glyph}${RESET}` : glyph;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function box(lines, width, borderColor = "", options = {}) {
|
|
128
|
+
const isFn = typeof borderColor === "function";
|
|
129
|
+
const totalRows = lines.length + 2;
|
|
130
|
+
const bc = isFn ? (row) => borderColor(row, totalRows) : () => borderColor;
|
|
131
|
+
const rst = (isFn || borderColor) ? RESET : "";
|
|
132
|
+
const highlightCell = borderHighlightCell(width, totalRows, options.highlightPos);
|
|
133
|
+
const highlightSeq = options.highlightColor
|
|
134
|
+
|| (() => {
|
|
135
|
+
const parsed = parseRgbSeq(typeof borderColor === "string" ? borderColor : "");
|
|
136
|
+
return parsed ? rgbSeq(brightenRgb(parsed, 0.45)) : `${BOLD}${FG.white}`;
|
|
137
|
+
})();
|
|
138
|
+
const topChars = [BOX.tl, ...Array.from({ length: width - 2 }, () => BOX.h), BOX.tr];
|
|
139
|
+
const botChars = [BOX.bl, ...Array.from({ length: width - 2 }, () => BOX.h), BOX.br];
|
|
140
|
+
const top = topChars
|
|
141
|
+
.map((glyph, col) => renderBorderChar(glyph, 0, col, highlightCell, bc(0), highlightSeq))
|
|
142
|
+
.join("");
|
|
143
|
+
const bot = botChars
|
|
144
|
+
.map((glyph, col) => renderBorderChar(glyph, totalRows - 1, col, highlightCell, bc(totalRows - 1), highlightSeq))
|
|
145
|
+
.join("");
|
|
146
|
+
const mid = `${bc(Math.floor(totalRows / 2))}${BOX.ml}${BOX.h.repeat(width - 2)}${BOX.mr}${rst}`;
|
|
147
|
+
const body = lines.map((l, i) => {
|
|
148
|
+
const row = i + 1;
|
|
149
|
+
const content = options.titleFlashBg && i === 0
|
|
150
|
+
? reapplyBackground(padRight(l, width - 4), options.titleFlashBg)
|
|
151
|
+
: padRight(l, width - 4);
|
|
152
|
+
return `${renderBorderChar(BOX.v, row, 0, highlightCell, bc(row), highlightSeq)} ${content} ${renderBorderChar(BOX.v, row, width - 1, highlightCell, bc(row), highlightSeq)}`;
|
|
153
|
+
});
|
|
78
154
|
return { top, body, bot, mid };
|
|
79
155
|
}
|
|
80
156
|
|
|
@@ -205,6 +281,12 @@ export const MOCHA = {
|
|
|
205
281
|
surface0: `${ESC}[38;2;49;50;68m`, // #313244 surface0
|
|
206
282
|
};
|
|
207
283
|
|
|
284
|
+
const MOCHA_RGB = {
|
|
285
|
+
ok: { r: 166, g: 227, b: 161 },
|
|
286
|
+
partial: { r: 250, g: 179, b: 135 },
|
|
287
|
+
fail: { r: 243, g: 139, b: 168 },
|
|
288
|
+
};
|
|
289
|
+
|
|
208
290
|
// ── badge 헬퍼 ──
|
|
209
291
|
// statusBadge(status) → ANSI 색상 문자열
|
|
210
292
|
export function statusBadge(status) {
|
|
@@ -231,13 +313,48 @@ export function statusBadge(status) {
|
|
|
231
313
|
}
|
|
232
314
|
|
|
233
315
|
// ── 진행률 바 ──
|
|
234
|
-
// progressBar(percent, width) — percent: 0~100,
|
|
235
|
-
export function progressBar(percent, width = 20) {
|
|
316
|
+
// progressBar(percent, width, time) — percent: 0~100, time 전달 시 shimmer sweep
|
|
317
|
+
export function progressBar(percent, width = 20, time) {
|
|
318
|
+
const ratio = Math.max(0, Math.min(100, percent)) / 100;
|
|
319
|
+
const filled = Math.round(ratio * width);
|
|
320
|
+
const empty = width - filled;
|
|
321
|
+
const fillRgb = percent >= 100 ? MOCHA_RGB.ok : percent >= 50 ? MOCHA_RGB.partial : MOCHA_RGB.fail;
|
|
322
|
+
const fillColor = rgbSeq(fillRgb);
|
|
323
|
+
let fillText = "█".repeat(filled);
|
|
324
|
+
|
|
325
|
+
if (filled > 0 && Number.isFinite(time)) {
|
|
326
|
+
const shinePos = Math.min(
|
|
327
|
+
filled - 1,
|
|
328
|
+
Math.floor(((((time % 2000) + 2000) % 2000) / 2000) * filled),
|
|
329
|
+
);
|
|
330
|
+
const shineColor = rgbSeq(brightenRgb(fillRgb, 0.3));
|
|
331
|
+
fillText = Array.from({ length: filled }, (_, idx) =>
|
|
332
|
+
idx === shinePos ? `${shineColor}█${RESET}` : `${fillColor}█${RESET}`
|
|
333
|
+
).join("");
|
|
334
|
+
} else if (filled > 0) {
|
|
335
|
+
fillText = `${fillColor}${fillText}${RESET}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const emptyText = empty > 0 ? `${MOCHA.border}${"░".repeat(empty)}${RESET}` : "";
|
|
339
|
+
return `${fillText}${emptyText}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── 애니메이션 진행률 바 (shimmer sweep) ──
|
|
343
|
+
export function animatedProgressBar(percent, width = 20, tick = 0) {
|
|
236
344
|
const ratio = Math.max(0, Math.min(100, percent)) / 100;
|
|
237
345
|
const filled = Math.round(ratio * width);
|
|
238
346
|
const empty = width - filled;
|
|
239
|
-
|
|
240
|
-
|
|
347
|
+
if (filled === 0 || percent >= 100) return progressBar(percent, width);
|
|
348
|
+
const baseClr = percent >= 50 ? MOCHA.partial : MOCHA.fail;
|
|
349
|
+
const pos = tick % (filled + 3);
|
|
350
|
+
let bar = "";
|
|
351
|
+
for (let i = 0; i < filled; i++) {
|
|
352
|
+
const d = Math.abs(i - pos);
|
|
353
|
+
if (d === 0) bar += `${ESC}[97m█`;
|
|
354
|
+
else if (d === 1) bar += `${baseClr}▓`;
|
|
355
|
+
else bar += `${baseClr}█`;
|
|
356
|
+
}
|
|
357
|
+
return `${bar}${MOCHA.border}${"░".repeat(empty)}${RESET}`;
|
|
241
358
|
}
|
|
242
359
|
|
|
243
360
|
// ── 상태 아이콘 ──
|
|
@@ -253,3 +370,9 @@ export const CLI_ICON = {
|
|
|
253
370
|
gemini: `${FG.gemini}🔵${RESET}`,
|
|
254
371
|
claude: `${FG.claude}🟠${RESET}`,
|
|
255
372
|
};
|
|
373
|
+
|
|
374
|
+
// ── 로딩 도트 (braille spinner) ──
|
|
375
|
+
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
376
|
+
export function loadingDots(tick = 0, clr = MOCHA.thinking) {
|
|
377
|
+
return `${clr}${BRAILLE_FRAMES[tick % BRAILLE_FRAMES.length]}${RESET}`;
|
|
378
|
+
}
|
package/hub/team/tui.mjs
CHANGED
|
@@ -61,24 +61,59 @@ function lerpRgb(a, b, t) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function
|
|
64
|
+
function rgbSeq(rgb, mode = 38) {
|
|
65
|
+
return `\x1b[${mode};2;${rgb.r};${rgb.g};${rgb.b}m`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pseudoRandomFrame(step, seed) {
|
|
69
|
+
return Math.abs(Math.imul(step + seed, 2654435761)) % SPINNER_FRAMES.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function heartbeat(status, shimmerIntensity = 0, statusChangedAt = 0, time = Date.now()) {
|
|
73
|
+
const transitionElapsed = statusChangedAt ? Math.max(0, time - statusChangedAt) : Number.POSITIVE_INFINITY;
|
|
74
|
+
if (transitionElapsed < 500) {
|
|
75
|
+
const step = Math.floor(transitionElapsed / 50);
|
|
76
|
+
const idx = pseudoRandomFrame(step, statusChangedAt % 997);
|
|
77
|
+
const targetColor = status === "failed" || status === "error"
|
|
78
|
+
? MOCHA.fail
|
|
79
|
+
: status === "done" || status === "completed"
|
|
80
|
+
? MOCHA.ok
|
|
81
|
+
: shimmerIntensity > 0
|
|
82
|
+
? rgbSeq(lerpRgb(SPINNER_BASE_COLOR, SPINNER_SHIMMER, shimmerIntensity))
|
|
83
|
+
: MOCHA.executing;
|
|
84
|
+
return `${targetColor}${SPINNER_FRAMES[idx]}${RESET}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
if (status === "done" || status === "completed") return color("✓", MOCHA.ok);
|
|
66
88
|
if (status === "failed" || status === "error") return color("✗", MOCHA.fail);
|
|
67
89
|
if (status !== "running") return dim("○");
|
|
68
|
-
const elapsed =
|
|
90
|
+
const elapsed = time - spinnerStart;
|
|
69
91
|
const idx = Math.floor((elapsed / SPINNER_CYCLE_MS) * SPINNER_FRAMES.length) % SPINNER_FRAMES.length;
|
|
70
92
|
const c = shimmerIntensity > 0
|
|
71
93
|
? lerpRgb(SPINNER_BASE_COLOR, SPINNER_SHIMMER, shimmerIntensity)
|
|
72
94
|
: SPINNER_BASE_COLOR;
|
|
73
|
-
return
|
|
95
|
+
return `${rgbSeq(c)}${SPINNER_FRAMES[idx]}${RESET}`;
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
function currentShimmer() {
|
|
77
|
-
const elapsed =
|
|
78
|
-
const
|
|
98
|
+
function currentShimmer(time = Date.now()) {
|
|
99
|
+
const elapsed = time - spinnerStart;
|
|
100
|
+
const quantized = Math.floor(elapsed / 80) * 80;
|
|
101
|
+
const t = (quantized % SPINNER_CYCLE_MS) / SPINNER_CYCLE_MS;
|
|
79
102
|
return 0.5 * (1 + Math.sin(t * Math.PI * 2));
|
|
80
103
|
}
|
|
81
104
|
|
|
105
|
+
// ── activity wave — Tier1 헤더용 미니 파형 ──
|
|
106
|
+
const WAVE_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
107
|
+
function activityWave(tick, count = 4) {
|
|
108
|
+
let wave = "";
|
|
109
|
+
for (let i = 0; i < count; i++) {
|
|
110
|
+
const phase = tick * 0.3 + i * 1.5;
|
|
111
|
+
const idx = Math.floor((Math.sin(phase) * 0.5 + 0.5) * (WAVE_CHARS.length - 1));
|
|
112
|
+
wave += WAVE_CHARS[idx];
|
|
113
|
+
}
|
|
114
|
+
return `${MOCHA.executing}${wave}${RESET}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
const GRID_GAP = 2;
|
|
83
118
|
const DEFAULT_DETAIL_LINES = 10;
|
|
84
119
|
// Tier1 상단 고정 행 수
|
|
@@ -200,6 +235,13 @@ const MOCHA_RGB = {
|
|
|
200
235
|
executing: { r: 116, g: 199, b: 236 },
|
|
201
236
|
muted: { r: 147, g: 153, b: 178 },
|
|
202
237
|
border: { r: 69, g: 71, b: 90 },
|
|
238
|
+
blue: { r: 137, g: 180, b: 250 },
|
|
239
|
+
sky: { r: 116, g: 199, b: 236 },
|
|
240
|
+
yellow: { r: 249, g: 226, b: 175 },
|
|
241
|
+
peach: { r: 250, g: 179, b: 135 },
|
|
242
|
+
maroon: { r: 235, g: 160, b: 172 },
|
|
243
|
+
surface0: { r: 49, g: 50, b: 68 },
|
|
244
|
+
thinking: { r: 203, g: 166, b: 247 },
|
|
203
245
|
};
|
|
204
246
|
|
|
205
247
|
function statusToRgb(status) {
|
|
@@ -211,17 +253,63 @@ function statusToRgb(status) {
|
|
|
211
253
|
}
|
|
212
254
|
|
|
213
255
|
const FADE_DURATION_MS = 1500;
|
|
256
|
+
const FLASH_PHASE_MS = 250;
|
|
257
|
+
const CARD_GLOW_MS = 3000;
|
|
258
|
+
|
|
259
|
+
// Effect 1: Pulse border — running 워커 보더가 heartbeat 동기 breathing
|
|
260
|
+
function pulseBorderColor(statusRgb, time = Date.now()) {
|
|
261
|
+
const intensity = 0.3 + 0.7 * currentShimmer(time);
|
|
262
|
+
const c = lerpRgb(MOCHA_RGB.border, statusRgb, intensity);
|
|
263
|
+
return rgbSeq(c);
|
|
264
|
+
}
|
|
214
265
|
|
|
215
|
-
|
|
266
|
+
// Effect 2: Gradient border — focus pane 보더 상단→하단 그라데이션
|
|
267
|
+
function gradientBorderFn(topRgb, bottomRgb) {
|
|
268
|
+
return (row, totalRows) => {
|
|
269
|
+
const t = totalRows <= 1 ? 0 : row / (totalRows - 1);
|
|
270
|
+
const c = lerpRgb(topRgb, bottomRgb, t);
|
|
271
|
+
return `\x1b[38;2;${c.r};${c.g};${c.b}m`;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Effect 3: Flash-fade border — 상태 변경 시 백색 플래시 → 페이드아웃
|
|
276
|
+
function flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
|
|
216
277
|
const elapsed = Date.now() - (changedAt || 0);
|
|
217
|
-
if (elapsed >= FADE_DURATION_MS || !prevStatus) return
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
278
|
+
if (elapsed >= FADE_DURATION_MS || !prevStatus) return null;
|
|
279
|
+
const statusRgb = statusToRgb(currentStatus);
|
|
280
|
+
if (elapsed < FLASH_PHASE_MS) {
|
|
281
|
+
const t = elapsed / FLASH_PHASE_MS;
|
|
282
|
+
const bright = { r: 255, g: 255, b: 255 };
|
|
283
|
+
const c = lerpRgb(bright, statusRgb, t);
|
|
284
|
+
return `\x1b[38;2;${c.r};${c.g};${c.b}m`;
|
|
285
|
+
}
|
|
286
|
+
const t = (elapsed - FLASH_PHASE_MS) / (FADE_DURATION_MS - FLASH_PHASE_MS);
|
|
287
|
+
const c = lerpRgb(statusRgb, MOCHA_RGB.border, t);
|
|
222
288
|
return `\x1b[38;2;${c.r};${c.g};${c.b}m`;
|
|
223
289
|
}
|
|
224
290
|
|
|
291
|
+
function easeOutCubic(t) {
|
|
292
|
+
return 1 - ((1 - t) ** 3);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function borderHighlightPosition(width, bodyLines, time = Date.now()) {
|
|
296
|
+
const totalRows = bodyLines + 2;
|
|
297
|
+
const perimeter = 2 * (width - 2) + 2 * totalRows;
|
|
298
|
+
if (perimeter <= 0) return undefined;
|
|
299
|
+
return Math.floor(time / 120) % perimeter;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function titleFlash(status, changeElapsed) {
|
|
303
|
+
const isCompleted = status === "completed" || status === "done" || status === "ok";
|
|
304
|
+
const isFailed = status === "failed" || status === "error" || status === "fail";
|
|
305
|
+
if ((!isCompleted && !isFailed) || changeElapsed > 800) return null;
|
|
306
|
+
const flashRgb = isCompleted ? MOCHA_RGB.ok : MOCHA_RGB.fail;
|
|
307
|
+
const bgRgb = changeElapsed <= 300
|
|
308
|
+
? flashRgb
|
|
309
|
+
: lerpRgb(flashRgb, MOCHA_RGB.surface0, clamp((changeElapsed - 300) / 500, 0, 1));
|
|
310
|
+
return rgbSeq(bgRgb, 48);
|
|
311
|
+
}
|
|
312
|
+
|
|
225
313
|
function dedupeRole(role, name, cli) {
|
|
226
314
|
if (!role) return "";
|
|
227
315
|
let r = role;
|
|
@@ -320,22 +408,28 @@ function countStatuses(names, workers) {
|
|
|
320
408
|
}
|
|
321
409
|
|
|
322
410
|
// ── Tier1: 상단 고정 1행 ─────────────────────────────────────────────────
|
|
323
|
-
function phaseColor(phase) {
|
|
324
|
-
|
|
325
|
-
if (phase === "
|
|
326
|
-
if (phase === "
|
|
411
|
+
function phaseColor(phase, time = Date.now()) {
|
|
412
|
+
const shimmer = currentShimmer(time);
|
|
413
|
+
if (phase === "exec" || phase === "executing") return rgbSeq(lerpRgb(MOCHA_RGB.blue, MOCHA_RGB.sky, shimmer));
|
|
414
|
+
if (phase === "verify" || phase === "verifying") return rgbSeq(lerpRgb(MOCHA_RGB.yellow, MOCHA_RGB.peach, shimmer));
|
|
415
|
+
if (phase === "fix" || phase === "fixing") return rgbSeq(lerpRgb(MOCHA_RGB.fail, MOCHA_RGB.maroon, shimmer));
|
|
327
416
|
return FG.accent;
|
|
328
417
|
}
|
|
329
418
|
|
|
330
|
-
function buildTier1(names, workers, pipeline, elapsed, width, version) {
|
|
419
|
+
function buildTier1(names, workers, pipeline, elapsed, width, version, time = Date.now()) {
|
|
331
420
|
const { ok, partial, failed, running } = countStatuses(names, workers);
|
|
332
421
|
const phase = pipeline.phase || "exec";
|
|
333
422
|
const row1 = truncate(
|
|
334
|
-
`${color("▲", FG.triflux)} v${version} ${dim("│")} ${color(phase, phaseColor(phase))} ${dim("│")} ${elapsed}s ${dim("│")} ` +
|
|
335
|
-
`${color(`✓${ok}`, MOCHA.ok)} ${color(`◑${partial}`, MOCHA.partial)} ${color(`✗${failed}`, MOCHA.fail)} ${dim(`▶${running}`)}
|
|
423
|
+
`${color("▲", FG.triflux)} v${version} ${dim("│")} ${color(phase, phaseColor(phase, time))} ${dim("│")} ${elapsed}s ${dim("│")} ` +
|
|
424
|
+
`${color(`✓${ok}`, MOCHA.ok)} ${color(`◑${partial}`, MOCHA.partial)} ${color(`✗${failed}`, MOCHA.fail)} ${dim(`▶${running}`)}${running > 0 ? ` ${activityWave(spinnerTick)}` : ""}`,
|
|
336
425
|
width,
|
|
337
426
|
);
|
|
338
|
-
|
|
427
|
+
const keysHint = color("Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • n:recent • 1-9:jump", MOCHA.subtext);
|
|
428
|
+
const hintWidth = wcswidth(stripAnsi(keysHint));
|
|
429
|
+
const row2 = hintWidth >= width
|
|
430
|
+
? truncate(keysHint, width)
|
|
431
|
+
: padRight(`${" ".repeat(width - hintWidth)}${keysHint}`, width);
|
|
432
|
+
return [row1, row2];
|
|
339
433
|
}
|
|
340
434
|
|
|
341
435
|
// ── 카드 렌더러 (Tier2 worker rail) ─────────────────────────────────────
|
|
@@ -368,33 +462,46 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
368
462
|
width,
|
|
369
463
|
selected = false,
|
|
370
464
|
focused = false, // rail 포커스 여부
|
|
465
|
+
previousSelected = false,
|
|
371
466
|
rawMode = false,
|
|
372
467
|
compact = false,
|
|
468
|
+
time = Date.now(),
|
|
373
469
|
} = opts;
|
|
374
470
|
const innerWidth = Math.max(12, width - 4);
|
|
375
471
|
const cli = st.cli || "codex";
|
|
376
472
|
const role = sanitizeOneLine(st.role);
|
|
377
473
|
const status = runtimeStatus(st);
|
|
378
474
|
const sec = Number.isFinite(st._logSec) ? st._logSec : 0;
|
|
475
|
+
const changeElapsed = st._statusChangedAt ? Math.max(0, time - st._statusChangedAt) : Number.POSITIVE_INFINITY;
|
|
379
476
|
|
|
380
477
|
// Tier2 행 1: 이름 + CLI + role
|
|
381
|
-
const selMark = selected
|
|
382
|
-
|
|
478
|
+
const selMark = selected
|
|
479
|
+
? (focused ? color("▶", MOCHA.blue) : color(">", FG.triflux))
|
|
480
|
+
: previousSelected
|
|
481
|
+
? dim("~")
|
|
482
|
+
: " ";
|
|
483
|
+
const hb = heartbeat(status, status === "running" ? currentShimmer(time) : 0, st._statusChangedAt, time);
|
|
383
484
|
const displayRole = dedupeRole(role, name, cli);
|
|
384
485
|
const title = truncate(
|
|
385
486
|
`${selMark} ${hb} ${color(name, FG.triflux)} ${color("•", MOCHA.overlay)} ${color(cli, cliColor(cli))}${displayRole ? ` ${color(`(${displayRole})`, MOCHA.overlay)}` : ""}`,
|
|
386
487
|
innerWidth,
|
|
387
488
|
);
|
|
388
489
|
|
|
389
|
-
|
|
490
|
+
const cardWidth = Math.max(MIN_CARD_WIDTH, width);
|
|
491
|
+
const borderHighlight = focused ? borderHighlightPosition(cardWidth, compact ? 2 : 6, time) : undefined;
|
|
492
|
+
const titleFlashBg = titleFlash(status, changeElapsed);
|
|
493
|
+
|
|
494
|
+
// status-specific border: focused→mauve, selected→bright, non-selected→glow decay
|
|
390
495
|
const statusBorderColor = (() => {
|
|
391
496
|
if (focused) return MOCHA.thinking;
|
|
497
|
+
if (selected && (status === "running" || status === "in_progress")) {
|
|
498
|
+
return pulseBorderColor(statusToRgb(status), time);
|
|
499
|
+
}
|
|
392
500
|
if (selected) return statusColor(status);
|
|
393
|
-
// Non-selected: status-tinted border (50% blend toward border gray)
|
|
394
501
|
const from = statusToRgb(status);
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
return
|
|
502
|
+
const decayBase = st._statusChangedAt ? clamp(changeElapsed / CARD_GLOW_MS, 0, 1) : 1;
|
|
503
|
+
const decayT = easeOutCubic(decayBase);
|
|
504
|
+
return rgbSeq(lerpRgb(from, MOCHA_RGB.border, 0.5 + (0.5 * decayT)));
|
|
398
505
|
})();
|
|
399
506
|
|
|
400
507
|
if (compact) {
|
|
@@ -407,7 +514,10 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
407
514
|
);
|
|
408
515
|
const verdict = sanitizeOneLine(st.handoff?.verdict || st.summary || st.snapshot, status);
|
|
409
516
|
const compactLine2 = truncate(color(verdict, MOCHA.text), innerWidth);
|
|
410
|
-
const framed = box([compactLine1, compactLine2],
|
|
517
|
+
const framed = box([compactLine1, compactLine2], cardWidth, statusBorderColor, {
|
|
518
|
+
highlightPos: borderHighlight,
|
|
519
|
+
titleFlashBg,
|
|
520
|
+
});
|
|
411
521
|
return [framed.top, ...framed.body, framed.bot];
|
|
412
522
|
}
|
|
413
523
|
|
|
@@ -422,8 +532,9 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
422
532
|
const progress = Number.isFinite(st.progress) ? clamp(st.progress, 0, 1) : (status === "running" ? 0.3 : 1);
|
|
423
533
|
const percent = Math.round(progress * 100);
|
|
424
534
|
const barWidth = clamp(Math.floor(innerWidth * 0.3), 8, 16);
|
|
535
|
+
const bar = progressBar(percent, barWidth, time);
|
|
425
536
|
const progressLine = truncate(
|
|
426
|
-
`${
|
|
537
|
+
`${bar} ${color(`${String(percent).padStart(3)}%`, MOCHA.text)}`,
|
|
427
538
|
innerWidth,
|
|
428
539
|
);
|
|
429
540
|
|
|
@@ -442,7 +553,10 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
442
553
|
truncate(`${color("files", MOCHA.overlay)} ${color(files, MOCHA.subtext)}`, innerWidth),
|
|
443
554
|
];
|
|
444
555
|
|
|
445
|
-
const framed = box(lines,
|
|
556
|
+
const framed = box(lines, cardWidth, statusBorderColor, {
|
|
557
|
+
highlightPos: borderHighlight,
|
|
558
|
+
titleFlashBg,
|
|
559
|
+
});
|
|
446
560
|
return [framed.top, ...framed.body, framed.bot];
|
|
447
561
|
}
|
|
448
562
|
|
|
@@ -455,6 +569,7 @@ function buildFocusPane(name, st, opts = {}) {
|
|
|
455
569
|
followTail = false,
|
|
456
570
|
rawMode = false,
|
|
457
571
|
focused = false,
|
|
572
|
+
time = Date.now(),
|
|
458
573
|
} = opts;
|
|
459
574
|
const innerWidth = Math.max(12, width - 4);
|
|
460
575
|
|
|
@@ -523,9 +638,14 @@ function buildFocusPane(name, st, opts = {}) {
|
|
|
523
638
|
truncate(scrollInfo, innerWidth),
|
|
524
639
|
];
|
|
525
640
|
|
|
526
|
-
// focused pane gets
|
|
527
|
-
const borderColor = focused
|
|
528
|
-
|
|
641
|
+
// Effect 2: focused pane gets gradient border (blue→border), unfocused gets dim
|
|
642
|
+
const borderColor = focused
|
|
643
|
+
? gradientBorderFn(MOCHA_RGB.blue, MOCHA_RGB.border)
|
|
644
|
+
: MOCHA.border;
|
|
645
|
+
const paneWidth = Math.max(MIN_CARD_WIDTH, width);
|
|
646
|
+
const framed = box(contentLines, paneWidth, borderColor, {
|
|
647
|
+
highlightPos: focused ? borderHighlightPosition(paneWidth, contentLines.length, time) : undefined,
|
|
648
|
+
});
|
|
529
649
|
return [framed.top, ...framed.body, framed.bot];
|
|
530
650
|
}
|
|
531
651
|
|
|
@@ -540,11 +660,52 @@ function buildSummaryBar(names, workers, selectedWorker, pipeline, width, versio
|
|
|
540
660
|
return padRight(truncate(label, maxChipWidth), maxChipWidth);
|
|
541
661
|
});
|
|
542
662
|
const chipsLine = truncate(chips.join(color(" │ ", MOCHA.overlay)), width - 4);
|
|
543
|
-
const keysLine = truncate(color("Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • 1-9:jump", MOCHA.subtext), width - 4);
|
|
663
|
+
const keysLine = truncate(color("Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • n:recent • 1-9:jump", MOCHA.subtext), width - 4);
|
|
544
664
|
const framed = box([chipsLine, keysLine], width);
|
|
545
665
|
return [framed.top, ...framed.body, framed.bot];
|
|
546
666
|
}
|
|
547
667
|
|
|
668
|
+
// ── help overlay ──────────────────────────────────────────────────────────
|
|
669
|
+
function buildHelpOverlay(width, height) {
|
|
670
|
+
const innerWidth = Math.min(50, width - 6);
|
|
671
|
+
const helpLines = [
|
|
672
|
+
color(" Keyboard Shortcuts", FG.triflux),
|
|
673
|
+
"",
|
|
674
|
+
` ${color("Tab", MOCHA.blue)} rail ↔ detail 포커스 전환`,
|
|
675
|
+
` ${color("j/↓", MOCHA.blue)} 다음 워커 / 스크롤 아래`,
|
|
676
|
+
` ${color("k/↑", MOCHA.blue)} 이전 워커 / 스크롤 위`,
|
|
677
|
+
` ${color("1-9", MOCHA.blue)} 워커 직접 선택`,
|
|
678
|
+
` ${color("n", MOCHA.blue)} 최근 상태 변경 워커 선택`,
|
|
679
|
+
` ${color("f", MOCHA.blue)} follow-tail 토글`,
|
|
680
|
+
` ${color("r", MOCHA.blue)} raw mode 토글`,
|
|
681
|
+
` ${color("l", MOCHA.blue)} 탭 전환 (Log/Detail/Files)`,
|
|
682
|
+
` ${color("g", MOCHA.blue)} focus pane 상단 점프`,
|
|
683
|
+
` ${color("G", MOCHA.blue)} focus pane 하단 점프`,
|
|
684
|
+
` ${color("PgUp", MOCHA.blue)} 페이지 위 스크롤`,
|
|
685
|
+
` ${color("PgDn", MOCHA.blue)} 페이지 아래 스크롤`,
|
|
686
|
+
` ${color("Shift+↑↓", MOCHA.blue)} 워커 선택 + 포커스 이동`,
|
|
687
|
+
` ${color("Shift+←→", MOCHA.blue)} rail ↔ detail 포커스`,
|
|
688
|
+
` ${color("h/?", MOCHA.blue)} 이 도움말 토글`,
|
|
689
|
+
` ${color("q", MOCHA.blue)} 대시보드 종료`,
|
|
690
|
+
"",
|
|
691
|
+
dim(" 아무 키나 눌러 닫기"),
|
|
692
|
+
];
|
|
693
|
+
const framed = box(helpLines, innerWidth + 4, MOCHA.blue);
|
|
694
|
+
const framedRows = [framed.top, ...framed.body, framed.bot];
|
|
695
|
+
const topPad = Math.max(0, Math.floor((height - framedRows.length) / 2));
|
|
696
|
+
const leftPad = " ".repeat(Math.max(0, Math.floor((width - innerWidth - 4) / 2)));
|
|
697
|
+
const result = [];
|
|
698
|
+
for (let i = 0; i < height; i++) {
|
|
699
|
+
const fi = i - topPad;
|
|
700
|
+
if (fi >= 0 && fi < framedRows.length) {
|
|
701
|
+
result.push(`${leftPad}${framedRows[fi]}`);
|
|
702
|
+
} else {
|
|
703
|
+
result.push("");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
|
|
548
709
|
// ── joinColumns ───────────────────────────────────────────────────────────
|
|
549
710
|
function joinColumns(blocks, gap = GRID_GAP) {
|
|
550
711
|
const maxHeight = Math.max(...blocks.map((b) => b.length));
|
|
@@ -628,12 +789,14 @@ export function createLogDashboard(opts = {}) {
|
|
|
628
789
|
let closed = false;
|
|
629
790
|
let frameCount = 0;
|
|
630
791
|
let selectedWorker = null;
|
|
792
|
+
let previousSelectedWorker = null;
|
|
631
793
|
// focus: "rail" | "detail"
|
|
632
794
|
let focus = "rail";
|
|
633
795
|
let detailScrollOffset = 0;
|
|
634
796
|
let followTail = false;
|
|
635
797
|
let rawMode = false;
|
|
636
798
|
let focusTab = "log"; // "log" | "detail" | "files"
|
|
799
|
+
let helpOverlay = false;
|
|
637
800
|
let inputAttached = false;
|
|
638
801
|
let rawModeEnabled = false;
|
|
639
802
|
|
|
@@ -678,13 +841,34 @@ export function createLogDashboard(opts = {}) {
|
|
|
678
841
|
if (!selectedWorker || !workers.has(selectedWorker)) selectedWorker = names[0];
|
|
679
842
|
}
|
|
680
843
|
|
|
844
|
+
function setSelectedWorker(nextWorker, { preserveTrail = true } = {}) {
|
|
845
|
+
if (!nextWorker || nextWorker === selectedWorker) return;
|
|
846
|
+
if (preserveTrail && selectedWorker && workers.has(selectedWorker)) {
|
|
847
|
+
previousSelectedWorker = selectedWorker;
|
|
848
|
+
}
|
|
849
|
+
selectedWorker = nextWorker;
|
|
850
|
+
detailScrollOffset = 0;
|
|
851
|
+
}
|
|
852
|
+
|
|
681
853
|
function selectRelative(offset) {
|
|
682
854
|
const names = visibleWorkerNames();
|
|
683
855
|
if (names.length === 0) return;
|
|
684
856
|
ensureSelectedWorker(names);
|
|
685
857
|
const idx = Math.max(0, names.indexOf(selectedWorker));
|
|
686
|
-
|
|
687
|
-
|
|
858
|
+
setSelectedWorker(names[(idx + offset + names.length) % names.length]);
|
|
859
|
+
render();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function selectMostRecentChangedWorker() {
|
|
863
|
+
const names = visibleWorkerNames();
|
|
864
|
+
if (names.length === 0) return;
|
|
865
|
+
ensureSelectedWorker(names);
|
|
866
|
+
const target = names.reduce((best, name) => {
|
|
867
|
+
const changedAt = workers.get(name)?._statusChangedAt || 0;
|
|
868
|
+
const bestChangedAt = workers.get(best)?._statusChangedAt || 0;
|
|
869
|
+
return changedAt > bestChangedAt ? name : best;
|
|
870
|
+
}, names[0]);
|
|
871
|
+
setSelectedWorker(target);
|
|
688
872
|
render();
|
|
689
873
|
}
|
|
690
874
|
|
|
@@ -694,11 +878,29 @@ export function createLogDashboard(opts = {}) {
|
|
|
694
878
|
render();
|
|
695
879
|
}
|
|
696
880
|
|
|
881
|
+
// ── doClose (내부 함수) ─────────────────────────────────────────────
|
|
882
|
+
function doClose() {
|
|
883
|
+
if (closed) return;
|
|
884
|
+
if (timer) clearInterval(timer);
|
|
885
|
+
if (inputAttached && typeof input?.off === "function") input.off("data", handleInput);
|
|
886
|
+
if (rawModeEnabled && typeof input?.setRawMode === "function") input.setRawMode(false);
|
|
887
|
+
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
888
|
+
exitAltScreen();
|
|
889
|
+
closed = true;
|
|
890
|
+
}
|
|
891
|
+
|
|
697
892
|
// ── 키 입력 ──────────────────────────────────────────────────────────
|
|
698
893
|
function handleInput(chunk) {
|
|
699
894
|
const key = String(chunk);
|
|
700
895
|
if (key === "\u0003") return; // Ctrl-C
|
|
701
896
|
|
|
897
|
+
// Help overlay: 아무 키나 누르면 닫기
|
|
898
|
+
if (helpOverlay) {
|
|
899
|
+
helpOverlay = false;
|
|
900
|
+
render();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
702
904
|
// Tab: rail ↔ detail 포커스 전환
|
|
703
905
|
if (key === "\t") {
|
|
704
906
|
focus = focus === "rail" ? "detail" : "rail";
|
|
@@ -722,6 +924,14 @@ export function createLogDashboard(opts = {}) {
|
|
|
722
924
|
if (key === "k" || key === "\u001b[A") { selectRelative(-1); return; }
|
|
723
925
|
}
|
|
724
926
|
|
|
927
|
+
// g: focus pane 상단 점프
|
|
928
|
+
if (key === "g") { followTail = false; detailScrollOffset = 0; render(); return; }
|
|
929
|
+
// G: focus pane 하단 점프
|
|
930
|
+
if (key === "G") { followTail = true; detailScrollOffset = 0; render(); return; }
|
|
931
|
+
// PgUp/PgDn: 페이지 단위 스크롤
|
|
932
|
+
const pageSize = Math.max(1, Math.floor(getViewportRows() / 2));
|
|
933
|
+
if (key === "\x1b[5~") { scrollDetail(-pageSize); return; } // PgUp
|
|
934
|
+
if (key === "\x1b[6~") { scrollDetail(pageSize); return; } // PgDn
|
|
725
935
|
// f: follow-tail 토글
|
|
726
936
|
if (key === "f") { followTail = !followTail; if (followTail) detailScrollOffset = 0; render(); return; }
|
|
727
937
|
// r: raw mode 토글
|
|
@@ -734,11 +944,17 @@ export function createLogDashboard(opts = {}) {
|
|
|
734
944
|
render();
|
|
735
945
|
return;
|
|
736
946
|
}
|
|
947
|
+
// n: 가장 최근 상태 변경 워커로 이동
|
|
948
|
+
if (key === "n") { selectMostRecentChangedWorker(); return; }
|
|
949
|
+
// h/?: 도움말 오버레이 토글
|
|
950
|
+
if (key === "h" || key === "?") { helpOverlay = true; render(); return; }
|
|
951
|
+
// q: 대시보드 종료
|
|
952
|
+
if (key === "q") { doClose(); return; }
|
|
737
953
|
// 1-9: 워커 직접 선택
|
|
738
954
|
if (/^[1-9]$/.test(key)) {
|
|
739
955
|
const names = visibleWorkerNames();
|
|
740
956
|
const target = names[Number.parseInt(key, 10) - 1];
|
|
741
|
-
if (target) {
|
|
957
|
+
if (target) { setSelectedWorker(target); render(); }
|
|
742
958
|
return;
|
|
743
959
|
}
|
|
744
960
|
}
|
|
@@ -773,10 +989,17 @@ export function createLogDashboard(opts = {}) {
|
|
|
773
989
|
|
|
774
990
|
const totalCols = getViewportColumns();
|
|
775
991
|
const totalRows = getViewportRows();
|
|
992
|
+
|
|
993
|
+
// Help overlay: 전체 화면 오버레이
|
|
994
|
+
if (helpOverlay) {
|
|
995
|
+
return buildHelpOverlay(totalCols, totalRows);
|
|
996
|
+
}
|
|
997
|
+
|
|
776
998
|
const elapsed = nowElapsedSec();
|
|
999
|
+
const renderTime = Date.now();
|
|
777
1000
|
|
|
778
1001
|
// Tier1: 상단 고정 2행
|
|
779
|
-
const tier1 = buildTier1(names, workers, pipeline, elapsed, totalCols, VERSION);
|
|
1002
|
+
const tier1 = buildTier1(names, workers, pipeline, elapsed, totalCols, VERSION, renderTime);
|
|
780
1003
|
|
|
781
1004
|
// 레이아웃 결정
|
|
782
1005
|
let effectiveLayout = layoutHint;
|
|
@@ -800,6 +1023,7 @@ export function createLogDashboard(opts = {}) {
|
|
|
800
1023
|
rawMode,
|
|
801
1024
|
focused: focus === "detail",
|
|
802
1025
|
activeTab: focusTab,
|
|
1026
|
+
time: renderTime,
|
|
803
1027
|
});
|
|
804
1028
|
return [...tier1, ...summaryBar, ...focusPane];
|
|
805
1029
|
}
|
|
@@ -807,12 +1031,14 @@ export function createLogDashboard(opts = {}) {
|
|
|
807
1031
|
// 좌우 분할: Left Rail (30%) | Right Focus (70%)
|
|
808
1032
|
// 목업: Tier2 Left Rail + Tier3 Focus 나란히 렌더링
|
|
809
1033
|
const GAP = 1; // rail과 focus 사이 구분선
|
|
810
|
-
const
|
|
1034
|
+
const railRatio = focus === "detail" ? 0.20 : 0.30;
|
|
1035
|
+
const railWidth = Math.max(MIN_CARD_WIDTH, Math.floor(totalCols * railRatio));
|
|
811
1036
|
const focusWidth = totalCols - railWidth - GAP;
|
|
812
1037
|
const bodyHeight = Math.max(6, totalRows - tier1.length - 1); // -1 for status bar
|
|
813
1038
|
|
|
814
|
-
// compact
|
|
815
|
-
const
|
|
1039
|
+
// 반응형 compact: 워커 카드가 가용 높이 초과 시 자동 전환
|
|
1040
|
+
const normalCardHeight = 8; // box top/bot + 6 content lines
|
|
1041
|
+
const useCompact = names.length * normalCardHeight > bodyHeight;
|
|
816
1042
|
|
|
817
1043
|
// Left Rail: 워커 카드 세로 스택
|
|
818
1044
|
const railLines = [];
|
|
@@ -820,9 +1046,11 @@ export function createLogDashboard(opts = {}) {
|
|
|
820
1046
|
const card = buildWorkerRail(name, workers.get(name), {
|
|
821
1047
|
width: railWidth,
|
|
822
1048
|
selected: name === selectedWorker,
|
|
1049
|
+
previousSelected: name === previousSelectedWorker,
|
|
823
1050
|
focused: focus === "rail" && name === selectedWorker,
|
|
824
1051
|
rawMode,
|
|
825
1052
|
compact: useCompact,
|
|
1053
|
+
time: renderTime,
|
|
826
1054
|
});
|
|
827
1055
|
railLines.push(...card);
|
|
828
1056
|
}
|
|
@@ -841,6 +1069,7 @@ export function createLogDashboard(opts = {}) {
|
|
|
841
1069
|
rawMode,
|
|
842
1070
|
focused: focus === "detail",
|
|
843
1071
|
activeTab: focusTab,
|
|
1072
|
+
time: renderTime,
|
|
844
1073
|
});
|
|
845
1074
|
}
|
|
846
1075
|
while (focusLines.length < bodyHeight) focusLines.push(padRight("", focusWidth));
|
|
@@ -899,10 +1128,14 @@ export function createLogDashboard(opts = {}) {
|
|
|
899
1128
|
if (closed) return;
|
|
900
1129
|
frameCount++;
|
|
901
1130
|
spinnerTick++;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1131
|
+
try {
|
|
1132
|
+
if (isTTY) {
|
|
1133
|
+
renderAltScreen();
|
|
1134
|
+
} else {
|
|
1135
|
+
renderAppendOnly();
|
|
1136
|
+
}
|
|
1137
|
+
} finally {
|
|
1138
|
+
previousSelectedWorker = null;
|
|
906
1139
|
}
|
|
907
1140
|
}
|
|
908
1141
|
|
|
@@ -950,7 +1183,7 @@ export function createLogDashboard(opts = {}) {
|
|
|
950
1183
|
|
|
951
1184
|
selectWorker(name) {
|
|
952
1185
|
if (!workers.has(name)) return;
|
|
953
|
-
|
|
1186
|
+
setSelectedWorker(name);
|
|
954
1187
|
},
|
|
955
1188
|
|
|
956
1189
|
toggleDetail(force) {
|
|
@@ -994,14 +1227,16 @@ export function createLogDashboard(opts = {}) {
|
|
|
994
1227
|
return layoutHint;
|
|
995
1228
|
},
|
|
996
1229
|
|
|
1230
|
+
toggleHelp(force) {
|
|
1231
|
+
helpOverlay = typeof force === "boolean" ? force : !helpOverlay;
|
|
1232
|
+
},
|
|
1233
|
+
|
|
1234
|
+
isHelpVisible() {
|
|
1235
|
+
return helpOverlay;
|
|
1236
|
+
},
|
|
1237
|
+
|
|
997
1238
|
close() {
|
|
998
|
-
|
|
999
|
-
if (timer) clearInterval(timer);
|
|
1000
|
-
if (inputAttached && typeof input?.off === "function") input.off("data", handleInput);
|
|
1001
|
-
if (rawModeEnabled && typeof input?.setRawMode === "function") input.setRawMode(false);
|
|
1002
|
-
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
1003
|
-
exitAltScreen();
|
|
1004
|
-
closed = true;
|
|
1239
|
+
doClose();
|
|
1005
1240
|
},
|
|
1006
1241
|
};
|
|
1007
1242
|
}
|