termdeck-cli 2.0.3 → 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 +693 -167
- package/src/logView.js +3 -0
- package/src/projectManager.js +331 -12
- package/src/util.js +32 -3
package/src/dashboard.js
CHANGED
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* The termdeck TUI.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Compact "email client" layout — hand-positioned widgets so every pane is
|
|
7
|
+
* exactly the rows it needs, with zero gaps between panes:
|
|
7
8
|
*
|
|
8
9
|
* +-------------------------------------------------------------------+
|
|
9
|
-
* |
|
|
10
|
+
* | 15:14:19 +------------+ ● DAEMON ON |
|
|
11
|
+
* | | TERMDECK | |
|
|
12
|
+
* | +------------+ |
|
|
13
|
+
* | [ALL 14] [LIVE 6] [EXP 4] … /search (regex) [box] |
|
|
10
14
|
* +---------------------------------+---------------------------------+
|
|
11
|
-
* | PROJECTS (14
|
|
12
|
-
* | ● hyperion-core
|
|
13
|
-
* | ● atlas-engine
|
|
14
|
-
* | …
|
|
15
|
+
* | PROJECTS (14) | DETAILS: hyperion-core |
|
|
16
|
+
* | ● hyperion-core 12m ago | status / path / branch / port |
|
|
17
|
+
* | ● atlas-engine 2h ago +---------------------------------+
|
|
18
|
+
* | … | ACTIONS [c] Claude Code … |
|
|
15
19
|
* | +---------------------------------+
|
|
16
|
-
* | | OUTPUT (dev server / agents)
|
|
17
|
-
* | | 21:04:12 ✓ vite ready 3000
|
|
20
|
+
* | | OUTPUT (dev server / agents) |
|
|
21
|
+
* | | 21:04:12 ✓ vite ready 3000 |
|
|
18
22
|
* +---------------------------------+---------------------------------+
|
|
19
|
-
* |
|
|
23
|
+
* | [1/14] SELECTED · keys · PANE: [PROJECTS] |
|
|
20
24
|
* +-------------------------------------------------------------------+
|
|
21
25
|
*
|
|
22
26
|
* The controller object returned by launchDashboard() keeps the same shape
|
|
@@ -26,23 +30,58 @@
|
|
|
26
30
|
* inside the ACTIONS cell but stay real blessed buttons (mouse + tab focus).
|
|
27
31
|
*/
|
|
28
32
|
|
|
29
|
-
const path = require('path');
|
|
30
|
-
|
|
31
|
-
const blessed = require('blessed');
|
|
33
|
+
const path = require('path');const blessed = require('blessed');
|
|
32
34
|
const contrib = require('blessed-contrib');
|
|
33
35
|
|
|
34
36
|
const { DevServerManager } = require('./devServer');
|
|
35
37
|
const { openInNewTerminal } = require('./terminal');
|
|
36
38
|
const { LogView } = require('./logView');
|
|
37
|
-
const {
|
|
38
|
-
const { escapeBraces, truncate, timestamp, timeAgo } = require('./util');
|
|
39
|
-
const { getGitInfo } = require('./projectManager');
|
|
40
|
-
const {
|
|
39
|
+
const { MODERN_STATUSES, loadConfig, loadConfigFromPath, displayPath, saveConfig } = require('./config');
|
|
40
|
+
const { escapeBraces, truncate, formatTimestamp, timestamp, timeAgo } = require('./util');
|
|
41
|
+
const { getGitInfo, detectStack, generateCommitMessage, commitAndPush } = require('./projectManager');
|
|
42
|
+
const { launchAgent, tailAgentLog, stopAllAgents, activeTails } = require('./agentManager');
|
|
41
43
|
const { startMonitoring, stopMonitoring, stopAllMonitoring } = require('./processMonitor');
|
|
42
44
|
|
|
43
|
-
const LAYOUT = { rows: 12, cols: 12, headerHeight:
|
|
45
|
+
const LAYOUT = { rows: 12, cols: 12, headerHeight: 6, footerHeight: 2 };
|
|
46
|
+
|
|
47
|
+
/* ------------------------------------------------------------------ *
|
|
48
|
+
* Theme — dark, modern palette, pastel status tags, thin borders.
|
|
49
|
+
* ------------------------------------------------------------------ */
|
|
50
|
+
|
|
51
|
+
const THEME = {
|
|
52
|
+
bg: '#1e1e2e', // base background (main screen + boxes)
|
|
53
|
+
surface: '#181825', // slightly darker panels (output, footer)
|
|
54
|
+
text: '#cdd6f4', // general text (light gray-white)
|
|
55
|
+
textDim: '#9399b2', // secondary text (timestamps, labels)
|
|
56
|
+
tagCyan: '#94e2d5', // log [termdeck] source tag
|
|
57
|
+
border: '#45475a', // thin, unobtrusive box borders
|
|
58
|
+
accentBg: '#3b82f6', // selected-project / focused-button highlight
|
|
59
|
+
accentFg: '#ffffff',
|
|
60
|
+
chipBg: '#2d2d3f', // action-button / stat-chip background
|
|
61
|
+
};
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
/** Pastel status colours per modern status; `unknown` is neutral gray. */
|
|
64
|
+
const STATUS_FG = {
|
|
65
|
+
live: '#a6e3a1', // soft green
|
|
66
|
+
exp: '#f9e2af', // soft yellow/orange
|
|
67
|
+
pend: '#89b4fa', // soft blue
|
|
68
|
+
scrap: '#6c7086', // soft gray
|
|
69
|
+
unknown: '#6c7086', // neutral gray for missing statuses
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Full uppercase words shown in the DETAILS pane. */
|
|
73
|
+
const STATUS_FULL = { live: 'LIVE', exp: 'EXPERIMENTAL', pend: 'PENDING', scrap: 'SCRAP' };
|
|
74
|
+
|
|
75
|
+
/** Agent buttons name the agent explicitly instead of a bare key hint. */
|
|
76
|
+
const AGENT_LABELS = {
|
|
77
|
+
claude: 'Claude Code',
|
|
78
|
+
codex: 'Codex',
|
|
79
|
+
opencode: 'OpenCode',
|
|
80
|
+
freebuff: 'Freebuff',
|
|
81
|
+
kilocode: 'Kilocode',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const PROJECT_COLORS = ['#89b4fa', '#a6e3a1', '#f9e2af', '#f5c2e7', '#f38ba8', '#94e2d5'];
|
|
46
85
|
|
|
47
86
|
/** Legacy status -> short modern label, used for dots, chips and cycling. */
|
|
48
87
|
const MODERN_OF = {
|
|
@@ -56,16 +95,11 @@ const MODERN_OF = {
|
|
|
56
95
|
scrap: 'scrap',
|
|
57
96
|
};
|
|
58
97
|
|
|
59
|
-
/** Dot / chip colour per modern status (design spec). */
|
|
60
|
-
const DOT_COLORS = { live: 'green', exp: 'yellow', pend: 'blue', scrap: 'gray' };
|
|
61
|
-
|
|
62
98
|
const DEMO_PID = 49201;
|
|
63
99
|
const SAMPLE_CONFIG_PATH = path.join(__dirname, '..', 'sample-config.json');
|
|
64
100
|
|
|
65
|
-
const DEFAULT_AGENT_COMMANDS = { claude: 'claude', codex: 'codex', opencode: 'opencode', freebuff: 'freebuff', kilocode: 'kilocode' };
|
|
66
|
-
|
|
67
101
|
const FOOTER_KEYS =
|
|
68
|
-
'{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';
|
|
69
103
|
const HINTS = ` ${FOOTER_KEYS} `;
|
|
70
104
|
|
|
71
105
|
/** Colour-coded demo log lines so the OUTPUT pane styling can be checked. */
|
|
@@ -81,6 +115,22 @@ const SAMPLE_LOG_LINES = [
|
|
|
81
115
|
{ stream: 'system', line: '{green-fg}✓{/green-fg} cache flush recovered after retry' },
|
|
82
116
|
];
|
|
83
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Modern short status for a project, or `null` when the status is missing or
|
|
120
|
+
* unrecognised (rendered as "[?] Unknown" instead of guessing).
|
|
121
|
+
*/
|
|
122
|
+
function modernStatusOf(project) {
|
|
123
|
+
if (!project) return null;
|
|
124
|
+
const raw = project.status;
|
|
125
|
+
if (raw === null || raw === undefined || raw === '') return null;
|
|
126
|
+
if (String(raw).toLowerCase() === 'unknown') return null;
|
|
127
|
+
return MODERN_OF[raw] || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function statusFg(modern) {
|
|
131
|
+
return STATUS_FG[modern] || STATUS_FG.unknown;
|
|
132
|
+
}
|
|
133
|
+
|
|
84
134
|
/**
|
|
85
135
|
* @param {object} config parsed config (real or the shipped demo dataset)
|
|
86
136
|
* @param {object} [options]
|
|
@@ -105,12 +155,14 @@ function launchDashboard(config, options = {}) {
|
|
|
105
155
|
const palette = new Map();
|
|
106
156
|
const status = { message: null, timer: null, quitArmed: false, quitTimer: null, clock: null, chip: null, search: null };
|
|
107
157
|
const gitInfo = new Map(); // project.path -> git snapshot (branch / hash / dirty)
|
|
158
|
+
const stackInfo = new Map();
|
|
108
159
|
const processStats = new Map(); // project.path -> {running, pid, memory, cpu}
|
|
109
160
|
let monitoredPath = null; // project.path currently polled by processMonitor
|
|
110
161
|
let searchActive = false;
|
|
111
162
|
let searchBuffer = '';
|
|
163
|
+
let gitModal = null;
|
|
112
164
|
|
|
113
|
-
const colorFor = (project) => palette.get(project.path) ||
|
|
165
|
+
const colorFor = (project) => palette.get(project.path) || THEME.text;
|
|
114
166
|
|
|
115
167
|
function rebuildPalette() {
|
|
116
168
|
palette.clear();
|
|
@@ -118,28 +170,185 @@ function launchDashboard(config, options = {}) {
|
|
|
118
170
|
}
|
|
119
171
|
rebuildPalette();
|
|
120
172
|
|
|
121
|
-
const dotColor = (project) => DOT_COLORS[MODERN_OF[project.status] || 'pend'] || 'gray';
|
|
122
|
-
const badgeColor = (project) => STATUS_COLORS[project.status] || dotColor(project);
|
|
123
|
-
|
|
124
173
|
const configLoader = config.demoMode ? () => loadConfigFromPath(SAMPLE_CONFIG_PATH, {}) : () => loadConfig({});
|
|
125
174
|
|
|
126
175
|
/* ---------------------------------------------------------------- *
|
|
127
|
-
* Widgets (
|
|
176
|
+
* Widgets (hand-positioned: exact rows, zero gaps between panes)
|
|
128
177
|
* ---------------------------------------------------------------- */
|
|
129
178
|
|
|
130
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Shared look for a bordered panel: dark bg, thin dark-gray border,
|
|
181
|
+
* subtle label. Style is assigned before setLabel on purpose — blessed
|
|
182
|
+
* bakes style.label into the label widget when it is created.
|
|
183
|
+
*/
|
|
184
|
+
function panel(el, label) {
|
|
185
|
+
el.style.border = { type: 'line', fg: THEME.border };
|
|
186
|
+
el.style.label = { fg: THEME.textDim };
|
|
187
|
+
el.style.fg = THEME.text;
|
|
188
|
+
el.style.bg = THEME.bg;
|
|
189
|
+
if (label) {
|
|
190
|
+
el.headerLabel = label;
|
|
191
|
+
el.setLabel(label);
|
|
192
|
+
}
|
|
193
|
+
return el;
|
|
194
|
+
}
|
|
195
|
+
|
|
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
|
+
}
|
|
131
229
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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())}`;
|
|
137
246
|
}
|
|
138
247
|
|
|
139
|
-
const
|
|
140
|
-
|
|
248
|
+
const masthead = blessed.box({
|
|
249
|
+
parent: screen,
|
|
250
|
+
top: 0,
|
|
251
|
+
left: 0,
|
|
252
|
+
width: '100%',
|
|
253
|
+
height: 3,
|
|
254
|
+
tags: true,
|
|
255
|
+
style: { bg: THEME.bg, fg: THEME.text },
|
|
256
|
+
});
|
|
257
|
+
|
|
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
|
+
});
|
|
141
281
|
|
|
142
|
-
const
|
|
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({
|
|
295
|
+
parent: screen,
|
|
296
|
+
top: CHIP_STRIP_TOP,
|
|
297
|
+
left: 0,
|
|
298
|
+
width: '100%',
|
|
299
|
+
height: 3,
|
|
300
|
+
tags: true,
|
|
301
|
+
style: { bg: THEME.bg, fg: THEME.text },
|
|
302
|
+
});
|
|
303
|
+
|
|
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;
|
|
345
|
+
|
|
346
|
+
const projectList = blessed.list({
|
|
347
|
+
parent: screen,
|
|
348
|
+
top: bodyTop,
|
|
349
|
+
left: 0,
|
|
350
|
+
width: '40%',
|
|
351
|
+
height: bodyHeight,
|
|
143
352
|
tags: true,
|
|
144
353
|
keys: true,
|
|
145
354
|
mouse: true,
|
|
@@ -147,26 +356,72 @@ function launchDashboard(config, options = {}) {
|
|
|
147
356
|
scrollable: true,
|
|
148
357
|
alwaysScroll: true,
|
|
149
358
|
items: [],
|
|
150
|
-
|
|
359
|
+
border: { type: 'line', fg: THEME.border },
|
|
360
|
+
style: {
|
|
361
|
+
bg: THEME.bg,
|
|
362
|
+
item: { fg: THEME.text, hover: { bg: '#313244' } },
|
|
363
|
+
selected: { bg: THEME.accentBg, fg: THEME.accentFg, bold: true },
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
panel(projectList, ' PROJECTS ');
|
|
367
|
+
paneHeaderRight(projectList, ' SORT: RECENT ');
|
|
368
|
+
|
|
369
|
+
const rightLeft = '40%';
|
|
370
|
+
const rightWidth = '60%';
|
|
371
|
+
|
|
372
|
+
const card = blessed.box({
|
|
373
|
+
parent: screen,
|
|
374
|
+
top: bodyTop,
|
|
375
|
+
left: rightLeft,
|
|
376
|
+
width: rightWidth,
|
|
377
|
+
height: cardHeight,
|
|
378
|
+
tags: true,
|
|
379
|
+
scrollable: true,
|
|
380
|
+
mouse: true,
|
|
381
|
+
border: { type: 'line', fg: THEME.border },
|
|
382
|
+
});
|
|
383
|
+
panel(card, ' DETAILS ');
|
|
384
|
+
const gitHeader = paneHeaderRight(card, '{#a6e3a1-fg}GIT: CLEAN{/#a6e3a1-fg}');
|
|
385
|
+
|
|
386
|
+
const actionsShell = blessed.box({
|
|
387
|
+
parent: screen,
|
|
388
|
+
top: bodyTop + cardHeight,
|
|
389
|
+
left: rightLeft,
|
|
390
|
+
width: rightWidth,
|
|
391
|
+
height: actionsHeight,
|
|
392
|
+
tags: true,
|
|
393
|
+
border: { type: 'line', fg: THEME.border },
|
|
394
|
+
});
|
|
395
|
+
panel(actionsShell, ' ACTIONS - r/e/c/x/o/f/k/s or [Enter] ');
|
|
396
|
+
paneHeaderRight(actionsShell, ' KEYMAP: VIM/CLI ');
|
|
397
|
+
|
|
398
|
+
const footerMeta = blessed.box({
|
|
399
|
+
parent: screen,
|
|
400
|
+
top: bodyTop + bodyHeight,
|
|
401
|
+
left: 0,
|
|
402
|
+
width: '100%',
|
|
403
|
+
height: 1,
|
|
404
|
+
tags: true,
|
|
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 },
|
|
151
415
|
});
|
|
152
|
-
styleCell(projectList, ' PROJECTS (14 repos) ');
|
|
153
|
-
|
|
154
|
-
const card = grid.set(1, 5, 3, 7, blessed.box, { tags: true, scrollable: true, mouse: true });
|
|
155
|
-
styleCell(card, ' DETAILS ');
|
|
156
|
-
|
|
157
|
-
const actionsShell = grid.set(4, 5, 3, 7, blessed.box, { tags: true });
|
|
158
|
-
styleCell(actionsShell, ' ACTIONS — r/e/c/x/o/f/k/s or [Enter] ');
|
|
159
|
-
|
|
160
|
-
const footer = grid.set(11, 0, 1, 12, blessed.box, { tags: true, style: { fg: 'white', bg: 'blue' } });
|
|
161
416
|
|
|
162
|
-
|
|
163
|
-
function makeButton({ content, color, onPress }) {
|
|
417
|
+
function makeButton({ content, onPress }) {
|
|
164
418
|
const button = blessed.button({
|
|
165
419
|
parent: actionsShell,
|
|
166
420
|
top: '0%',
|
|
167
421
|
left: '0%',
|
|
168
422
|
width: '47%',
|
|
169
|
-
height:
|
|
423
|
+
height: large ? 3 : 1,
|
|
424
|
+
shrink: true,
|
|
170
425
|
content,
|
|
171
426
|
align: 'center',
|
|
172
427
|
valign: 'middle',
|
|
@@ -174,58 +429,80 @@ function launchDashboard(config, options = {}) {
|
|
|
174
429
|
mouse: true,
|
|
175
430
|
clickable: true,
|
|
176
431
|
autoFocus: false,
|
|
177
|
-
|
|
432
|
+
padding: large ? { left: 1, right: 1 } : {},
|
|
433
|
+
border: large ? { type: 'line', fg: THEME.border } : undefined,
|
|
434
|
+
style: {
|
|
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,
|
|
440
|
+
},
|
|
178
441
|
});
|
|
179
442
|
|
|
180
443
|
button.on('press', () => {
|
|
444
|
+
let handled = false;
|
|
181
445
|
try {
|
|
182
|
-
onPress();
|
|
446
|
+
handled = onPress() === true;
|
|
183
447
|
} catch (err) {
|
|
184
448
|
setStatus(`Error: ${err.message}`);
|
|
185
449
|
} finally {
|
|
186
|
-
|
|
187
|
-
|
|
450
|
+
if (!handled && !gitModal) {
|
|
451
|
+
projectList.focus();
|
|
452
|
+
screen.render();
|
|
453
|
+
}
|
|
188
454
|
}
|
|
189
455
|
});
|
|
190
456
|
return button;
|
|
191
457
|
}
|
|
192
458
|
|
|
193
459
|
const buttons = {};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
button.
|
|
460
|
+
|
|
461
|
+
function addButton(name, slot, content, opts) {
|
|
462
|
+
const button = makeButton({ content, ...opts });
|
|
463
|
+
const rowH = large ? 3 : 1;
|
|
464
|
+
button.top = 1 + slot.row * rowH;
|
|
465
|
+
button.height = rowH;
|
|
466
|
+
button.left = slot.col === 0 ? '2%' : '51%';
|
|
467
|
+
button.width = '47%';
|
|
199
468
|
buttons[name] = button;
|
|
200
469
|
return button;
|
|
201
470
|
}
|
|
202
|
-
const BUTTON_SLOTS = new Map();
|
|
203
471
|
|
|
204
|
-
addButton('dev', { row: 0, col: 0 },
|
|
205
|
-
addButton('editor', { row: 0, col: 1 },
|
|
206
|
-
addButton('claude', { row: 1, col: 0 },
|
|
207
|
-
addButton('codex', { row: 1, col: 1 },
|
|
208
|
-
addButton('opencode', { row: 2, col: 0 },
|
|
209
|
-
addButton('freebuff', { row: 2, col: 1 },
|
|
210
|
-
addButton('kilocode', { row: 3, col: 0 },
|
|
211
|
-
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 });
|
|
212
481
|
|
|
213
482
|
// Created after the buttons so tab-focus order is list -> actions -> output.
|
|
214
|
-
const logBox =
|
|
483
|
+
const logBox = contrib.log({
|
|
484
|
+
parent: screen,
|
|
485
|
+
top: bodyTop + cardHeight + actionsHeight,
|
|
486
|
+
left: rightLeft,
|
|
487
|
+
width: rightWidth,
|
|
488
|
+
height: logHeight,
|
|
215
489
|
tags: true,
|
|
216
490
|
keys: true,
|
|
217
491
|
mouse: true,
|
|
218
492
|
bufferLength: 600,
|
|
219
|
-
|
|
493
|
+
border: { type: 'line', fg: THEME.border },
|
|
494
|
+
style: { bg: THEME.surface, item: { fg: THEME.text }, selected: { fg: THEME.text, bg: '#313244' } },
|
|
220
495
|
});
|
|
221
|
-
|
|
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} `);
|
|
222
498
|
|
|
223
499
|
const logView = new LogView(logBox, {
|
|
224
500
|
maxLines: 800,
|
|
225
501
|
flushInterval: 120,
|
|
226
502
|
viewportHeight: () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : screen.rows) - 2),
|
|
227
503
|
onChange: () => screen.render(),
|
|
228
|
-
|
|
504
|
+
onLabelChange: (label) => bufferHeader.refresh(label),
|
|
505
|
+
label: ' OUTPUT (dev server / agents) [ON] ',
|
|
229
506
|
});
|
|
230
507
|
|
|
231
508
|
/* ---------------------------------------------------------------- *
|
|
@@ -245,10 +522,10 @@ function launchDashboard(config, options = {}) {
|
|
|
245
522
|
},
|
|
246
523
|
onExit: (project, info) => {
|
|
247
524
|
if (info.restart) {
|
|
248
|
-
appendLog(project, `{
|
|
525
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}dev server crashed — auto-restarting ({bold}${info.attempt}/${info.max}{/bold})\u2026{/${STATUS_FG.exp}-fg}`, 'system');
|
|
249
526
|
} else {
|
|
250
527
|
const detail = info.code === null || info.code === undefined ? `signal ${info.signal}` : `exit code ${info.code}`;
|
|
251
|
-
appendLog(project, `{
|
|
528
|
+
appendLog(project, `{${THEME.textDim}-fg}dev server stopped (${detail}){/${THEME.textDim}-fg}`, 'system');
|
|
252
529
|
}
|
|
253
530
|
refreshList();
|
|
254
531
|
updateCard();
|
|
@@ -260,48 +537,56 @@ function launchDashboard(config, options = {}) {
|
|
|
260
537
|
* ---------------------------------------------------------------- */
|
|
261
538
|
|
|
262
539
|
function modernCounts() {
|
|
263
|
-
const counts = { live: 0, exp: 0, pend: 0, scrap: 0 };
|
|
540
|
+
const counts = { live: 0, exp: 0, pend: 0, scrap: 0, unknown: 0 };
|
|
264
541
|
for (const project of projects) {
|
|
265
|
-
const modern =
|
|
542
|
+
const modern = modernStatusOf(project) || 'unknown';
|
|
266
543
|
counts[modern] = (counts[modern] || 0) + 1;
|
|
267
544
|
}
|
|
268
545
|
return counts;
|
|
269
546
|
}
|
|
270
547
|
|
|
271
|
-
function
|
|
548
|
+
function chipData() {
|
|
272
549
|
const counts = modernCounts();
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
};
|
|
279
|
-
const allActive = status.chip === null;
|
|
280
|
-
return [
|
|
281
|
-
`{white-fg}${allActive ? '{bold}' : ''}[ALL ${projects.length}]${allActive ? '{/bold}' : ''}{/white-fg}`,
|
|
282
|
-
chip('LIVE', counts.live, 'green'),
|
|
283
|
-
chip('EXP', counts.exp, 'yellow'),
|
|
284
|
-
chip('PEND', counts.pend, 'blue'),
|
|
285
|
-
chip('SCRAP', counts.scrap, 'gray'),
|
|
286
|
-
].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
|
+
});
|
|
287
555
|
}
|
|
288
556
|
|
|
289
557
|
function searchLabel() {
|
|
290
|
-
if (searchActive) return `/search: ${searchBuffer}`;
|
|
291
|
-
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)';
|
|
292
560
|
}
|
|
293
561
|
|
|
294
562
|
function updateHeader() {
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
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()));
|
|
298
581
|
}
|
|
299
582
|
|
|
300
583
|
/** Projects after the chip (status) + search (regex on name) filters. */
|
|
301
584
|
function filteredProjects() {
|
|
302
585
|
let list = projects;
|
|
303
|
-
if (status.chip) {
|
|
304
|
-
list = list.filter((project) => (
|
|
586
|
+
if (status.chip === 'unknown') {
|
|
587
|
+
list = list.filter((project) => modernStatusOf(project) === null);
|
|
588
|
+
} else if (status.chip) {
|
|
589
|
+
list = list.filter((project) => modernStatusOf(project) === status.chip);
|
|
305
590
|
}
|
|
306
591
|
if (status.search) {
|
|
307
592
|
let re = null;
|
|
@@ -316,68 +601,61 @@ function launchDashboard(config, options = {}) {
|
|
|
316
601
|
}
|
|
317
602
|
|
|
318
603
|
function listItems() {
|
|
319
|
-
const inner = Math.max(12, Math.floor(
|
|
320
|
-
const nameWidth = Math.max(6, inner - 12);
|
|
604
|
+
const inner = Math.max(12, Math.floor(screen.cols * 0.4) - 4);
|
|
321
605
|
return filteredProjects().map((project) => {
|
|
322
606
|
const state = runStates.get(project.path);
|
|
323
607
|
const running = state && (state.status === 'running' || state.status === 'starting');
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
: '{yellow-fg}●{/yellow-fg}'
|
|
328
|
-
: `{${dotColor(project)}-fg}●{/${dotColor(project)}-fg}`;
|
|
329
|
-
const name = escapeBraces(truncate(project.name, nameWidth));
|
|
608
|
+
const modern = modernStatusOf(project);
|
|
609
|
+
const dotFg = running ? (state.status === 'running' ? STATUS_FG.live : STATUS_FG.exp) : statusFg(modern);
|
|
610
|
+
const name = escapeBraces(truncate(project.name, Math.max(6, inner - 14)));
|
|
330
611
|
const info = gitInfo.get(project.path);
|
|
331
|
-
const activity = escapeBraces(truncate(project.lastActivity || timeAgo(info && info.lastCommitAt) || '\u2014', 10));
|
|
332
|
-
|
|
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}`;
|
|
333
615
|
});
|
|
334
616
|
}
|
|
335
617
|
|
|
336
618
|
function devStateLine(project, state) {
|
|
337
619
|
if (state && (state.status === 'running' || state.status === 'starting')) {
|
|
338
620
|
const where = state.url ? ` \u2192 ${escapeBraces(state.url)}` : ' \u2014 waiting for a localhost URL\u2026';
|
|
339
|
-
return ` {
|
|
621
|
+
return ` {${STATUS_FG.live}-fg}● dev server ${state.status} (pid ${state.pid}){/${STATUS_FG.live}-fg}${where}`;
|
|
340
622
|
}
|
|
341
623
|
if (state && state.status === 'error') {
|
|
342
|
-
return ` {
|
|
624
|
+
return ` {#f38ba8-fg}● ${escapeBraces(truncate(state.error || 'failed to start', 50))}{/#f38ba8-fg}`;
|
|
343
625
|
}
|
|
344
626
|
const last = servers.lastExit.get(project.path);
|
|
345
627
|
if (last) {
|
|
346
628
|
const detail = last.code === null || last.code === undefined ? `signal ${last.signal}` : `exit code ${last.code}`;
|
|
347
|
-
return ` {
|
|
629
|
+
return ` {${THEME.textDim}-fg}○ dev server stopped (${detail}){/${THEME.textDim}-fg}`;
|
|
348
630
|
}
|
|
349
|
-
return
|
|
631
|
+
return ` {${THEME.textDim}-fg}○ dev server not running{/${THEME.textDim}-fg}`;
|
|
350
632
|
}
|
|
351
633
|
|
|
352
634
|
function updateCard() {
|
|
353
635
|
const project = selectedProject();
|
|
354
636
|
if (!project) {
|
|
355
|
-
card.setContent(
|
|
637
|
+
card.setContent(` {${THEME.textDim}-fg}No projects configured. Run \`termdeck --setup\`.{/${THEME.textDim}-fg}`);
|
|
356
638
|
screen.render();
|
|
357
639
|
return;
|
|
358
640
|
}
|
|
359
641
|
|
|
360
642
|
const state = runStates.get(project.path) || { status: 'idle' };
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
643
|
+
const modern = modernStatusOf(project);
|
|
644
|
+
const sFg = statusFg(modern);
|
|
645
|
+
const inner = Math.max(24, Math.floor(screen.cols * 0.6) - 4);
|
|
364
646
|
const info = gitInfo.get(project.path);
|
|
365
647
|
const stats = processStats.get(project.path);
|
|
366
|
-
const pidLabel = runPid
|
|
367
|
-
? runPid
|
|
368
|
-
: stats && stats.pid
|
|
369
|
-
? stats.pid
|
|
370
|
-
: config.demoMode && project.port
|
|
371
|
-
? `${DEMO_PID} {gray-fg}(demo){/gray-fg}`
|
|
372
|
-
: '\u2014';
|
|
373
648
|
const memCpu = stats && stats.memory
|
|
374
649
|
? `${escapeBraces(stats.memory)} \u00b7 ${escapeBraces(stats.cpu || '\u2014')}`
|
|
375
650
|
: config.demoMode
|
|
376
|
-
?
|
|
651
|
+
? `213.4 MB \u00b7 0.8% {${THEME.textDim}-fg}(demo){/${THEME.textDim}-fg}`
|
|
377
652
|
: '\u2014';
|
|
378
653
|
const dirty = info && info.dirty ? info.dirty : { added: 0, removed: 0 };
|
|
379
|
-
const dirtyLabel = dirty.added
|
|
380
|
-
? ` {
|
|
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}`
|
|
381
659
|
: '';
|
|
382
660
|
const branch = (info && info.branch) || project.branch || '\u2014';
|
|
383
661
|
const hash = (info && info.commitHash) || (project.lastCommit && project.lastCommit.hash) || '';
|
|
@@ -385,36 +663,89 @@ function launchDashboard(config, options = {}) {
|
|
|
385
663
|
const lastAt = (info && timeAgo(info.lastCommitAt)) || project.lastActivity || null;
|
|
386
664
|
const commit = `${hash} ${msg} ${lastAt ? `(${lastAt})` : ''}`.trim() || branch;
|
|
387
665
|
|
|
666
|
+
// Unknown / missing status renders as "[?] Unknown" in neutral gray; the
|
|
667
|
+
// status button is recoloured below to prompt the user to set it.
|
|
668
|
+
const unknown = !modern;
|
|
669
|
+
const fullLabel = modern ? (STATUS_FULL[modern] || modern.toUpperCase()) : 'UNKNOWN';
|
|
670
|
+
const statusChip = unknown
|
|
671
|
+
? `{${STATUS_FG.unknown}-fg}{bold}[?] Unknown{/bold}{/${STATUS_FG.unknown}-fg}`
|
|
672
|
+
: `{${sFg}-fg}{bold}[${modern.toUpperCase()}]{/bold} ${fullLabel}{/${sFg}-fg}`;
|
|
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
|
+
|
|
388
682
|
const lines = [
|
|
389
|
-
` {${
|
|
390
|
-
` {
|
|
391
|
-
` {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
683
|
+
` {${sFg}-fg}\u25cf{/${sFg}-fg} {bold}${escapeBraces(truncate(project.name, 40))}{/bold}`,
|
|
684
|
+
` {${THEME.textDim}-fg}${escapeBraces(truncate(project.info || '(no description)', inner - 2))}{/${THEME.textDim}-fg}`,
|
|
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),
|
|
398
692
|
devStateLine(project, state),
|
|
399
693
|
];
|
|
400
694
|
|
|
401
695
|
card.setContent(lines.join('\n'));
|
|
402
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} `;
|
|
702
|
+
|
|
703
|
+
// Visual cue while the status is unknown: red button asking to be set.
|
|
704
|
+
if (unknown) {
|
|
705
|
+
buttons.status.setContent('{bold}[s]{/bold} Change status!');
|
|
706
|
+
buttons.status.style.fg = '#f38ba8';
|
|
707
|
+
buttons.status.style.bold = true;
|
|
708
|
+
} else {
|
|
709
|
+
buttons.status.setContent(`{bold}[s]{/bold} Change status \u2192 ${fullLabel}`);
|
|
710
|
+
buttons.status.style.fg = sFg;
|
|
711
|
+
buttons.status.style.bold = false;
|
|
712
|
+
}
|
|
713
|
+
|
|
403
714
|
screen.render();
|
|
404
715
|
}
|
|
405
716
|
|
|
406
|
-
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() {
|
|
407
733
|
const sel = selectedProject();
|
|
408
734
|
const index = sel ? filteredProjects().indexOf(sel) + 1 : 0;
|
|
409
|
-
const
|
|
735
|
+
const paneLabel = currentPane();
|
|
410
736
|
const size = `${screen.cols}x${screen.rows}`;
|
|
411
737
|
const chipLabel = status.chip ? status.chip.toUpperCase() : 'ALL';
|
|
412
|
-
const
|
|
413
|
-
|
|
738
|
+
const search = status.search ? ` /${status.search}` : '';
|
|
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;
|
|
414
744
|
}
|
|
415
745
|
|
|
416
746
|
function updateFooter() {
|
|
417
|
-
|
|
747
|
+
footerMeta.setContent(buildFooterRow1());
|
|
748
|
+
footer.setContent(buildFooterRow2());
|
|
418
749
|
}
|
|
419
750
|
|
|
420
751
|
function currentPane() {
|
|
@@ -428,7 +759,7 @@ function launchDashboard(config, options = {}) {
|
|
|
428
759
|
function refreshList() {
|
|
429
760
|
const selected = projectList.selected;
|
|
430
761
|
projectList.setItems(listItems());
|
|
431
|
-
projectList.setLabel(` PROJECTS (${projects.length}
|
|
762
|
+
projectList.setLabel(` PROJECTS (${projects.length}) `);
|
|
432
763
|
if (typeof selected === 'number' && selected < projects.length) projectList.select(selected);
|
|
433
764
|
updateHeader();
|
|
434
765
|
updateFooter();
|
|
@@ -445,6 +776,7 @@ function launchDashboard(config, options = {}) {
|
|
|
445
776
|
function setStatus(message) {
|
|
446
777
|
if (status.timer) clearTimeout(status.timer);
|
|
447
778
|
status.message = message;
|
|
779
|
+
footerMeta.setContent(buildFooterRow1());
|
|
448
780
|
footer.setContent(` {bold}${escapeBraces(message)}{/bold}`);
|
|
449
781
|
screen.render();
|
|
450
782
|
|
|
@@ -458,12 +790,18 @@ function launchDashboard(config, options = {}) {
|
|
|
458
790
|
|
|
459
791
|
/** Log lines from child processes are raw text -> escape blessed markup. */
|
|
460
792
|
function appendLog(project, line, stream = 'stdout') {
|
|
461
|
-
const
|
|
793
|
+
const time = `{${THEME.textDim}-fg}${timestamp()}{/${THEME.textDim}-fg}`;
|
|
462
794
|
if (stream === 'system') {
|
|
463
|
-
|
|
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}`);
|
|
464
800
|
return;
|
|
465
801
|
}
|
|
466
|
-
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} ' : '';
|
|
467
805
|
logView.push(`${prefix} ${marker}${escapeBraces(line)}`);
|
|
468
806
|
}
|
|
469
807
|
|
|
@@ -471,6 +809,187 @@ function launchDashboard(config, options = {}) {
|
|
|
471
809
|
* Actions
|
|
472
810
|
* ---------------------------------------------------------------- */
|
|
473
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
|
+
|
|
474
993
|
function startDevServer() {
|
|
475
994
|
const project = selectedProject();
|
|
476
995
|
if (!project) return;
|
|
@@ -488,7 +1007,7 @@ function launchDashboard(config, options = {}) {
|
|
|
488
1007
|
const result = servers.start(project);
|
|
489
1008
|
|
|
490
1009
|
if (!result.ok) {
|
|
491
|
-
appendLog(project, `{
|
|
1010
|
+
appendLog(project, `{#f38ba8-fg}could not start: ${escapeBraces(result.error)}{/#f38ba8-fg}`, 'system');
|
|
492
1011
|
setStatus(`Could not start ${project.name}: ${result.error}`);
|
|
493
1012
|
} else {
|
|
494
1013
|
appendLog(project, `logs streaming into the OUTPUT pane — press shift+x to stop`, 'system');
|
|
@@ -522,28 +1041,29 @@ function launchDashboard(config, options = {}) {
|
|
|
522
1041
|
appendLog(project, `opening editor in a new terminal: ${escapeBraces(command)}`, 'system');
|
|
523
1042
|
const result = await openInNewTerminal({ cwd: project.path, command });
|
|
524
1043
|
if (result.ok) {
|
|
525
|
-
appendLog(project, `{
|
|
1044
|
+
appendLog(project, `{${STATUS_FG.live}-fg}new ${escapeBraces(result.terminal)} window \u2192 ${escapeBraces(displayPath(project.path, config.root))}{/${STATUS_FG.live}-fg}`, 'system');
|
|
526
1045
|
setStatus(`Opened editor in a new terminal window.`);
|
|
527
1046
|
} else {
|
|
528
|
-
appendLog(project, `{
|
|
1047
|
+
appendLog(project, `{#f38ba8-fg}could not open a terminal: ${escapeBraces(result.error)}{/#f38ba8-fg}`, 'system');
|
|
529
1048
|
setStatus(`Could not open a terminal for ${project.name}.`);
|
|
530
1049
|
}
|
|
531
1050
|
return result;
|
|
532
1051
|
}
|
|
533
1052
|
|
|
534
1053
|
// Agent: launch in a new terminal with log capture via tee where possible.
|
|
535
|
-
|
|
536
|
-
|
|
1054
|
+
const agentName = AGENT_LABELS[kind] || kind;
|
|
1055
|
+
appendLog(project, `{${STATUS_FG.exp}-fg}[${escapeBraces(kind)}]{/${STATUS_FG.exp}-fg} launching ${escapeBraces(agentName)} in a new terminal`, 'system');
|
|
1056
|
+
setStatus(`Launching ${agentName} for ${project.name}\u2026`);
|
|
537
1057
|
const result = await launchAgent(project, kind);
|
|
538
1058
|
if (result.ok) {
|
|
539
|
-
appendLog(project, `{
|
|
540
|
-
setStatus(`Launched ${
|
|
1059
|
+
appendLog(project, `{${STATUS_FG.live}-fg}${escapeBraces(agentName)} launched in new ${escapeBraces(result.terminal || 'terminal')} window \u2192 logs \u2192 ${escapeBraces(result.logFile || 'terminal only')}{/${STATUS_FG.live}-fg}`, 'system');
|
|
1060
|
+
setStatus(`Launched ${agentName} for ${project.name}.`);
|
|
541
1061
|
if (result.logFile) {
|
|
542
1062
|
tailAgentLog(project, kind, (line) => appendLog(project, line, 'stdout'));
|
|
543
1063
|
}
|
|
544
1064
|
} else {
|
|
545
|
-
appendLog(project, `{
|
|
546
|
-
setStatus(`Could not launch ${
|
|
1065
|
+
appendLog(project, `{#f38ba8-fg}could not launch ${escapeBraces(agentName)}: ${escapeBraces(result.error)}{/#f38ba8-fg}`, 'system');
|
|
1066
|
+
setStatus(`Could not launch ${agentName} for ${project.name}.`);
|
|
547
1067
|
}
|
|
548
1068
|
return result;
|
|
549
1069
|
}
|
|
@@ -551,11 +1071,13 @@ function launchDashboard(config, options = {}) {
|
|
|
551
1071
|
function cycleStatus() {
|
|
552
1072
|
const project = selectedProject();
|
|
553
1073
|
if (!project) return;
|
|
554
|
-
|
|
1074
|
+
// Unknown / missing statuses enter the cycle at `exp` so a single press
|
|
1075
|
+
// gives the project a real status.
|
|
1076
|
+
const current = modernStatusOf(project) || 'exp';
|
|
555
1077
|
const index = MODERN_STATUSES.indexOf(current);
|
|
556
1078
|
const next = MODERN_STATUSES[(index + 1) % MODERN_STATUSES.length];
|
|
557
1079
|
project.status = next;
|
|
558
|
-
appendLog(project, `{
|
|
1080
|
+
appendLog(project, `{${THEME.tagCyan}-fg}[termdeck]{/${THEME.tagCyan}-fg} status changed to {bold}${next}{/bold}`, 'system');
|
|
559
1081
|
setStatus(`${project.name}: status \u2192 ${next}`);
|
|
560
1082
|
if (!config.demoMode) {
|
|
561
1083
|
try { saveConfig(config); } catch (_) { /* best effort */ }
|
|
@@ -590,6 +1112,7 @@ function launchDashboard(config, options = {}) {
|
|
|
590
1112
|
}
|
|
591
1113
|
|
|
592
1114
|
function destroy() {
|
|
1115
|
+
if (gitModal) gitModal.close();
|
|
593
1116
|
if (status.timer) clearTimeout(status.timer);
|
|
594
1117
|
if (status.quitTimer) clearTimeout(status.quitTimer);
|
|
595
1118
|
if (status.clock) clearInterval(status.clock);
|
|
@@ -626,9 +1149,10 @@ function launchDashboard(config, options = {}) {
|
|
|
626
1149
|
* Git / process-monitor refresh on selection
|
|
627
1150
|
* ---------------------------------------------------------------- */
|
|
628
1151
|
|
|
629
|
-
function refreshProjectGit(project) {
|
|
1152
|
+
function refreshProjectGit(project, { force = false } = {}) {
|
|
630
1153
|
if (!project || config.demoMode) return;
|
|
631
|
-
|
|
1154
|
+
refreshProjectStack(project);
|
|
1155
|
+
const info = getGitInfo(project.path, { force });
|
|
632
1156
|
gitInfo.set(project.path, info);
|
|
633
1157
|
if (selectedProject() === project) {
|
|
634
1158
|
refreshList();
|
|
@@ -687,10 +1211,9 @@ function launchDashboard(config, options = {}) {
|
|
|
687
1211
|
screen.key(['o'], () => { if (!searchActive) openTool('opencode'); });
|
|
688
1212
|
screen.key(['f'], () => { if (!searchActive) openTool('freebuff'); });
|
|
689
1213
|
screen.key(['k'], () => { if (!searchActive) openTool('kilocode'); });
|
|
690
|
-
screen.key(['
|
|
1214
|
+
screen.key(['g'], () => { if (!searchActive) openGitCommitModal(); });
|
|
691
1215
|
screen.key(['S-x'], () => { if (!searchActive) stopDevServer(); });
|
|
692
1216
|
screen.key(['j'], () => { if (!searchActive) projectList.down(1); });
|
|
693
|
-
screen.key(['k'], () => { if (!searchActive) projectList.up(1); });
|
|
694
1217
|
screen.key(['tab'], () => { if (!searchActive) screen.focusNext(); });
|
|
695
1218
|
screen.key(['S-tab'], () => { if (!searchActive) screen.focusPrevious(); });
|
|
696
1219
|
screen.key(['S-g', 'end'], () => logView.followTail());
|
|
@@ -699,7 +1222,7 @@ function launchDashboard(config, options = {}) {
|
|
|
699
1222
|
screen.key(['S-pageup', 'home'], () => logView.scrollTop());
|
|
700
1223
|
|
|
701
1224
|
/* ---------------------------------------------------------------- *
|
|
702
|
-
* Filter chips: 1 = ALL, 2 = LIVE, 3 = EXP, 4 = PEND, 5 = SCRAP
|
|
1225
|
+
* Filter chips: 1 = ALL, 2 = LIVE, 3 = EXP, 4 = PEND, 5 = UNKNOWN, 6 = SCRAP
|
|
703
1226
|
* ---------------------------------------------------------------- */
|
|
704
1227
|
|
|
705
1228
|
const FILTER_KEYS = {
|
|
@@ -707,7 +1230,8 @@ function launchDashboard(config, options = {}) {
|
|
|
707
1230
|
'2': 'live',
|
|
708
1231
|
'3': 'exp',
|
|
709
1232
|
'4': 'pend',
|
|
710
|
-
'5': '
|
|
1233
|
+
'5': 'unknown',
|
|
1234
|
+
'6': 'scrap',
|
|
711
1235
|
};
|
|
712
1236
|
screen.on('keypress', (ch, key) => {
|
|
713
1237
|
// While search mode is active, capture every keystroke for the search buffer.
|
|
@@ -743,7 +1267,7 @@ function launchDashboard(config, options = {}) {
|
|
|
743
1267
|
return; // ignore everything else while search-active
|
|
744
1268
|
}
|
|
745
1269
|
|
|
746
|
-
// Filter chips: 1–
|
|
1270
|
+
// Filter chips: 1–6.
|
|
747
1271
|
if (FILTER_KEYS.hasOwnProperty(ch)) {
|
|
748
1272
|
status.chip = FILTER_KEYS[ch];
|
|
749
1273
|
refreshList();
|
|
@@ -787,7 +1311,7 @@ function launchDashboard(config, options = {}) {
|
|
|
787
1311
|
projectList.focus();
|
|
788
1312
|
refreshList();
|
|
789
1313
|
updateCard();
|
|
790
|
-
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');
|
|
791
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');
|
|
792
1316
|
if (config.demoMode) {
|
|
793
1317
|
const demoProject = projects[0] || { name: 'hyperion-core', path: 'demo' };
|
|
@@ -796,7 +1320,7 @@ function launchDashboard(config, options = {}) {
|
|
|
796
1320
|
}
|
|
797
1321
|
}
|
|
798
1322
|
|
|
799
|
-
// Staggered git-info refresh so the first
|
|
1323
|
+
// Staggered git-info refresh so the first git spawns do not block the
|
|
800
1324
|
// initial render. Each spawn takes ~30-60 ms on a warm filesystem.
|
|
801
1325
|
let gitBootIdx = 0;
|
|
802
1326
|
const gitBoot = setInterval(() => {
|
|
@@ -819,6 +1343,8 @@ function launchDashboard(config, options = {}) {
|
|
|
819
1343
|
}, 200);
|
|
820
1344
|
if (bootMonitor.unref) bootMonitor.unref();
|
|
821
1345
|
|
|
1346
|
+
screen.render();
|
|
1347
|
+
updateHeader(); // re-center the title now that geometry exists
|
|
822
1348
|
screen.render();
|
|
823
1349
|
|
|
824
1350
|
// One-shot boot toast (e.g. "✨ Discovered and added 2 new projects") from
|
|
@@ -831,7 +1357,7 @@ function launchDashboard(config, options = {}) {
|
|
|
831
1357
|
|
|
832
1358
|
return {
|
|
833
1359
|
screen,
|
|
834
|
-
widgets: { header, projectList, card, logBox, footer, buttons },
|
|
1360
|
+
widgets: { header: titleBox, title: titleBox, statsBar: chipStrip, projectList, card, logBox, footer, buttons },
|
|
835
1361
|
servers,
|
|
836
1362
|
logView,
|
|
837
1363
|
runStates,
|
|
@@ -843,4 +1369,4 @@ function launchDashboard(config, options = {}) {
|
|
|
843
1369
|
};
|
|
844
1370
|
}
|
|
845
1371
|
|
|
846
|
-
module.exports = { launchDashboard, LAYOUT, HINTS, SAMPLE_LOG_LINES };
|
|
1372
|
+
module.exports = { launchDashboard, LAYOUT, HINTS, SAMPLE_LOG_LINES };
|