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.
@@ -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 rootPath = path.join(workspacePath, config.markerDir);
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 parseSnapshot = async () => config.parse(workspacePath);
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
- parseSnapshot,
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
  };
@@ -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, cycleRunFilter } = require('./store');
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) => truncate(line, width));
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(truncate(`... +${safeLines.length - visible.length} more`, width));
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, runFilter, width) {
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}/${runFilter} | ${formatTime(lastRefreshAt)}`;
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 buildFireRunFilesLines(snapshot, width, icons) {
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, runFilter, width) {
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 buildAidlcRunFilesLines(snapshot, width, icons) {
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, runFilter, width) {
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 buildSimpleRunFilesLines(snapshot, width, icons) {
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, runFilter, width) {
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 buildRunFilesLines(snapshot, width, icons, flow) {
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, runFilter, width);
634
+ return buildAidlcPendingLines(snapshot, width);
677
635
  }
678
636
  if (effectiveFlow === 'simple') {
679
- return buildSimplePendingLines(snapshot, runFilter, width);
637
+ return buildSimplePendingLines(snapshot, width);
680
638
  }
681
- return buildFirePendingLines(snapshot, runFilter, width);
639
+ return buildFirePendingLines(snapshot, width);
682
640
  }
683
641
 
684
- function buildCompletedLines(snapshot, runFilter, width, flow) {
642
+ function buildCompletedLines(snapshot, width, flow) {
685
643
  const effectiveFlow = getEffectiveFlow(flow, snapshot);
686
644
  if (effectiveFlow === 'aidlc') {
687
- return buildAidlcCompletedLines(snapshot, runFilter, width);
645
+ return buildAidlcCompletedLines(snapshot, width);
688
646
  }
689
647
  if (effectiveFlow === 'simple') {
690
- return buildSimpleCompletedLines(snapshot, runFilter, width);
648
+ return buildSimpleCompletedLines(snapshot, width);
691
649
  }
692
- return buildFireCompletedLines(snapshot, runFilter, width);
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(Text, { key: `${title}-${index}` }, line))
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 parseSnapshot();
1284
+ const result = await parseSnapshotForActiveFlow(activeFlow);
905
1285
 
906
1286
  if (result?.ok) {
907
- const nextSnapshot = result.snapshot || null;
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
- }, [parseSnapshot, watchEnabled]);
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 === 'f') {
994
- setUi((previous) => ({ ...previous, runFilter: cycleRunFilter(previous.runFilter) }));
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: rootPath || `${workspacePath}/.specs-fire`,
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(flow, snapshot);
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, flow),
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, flow),
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, flow),
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, flow),
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, flow),
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: buildRunFilesLines(snapshot, compactWidth, icons, flow),
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, ui.runFilter, compactWidth, flow),
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, ui.runFilter, compactWidth, flow),
1627
+ lines: buildCompletedLines(snapshot, compactWidth, activeFlow),
1153
1628
  borderColor: 'blue'
1154
1629
  }
1155
1630
  ];
1156
1631
  }
1157
1632
 
1158
1633
  if (ultraCompact) {
1159
- panelCandidates = [panelCandidates[0]];
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 helpText = 'q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health | f run filter';
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, flow, watchEnabled, watchStatus, lastRefreshAt, ui.view, ui.runFilter, fullWidth)),
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 | f cycle run filter', width)
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, runFilter, safeWidth));
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, runFilter, width) {
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
- if (runFilter !== 'completed') {
83
- lines.push(...renderActiveRunLines(snapshot.activeRuns, width));
84
- lines.push('');
85
- lines.push(...renderPendingQueueLines(snapshot.pendingItems, width));
86
- lines.push('');
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.30",
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": {