specsmd 0.1.25 → 0.1.26

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.
@@ -32,6 +32,19 @@ function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
32
32
  };
33
33
  }
34
34
 
35
+ function safeJsonHash(value) {
36
+ try {
37
+ return JSON.stringify(value, (key, nestedValue) => {
38
+ if (key === 'generatedAt') {
39
+ return undefined;
40
+ }
41
+ return nestedValue;
42
+ });
43
+ } catch {
44
+ return String(value);
45
+ }
46
+ }
47
+
35
48
  function truncate(value, width) {
36
49
  const text = String(value ?? '');
37
50
  if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
@@ -46,8 +59,7 @@ function truncate(value, width) {
46
59
  }
47
60
 
48
61
  function fitLines(lines, maxLines, width) {
49
- const safeLines = (Array.isArray(lines) ? lines : [])
50
- .map((line) => truncate(line, width));
62
+ const safeLines = (Array.isArray(lines) ? lines : []).map((line) => truncate(line, width));
51
63
 
52
64
  if (safeLines.length <= maxLines) {
53
65
  return safeLines;
@@ -71,14 +83,22 @@ function formatTime(value) {
71
83
  return date.toLocaleTimeString();
72
84
  }
73
85
 
74
- function buildHeaderLines(snapshot, flow, workspacePath, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
86
+ function buildShortStats(snapshot) {
87
+ if (!snapshot?.initialized) {
88
+ return 'init: waiting for state.yaml';
89
+ }
90
+
91
+ const stats = snapshot.stats;
92
+ return `runs ${stats.activeRunsCount}/${stats.completedRuns} | intents ${stats.completedIntents}/${stats.totalIntents} | work ${stats.completedWorkItems}/${stats.totalWorkItems}`;
93
+ }
94
+
95
+ function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
75
96
  const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
97
+ const shortStats = buildShortStats(snapshot);
76
98
 
77
- return [
78
- `specsmd dashboard | ${flow.toUpperCase()} | ${projectName}`,
79
- `path: ${workspacePath}`,
80
- `updated: ${formatTime(lastRefreshAt)} | watch: ${watchEnabled ? watchStatus : 'off'} | view: ${view} | filter: ${runFilter}`
81
- ].map((line) => truncate(line, width));
99
+ const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}/${runFilter} | ${formatTime(lastRefreshAt)}`;
100
+
101
+ return truncate(line, width);
82
102
  }
83
103
 
84
104
  function buildErrorLines(error, width) {
@@ -86,9 +106,7 @@ function buildErrorLines(error, width) {
86
106
  return [];
87
107
  }
88
108
 
89
- const lines = [
90
- `[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`
91
- ];
109
+ const lines = [`[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`];
92
110
 
93
111
  if (error.details) {
94
112
  lines.push(`details: ${error.details}`);
@@ -105,7 +123,7 @@ function buildErrorLines(error, width) {
105
123
 
106
124
  function buildActiveRunLines(snapshot, runFilter, width) {
107
125
  if (runFilter === 'completed') {
108
- return [truncate('Hidden by active filter: completed', width)];
126
+ return [truncate('Hidden by run filter: completed', width)];
109
127
  }
110
128
 
111
129
  const activeRuns = snapshot?.activeRuns || [];
@@ -119,14 +137,9 @@ function buildActiveRunLines(snapshot, runFilter, width) {
119
137
  const workItems = Array.isArray(run.workItems) ? run.workItems : [];
120
138
  const completed = workItems.filter((item) => item.status === 'completed').length;
121
139
  const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
122
- const artifacts = [
123
- run.hasPlan ? 'plan' : null,
124
- run.hasWalkthrough ? 'walkthrough' : null,
125
- run.hasTestReport ? 'test-report' : null
126
- ].filter(Boolean).join(', ') || 'none';
127
140
 
128
141
  lines.push(`${run.id} [${run.scope}] current: ${currentItem}`);
129
- lines.push(`progress ${completed}/${workItems.length} done, ${inProgress} active | artifacts: ${artifacts}`);
142
+ lines.push(`progress: ${completed}/${workItems.length} done, ${inProgress} active`);
130
143
  }
131
144
 
132
145
  return lines.map((line) => truncate(line, width));
@@ -134,7 +147,7 @@ function buildActiveRunLines(snapshot, runFilter, width) {
134
147
 
135
148
  function buildPendingLines(snapshot, runFilter, width) {
136
149
  if (runFilter === 'completed') {
137
- return [truncate('Hidden by active filter: completed', width)];
150
+ return [truncate('Hidden by run filter: completed', width)];
138
151
  }
139
152
 
140
153
  const pending = snapshot?.pendingItems || [];
@@ -143,16 +156,14 @@ function buildPendingLines(snapshot, runFilter, width) {
143
156
  }
144
157
 
145
158
  return pending.map((item) => {
146
- const deps = item.dependencies && item.dependencies.length > 0
147
- ? ` deps:${item.dependencies.join(',')}`
148
- : '';
159
+ const deps = item.dependencies && item.dependencies.length > 0 ? ` deps:${item.dependencies.join(',')}` : '';
149
160
  return truncate(`${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`, width);
150
161
  });
151
162
  }
152
163
 
153
164
  function buildCompletedLines(snapshot, runFilter, width) {
154
165
  if (runFilter === 'active') {
155
- return [truncate('Hidden by active filter: active', width)];
166
+ return [truncate('Hidden by run filter: active', width)];
156
167
  }
157
168
 
158
169
  const completedRuns = snapshot?.completedRuns || [];
@@ -180,6 +191,15 @@ function buildStatsLines(snapshot, width) {
180
191
  ].map((line) => truncate(line, width));
181
192
  }
182
193
 
194
+ function buildWarningsLines(snapshot, width) {
195
+ const warnings = snapshot?.warnings || [];
196
+ if (warnings.length === 0) {
197
+ return [truncate('No warnings', width)];
198
+ }
199
+
200
+ return warnings.map((warning) => truncate(warning, width));
201
+ }
202
+
183
203
  function buildOverviewProjectLines(snapshot, width) {
184
204
  if (!snapshot?.initialized) {
185
205
  return [
@@ -221,13 +241,37 @@ function buildOverviewStandardsLines(snapshot, width) {
221
241
  });
222
242
  }
223
243
 
224
- function buildWarningsLines(snapshot, width) {
225
- const warnings = snapshot?.warnings || [];
226
- if (warnings.length === 0) {
227
- return [truncate('No warnings', width)];
244
+ function allocateSingleColumnPanels(candidates, rowsBudget) {
245
+ const filtered = (candidates || []).filter(Boolean);
246
+ if (filtered.length === 0) {
247
+ return [];
228
248
  }
229
249
 
230
- return warnings.map((warning) => truncate(warning, width));
250
+ const selected = [];
251
+ let remaining = Math.max(4, rowsBudget);
252
+
253
+ for (const panel of filtered) {
254
+ const margin = selected.length > 0 ? 1 : 0;
255
+ const minimumRows = 4 + margin;
256
+
257
+ if (remaining >= minimumRows || selected.length === 0) {
258
+ selected.push({
259
+ ...panel,
260
+ maxLines: 1
261
+ });
262
+ remaining -= minimumRows;
263
+ }
264
+ }
265
+
266
+ let index = 0;
267
+ while (remaining > 0 && selected.length > 0) {
268
+ const panelIndex = index % selected.length;
269
+ selected[panelIndex].maxLines += 1;
270
+ remaining -= 1;
271
+ index += 1;
272
+ }
273
+
274
+ return selected;
231
275
  }
232
276
 
233
277
  function createDashboardApp(deps) {
@@ -245,7 +289,7 @@ function createDashboardApp(deps) {
245
289
  } = deps;
246
290
 
247
291
  const { Box, Text, useApp, useInput, useStdout } = ink;
248
- const { useState, useEffect, useCallback } = React;
292
+ const { useState, useEffect, useCallback, useRef } = React;
249
293
 
250
294
  function SectionPanel(props) {
251
295
  const {
@@ -254,7 +298,6 @@ function createDashboardApp(deps) {
254
298
  width,
255
299
  maxLines,
256
300
  borderColor,
257
- marginRight,
258
301
  marginBottom
259
302
  } = props;
260
303
 
@@ -269,7 +312,6 @@ function createDashboardApp(deps) {
269
312
  borderColor: borderColor || 'gray',
270
313
  paddingX: 1,
271
314
  width,
272
- marginRight: marginRight || 0,
273
315
  marginBottom: marginBottom || 0
274
316
  },
275
317
  React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
@@ -277,12 +319,43 @@ function createDashboardApp(deps) {
277
319
  );
278
320
  }
279
321
 
322
+ function TabsBar(props) {
323
+ const { view, width } = props;
324
+ const tabs = [
325
+ { id: 'runs', label: ' 1 RUNS ' },
326
+ { id: 'overview', label: ' 2 OVERVIEW ' },
327
+ { id: 'health', label: ' 3 HEALTH ' }
328
+ ];
329
+
330
+ return React.createElement(
331
+ Box,
332
+ { width, flexWrap: 'nowrap' },
333
+ ...tabs.map((tab) => {
334
+ const isActive = tab.id === view;
335
+ return React.createElement(
336
+ Text,
337
+ {
338
+ key: tab.id,
339
+ bold: isActive,
340
+ color: isActive ? 'black' : 'gray',
341
+ backgroundColor: isActive ? 'cyan' : undefined
342
+ },
343
+ tab.label
344
+ );
345
+ })
346
+ );
347
+ }
348
+
280
349
  function DashboardApp() {
281
350
  const { exit } = useApp();
282
351
  const { stdout } = useStdout();
283
352
 
353
+ const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
354
+ const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
355
+ const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
356
+
284
357
  const [snapshot, setSnapshot] = useState(initialSnapshot || null);
285
- const [error, setError] = useState(initialError ? toDashboardError(initialError) : null);
358
+ const [error, setError] = useState(initialNormalizedError);
286
359
  const [ui, setUi] = useState(createInitialUIState());
287
360
  const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
288
361
  const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
@@ -292,20 +365,49 @@ function createDashboardApp(deps) {
292
365
  }));
293
366
 
294
367
  const refresh = useCallback(async () => {
368
+ const now = new Date().toISOString();
369
+
295
370
  try {
296
371
  const result = await parseSnapshot();
297
372
 
298
373
  if (result?.ok) {
299
- setSnapshot(result.snapshot || null);
300
- setError(null);
301
- setWatchStatus(watchEnabled ? 'watching' : 'off');
374
+ const nextSnapshot = result.snapshot || null;
375
+ const nextSnapshotHash = safeJsonHash(nextSnapshot);
376
+
377
+ if (nextSnapshotHash !== snapshotHashRef.current) {
378
+ snapshotHashRef.current = nextSnapshotHash;
379
+ setSnapshot(nextSnapshot);
380
+ setLastRefreshAt(now);
381
+ }
382
+
383
+ if (errorHashRef.current !== null) {
384
+ errorHashRef.current = null;
385
+ setError(null);
386
+ setLastRefreshAt(now);
387
+ }
388
+
389
+ if (watchEnabled) {
390
+ setWatchStatus((previous) => (previous === 'watching' ? previous : 'watching'));
391
+ }
302
392
  } else {
303
- setError(toDashboardError(result?.error, 'PARSE_ERROR'));
393
+ const nextError = toDashboardError(result?.error, 'PARSE_ERROR');
394
+ const nextErrorHash = safeJsonHash(nextError);
395
+
396
+ if (nextErrorHash !== errorHashRef.current) {
397
+ errorHashRef.current = nextErrorHash;
398
+ setError(nextError);
399
+ setLastRefreshAt(now);
400
+ }
304
401
  }
305
402
  } catch (refreshError) {
306
- setError(toDashboardError(refreshError, 'REFRESH_FAILED'));
307
- } finally {
308
- setLastRefreshAt(new Date().toISOString());
403
+ const nextError = toDashboardError(refreshError, 'REFRESH_FAILED');
404
+ const nextErrorHash = safeJsonHash(nextError);
405
+
406
+ if (nextErrorHash !== errorHashRef.current) {
407
+ errorHashRef.current = nextErrorHash;
408
+ setError(nextError);
409
+ setLastRefreshAt(now);
410
+ }
309
411
  }
310
412
  }, [parseSnapshot, watchEnabled]);
311
413
 
@@ -335,6 +437,11 @@ function createDashboardApp(deps) {
335
437
  return;
336
438
  }
337
439
 
440
+ if (input === '3') {
441
+ setUi((previous) => ({ ...previous, view: 'health' }));
442
+ return;
443
+ }
444
+
338
445
  if (key.tab) {
339
446
  setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
340
447
  return;
@@ -384,20 +491,29 @@ function createDashboardApp(deps) {
384
491
 
385
492
  const runtime = createWatchRuntime({
386
493
  rootPath: rootPath || `${workspacePath}/.specs-fire`,
387
- debounceMs: 250,
494
+ debounceMs: 200,
388
495
  onRefresh: () => {
389
496
  void refresh();
390
497
  },
391
498
  onError: (watchError) => {
392
- setWatchStatus('reconnecting');
393
- setError(toDashboardError(watchError, 'WATCH_ERROR'));
499
+ const now = new Date().toISOString();
500
+ setWatchStatus((previous) => (previous === 'reconnecting' ? previous : 'reconnecting'));
501
+
502
+ const nextError = toDashboardError(watchError, 'WATCH_ERROR');
503
+ const nextErrorHash = safeJsonHash(nextError);
504
+ if (nextErrorHash !== errorHashRef.current) {
505
+ errorHashRef.current = nextErrorHash;
506
+ setError(nextError);
507
+ setLastRefreshAt(now);
508
+ }
394
509
  }
395
510
  });
396
511
 
397
512
  runtime.start();
513
+ const fallbackIntervalMs = Math.max(refreshMs, 5000);
398
514
  const interval = setInterval(() => {
399
515
  void refresh();
400
- }, refreshMs);
516
+ }, fallbackIntervalMs);
401
517
 
402
518
  return () => {
403
519
  clearInterval(interval);
@@ -408,151 +524,124 @@ function createDashboardApp(deps) {
408
524
  const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
409
525
  const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
410
526
 
411
- const compact = cols < 110;
412
- const veryCompact = cols < 80 || rows < 22;
413
- const contentAreaHeight = Math.max(12, rows - (ui.showHelp ? 7 : 5));
414
- const sectionLineLimit = compact
415
- ? Math.max(2, Math.floor(contentAreaHeight / 5))
416
- : Math.max(3, Math.floor(contentAreaHeight / 4));
417
-
418
527
  const fullWidth = Math.max(40, cols - 1);
419
- const leftWidth = compact ? fullWidth : Math.max(28, Math.floor((fullWidth - 1) / 2));
420
- const rightWidth = compact ? fullWidth : Math.max(28, fullWidth - leftWidth - 1);
421
-
422
- const headerLines = buildHeaderLines(
423
- snapshot,
424
- flow,
425
- workspacePath,
426
- watchEnabled,
427
- watchStatus,
428
- lastRefreshAt,
429
- ui.view,
430
- ui.runFilter,
431
- fullWidth - 4
432
- );
433
-
434
- const helpLines = ui.showHelp
435
- ? ['q quit | r refresh | h/? help | tab switch view | 1 runs | 2 overview | f run filter']
436
- : ['press h to show shortcuts'];
528
+ const compactWidth = Math.max(18, fullWidth - 4);
437
529
 
438
- const errorLines = buildErrorLines(error, fullWidth - 4);
530
+ const showHelpLine = ui.showHelp && rows >= 14;
531
+ const showErrorPanel = Boolean(error) && rows >= 18;
532
+ const showErrorInline = Boolean(error) && !showErrorPanel;
439
533
 
440
- const leftPanels = [];
441
- const rightPanels = [];
534
+ const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
535
+ const contentRowsBudget = Math.max(4, rows - reservedRows);
536
+ const ultraCompact = rows <= 14;
442
537
 
538
+ let panelCandidates;
443
539
  if (ui.view === 'overview') {
444
- leftPanels.push({
445
- title: 'Project + Workspace',
446
- lines: buildOverviewProjectLines(snapshot, leftWidth - 4),
447
- borderColor: 'green'
448
- });
449
- leftPanels.push({
450
- title: 'Intent Status',
451
- lines: buildOverviewIntentLines(snapshot, leftWidth - 4),
452
- borderColor: 'yellow'
453
- });
454
-
455
- rightPanels.push({
456
- title: 'Stats',
457
- lines: buildStatsLines(snapshot, rightWidth - 4),
458
- borderColor: 'magenta'
459
- });
460
- rightPanels.push({
461
- title: 'Standards',
462
- lines: buildOverviewStandardsLines(snapshot, rightWidth - 4),
463
- borderColor: 'blue'
464
- });
465
- rightPanels.push({
466
- title: 'Warnings',
467
- lines: buildWarningsLines(snapshot, rightWidth - 4),
468
- borderColor: 'red'
469
- });
540
+ panelCandidates = [
541
+ {
542
+ key: 'project',
543
+ title: 'Project + Workspace',
544
+ lines: buildOverviewProjectLines(snapshot, compactWidth),
545
+ borderColor: 'green'
546
+ },
547
+ {
548
+ key: 'intent-status',
549
+ title: 'Intent Status',
550
+ lines: buildOverviewIntentLines(snapshot, compactWidth),
551
+ borderColor: 'yellow'
552
+ },
553
+ {
554
+ key: 'standards',
555
+ title: 'Standards',
556
+ lines: buildOverviewStandardsLines(snapshot, compactWidth),
557
+ borderColor: 'blue'
558
+ }
559
+ ];
560
+ } else if (ui.view === 'health') {
561
+ panelCandidates = [
562
+ {
563
+ key: 'stats',
564
+ title: 'Stats',
565
+ lines: buildStatsLines(snapshot, compactWidth),
566
+ borderColor: 'magenta'
567
+ },
568
+ {
569
+ key: 'warnings',
570
+ title: 'Warnings',
571
+ lines: buildWarningsLines(snapshot, compactWidth),
572
+ borderColor: 'red'
573
+ }
574
+ ];
575
+
576
+ if (error && showErrorPanel) {
577
+ panelCandidates.push({
578
+ key: 'error-details',
579
+ title: 'Error Details',
580
+ lines: buildErrorLines(error, compactWidth),
581
+ borderColor: 'red'
582
+ });
583
+ }
470
584
  } else {
471
- leftPanels.push({
472
- title: 'Active Runs',
473
- lines: buildActiveRunLines(snapshot, ui.runFilter, leftWidth - 4),
474
- borderColor: 'green'
475
- });
476
- leftPanels.push({
477
- title: 'Pending Queue',
478
- lines: buildPendingLines(snapshot, ui.runFilter, leftWidth - 4),
479
- borderColor: 'yellow'
480
- });
585
+ panelCandidates = [
586
+ {
587
+ key: 'active-runs',
588
+ title: 'Active Runs',
589
+ lines: buildActiveRunLines(snapshot, ui.runFilter, compactWidth),
590
+ borderColor: 'green'
591
+ },
592
+ {
593
+ key: 'pending',
594
+ title: 'Pending Queue',
595
+ lines: buildPendingLines(snapshot, ui.runFilter, compactWidth),
596
+ borderColor: 'yellow'
597
+ },
598
+ {
599
+ key: 'completed',
600
+ title: 'Recent Completed Runs',
601
+ lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth),
602
+ borderColor: 'blue'
603
+ }
604
+ ];
605
+ }
481
606
 
482
- rightPanels.push({
483
- title: 'Recent Completed Runs',
484
- lines: buildCompletedLines(snapshot, ui.runFilter, rightWidth - 4),
485
- borderColor: 'blue'
486
- });
487
- rightPanels.push({
488
- title: 'Stats',
489
- lines: buildStatsLines(snapshot, rightWidth - 4),
490
- borderColor: 'magenta'
491
- });
492
- rightPanels.push({
493
- title: 'Warnings',
494
- lines: buildWarningsLines(snapshot, rightWidth - 4),
495
- borderColor: 'red'
496
- });
607
+ if (ultraCompact) {
608
+ panelCandidates = [panelCandidates[0]];
497
609
  }
498
610
 
611
+ const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
612
+
613
+ const helpText = 'q quit | r refresh | h/? help | tab next view | 1 runs | 2 overview | 3 health | f run filter';
614
+
499
615
  return React.createElement(
500
616
  Box,
501
617
  { flexDirection: 'column', width: fullWidth },
502
- React.createElement(SectionPanel, {
503
- title: veryCompact ? 'specsmd dashboard' : 'specsmd dashboard / FIRE',
504
- lines: headerLines,
505
- width: fullWidth,
506
- maxLines: veryCompact ? 2 : 3,
507
- borderColor: 'cyan',
508
- marginBottom: 1
509
- }),
510
- error ? React.createElement(SectionPanel, {
511
- title: 'Errors',
512
- lines: errorLines,
618
+ 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 }),
620
+ showErrorInline
621
+ ? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
622
+ : null,
623
+ showErrorPanel
624
+ ? React.createElement(SectionPanel, {
625
+ title: 'Errors',
626
+ lines: buildErrorLines(error, compactWidth),
627
+ width: fullWidth,
628
+ maxLines: 2,
629
+ borderColor: 'red',
630
+ marginBottom: 1
631
+ })
632
+ : null,
633
+ ...panels.map((panel, index) => React.createElement(SectionPanel, {
634
+ key: panel.key,
635
+ title: panel.title,
636
+ lines: panel.lines,
513
637
  width: fullWidth,
514
- maxLines: Math.max(2, sectionLineLimit),
515
- borderColor: 'red',
516
- marginBottom: 1
517
- }) : null,
518
- React.createElement(
519
- Box,
520
- { flexDirection: compact ? 'column' : 'row', width: fullWidth },
521
- React.createElement(
522
- Box,
523
- { flexDirection: 'column', width: leftWidth, marginRight: compact ? 0 : 1 },
524
- ...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
525
- key: `left-${panel.title}`,
526
- title: panel.title,
527
- lines: panel.lines,
528
- width: leftWidth,
529
- maxLines: sectionLineLimit,
530
- borderColor: panel.borderColor,
531
- marginBottom: index === leftPanels.length - 1 ? 0 : 1
532
- }))
533
- ),
534
- React.createElement(
535
- Box,
536
- { flexDirection: 'column', width: rightWidth },
537
- ...rightPanels.map((panel, index) => React.createElement(SectionPanel, {
538
- key: `right-${panel.title}`,
539
- title: panel.title,
540
- lines: panel.lines,
541
- width: rightWidth,
542
- maxLines: sectionLineLimit,
543
- borderColor: panel.borderColor,
544
- marginBottom: index === rightPanels.length - 1 ? 0 : 1
545
- }))
546
- )
547
- ),
548
- React.createElement(SectionPanel, {
549
- title: 'Help',
550
- lines: helpLines,
551
- width: fullWidth,
552
- maxLines: 2,
553
- borderColor: 'gray',
554
- marginTop: 1
555
- })
638
+ maxLines: panel.maxLines,
639
+ borderColor: panel.borderColor,
640
+ marginBottom: index === panels.length - 1 ? 0 : 1
641
+ })),
642
+ showHelpLine
643
+ ? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
644
+ : null
556
645
  );
557
646
  }
558
647
 
@@ -563,5 +652,7 @@ module.exports = {
563
652
  createDashboardApp,
564
653
  toDashboardError,
565
654
  truncate,
566
- fitLines
655
+ fitLines,
656
+ safeJsonHash,
657
+ allocateSingleColumnPanels
567
658
  };
@@ -10,6 +10,9 @@ function cycleView(current) {
10
10
  if (current === 'runs') {
11
11
  return 'overview';
12
12
  }
13
+ if (current === 'overview') {
14
+ return 'health';
15
+ }
13
16
  return 'runs';
14
17
  }
15
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
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": {