specsmd 0.1.29 → 0.1.31
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/index.js +66 -11
- package/lib/dashboard/tui/app.js +431 -121
- package/lib/dashboard/tui/components/header.js +1 -3
- package/lib/dashboard/tui/components/help-footer.js +1 -1
- package/lib/dashboard/tui/renderer.js +1 -3
- package/lib/dashboard/tui/store.js +1 -13
- package/lib/dashboard/tui/views/runs-view.js +6 -11
- package/package.json +1 -1
package/lib/dashboard/index.js
CHANGED
|
@@ -16,13 +16,41 @@ function parseRefreshMs(raw) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function clearTerminalOutput(stream = process.stdout) {
|
|
19
|
-
if (!stream ||
|
|
19
|
+
if (!stream || typeof stream.write !== 'function') {
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
if (stream.isTTY === false) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof console.clear === 'function') {
|
|
28
|
+
console.clear();
|
|
29
|
+
}
|
|
23
30
|
stream.write('\u001B[2J\u001B[3J\u001B[H');
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
function createInkStdout(stream = process.stdout) {
|
|
34
|
+
if (!stream || typeof stream.write !== 'function') {
|
|
35
|
+
return stream;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isTTY: true,
|
|
40
|
+
get columns() {
|
|
41
|
+
return stream.columns;
|
|
42
|
+
},
|
|
43
|
+
get rows() {
|
|
44
|
+
return stream.rows;
|
|
45
|
+
},
|
|
46
|
+
write: (...args) => stream.write(...args),
|
|
47
|
+
on: (...args) => (typeof stream.on === 'function' ? stream.on(...args) : undefined),
|
|
48
|
+
off: (...args) => (typeof stream.off === 'function' ? stream.off(...args) : undefined),
|
|
49
|
+
once: (...args) => (typeof stream.once === 'function' ? stream.once(...args) : undefined),
|
|
50
|
+
removeListener: (...args) => (typeof stream.removeListener === 'function' ? stream.removeListener(...args) : undefined)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
const FLOW_CONFIG = {
|
|
27
55
|
fire: {
|
|
28
56
|
markerDir: '.specs-fire',
|
|
@@ -38,6 +66,14 @@ const FLOW_CONFIG = {
|
|
|
38
66
|
}
|
|
39
67
|
};
|
|
40
68
|
|
|
69
|
+
function resolveRootPathForFlow(workspacePath, flow) {
|
|
70
|
+
const config = FLOW_CONFIG[flow];
|
|
71
|
+
if (!config) {
|
|
72
|
+
return workspacePath;
|
|
73
|
+
}
|
|
74
|
+
return path.join(workspacePath, config.markerDir);
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
function formatStaticFlowText(flow, snapshot, error) {
|
|
42
78
|
if (flow === 'fire') {
|
|
43
79
|
return formatDashboardText({
|
|
@@ -88,7 +124,7 @@ function formatStaticFlowText(flow, snapshot, error) {
|
|
|
88
124
|
return 'Unsupported flow.';
|
|
89
125
|
}
|
|
90
126
|
|
|
91
|
-
async function runFlowDashboard(options, flow) {
|
|
127
|
+
async function runFlowDashboard(options, flow, availableFlows = []) {
|
|
92
128
|
const workspacePath = path.resolve(options.path || process.cwd());
|
|
93
129
|
const config = FLOW_CONFIG[flow];
|
|
94
130
|
|
|
@@ -98,13 +134,28 @@ async function runFlowDashboard(options, flow) {
|
|
|
98
134
|
return;
|
|
99
135
|
}
|
|
100
136
|
|
|
101
|
-
const
|
|
137
|
+
const flowIds = Array.from(new Set([
|
|
138
|
+
String(flow || '').toLowerCase(),
|
|
139
|
+
...(Array.isArray(availableFlows) ? availableFlows.map((value) => String(value || '').toLowerCase()) : [])
|
|
140
|
+
].filter(Boolean)));
|
|
141
|
+
|
|
102
142
|
const watchEnabled = options.watch !== false;
|
|
103
143
|
const refreshMs = parseRefreshMs(options.refreshMs);
|
|
144
|
+
const parseSnapshotForFlow = async (flowId) => {
|
|
145
|
+
const flowConfig = FLOW_CONFIG[flowId];
|
|
146
|
+
if (!flowConfig) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: {
|
|
150
|
+
code: 'UNSUPPORTED_FLOW',
|
|
151
|
+
message: `Flow \"${flowId}\" is not supported.`
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return flowConfig.parse(workspacePath);
|
|
156
|
+
};
|
|
104
157
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const initialResult = await parseSnapshot();
|
|
158
|
+
const initialResult = await parseSnapshotForFlow(flow);
|
|
108
159
|
clearTerminalOutput();
|
|
109
160
|
|
|
110
161
|
if (!watchEnabled) {
|
|
@@ -127,10 +178,11 @@ async function runFlowDashboard(options, flow) {
|
|
|
127
178
|
const App = createDashboardApp({
|
|
128
179
|
React,
|
|
129
180
|
ink,
|
|
130
|
-
|
|
181
|
+
parseSnapshotForFlow,
|
|
131
182
|
workspacePath,
|
|
132
|
-
rootPath,
|
|
133
183
|
flow,
|
|
184
|
+
availableFlows: flowIds,
|
|
185
|
+
resolveRootPathForFlow: (flowId) => resolveRootPathForFlow(workspacePath, flowId),
|
|
134
186
|
refreshMs,
|
|
135
187
|
watchEnabled,
|
|
136
188
|
initialSnapshot: initialResult.ok ? initialResult.snapshot : null,
|
|
@@ -138,7 +190,9 @@ async function runFlowDashboard(options, flow) {
|
|
|
138
190
|
});
|
|
139
191
|
|
|
140
192
|
const { waitUntilExit } = ink.render(React.createElement(App), {
|
|
141
|
-
exitOnCtrlC: true
|
|
193
|
+
exitOnCtrlC: true,
|
|
194
|
+
stdout: createInkStdout(process.stdout),
|
|
195
|
+
stdin: process.stdin
|
|
142
196
|
});
|
|
143
197
|
|
|
144
198
|
await waitUntilExit();
|
|
@@ -166,7 +220,7 @@ async function run(options = {}) {
|
|
|
166
220
|
console.warn(`Warning: ${detection.warning}`);
|
|
167
221
|
}
|
|
168
222
|
|
|
169
|
-
await runFlowDashboard(options, detection.flow);
|
|
223
|
+
await runFlowDashboard(options, detection.flow, detection.availableFlows);
|
|
170
224
|
}
|
|
171
225
|
|
|
172
226
|
module.exports = {
|
|
@@ -174,5 +228,6 @@ module.exports = {
|
|
|
174
228
|
runFlowDashboard,
|
|
175
229
|
parseRefreshMs,
|
|
176
230
|
formatStaticFlowText,
|
|
177
|
-
clearTerminalOutput
|
|
231
|
+
clearTerminalOutput,
|
|
232
|
+
createInkStdout
|
|
178
233
|
};
|
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
2
|
-
const { createInitialUIState, cycleView, cycleViewBackward
|
|
4
|
+
const { createInitialUIState, cycleView, cycleViewBackward } = require('./store');
|
|
3
5
|
|
|
4
6
|
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
5
7
|
if (!error) {
|
|
@@ -89,15 +91,41 @@ function truncate(value, width) {
|
|
|
89
91
|
return `${text.slice(0, width - 3)}...`;
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
function normalizePanelLine(line) {
|
|
95
|
+
if (line && typeof line === 'object' && !Array.isArray(line)) {
|
|
96
|
+
return {
|
|
97
|
+
text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
|
|
98
|
+
color: line.color,
|
|
99
|
+
bold: Boolean(line.bold)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
text: String(line ?? ''),
|
|
105
|
+
color: undefined,
|
|
106
|
+
bold: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
function fitLines(lines, maxLines, width) {
|
|
93
|
-
const safeLines = (Array.isArray(lines) ? lines : []).map((line) =>
|
|
111
|
+
const safeLines = (Array.isArray(lines) ? lines : []).map((line) => {
|
|
112
|
+
const normalized = normalizePanelLine(line);
|
|
113
|
+
return {
|
|
114
|
+
...normalized,
|
|
115
|
+
text: truncate(normalized.text, width)
|
|
116
|
+
};
|
|
117
|
+
});
|
|
94
118
|
|
|
95
119
|
if (safeLines.length <= maxLines) {
|
|
96
120
|
return safeLines;
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
100
|
-
visible.push(
|
|
124
|
+
visible.push({
|
|
125
|
+
text: truncate(`... +${safeLines.length - visible.length} more`, width),
|
|
126
|
+
color: 'gray',
|
|
127
|
+
bold: false
|
|
128
|
+
});
|
|
101
129
|
return visible;
|
|
102
130
|
}
|
|
103
131
|
|
|
@@ -138,11 +166,11 @@ function buildShortStats(snapshot, flow) {
|
|
|
138
166
|
return `runs ${stats.activeRunsCount || 0}/${stats.completedRuns || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | work ${stats.completedWorkItems || 0}/${stats.totalWorkItems || 0}`;
|
|
139
167
|
}
|
|
140
168
|
|
|
141
|
-
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view,
|
|
169
|
+
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, width) {
|
|
142
170
|
const projectName = snapshot?.project?.name || 'Unnamed project';
|
|
143
171
|
const shortStats = buildShortStats(snapshot, flow);
|
|
144
172
|
|
|
145
|
-
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}
|
|
173
|
+
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view} | ${formatTime(lastRefreshAt)}`;
|
|
146
174
|
|
|
147
175
|
return truncate(line, width);
|
|
148
176
|
}
|
|
@@ -233,25 +261,7 @@ function buildFireCurrentRunLines(snapshot, width) {
|
|
|
233
261
|
return lines.map((line) => truncate(line, width));
|
|
234
262
|
}
|
|
235
263
|
|
|
236
|
-
function
|
|
237
|
-
const run = getCurrentRun(snapshot);
|
|
238
|
-
if (!run) {
|
|
239
|
-
return [truncate('No run files (no active run)', width)];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const files = ['run.md'];
|
|
243
|
-
if (run.hasPlan) files.push('plan.md');
|
|
244
|
-
if (run.hasTestReport) files.push('test-report.md');
|
|
245
|
-
if (run.hasWalkthrough) files.push('walkthrough.md');
|
|
246
|
-
|
|
247
|
-
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function buildFirePendingLines(snapshot, runFilter, width) {
|
|
251
|
-
if (runFilter === 'completed') {
|
|
252
|
-
return [truncate('Hidden by run filter: completed', width)];
|
|
253
|
-
}
|
|
254
|
-
|
|
264
|
+
function buildFirePendingLines(snapshot, width) {
|
|
255
265
|
const pending = snapshot?.pendingItems || [];
|
|
256
266
|
if (pending.length === 0) {
|
|
257
267
|
return [truncate('No pending work items', width)];
|
|
@@ -263,11 +273,7 @@ function buildFirePendingLines(snapshot, runFilter, width) {
|
|
|
263
273
|
});
|
|
264
274
|
}
|
|
265
275
|
|
|
266
|
-
function buildFireCompletedLines(snapshot,
|
|
267
|
-
if (runFilter === 'active') {
|
|
268
|
-
return [truncate('Hidden by run filter: active', width)];
|
|
269
|
-
}
|
|
270
|
-
|
|
276
|
+
function buildFireCompletedLines(snapshot, width) {
|
|
271
277
|
const completedRuns = snapshot?.completedRuns || [];
|
|
272
278
|
if (completedRuns.length === 0) {
|
|
273
279
|
return [truncate('No completed runs yet', width)];
|
|
@@ -406,25 +412,7 @@ function buildAidlcCurrentRunLines(snapshot, width) {
|
|
|
406
412
|
return lines.map((line) => truncate(line, width));
|
|
407
413
|
}
|
|
408
414
|
|
|
409
|
-
function
|
|
410
|
-
const bolt = getCurrentBolt(snapshot);
|
|
411
|
-
if (!bolt) {
|
|
412
|
-
return [truncate('No bolt files (no active bolt)', width)];
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const files = Array.isArray(bolt.files) ? bolt.files : [];
|
|
416
|
-
if (files.length === 0) {
|
|
417
|
-
return [truncate('No markdown files found in active bolt', width)];
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function buildAidlcPendingLines(snapshot, runFilter, width) {
|
|
424
|
-
if (runFilter === 'completed') {
|
|
425
|
-
return [truncate('Hidden by run filter: completed', width)];
|
|
426
|
-
}
|
|
427
|
-
|
|
415
|
+
function buildAidlcPendingLines(snapshot, width) {
|
|
428
416
|
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
429
417
|
if (pendingBolts.length === 0) {
|
|
430
418
|
return [truncate('No queued bolts', width)];
|
|
@@ -439,11 +427,7 @@ function buildAidlcPendingLines(snapshot, runFilter, width) {
|
|
|
439
427
|
});
|
|
440
428
|
}
|
|
441
429
|
|
|
442
|
-
function buildAidlcCompletedLines(snapshot,
|
|
443
|
-
if (runFilter === 'active') {
|
|
444
|
-
return [truncate('Hidden by run filter: active', width)];
|
|
445
|
-
}
|
|
446
|
-
|
|
430
|
+
function buildAidlcCompletedLines(snapshot, width) {
|
|
447
431
|
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
448
432
|
if (completedBolts.length === 0) {
|
|
449
433
|
return [truncate('No completed bolts yet', width)];
|
|
@@ -550,29 +534,7 @@ function buildSimpleCurrentRunLines(snapshot, width) {
|
|
|
550
534
|
return lines.map((line) => truncate(line, width));
|
|
551
535
|
}
|
|
552
536
|
|
|
553
|
-
function
|
|
554
|
-
const spec = getCurrentSpec(snapshot);
|
|
555
|
-
if (!spec) {
|
|
556
|
-
return [truncate('No spec files (no active spec)', width)];
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const files = [];
|
|
560
|
-
if (spec.hasRequirements) files.push('requirements.md');
|
|
561
|
-
if (spec.hasDesign) files.push('design.md');
|
|
562
|
-
if (spec.hasTasks) files.push('tasks.md');
|
|
563
|
-
|
|
564
|
-
if (files.length === 0) {
|
|
565
|
-
return [truncate('No files found in active spec folder', width)];
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function buildSimplePendingLines(snapshot, runFilter, width) {
|
|
572
|
-
if (runFilter === 'completed') {
|
|
573
|
-
return [truncate('Hidden by run filter: completed', width)];
|
|
574
|
-
}
|
|
575
|
-
|
|
537
|
+
function buildSimplePendingLines(snapshot, width) {
|
|
576
538
|
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
577
539
|
if (pendingSpecs.length === 0) {
|
|
578
540
|
return [truncate('No pending specs', width)];
|
|
@@ -583,11 +545,7 @@ function buildSimplePendingLines(snapshot, runFilter, width) {
|
|
|
583
545
|
);
|
|
584
546
|
}
|
|
585
547
|
|
|
586
|
-
function buildSimpleCompletedLines(snapshot,
|
|
587
|
-
if (runFilter === 'active') {
|
|
588
|
-
return [truncate('Hidden by run filter: active', width)];
|
|
589
|
-
}
|
|
590
|
-
|
|
548
|
+
function buildSimpleCompletedLines(snapshot, width) {
|
|
591
549
|
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
592
550
|
if (completedSpecs.length === 0) {
|
|
593
551
|
return [truncate('No completed specs yet', width)];
|
|
@@ -659,37 +617,26 @@ function buildCurrentRunLines(snapshot, width, flow) {
|
|
|
659
617
|
return buildFireCurrentRunLines(snapshot, width);
|
|
660
618
|
}
|
|
661
619
|
|
|
662
|
-
function
|
|
620
|
+
function buildPendingLines(snapshot, width, flow) {
|
|
663
621
|
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
664
622
|
if (effectiveFlow === 'aidlc') {
|
|
665
|
-
return
|
|
623
|
+
return buildAidlcPendingLines(snapshot, width);
|
|
666
624
|
}
|
|
667
625
|
if (effectiveFlow === 'simple') {
|
|
668
|
-
return
|
|
626
|
+
return buildSimplePendingLines(snapshot, width);
|
|
669
627
|
}
|
|
670
|
-
return
|
|
628
|
+
return buildFirePendingLines(snapshot, width);
|
|
671
629
|
}
|
|
672
630
|
|
|
673
|
-
function
|
|
631
|
+
function buildCompletedLines(snapshot, width, flow) {
|
|
674
632
|
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
675
633
|
if (effectiveFlow === 'aidlc') {
|
|
676
|
-
return
|
|
634
|
+
return buildAidlcCompletedLines(snapshot, width);
|
|
677
635
|
}
|
|
678
636
|
if (effectiveFlow === 'simple') {
|
|
679
|
-
return
|
|
637
|
+
return buildSimpleCompletedLines(snapshot, width);
|
|
680
638
|
}
|
|
681
|
-
return
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function buildCompletedLines(snapshot, runFilter, width, flow) {
|
|
685
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
686
|
-
if (effectiveFlow === 'aidlc') {
|
|
687
|
-
return buildAidlcCompletedLines(snapshot, runFilter, width);
|
|
688
|
-
}
|
|
689
|
-
if (effectiveFlow === 'simple') {
|
|
690
|
-
return buildSimpleCompletedLines(snapshot, runFilter, width);
|
|
691
|
-
}
|
|
692
|
-
return buildFireCompletedLines(snapshot, runFilter, width);
|
|
639
|
+
return buildFireCompletedLines(snapshot, width);
|
|
693
640
|
}
|
|
694
641
|
|
|
695
642
|
function buildStatsLines(snapshot, width, flow) {
|
|
@@ -772,6 +719,203 @@ function getPanelTitles(flow, snapshot) {
|
|
|
772
719
|
};
|
|
773
720
|
}
|
|
774
721
|
|
|
722
|
+
function getRunFileEntries(snapshot, flow) {
|
|
723
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
724
|
+
|
|
725
|
+
if (effectiveFlow === 'aidlc') {
|
|
726
|
+
const bolt = getCurrentBolt(snapshot);
|
|
727
|
+
if (!bolt || typeof bolt.path !== 'string') {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
const files = Array.isArray(bolt.files) ? bolt.files : [];
|
|
731
|
+
return files.map((fileName) => ({
|
|
732
|
+
name: fileName,
|
|
733
|
+
path: path.join(bolt.path, fileName)
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (effectiveFlow === 'simple') {
|
|
738
|
+
const spec = getCurrentSpec(snapshot);
|
|
739
|
+
if (!spec || typeof spec.path !== 'string') {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const files = [];
|
|
744
|
+
if (spec.hasRequirements) files.push('requirements.md');
|
|
745
|
+
if (spec.hasDesign) files.push('design.md');
|
|
746
|
+
if (spec.hasTasks) files.push('tasks.md');
|
|
747
|
+
|
|
748
|
+
return files.map((fileName) => ({
|
|
749
|
+
name: fileName,
|
|
750
|
+
path: path.join(spec.path, fileName)
|
|
751
|
+
}));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const run = getCurrentRun(snapshot);
|
|
755
|
+
if (!run || typeof run.folderPath !== 'string') {
|
|
756
|
+
return [];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const files = ['run.md'];
|
|
760
|
+
if (run.hasPlan) files.push('plan.md');
|
|
761
|
+
if (run.hasTestReport) files.push('test-report.md');
|
|
762
|
+
if (run.hasWalkthrough) files.push('walkthrough.md');
|
|
763
|
+
|
|
764
|
+
return files.map((fileName) => ({
|
|
765
|
+
name: fileName,
|
|
766
|
+
path: path.join(run.folderPath, fileName)
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function clampIndex(value, length) {
|
|
771
|
+
if (!Number.isFinite(value)) {
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
if (!Number.isFinite(length) || length <= 0) {
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
return Math.max(0, Math.min(length - 1, Math.floor(value)));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function getNoFileMessage(flow) {
|
|
781
|
+
if (flow === 'aidlc') {
|
|
782
|
+
return 'No bolt files (no active bolt)';
|
|
783
|
+
}
|
|
784
|
+
if (flow === 'simple') {
|
|
785
|
+
return 'No spec files (no active spec)';
|
|
786
|
+
}
|
|
787
|
+
return 'No run files (no active run)';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function buildSelectableRunFileLines(fileEntries, selectedIndex, icons, width, flow) {
|
|
791
|
+
if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
|
|
792
|
+
return [truncate(getNoFileMessage(flow), width)];
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const clampedIndex = clampIndex(selectedIndex, fileEntries.length);
|
|
796
|
+
return fileEntries.map((file, index) => {
|
|
797
|
+
const isSelected = index === clampedIndex;
|
|
798
|
+
const prefix = isSelected ? '>' : ' ';
|
|
799
|
+
return {
|
|
800
|
+
text: truncate(`${prefix} ${icons.runFile} ${file.name}`, width),
|
|
801
|
+
color: isSelected ? 'cyan' : undefined,
|
|
802
|
+
bold: isSelected
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function colorizeMarkdownLine(line, inCodeBlock) {
|
|
808
|
+
const text = String(line ?? '');
|
|
809
|
+
|
|
810
|
+
if (/^\s*```/.test(text)) {
|
|
811
|
+
return {
|
|
812
|
+
color: 'magenta',
|
|
813
|
+
bold: true,
|
|
814
|
+
togglesCodeBlock: true
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (/^\s{0,3}#{1,6}\s+/.test(text)) {
|
|
819
|
+
return {
|
|
820
|
+
color: 'cyan',
|
|
821
|
+
bold: true,
|
|
822
|
+
togglesCodeBlock: false
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (/^\s*[-*+]\s+\[[ xX]\]/.test(text) || /^\s*[-*+]\s+/.test(text) || /^\s*\d+\.\s+/.test(text)) {
|
|
827
|
+
return {
|
|
828
|
+
color: 'yellow',
|
|
829
|
+
bold: false,
|
|
830
|
+
togglesCodeBlock: false
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (/^\s*>\s+/.test(text)) {
|
|
835
|
+
return {
|
|
836
|
+
color: 'gray',
|
|
837
|
+
bold: false,
|
|
838
|
+
togglesCodeBlock: false
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (/^\s*---\s*$/.test(text)) {
|
|
843
|
+
return {
|
|
844
|
+
color: 'yellow',
|
|
845
|
+
bold: false,
|
|
846
|
+
togglesCodeBlock: false
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (inCodeBlock) {
|
|
851
|
+
return {
|
|
852
|
+
color: 'green',
|
|
853
|
+
bold: false,
|
|
854
|
+
togglesCodeBlock: false
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
color: undefined,
|
|
860
|
+
bold: false,
|
|
861
|
+
togglesCodeBlock: false
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
866
|
+
if (!fileEntry || typeof fileEntry.path !== 'string') {
|
|
867
|
+
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
let content;
|
|
871
|
+
try {
|
|
872
|
+
content = fs.readFileSync(fileEntry.path, 'utf8');
|
|
873
|
+
} catch (error) {
|
|
874
|
+
return [{
|
|
875
|
+
text: truncate(`Unable to read ${fileEntry.name}: ${error.message}`, width),
|
|
876
|
+
color: 'red',
|
|
877
|
+
bold: false
|
|
878
|
+
}];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const rawLines = String(content).split(/\r?\n/);
|
|
882
|
+
const headLine = {
|
|
883
|
+
text: truncate(`file: ${fileEntry.path}`, width),
|
|
884
|
+
color: 'cyan',
|
|
885
|
+
bold: true
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const cappedLines = rawLines.slice(0, 300);
|
|
889
|
+
const hiddenLineCount = Math.max(0, rawLines.length - cappedLines.length);
|
|
890
|
+
let inCodeBlock = false;
|
|
891
|
+
|
|
892
|
+
const highlighted = cappedLines.map((rawLine, index) => {
|
|
893
|
+
const prefixedLine = `${String(index + 1).padStart(4, ' ')} | ${rawLine}`;
|
|
894
|
+
const { color, bold, togglesCodeBlock } = colorizeMarkdownLine(rawLine, inCodeBlock);
|
|
895
|
+
if (togglesCodeBlock) {
|
|
896
|
+
inCodeBlock = !inCodeBlock;
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
text: truncate(prefixedLine, width),
|
|
900
|
+
color,
|
|
901
|
+
bold
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
if (hiddenLineCount > 0) {
|
|
906
|
+
highlighted.push({
|
|
907
|
+
text: truncate(`... ${hiddenLineCount} additional lines hidden`, width),
|
|
908
|
+
color: 'gray',
|
|
909
|
+
bold: false
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const clampedOffset = clampIndex(scrollOffset, highlighted.length);
|
|
914
|
+
const body = highlighted.slice(clampedOffset);
|
|
915
|
+
|
|
916
|
+
return [headLine, { text: '', color: undefined, bold: false }, ...body];
|
|
917
|
+
}
|
|
918
|
+
|
|
775
919
|
function allocateSingleColumnPanels(candidates, rowsBudget) {
|
|
776
920
|
const filtered = (candidates || []).filter(Boolean);
|
|
777
921
|
if (filtered.length === 0) {
|
|
@@ -810,9 +954,12 @@ function createDashboardApp(deps) {
|
|
|
810
954
|
React,
|
|
811
955
|
ink,
|
|
812
956
|
parseSnapshot,
|
|
957
|
+
parseSnapshotForFlow,
|
|
813
958
|
workspacePath,
|
|
814
959
|
rootPath,
|
|
815
960
|
flow,
|
|
961
|
+
availableFlows,
|
|
962
|
+
resolveRootPathForFlow,
|
|
816
963
|
refreshMs,
|
|
817
964
|
watchEnabled,
|
|
818
965
|
initialSnapshot,
|
|
@@ -847,7 +994,15 @@ function createDashboardApp(deps) {
|
|
|
847
994
|
marginBottom: marginBottom || 0
|
|
848
995
|
},
|
|
849
996
|
React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
|
|
850
|
-
...visibleLines.map((line, index) => React.createElement(
|
|
997
|
+
...visibleLines.map((line, index) => React.createElement(
|
|
998
|
+
Text,
|
|
999
|
+
{
|
|
1000
|
+
key: `${title}-${index}`,
|
|
1001
|
+
color: line.color,
|
|
1002
|
+
bold: line.bold
|
|
1003
|
+
},
|
|
1004
|
+
line.text
|
|
1005
|
+
))
|
|
851
1006
|
);
|
|
852
1007
|
}
|
|
853
1008
|
|
|
@@ -878,17 +1033,53 @@ function createDashboardApp(deps) {
|
|
|
878
1033
|
);
|
|
879
1034
|
}
|
|
880
1035
|
|
|
1036
|
+
function FlowBar(props) {
|
|
1037
|
+
const { activeFlow, width, flowIds } = props;
|
|
1038
|
+
if (!Array.isArray(flowIds) || flowIds.length <= 1) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return React.createElement(
|
|
1043
|
+
Box,
|
|
1044
|
+
{ width, flexWrap: 'nowrap' },
|
|
1045
|
+
...flowIds.map((flowId) => {
|
|
1046
|
+
const isActive = flowId === activeFlow;
|
|
1047
|
+
return React.createElement(
|
|
1048
|
+
Text,
|
|
1049
|
+
{
|
|
1050
|
+
key: flowId,
|
|
1051
|
+
bold: isActive,
|
|
1052
|
+
color: isActive ? 'black' : 'gray',
|
|
1053
|
+
backgroundColor: isActive ? 'green' : undefined
|
|
1054
|
+
},
|
|
1055
|
+
` ${flowId.toUpperCase()} `
|
|
1056
|
+
);
|
|
1057
|
+
})
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
881
1061
|
function DashboardApp() {
|
|
882
1062
|
const { exit } = useApp();
|
|
883
1063
|
const { stdout } = useStdout();
|
|
884
1064
|
|
|
1065
|
+
const fallbackFlow = (initialSnapshot?.flow || flow || 'fire').toLowerCase();
|
|
1066
|
+
const availableFlowIds = Array.from(new Set(
|
|
1067
|
+
(Array.isArray(availableFlows) && availableFlows.length > 0 ? availableFlows : [fallbackFlow])
|
|
1068
|
+
.map((value) => String(value || '').toLowerCase().trim())
|
|
1069
|
+
.filter(Boolean)
|
|
1070
|
+
));
|
|
1071
|
+
|
|
885
1072
|
const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
|
|
886
1073
|
const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
|
|
887
1074
|
const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
|
|
888
1075
|
|
|
1076
|
+
const [activeFlow, setActiveFlow] = useState(fallbackFlow);
|
|
889
1077
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
890
1078
|
const [error, setError] = useState(initialNormalizedError);
|
|
891
1079
|
const [ui, setUi] = useState(createInitialUIState());
|
|
1080
|
+
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
|
1081
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
1082
|
+
const [previewScroll, setPreviewScroll] = useState(0);
|
|
892
1083
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
893
1084
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
894
1085
|
const [terminalSize, setTerminalSize] = useState(() => ({
|
|
@@ -896,15 +1087,35 @@ function createDashboardApp(deps) {
|
|
|
896
1087
|
rows: stdout?.rows || process.stdout.rows || 40
|
|
897
1088
|
}));
|
|
898
1089
|
const icons = resolveIconSet();
|
|
1090
|
+
const parseSnapshotForActiveFlow = useCallback(async (flowId) => {
|
|
1091
|
+
if (typeof parseSnapshotForFlow === 'function') {
|
|
1092
|
+
return parseSnapshotForFlow(flowId);
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof parseSnapshot === 'function') {
|
|
1095
|
+
return parseSnapshot();
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
ok: false,
|
|
1099
|
+
error: {
|
|
1100
|
+
code: 'PARSE_CALLBACK_MISSING',
|
|
1101
|
+
message: 'Dashboard parser callback is not configured.'
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}, [parseSnapshotForFlow, parseSnapshot]);
|
|
1105
|
+
const runFileEntries = getRunFileEntries(snapshot, activeFlow);
|
|
1106
|
+
const clampedSelectedFileIndex = clampIndex(selectedFileIndex, runFileEntries.length);
|
|
1107
|
+
const selectedFile = runFileEntries[clampedSelectedFileIndex] || null;
|
|
899
1108
|
|
|
900
1109
|
const refresh = useCallback(async () => {
|
|
901
1110
|
const now = new Date().toISOString();
|
|
902
1111
|
|
|
903
1112
|
try {
|
|
904
|
-
const result = await
|
|
1113
|
+
const result = await parseSnapshotForActiveFlow(activeFlow);
|
|
905
1114
|
|
|
906
1115
|
if (result?.ok) {
|
|
907
|
-
const nextSnapshot = result.snapshot
|
|
1116
|
+
const nextSnapshot = result.snapshot
|
|
1117
|
+
? { ...result.snapshot, flow: getEffectiveFlow(activeFlow, result.snapshot) }
|
|
1118
|
+
: null;
|
|
908
1119
|
const nextSnapshotHash = safeJsonHash(nextSnapshot);
|
|
909
1120
|
|
|
910
1121
|
if (nextSnapshotHash !== snapshotHashRef.current) {
|
|
@@ -942,7 +1153,7 @@ function createDashboardApp(deps) {
|
|
|
942
1153
|
setLastRefreshAt(now);
|
|
943
1154
|
}
|
|
944
1155
|
}
|
|
945
|
-
}, [
|
|
1156
|
+
}, [activeFlow, parseSnapshotForActiveFlow, watchEnabled]);
|
|
946
1157
|
|
|
947
1158
|
useInput((input, key) => {
|
|
948
1159
|
if ((key.ctrl && input === 'c') || input === 'q') {
|
|
@@ -955,6 +1166,41 @@ function createDashboardApp(deps) {
|
|
|
955
1166
|
return;
|
|
956
1167
|
}
|
|
957
1168
|
|
|
1169
|
+
if (input === 'v' && ui.view === 'runs') {
|
|
1170
|
+
if (selectedFile) {
|
|
1171
|
+
setPreviewOpen((previous) => !previous);
|
|
1172
|
+
setPreviewScroll(0);
|
|
1173
|
+
}
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (key.escape && previewOpen) {
|
|
1178
|
+
setPreviewOpen(false);
|
|
1179
|
+
setPreviewScroll(0);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
|
|
1184
|
+
const moveDown = key.downArrow || input === 'j';
|
|
1185
|
+
const moveUp = key.upArrow || input === 'k';
|
|
1186
|
+
|
|
1187
|
+
if (previewOpen) {
|
|
1188
|
+
if (moveDown) {
|
|
1189
|
+
setPreviewScroll((previous) => previous + 1);
|
|
1190
|
+
} else if (moveUp) {
|
|
1191
|
+
setPreviewScroll((previous) => Math.max(0, previous - 1));
|
|
1192
|
+
}
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (moveDown) {
|
|
1197
|
+
setSelectedFileIndex((previous) => clampIndex(previous + 1, runFileEntries.length));
|
|
1198
|
+
} else if (moveUp) {
|
|
1199
|
+
setSelectedFileIndex((previous) => clampIndex(previous - 1, runFileEntries.length));
|
|
1200
|
+
}
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
958
1204
|
if (input === 'h' || input === '?') {
|
|
959
1205
|
setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }));
|
|
960
1206
|
return;
|
|
@@ -990,8 +1236,37 @@ function createDashboardApp(deps) {
|
|
|
990
1236
|
return;
|
|
991
1237
|
}
|
|
992
1238
|
|
|
993
|
-
if (input === '
|
|
994
|
-
|
|
1239
|
+
if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
|
|
1240
|
+
snapshotHashRef.current = safeJsonHash(null);
|
|
1241
|
+
errorHashRef.current = null;
|
|
1242
|
+
setSnapshot(null);
|
|
1243
|
+
setError(null);
|
|
1244
|
+
setActiveFlow((previous) => {
|
|
1245
|
+
const index = availableFlowIds.indexOf(previous);
|
|
1246
|
+
const nextIndex = index >= 0
|
|
1247
|
+
? ((index + 1) % availableFlowIds.length)
|
|
1248
|
+
: 0;
|
|
1249
|
+
return availableFlowIds[nextIndex];
|
|
1250
|
+
});
|
|
1251
|
+
setPreviewOpen(false);
|
|
1252
|
+
setPreviewScroll(0);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (input === '[' && availableFlowIds.length > 1) {
|
|
1257
|
+
snapshotHashRef.current = safeJsonHash(null);
|
|
1258
|
+
errorHashRef.current = null;
|
|
1259
|
+
setSnapshot(null);
|
|
1260
|
+
setError(null);
|
|
1261
|
+
setActiveFlow((previous) => {
|
|
1262
|
+
const index = availableFlowIds.indexOf(previous);
|
|
1263
|
+
const nextIndex = index >= 0
|
|
1264
|
+
? ((index - 1 + availableFlowIds.length) % availableFlowIds.length)
|
|
1265
|
+
: 0;
|
|
1266
|
+
return availableFlowIds[nextIndex];
|
|
1267
|
+
});
|
|
1268
|
+
setPreviewOpen(false);
|
|
1269
|
+
setPreviewScroll(0);
|
|
995
1270
|
}
|
|
996
1271
|
});
|
|
997
1272
|
|
|
@@ -999,6 +1274,21 @@ function createDashboardApp(deps) {
|
|
|
999
1274
|
void refresh();
|
|
1000
1275
|
}, [refresh]);
|
|
1001
1276
|
|
|
1277
|
+
useEffect(() => {
|
|
1278
|
+
setSelectedFileIndex((previous) => clampIndex(previous, runFileEntries.length));
|
|
1279
|
+
if (runFileEntries.length === 0) {
|
|
1280
|
+
setPreviewOpen(false);
|
|
1281
|
+
setPreviewScroll(0);
|
|
1282
|
+
}
|
|
1283
|
+
}, [activeFlow, runFileEntries.length, snapshot?.generatedAt]);
|
|
1284
|
+
|
|
1285
|
+
useEffect(() => {
|
|
1286
|
+
if (ui.view !== 'runs') {
|
|
1287
|
+
setPreviewOpen(false);
|
|
1288
|
+
setPreviewScroll(0);
|
|
1289
|
+
}
|
|
1290
|
+
}, [ui.view]);
|
|
1291
|
+
|
|
1002
1292
|
useEffect(() => {
|
|
1003
1293
|
if (!stdout || typeof stdout.on !== 'function') {
|
|
1004
1294
|
setTerminalSize({
|
|
@@ -1032,8 +1322,12 @@ function createDashboardApp(deps) {
|
|
|
1032
1322
|
return undefined;
|
|
1033
1323
|
}
|
|
1034
1324
|
|
|
1325
|
+
const watchRootPath = resolveRootPathForFlow
|
|
1326
|
+
? resolveRootPathForFlow(activeFlow)
|
|
1327
|
+
: (rootPath || `${workspacePath}/.specs-fire`);
|
|
1328
|
+
|
|
1035
1329
|
const runtime = createWatchRuntime({
|
|
1036
|
-
rootPath:
|
|
1330
|
+
rootPath: watchRootPath,
|
|
1037
1331
|
debounceMs: 200,
|
|
1038
1332
|
onRefresh: () => {
|
|
1039
1333
|
void refresh();
|
|
@@ -1062,7 +1356,7 @@ function createDashboardApp(deps) {
|
|
|
1062
1356
|
clearInterval(interval);
|
|
1063
1357
|
void runtime.close();
|
|
1064
1358
|
};
|
|
1065
|
-
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath]);
|
|
1359
|
+
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
|
|
1066
1360
|
|
|
1067
1361
|
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
1068
1362
|
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
@@ -1078,7 +1372,9 @@ function createDashboardApp(deps) {
|
|
|
1078
1372
|
const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
|
|
1079
1373
|
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
1080
1374
|
const ultraCompact = rows <= 14;
|
|
1081
|
-
const panelTitles = getPanelTitles(
|
|
1375
|
+
const panelTitles = getPanelTitles(activeFlow, snapshot);
|
|
1376
|
+
const runFileLines = buildSelectableRunFileLines(runFileEntries, clampedSelectedFileIndex, icons, compactWidth, activeFlow);
|
|
1377
|
+
const previewLines = previewOpen ? buildPreviewLines(selectedFile, compactWidth, previewScroll) : [];
|
|
1082
1378
|
|
|
1083
1379
|
let panelCandidates;
|
|
1084
1380
|
if (ui.view === 'overview') {
|
|
@@ -1086,19 +1382,19 @@ function createDashboardApp(deps) {
|
|
|
1086
1382
|
{
|
|
1087
1383
|
key: 'project',
|
|
1088
1384
|
title: 'Project + Workspace',
|
|
1089
|
-
lines: buildOverviewProjectLines(snapshot, compactWidth,
|
|
1385
|
+
lines: buildOverviewProjectLines(snapshot, compactWidth, activeFlow),
|
|
1090
1386
|
borderColor: 'green'
|
|
1091
1387
|
},
|
|
1092
1388
|
{
|
|
1093
1389
|
key: 'intent-status',
|
|
1094
1390
|
title: 'Intent Status',
|
|
1095
|
-
lines: buildOverviewIntentLines(snapshot, compactWidth,
|
|
1391
|
+
lines: buildOverviewIntentLines(snapshot, compactWidth, activeFlow),
|
|
1096
1392
|
borderColor: 'yellow'
|
|
1097
1393
|
},
|
|
1098
1394
|
{
|
|
1099
1395
|
key: 'standards',
|
|
1100
1396
|
title: 'Standards',
|
|
1101
|
-
lines: buildOverviewStandardsLines(snapshot, compactWidth,
|
|
1397
|
+
lines: buildOverviewStandardsLines(snapshot, compactWidth, activeFlow),
|
|
1102
1398
|
borderColor: 'blue'
|
|
1103
1399
|
}
|
|
1104
1400
|
];
|
|
@@ -1107,7 +1403,7 @@ function createDashboardApp(deps) {
|
|
|
1107
1403
|
{
|
|
1108
1404
|
key: 'stats',
|
|
1109
1405
|
title: 'Stats',
|
|
1110
|
-
lines: buildStatsLines(snapshot, compactWidth,
|
|
1406
|
+
lines: buildStatsLines(snapshot, compactWidth, activeFlow),
|
|
1111
1407
|
borderColor: 'magenta'
|
|
1112
1408
|
},
|
|
1113
1409
|
{
|
|
@@ -1131,42 +1427,56 @@ function createDashboardApp(deps) {
|
|
|
1131
1427
|
{
|
|
1132
1428
|
key: 'current-run',
|
|
1133
1429
|
title: panelTitles.current,
|
|
1134
|
-
lines: buildCurrentRunLines(snapshot, compactWidth,
|
|
1430
|
+
lines: buildCurrentRunLines(snapshot, compactWidth, activeFlow),
|
|
1135
1431
|
borderColor: 'green'
|
|
1136
1432
|
},
|
|
1137
1433
|
{
|
|
1138
1434
|
key: 'run-files',
|
|
1139
1435
|
title: panelTitles.files,
|
|
1140
|
-
lines:
|
|
1436
|
+
lines: runFileLines,
|
|
1141
1437
|
borderColor: 'yellow'
|
|
1142
1438
|
},
|
|
1439
|
+
previewOpen
|
|
1440
|
+
? {
|
|
1441
|
+
key: 'preview',
|
|
1442
|
+
title: `Preview: ${selectedFile?.name || 'unknown'}`,
|
|
1443
|
+
lines: previewLines,
|
|
1444
|
+
borderColor: 'magenta'
|
|
1445
|
+
}
|
|
1446
|
+
: null,
|
|
1143
1447
|
{
|
|
1144
1448
|
key: 'pending',
|
|
1145
1449
|
title: panelTitles.pending,
|
|
1146
|
-
lines: buildPendingLines(snapshot,
|
|
1450
|
+
lines: buildPendingLines(snapshot, compactWidth, activeFlow),
|
|
1147
1451
|
borderColor: 'yellow'
|
|
1148
1452
|
},
|
|
1149
1453
|
{
|
|
1150
1454
|
key: 'completed',
|
|
1151
1455
|
title: panelTitles.completed,
|
|
1152
|
-
lines: buildCompletedLines(snapshot,
|
|
1456
|
+
lines: buildCompletedLines(snapshot, compactWidth, activeFlow),
|
|
1153
1457
|
borderColor: 'blue'
|
|
1154
1458
|
}
|
|
1155
1459
|
];
|
|
1156
1460
|
}
|
|
1157
1461
|
|
|
1158
1462
|
if (ultraCompact) {
|
|
1159
|
-
|
|
1463
|
+
if (previewOpen) {
|
|
1464
|
+
panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === 'current-run' || panel.key === 'preview'));
|
|
1465
|
+
} else {
|
|
1466
|
+
panelCandidates = [panelCandidates[0]];
|
|
1467
|
+
}
|
|
1160
1468
|
}
|
|
1161
1469
|
|
|
1162
1470
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1471
|
+
const flowSwitchHint = availableFlowIds.length > 1 ? ' | [ or ] switch flow' : '';
|
|
1472
|
+
const previewHint = previewOpen ? ' | ↑/↓ scroll preview' : ' | ↑/↓ select file | v preview';
|
|
1473
|
+
const helpText = `q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health${previewHint}${flowSwitchHint}`;
|
|
1165
1474
|
|
|
1166
1475
|
return React.createElement(
|
|
1167
1476
|
Box,
|
|
1168
1477
|
{ flexDirection: 'column', width: fullWidth },
|
|
1169
|
-
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot,
|
|
1478
|
+
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
|
|
1479
|
+
React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
|
|
1170
1480
|
React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons }),
|
|
1171
1481
|
showErrorInline
|
|
1172
1482
|
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
@@ -30,7 +30,6 @@ function renderHeaderLines(params) {
|
|
|
30
30
|
flow,
|
|
31
31
|
workspacePath,
|
|
32
32
|
view,
|
|
33
|
-
runFilter,
|
|
34
33
|
watchEnabled,
|
|
35
34
|
watchStatus,
|
|
36
35
|
lastRefreshAt,
|
|
@@ -43,8 +42,7 @@ function renderHeaderLines(params) {
|
|
|
43
42
|
`path: ${workspacePath}`,
|
|
44
43
|
`updated: ${formatTime(lastRefreshAt)}`,
|
|
45
44
|
`watch: ${watchEnabled ? watchStatus : 'off'}`,
|
|
46
|
-
`view: ${view}
|
|
47
|
-
`filter: ${runFilter}`
|
|
45
|
+
`view: ${view}`
|
|
48
46
|
].join(' | ');
|
|
49
47
|
|
|
50
48
|
const horizontal = '-'.repeat(Math.max(20, Math.min(width || 120, 120)));
|
|
@@ -6,7 +6,7 @@ function renderHelpLines(showHelp, width) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
return [
|
|
9
|
-
truncate('Keys: q quit | r refresh | h/? toggle help | tab cycle view | 1 runs | 2 overview
|
|
9
|
+
truncate('Keys: q quit | r refresh | h/? toggle help | tab cycle view | 1 runs | 2 overview', width)
|
|
10
10
|
];
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -20,7 +20,6 @@ function buildDashboardLines(params) {
|
|
|
20
20
|
flow,
|
|
21
21
|
workspacePath,
|
|
22
22
|
view,
|
|
23
|
-
runFilter,
|
|
24
23
|
watchEnabled,
|
|
25
24
|
watchStatus,
|
|
26
25
|
showHelp,
|
|
@@ -36,7 +35,6 @@ function buildDashboardLines(params) {
|
|
|
36
35
|
flow,
|
|
37
36
|
workspacePath,
|
|
38
37
|
view,
|
|
39
|
-
runFilter,
|
|
40
38
|
watchEnabled,
|
|
41
39
|
watchStatus,
|
|
42
40
|
lastRefreshAt,
|
|
@@ -58,7 +56,7 @@ function buildDashboardLines(params) {
|
|
|
58
56
|
} else if (view === 'overview') {
|
|
59
57
|
lines.push(...renderOverviewViewLines(snapshot, safeWidth));
|
|
60
58
|
} else {
|
|
61
|
-
lines.push(...renderRunsViewLines(snapshot,
|
|
59
|
+
lines.push(...renderRunsViewLines(snapshot, safeWidth));
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
lines.push('');
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
function createInitialUIState() {
|
|
2
2
|
return {
|
|
3
3
|
view: 'runs',
|
|
4
|
-
runFilter: 'all',
|
|
5
4
|
showHelp: true
|
|
6
5
|
};
|
|
7
6
|
}
|
|
@@ -26,19 +25,8 @@ function cycleViewBackward(current) {
|
|
|
26
25
|
return 'overview';
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
function cycleRunFilter(current) {
|
|
30
|
-
if (current === 'all') {
|
|
31
|
-
return 'active';
|
|
32
|
-
}
|
|
33
|
-
if (current === 'active') {
|
|
34
|
-
return 'completed';
|
|
35
|
-
}
|
|
36
|
-
return 'all';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
28
|
module.exports = {
|
|
40
29
|
createInitialUIState,
|
|
41
30
|
cycleView,
|
|
42
|
-
cycleViewBackward
|
|
43
|
-
cycleRunFilter
|
|
31
|
+
cycleViewBackward
|
|
44
32
|
};
|
|
@@ -70,7 +70,7 @@ function renderCompletedRunLines(completedRuns, width) {
|
|
|
70
70
|
return lines.map((line) => truncate(line, width));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function renderRunsViewLines(snapshot,
|
|
73
|
+
function renderRunsViewLines(snapshot, width) {
|
|
74
74
|
const lines = [];
|
|
75
75
|
|
|
76
76
|
if (!snapshot?.initialized) {
|
|
@@ -79,16 +79,11 @@ function renderRunsViewLines(snapshot, runFilter, width) {
|
|
|
79
79
|
return lines.map((line) => truncate(line, width));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (runFilter !== 'active') {
|
|
90
|
-
lines.push(...renderCompletedRunLines(snapshot.completedRuns, width));
|
|
91
|
-
}
|
|
82
|
+
lines.push(...renderActiveRunLines(snapshot.activeRuns, width));
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(...renderPendingQueueLines(snapshot.pendingItems, width));
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(...renderCompletedRunLines(snapshot.completedRuns, width));
|
|
92
87
|
|
|
93
88
|
return lines.map((line) => truncate(line, width));
|
|
94
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
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": {
|