termdeck-cli 2.0.4 → 2.0.5
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 +108 -163
- package/package.json +1 -1
- package/src/agentManager.js +6 -0
- package/src/dashboard.js +479 -135
- package/src/logView.js +3 -0
- package/src/projectManager.js +331 -12
package/src/dashboard.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* exactly the rows it needs, with zero gaps between panes:
|
|
8
8
|
*
|
|
9
9
|
* +-------------------------------------------------------------------+
|
|
10
|
-
* |
|
|
11
|
-
* |
|
|
12
|
-
*
|
|
13
|
-
* | [ALL 14] [LIVE 6] [EXP 4] …
|
|
10
|
+
* | 15:14:19 +------------+ ● DAEMON ON |
|
|
11
|
+
* | | TERMDECK | |
|
|
12
|
+
* | +------------+ |
|
|
13
|
+
* | [ALL 14] [LIVE 6] [EXP 4] … /search (regex) [box] |
|
|
14
14
|
* +---------------------------------+---------------------------------+
|
|
15
15
|
* | PROJECTS (14) | DETAILS: hyperion-core |
|
|
16
16
|
* | ● hyperion-core 12m ago | status / path / branch / port |
|
|
@@ -38,11 +38,11 @@ const { openInNewTerminal } = require('./terminal');
|
|
|
38
38
|
const { LogView } = require('./logView');
|
|
39
39
|
const { MODERN_STATUSES, loadConfig, loadConfigFromPath, displayPath, saveConfig } = require('./config');
|
|
40
40
|
const { escapeBraces, truncate, formatTimestamp, timestamp, timeAgo } = require('./util');
|
|
41
|
-
const { getGitInfo } = require('./projectManager');
|
|
42
|
-
const { launchAgent, tailAgentLog, stopAllAgents } = require('./agentManager');
|
|
41
|
+
const { getGitInfo, detectStack, generateCommitMessage, commitAndPush } = require('./projectManager');
|
|
42
|
+
const { launchAgent, tailAgentLog, stopAllAgents, activeTails } = require('./agentManager');
|
|
43
43
|
const { startMonitoring, stopMonitoring, stopAllMonitoring } = require('./processMonitor');
|
|
44
44
|
|
|
45
|
-
const LAYOUT = { rows: 12, cols: 12, headerHeight: 6, footerHeight:
|
|
45
|
+
const LAYOUT = { rows: 12, cols: 12, headerHeight: 6, footerHeight: 2 };
|
|
46
46
|
|
|
47
47
|
/* ------------------------------------------------------------------ *
|
|
48
48
|
* Theme — dark, modern palette, pastel status tags, thin borders.
|
|
@@ -53,9 +53,11 @@ const THEME = {
|
|
|
53
53
|
surface: '#181825', // slightly darker panels (output, footer)
|
|
54
54
|
text: '#cdd6f4', // general text (light gray-white)
|
|
55
55
|
textDim: '#9399b2', // secondary text (timestamps, labels)
|
|
56
|
+
tagCyan: '#94e2d5', // log [termdeck] source tag
|
|
56
57
|
border: '#45475a', // thin, unobtrusive box borders
|
|
57
|
-
accentBg: '#3b82f6', // selected-project
|
|
58
|
+
accentBg: '#3b82f6', // selected-project / focused-button highlight
|
|
58
59
|
accentFg: '#ffffff',
|
|
60
|
+
chipBg: '#2d2d3f', // action-button / stat-chip background
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
/** Pastel status colours per modern status; `unknown` is neutral gray. */
|
|
@@ -97,7 +99,7 @@ const DEMO_PID = 49201;
|
|
|
97
99
|
const SAMPLE_CONFIG_PATH = path.join(__dirname, '..', 'sample-config.json');
|
|
98
100
|
|
|
99
101
|
const FOOTER_KEYS =
|
|
100
|
-
'{bold}
|
|
102
|
+
'{bold}\u2191\u2193{/bold} navigate {bold}tab{/bold} pane {bold}s{/bold} status {bold}/{/bold} search {bold}r{/bold} dev {bold}shift+x{/bold} stop {bold}q{/bold} quit';
|
|
101
103
|
const HINTS = ` ${FOOTER_KEYS} `;
|
|
102
104
|
|
|
103
105
|
/** Colour-coded demo log lines so the OUTPUT pane styling can be checked. */
|
|
@@ -153,10 +155,12 @@ function launchDashboard(config, options = {}) {
|
|
|
153
155
|
const palette = new Map();
|
|
154
156
|
const status = { message: null, timer: null, quitArmed: false, quitTimer: null, clock: null, chip: null, search: null };
|
|
155
157
|
const gitInfo = new Map(); // project.path -> git snapshot (branch / hash / dirty)
|
|
158
|
+
const stackInfo = new Map();
|
|
156
159
|
const processStats = new Map(); // project.path -> {running, pid, memory, cpu}
|
|
157
160
|
let monitoredPath = null; // project.path currently polled by processMonitor
|
|
158
161
|
let searchActive = false;
|
|
159
162
|
let searchBuffer = '';
|
|
163
|
+
let gitModal = null;
|
|
160
164
|
|
|
161
165
|
const colorFor = (project) => palette.get(project.path) || THEME.text;
|
|
162
166
|
|
|
@@ -182,45 +186,162 @@ function launchDashboard(config, options = {}) {
|
|
|
182
186
|
el.style.label = { fg: THEME.textDim };
|
|
183
187
|
el.style.fg = THEME.text;
|
|
184
188
|
el.style.bg = THEME.bg;
|
|
185
|
-
if (label)
|
|
189
|
+
if (label) {
|
|
190
|
+
el.headerLabel = label;
|
|
191
|
+
el.setLabel(label);
|
|
192
|
+
}
|
|
186
193
|
return el;
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Right-aligned header text drawn on a pane's top border. Has to be a direct
|
|
198
|
+
* screen child: blessed refuses to lay out top:-1 children of scrollable
|
|
199
|
+
* panes (projectList/card/logBox are all scrollable), so a pane-relative
|
|
200
|
+
* label would never paint. Position is derived from the pane's own geometry.
|
|
201
|
+
* Returns { widget, refresh(labelText) } so callers can re-evaluate whether the
|
|
202
|
+
* header still fits (the OUTPUT pane's label grows when logs are paused).
|
|
203
|
+
*/
|
|
204
|
+
function paneHeaderRight(parent, content, fg = THEME.textDim) {
|
|
205
|
+
const pct = (v, fb) => {
|
|
206
|
+
const m = /^(\d+(?:\.\d+)?)%$/.exec(String(v));
|
|
207
|
+
return m ? parseFloat(m[1]) : fb;
|
|
208
|
+
};
|
|
209
|
+
const visible = String(content).replace(/\{[^{}]*\}/g, '').length;
|
|
210
|
+
const paneWidth = pct(parent.options.width, 0);
|
|
211
|
+
const position = `${(pct(parent.options.left, 0) + paneWidth).toFixed(2)}%-${visible + 1}`;
|
|
212
|
+
const widget = blessed.text({
|
|
213
|
+
parent: screen,
|
|
214
|
+
top: parent.options.top,
|
|
215
|
+
left: position,
|
|
216
|
+
height: 1,
|
|
217
|
+
tags: true,
|
|
218
|
+
content,
|
|
219
|
+
style: { fg, bg: 'transparent' },
|
|
220
|
+
});
|
|
221
|
+
widget.refresh = (labelText) => {
|
|
222
|
+
const labelLen = String(labelText != null ? labelText : parent.headerLabel || '').replace(/\{[^{}]*\}/g, '').length;
|
|
223
|
+
const px = Math.round((paneWidth / 100) * screen.width);
|
|
224
|
+
widget.hidden = visible + 1 > px - labelLen - 4;
|
|
225
|
+
};
|
|
226
|
+
widget.refresh();
|
|
227
|
+
return widget;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Two-strip header: a 3-row masthead (clock | TERMDECK box | daemon) above a
|
|
231
|
+
// 3-row chips strip (status chips | search box). updateHeader() is the only
|
|
232
|
+
// renderer that mutates these.
|
|
233
|
+
const TITLE_TEXT = 'T E R M D E C K';
|
|
234
|
+
const LOGO_GREEN = '#00ff00';
|
|
235
|
+
const HEADER_ROWS = 6;
|
|
236
|
+
const CHIP_STRIP_TOP = 3;
|
|
237
|
+
const CHIP_ORDER = ['all', 'live', 'exp', 'pend', 'unknown', 'scrap'];
|
|
238
|
+
const CHIP_LABELS = { all: 'ALL', live: 'LIVE', exp: 'EXP', pend: 'PEND', unknown: 'UNKNOWN', scrap: 'SCRAP' };
|
|
239
|
+
const CHIP_WIDTHS = { all: 8, live: 8, exp: 8, pend: 8, unknown: 12, scrap: 11 };
|
|
240
|
+
const CHIP_COLORS = { live: STATUS_FG.live, exp: STATUS_FG.exp, pend: STATUS_FG.pend, unknown: STATUS_FG.unknown, scrap: STATUS_FG.scrap };
|
|
241
|
+
|
|
242
|
+
/** `15:14:19` — 24-hour clock for the masthead (logs stay 12-hour). */
|
|
243
|
+
function h24Time(date = new Date()) {
|
|
244
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
245
|
+
return `${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const masthead = blessed.box({
|
|
199
249
|
parent: screen,
|
|
200
250
|
top: 0,
|
|
201
251
|
left: 0,
|
|
202
252
|
width: '100%',
|
|
203
|
-
height:
|
|
253
|
+
height: 3,
|
|
204
254
|
tags: true,
|
|
205
|
-
|
|
206
|
-
border: { type: 'line', fg: HEADER_ACCENT },
|
|
207
|
-
style: { fg: HEADER_ACCENT, bold: true },
|
|
255
|
+
style: { bg: THEME.bg, fg: THEME.text },
|
|
208
256
|
});
|
|
209
257
|
|
|
210
|
-
|
|
211
|
-
|
|
258
|
+
const clockLabel = blessed.text({
|
|
259
|
+
parent: masthead,
|
|
260
|
+
top: 1,
|
|
261
|
+
left: 1,
|
|
262
|
+
height: 1,
|
|
263
|
+
tags: true,
|
|
264
|
+
content: '',
|
|
265
|
+
style: { fg: THEME.textDim, bg: THEME.bg },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const titleBox = blessed.box({
|
|
269
|
+
parent: masthead,
|
|
270
|
+
top: 0,
|
|
271
|
+
left: 'center',
|
|
272
|
+
width: TITLE_TEXT.length + 2,
|
|
273
|
+
height: 3,
|
|
274
|
+
tags: true,
|
|
275
|
+
align: 'center',
|
|
276
|
+
valign: 'middle',
|
|
277
|
+
border: { type: 'line', fg: LOGO_GREEN },
|
|
278
|
+
style: { fg: LOGO_GREEN, bold: true, bg: THEME.bg },
|
|
279
|
+
content: TITLE_TEXT,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const daemonLabel = blessed.text({
|
|
283
|
+
parent: masthead,
|
|
284
|
+
top: 1,
|
|
285
|
+
right: 1,
|
|
286
|
+
height: 1,
|
|
287
|
+
tags: true,
|
|
288
|
+
content: '',
|
|
289
|
+
style: { bg: THEME.bg },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Row 2 of the header: bordered status chips at the left, bordered search box
|
|
293
|
+
// at the right. Chips are chunky 3-row boxes so the line border closes cleanly.
|
|
294
|
+
const chipStrip = blessed.box({
|
|
212
295
|
parent: screen,
|
|
213
|
-
top:
|
|
296
|
+
top: CHIP_STRIP_TOP,
|
|
214
297
|
left: 0,
|
|
215
298
|
width: '100%',
|
|
216
|
-
height:
|
|
299
|
+
height: 3,
|
|
217
300
|
tags: true,
|
|
218
301
|
style: { bg: THEME.bg, fg: THEME.text },
|
|
219
302
|
});
|
|
220
303
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
304
|
+
const searchBox = blessed.box({
|
|
305
|
+
parent: chipStrip,
|
|
306
|
+
top: 0,
|
|
307
|
+
right: 1,
|
|
308
|
+
width: Math.max(18, Math.min(26, Math.floor(screen.cols * 0.26))),
|
|
309
|
+
height: 3,
|
|
310
|
+
tags: true,
|
|
311
|
+
align: 'center',
|
|
312
|
+
valign: 'middle',
|
|
313
|
+
border: { type: 'line', fg: THEME.border },
|
|
314
|
+
style: { fg: THEME.textDim, bg: THEME.bg },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
let chipCursor = 0;
|
|
318
|
+
const chipBoxes = CHIP_ORDER.map((key) => {
|
|
319
|
+
const box = blessed.box({
|
|
320
|
+
parent: chipStrip,
|
|
321
|
+
top: 0,
|
|
322
|
+
left: chipCursor,
|
|
323
|
+
width: CHIP_WIDTHS[key],
|
|
324
|
+
height: 3,
|
|
325
|
+
tags: true,
|
|
326
|
+
align: 'center',
|
|
327
|
+
valign: 'middle',
|
|
328
|
+
border: { type: 'line', fg: THEME.border },
|
|
329
|
+
style: { fg: CHIP_COLORS[key] ? CHIP_COLORS[key] : THEME.text, bg: THEME.bg },
|
|
330
|
+
});
|
|
331
|
+
chipCursor += CHIP_WIDTHS[key] + 1;
|
|
332
|
+
return box;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const bodyTop = HEADER_ROWS; // masthead + chips strip
|
|
336
|
+
const footerHeight = 2; // bordered footer box
|
|
337
|
+
const bodyHeight = Math.max(6, screen.rows - bodyTop - footerHeight);
|
|
338
|
+
// Bordered buttons need 5 rows x 3 cells (15) + border(2) + label-row(0). When the
|
|
339
|
+
// window is too short (80x24 → body 16), the grid collapses to 1-line chips.
|
|
340
|
+
const large = bodyHeight >= 29;
|
|
341
|
+
const actionsHeight = large ? 18 : 8; // border(2) + grid(5x3 or 5x1)
|
|
342
|
+
const outputHeight = Math.max(3, Math.min(large ? 6 : 4, bodyHeight - actionsHeight - (large ? 8 : 5)));
|
|
343
|
+
const cardHeight = Math.max(3, bodyHeight - actionsHeight - outputHeight);
|
|
344
|
+
const logHeight = outputHeight;
|
|
224
345
|
|
|
225
346
|
const projectList = blessed.list({
|
|
226
347
|
parent: screen,
|
|
@@ -243,13 +364,10 @@ function launchDashboard(config, options = {}) {
|
|
|
243
364
|
},
|
|
244
365
|
});
|
|
245
366
|
panel(projectList, ' PROJECTS ');
|
|
367
|
+
paneHeaderRight(projectList, ' SORT: RECENT ');
|
|
246
368
|
|
|
247
369
|
const rightLeft = '40%';
|
|
248
370
|
const rightWidth = '60%';
|
|
249
|
-
const cardHeight = Math.max(3, Math.round(bodyHeight * 0.42));
|
|
250
|
-
// The ACTIONS pane must fit 4 button rows plus borders; OUTPUT gets the rest.
|
|
251
|
-
const actionsHeight = Math.max(6, Math.round(bodyHeight * 0.34));
|
|
252
|
-
const logHeight = Math.max(3, bodyHeight - cardHeight - actionsHeight);
|
|
253
371
|
|
|
254
372
|
const card = blessed.box({
|
|
255
373
|
parent: screen,
|
|
@@ -263,6 +381,7 @@ function launchDashboard(config, options = {}) {
|
|
|
263
381
|
border: { type: 'line', fg: THEME.border },
|
|
264
382
|
});
|
|
265
383
|
panel(card, ' DETAILS ');
|
|
384
|
+
const gitHeader = paneHeaderRight(card, '{#a6e3a1-fg}GIT: CLEAN{/#a6e3a1-fg}');
|
|
266
385
|
|
|
267
386
|
const actionsShell = blessed.box({
|
|
268
387
|
parent: screen,
|
|
@@ -273,30 +392,35 @@ function launchDashboard(config, options = {}) {
|
|
|
273
392
|
tags: true,
|
|
274
393
|
border: { type: 'line', fg: THEME.border },
|
|
275
394
|
});
|
|
276
|
-
panel(actionsShell, ' ACTIONS
|
|
395
|
+
panel(actionsShell, ' ACTIONS - r/e/c/x/o/f/k/s or [Enter] ');
|
|
396
|
+
paneHeaderRight(actionsShell, ' KEYMAP: VIM/CLI ');
|
|
277
397
|
|
|
278
|
-
const
|
|
398
|
+
const footerMeta = blessed.box({
|
|
279
399
|
parent: screen,
|
|
280
400
|
top: bodyTop + bodyHeight,
|
|
281
401
|
left: 0,
|
|
282
402
|
width: '100%',
|
|
283
|
-
height:
|
|
403
|
+
height: 1,
|
|
284
404
|
tags: true,
|
|
285
|
-
style: { fg: THEME.
|
|
405
|
+
style: { fg: THEME.textDim, bg: THEME.surface },
|
|
406
|
+
});
|
|
407
|
+
const footer = blessed.box({
|
|
408
|
+
parent: screen,
|
|
409
|
+
top: bodyTop + bodyHeight + 1,
|
|
410
|
+
left: 0,
|
|
411
|
+
width: '100%',
|
|
412
|
+
height: 1,
|
|
413
|
+
tags: true,
|
|
414
|
+
style: { fg: THEME.accentFg, bg: THEME.accentBg },
|
|
286
415
|
});
|
|
287
416
|
|
|
288
|
-
|
|
289
|
-
* One-line action button, nested inside the ACTIONS cell. `focusBg/focusFg`
|
|
290
|
-
* drive the focused/hover look; the status button is re-styled at render
|
|
291
|
-
* time to nudge the user while a project has no status.
|
|
292
|
-
*/
|
|
293
|
-
function makeButton({ content, fg, focusBg, focusFg, onPress }) {
|
|
417
|
+
function makeButton({ content, onPress }) {
|
|
294
418
|
const button = blessed.button({
|
|
295
419
|
parent: actionsShell,
|
|
296
420
|
top: '0%',
|
|
297
421
|
left: '0%',
|
|
298
422
|
width: '47%',
|
|
299
|
-
height:
|
|
423
|
+
height: large ? 3 : 1,
|
|
300
424
|
shrink: true,
|
|
301
425
|
content,
|
|
302
426
|
align: 'center',
|
|
@@ -305,57 +429,55 @@ function launchDashboard(config, options = {}) {
|
|
|
305
429
|
mouse: true,
|
|
306
430
|
clickable: true,
|
|
307
431
|
autoFocus: false,
|
|
432
|
+
padding: large ? { left: 1, right: 1 } : {},
|
|
433
|
+
border: large ? { type: 'line', fg: THEME.border } : undefined,
|
|
308
434
|
style: {
|
|
309
|
-
fg:
|
|
310
|
-
bg: THEME.
|
|
311
|
-
focus: { bg:
|
|
312
|
-
hover: { bg:
|
|
435
|
+
fg: THEME.text,
|
|
436
|
+
bg: THEME.chipBg,
|
|
437
|
+
focus: { bg: THEME.accentBg, fg: THEME.accentFg, bold: true },
|
|
438
|
+
hover: { bg: THEME.accentBg, fg: THEME.accentFg, bold: true },
|
|
439
|
+
border: large ? { fg: THEME.border } : undefined,
|
|
313
440
|
},
|
|
314
441
|
});
|
|
315
442
|
|
|
316
443
|
button.on('press', () => {
|
|
444
|
+
let handled = false;
|
|
317
445
|
try {
|
|
318
|
-
onPress();
|
|
446
|
+
handled = onPress() === true;
|
|
319
447
|
} catch (err) {
|
|
320
448
|
setStatus(`Error: ${err.message}`);
|
|
321
449
|
} finally {
|
|
322
|
-
|
|
323
|
-
|
|
450
|
+
if (!handled && !gitModal) {
|
|
451
|
+
projectList.focus();
|
|
452
|
+
screen.render();
|
|
453
|
+
}
|
|
324
454
|
}
|
|
325
455
|
});
|
|
326
456
|
return button;
|
|
327
457
|
}
|
|
328
458
|
|
|
329
459
|
const buttons = {};
|
|
330
|
-
const BUTTON_SLOTS = new Map();
|
|
331
460
|
|
|
332
|
-
/**
|
|
333
|
-
* 2-col x 4-row button grid nested inside the ACTIONS pane. Geometry is
|
|
334
|
-
* computed from the pane's exact row count so buttons never clip borders,
|
|
335
|
-
* even on an 80x24 terminal.
|
|
336
|
-
*/
|
|
337
461
|
function addButton(name, slot, content, opts) {
|
|
338
462
|
const button = makeButton({ content, ...opts });
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const rowH = Math.max(1, Math.floor(inner / 4));
|
|
342
|
-
const pad = Math.max(0, Math.floor((inner - 4 * rowH) / 2));
|
|
343
|
-
button.top = 1 + pad + slot.row * rowH;
|
|
463
|
+
const rowH = large ? 3 : 1;
|
|
464
|
+
button.top = 1 + slot.row * rowH;
|
|
344
465
|
button.height = rowH;
|
|
345
|
-
button.left = slot.col === 0 ? '2%' : '
|
|
346
|
-
button.width = '
|
|
466
|
+
button.left = slot.col === 0 ? '2%' : '51%';
|
|
467
|
+
button.width = '47%';
|
|
347
468
|
buttons[name] = button;
|
|
348
469
|
return button;
|
|
349
470
|
}
|
|
350
471
|
|
|
351
|
-
addButton('dev', { row: 0, col: 0 },
|
|
352
|
-
addButton('editor', { row: 0, col: 1 },
|
|
353
|
-
addButton('claude', { row: 1, col: 0 }, `{bold}[c]{/bold} ${AGENT_LABELS.claude}`, {
|
|
354
|
-
addButton('codex', { row: 1, col: 1 }, `{bold}[x]{/bold} ${AGENT_LABELS.codex}`, {
|
|
355
|
-
addButton('opencode', { row: 2, col: 0 }, `{bold}[o]{/bold} ${AGENT_LABELS.opencode}`, {
|
|
356
|
-
addButton('freebuff', { row: 2, col: 1 }, `{bold}[f]{/bold} ${AGENT_LABELS.freebuff}`, {
|
|
357
|
-
addButton('kilocode', { row: 3, col: 0 }, `{bold}[k]{/bold} ${AGENT_LABELS.kilocode}`, {
|
|
358
|
-
addButton('status', { row: 3, col: 1 },
|
|
472
|
+
addButton('dev', { row: 0, col: 0 }, `{bold}[r]{/bold} Run dev server`, { onPress: () => startDevServer() });
|
|
473
|
+
addButton('editor', { row: 0, col: 1 }, `{bold}[e]{/bold} Open in editor`, { onPress: () => openTool('editor') });
|
|
474
|
+
addButton('claude', { row: 1, col: 0 }, `{bold}[c]{/bold} ${AGENT_LABELS.claude}`, { onPress: () => openTool('claude') });
|
|
475
|
+
addButton('codex', { row: 1, col: 1 }, `{bold}[x]{/bold} ${AGENT_LABELS.codex}`, { onPress: () => openTool('codex') });
|
|
476
|
+
addButton('opencode', { row: 2, col: 0 }, `{bold}[o]{/bold} ${AGENT_LABELS.opencode}`, { onPress: () => openTool('opencode') });
|
|
477
|
+
addButton('freebuff', { row: 2, col: 1 }, `{bold}[f]{/bold} ${AGENT_LABELS.freebuff}`, { onPress: () => openTool('freebuff') });
|
|
478
|
+
addButton('kilocode', { row: 3, col: 0 }, `{bold}[k]{/bold} ${AGENT_LABELS.kilocode}`, { onPress: () => openTool('kilocode') });
|
|
479
|
+
addButton('status', { row: 3, col: 1 }, `{bold}[s]{/bold} Change status`, { onPress: () => cycleStatus() });
|
|
480
|
+
addButton('git', { row: 4, col: 0 }, `{bold}[g]{/bold} Git commit & push \u25b8 git`, { onPress: openGitCommitModal });
|
|
359
481
|
|
|
360
482
|
// Created after the buttons so tab-focus order is list -> actions -> output.
|
|
361
483
|
const logBox = contrib.log({
|
|
@@ -371,14 +493,16 @@ function launchDashboard(config, options = {}) {
|
|
|
371
493
|
border: { type: 'line', fg: THEME.border },
|
|
372
494
|
style: { bg: THEME.surface, item: { fg: THEME.text }, selected: { fg: THEME.text, bg: '#313244' } },
|
|
373
495
|
});
|
|
374
|
-
panel(logBox, ' OUTPUT (dev server / agents) ');
|
|
496
|
+
panel(logBox, ' OUTPUT (dev server / agents) [ON] ');
|
|
497
|
+
const bufferHeader = paneHeaderRight(logBox, ` BUFFER: 1024L {${STATUS_FG.live}-fg}STREAM ACTIVE{/${STATUS_FG.live}-fg} `);
|
|
375
498
|
|
|
376
499
|
const logView = new LogView(logBox, {
|
|
377
500
|
maxLines: 800,
|
|
378
501
|
flushInterval: 120,
|
|
379
502
|
viewportHeight: () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : screen.rows) - 2),
|
|
380
503
|
onChange: () => screen.render(),
|
|
381
|
-
|
|
504
|
+
onLabelChange: (label) => bufferHeader.refresh(label),
|
|
505
|
+
label: ' OUTPUT (dev server / agents) [ON] ',
|
|
382
506
|
});
|
|
383
507
|
|
|
384
508
|
/* ---------------------------------------------------------------- *
|
|
@@ -421,37 +545,39 @@ function launchDashboard(config, options = {}) {
|
|
|
421
545
|
return counts;
|
|
422
546
|
}
|
|
423
547
|
|
|
424
|
-
function
|
|
548
|
+
function chipData() {
|
|
425
549
|
const counts = modernCounts();
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
return
|
|
430
|
-
};
|
|
431
|
-
const allActive = status.chip === null;
|
|
432
|
-
return [
|
|
433
|
-
`{${THEME.text}-fg}${allActive ? '{bold}' : ''}[ALL ${projects.length}]${allActive ? '{/bold}' : ''}{/${THEME.text}-fg}`,
|
|
434
|
-
chip('LIVE', counts.live, STATUS_FG.live, 'live'),
|
|
435
|
-
chip('EXP', counts.exp, STATUS_FG.exp, 'exp'),
|
|
436
|
-
chip('PEND', counts.pend, STATUS_FG.pend, 'pend'),
|
|
437
|
-
chip('UNKNOWN', counts.unknown, STATUS_FG.unknown, 'unknown'),
|
|
438
|
-
chip('SCRAP', counts.scrap, STATUS_FG.scrap, 'scrap'),
|
|
439
|
-
].filter(Boolean).join(' ');
|
|
550
|
+
const activeKey = status.chip || 'all';
|
|
551
|
+
return CHIP_ORDER.map((key) => {
|
|
552
|
+
const count = key === 'all' ? projects.length : counts[key] || 0;
|
|
553
|
+
return { key, label: CHIP_LABELS[key], count, active: key === activeKey, hidden: key !== 'all' && count <= 0 };
|
|
554
|
+
});
|
|
440
555
|
}
|
|
441
556
|
|
|
442
557
|
function searchLabel() {
|
|
443
|
-
if (searchActive) return `/search: ${searchBuffer}`;
|
|
444
|
-
return status.search ? `/search: ${status.search}` : '/search (regex)';
|
|
558
|
+
if (searchActive) return `/search (regex): ${searchBuffer}`;
|
|
559
|
+
return status.search ? `/search (regex): ${status.search}` : '/search (regex)';
|
|
445
560
|
}
|
|
446
561
|
|
|
447
562
|
function updateHeader() {
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
563
|
+
clockLabel.setContent(` ${h24Time()}`);
|
|
564
|
+
const busy = servers.runningCount > 0 || activeTails() > 0;
|
|
565
|
+
daemonLabel.setContent(busy
|
|
566
|
+
? `{${STATUS_FG.live}-fg}\u25cf DAEMON ON{/${STATUS_FG.live}-fg}`
|
|
567
|
+
: `{${THEME.textDim}-fg}\u25cb DAEMON OFF{/${THEME.textDim}-fg}`);
|
|
568
|
+
|
|
569
|
+
chipData().forEach((chip, index) => {
|
|
570
|
+
const box = chipBoxes[index];
|
|
571
|
+
if (box.hidden === chip.hidden && box.content === `${chip.label} ${chip.count}` && box.active === chip.active) return;
|
|
572
|
+
box.hidden = chip.hidden;
|
|
573
|
+
box.setContent(`${chip.label} ${chip.count}`);
|
|
574
|
+
box.style.bg = chip.active ? THEME.accentBg : THEME.bg;
|
|
575
|
+
box.style.fg = chip.active ? THEME.accentFg : (CHIP_COLORS[chip.key] || THEME.text);
|
|
576
|
+
box.style.bold = chip.key === 'all';
|
|
577
|
+
box.active = chip.active;
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
searchBox.setContent(escapeBraces(searchLabel()));
|
|
455
581
|
}
|
|
456
582
|
|
|
457
583
|
/** Projects after the chip (status) + search (regex on name) filters. */
|
|
@@ -476,16 +602,16 @@ function launchDashboard(config, options = {}) {
|
|
|
476
602
|
|
|
477
603
|
function listItems() {
|
|
478
604
|
const inner = Math.max(12, Math.floor(screen.cols * 0.4) - 4);
|
|
479
|
-
const nameWidth = Math.max(6, inner - 12);
|
|
480
605
|
return filteredProjects().map((project) => {
|
|
481
606
|
const state = runStates.get(project.path);
|
|
482
607
|
const running = state && (state.status === 'running' || state.status === 'starting');
|
|
483
608
|
const modern = modernStatusOf(project);
|
|
484
609
|
const dotFg = running ? (state.status === 'running' ? STATUS_FG.live : STATUS_FG.exp) : statusFg(modern);
|
|
485
|
-
const name = escapeBraces(truncate(project.name,
|
|
610
|
+
const name = escapeBraces(truncate(project.name, Math.max(6, inner - 14)));
|
|
486
611
|
const info = gitInfo.get(project.path);
|
|
487
|
-
const activity = escapeBraces(truncate(project.lastActivity || timeAgo(info && info.lastCommitAt) || '\u2014', 10));
|
|
488
|
-
|
|
612
|
+
const activity = escapeBraces(String(truncate(project.lastActivity || timeAgo(info && info.lastCommitAt) || '\u2014', 10)));
|
|
613
|
+
const pads = ' '.repeat(Math.max(0, inner - name.length - activity.length - 3));
|
|
614
|
+
return `{${dotFg}-fg}\u25cf{/${dotFg}-fg} ${name}${pads} {${THEME.textDim}-fg}${activity}{/${THEME.textDim}-fg}`;
|
|
489
615
|
});
|
|
490
616
|
}
|
|
491
617
|
|
|
@@ -517,24 +643,19 @@ function launchDashboard(config, options = {}) {
|
|
|
517
643
|
const modern = modernStatusOf(project);
|
|
518
644
|
const sFg = statusFg(modern);
|
|
519
645
|
const inner = Math.max(24, Math.floor(screen.cols * 0.6) - 4);
|
|
520
|
-
const runPid = state && state.pid ? state.pid : null;
|
|
521
646
|
const info = gitInfo.get(project.path);
|
|
522
647
|
const stats = processStats.get(project.path);
|
|
523
|
-
const pidLabel = runPid
|
|
524
|
-
? String(runPid)
|
|
525
|
-
: stats && stats.pid
|
|
526
|
-
? String(stats.pid)
|
|
527
|
-
: config.demoMode && project.port
|
|
528
|
-
? `${DEMO_PID} {${THEME.textDim}-fg}(demo){/${THEME.textDim}-fg}`
|
|
529
|
-
: '\u2014';
|
|
530
648
|
const memCpu = stats && stats.memory
|
|
531
649
|
? `${escapeBraces(stats.memory)} \u00b7 ${escapeBraces(stats.cpu || '\u2014')}`
|
|
532
650
|
: config.demoMode
|
|
533
651
|
? `213.4 MB \u00b7 0.8% {${THEME.textDim}-fg}(demo){/${THEME.textDim}-fg}`
|
|
534
652
|
: '\u2014';
|
|
535
653
|
const dirty = info && info.dirty ? info.dirty : { added: 0, removed: 0 };
|
|
536
|
-
const dirtyLabel = dirty.added
|
|
537
|
-
? ` {
|
|
654
|
+
const dirtyLabel = dirty.added
|
|
655
|
+
? ` {${STATUS_FG.live}-fg}+${dirty.added}{/${STATUS_FG.live}-fg}`
|
|
656
|
+
: '';
|
|
657
|
+
const dirtyLabel2 = dirty.removed
|
|
658
|
+
? ` {#f38ba8-fg}-${dirty.removed}{/#f38ba8-fg}`
|
|
538
659
|
: '';
|
|
539
660
|
const branch = (info && info.branch) || project.branch || '\u2014';
|
|
540
661
|
const hash = (info && info.commitHash) || (project.lastCommit && project.lastCommit.hash) || '';
|
|
@@ -550,21 +671,34 @@ function launchDashboard(config, options = {}) {
|
|
|
550
671
|
? `{${STATUS_FG.unknown}-fg}{bold}[?] Unknown{/bold}{/${STATUS_FG.unknown}-fg}`
|
|
551
672
|
: `{${sFg}-fg}{bold}[${modern.toUpperCase()}]{/bold} ${fullLabel}{/${sFg}-fg}`;
|
|
552
673
|
|
|
674
|
+
// Escapes plain values for blessed markup; set `raw` for values that already
|
|
675
|
+
// carry blessed tags (branch colors, mem/cpu demo suffix).
|
|
676
|
+
const row = (label, value, raw) => {
|
|
677
|
+
const v = truncate(value, Math.max(4, inner - label.length - 2));
|
|
678
|
+
return ` {${THEME.textDim}-fg}${label}{/${THEME.textDim}-fg} ${raw ? v : escapeBraces(v)}`;
|
|
679
|
+
};
|
|
680
|
+
const selector = `{${STATUS_FG.live}-fg}${escapeBraces(truncate(branch, 30))}{/${STATUS_FG.live}-fg}`;
|
|
681
|
+
|
|
553
682
|
const lines = [
|
|
554
|
-
` {${sFg}-fg}
|
|
683
|
+
` {${sFg}-fg}\u25cf{/${sFg}-fg} {bold}${escapeBraces(truncate(project.name, 40))}{/bold}`,
|
|
555
684
|
` {${THEME.textDim}-fg}${escapeBraces(truncate(project.info || '(no description)', inner - 2))}{/${THEME.textDim}-fg}`,
|
|
556
|
-
` {${THEME.textDim}-fg}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
685
|
+
` {${THEME.textDim}-fg}Status:{/${THEME.textDim}-fg} ${statusChip}`,
|
|
686
|
+
row('Path:', displayPath(project.path, config.root)),
|
|
687
|
+
row('Branch:', `${selector}${dirtyLabel}${dirtyLabel2}`, true),
|
|
688
|
+
row('Package mgr:', String(project.packageManager || '\u2014')),
|
|
689
|
+
row('Stack:', stackLabel(project)),
|
|
690
|
+
row('Mem/CPU:', memCpu, true),
|
|
691
|
+
row('Last commit:', commit),
|
|
563
692
|
devStateLine(project, state),
|
|
564
693
|
];
|
|
565
694
|
|
|
566
695
|
card.setContent(lines.join('\n'));
|
|
567
696
|
card.setLabel(` DETAILS: ${project.name} `);
|
|
697
|
+
const dirtyNow = Boolean(dirty && (dirty.added || dirty.removed));
|
|
698
|
+
gitHeader.setContent(dirtyNow
|
|
699
|
+
? `{#f38ba8-fg}GIT: DIRTY{/#f38ba8-fg}`
|
|
700
|
+
: `{#a6e3a1-fg}GIT: CLEAN{/#a6e3a1-fg}`);
|
|
701
|
+
card.headerLabel = ` DETAILS: ${project.name} `;
|
|
568
702
|
|
|
569
703
|
// Visual cue while the status is unknown: red button asking to be set.
|
|
570
704
|
if (unknown) {
|
|
@@ -580,18 +714,38 @@ function launchDashboard(config, options = {}) {
|
|
|
580
714
|
screen.render();
|
|
581
715
|
}
|
|
582
716
|
|
|
583
|
-
function
|
|
717
|
+
function footerHints() {
|
|
718
|
+
if (screen.cols >= 110) return FOOTER_KEYS;
|
|
719
|
+
if (screen.cols >= 90) return '{bold}\u2191\u2193{/bold} navigate {bold}s{/bold} status {bold}r{/bold} dev {bold}shift+x{/bold} stop {bold}q{/bold} quit';
|
|
720
|
+
return '{bold}\u2191\u2193{/bold} navigate {bold}s{/bold} status {bold}r{/bold} dev {bold}q{/bold} quit';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function buildFooterRow1() {
|
|
724
|
+
const showing = ` SHOWING ${filteredProjects().length} OF ${projects.length} `;
|
|
725
|
+
const press = 'PRESS [/] FILTER';
|
|
726
|
+
const centerAt = Math.floor(screen.cols / 2) - Math.floor(press.length / 2);
|
|
727
|
+
const pad = Math.max(0, centerAt - showing.length);
|
|
728
|
+
const row = `${showing}${' '.repeat(pad)}${press}`.slice(0, screen.cols);
|
|
729
|
+
return row;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function buildFooterRow2() {
|
|
584
733
|
const sel = selectedProject();
|
|
585
734
|
const index = sel ? filteredProjects().indexOf(sel) + 1 : 0;
|
|
586
735
|
const paneLabel = currentPane();
|
|
587
736
|
const size = `${screen.cols}x${screen.rows}`;
|
|
588
737
|
const chipLabel = status.chip ? status.chip.toUpperCase() : 'ALL';
|
|
589
738
|
const search = status.search ? ` /${status.search}` : '';
|
|
590
|
-
|
|
739
|
+
const fixed = `[${index}/${filteredProjects().length}] SELECTED FILTER: ${chipLabel}${search}`;
|
|
740
|
+
const hints = ` ${footerHints()} `;
|
|
741
|
+
const tail = ` PANE: [${paneLabel}] \u2502 ${size} `;
|
|
742
|
+
const row = ` ${fixed}${hints}${tail}`.slice(0, screen.cols);
|
|
743
|
+
return row;
|
|
591
744
|
}
|
|
592
745
|
|
|
593
746
|
function updateFooter() {
|
|
594
|
-
|
|
747
|
+
footerMeta.setContent(buildFooterRow1());
|
|
748
|
+
footer.setContent(buildFooterRow2());
|
|
595
749
|
}
|
|
596
750
|
|
|
597
751
|
function currentPane() {
|
|
@@ -622,6 +776,7 @@ function launchDashboard(config, options = {}) {
|
|
|
622
776
|
function setStatus(message) {
|
|
623
777
|
if (status.timer) clearTimeout(status.timer);
|
|
624
778
|
status.message = message;
|
|
779
|
+
footerMeta.setContent(buildFooterRow1());
|
|
625
780
|
footer.setContent(` {bold}${escapeBraces(message)}{/bold}`);
|
|
626
781
|
screen.render();
|
|
627
782
|
|
|
@@ -635,13 +790,18 @@ function launchDashboard(config, options = {}) {
|
|
|
635
790
|
|
|
636
791
|
/** Log lines from child processes are raw text -> escape blessed markup. */
|
|
637
792
|
function appendLog(project, line, stream = 'stdout') {
|
|
638
|
-
const
|
|
639
|
-
const prefix = `{${THEME.textDim}-fg}${timestamp()}{/${THEME.textDim}-fg} {${fg}-fg}${escapeBraces(truncate(project.name, 10))}{/${fg}-fg}`;
|
|
793
|
+
const time = `{${THEME.textDim}-fg}${timestamp()}{/${THEME.textDim}-fg}`;
|
|
640
794
|
if (stream === 'system') {
|
|
641
|
-
|
|
795
|
+
// Give generic notes a cyan [termdeck] tag; lines that already carry their
|
|
796
|
+
// own [tag] keep it.
|
|
797
|
+
const visible = String(line).replace(/^(?:\{[^{}]*\})*/, '').replace(/^\s+/, '');
|
|
798
|
+
const tag = visible.startsWith('[') ? '' : `{${THEME.tagCyan}-fg}[termdeck]{/${THEME.tagCyan}-fg} `;
|
|
799
|
+
logView.push(`${time} ${tag}${line}`);
|
|
642
800
|
return;
|
|
643
801
|
}
|
|
644
|
-
const
|
|
802
|
+
const fg = colorFor(project);
|
|
803
|
+
const prefix = `{${THEME.textDim}-fg}${timestamp()}{/${THEME.textDim}-fg} ${escapeBraces(truncate(project.name, 10))}`;
|
|
804
|
+
const marker = stream === 'stderr' ? '{#f38ba8-fg}\u2717{/#f38ba8-fg} ' : '';
|
|
645
805
|
logView.push(`${prefix} ${marker}${escapeBraces(line)}`);
|
|
646
806
|
}
|
|
647
807
|
|
|
@@ -649,6 +809,187 @@ function launchDashboard(config, options = {}) {
|
|
|
649
809
|
* Actions
|
|
650
810
|
* ---------------------------------------------------------------- */
|
|
651
811
|
|
|
812
|
+
function getProjectStack(project) {
|
|
813
|
+
if (!project) return '\u2014';
|
|
814
|
+
if (project.stack) return project.stack;
|
|
815
|
+
if (config.demoMode) return '\u2014';
|
|
816
|
+
if (!stackInfo.has(project.path)) stackInfo.set(project.path, detectStack(project.path));
|
|
817
|
+
return stackInfo.get(project.path) || '\u2014';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Stack shown as comma-separated names, even when config used dashes. Plain
|
|
821
|
+
* audit text — the row() helper escapes it for blessed markup. */
|
|
822
|
+
function stackLabel(project) {
|
|
823
|
+
return String(getProjectStack(project) || '\u2014').replace(/\s*-\s*/g, ', ');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function refreshProjectStack(project) {
|
|
827
|
+
if (!project || config.demoMode || project.stack) return;
|
|
828
|
+
const stack = detectStack(project.path);
|
|
829
|
+
stackInfo.set(project.path, stack);
|
|
830
|
+
if (selectedProject() === project) updateCard();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function openGitCommitModal() {
|
|
834
|
+
const project = selectedProject();
|
|
835
|
+
if (!project) return false;
|
|
836
|
+
if (gitModal) {
|
|
837
|
+
gitModal.textbox.focus();
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
let generated;
|
|
842
|
+
try {
|
|
843
|
+
generated = generateCommitMessage(project.path);
|
|
844
|
+
} catch (_) {
|
|
845
|
+
generated = 'Update project files';
|
|
846
|
+
}
|
|
847
|
+
if (generated === null) {
|
|
848
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}⚠ No changes to commit.{/${STATUS_FG.exp}-fg}`, 'system');
|
|
849
|
+
setStatus(`${project.name}: No changes to commit.`);
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const width = Math.max(32, Math.min(72, screen.cols - 4));
|
|
854
|
+
const height = Math.max(10, Math.min(14, screen.rows - 4));
|
|
855
|
+
const modal = blessed.box({
|
|
856
|
+
parent: screen,
|
|
857
|
+
top: 'center',
|
|
858
|
+
left: 'center',
|
|
859
|
+
width,
|
|
860
|
+
height,
|
|
861
|
+
tags: true,
|
|
862
|
+
keys: true,
|
|
863
|
+
border: { type: 'line', fg: THEME.border },
|
|
864
|
+
style: { bg: THEME.surface, fg: THEME.text },
|
|
865
|
+
});
|
|
866
|
+
modal.setLabel(' Git Commit Message ');
|
|
867
|
+
blessed.text({
|
|
868
|
+
parent: modal,
|
|
869
|
+
top: 1,
|
|
870
|
+
left: 2,
|
|
871
|
+
width: width - 4,
|
|
872
|
+
height: 1,
|
|
873
|
+
tags: true,
|
|
874
|
+
content: 'Press Enter to commit & push, Esc to cancel',
|
|
875
|
+
style: { fg: THEME.textDim },
|
|
876
|
+
});
|
|
877
|
+
const textbox = blessed.textbox({
|
|
878
|
+
parent: modal,
|
|
879
|
+
top: 3,
|
|
880
|
+
left: 2,
|
|
881
|
+
width: width - 4,
|
|
882
|
+
height: Math.max(3, height - 7),
|
|
883
|
+
keys: true,
|
|
884
|
+
mouse: true,
|
|
885
|
+
value: generated || 'Update project files',
|
|
886
|
+
style: {
|
|
887
|
+
fg: THEME.text,
|
|
888
|
+
bg: THEME.surface,
|
|
889
|
+
focus: { fg: THEME.text, bg: '#282838' },
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
const hint = blessed.text({
|
|
893
|
+
parent: modal,
|
|
894
|
+
bottom: 1,
|
|
895
|
+
left: 2,
|
|
896
|
+
width: width - 4,
|
|
897
|
+
height: 1,
|
|
898
|
+
tags: true,
|
|
899
|
+
content: "Press 'a' to auto-generate, or edit manually. Enter to commit, Esc to cancel.",
|
|
900
|
+
style: { fg: THEME.textDim },
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const originalListener = textbox._listener;
|
|
904
|
+
textbox._listener = function(ch, key) {
|
|
905
|
+
if ((key.name === 'a' || key.name === 'A') && !key.ctrl && !key.meta) {
|
|
906
|
+
let next;
|
|
907
|
+
try {
|
|
908
|
+
next = generateCommitMessage(project.path);
|
|
909
|
+
} catch (_) {
|
|
910
|
+
next = 'Update project files';
|
|
911
|
+
}
|
|
912
|
+
if (next === null) {
|
|
913
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}⚠ No changes to commit.{/${STATUS_FG.exp}-fg}`, 'system');
|
|
914
|
+
hint.setContent("Press 'a' to auto-generate, or edit manually. Enter to commit, Esc to cancel.");
|
|
915
|
+
} else {
|
|
916
|
+
textbox.setValue(next || 'Update project files');
|
|
917
|
+
hint.setContent("Auto-generated. Press 'a' to regenerate, or edit manually. Enter to commit, Esc to cancel.");
|
|
918
|
+
}
|
|
919
|
+
screen.render();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
return originalListener.call(this, ch, key);
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
function closeGitModal() {
|
|
926
|
+
if (!gitModal) return;
|
|
927
|
+
gitModal = null;
|
|
928
|
+
try { modal.destroy(); } catch (_) {}
|
|
929
|
+
try { projectList.focus(); } catch (_) {}
|
|
930
|
+
try { screen.render(); } catch (_) {}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function finish(value) {
|
|
934
|
+
closeGitModal();
|
|
935
|
+
if (value == null) {
|
|
936
|
+
setStatus(`${project.name}: Git commit cancelled.`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const message = String(value).trim();
|
|
940
|
+
if (!message) {
|
|
941
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}⚠ Commit message cannot be empty.{/${STATUS_FG.exp}-fg}`, 'system');
|
|
942
|
+
setStatus(`${project.name}: Commit message cannot be empty.`);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
setImmediate(() => runGitCommit(project, message));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
textbox.on('submit', finish);
|
|
949
|
+
textbox.on('cancel', () => finish(null));
|
|
950
|
+
gitModal = { modal, textbox, close: closeGitModal };
|
|
951
|
+
textbox.readInput();
|
|
952
|
+
screen.render();
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function runGitCommit(project, message) {
|
|
957
|
+
logView.followTail();
|
|
958
|
+
appendLog(project, `{${STATUS_FG.live}-fg}[git]{/${STATUS_FG.live}-fg} committing and pushing…`, 'system');
|
|
959
|
+
let result;
|
|
960
|
+
try {
|
|
961
|
+
result = commitAndPush(project.path, message, {
|
|
962
|
+
onOutput: (line, stream) => appendLog(project, line, stream),
|
|
963
|
+
});
|
|
964
|
+
} catch (err) {
|
|
965
|
+
appendLog(project, `{#f38ba8-fg}✗ Git operation failed: ${escapeBraces(err.message)}{/#f38ba8-fg}`, 'system');
|
|
966
|
+
setStatus(`${project.name}: Git operation failed: ${err.message}`);
|
|
967
|
+
refreshProjectGit(project, { force: true });
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (result.warning) {
|
|
972
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}⚠ ${escapeBraces(result.warning)}{/${STATUS_FG.exp}-fg}`, 'system');
|
|
973
|
+
setStatus(`${project.name}: ${result.warning}`);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (!result.ok) {
|
|
977
|
+
appendLog(project, `{#f38ba8-fg}✗ ${escapeBraces(result.error)}{/#f38ba8-fg}`, 'system');
|
|
978
|
+
setStatus(`${project.name}: ${result.error}`);
|
|
979
|
+
refreshProjectGit(project, { force: true });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const fileCount = result.fileCount || 1;
|
|
984
|
+
appendLog(
|
|
985
|
+
project,
|
|
986
|
+
`{${STATUS_FG.live}-fg}✓ [git] Committed and pushed ${fileCount} files: "${escapeBraces(result.message)}"{/${STATUS_FG.live}-fg}`,
|
|
987
|
+
'system'
|
|
988
|
+
);
|
|
989
|
+
setStatus(`${project.name}: committed and pushed.`);
|
|
990
|
+
refreshProjectGit(project, { force: true });
|
|
991
|
+
}
|
|
992
|
+
|
|
652
993
|
function startDevServer() {
|
|
653
994
|
const project = selectedProject();
|
|
654
995
|
if (!project) return;
|
|
@@ -711,7 +1052,7 @@ function launchDashboard(config, options = {}) {
|
|
|
711
1052
|
|
|
712
1053
|
// Agent: launch in a new terminal with log capture via tee where possible.
|
|
713
1054
|
const agentName = AGENT_LABELS[kind] || kind;
|
|
714
|
-
appendLog(project, `{
|
|
1055
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}[${escapeBraces(kind)}]{/${STATUS_FG.exp}-fg} launching ${escapeBraces(agentName)} in a new terminal`, 'system');
|
|
715
1056
|
setStatus(`Launching ${agentName} for ${project.name}\u2026`);
|
|
716
1057
|
const result = await launchAgent(project, kind);
|
|
717
1058
|
if (result.ok) {
|
|
@@ -736,7 +1077,7 @@ function launchDashboard(config, options = {}) {
|
|
|
736
1077
|
const index = MODERN_STATUSES.indexOf(current);
|
|
737
1078
|
const next = MODERN_STATUSES[(index + 1) % MODERN_STATUSES.length];
|
|
738
1079
|
project.status = next;
|
|
739
|
-
appendLog(project, `{
|
|
1080
|
+
appendLog(project, `{${THEME.tagCyan}-fg}[termdeck]{/${THEME.tagCyan}-fg} status changed to {bold}${next}{/bold}`, 'system');
|
|
740
1081
|
setStatus(`${project.name}: status \u2192 ${next}`);
|
|
741
1082
|
if (!config.demoMode) {
|
|
742
1083
|
try { saveConfig(config); } catch (_) { /* best effort */ }
|
|
@@ -771,6 +1112,7 @@ function launchDashboard(config, options = {}) {
|
|
|
771
1112
|
}
|
|
772
1113
|
|
|
773
1114
|
function destroy() {
|
|
1115
|
+
if (gitModal) gitModal.close();
|
|
774
1116
|
if (status.timer) clearTimeout(status.timer);
|
|
775
1117
|
if (status.quitTimer) clearTimeout(status.quitTimer);
|
|
776
1118
|
if (status.clock) clearInterval(status.clock);
|
|
@@ -807,9 +1149,10 @@ function launchDashboard(config, options = {}) {
|
|
|
807
1149
|
* Git / process-monitor refresh on selection
|
|
808
1150
|
* ---------------------------------------------------------------- */
|
|
809
1151
|
|
|
810
|
-
function refreshProjectGit(project) {
|
|
1152
|
+
function refreshProjectGit(project, { force = false } = {}) {
|
|
811
1153
|
if (!project || config.demoMode) return;
|
|
812
|
-
|
|
1154
|
+
refreshProjectStack(project);
|
|
1155
|
+
const info = getGitInfo(project.path, { force });
|
|
813
1156
|
gitInfo.set(project.path, info);
|
|
814
1157
|
if (selectedProject() === project) {
|
|
815
1158
|
refreshList();
|
|
@@ -868,6 +1211,7 @@ function launchDashboard(config, options = {}) {
|
|
|
868
1211
|
screen.key(['o'], () => { if (!searchActive) openTool('opencode'); });
|
|
869
1212
|
screen.key(['f'], () => { if (!searchActive) openTool('freebuff'); });
|
|
870
1213
|
screen.key(['k'], () => { if (!searchActive) openTool('kilocode'); });
|
|
1214
|
+
screen.key(['g'], () => { if (!searchActive) openGitCommitModal(); });
|
|
871
1215
|
screen.key(['S-x'], () => { if (!searchActive) stopDevServer(); });
|
|
872
1216
|
screen.key(['j'], () => { if (!searchActive) projectList.down(1); });
|
|
873
1217
|
screen.key(['tab'], () => { if (!searchActive) screen.focusNext(); });
|
|
@@ -967,7 +1311,7 @@ function launchDashboard(config, options = {}) {
|
|
|
967
1311
|
projectList.focus();
|
|
968
1312
|
refreshList();
|
|
969
1313
|
updateCard();
|
|
970
|
-
appendLog({ name: 'termdeck', path: '__termdeck__' }, `{bold}termdeck{/bold} ready
|
|
1314
|
+
appendLog({ name: 'termdeck', path: '__termdeck__' }, `{bold}termdeck{/bold} ready - ${projects.length} projects discovered from ${escapeBraces(displayPath(config.root, config.root))}`, 'system');
|
|
971
1315
|
appendLog({ name: 'termdeck', path: '__termdeck__' }, `pick a project and press {bold}r{/bold} for the dev server, {bold}e{/bold} for your editor, {bold}c/x/o/f/k{/bold} for an agent.`, 'system');
|
|
972
1316
|
if (config.demoMode) {
|
|
973
1317
|
const demoProject = projects[0] || { name: 'hyperion-core', path: 'demo' };
|
|
@@ -1013,7 +1357,7 @@ function launchDashboard(config, options = {}) {
|
|
|
1013
1357
|
|
|
1014
1358
|
return {
|
|
1015
1359
|
screen,
|
|
1016
|
-
widgets: { header: titleBox, title: titleBox, statsBar, projectList, card, logBox, footer, buttons },
|
|
1360
|
+
widgets: { header: titleBox, title: titleBox, statsBar: chipStrip, projectList, card, logBox, footer, buttons },
|
|
1017
1361
|
servers,
|
|
1018
1362
|
logView,
|
|
1019
1363
|
runStates,
|