specsmd 0.1.26 → 0.1.27

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