specsmd 0.1.26 → 0.1.28

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) {
@@ -83,18 +114,33 @@ function formatTime(value) {
83
114
  return date.toLocaleTimeString();
84
115
  }
85
116
 
86
- function buildShortStats(snapshot) {
117
+ function buildShortStats(snapshot, flow) {
87
118
  if (!snapshot?.initialized) {
119
+ if (flow === 'aidlc') {
120
+ return 'init: waiting for memory-bank scan';
121
+ }
122
+ if (flow === 'simple') {
123
+ return 'init: waiting for specs scan';
124
+ }
88
125
  return 'init: waiting for state.yaml';
89
126
  }
90
127
 
91
- const stats = snapshot.stats;
92
- return `runs ${stats.activeRunsCount}/${stats.completedRuns} | intents ${stats.completedIntents}/${stats.totalIntents} | work ${stats.completedWorkItems}/${stats.totalWorkItems}`;
128
+ const stats = snapshot?.stats || {};
129
+
130
+ if (flow === 'aidlc') {
131
+ return `bolts ${stats.activeBoltsCount || 0}/${stats.completedBolts || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | stories ${stats.completedStories || 0}/${stats.totalStories || 0}`;
132
+ }
133
+
134
+ if (flow === 'simple') {
135
+ return `specs ${stats.completedSpecs || 0}/${stats.totalSpecs || 0} | tasks ${stats.completedTasks || 0}/${stats.totalTasks || 0} | active ${stats.activeSpecsCount || 0}`;
136
+ }
137
+
138
+ return `runs ${stats.activeRunsCount || 0}/${stats.completedRuns || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | work ${stats.completedWorkItems || 0}/${stats.totalWorkItems || 0}`;
93
139
  }
94
140
 
95
141
  function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
96
- const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
97
- const shortStats = buildShortStats(snapshot);
142
+ const projectName = snapshot?.project?.name || 'Unnamed project';
143
+ const shortStats = buildShortStats(snapshot, flow);
98
144
 
99
145
  const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}/${runFilter} | ${formatTime(lastRefreshAt)}`;
100
146
 
@@ -121,31 +167,87 @@ function buildErrorLines(error, width) {
121
167
  return lines.map((line) => truncate(line, width));
122
168
  }
123
169
 
124
- function buildActiveRunLines(snapshot, runFilter, width) {
125
- if (runFilter === 'completed') {
126
- return [truncate('Hidden by run filter: completed', width)];
170
+ function getCurrentRun(snapshot) {
171
+ const activeRuns = Array.isArray(snapshot?.activeRuns) ? [...snapshot.activeRuns] : [];
172
+ if (activeRuns.length === 0) {
173
+ return null;
127
174
  }
128
175
 
129
- const activeRuns = snapshot?.activeRuns || [];
130
- if (activeRuns.length === 0) {
131
- return [truncate('No active runs', width)];
176
+ activeRuns.sort((a, b) => {
177
+ const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
178
+ const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
179
+ if (bTime !== aTime) {
180
+ return bTime - aTime;
181
+ }
182
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
183
+ });
184
+
185
+ return activeRuns[0] || null;
186
+ }
187
+
188
+ function getCurrentPhaseLabel(run, currentWorkItem) {
189
+ const phase = currentWorkItem?.currentPhase || '';
190
+ if (typeof phase === 'string' && phase !== '') {
191
+ return phase.toLowerCase();
132
192
  }
133
193
 
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;
194
+ if (run?.hasTestReport) {
195
+ return 'review';
196
+ }
197
+ if (run?.hasPlan) {
198
+ return 'execute';
199
+ }
200
+ return 'plan';
201
+ }
140
202
 
141
- lines.push(`${run.id} [${run.scope}] current: ${currentItem}`);
142
- lines.push(`progress: ${completed}/${workItems.length} done, ${inProgress} active`);
203
+ function buildPhaseTrack(currentPhase) {
204
+ const order = ['plan', 'execute', 'test', 'review'];
205
+ const labels = ['P', 'E', 'T', 'R'];
206
+ const currentIndex = Math.max(0, order.indexOf(currentPhase));
207
+ return labels.map((label, index) => (index === currentIndex ? `[${label}]` : ` ${label} `)).join(' - ');
208
+ }
209
+
210
+ function buildFireCurrentRunLines(snapshot, width) {
211
+ const run = getCurrentRun(snapshot);
212
+ if (!run) {
213
+ return [truncate('No active run', width)];
143
214
  }
144
215
 
216
+ const workItems = Array.isArray(run.workItems) ? run.workItems : [];
217
+ const completed = workItems.filter((item) => item.status === 'completed').length;
218
+ const currentWorkItem = workItems.find((item) => item.id === run.currentItem) || workItems.find((item) => item.status === 'in_progress') || workItems[0];
219
+
220
+ const itemId = currentWorkItem?.id || run.currentItem || 'n/a';
221
+ const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
222
+ const status = currentWorkItem?.status || 'pending';
223
+ const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
224
+ const phaseTrack = buildPhaseTrack(currentPhase);
225
+
226
+ const lines = [
227
+ `${run.id} [${run.scope}] ${completed}/${workItems.length} items done`,
228
+ `work item: ${itemId}`,
229
+ `mode: ${mode} | status: ${status}`,
230
+ `phase: ${phaseTrack}`
231
+ ];
232
+
145
233
  return lines.map((line) => truncate(line, width));
146
234
  }
147
235
 
148
- function buildPendingLines(snapshot, runFilter, width) {
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) {
149
251
  if (runFilter === 'completed') {
150
252
  return [truncate('Hidden by run filter: completed', width)];
151
253
  }
@@ -161,7 +263,7 @@ function buildPendingLines(snapshot, runFilter, width) {
161
263
  });
162
264
  }
163
265
 
164
- function buildCompletedLines(snapshot, runFilter, width) {
266
+ function buildFireCompletedLines(snapshot, runFilter, width) {
165
267
  if (runFilter === 'active') {
166
268
  return [truncate('Hidden by run filter: active', width)];
167
269
  }
@@ -178,7 +280,7 @@ function buildCompletedLines(snapshot, runFilter, width) {
178
280
  });
179
281
  }
180
282
 
181
- function buildStatsLines(snapshot, width) {
283
+ function buildFireStatsLines(snapshot, width) {
182
284
  if (!snapshot?.initialized) {
183
285
  return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
184
286
  }
@@ -200,7 +302,7 @@ function buildWarningsLines(snapshot, width) {
200
302
  return warnings.map((warning) => truncate(warning, width));
201
303
  }
202
304
 
203
- function buildOverviewProjectLines(snapshot, width) {
305
+ function buildFireOverviewProjectLines(snapshot, width) {
204
306
  if (!snapshot?.initialized) {
205
307
  return [
206
308
  truncate('FIRE folder detected, but state.yaml is missing.', width),
@@ -218,7 +320,7 @@ function buildOverviewProjectLines(snapshot, width) {
218
320
  ].map((line) => truncate(line, width));
219
321
  }
220
322
 
221
- function buildOverviewIntentLines(snapshot, width) {
323
+ function buildFireOverviewIntentLines(snapshot, width) {
222
324
  const intents = snapshot?.intents || [];
223
325
  if (intents.length === 0) {
224
326
  return [truncate('No intents found', width)];
@@ -231,7 +333,7 @@ function buildOverviewIntentLines(snapshot, width) {
231
333
  });
232
334
  }
233
335
 
234
- function buildOverviewStandardsLines(snapshot, width) {
336
+ function buildFireOverviewStandardsLines(snapshot, width) {
235
337
  const expected = ['constitution', 'tech-stack', 'coding-standards', 'testing-standards', 'system-architecture'];
236
338
  const actual = new Set((snapshot?.standards || []).map((item) => item.type));
237
339
 
@@ -241,6 +343,435 @@ function buildOverviewStandardsLines(snapshot, width) {
241
343
  });
242
344
  }
243
345
 
346
+ function getEffectiveFlow(flow, snapshot) {
347
+ const explicitFlow = typeof flow === 'string' && flow !== '' ? flow : null;
348
+ const snapshotFlow = typeof snapshot?.flow === 'string' && snapshot.flow !== '' ? snapshot.flow : null;
349
+ return (snapshotFlow || explicitFlow || 'fire').toLowerCase();
350
+ }
351
+
352
+ function getCurrentBolt(snapshot) {
353
+ const activeBolts = Array.isArray(snapshot?.activeBolts) ? [...snapshot.activeBolts] : [];
354
+ if (activeBolts.length === 0) {
355
+ return null;
356
+ }
357
+
358
+ activeBolts.sort((a, b) => {
359
+ const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
360
+ const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
361
+ if (bTime !== aTime) {
362
+ return bTime - aTime;
363
+ }
364
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
365
+ });
366
+
367
+ return activeBolts[0] || null;
368
+ }
369
+
370
+ function buildAidlcStageTrack(bolt) {
371
+ const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
372
+ if (stages.length === 0) {
373
+ return 'n/a';
374
+ }
375
+
376
+ return stages.map((stage) => {
377
+ const label = String(stage?.name || '?').charAt(0).toUpperCase();
378
+ if (stage?.status === 'completed') {
379
+ return `[${label}]`;
380
+ }
381
+ if (stage?.status === 'in_progress') {
382
+ return `<${label}>`;
383
+ }
384
+ return ` ${label} `;
385
+ }).join('-');
386
+ }
387
+
388
+ function buildAidlcCurrentRunLines(snapshot, width) {
389
+ const bolt = getCurrentBolt(snapshot);
390
+ if (!bolt) {
391
+ return [truncate('No active bolt', width)];
392
+ }
393
+
394
+ const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
395
+ const completedStages = stages.filter((stage) => stage.status === 'completed').length;
396
+ const phaseTrack = buildAidlcStageTrack(bolt);
397
+ const location = `${bolt.intent || 'unknown-intent'} / ${bolt.unit || 'unknown-unit'}`;
398
+
399
+ const lines = [
400
+ `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages done`,
401
+ `scope: ${location}`,
402
+ `stage: ${bolt.currentStage || 'n/a'} | status: ${bolt.status}`,
403
+ `phase: ${phaseTrack}`
404
+ ];
405
+
406
+ return lines.map((line) => truncate(line, width));
407
+ }
408
+
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
+
428
+ const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
429
+ if (pendingBolts.length === 0) {
430
+ return [truncate('No queued bolts', width)];
431
+ }
432
+
433
+ return pendingBolts.map((bolt) => {
434
+ const deps = Array.isArray(bolt.blockedBy) && bolt.blockedBy.length > 0
435
+ ? ` blocked_by:${bolt.blockedBy.join(',')}`
436
+ : '';
437
+ const location = `${bolt.intent || 'unknown'}/${bolt.unit || 'unknown'}`;
438
+ return truncate(`${bolt.id} (${bolt.status}) in ${location}${deps}`, width);
439
+ });
440
+ }
441
+
442
+ function buildAidlcCompletedLines(snapshot, runFilter, width) {
443
+ if (runFilter === 'active') {
444
+ return [truncate('Hidden by run filter: active', width)];
445
+ }
446
+
447
+ const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
448
+ if (completedBolts.length === 0) {
449
+ return [truncate('No completed bolts yet', width)];
450
+ }
451
+
452
+ return completedBolts.map((bolt) =>
453
+ truncate(`${bolt.id} [${bolt.type}] done at ${bolt.completedAt || 'unknown'}`, width)
454
+ );
455
+ }
456
+
457
+ function buildAidlcStatsLines(snapshot, width) {
458
+ const stats = snapshot?.stats || {};
459
+
460
+ return [
461
+ `intents: ${stats.completedIntents || 0}/${stats.totalIntents || 0} done | in_progress: ${stats.inProgressIntents || 0} | blocked: ${stats.blockedIntents || 0}`,
462
+ `stories: ${stats.completedStories || 0}/${stats.totalStories || 0} done | in_progress: ${stats.inProgressStories || 0} | pending: ${stats.pendingStories || 0} | blocked: ${stats.blockedStories || 0}`,
463
+ `bolts: ${stats.activeBoltsCount || 0} active | ${stats.queuedBolts || 0} queued | ${stats.blockedBolts || 0} blocked | ${stats.completedBolts || 0} done`
464
+ ].map((line) => truncate(line, width));
465
+ }
466
+
467
+ function buildAidlcOverviewProjectLines(snapshot, width) {
468
+ const project = snapshot?.project || {};
469
+ const stats = snapshot?.stats || {};
470
+
471
+ return [
472
+ `project: ${project.name || 'unknown'} | project_type: ${project.projectType || 'unknown'}`,
473
+ `memory-bank: intents ${stats.totalIntents || 0} | units ${stats.totalUnits || 0} | stories ${stats.totalStories || 0}`,
474
+ `progress: ${stats.progressPercent || 0}% stories complete | standards: ${(snapshot?.standards || []).length}`
475
+ ].map((line) => truncate(line, width));
476
+ }
477
+
478
+ function buildAidlcOverviewIntentLines(snapshot, width) {
479
+ const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
480
+ if (intents.length === 0) {
481
+ return [truncate('No intents found', width)];
482
+ }
483
+
484
+ return intents.map((intent) => {
485
+ return truncate(
486
+ `${intent.id}: ${intent.status} (${intent.completedStories || 0}/${intent.storyCount || 0} stories, ${intent.completedUnits || 0}/${intent.unitCount || 0} units)`,
487
+ width
488
+ );
489
+ });
490
+ }
491
+
492
+ function buildAidlcOverviewStandardsLines(snapshot, width) {
493
+ const standards = Array.isArray(snapshot?.standards) ? snapshot.standards : [];
494
+ if (standards.length === 0) {
495
+ return [truncate('No standards found under memory-bank/standards', width)];
496
+ }
497
+
498
+ return standards.map((standard) =>
499
+ truncate(`[x] ${standard.name || standard.type || 'unknown'}.md`, width)
500
+ );
501
+ }
502
+
503
+ function getCurrentSpec(snapshot) {
504
+ const specs = Array.isArray(snapshot?.activeSpecs) ? snapshot.activeSpecs : [];
505
+ if (specs.length === 0) {
506
+ return null;
507
+ }
508
+ return specs[0] || null;
509
+ }
510
+
511
+ function simplePhaseIndex(state) {
512
+ if (state === 'requirements_pending') {
513
+ return 0;
514
+ }
515
+ if (state === 'design_pending') {
516
+ return 1;
517
+ }
518
+ return 2;
519
+ }
520
+
521
+ function buildSimplePhaseTrack(spec) {
522
+ if (spec?.state === 'completed') {
523
+ return '[R] - [D] - [T]';
524
+ }
525
+
526
+ const labels = ['R', 'D', 'T'];
527
+ const current = simplePhaseIndex(spec?.state);
528
+ return labels.map((label, index) => (index === current ? `[${label}]` : ` ${label} `)).join(' - ');
529
+ }
530
+
531
+ function buildSimpleCurrentRunLines(snapshot, width) {
532
+ const spec = getCurrentSpec(snapshot);
533
+ if (!spec) {
534
+ return [truncate('No active spec', width)];
535
+ }
536
+
537
+ const files = [
538
+ spec.hasRequirements ? 'req' : '-',
539
+ spec.hasDesign ? 'design' : '-',
540
+ spec.hasTasks ? 'tasks' : '-'
541
+ ].join('/');
542
+
543
+ const lines = [
544
+ `${spec.name} [${spec.state}] ${spec.tasksCompleted}/${spec.tasksTotal} tasks done`,
545
+ `phase: ${spec.phase}`,
546
+ `files: ${files}`,
547
+ `track: ${buildSimplePhaseTrack(spec)}`
548
+ ];
549
+
550
+ return lines.map((line) => truncate(line, width));
551
+ }
552
+
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
+
576
+ const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
577
+ if (pendingSpecs.length === 0) {
578
+ return [truncate('No pending specs', width)];
579
+ }
580
+
581
+ return pendingSpecs.map((spec) =>
582
+ truncate(`${spec.name} (${spec.state}) ${spec.tasksCompleted}/${spec.tasksTotal} tasks`, width)
583
+ );
584
+ }
585
+
586
+ function buildSimpleCompletedLines(snapshot, runFilter, width) {
587
+ if (runFilter === 'active') {
588
+ return [truncate('Hidden by run filter: active', width)];
589
+ }
590
+
591
+ const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
592
+ if (completedSpecs.length === 0) {
593
+ return [truncate('No completed specs yet', width)];
594
+ }
595
+
596
+ return completedSpecs.map((spec) =>
597
+ truncate(`${spec.name} done at ${spec.updatedAt || 'unknown'} (${spec.tasksCompleted}/${spec.tasksTotal})`, width)
598
+ );
599
+ }
600
+
601
+ function buildSimpleStatsLines(snapshot, width) {
602
+ const stats = snapshot?.stats || {};
603
+
604
+ return [
605
+ `specs: ${stats.completedSpecs || 0}/${stats.totalSpecs || 0} complete | in_progress: ${stats.inProgressSpecs || 0} | pending: ${stats.pendingSpecs || 0}`,
606
+ `pipeline: ready ${stats.readySpecs || 0} | design_pending ${stats.designPendingSpecs || 0} | tasks_pending ${stats.tasksPendingSpecs || 0}`,
607
+ `tasks: ${stats.completedTasks || 0}/${stats.totalTasks || 0} complete | pending: ${stats.pendingTasks || 0} | optional: ${stats.optionalTasks || 0}`
608
+ ].map((line) => truncate(line, width));
609
+ }
610
+
611
+ function buildSimpleOverviewProjectLines(snapshot, width) {
612
+ const project = snapshot?.project || {};
613
+ const stats = snapshot?.stats || {};
614
+
615
+ return [
616
+ `project: ${project.name || 'unknown'} | simple flow`,
617
+ `specs: ${stats.totalSpecs || 0} total | active: ${stats.activeSpecsCount || 0} | completed: ${stats.completedSpecs || 0}`,
618
+ `tasks: ${stats.completedTasks || 0}/${stats.totalTasks || 0} complete (${stats.progressPercent || 0}%)`
619
+ ].map((line) => truncate(line, width));
620
+ }
621
+
622
+ function buildSimpleOverviewIntentLines(snapshot, width) {
623
+ const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
624
+ if (specs.length === 0) {
625
+ return [truncate('No specs found', width)];
626
+ }
627
+
628
+ return specs.map((spec) =>
629
+ truncate(`${spec.name}: ${spec.state} (${spec.tasksCompleted}/${spec.tasksTotal} tasks)`, width)
630
+ );
631
+ }
632
+
633
+ function buildSimpleOverviewStandardsLines(snapshot, width) {
634
+ const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
635
+ if (specs.length === 0) {
636
+ return [truncate('No spec artifacts found', width)];
637
+ }
638
+
639
+ const reqCount = specs.filter((spec) => spec.hasRequirements).length;
640
+ const designCount = specs.filter((spec) => spec.hasDesign).length;
641
+ const tasksCount = specs.filter((spec) => spec.hasTasks).length;
642
+ const total = specs.length;
643
+
644
+ return [
645
+ `[x] requirements.md coverage ${reqCount}/${total}`,
646
+ `[x] design.md coverage ${designCount}/${total}`,
647
+ `[x] tasks.md coverage ${tasksCount}/${total}`
648
+ ].map((line) => truncate(line, width));
649
+ }
650
+
651
+ function buildCurrentRunLines(snapshot, width, flow) {
652
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
653
+ if (effectiveFlow === 'aidlc') {
654
+ return buildAidlcCurrentRunLines(snapshot, width);
655
+ }
656
+ if (effectiveFlow === 'simple') {
657
+ return buildSimpleCurrentRunLines(snapshot, width);
658
+ }
659
+ return buildFireCurrentRunLines(snapshot, width);
660
+ }
661
+
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) {
674
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
675
+ if (effectiveFlow === 'aidlc') {
676
+ return buildAidlcPendingLines(snapshot, runFilter, width);
677
+ }
678
+ if (effectiveFlow === 'simple') {
679
+ return buildSimplePendingLines(snapshot, runFilter, width);
680
+ }
681
+ return buildFirePendingLines(snapshot, runFilter, width);
682
+ }
683
+
684
+ function buildCompletedLines(snapshot, runFilter, width, flow) {
685
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
686
+ if (effectiveFlow === 'aidlc') {
687
+ return buildAidlcCompletedLines(snapshot, runFilter, width);
688
+ }
689
+ if (effectiveFlow === 'simple') {
690
+ return buildSimpleCompletedLines(snapshot, runFilter, width);
691
+ }
692
+ return buildFireCompletedLines(snapshot, runFilter, width);
693
+ }
694
+
695
+ function buildStatsLines(snapshot, width, flow) {
696
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
697
+ if (!snapshot?.initialized) {
698
+ if (effectiveFlow === 'aidlc') {
699
+ return [truncate('Waiting for memory-bank initialization.', width)];
700
+ }
701
+ if (effectiveFlow === 'simple') {
702
+ return [truncate('Waiting for specs/ initialization.', width)];
703
+ }
704
+ return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
705
+ }
706
+
707
+ if (effectiveFlow === 'aidlc') {
708
+ return buildAidlcStatsLines(snapshot, width);
709
+ }
710
+ if (effectiveFlow === 'simple') {
711
+ return buildSimpleStatsLines(snapshot, width);
712
+ }
713
+ return buildFireStatsLines(snapshot, width);
714
+ }
715
+
716
+ function buildOverviewProjectLines(snapshot, width, flow) {
717
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
718
+ if (effectiveFlow === 'aidlc') {
719
+ return buildAidlcOverviewProjectLines(snapshot, width);
720
+ }
721
+ if (effectiveFlow === 'simple') {
722
+ return buildSimpleOverviewProjectLines(snapshot, width);
723
+ }
724
+ return buildFireOverviewProjectLines(snapshot, width);
725
+ }
726
+
727
+ function buildOverviewIntentLines(snapshot, width, flow) {
728
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
729
+ if (effectiveFlow === 'aidlc') {
730
+ return buildAidlcOverviewIntentLines(snapshot, width);
731
+ }
732
+ if (effectiveFlow === 'simple') {
733
+ return buildSimpleOverviewIntentLines(snapshot, width);
734
+ }
735
+ return buildFireOverviewIntentLines(snapshot, width);
736
+ }
737
+
738
+ function buildOverviewStandardsLines(snapshot, width, flow) {
739
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
740
+ if (effectiveFlow === 'aidlc') {
741
+ return buildAidlcOverviewStandardsLines(snapshot, width);
742
+ }
743
+ if (effectiveFlow === 'simple') {
744
+ return buildSimpleOverviewStandardsLines(snapshot, width);
745
+ }
746
+ return buildFireOverviewStandardsLines(snapshot, width);
747
+ }
748
+
749
+ function getPanelTitles(flow, snapshot) {
750
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
751
+ if (effectiveFlow === 'aidlc') {
752
+ return {
753
+ current: 'Current Bolt',
754
+ files: 'Bolt Files',
755
+ pending: 'Queued Bolts',
756
+ completed: 'Recent Completed Bolts'
757
+ };
758
+ }
759
+ if (effectiveFlow === 'simple') {
760
+ return {
761
+ current: 'Current Spec',
762
+ files: 'Spec Files',
763
+ pending: 'Pending Specs',
764
+ completed: 'Recent Completed Specs'
765
+ };
766
+ }
767
+ return {
768
+ current: 'Current Run',
769
+ files: 'Run Files',
770
+ pending: 'Pending Queue',
771
+ completed: 'Recent Completed Runs'
772
+ };
773
+ }
774
+
244
775
  function allocateSingleColumnPanels(candidates, rowsBudget) {
245
776
  const filtered = (candidates || []).filter(Boolean);
246
777
  if (filtered.length === 0) {
@@ -298,7 +829,8 @@ function createDashboardApp(deps) {
298
829
  width,
299
830
  maxLines,
300
831
  borderColor,
301
- marginBottom
832
+ marginBottom,
833
+ dense
302
834
  } = props;
303
835
 
304
836
  const contentWidth = Math.max(18, width - 4);
@@ -308,9 +840,9 @@ function createDashboardApp(deps) {
308
840
  Box,
309
841
  {
310
842
  flexDirection: 'column',
311
- borderStyle: 'round',
843
+ borderStyle: dense ? 'single' : 'round',
312
844
  borderColor: borderColor || 'gray',
313
- paddingX: 1,
845
+ paddingX: dense ? 0 : 1,
314
846
  width,
315
847
  marginBottom: marginBottom || 0
316
848
  },
@@ -320,11 +852,11 @@ function createDashboardApp(deps) {
320
852
  }
321
853
 
322
854
  function TabsBar(props) {
323
- const { view, width } = props;
855
+ const { view, width, icons } = props;
324
856
  const tabs = [
325
- { id: 'runs', label: ' 1 RUNS ' },
326
- { id: 'overview', label: ' 2 OVERVIEW ' },
327
- { id: 'health', label: ' 3 HEALTH ' }
857
+ { id: 'runs', label: ` 1 ${icons.runs} RUNS ` },
858
+ { id: 'overview', label: ` 2 ${icons.overview} OVERVIEW ` },
859
+ { id: 'health', label: ` 3 ${icons.health} HEALTH ` }
328
860
  ];
329
861
 
330
862
  return React.createElement(
@@ -363,6 +895,7 @@ function createDashboardApp(deps) {
363
895
  columns: stdout?.columns || process.stdout.columns || 120,
364
896
  rows: stdout?.rows || process.stdout.rows || 40
365
897
  }));
898
+ const icons = resolveIconSet();
366
899
 
367
900
  const refresh = useCallback(async () => {
368
901
  const now = new Date().toISOString();
@@ -447,6 +980,16 @@ function createDashboardApp(deps) {
447
980
  return;
448
981
  }
449
982
 
983
+ if (key.rightArrow) {
984
+ setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
985
+ return;
986
+ }
987
+
988
+ if (key.leftArrow) {
989
+ setUi((previous) => ({ ...previous, view: cycleViewBackward(previous.view) }));
990
+ return;
991
+ }
992
+
450
993
  if (input === 'f') {
451
994
  setUi((previous) => ({ ...previous, runFilter: cycleRunFilter(previous.runFilter) }));
452
995
  }
@@ -530,10 +1073,12 @@ function createDashboardApp(deps) {
530
1073
  const showHelpLine = ui.showHelp && rows >= 14;
531
1074
  const showErrorPanel = Boolean(error) && rows >= 18;
532
1075
  const showErrorInline = Boolean(error) && !showErrorPanel;
1076
+ const densePanels = rows <= 28 || cols <= 120;
533
1077
 
534
1078
  const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
535
1079
  const contentRowsBudget = Math.max(4, rows - reservedRows);
536
1080
  const ultraCompact = rows <= 14;
1081
+ const panelTitles = getPanelTitles(flow, snapshot);
537
1082
 
538
1083
  let panelCandidates;
539
1084
  if (ui.view === 'overview') {
@@ -541,19 +1086,19 @@ function createDashboardApp(deps) {
541
1086
  {
542
1087
  key: 'project',
543
1088
  title: 'Project + Workspace',
544
- lines: buildOverviewProjectLines(snapshot, compactWidth),
1089
+ lines: buildOverviewProjectLines(snapshot, compactWidth, flow),
545
1090
  borderColor: 'green'
546
1091
  },
547
1092
  {
548
1093
  key: 'intent-status',
549
1094
  title: 'Intent Status',
550
- lines: buildOverviewIntentLines(snapshot, compactWidth),
1095
+ lines: buildOverviewIntentLines(snapshot, compactWidth, flow),
551
1096
  borderColor: 'yellow'
552
1097
  },
553
1098
  {
554
1099
  key: 'standards',
555
1100
  title: 'Standards',
556
- lines: buildOverviewStandardsLines(snapshot, compactWidth),
1101
+ lines: buildOverviewStandardsLines(snapshot, compactWidth, flow),
557
1102
  borderColor: 'blue'
558
1103
  }
559
1104
  ];
@@ -562,7 +1107,7 @@ function createDashboardApp(deps) {
562
1107
  {
563
1108
  key: 'stats',
564
1109
  title: 'Stats',
565
- lines: buildStatsLines(snapshot, compactWidth),
1110
+ lines: buildStatsLines(snapshot, compactWidth, flow),
566
1111
  borderColor: 'magenta'
567
1112
  },
568
1113
  {
@@ -584,21 +1129,27 @@ function createDashboardApp(deps) {
584
1129
  } else {
585
1130
  panelCandidates = [
586
1131
  {
587
- key: 'active-runs',
588
- title: 'Active Runs',
589
- lines: buildActiveRunLines(snapshot, ui.runFilter, compactWidth),
1132
+ key: 'current-run',
1133
+ title: panelTitles.current,
1134
+ lines: buildCurrentRunLines(snapshot, compactWidth, flow),
590
1135
  borderColor: 'green'
591
1136
  },
1137
+ {
1138
+ key: 'run-files',
1139
+ title: panelTitles.files,
1140
+ lines: buildRunFilesLines(snapshot, compactWidth, icons, flow),
1141
+ borderColor: 'yellow'
1142
+ },
592
1143
  {
593
1144
  key: 'pending',
594
- title: 'Pending Queue',
595
- lines: buildPendingLines(snapshot, ui.runFilter, compactWidth),
1145
+ title: panelTitles.pending,
1146
+ lines: buildPendingLines(snapshot, ui.runFilter, compactWidth, flow),
596
1147
  borderColor: 'yellow'
597
1148
  },
598
1149
  {
599
1150
  key: 'completed',
600
- title: 'Recent Completed Runs',
601
- lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth),
1151
+ title: panelTitles.completed,
1152
+ lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth, flow),
602
1153
  borderColor: 'blue'
603
1154
  }
604
1155
  ];
@@ -610,13 +1161,13 @@ function createDashboardApp(deps) {
610
1161
 
611
1162
  const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
612
1163
 
613
- const helpText = 'q quit | r refresh | h/? help | tab next view | 1 runs | 2 overview | 3 health | f run filter';
1164
+ const helpText = 'q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health | f run filter';
614
1165
 
615
1166
  return React.createElement(
616
1167
  Box,
617
1168
  { flexDirection: 'column', width: fullWidth },
618
1169
  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 }),
1170
+ React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons }),
620
1171
  showErrorInline
621
1172
  ? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
622
1173
  : null,
@@ -627,7 +1178,8 @@ function createDashboardApp(deps) {
627
1178
  width: fullWidth,
628
1179
  maxLines: 2,
629
1180
  borderColor: 'red',
630
- marginBottom: 1
1181
+ marginBottom: densePanels ? 0 : 1,
1182
+ dense: densePanels
631
1183
  })
632
1184
  : null,
633
1185
  ...panels.map((panel, index) => React.createElement(SectionPanel, {
@@ -637,7 +1189,8 @@ function createDashboardApp(deps) {
637
1189
  width: fullWidth,
638
1190
  maxLines: panel.maxLines,
639
1191
  borderColor: panel.borderColor,
640
- marginBottom: index === panels.length - 1 ? 0 : 1
1192
+ marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
1193
+ dense: densePanels
641
1194
  })),
642
1195
  showHelpLine
643
1196
  ? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))