specsmd 0.1.30 → 0.1.32
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 +58 -10
- package/lib/dashboard/tui/app.js +602 -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
|
@@ -30,6 +30,27 @@ function clearTerminalOutput(stream = process.stdout) {
|
|
|
30
30
|
stream.write('\u001B[2J\u001B[3J\u001B[H');
|
|
31
31
|
}
|
|
32
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
|
+
|
|
33
54
|
const FLOW_CONFIG = {
|
|
34
55
|
fire: {
|
|
35
56
|
markerDir: '.specs-fire',
|
|
@@ -45,6 +66,14 @@ const FLOW_CONFIG = {
|
|
|
45
66
|
}
|
|
46
67
|
};
|
|
47
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
|
+
|
|
48
77
|
function formatStaticFlowText(flow, snapshot, error) {
|
|
49
78
|
if (flow === 'fire') {
|
|
50
79
|
return formatDashboardText({
|
|
@@ -95,7 +124,7 @@ function formatStaticFlowText(flow, snapshot, error) {
|
|
|
95
124
|
return 'Unsupported flow.';
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
async function runFlowDashboard(options, flow) {
|
|
127
|
+
async function runFlowDashboard(options, flow, availableFlows = []) {
|
|
99
128
|
const workspacePath = path.resolve(options.path || process.cwd());
|
|
100
129
|
const config = FLOW_CONFIG[flow];
|
|
101
130
|
|
|
@@ -105,13 +134,28 @@ async function runFlowDashboard(options, flow) {
|
|
|
105
134
|
return;
|
|
106
135
|
}
|
|
107
136
|
|
|
108
|
-
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
|
+
|
|
109
142
|
const watchEnabled = options.watch !== false;
|
|
110
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
|
+
};
|
|
111
157
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const initialResult = await parseSnapshot();
|
|
158
|
+
const initialResult = await parseSnapshotForFlow(flow);
|
|
115
159
|
clearTerminalOutput();
|
|
116
160
|
|
|
117
161
|
if (!watchEnabled) {
|
|
@@ -134,10 +178,11 @@ async function runFlowDashboard(options, flow) {
|
|
|
134
178
|
const App = createDashboardApp({
|
|
135
179
|
React,
|
|
136
180
|
ink,
|
|
137
|
-
|
|
181
|
+
parseSnapshotForFlow,
|
|
138
182
|
workspacePath,
|
|
139
|
-
rootPath,
|
|
140
183
|
flow,
|
|
184
|
+
availableFlows: flowIds,
|
|
185
|
+
resolveRootPathForFlow: (flowId) => resolveRootPathForFlow(workspacePath, flowId),
|
|
141
186
|
refreshMs,
|
|
142
187
|
watchEnabled,
|
|
143
188
|
initialSnapshot: initialResult.ok ? initialResult.snapshot : null,
|
|
@@ -145,7 +190,9 @@ async function runFlowDashboard(options, flow) {
|
|
|
145
190
|
});
|
|
146
191
|
|
|
147
192
|
const { waitUntilExit } = ink.render(React.createElement(App), {
|
|
148
|
-
exitOnCtrlC: true
|
|
193
|
+
exitOnCtrlC: true,
|
|
194
|
+
stdout: createInkStdout(process.stdout),
|
|
195
|
+
stdin: process.stdin
|
|
149
196
|
});
|
|
150
197
|
|
|
151
198
|
await waitUntilExit();
|
|
@@ -173,7 +220,7 @@ async function run(options = {}) {
|
|
|
173
220
|
console.warn(`Warning: ${detection.warning}`);
|
|
174
221
|
}
|
|
175
222
|
|
|
176
|
-
await runFlowDashboard(options, detection.flow);
|
|
223
|
+
await runFlowDashboard(options, detection.flow, detection.availableFlows);
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
module.exports = {
|
|
@@ -181,5 +228,6 @@ module.exports = {
|
|
|
181
228
|
runFlowDashboard,
|
|
182
229
|
parseRefreshMs,
|
|
183
230
|
formatStaticFlowText,
|
|
184
|
-
clearTerminalOutput
|
|
231
|
+
clearTerminalOutput,
|
|
232
|
+
createInkStdout
|
|
185
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,52 @@ 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
|
+
selected: Boolean(line.selected)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
text: String(line ?? ''),
|
|
106
|
+
color: undefined,
|
|
107
|
+
bold: false,
|
|
108
|
+
selected: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
function fitLines(lines, maxLines, width) {
|
|
93
|
-
const safeLines = (Array.isArray(lines) ? lines : []).map((line) =>
|
|
113
|
+
const safeLines = (Array.isArray(lines) ? lines : []).map((line) => {
|
|
114
|
+
const normalized = normalizePanelLine(line);
|
|
115
|
+
return {
|
|
116
|
+
...normalized,
|
|
117
|
+
text: truncate(normalized.text, width)
|
|
118
|
+
};
|
|
119
|
+
});
|
|
94
120
|
|
|
95
121
|
if (safeLines.length <= maxLines) {
|
|
96
122
|
return safeLines;
|
|
97
123
|
}
|
|
98
124
|
|
|
125
|
+
const selectedIndex = safeLines.findIndex((line) => line.selected);
|
|
126
|
+
if (selectedIndex >= 0) {
|
|
127
|
+
const windowSize = Math.max(1, maxLines);
|
|
128
|
+
let start = selectedIndex - Math.floor(windowSize / 2);
|
|
129
|
+
start = Math.max(0, start);
|
|
130
|
+
start = Math.min(start, Math.max(0, safeLines.length - windowSize));
|
|
131
|
+
return safeLines.slice(start, start + windowSize);
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
100
|
-
visible.push(
|
|
135
|
+
visible.push({
|
|
136
|
+
text: truncate(`... +${safeLines.length - visible.length} more`, width),
|
|
137
|
+
color: 'gray',
|
|
138
|
+
bold: false
|
|
139
|
+
});
|
|
101
140
|
return visible;
|
|
102
141
|
}
|
|
103
142
|
|
|
@@ -138,11 +177,11 @@ function buildShortStats(snapshot, flow) {
|
|
|
138
177
|
return `runs ${stats.activeRunsCount || 0}/${stats.completedRuns || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | work ${stats.completedWorkItems || 0}/${stats.totalWorkItems || 0}`;
|
|
139
178
|
}
|
|
140
179
|
|
|
141
|
-
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view,
|
|
180
|
+
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, width) {
|
|
142
181
|
const projectName = snapshot?.project?.name || 'Unnamed project';
|
|
143
182
|
const shortStats = buildShortStats(snapshot, flow);
|
|
144
183
|
|
|
145
|
-
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}
|
|
184
|
+
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view} | ${formatTime(lastRefreshAt)}`;
|
|
146
185
|
|
|
147
186
|
return truncate(line, width);
|
|
148
187
|
}
|
|
@@ -233,25 +272,7 @@ function buildFireCurrentRunLines(snapshot, width) {
|
|
|
233
272
|
return lines.map((line) => truncate(line, width));
|
|
234
273
|
}
|
|
235
274
|
|
|
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
|
-
|
|
275
|
+
function buildFirePendingLines(snapshot, width) {
|
|
255
276
|
const pending = snapshot?.pendingItems || [];
|
|
256
277
|
if (pending.length === 0) {
|
|
257
278
|
return [truncate('No pending work items', width)];
|
|
@@ -263,11 +284,7 @@ function buildFirePendingLines(snapshot, runFilter, width) {
|
|
|
263
284
|
});
|
|
264
285
|
}
|
|
265
286
|
|
|
266
|
-
function buildFireCompletedLines(snapshot,
|
|
267
|
-
if (runFilter === 'active') {
|
|
268
|
-
return [truncate('Hidden by run filter: active', width)];
|
|
269
|
-
}
|
|
270
|
-
|
|
287
|
+
function buildFireCompletedLines(snapshot, width) {
|
|
271
288
|
const completedRuns = snapshot?.completedRuns || [];
|
|
272
289
|
if (completedRuns.length === 0) {
|
|
273
290
|
return [truncate('No completed runs yet', width)];
|
|
@@ -406,25 +423,7 @@ function buildAidlcCurrentRunLines(snapshot, width) {
|
|
|
406
423
|
return lines.map((line) => truncate(line, width));
|
|
407
424
|
}
|
|
408
425
|
|
|
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
|
-
|
|
426
|
+
function buildAidlcPendingLines(snapshot, width) {
|
|
428
427
|
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
429
428
|
if (pendingBolts.length === 0) {
|
|
430
429
|
return [truncate('No queued bolts', width)];
|
|
@@ -439,11 +438,7 @@ function buildAidlcPendingLines(snapshot, runFilter, width) {
|
|
|
439
438
|
});
|
|
440
439
|
}
|
|
441
440
|
|
|
442
|
-
function buildAidlcCompletedLines(snapshot,
|
|
443
|
-
if (runFilter === 'active') {
|
|
444
|
-
return [truncate('Hidden by run filter: active', width)];
|
|
445
|
-
}
|
|
446
|
-
|
|
441
|
+
function buildAidlcCompletedLines(snapshot, width) {
|
|
447
442
|
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
448
443
|
if (completedBolts.length === 0) {
|
|
449
444
|
return [truncate('No completed bolts yet', width)];
|
|
@@ -550,29 +545,7 @@ function buildSimpleCurrentRunLines(snapshot, width) {
|
|
|
550
545
|
return lines.map((line) => truncate(line, width));
|
|
551
546
|
}
|
|
552
547
|
|
|
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
|
-
|
|
548
|
+
function buildSimplePendingLines(snapshot, width) {
|
|
576
549
|
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
577
550
|
if (pendingSpecs.length === 0) {
|
|
578
551
|
return [truncate('No pending specs', width)];
|
|
@@ -583,11 +556,7 @@ function buildSimplePendingLines(snapshot, runFilter, width) {
|
|
|
583
556
|
);
|
|
584
557
|
}
|
|
585
558
|
|
|
586
|
-
function buildSimpleCompletedLines(snapshot,
|
|
587
|
-
if (runFilter === 'active') {
|
|
588
|
-
return [truncate('Hidden by run filter: active', width)];
|
|
589
|
-
}
|
|
590
|
-
|
|
559
|
+
function buildSimpleCompletedLines(snapshot, width) {
|
|
591
560
|
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
592
561
|
if (completedSpecs.length === 0) {
|
|
593
562
|
return [truncate('No completed specs yet', width)];
|
|
@@ -659,37 +628,26 @@ function buildCurrentRunLines(snapshot, width, flow) {
|
|
|
659
628
|
return buildFireCurrentRunLines(snapshot, width);
|
|
660
629
|
}
|
|
661
630
|
|
|
662
|
-
function
|
|
663
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
664
|
-
if (effectiveFlow === 'aidlc') {
|
|
665
|
-
return buildAidlcRunFilesLines(snapshot, width, icons);
|
|
666
|
-
}
|
|
667
|
-
if (effectiveFlow === 'simple') {
|
|
668
|
-
return buildSimpleRunFilesLines(snapshot, width, icons);
|
|
669
|
-
}
|
|
670
|
-
return buildFireRunFilesLines(snapshot, width, icons);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function buildPendingLines(snapshot, runFilter, width, flow) {
|
|
631
|
+
function buildPendingLines(snapshot, width, flow) {
|
|
674
632
|
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
675
633
|
if (effectiveFlow === 'aidlc') {
|
|
676
|
-
return buildAidlcPendingLines(snapshot,
|
|
634
|
+
return buildAidlcPendingLines(snapshot, width);
|
|
677
635
|
}
|
|
678
636
|
if (effectiveFlow === 'simple') {
|
|
679
|
-
return buildSimplePendingLines(snapshot,
|
|
637
|
+
return buildSimplePendingLines(snapshot, width);
|
|
680
638
|
}
|
|
681
|
-
return buildFirePendingLines(snapshot,
|
|
639
|
+
return buildFirePendingLines(snapshot, width);
|
|
682
640
|
}
|
|
683
641
|
|
|
684
|
-
function buildCompletedLines(snapshot,
|
|
642
|
+
function buildCompletedLines(snapshot, width, flow) {
|
|
685
643
|
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
686
644
|
if (effectiveFlow === 'aidlc') {
|
|
687
|
-
return buildAidlcCompletedLines(snapshot,
|
|
645
|
+
return buildAidlcCompletedLines(snapshot, width);
|
|
688
646
|
}
|
|
689
647
|
if (effectiveFlow === 'simple') {
|
|
690
|
-
return buildSimpleCompletedLines(snapshot,
|
|
648
|
+
return buildSimpleCompletedLines(snapshot, width);
|
|
691
649
|
}
|
|
692
|
-
return buildFireCompletedLines(snapshot,
|
|
650
|
+
return buildFireCompletedLines(snapshot, width);
|
|
693
651
|
}
|
|
694
652
|
|
|
695
653
|
function buildStatsLines(snapshot, width, flow) {
|
|
@@ -772,6 +730,363 @@ function getPanelTitles(flow, snapshot) {
|
|
|
772
730
|
};
|
|
773
731
|
}
|
|
774
732
|
|
|
733
|
+
function fileExists(filePath) {
|
|
734
|
+
try {
|
|
735
|
+
return fs.statSync(filePath).isFile();
|
|
736
|
+
} catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function listMarkdownFiles(dirPath) {
|
|
742
|
+
try {
|
|
743
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
744
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
745
|
+
.map((entry) => entry.name)
|
|
746
|
+
.sort((a, b) => a.localeCompare(b));
|
|
747
|
+
} catch {
|
|
748
|
+
return [];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function pushFileEntry(entries, seenPaths, candidate) {
|
|
753
|
+
if (!candidate || typeof candidate.path !== 'string' || typeof candidate.label !== 'string') {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (!fileExists(candidate.path)) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (seenPaths.has(candidate.path)) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
seenPaths.add(candidate.path);
|
|
766
|
+
entries.push({
|
|
767
|
+
path: candidate.path,
|
|
768
|
+
label: candidate.label,
|
|
769
|
+
scope: candidate.scope || 'other'
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function collectFireRunFiles(run) {
|
|
774
|
+
if (!run || typeof run.folderPath !== 'string') {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const names = ['run.md'];
|
|
779
|
+
if (run.hasPlan) names.push('plan.md');
|
|
780
|
+
if (run.hasTestReport) names.push('test-report.md');
|
|
781
|
+
if (run.hasWalkthrough) names.push('walkthrough.md');
|
|
782
|
+
|
|
783
|
+
return names.map((fileName) => ({
|
|
784
|
+
label: `${run.id}/${fileName}`,
|
|
785
|
+
path: path.join(run.folderPath, fileName)
|
|
786
|
+
}));
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function collectAidlcBoltFiles(bolt) {
|
|
790
|
+
if (!bolt || typeof bolt.path !== 'string') {
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const fileNames = Array.isArray(bolt.files) && bolt.files.length > 0
|
|
795
|
+
? bolt.files
|
|
796
|
+
: listMarkdownFiles(bolt.path);
|
|
797
|
+
|
|
798
|
+
return fileNames.map((fileName) => ({
|
|
799
|
+
label: `${bolt.id}/${fileName}`,
|
|
800
|
+
path: path.join(bolt.path, fileName)
|
|
801
|
+
}));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function collectSimpleSpecFiles(spec) {
|
|
805
|
+
if (!spec || typeof spec.path !== 'string') {
|
|
806
|
+
return [];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const names = [];
|
|
810
|
+
if (spec.hasRequirements) names.push('requirements.md');
|
|
811
|
+
if (spec.hasDesign) names.push('design.md');
|
|
812
|
+
if (spec.hasTasks) names.push('tasks.md');
|
|
813
|
+
|
|
814
|
+
return names.map((fileName) => ({
|
|
815
|
+
label: `${spec.name}/${fileName}`,
|
|
816
|
+
path: path.join(spec.path, fileName)
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function getRunFileEntries(snapshot, flow) {
|
|
821
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
822
|
+
const entries = [];
|
|
823
|
+
const seenPaths = new Set();
|
|
824
|
+
|
|
825
|
+
if (effectiveFlow === 'aidlc') {
|
|
826
|
+
const bolt = getCurrentBolt(snapshot);
|
|
827
|
+
for (const file of collectAidlcBoltFiles(bolt)) {
|
|
828
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
832
|
+
for (const pendingBolt of pendingBolts) {
|
|
833
|
+
for (const file of collectAidlcBoltFiles(pendingBolt)) {
|
|
834
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
839
|
+
for (const completedBolt of completedBolts) {
|
|
840
|
+
for (const file of collectAidlcBoltFiles(completedBolt)) {
|
|
841
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const intentIds = new Set([
|
|
846
|
+
...pendingBolts.map((item) => item?.intent).filter(Boolean),
|
|
847
|
+
...completedBolts.map((item) => item?.intent).filter(Boolean)
|
|
848
|
+
]);
|
|
849
|
+
|
|
850
|
+
for (const intentId of intentIds) {
|
|
851
|
+
const intentPath = path.join(snapshot?.rootPath || '', 'intents', intentId);
|
|
852
|
+
pushFileEntry(entries, seenPaths, {
|
|
853
|
+
label: `${intentId}/requirements.md`,
|
|
854
|
+
path: path.join(intentPath, 'requirements.md'),
|
|
855
|
+
scope: 'intent'
|
|
856
|
+
});
|
|
857
|
+
pushFileEntry(entries, seenPaths, {
|
|
858
|
+
label: `${intentId}/system-context.md`,
|
|
859
|
+
path: path.join(intentPath, 'system-context.md'),
|
|
860
|
+
scope: 'intent'
|
|
861
|
+
});
|
|
862
|
+
pushFileEntry(entries, seenPaths, {
|
|
863
|
+
label: `${intentId}/units.md`,
|
|
864
|
+
path: path.join(intentPath, 'units.md'),
|
|
865
|
+
scope: 'intent'
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return entries;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (effectiveFlow === 'simple') {
|
|
872
|
+
const spec = getCurrentSpec(snapshot);
|
|
873
|
+
for (const file of collectSimpleSpecFiles(spec)) {
|
|
874
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
878
|
+
for (const pendingSpec of pendingSpecs) {
|
|
879
|
+
for (const file of collectSimpleSpecFiles(pendingSpec)) {
|
|
880
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
885
|
+
for (const completedSpec of completedSpecs) {
|
|
886
|
+
for (const file of collectSimpleSpecFiles(completedSpec)) {
|
|
887
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return entries;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const run = getCurrentRun(snapshot);
|
|
895
|
+
for (const file of collectFireRunFiles(run)) {
|
|
896
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
900
|
+
for (const pendingItem of pendingItems) {
|
|
901
|
+
pushFileEntry(entries, seenPaths, {
|
|
902
|
+
label: `${pendingItem?.intentId || 'intent'}/${pendingItem?.id || 'work-item'}.md`,
|
|
903
|
+
path: pendingItem?.filePath,
|
|
904
|
+
scope: 'upcoming'
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (pendingItem?.intentId) {
|
|
908
|
+
pushFileEntry(entries, seenPaths, {
|
|
909
|
+
label: `${pendingItem.intentId}/brief.md`,
|
|
910
|
+
path: path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
|
|
911
|
+
scope: 'intent'
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
917
|
+
for (const completedRun of completedRuns) {
|
|
918
|
+
for (const file of collectFireRunFiles(completedRun)) {
|
|
919
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const completedIntents = Array.isArray(snapshot?.intents)
|
|
924
|
+
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
925
|
+
: [];
|
|
926
|
+
for (const intent of completedIntents) {
|
|
927
|
+
pushFileEntry(entries, seenPaths, {
|
|
928
|
+
label: `${intent.id}/brief.md`,
|
|
929
|
+
path: path.join(snapshot?.rootPath || '', 'intents', intent.id, 'brief.md'),
|
|
930
|
+
scope: 'intent'
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return entries;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function clampIndex(value, length) {
|
|
938
|
+
if (!Number.isFinite(value)) {
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
if (!Number.isFinite(length) || length <= 0) {
|
|
942
|
+
return 0;
|
|
943
|
+
}
|
|
944
|
+
return Math.max(0, Math.min(length - 1, Math.floor(value)));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function getNoFileMessage(flow) {
|
|
948
|
+
return `No selectable files for ${String(flow || 'flow').toUpperCase()}`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function formatScope(scope) {
|
|
952
|
+
if (scope === 'active') return 'ACTIVE';
|
|
953
|
+
if (scope === 'upcoming') return 'UPNEXT';
|
|
954
|
+
if (scope === 'completed') return 'DONE';
|
|
955
|
+
if (scope === 'intent') return 'INTENT';
|
|
956
|
+
return 'FILE';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function buildSelectableRunFileLines(fileEntries, selectedIndex, icons, width, flow) {
|
|
960
|
+
if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
|
|
961
|
+
return [truncate(getNoFileMessage(flow), width)];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const clampedIndex = clampIndex(selectedIndex, fileEntries.length);
|
|
965
|
+
return fileEntries.map((file, index) => {
|
|
966
|
+
const isSelected = index === clampedIndex;
|
|
967
|
+
const prefix = isSelected ? '>' : ' ';
|
|
968
|
+
const scope = formatScope(file.scope);
|
|
969
|
+
return {
|
|
970
|
+
text: truncate(`${prefix} ${icons.runFile} [${scope}] ${file.label}`, width),
|
|
971
|
+
color: isSelected ? 'cyan' : undefined,
|
|
972
|
+
bold: isSelected,
|
|
973
|
+
selected: isSelected
|
|
974
|
+
};
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function colorizeMarkdownLine(line, inCodeBlock) {
|
|
979
|
+
const text = String(line ?? '');
|
|
980
|
+
|
|
981
|
+
if (/^\s*```/.test(text)) {
|
|
982
|
+
return {
|
|
983
|
+
color: 'magenta',
|
|
984
|
+
bold: true,
|
|
985
|
+
togglesCodeBlock: true
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (/^\s{0,3}#{1,6}\s+/.test(text)) {
|
|
990
|
+
return {
|
|
991
|
+
color: 'cyan',
|
|
992
|
+
bold: true,
|
|
993
|
+
togglesCodeBlock: false
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (/^\s*[-*+]\s+\[[ xX]\]/.test(text) || /^\s*[-*+]\s+/.test(text) || /^\s*\d+\.\s+/.test(text)) {
|
|
998
|
+
return {
|
|
999
|
+
color: 'yellow',
|
|
1000
|
+
bold: false,
|
|
1001
|
+
togglesCodeBlock: false
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (/^\s*>\s+/.test(text)) {
|
|
1006
|
+
return {
|
|
1007
|
+
color: 'gray',
|
|
1008
|
+
bold: false,
|
|
1009
|
+
togglesCodeBlock: false
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (/^\s*---\s*$/.test(text)) {
|
|
1014
|
+
return {
|
|
1015
|
+
color: 'yellow',
|
|
1016
|
+
bold: false,
|
|
1017
|
+
togglesCodeBlock: false
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (inCodeBlock) {
|
|
1022
|
+
return {
|
|
1023
|
+
color: 'green',
|
|
1024
|
+
bold: false,
|
|
1025
|
+
togglesCodeBlock: false
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return {
|
|
1030
|
+
color: undefined,
|
|
1031
|
+
bold: false,
|
|
1032
|
+
togglesCodeBlock: false
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
1037
|
+
if (!fileEntry || typeof fileEntry.path !== 'string') {
|
|
1038
|
+
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
let content;
|
|
1042
|
+
try {
|
|
1043
|
+
content = fs.readFileSync(fileEntry.path, 'utf8');
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
return [{
|
|
1046
|
+
text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
|
|
1047
|
+
color: 'red',
|
|
1048
|
+
bold: false
|
|
1049
|
+
}];
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const rawLines = String(content).split(/\r?\n/);
|
|
1053
|
+
const headLine = {
|
|
1054
|
+
text: truncate(`file: ${fileEntry.path}`, width),
|
|
1055
|
+
color: 'cyan',
|
|
1056
|
+
bold: true
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const cappedLines = rawLines.slice(0, 300);
|
|
1060
|
+
const hiddenLineCount = Math.max(0, rawLines.length - cappedLines.length);
|
|
1061
|
+
let inCodeBlock = false;
|
|
1062
|
+
|
|
1063
|
+
const highlighted = cappedLines.map((rawLine, index) => {
|
|
1064
|
+
const prefixedLine = `${String(index + 1).padStart(4, ' ')} | ${rawLine}`;
|
|
1065
|
+
const { color, bold, togglesCodeBlock } = colorizeMarkdownLine(rawLine, inCodeBlock);
|
|
1066
|
+
if (togglesCodeBlock) {
|
|
1067
|
+
inCodeBlock = !inCodeBlock;
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
text: truncate(prefixedLine, width),
|
|
1071
|
+
color,
|
|
1072
|
+
bold
|
|
1073
|
+
};
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
if (hiddenLineCount > 0) {
|
|
1077
|
+
highlighted.push({
|
|
1078
|
+
text: truncate(`... ${hiddenLineCount} additional lines hidden`, width),
|
|
1079
|
+
color: 'gray',
|
|
1080
|
+
bold: false
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const clampedOffset = clampIndex(scrollOffset, highlighted.length);
|
|
1085
|
+
const body = highlighted.slice(clampedOffset);
|
|
1086
|
+
|
|
1087
|
+
return [headLine, { text: '', color: undefined, bold: false }, ...body];
|
|
1088
|
+
}
|
|
1089
|
+
|
|
775
1090
|
function allocateSingleColumnPanels(candidates, rowsBudget) {
|
|
776
1091
|
const filtered = (candidates || []).filter(Boolean);
|
|
777
1092
|
if (filtered.length === 0) {
|
|
@@ -810,9 +1125,12 @@ function createDashboardApp(deps) {
|
|
|
810
1125
|
React,
|
|
811
1126
|
ink,
|
|
812
1127
|
parseSnapshot,
|
|
1128
|
+
parseSnapshotForFlow,
|
|
813
1129
|
workspacePath,
|
|
814
1130
|
rootPath,
|
|
815
1131
|
flow,
|
|
1132
|
+
availableFlows,
|
|
1133
|
+
resolveRootPathForFlow,
|
|
816
1134
|
refreshMs,
|
|
817
1135
|
watchEnabled,
|
|
818
1136
|
initialSnapshot,
|
|
@@ -847,7 +1165,15 @@ function createDashboardApp(deps) {
|
|
|
847
1165
|
marginBottom: marginBottom || 0
|
|
848
1166
|
},
|
|
849
1167
|
React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
|
|
850
|
-
...visibleLines.map((line, index) => React.createElement(
|
|
1168
|
+
...visibleLines.map((line, index) => React.createElement(
|
|
1169
|
+
Text,
|
|
1170
|
+
{
|
|
1171
|
+
key: `${title}-${index}`,
|
|
1172
|
+
color: line.color,
|
|
1173
|
+
bold: line.bold
|
|
1174
|
+
},
|
|
1175
|
+
line.text
|
|
1176
|
+
))
|
|
851
1177
|
);
|
|
852
1178
|
}
|
|
853
1179
|
|
|
@@ -878,17 +1204,53 @@ function createDashboardApp(deps) {
|
|
|
878
1204
|
);
|
|
879
1205
|
}
|
|
880
1206
|
|
|
1207
|
+
function FlowBar(props) {
|
|
1208
|
+
const { activeFlow, width, flowIds } = props;
|
|
1209
|
+
if (!Array.isArray(flowIds) || flowIds.length <= 1) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return React.createElement(
|
|
1214
|
+
Box,
|
|
1215
|
+
{ width, flexWrap: 'nowrap' },
|
|
1216
|
+
...flowIds.map((flowId) => {
|
|
1217
|
+
const isActive = flowId === activeFlow;
|
|
1218
|
+
return React.createElement(
|
|
1219
|
+
Text,
|
|
1220
|
+
{
|
|
1221
|
+
key: flowId,
|
|
1222
|
+
bold: isActive,
|
|
1223
|
+
color: isActive ? 'black' : 'gray',
|
|
1224
|
+
backgroundColor: isActive ? 'green' : undefined
|
|
1225
|
+
},
|
|
1226
|
+
` ${flowId.toUpperCase()} `
|
|
1227
|
+
);
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
881
1232
|
function DashboardApp() {
|
|
882
1233
|
const { exit } = useApp();
|
|
883
1234
|
const { stdout } = useStdout();
|
|
884
1235
|
|
|
1236
|
+
const fallbackFlow = (initialSnapshot?.flow || flow || 'fire').toLowerCase();
|
|
1237
|
+
const availableFlowIds = Array.from(new Set(
|
|
1238
|
+
(Array.isArray(availableFlows) && availableFlows.length > 0 ? availableFlows : [fallbackFlow])
|
|
1239
|
+
.map((value) => String(value || '').toLowerCase().trim())
|
|
1240
|
+
.filter(Boolean)
|
|
1241
|
+
));
|
|
1242
|
+
|
|
885
1243
|
const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
|
|
886
1244
|
const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
|
|
887
1245
|
const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
|
|
888
1246
|
|
|
1247
|
+
const [activeFlow, setActiveFlow] = useState(fallbackFlow);
|
|
889
1248
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
890
1249
|
const [error, setError] = useState(initialNormalizedError);
|
|
891
1250
|
const [ui, setUi] = useState(createInitialUIState());
|
|
1251
|
+
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
|
1252
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
1253
|
+
const [previewScroll, setPreviewScroll] = useState(0);
|
|
892
1254
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
893
1255
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
894
1256
|
const [terminalSize, setTerminalSize] = useState(() => ({
|
|
@@ -896,15 +1258,35 @@ function createDashboardApp(deps) {
|
|
|
896
1258
|
rows: stdout?.rows || process.stdout.rows || 40
|
|
897
1259
|
}));
|
|
898
1260
|
const icons = resolveIconSet();
|
|
1261
|
+
const parseSnapshotForActiveFlow = useCallback(async (flowId) => {
|
|
1262
|
+
if (typeof parseSnapshotForFlow === 'function') {
|
|
1263
|
+
return parseSnapshotForFlow(flowId);
|
|
1264
|
+
}
|
|
1265
|
+
if (typeof parseSnapshot === 'function') {
|
|
1266
|
+
return parseSnapshot();
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
ok: false,
|
|
1270
|
+
error: {
|
|
1271
|
+
code: 'PARSE_CALLBACK_MISSING',
|
|
1272
|
+
message: 'Dashboard parser callback is not configured.'
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
}, [parseSnapshotForFlow, parseSnapshot]);
|
|
1276
|
+
const runFileEntries = getRunFileEntries(snapshot, activeFlow);
|
|
1277
|
+
const clampedSelectedFileIndex = clampIndex(selectedFileIndex, runFileEntries.length);
|
|
1278
|
+
const selectedFile = runFileEntries[clampedSelectedFileIndex] || null;
|
|
899
1279
|
|
|
900
1280
|
const refresh = useCallback(async () => {
|
|
901
1281
|
const now = new Date().toISOString();
|
|
902
1282
|
|
|
903
1283
|
try {
|
|
904
|
-
const result = await
|
|
1284
|
+
const result = await parseSnapshotForActiveFlow(activeFlow);
|
|
905
1285
|
|
|
906
1286
|
if (result?.ok) {
|
|
907
|
-
const nextSnapshot = result.snapshot
|
|
1287
|
+
const nextSnapshot = result.snapshot
|
|
1288
|
+
? { ...result.snapshot, flow: getEffectiveFlow(activeFlow, result.snapshot) }
|
|
1289
|
+
: null;
|
|
908
1290
|
const nextSnapshotHash = safeJsonHash(nextSnapshot);
|
|
909
1291
|
|
|
910
1292
|
if (nextSnapshotHash !== snapshotHashRef.current) {
|
|
@@ -942,7 +1324,7 @@ function createDashboardApp(deps) {
|
|
|
942
1324
|
setLastRefreshAt(now);
|
|
943
1325
|
}
|
|
944
1326
|
}
|
|
945
|
-
}, [
|
|
1327
|
+
}, [activeFlow, parseSnapshotForActiveFlow, watchEnabled]);
|
|
946
1328
|
|
|
947
1329
|
useInput((input, key) => {
|
|
948
1330
|
if ((key.ctrl && input === 'c') || input === 'q') {
|
|
@@ -955,6 +1337,41 @@ function createDashboardApp(deps) {
|
|
|
955
1337
|
return;
|
|
956
1338
|
}
|
|
957
1339
|
|
|
1340
|
+
if (input === 'v' && ui.view === 'runs') {
|
|
1341
|
+
if (selectedFile) {
|
|
1342
|
+
setPreviewOpen((previous) => !previous);
|
|
1343
|
+
setPreviewScroll(0);
|
|
1344
|
+
}
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (key.escape && previewOpen) {
|
|
1349
|
+
setPreviewOpen(false);
|
|
1350
|
+
setPreviewScroll(0);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
|
|
1355
|
+
const moveDown = key.downArrow || input === 'j';
|
|
1356
|
+
const moveUp = key.upArrow || input === 'k';
|
|
1357
|
+
|
|
1358
|
+
if (previewOpen) {
|
|
1359
|
+
if (moveDown) {
|
|
1360
|
+
setPreviewScroll((previous) => previous + 1);
|
|
1361
|
+
} else if (moveUp) {
|
|
1362
|
+
setPreviewScroll((previous) => Math.max(0, previous - 1));
|
|
1363
|
+
}
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (moveDown) {
|
|
1368
|
+
setSelectedFileIndex((previous) => clampIndex(previous + 1, runFileEntries.length));
|
|
1369
|
+
} else if (moveUp) {
|
|
1370
|
+
setSelectedFileIndex((previous) => clampIndex(previous - 1, runFileEntries.length));
|
|
1371
|
+
}
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
958
1375
|
if (input === 'h' || input === '?') {
|
|
959
1376
|
setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }));
|
|
960
1377
|
return;
|
|
@@ -990,8 +1407,37 @@ function createDashboardApp(deps) {
|
|
|
990
1407
|
return;
|
|
991
1408
|
}
|
|
992
1409
|
|
|
993
|
-
if (input === '
|
|
994
|
-
|
|
1410
|
+
if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
|
|
1411
|
+
snapshotHashRef.current = safeJsonHash(null);
|
|
1412
|
+
errorHashRef.current = null;
|
|
1413
|
+
setSnapshot(null);
|
|
1414
|
+
setError(null);
|
|
1415
|
+
setActiveFlow((previous) => {
|
|
1416
|
+
const index = availableFlowIds.indexOf(previous);
|
|
1417
|
+
const nextIndex = index >= 0
|
|
1418
|
+
? ((index + 1) % availableFlowIds.length)
|
|
1419
|
+
: 0;
|
|
1420
|
+
return availableFlowIds[nextIndex];
|
|
1421
|
+
});
|
|
1422
|
+
setPreviewOpen(false);
|
|
1423
|
+
setPreviewScroll(0);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (input === '[' && availableFlowIds.length > 1) {
|
|
1428
|
+
snapshotHashRef.current = safeJsonHash(null);
|
|
1429
|
+
errorHashRef.current = null;
|
|
1430
|
+
setSnapshot(null);
|
|
1431
|
+
setError(null);
|
|
1432
|
+
setActiveFlow((previous) => {
|
|
1433
|
+
const index = availableFlowIds.indexOf(previous);
|
|
1434
|
+
const nextIndex = index >= 0
|
|
1435
|
+
? ((index - 1 + availableFlowIds.length) % availableFlowIds.length)
|
|
1436
|
+
: 0;
|
|
1437
|
+
return availableFlowIds[nextIndex];
|
|
1438
|
+
});
|
|
1439
|
+
setPreviewOpen(false);
|
|
1440
|
+
setPreviewScroll(0);
|
|
995
1441
|
}
|
|
996
1442
|
});
|
|
997
1443
|
|
|
@@ -999,6 +1445,21 @@ function createDashboardApp(deps) {
|
|
|
999
1445
|
void refresh();
|
|
1000
1446
|
}, [refresh]);
|
|
1001
1447
|
|
|
1448
|
+
useEffect(() => {
|
|
1449
|
+
setSelectedFileIndex((previous) => clampIndex(previous, runFileEntries.length));
|
|
1450
|
+
if (runFileEntries.length === 0) {
|
|
1451
|
+
setPreviewOpen(false);
|
|
1452
|
+
setPreviewScroll(0);
|
|
1453
|
+
}
|
|
1454
|
+
}, [activeFlow, runFileEntries.length, snapshot?.generatedAt]);
|
|
1455
|
+
|
|
1456
|
+
useEffect(() => {
|
|
1457
|
+
if (ui.view !== 'runs') {
|
|
1458
|
+
setPreviewOpen(false);
|
|
1459
|
+
setPreviewScroll(0);
|
|
1460
|
+
}
|
|
1461
|
+
}, [ui.view]);
|
|
1462
|
+
|
|
1002
1463
|
useEffect(() => {
|
|
1003
1464
|
if (!stdout || typeof stdout.on !== 'function') {
|
|
1004
1465
|
setTerminalSize({
|
|
@@ -1032,8 +1493,12 @@ function createDashboardApp(deps) {
|
|
|
1032
1493
|
return undefined;
|
|
1033
1494
|
}
|
|
1034
1495
|
|
|
1496
|
+
const watchRootPath = resolveRootPathForFlow
|
|
1497
|
+
? resolveRootPathForFlow(activeFlow)
|
|
1498
|
+
: (rootPath || `${workspacePath}/.specs-fire`);
|
|
1499
|
+
|
|
1035
1500
|
const runtime = createWatchRuntime({
|
|
1036
|
-
rootPath:
|
|
1501
|
+
rootPath: watchRootPath,
|
|
1037
1502
|
debounceMs: 200,
|
|
1038
1503
|
onRefresh: () => {
|
|
1039
1504
|
void refresh();
|
|
@@ -1062,7 +1527,7 @@ function createDashboardApp(deps) {
|
|
|
1062
1527
|
clearInterval(interval);
|
|
1063
1528
|
void runtime.close();
|
|
1064
1529
|
};
|
|
1065
|
-
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath]);
|
|
1530
|
+
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
|
|
1066
1531
|
|
|
1067
1532
|
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
1068
1533
|
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
@@ -1078,7 +1543,9 @@ function createDashboardApp(deps) {
|
|
|
1078
1543
|
const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
|
|
1079
1544
|
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
1080
1545
|
const ultraCompact = rows <= 14;
|
|
1081
|
-
const panelTitles = getPanelTitles(
|
|
1546
|
+
const panelTitles = getPanelTitles(activeFlow, snapshot);
|
|
1547
|
+
const runFileLines = buildSelectableRunFileLines(runFileEntries, clampedSelectedFileIndex, icons, compactWidth, activeFlow);
|
|
1548
|
+
const previewLines = previewOpen ? buildPreviewLines(selectedFile, compactWidth, previewScroll) : [];
|
|
1082
1549
|
|
|
1083
1550
|
let panelCandidates;
|
|
1084
1551
|
if (ui.view === 'overview') {
|
|
@@ -1086,19 +1553,19 @@ function createDashboardApp(deps) {
|
|
|
1086
1553
|
{
|
|
1087
1554
|
key: 'project',
|
|
1088
1555
|
title: 'Project + Workspace',
|
|
1089
|
-
lines: buildOverviewProjectLines(snapshot, compactWidth,
|
|
1556
|
+
lines: buildOverviewProjectLines(snapshot, compactWidth, activeFlow),
|
|
1090
1557
|
borderColor: 'green'
|
|
1091
1558
|
},
|
|
1092
1559
|
{
|
|
1093
1560
|
key: 'intent-status',
|
|
1094
1561
|
title: 'Intent Status',
|
|
1095
|
-
lines: buildOverviewIntentLines(snapshot, compactWidth,
|
|
1562
|
+
lines: buildOverviewIntentLines(snapshot, compactWidth, activeFlow),
|
|
1096
1563
|
borderColor: 'yellow'
|
|
1097
1564
|
},
|
|
1098
1565
|
{
|
|
1099
1566
|
key: 'standards',
|
|
1100
1567
|
title: 'Standards',
|
|
1101
|
-
lines: buildOverviewStandardsLines(snapshot, compactWidth,
|
|
1568
|
+
lines: buildOverviewStandardsLines(snapshot, compactWidth, activeFlow),
|
|
1102
1569
|
borderColor: 'blue'
|
|
1103
1570
|
}
|
|
1104
1571
|
];
|
|
@@ -1107,7 +1574,7 @@ function createDashboardApp(deps) {
|
|
|
1107
1574
|
{
|
|
1108
1575
|
key: 'stats',
|
|
1109
1576
|
title: 'Stats',
|
|
1110
|
-
lines: buildStatsLines(snapshot, compactWidth,
|
|
1577
|
+
lines: buildStatsLines(snapshot, compactWidth, activeFlow),
|
|
1111
1578
|
borderColor: 'magenta'
|
|
1112
1579
|
},
|
|
1113
1580
|
{
|
|
@@ -1131,42 +1598,56 @@ function createDashboardApp(deps) {
|
|
|
1131
1598
|
{
|
|
1132
1599
|
key: 'current-run',
|
|
1133
1600
|
title: panelTitles.current,
|
|
1134
|
-
lines: buildCurrentRunLines(snapshot, compactWidth,
|
|
1601
|
+
lines: buildCurrentRunLines(snapshot, compactWidth, activeFlow),
|
|
1135
1602
|
borderColor: 'green'
|
|
1136
1603
|
},
|
|
1137
1604
|
{
|
|
1138
1605
|
key: 'run-files',
|
|
1139
1606
|
title: panelTitles.files,
|
|
1140
|
-
lines:
|
|
1607
|
+
lines: runFileLines,
|
|
1141
1608
|
borderColor: 'yellow'
|
|
1142
1609
|
},
|
|
1610
|
+
previewOpen
|
|
1611
|
+
? {
|
|
1612
|
+
key: 'preview',
|
|
1613
|
+
title: `Preview: ${selectedFile?.label || 'unknown'}`,
|
|
1614
|
+
lines: previewLines,
|
|
1615
|
+
borderColor: 'magenta'
|
|
1616
|
+
}
|
|
1617
|
+
: null,
|
|
1143
1618
|
{
|
|
1144
1619
|
key: 'pending',
|
|
1145
1620
|
title: panelTitles.pending,
|
|
1146
|
-
lines: buildPendingLines(snapshot,
|
|
1621
|
+
lines: buildPendingLines(snapshot, compactWidth, activeFlow),
|
|
1147
1622
|
borderColor: 'yellow'
|
|
1148
1623
|
},
|
|
1149
1624
|
{
|
|
1150
1625
|
key: 'completed',
|
|
1151
1626
|
title: panelTitles.completed,
|
|
1152
|
-
lines: buildCompletedLines(snapshot,
|
|
1627
|
+
lines: buildCompletedLines(snapshot, compactWidth, activeFlow),
|
|
1153
1628
|
borderColor: 'blue'
|
|
1154
1629
|
}
|
|
1155
1630
|
];
|
|
1156
1631
|
}
|
|
1157
1632
|
|
|
1158
1633
|
if (ultraCompact) {
|
|
1159
|
-
|
|
1634
|
+
if (previewOpen) {
|
|
1635
|
+
panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === 'current-run' || panel.key === 'preview'));
|
|
1636
|
+
} else {
|
|
1637
|
+
panelCandidates = [panelCandidates[0]];
|
|
1638
|
+
}
|
|
1160
1639
|
}
|
|
1161
1640
|
|
|
1162
1641
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1642
|
+
const flowSwitchHint = availableFlowIds.length > 1 ? ' | [ or ] switch flow' : '';
|
|
1643
|
+
const previewHint = previewOpen ? ' | ↑/↓ scroll preview' : ' | ↑/↓ select file | v preview';
|
|
1644
|
+
const helpText = `q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health${previewHint}${flowSwitchHint}`;
|
|
1165
1645
|
|
|
1166
1646
|
return React.createElement(
|
|
1167
1647
|
Box,
|
|
1168
1648
|
{ flexDirection: 'column', width: fullWidth },
|
|
1169
|
-
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot,
|
|
1649
|
+
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
|
|
1650
|
+
React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
|
|
1170
1651
|
React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons }),
|
|
1171
1652
|
showErrorInline
|
|
1172
1653
|
? 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.32",
|
|
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": {
|