specsmd 0.1.25 → 0.1.26
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/lib/dashboard/tui/app.js +267 -176
- package/lib/dashboard/tui/store.js +3 -0
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -32,6 +32,19 @@ function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function safeJsonHash(value) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.stringify(value, (key, nestedValue) => {
|
|
38
|
+
if (key === 'generatedAt') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return nestedValue;
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
function truncate(value, width) {
|
|
36
49
|
const text = String(value ?? '');
|
|
37
50
|
if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
|
|
@@ -46,8 +59,7 @@ function truncate(value, width) {
|
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
function fitLines(lines, maxLines, width) {
|
|
49
|
-
const safeLines = (Array.isArray(lines) ? lines : [])
|
|
50
|
-
.map((line) => truncate(line, width));
|
|
62
|
+
const safeLines = (Array.isArray(lines) ? lines : []).map((line) => truncate(line, width));
|
|
51
63
|
|
|
52
64
|
if (safeLines.length <= maxLines) {
|
|
53
65
|
return safeLines;
|
|
@@ -71,14 +83,22 @@ function formatTime(value) {
|
|
|
71
83
|
return date.toLocaleTimeString();
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
function
|
|
86
|
+
function buildShortStats(snapshot) {
|
|
87
|
+
if (!snapshot?.initialized) {
|
|
88
|
+
return 'init: waiting for state.yaml';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stats = snapshot.stats;
|
|
92
|
+
return `runs ${stats.activeRunsCount}/${stats.completedRuns} | intents ${stats.completedIntents}/${stats.totalIntents} | work ${stats.completedWorkItems}/${stats.totalWorkItems}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
|
|
75
96
|
const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
|
|
97
|
+
const shortStats = buildShortStats(snapshot);
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`updated: ${formatTime(lastRefreshAt)} | watch: ${watchEnabled ? watchStatus : 'off'} | view: ${view} | filter: ${runFilter}`
|
|
81
|
-
].map((line) => truncate(line, width));
|
|
99
|
+
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}/${runFilter} | ${formatTime(lastRefreshAt)}`;
|
|
100
|
+
|
|
101
|
+
return truncate(line, width);
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
function buildErrorLines(error, width) {
|
|
@@ -86,9 +106,7 @@ function buildErrorLines(error, width) {
|
|
|
86
106
|
return [];
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
const lines = [
|
|
90
|
-
`[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`
|
|
91
|
-
];
|
|
109
|
+
const lines = [`[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`];
|
|
92
110
|
|
|
93
111
|
if (error.details) {
|
|
94
112
|
lines.push(`details: ${error.details}`);
|
|
@@ -105,7 +123,7 @@ function buildErrorLines(error, width) {
|
|
|
105
123
|
|
|
106
124
|
function buildActiveRunLines(snapshot, runFilter, width) {
|
|
107
125
|
if (runFilter === 'completed') {
|
|
108
|
-
return [truncate('Hidden by
|
|
126
|
+
return [truncate('Hidden by run filter: completed', width)];
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
const activeRuns = snapshot?.activeRuns || [];
|
|
@@ -119,14 +137,9 @@ function buildActiveRunLines(snapshot, runFilter, width) {
|
|
|
119
137
|
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
120
138
|
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
121
139
|
const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
|
|
122
|
-
const artifacts = [
|
|
123
|
-
run.hasPlan ? 'plan' : null,
|
|
124
|
-
run.hasWalkthrough ? 'walkthrough' : null,
|
|
125
|
-
run.hasTestReport ? 'test-report' : null
|
|
126
|
-
].filter(Boolean).join(', ') || 'none';
|
|
127
140
|
|
|
128
141
|
lines.push(`${run.id} [${run.scope}] current: ${currentItem}`);
|
|
129
|
-
lines.push(`progress ${completed}/${workItems.length} done, ${inProgress} active
|
|
142
|
+
lines.push(`progress: ${completed}/${workItems.length} done, ${inProgress} active`);
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
return lines.map((line) => truncate(line, width));
|
|
@@ -134,7 +147,7 @@ function buildActiveRunLines(snapshot, runFilter, width) {
|
|
|
134
147
|
|
|
135
148
|
function buildPendingLines(snapshot, runFilter, width) {
|
|
136
149
|
if (runFilter === 'completed') {
|
|
137
|
-
return [truncate('Hidden by
|
|
150
|
+
return [truncate('Hidden by run filter: completed', width)];
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
const pending = snapshot?.pendingItems || [];
|
|
@@ -143,16 +156,14 @@ function buildPendingLines(snapshot, runFilter, width) {
|
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
return pending.map((item) => {
|
|
146
|
-
const deps = item.dependencies && item.dependencies.length > 0
|
|
147
|
-
? ` deps:${item.dependencies.join(',')}`
|
|
148
|
-
: '';
|
|
159
|
+
const deps = item.dependencies && item.dependencies.length > 0 ? ` deps:${item.dependencies.join(',')}` : '';
|
|
149
160
|
return truncate(`${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`, width);
|
|
150
161
|
});
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
function buildCompletedLines(snapshot, runFilter, width) {
|
|
154
165
|
if (runFilter === 'active') {
|
|
155
|
-
return [truncate('Hidden by
|
|
166
|
+
return [truncate('Hidden by run filter: active', width)];
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
const completedRuns = snapshot?.completedRuns || [];
|
|
@@ -180,6 +191,15 @@ function buildStatsLines(snapshot, width) {
|
|
|
180
191
|
].map((line) => truncate(line, width));
|
|
181
192
|
}
|
|
182
193
|
|
|
194
|
+
function buildWarningsLines(snapshot, width) {
|
|
195
|
+
const warnings = snapshot?.warnings || [];
|
|
196
|
+
if (warnings.length === 0) {
|
|
197
|
+
return [truncate('No warnings', width)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return warnings.map((warning) => truncate(warning, width));
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
function buildOverviewProjectLines(snapshot, width) {
|
|
184
204
|
if (!snapshot?.initialized) {
|
|
185
205
|
return [
|
|
@@ -221,13 +241,37 @@ function buildOverviewStandardsLines(snapshot, width) {
|
|
|
221
241
|
});
|
|
222
242
|
}
|
|
223
243
|
|
|
224
|
-
function
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
return [
|
|
244
|
+
function allocateSingleColumnPanels(candidates, rowsBudget) {
|
|
245
|
+
const filtered = (candidates || []).filter(Boolean);
|
|
246
|
+
if (filtered.length === 0) {
|
|
247
|
+
return [];
|
|
228
248
|
}
|
|
229
249
|
|
|
230
|
-
|
|
250
|
+
const selected = [];
|
|
251
|
+
let remaining = Math.max(4, rowsBudget);
|
|
252
|
+
|
|
253
|
+
for (const panel of filtered) {
|
|
254
|
+
const margin = selected.length > 0 ? 1 : 0;
|
|
255
|
+
const minimumRows = 4 + margin;
|
|
256
|
+
|
|
257
|
+
if (remaining >= minimumRows || selected.length === 0) {
|
|
258
|
+
selected.push({
|
|
259
|
+
...panel,
|
|
260
|
+
maxLines: 1
|
|
261
|
+
});
|
|
262
|
+
remaining -= minimumRows;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let index = 0;
|
|
267
|
+
while (remaining > 0 && selected.length > 0) {
|
|
268
|
+
const panelIndex = index % selected.length;
|
|
269
|
+
selected[panelIndex].maxLines += 1;
|
|
270
|
+
remaining -= 1;
|
|
271
|
+
index += 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return selected;
|
|
231
275
|
}
|
|
232
276
|
|
|
233
277
|
function createDashboardApp(deps) {
|
|
@@ -245,7 +289,7 @@ function createDashboardApp(deps) {
|
|
|
245
289
|
} = deps;
|
|
246
290
|
|
|
247
291
|
const { Box, Text, useApp, useInput, useStdout } = ink;
|
|
248
|
-
const { useState, useEffect, useCallback } = React;
|
|
292
|
+
const { useState, useEffect, useCallback, useRef } = React;
|
|
249
293
|
|
|
250
294
|
function SectionPanel(props) {
|
|
251
295
|
const {
|
|
@@ -254,7 +298,6 @@ function createDashboardApp(deps) {
|
|
|
254
298
|
width,
|
|
255
299
|
maxLines,
|
|
256
300
|
borderColor,
|
|
257
|
-
marginRight,
|
|
258
301
|
marginBottom
|
|
259
302
|
} = props;
|
|
260
303
|
|
|
@@ -269,7 +312,6 @@ function createDashboardApp(deps) {
|
|
|
269
312
|
borderColor: borderColor || 'gray',
|
|
270
313
|
paddingX: 1,
|
|
271
314
|
width,
|
|
272
|
-
marginRight: marginRight || 0,
|
|
273
315
|
marginBottom: marginBottom || 0
|
|
274
316
|
},
|
|
275
317
|
React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
|
|
@@ -277,12 +319,43 @@ function createDashboardApp(deps) {
|
|
|
277
319
|
);
|
|
278
320
|
}
|
|
279
321
|
|
|
322
|
+
function TabsBar(props) {
|
|
323
|
+
const { view, width } = props;
|
|
324
|
+
const tabs = [
|
|
325
|
+
{ id: 'runs', label: ' 1 RUNS ' },
|
|
326
|
+
{ id: 'overview', label: ' 2 OVERVIEW ' },
|
|
327
|
+
{ id: 'health', label: ' 3 HEALTH ' }
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
return React.createElement(
|
|
331
|
+
Box,
|
|
332
|
+
{ width, flexWrap: 'nowrap' },
|
|
333
|
+
...tabs.map((tab) => {
|
|
334
|
+
const isActive = tab.id === view;
|
|
335
|
+
return React.createElement(
|
|
336
|
+
Text,
|
|
337
|
+
{
|
|
338
|
+
key: tab.id,
|
|
339
|
+
bold: isActive,
|
|
340
|
+
color: isActive ? 'black' : 'gray',
|
|
341
|
+
backgroundColor: isActive ? 'cyan' : undefined
|
|
342
|
+
},
|
|
343
|
+
tab.label
|
|
344
|
+
);
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
280
349
|
function DashboardApp() {
|
|
281
350
|
const { exit } = useApp();
|
|
282
351
|
const { stdout } = useStdout();
|
|
283
352
|
|
|
353
|
+
const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
|
|
354
|
+
const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
|
|
355
|
+
const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
|
|
356
|
+
|
|
284
357
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
285
|
-
const [error, setError] = useState(
|
|
358
|
+
const [error, setError] = useState(initialNormalizedError);
|
|
286
359
|
const [ui, setUi] = useState(createInitialUIState());
|
|
287
360
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
288
361
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
@@ -292,20 +365,49 @@ function createDashboardApp(deps) {
|
|
|
292
365
|
}));
|
|
293
366
|
|
|
294
367
|
const refresh = useCallback(async () => {
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
|
|
295
370
|
try {
|
|
296
371
|
const result = await parseSnapshot();
|
|
297
372
|
|
|
298
373
|
if (result?.ok) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
374
|
+
const nextSnapshot = result.snapshot || null;
|
|
375
|
+
const nextSnapshotHash = safeJsonHash(nextSnapshot);
|
|
376
|
+
|
|
377
|
+
if (nextSnapshotHash !== snapshotHashRef.current) {
|
|
378
|
+
snapshotHashRef.current = nextSnapshotHash;
|
|
379
|
+
setSnapshot(nextSnapshot);
|
|
380
|
+
setLastRefreshAt(now);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (errorHashRef.current !== null) {
|
|
384
|
+
errorHashRef.current = null;
|
|
385
|
+
setError(null);
|
|
386
|
+
setLastRefreshAt(now);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (watchEnabled) {
|
|
390
|
+
setWatchStatus((previous) => (previous === 'watching' ? previous : 'watching'));
|
|
391
|
+
}
|
|
302
392
|
} else {
|
|
303
|
-
|
|
393
|
+
const nextError = toDashboardError(result?.error, 'PARSE_ERROR');
|
|
394
|
+
const nextErrorHash = safeJsonHash(nextError);
|
|
395
|
+
|
|
396
|
+
if (nextErrorHash !== errorHashRef.current) {
|
|
397
|
+
errorHashRef.current = nextErrorHash;
|
|
398
|
+
setError(nextError);
|
|
399
|
+
setLastRefreshAt(now);
|
|
400
|
+
}
|
|
304
401
|
}
|
|
305
402
|
} catch (refreshError) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
403
|
+
const nextError = toDashboardError(refreshError, 'REFRESH_FAILED');
|
|
404
|
+
const nextErrorHash = safeJsonHash(nextError);
|
|
405
|
+
|
|
406
|
+
if (nextErrorHash !== errorHashRef.current) {
|
|
407
|
+
errorHashRef.current = nextErrorHash;
|
|
408
|
+
setError(nextError);
|
|
409
|
+
setLastRefreshAt(now);
|
|
410
|
+
}
|
|
309
411
|
}
|
|
310
412
|
}, [parseSnapshot, watchEnabled]);
|
|
311
413
|
|
|
@@ -335,6 +437,11 @@ function createDashboardApp(deps) {
|
|
|
335
437
|
return;
|
|
336
438
|
}
|
|
337
439
|
|
|
440
|
+
if (input === '3') {
|
|
441
|
+
setUi((previous) => ({ ...previous, view: 'health' }));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
338
445
|
if (key.tab) {
|
|
339
446
|
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
340
447
|
return;
|
|
@@ -384,20 +491,29 @@ function createDashboardApp(deps) {
|
|
|
384
491
|
|
|
385
492
|
const runtime = createWatchRuntime({
|
|
386
493
|
rootPath: rootPath || `${workspacePath}/.specs-fire`,
|
|
387
|
-
debounceMs:
|
|
494
|
+
debounceMs: 200,
|
|
388
495
|
onRefresh: () => {
|
|
389
496
|
void refresh();
|
|
390
497
|
},
|
|
391
498
|
onError: (watchError) => {
|
|
392
|
-
|
|
393
|
-
|
|
499
|
+
const now = new Date().toISOString();
|
|
500
|
+
setWatchStatus((previous) => (previous === 'reconnecting' ? previous : 'reconnecting'));
|
|
501
|
+
|
|
502
|
+
const nextError = toDashboardError(watchError, 'WATCH_ERROR');
|
|
503
|
+
const nextErrorHash = safeJsonHash(nextError);
|
|
504
|
+
if (nextErrorHash !== errorHashRef.current) {
|
|
505
|
+
errorHashRef.current = nextErrorHash;
|
|
506
|
+
setError(nextError);
|
|
507
|
+
setLastRefreshAt(now);
|
|
508
|
+
}
|
|
394
509
|
}
|
|
395
510
|
});
|
|
396
511
|
|
|
397
512
|
runtime.start();
|
|
513
|
+
const fallbackIntervalMs = Math.max(refreshMs, 5000);
|
|
398
514
|
const interval = setInterval(() => {
|
|
399
515
|
void refresh();
|
|
400
|
-
},
|
|
516
|
+
}, fallbackIntervalMs);
|
|
401
517
|
|
|
402
518
|
return () => {
|
|
403
519
|
clearInterval(interval);
|
|
@@ -408,151 +524,124 @@ function createDashboardApp(deps) {
|
|
|
408
524
|
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
409
525
|
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
410
526
|
|
|
411
|
-
const compact = cols < 110;
|
|
412
|
-
const veryCompact = cols < 80 || rows < 22;
|
|
413
|
-
const contentAreaHeight = Math.max(12, rows - (ui.showHelp ? 7 : 5));
|
|
414
|
-
const sectionLineLimit = compact
|
|
415
|
-
? Math.max(2, Math.floor(contentAreaHeight / 5))
|
|
416
|
-
: Math.max(3, Math.floor(contentAreaHeight / 4));
|
|
417
|
-
|
|
418
527
|
const fullWidth = Math.max(40, cols - 1);
|
|
419
|
-
const
|
|
420
|
-
const rightWidth = compact ? fullWidth : Math.max(28, fullWidth - leftWidth - 1);
|
|
421
|
-
|
|
422
|
-
const headerLines = buildHeaderLines(
|
|
423
|
-
snapshot,
|
|
424
|
-
flow,
|
|
425
|
-
workspacePath,
|
|
426
|
-
watchEnabled,
|
|
427
|
-
watchStatus,
|
|
428
|
-
lastRefreshAt,
|
|
429
|
-
ui.view,
|
|
430
|
-
ui.runFilter,
|
|
431
|
-
fullWidth - 4
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
const helpLines = ui.showHelp
|
|
435
|
-
? ['q quit | r refresh | h/? help | tab switch view | 1 runs | 2 overview | f run filter']
|
|
436
|
-
: ['press h to show shortcuts'];
|
|
528
|
+
const compactWidth = Math.max(18, fullWidth - 4);
|
|
437
529
|
|
|
438
|
-
const
|
|
530
|
+
const showHelpLine = ui.showHelp && rows >= 14;
|
|
531
|
+
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
532
|
+
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
439
533
|
|
|
440
|
-
const
|
|
441
|
-
const
|
|
534
|
+
const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
|
|
535
|
+
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
536
|
+
const ultraCompact = rows <= 14;
|
|
442
537
|
|
|
538
|
+
let panelCandidates;
|
|
443
539
|
if (ui.view === 'overview') {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
540
|
+
panelCandidates = [
|
|
541
|
+
{
|
|
542
|
+
key: 'project',
|
|
543
|
+
title: 'Project + Workspace',
|
|
544
|
+
lines: buildOverviewProjectLines(snapshot, compactWidth),
|
|
545
|
+
borderColor: 'green'
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
key: 'intent-status',
|
|
549
|
+
title: 'Intent Status',
|
|
550
|
+
lines: buildOverviewIntentLines(snapshot, compactWidth),
|
|
551
|
+
borderColor: 'yellow'
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
key: 'standards',
|
|
555
|
+
title: 'Standards',
|
|
556
|
+
lines: buildOverviewStandardsLines(snapshot, compactWidth),
|
|
557
|
+
borderColor: 'blue'
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
} else if (ui.view === 'health') {
|
|
561
|
+
panelCandidates = [
|
|
562
|
+
{
|
|
563
|
+
key: 'stats',
|
|
564
|
+
title: 'Stats',
|
|
565
|
+
lines: buildStatsLines(snapshot, compactWidth),
|
|
566
|
+
borderColor: 'magenta'
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
key: 'warnings',
|
|
570
|
+
title: 'Warnings',
|
|
571
|
+
lines: buildWarningsLines(snapshot, compactWidth),
|
|
572
|
+
borderColor: 'red'
|
|
573
|
+
}
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
if (error && showErrorPanel) {
|
|
577
|
+
panelCandidates.push({
|
|
578
|
+
key: 'error-details',
|
|
579
|
+
title: 'Error Details',
|
|
580
|
+
lines: buildErrorLines(error, compactWidth),
|
|
581
|
+
borderColor: 'red'
|
|
582
|
+
});
|
|
583
|
+
}
|
|
470
584
|
} else {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
585
|
+
panelCandidates = [
|
|
586
|
+
{
|
|
587
|
+
key: 'active-runs',
|
|
588
|
+
title: 'Active Runs',
|
|
589
|
+
lines: buildActiveRunLines(snapshot, ui.runFilter, compactWidth),
|
|
590
|
+
borderColor: 'green'
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
key: 'pending',
|
|
594
|
+
title: 'Pending Queue',
|
|
595
|
+
lines: buildPendingLines(snapshot, ui.runFilter, compactWidth),
|
|
596
|
+
borderColor: 'yellow'
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
key: 'completed',
|
|
600
|
+
title: 'Recent Completed Runs',
|
|
601
|
+
lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth),
|
|
602
|
+
borderColor: 'blue'
|
|
603
|
+
}
|
|
604
|
+
];
|
|
605
|
+
}
|
|
481
606
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
lines: buildCompletedLines(snapshot, ui.runFilter, rightWidth - 4),
|
|
485
|
-
borderColor: 'blue'
|
|
486
|
-
});
|
|
487
|
-
rightPanels.push({
|
|
488
|
-
title: 'Stats',
|
|
489
|
-
lines: buildStatsLines(snapshot, rightWidth - 4),
|
|
490
|
-
borderColor: 'magenta'
|
|
491
|
-
});
|
|
492
|
-
rightPanels.push({
|
|
493
|
-
title: 'Warnings',
|
|
494
|
-
lines: buildWarningsLines(snapshot, rightWidth - 4),
|
|
495
|
-
borderColor: 'red'
|
|
496
|
-
});
|
|
607
|
+
if (ultraCompact) {
|
|
608
|
+
panelCandidates = [panelCandidates[0]];
|
|
497
609
|
}
|
|
498
610
|
|
|
611
|
+
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
612
|
+
|
|
613
|
+
const helpText = 'q quit | r refresh | h/? help | tab next view | 1 runs | 2 overview | 3 health | f run filter';
|
|
614
|
+
|
|
499
615
|
return React.createElement(
|
|
500
616
|
Box,
|
|
501
617
|
{ flexDirection: 'column', width: fullWidth },
|
|
502
|
-
React.createElement(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
618
|
+
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, ui.view, ui.runFilter, fullWidth)),
|
|
619
|
+
React.createElement(TabsBar, { view: ui.view, width: fullWidth }),
|
|
620
|
+
showErrorInline
|
|
621
|
+
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
622
|
+
: null,
|
|
623
|
+
showErrorPanel
|
|
624
|
+
? React.createElement(SectionPanel, {
|
|
625
|
+
title: 'Errors',
|
|
626
|
+
lines: buildErrorLines(error, compactWidth),
|
|
627
|
+
width: fullWidth,
|
|
628
|
+
maxLines: 2,
|
|
629
|
+
borderColor: 'red',
|
|
630
|
+
marginBottom: 1
|
|
631
|
+
})
|
|
632
|
+
: null,
|
|
633
|
+
...panels.map((panel, index) => React.createElement(SectionPanel, {
|
|
634
|
+
key: panel.key,
|
|
635
|
+
title: panel.title,
|
|
636
|
+
lines: panel.lines,
|
|
513
637
|
width: fullWidth,
|
|
514
|
-
maxLines:
|
|
515
|
-
borderColor:
|
|
516
|
-
marginBottom: 1
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
React.createElement(
|
|
522
|
-
Box,
|
|
523
|
-
{ flexDirection: 'column', width: leftWidth, marginRight: compact ? 0 : 1 },
|
|
524
|
-
...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
|
|
525
|
-
key: `left-${panel.title}`,
|
|
526
|
-
title: panel.title,
|
|
527
|
-
lines: panel.lines,
|
|
528
|
-
width: leftWidth,
|
|
529
|
-
maxLines: sectionLineLimit,
|
|
530
|
-
borderColor: panel.borderColor,
|
|
531
|
-
marginBottom: index === leftPanels.length - 1 ? 0 : 1
|
|
532
|
-
}))
|
|
533
|
-
),
|
|
534
|
-
React.createElement(
|
|
535
|
-
Box,
|
|
536
|
-
{ flexDirection: 'column', width: rightWidth },
|
|
537
|
-
...rightPanels.map((panel, index) => React.createElement(SectionPanel, {
|
|
538
|
-
key: `right-${panel.title}`,
|
|
539
|
-
title: panel.title,
|
|
540
|
-
lines: panel.lines,
|
|
541
|
-
width: rightWidth,
|
|
542
|
-
maxLines: sectionLineLimit,
|
|
543
|
-
borderColor: panel.borderColor,
|
|
544
|
-
marginBottom: index === rightPanels.length - 1 ? 0 : 1
|
|
545
|
-
}))
|
|
546
|
-
)
|
|
547
|
-
),
|
|
548
|
-
React.createElement(SectionPanel, {
|
|
549
|
-
title: 'Help',
|
|
550
|
-
lines: helpLines,
|
|
551
|
-
width: fullWidth,
|
|
552
|
-
maxLines: 2,
|
|
553
|
-
borderColor: 'gray',
|
|
554
|
-
marginTop: 1
|
|
555
|
-
})
|
|
638
|
+
maxLines: panel.maxLines,
|
|
639
|
+
borderColor: panel.borderColor,
|
|
640
|
+
marginBottom: index === panels.length - 1 ? 0 : 1
|
|
641
|
+
})),
|
|
642
|
+
showHelpLine
|
|
643
|
+
? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
|
|
644
|
+
: null
|
|
556
645
|
);
|
|
557
646
|
}
|
|
558
647
|
|
|
@@ -563,5 +652,7 @@ module.exports = {
|
|
|
563
652
|
createDashboardApp,
|
|
564
653
|
toDashboardError,
|
|
565
654
|
truncate,
|
|
566
|
-
fitLines
|
|
655
|
+
fitLines,
|
|
656
|
+
safeJsonHash,
|
|
657
|
+
allocateSingleColumnPanels
|
|
567
658
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
|
|
5
5
|
"main": "lib/installer.js",
|
|
6
6
|
"bin": {
|