specsmd 0.1.26 → 0.1.27
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 +137 -29
- package/lib/dashboard/tui/store.js +11 -0
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
2
|
-
const { createInitialUIState, cycleView, cycleRunFilter } = require('./store');
|
|
2
|
+
const { createInitialUIState, cycleView, cycleViewBackward, cycleRunFilter } = require('./store');
|
|
3
3
|
|
|
4
4
|
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
5
5
|
if (!error) {
|
|
@@ -45,6 +45,37 @@ function safeJsonHash(value) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function resolveIconSet() {
|
|
49
|
+
const mode = (process.env.SPECSMD_ICON_SET || 'auto').toLowerCase();
|
|
50
|
+
|
|
51
|
+
const ascii = {
|
|
52
|
+
runs: '[R]',
|
|
53
|
+
overview: '[O]',
|
|
54
|
+
health: '[H]',
|
|
55
|
+
runFile: '*'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const nerd = {
|
|
59
|
+
runs: '',
|
|
60
|
+
overview: '',
|
|
61
|
+
health: '',
|
|
62
|
+
runFile: ''
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (mode === 'ascii') {
|
|
66
|
+
return ascii;
|
|
67
|
+
}
|
|
68
|
+
if (mode === 'nerd') {
|
|
69
|
+
return nerd;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const locale = `${process.env.LC_ALL || ''}${process.env.LC_CTYPE || ''}${process.env.LANG || ''}`;
|
|
73
|
+
const isUtf8 = /utf-?8/i.test(locale);
|
|
74
|
+
const looksLikeVsCodeTerminal = (process.env.TERM_PROGRAM || '').toLowerCase().includes('vscode');
|
|
75
|
+
|
|
76
|
+
return isUtf8 && looksLikeVsCodeTerminal ? nerd : ascii;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
function truncate(value, width) {
|
|
49
80
|
const text = String(value ?? '');
|
|
50
81
|
if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
|
|
@@ -121,30 +152,86 @@ function buildErrorLines(error, width) {
|
|
|
121
152
|
return lines.map((line) => truncate(line, width));
|
|
122
153
|
}
|
|
123
154
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
function getCurrentRun(snapshot) {
|
|
156
|
+
const activeRuns = Array.isArray(snapshot?.activeRuns) ? [...snapshot.activeRuns] : [];
|
|
157
|
+
if (activeRuns.length === 0) {
|
|
158
|
+
return null;
|
|
127
159
|
}
|
|
128
160
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
activeRuns.sort((a, b) => {
|
|
162
|
+
const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
|
|
163
|
+
const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
|
|
164
|
+
if (bTime !== aTime) {
|
|
165
|
+
return bTime - aTime;
|
|
166
|
+
}
|
|
167
|
+
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return activeRuns[0] || null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getCurrentPhaseLabel(run, currentWorkItem) {
|
|
174
|
+
const phase = currentWorkItem?.currentPhase || '';
|
|
175
|
+
if (typeof phase === 'string' && phase !== '') {
|
|
176
|
+
return phase.toLowerCase();
|
|
132
177
|
}
|
|
133
178
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
179
|
+
if (run?.hasTestReport) {
|
|
180
|
+
return 'review';
|
|
181
|
+
}
|
|
182
|
+
if (run?.hasPlan) {
|
|
183
|
+
return 'execute';
|
|
184
|
+
}
|
|
185
|
+
return 'plan';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildPhaseTrack(currentPhase) {
|
|
189
|
+
const order = ['plan', 'execute', 'test', 'review'];
|
|
190
|
+
const labels = ['P', 'E', 'T', 'R'];
|
|
191
|
+
const currentIndex = Math.max(0, order.indexOf(currentPhase));
|
|
192
|
+
return labels.map((label, index) => (index === currentIndex ? `[${label}]` : ` ${label} `)).join(' - ');
|
|
193
|
+
}
|
|
140
194
|
|
|
141
|
-
|
|
142
|
-
|
|
195
|
+
function buildCurrentRunLines(snapshot, width) {
|
|
196
|
+
const run = getCurrentRun(snapshot);
|
|
197
|
+
if (!run) {
|
|
198
|
+
return [truncate('No active run', width)];
|
|
143
199
|
}
|
|
144
200
|
|
|
201
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
202
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
203
|
+
const currentWorkItem = workItems.find((item) => item.id === run.currentItem) || workItems.find((item) => item.status === 'in_progress') || workItems[0];
|
|
204
|
+
|
|
205
|
+
const itemId = currentWorkItem?.id || run.currentItem || 'n/a';
|
|
206
|
+
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
207
|
+
const status = currentWorkItem?.status || 'pending';
|
|
208
|
+
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
209
|
+
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
210
|
+
|
|
211
|
+
const lines = [
|
|
212
|
+
`${run.id} [${run.scope}] ${completed}/${workItems.length} items done`,
|
|
213
|
+
`work item: ${itemId}`,
|
|
214
|
+
`mode: ${mode} | status: ${status}`,
|
|
215
|
+
`phase: ${phaseTrack}`
|
|
216
|
+
];
|
|
217
|
+
|
|
145
218
|
return lines.map((line) => truncate(line, width));
|
|
146
219
|
}
|
|
147
220
|
|
|
221
|
+
function buildRunFilesLines(snapshot, width, icons) {
|
|
222
|
+
const run = getCurrentRun(snapshot);
|
|
223
|
+
if (!run) {
|
|
224
|
+
return [truncate('No run files (no active run)', width)];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const files = ['run.md'];
|
|
228
|
+
if (run.hasPlan) files.push('plan.md');
|
|
229
|
+
if (run.hasTestReport) files.push('test-report.md');
|
|
230
|
+
if (run.hasWalkthrough) files.push('walkthrough.md');
|
|
231
|
+
|
|
232
|
+
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
233
|
+
}
|
|
234
|
+
|
|
148
235
|
function buildPendingLines(snapshot, runFilter, width) {
|
|
149
236
|
if (runFilter === 'completed') {
|
|
150
237
|
return [truncate('Hidden by run filter: completed', width)];
|
|
@@ -298,7 +385,8 @@ function createDashboardApp(deps) {
|
|
|
298
385
|
width,
|
|
299
386
|
maxLines,
|
|
300
387
|
borderColor,
|
|
301
|
-
marginBottom
|
|
388
|
+
marginBottom,
|
|
389
|
+
dense
|
|
302
390
|
} = props;
|
|
303
391
|
|
|
304
392
|
const contentWidth = Math.max(18, width - 4);
|
|
@@ -308,9 +396,9 @@ function createDashboardApp(deps) {
|
|
|
308
396
|
Box,
|
|
309
397
|
{
|
|
310
398
|
flexDirection: 'column',
|
|
311
|
-
borderStyle: 'round',
|
|
399
|
+
borderStyle: dense ? 'single' : 'round',
|
|
312
400
|
borderColor: borderColor || 'gray',
|
|
313
|
-
paddingX: 1,
|
|
401
|
+
paddingX: dense ? 0 : 1,
|
|
314
402
|
width,
|
|
315
403
|
marginBottom: marginBottom || 0
|
|
316
404
|
},
|
|
@@ -320,11 +408,11 @@ function createDashboardApp(deps) {
|
|
|
320
408
|
}
|
|
321
409
|
|
|
322
410
|
function TabsBar(props) {
|
|
323
|
-
const { view, width } = props;
|
|
411
|
+
const { view, width, icons } = props;
|
|
324
412
|
const tabs = [
|
|
325
|
-
{ id: 'runs', label:
|
|
326
|
-
{ id: 'overview', label:
|
|
327
|
-
{ id: 'health', label:
|
|
413
|
+
{ id: 'runs', label: ` 1 ${icons.runs} RUNS ` },
|
|
414
|
+
{ id: 'overview', label: ` 2 ${icons.overview} OVERVIEW ` },
|
|
415
|
+
{ id: 'health', label: ` 3 ${icons.health} HEALTH ` }
|
|
328
416
|
];
|
|
329
417
|
|
|
330
418
|
return React.createElement(
|
|
@@ -363,6 +451,7 @@ function createDashboardApp(deps) {
|
|
|
363
451
|
columns: stdout?.columns || process.stdout.columns || 120,
|
|
364
452
|
rows: stdout?.rows || process.stdout.rows || 40
|
|
365
453
|
}));
|
|
454
|
+
const icons = resolveIconSet();
|
|
366
455
|
|
|
367
456
|
const refresh = useCallback(async () => {
|
|
368
457
|
const now = new Date().toISOString();
|
|
@@ -447,6 +536,16 @@ function createDashboardApp(deps) {
|
|
|
447
536
|
return;
|
|
448
537
|
}
|
|
449
538
|
|
|
539
|
+
if (key.rightArrow) {
|
|
540
|
+
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (key.leftArrow) {
|
|
545
|
+
setUi((previous) => ({ ...previous, view: cycleViewBackward(previous.view) }));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
450
549
|
if (input === 'f') {
|
|
451
550
|
setUi((previous) => ({ ...previous, runFilter: cycleRunFilter(previous.runFilter) }));
|
|
452
551
|
}
|
|
@@ -530,6 +629,7 @@ function createDashboardApp(deps) {
|
|
|
530
629
|
const showHelpLine = ui.showHelp && rows >= 14;
|
|
531
630
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
532
631
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
632
|
+
const densePanels = rows <= 28 || cols <= 120;
|
|
533
633
|
|
|
534
634
|
const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
|
|
535
635
|
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
@@ -584,11 +684,17 @@ function createDashboardApp(deps) {
|
|
|
584
684
|
} else {
|
|
585
685
|
panelCandidates = [
|
|
586
686
|
{
|
|
587
|
-
key: '
|
|
588
|
-
title: '
|
|
589
|
-
lines:
|
|
687
|
+
key: 'current-run',
|
|
688
|
+
title: 'Current Run',
|
|
689
|
+
lines: buildCurrentRunLines(snapshot, compactWidth),
|
|
590
690
|
borderColor: 'green'
|
|
591
691
|
},
|
|
692
|
+
{
|
|
693
|
+
key: 'run-files',
|
|
694
|
+
title: 'Run Files',
|
|
695
|
+
lines: buildRunFilesLines(snapshot, compactWidth, icons),
|
|
696
|
+
borderColor: 'yellow'
|
|
697
|
+
},
|
|
592
698
|
{
|
|
593
699
|
key: 'pending',
|
|
594
700
|
title: 'Pending Queue',
|
|
@@ -610,13 +716,13 @@ function createDashboardApp(deps) {
|
|
|
610
716
|
|
|
611
717
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
612
718
|
|
|
613
|
-
const helpText = 'q quit | r refresh | h/? help | tab
|
|
719
|
+
const helpText = 'q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health | f run filter';
|
|
614
720
|
|
|
615
721
|
return React.createElement(
|
|
616
722
|
Box,
|
|
617
723
|
{ flexDirection: 'column', width: fullWidth },
|
|
618
724
|
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 }),
|
|
725
|
+
React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons }),
|
|
620
726
|
showErrorInline
|
|
621
727
|
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
622
728
|
: null,
|
|
@@ -627,7 +733,8 @@ function createDashboardApp(deps) {
|
|
|
627
733
|
width: fullWidth,
|
|
628
734
|
maxLines: 2,
|
|
629
735
|
borderColor: 'red',
|
|
630
|
-
marginBottom: 1
|
|
736
|
+
marginBottom: densePanels ? 0 : 1,
|
|
737
|
+
dense: densePanels
|
|
631
738
|
})
|
|
632
739
|
: null,
|
|
633
740
|
...panels.map((panel, index) => React.createElement(SectionPanel, {
|
|
@@ -637,7 +744,8 @@ function createDashboardApp(deps) {
|
|
|
637
744
|
width: fullWidth,
|
|
638
745
|
maxLines: panel.maxLines,
|
|
639
746
|
borderColor: panel.borderColor,
|
|
640
|
-
marginBottom: index === panels.length - 1 ? 0 : 1
|
|
747
|
+
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
748
|
+
dense: densePanels
|
|
641
749
|
})),
|
|
642
750
|
showHelpLine
|
|
643
751
|
? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
|
|
@@ -16,6 +16,16 @@ function cycleView(current) {
|
|
|
16
16
|
return 'runs';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function cycleViewBackward(current) {
|
|
20
|
+
if (current === 'runs') {
|
|
21
|
+
return 'health';
|
|
22
|
+
}
|
|
23
|
+
if (current === 'overview') {
|
|
24
|
+
return 'runs';
|
|
25
|
+
}
|
|
26
|
+
return 'overview';
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
function cycleRunFilter(current) {
|
|
20
30
|
if (current === 'all') {
|
|
21
31
|
return 'active';
|
|
@@ -29,5 +39,6 @@ function cycleRunFilter(current) {
|
|
|
29
39
|
module.exports = {
|
|
30
40
|
createInitialUIState,
|
|
31
41
|
cycleView,
|
|
42
|
+
cycleViewBackward,
|
|
32
43
|
cycleRunFilter
|
|
33
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
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": {
|