specsmd 0.1.35 → 0.1.36

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.
@@ -793,13 +793,16 @@ function getPanelTitles(flow, snapshot) {
793
793
  }
794
794
 
795
795
  function getSectionOrderForView(view) {
796
- if (view === 'overview') {
797
- return ['intent-status', 'completed-runs', 'standards', 'project'];
796
+ if (view === 'intents') {
797
+ return ['intent-status'];
798
+ }
799
+ if (view === 'completed') {
800
+ return ['completed-runs'];
798
801
  }
799
802
  if (view === 'health') {
800
- return ['stats', 'warnings', 'error-details'];
803
+ return ['standards', 'stats', 'warnings', 'error-details'];
801
804
  }
802
- return ['current-run', 'run-files', 'pending'];
805
+ return ['current-run'];
803
806
  }
804
807
 
805
808
  function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
@@ -1054,24 +1057,221 @@ function getNoCompletedMessage(flow) {
1054
1057
  return 'No completed runs yet';
1055
1058
  }
1056
1059
 
1057
- function toRunFileRows(fileEntries, flow) {
1058
- if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
1060
+ function getNoCurrentMessage(flow) {
1061
+ if (flow === 'aidlc') return 'No active bolt';
1062
+ if (flow === 'simple') return 'No active spec';
1063
+ return 'No active run';
1064
+ }
1065
+
1066
+ function buildCurrentGroups(snapshot, flow) {
1067
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1068
+
1069
+ if (effectiveFlow === 'aidlc') {
1070
+ const bolt = getCurrentBolt(snapshot);
1071
+ if (!bolt) {
1072
+ return [];
1073
+ }
1074
+ const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
1075
+ const completedStages = stages.filter((stage) => stage.status === 'completed').length;
1076
+ return [{
1077
+ key: `current:bolt:${bolt.id}`,
1078
+ label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages`,
1079
+ files: filterExistingFiles([
1080
+ ...collectAidlcBoltFiles(bolt),
1081
+ ...collectAidlcIntentContextFiles(snapshot, bolt.intent)
1082
+ ])
1083
+ }];
1084
+ }
1085
+
1086
+ if (effectiveFlow === 'simple') {
1087
+ const spec = getCurrentSpec(snapshot);
1088
+ if (!spec) {
1089
+ return [];
1090
+ }
1091
+ return [{
1092
+ key: `current:spec:${spec.name}`,
1093
+ label: `${spec.name} [${spec.state}] ${spec.tasksCompleted}/${spec.tasksTotal} tasks`,
1094
+ files: filterExistingFiles(collectSimpleSpecFiles(spec))
1095
+ }];
1096
+ }
1097
+
1098
+ const run = getCurrentRun(snapshot);
1099
+ if (!run) {
1100
+ return [];
1101
+ }
1102
+ const workItems = Array.isArray(run.workItems) ? run.workItems : [];
1103
+ const completed = workItems.filter((item) => item.status === 'completed').length;
1104
+ return [{
1105
+ key: `current:run:${run.id}`,
1106
+ label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items`,
1107
+ files: filterExistingFiles(collectFireRunFiles(run).map((file) => ({ ...file, scope: 'active' })))
1108
+ }];
1109
+ }
1110
+
1111
+ function buildRunFileGroups(fileEntries) {
1112
+ const order = ['active', 'upcoming', 'completed', 'intent', 'other'];
1113
+ const buckets = new Map(order.map((scope) => [scope, []]));
1114
+
1115
+ for (const fileEntry of Array.isArray(fileEntries) ? fileEntries : []) {
1116
+ const scope = order.includes(fileEntry?.scope) ? fileEntry.scope : 'other';
1117
+ buckets.get(scope).push(fileEntry);
1118
+ }
1119
+
1120
+ const groups = [];
1121
+ for (const scope of order) {
1122
+ const files = buckets.get(scope) || [];
1123
+ if (files.length === 0) {
1124
+ continue;
1125
+ }
1126
+ groups.push({
1127
+ key: `run-files:scope:${scope}`,
1128
+ label: `${formatScope(scope)} files (${files.length})`,
1129
+ files: filterExistingFiles(files)
1130
+ });
1131
+ }
1132
+ return groups;
1133
+ }
1134
+
1135
+ function normalizeInfoLine(line) {
1136
+ const normalized = normalizePanelLine(line);
1137
+ return {
1138
+ label: normalized.text,
1139
+ color: normalized.color,
1140
+ bold: normalized.bold
1141
+ };
1142
+ }
1143
+
1144
+ function toInfoRows(lines, keyPrefix, emptyLabel = 'No data') {
1145
+ const safe = Array.isArray(lines) ? lines : [];
1146
+ if (safe.length === 0) {
1059
1147
  return [{
1060
1148
  kind: 'info',
1061
- key: 'run-files:empty',
1062
- label: getNoFileMessage(flow),
1149
+ key: `${keyPrefix}:empty`,
1150
+ label: emptyLabel,
1063
1151
  selectable: false
1064
1152
  }];
1065
1153
  }
1066
1154
 
1067
- return fileEntries.map((file, index) => ({
1068
- kind: 'file',
1069
- key: `run-files:${file.path}:${index}`,
1070
- label: file.label,
1071
- path: file.path,
1072
- scope: file.scope || 'file',
1073
- selectable: true
1074
- }));
1155
+ return safe.map((line, index) => {
1156
+ const normalized = normalizeInfoLine(line);
1157
+ return {
1158
+ kind: 'info',
1159
+ key: `${keyPrefix}:${index}`,
1160
+ label: normalized.label,
1161
+ color: normalized.color,
1162
+ bold: normalized.bold,
1163
+ selectable: true
1164
+ };
1165
+ });
1166
+ }
1167
+
1168
+ function buildOverviewIntentGroups(snapshot, flow, filter = 'next') {
1169
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1170
+ const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
1171
+ const isIncluded = (status) => {
1172
+ if (normalizedFilter === 'completed') {
1173
+ return status === 'completed';
1174
+ }
1175
+ return status !== 'completed';
1176
+ };
1177
+
1178
+ if (effectiveFlow === 'aidlc') {
1179
+ const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
1180
+ return intents
1181
+ .filter((intent) => isIncluded(intent?.status || 'pending'))
1182
+ .map((intent, index) => ({
1183
+ key: `overview:intent:${intent?.id || index}`,
1184
+ label: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${intent?.completedStories || 0}/${intent?.storyCount || 0} stories, ${intent?.completedUnits || 0}/${intent?.unitCount || 0} units)`,
1185
+ files: filterExistingFiles(collectAidlcIntentContextFiles(snapshot, intent?.id))
1186
+ }));
1187
+ }
1188
+
1189
+ if (effectiveFlow === 'simple') {
1190
+ const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
1191
+ return specs
1192
+ .filter((spec) => isIncluded(spec?.state || 'pending'))
1193
+ .map((spec, index) => ({
1194
+ key: `overview:spec:${spec?.name || index}`,
1195
+ label: `${spec?.name || 'unknown'}: ${spec?.state || 'pending'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks)`,
1196
+ files: filterExistingFiles(collectSimpleSpecFiles(spec))
1197
+ }));
1198
+ }
1199
+
1200
+ const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
1201
+ return intents
1202
+ .filter((intent) => isIncluded(intent?.status || 'pending'))
1203
+ .map((intent, index) => {
1204
+ const workItems = Array.isArray(intent?.workItems) ? intent.workItems : [];
1205
+ const done = workItems.filter((item) => item.status === 'completed').length;
1206
+ const files = [{
1207
+ label: `${intent?.id || 'intent'}/brief.md`,
1208
+ path: intent?.filePath,
1209
+ scope: 'intent'
1210
+ }, ...workItems.map((item) => ({
1211
+ label: `${intent?.id || 'intent'}/${item?.id || 'work-item'}.md`,
1212
+ path: item?.filePath,
1213
+ scope: item?.status === 'completed' ? 'completed' : 'upcoming'
1214
+ }))];
1215
+ return {
1216
+ key: `overview:intent:${intent?.id || index}`,
1217
+ label: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${done}/${workItems.length} work items)`,
1218
+ files: filterExistingFiles(files)
1219
+ };
1220
+ });
1221
+ }
1222
+
1223
+ function buildStandardsGroups(snapshot, flow) {
1224
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1225
+ if (effectiveFlow === 'simple') {
1226
+ return [];
1227
+ }
1228
+
1229
+ const standards = Array.isArray(snapshot?.standards) ? snapshot.standards : [];
1230
+ return standards.map((standard, index) => {
1231
+ const id = standard?.type || standard?.name || String(index);
1232
+ const name = `${standard?.name || standard?.type || 'standard'}.md`;
1233
+ return {
1234
+ key: `standards:${id}`,
1235
+ label: name,
1236
+ files: filterExistingFiles([{
1237
+ label: name,
1238
+ path: standard?.filePath,
1239
+ scope: 'file'
1240
+ }])
1241
+ };
1242
+ });
1243
+ }
1244
+
1245
+ function buildProjectGroups(snapshot, flow) {
1246
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1247
+ const files = [];
1248
+
1249
+ if (effectiveFlow === 'aidlc') {
1250
+ files.push({
1251
+ label: 'memory-bank/project.yaml',
1252
+ path: path.join(snapshot?.rootPath || '', 'project.yaml'),
1253
+ scope: 'file'
1254
+ });
1255
+ } else if (effectiveFlow === 'simple') {
1256
+ files.push({
1257
+ label: 'package.json',
1258
+ path: path.join(snapshot?.workspacePath || '', 'package.json'),
1259
+ scope: 'file'
1260
+ });
1261
+ } else {
1262
+ files.push({
1263
+ label: '.specs-fire/state.yaml',
1264
+ path: path.join(snapshot?.rootPath || '', 'state.yaml'),
1265
+ scope: 'file'
1266
+ });
1267
+ }
1268
+
1269
+ const projectName = snapshot?.project?.name || 'unknown-project';
1270
+ return [{
1271
+ key: `project:${projectName}`,
1272
+ label: `project ${projectName}`,
1273
+ files: filterExistingFiles(files)
1274
+ }];
1075
1275
  }
1076
1276
 
1077
1277
  function collectAidlcIntentContextFiles(snapshot, intentId) {
@@ -1303,10 +1503,10 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
1303
1503
  }
1304
1504
 
1305
1505
  return {
1306
- text: truncate(` ${row.label || ''}`, width),
1307
- color: 'gray',
1308
- bold: false,
1309
- selected: false
1506
+ text: truncate(`${isSelected ? `${cursor} ` : ' '}${row.label || ''}`, width),
1507
+ color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : (row.color || 'gray'),
1508
+ bold: isSelected || Boolean(row.bold),
1509
+ selected: isSelected
1310
1510
  };
1311
1511
  });
1312
1512
  }
@@ -1399,19 +1599,24 @@ function openFileWithDefaultApp(filePath) {
1399
1599
 
1400
1600
  function buildQuickHelpText(view, options = {}) {
1401
1601
  const {
1602
+ flow = 'fire',
1402
1603
  previewOpen = false,
1403
1604
  availableFlowCount = 1
1404
1605
  } = options;
1606
+ const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
1607
+ const isSimple = String(flow || '').toLowerCase() === 'simple';
1608
+ const activeLabel = isAidlc ? 'active bolt' : (isSimple ? 'active spec' : 'active run');
1405
1609
 
1406
- const parts = ['1/2/3 views', 'g/G sections'];
1610
+ const parts = ['1/2/3/4 tabs', 'g/G sections'];
1407
1611
 
1408
- if (view === 'runs') {
1612
+ if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health') {
1409
1613
  if (previewOpen) {
1410
1614
  parts.push('tab pane', '↑/↓ nav/scroll', 'v close');
1411
1615
  } else {
1412
1616
  parts.push('↑/↓ navigate', 'enter expand', 'v preview');
1413
1617
  }
1414
1618
  }
1619
+ parts.push(`tab1 ${activeLabel}`);
1415
1620
 
1416
1621
  if (availableFlowCount > 1) {
1417
1622
  parts.push('[/] flow');
@@ -1424,25 +1629,30 @@ function buildQuickHelpText(view, options = {}) {
1424
1629
  function buildHelpOverlayLines(options = {}) {
1425
1630
  const {
1426
1631
  view = 'runs',
1632
+ flow = 'fire',
1427
1633
  previewOpen = false,
1428
1634
  paneFocus = 'main',
1429
1635
  availableFlowCount = 1,
1430
1636
  showErrorSection = false
1431
1637
  } = options;
1638
+ const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
1639
+ const isSimple = String(flow || '').toLowerCase() === 'simple';
1640
+ const itemLabel = isAidlc ? 'bolt' : (isSimple ? 'spec' : 'run');
1641
+ const itemPlural = isAidlc ? 'bolts' : (isSimple ? 'specs' : 'runs');
1432
1642
 
1433
1643
  const lines = [
1434
1644
  { text: 'Global', color: 'cyan', bold: true },
1435
1645
  'q or Ctrl+C quit',
1436
1646
  'r refresh snapshot',
1437
- '1 runs | 2 overview | 3 health',
1647
+ `1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health`,
1438
1648
  'g next section | G previous section',
1439
1649
  'h/? toggle this shortcuts overlay',
1440
1650
  'esc close overlays (help/preview/fullscreen)',
1441
1651
  { text: '', color: undefined, bold: false },
1442
- { text: 'Runs View', color: 'yellow', bold: true },
1443
- 'a current | f files | p pending',
1652
+ { text: 'Tab 1 Active', color: 'yellow', bold: true },
1653
+ `a focus active ${itemLabel}`,
1444
1654
  'up/down or j/k move selection',
1445
- 'enter expand/collapse pending/completed groups',
1655
+ 'enter expand/collapse selected folder row',
1446
1656
  'v preview selected file',
1447
1657
  'v twice quickly opens fullscreen preview overlay',
1448
1658
  'tab switch focus between main and preview panes',
@@ -1459,12 +1669,16 @@ function buildHelpOverlayLines(options = {}) {
1459
1669
 
1460
1670
  lines.push(
1461
1671
  { text: '', color: undefined, bold: false },
1462
- { text: 'Overview View', color: 'green', bold: true },
1463
- 'i intents | c completed | s standards | p project',
1464
- 'when Intents is focused: n next | x completed | left/right toggle filter',
1672
+ { text: 'Tab 2 Intents', color: 'green', bold: true },
1673
+ 'i focus intents',
1674
+ 'n next intents | x completed intents',
1675
+ 'left/right toggles next/completed when intents is focused',
1676
+ { text: '', color: undefined, bold: false },
1677
+ { text: 'Tab 3 Completed', color: 'blue', bold: true },
1678
+ 'c focus completed items',
1465
1679
  { text: '', color: undefined, bold: false },
1466
- { text: 'Health View', color: 'magenta', bold: true },
1467
- `t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
1680
+ { text: 'Tab 4 Standards/Health', color: 'magenta', bold: true },
1681
+ `s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
1468
1682
  { text: '', color: undefined, bold: false },
1469
1683
  { text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
1470
1684
  );
@@ -1685,11 +1899,15 @@ function createDashboardApp(deps) {
1685
1899
  }
1686
1900
 
1687
1901
  function TabsBar(props) {
1688
- const { view, width, icons } = props;
1902
+ const { view, width, icons, flow: activeFlow } = props;
1903
+ const effectiveFlow = String(activeFlow || '').toLowerCase();
1904
+ const primaryLabel = effectiveFlow === 'aidlc' ? 'BOLTS' : (effectiveFlow === 'simple' ? 'SPECS' : 'RUNS');
1905
+ const completedLabel = effectiveFlow === 'aidlc' ? 'COMPLETED BOLTS' : (effectiveFlow === 'simple' ? 'COMPLETED SPECS' : 'COMPLETED RUNS');
1689
1906
  const tabs = [
1690
- { id: 'runs', label: ` 1 ${icons.runs} RUNS ` },
1691
- { id: 'overview', label: ` 2 ${icons.overview} OVERVIEW ` },
1692
- { id: 'health', label: ` 3 ${icons.health} HEALTH ` }
1907
+ { id: 'runs', label: ` 1 ${icons.runs} ${primaryLabel} ` },
1908
+ { id: 'intents', label: ` 2 ${icons.overview} INTENTS ` },
1909
+ { id: 'completed', label: ` 3 ${icons.runs} ${completedLabel} ` },
1910
+ { id: 'health', label: ` 4 ${icons.health} STANDARDS/HEALTH ` }
1693
1911
  ];
1694
1912
 
1695
1913
  return React.createElement(
@@ -1757,18 +1975,24 @@ function createDashboardApp(deps) {
1757
1975
  const [error, setError] = useState(initialNormalizedError);
1758
1976
  const [ui, setUi] = useState(createInitialUIState());
1759
1977
  const [sectionFocus, setSectionFocus] = useState({
1760
- runs: 'run-files',
1761
- overview: 'project',
1762
- health: 'stats'
1978
+ runs: 'current-run',
1979
+ intents: 'intent-status',
1980
+ completed: 'completed-runs',
1981
+ health: 'standards'
1763
1982
  });
1764
1983
  const [selectionBySection, setSelectionBySection] = useState({
1765
- 'run-files': 0,
1766
- pending: 0,
1767
- completed: 0
1984
+ 'current-run': 0,
1985
+ 'intent-status': 0,
1986
+ 'completed-runs': 0,
1987
+ standards: 0,
1988
+ stats: 0,
1989
+ warnings: 0,
1990
+ 'error-details': 0
1768
1991
  });
1769
1992
  const [expandedGroups, setExpandedGroups] = useState({});
1770
1993
  const [previewTarget, setPreviewTarget] = useState(null);
1771
1994
  const [overviewIntentFilter, setOverviewIntentFilter] = useState('next');
1995
+ const [deferredTabsReady, setDeferredTabsReady] = useState(false);
1772
1996
  const [previewOpen, setPreviewOpen] = useState(false);
1773
1997
  const [paneFocus, setPaneFocus] = useState('main');
1774
1998
  const [overlayPreviewOpen, setOverlayPreviewOpen] = useState(false);
@@ -1804,24 +2028,86 @@ function createDashboardApp(deps) {
1804
2028
  return base.filter((sectionKey) => sectionKey !== 'error-details' || showErrorPanelForSections);
1805
2029
  }, [showErrorPanelForSections]);
1806
2030
 
1807
- const runFileEntries = getRunFileEntries(snapshot, activeFlow);
1808
- const runFileRows = toRunFileRows(runFileEntries, activeFlow);
1809
- const pendingRows = toExpandableRows(
1810
- buildPendingGroups(snapshot, activeFlow),
1811
- getNoPendingMessage(getEffectiveFlow(activeFlow, snapshot)),
1812
- expandedGroups
1813
- );
1814
- const completedRows = toExpandableRows(
1815
- buildCompletedGroups(snapshot, activeFlow),
1816
- getNoCompletedMessage(getEffectiveFlow(activeFlow, snapshot)),
1817
- expandedGroups
2031
+ const effectiveFlow = getEffectiveFlow(activeFlow, snapshot);
2032
+ const currentGroups = buildCurrentGroups(snapshot, activeFlow);
2033
+ const currentExpandedGroups = { ...expandedGroups };
2034
+ for (const group of currentGroups) {
2035
+ if (currentExpandedGroups[group.key] == null) {
2036
+ currentExpandedGroups[group.key] = true;
2037
+ }
2038
+ }
2039
+
2040
+ const currentRunRows = toExpandableRows(
2041
+ currentGroups,
2042
+ getNoCurrentMessage(effectiveFlow),
2043
+ currentExpandedGroups
1818
2044
  );
2045
+ const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
2046
+ const intentRows = shouldHydrateSecondaryTabs
2047
+ ? [
2048
+ {
2049
+ kind: 'info',
2050
+ key: 'intent-filter',
2051
+ label: `filter ${overviewIntentFilter === 'completed' ? 'next | [COMPLETED]' : '[NEXT] | completed'} (n/x)`,
2052
+ color: 'cyan',
2053
+ bold: true,
2054
+ selectable: false
2055
+ },
2056
+ ...toExpandableRows(
2057
+ buildOverviewIntentGroups(snapshot, activeFlow, overviewIntentFilter),
2058
+ overviewIntentFilter === 'completed' ? 'No completed intents yet' : 'No upcoming intents',
2059
+ expandedGroups
2060
+ )
2061
+ ]
2062
+ : toInfoRows(['Loading intents...'], 'intent-loading');
2063
+ const completedRows = shouldHydrateSecondaryTabs
2064
+ ? toExpandableRows(
2065
+ buildCompletedGroups(snapshot, activeFlow),
2066
+ getNoCompletedMessage(effectiveFlow),
2067
+ expandedGroups
2068
+ )
2069
+ : toInfoRows(['Loading completed items...'], 'completed-loading');
2070
+ const standardsRows = shouldHydrateSecondaryTabs
2071
+ ? toExpandableRows(
2072
+ buildStandardsGroups(snapshot, activeFlow),
2073
+ effectiveFlow === 'simple' ? 'No standards for SIMPLE flow' : 'No standards found',
2074
+ expandedGroups
2075
+ )
2076
+ : toInfoRows(['Loading standards...'], 'standards-loading');
2077
+ const statsRows = shouldHydrateSecondaryTabs
2078
+ ? toInfoRows(
2079
+ buildStatsLines(snapshot, 200, activeFlow),
2080
+ 'stats',
2081
+ 'No stats available'
2082
+ )
2083
+ : toInfoRows(['Loading stats...'], 'stats-loading');
2084
+ const warningsRows = shouldHydrateSecondaryTabs
2085
+ ? toInfoRows(
2086
+ buildWarningsLines(snapshot, 200),
2087
+ 'warnings',
2088
+ 'No warnings'
2089
+ )
2090
+ : toInfoRows(['Loading warnings...'], 'warnings-loading');
2091
+ const errorDetailsRows = shouldHydrateSecondaryTabs
2092
+ ? toInfoRows(
2093
+ buildErrorLines(error, 200),
2094
+ 'error-details',
2095
+ 'No error details'
2096
+ )
2097
+ : toInfoRows(['Loading error details...'], 'error-loading');
1819
2098
 
1820
2099
  const rowsBySection = {
1821
- 'run-files': runFileRows,
1822
- pending: pendingRows,
1823
- completed: completedRows
2100
+ 'current-run': currentRunRows,
2101
+ 'intent-status': intentRows,
2102
+ 'completed-runs': completedRows,
2103
+ standards: standardsRows,
2104
+ stats: statsRows,
2105
+ warnings: warningsRows,
2106
+ 'error-details': errorDetailsRows
1824
2107
  };
2108
+ const rowLengthSignature = Object.entries(rowsBySection)
2109
+ .map(([key, rowsForSection]) => `${key}:${Array.isArray(rowsForSection) ? rowsForSection.length : 0}`)
2110
+ .join('|');
1825
2111
 
1826
2112
  const currentSectionOrder = getAvailableSections(ui.view);
1827
2113
  const focusedSection = currentSectionOrder.includes(sectionFocus[ui.view])
@@ -1914,12 +2200,18 @@ function createDashboardApp(deps) {
1914
2200
  }
1915
2201
 
1916
2202
  if (input === '2') {
1917
- setUi((previous) => ({ ...previous, view: 'overview' }));
2203
+ setUi((previous) => ({ ...previous, view: 'intents' }));
1918
2204
  setPaneFocus('main');
1919
2205
  return;
1920
2206
  }
1921
2207
 
1922
2208
  if (input === '3') {
2209
+ setUi((previous) => ({ ...previous, view: 'completed' }));
2210
+ setPaneFocus('main');
2211
+ return;
2212
+ }
2213
+
2214
+ if (input === '4') {
1923
2215
  setUi((previous) => ({ ...previous, view: 'health' }));
1924
2216
  setPaneFocus('main');
1925
2217
  return;
@@ -1938,14 +2230,19 @@ function createDashboardApp(deps) {
1938
2230
  return availableFlowIds[nextIndex];
1939
2231
  });
1940
2232
  setSelectionBySection({
1941
- 'run-files': 0,
1942
- pending: 0,
1943
- completed: 0
2233
+ 'current-run': 0,
2234
+ 'intent-status': 0,
2235
+ 'completed-runs': 0,
2236
+ standards: 0,
2237
+ stats: 0,
2238
+ warnings: 0,
2239
+ 'error-details': 0
1944
2240
  });
1945
2241
  setSectionFocus({
1946
- runs: 'run-files',
1947
- overview: 'project',
1948
- health: 'stats'
2242
+ runs: 'current-run',
2243
+ intents: 'intent-status',
2244
+ completed: 'completed-runs',
2245
+ health: 'standards'
1949
2246
  });
1950
2247
  setOverviewIntentFilter('next');
1951
2248
  setExpandedGroups({});
@@ -1970,14 +2267,19 @@ function createDashboardApp(deps) {
1970
2267
  return availableFlowIds[nextIndex];
1971
2268
  });
1972
2269
  setSelectionBySection({
1973
- 'run-files': 0,
1974
- pending: 0,
1975
- completed: 0
2270
+ 'current-run': 0,
2271
+ 'intent-status': 0,
2272
+ 'completed-runs': 0,
2273
+ standards: 0,
2274
+ stats: 0,
2275
+ warnings: 0,
2276
+ 'error-details': 0
1976
2277
  });
1977
2278
  setSectionFocus({
1978
- runs: 'run-files',
1979
- overview: 'project',
1980
- health: 'stats'
2279
+ runs: 'current-run',
2280
+ intents: 'intent-status',
2281
+ completed: 'completed-runs',
2282
+ health: 'standards'
1981
2283
  });
1982
2284
  setOverviewIntentFilter('next');
1983
2285
  setExpandedGroups({});
@@ -1994,12 +2296,12 @@ function createDashboardApp(deps) {
1994
2296
  ? sectionFocus[ui.view]
1995
2297
  : (availableSections[0] || 'current-run');
1996
2298
 
1997
- if (key.tab && ui.view === 'runs' && previewOpen) {
2299
+ if (key.tab && previewOpen) {
1998
2300
  setPaneFocus((previous) => (previous === 'main' ? 'preview' : 'main'));
1999
2301
  return;
2000
2302
  }
2001
2303
 
2002
- if (ui.view === 'overview' && activeSection === 'intent-status') {
2304
+ if (ui.view === 'intents' && activeSection === 'intent-status') {
2003
2305
  if (input === 'n') {
2004
2306
  setOverviewIntentFilter('next');
2005
2307
  return;
@@ -2038,34 +2340,21 @@ function createDashboardApp(deps) {
2038
2340
  setPaneFocus('main');
2039
2341
  return;
2040
2342
  }
2041
- if (input === 'f') {
2042
- setSectionFocus((previous) => ({ ...previous, runs: 'run-files' }));
2043
- setPaneFocus('main');
2044
- return;
2045
- }
2046
- if (input === 'p') {
2047
- setSectionFocus((previous) => ({ ...previous, runs: 'pending' }));
2048
- setPaneFocus('main');
2049
- return;
2050
- }
2051
- } else if (ui.view === 'overview') {
2052
- if (input === 'p') {
2053
- setSectionFocus((previous) => ({ ...previous, overview: 'project' }));
2054
- return;
2055
- }
2343
+ } else if (ui.view === 'intents') {
2056
2344
  if (input === 'i') {
2057
- setSectionFocus((previous) => ({ ...previous, overview: 'intent-status' }));
2058
- return;
2059
- }
2060
- if (input === 's') {
2061
- setSectionFocus((previous) => ({ ...previous, overview: 'standards' }));
2345
+ setSectionFocus((previous) => ({ ...previous, intents: 'intent-status' }));
2062
2346
  return;
2063
2347
  }
2348
+ } else if (ui.view === 'completed') {
2064
2349
  if (input === 'c') {
2065
- setSectionFocus((previous) => ({ ...previous, overview: 'completed-runs' }));
2350
+ setSectionFocus((previous) => ({ ...previous, completed: 'completed-runs' }));
2066
2351
  return;
2067
2352
  }
2068
2353
  } else if (ui.view === 'health') {
2354
+ if (input === 's') {
2355
+ setSectionFocus((previous) => ({ ...previous, health: 'standards' }));
2356
+ return;
2357
+ }
2069
2358
  if (input === 't') {
2070
2359
  setSectionFocus((previous) => ({ ...previous, health: 'stats' }));
2071
2360
  return;
@@ -2094,7 +2383,7 @@ function createDashboardApp(deps) {
2094
2383
  }
2095
2384
  }
2096
2385
 
2097
- if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
2386
+ if (key.upArrow || key.downArrow || input === 'j' || input === 'k') {
2098
2387
  const moveDown = key.downArrow || input === 'j';
2099
2388
  const moveUp = key.upArrow || input === 'k';
2100
2389
 
@@ -2107,11 +2396,7 @@ function createDashboardApp(deps) {
2107
2396
  return;
2108
2397
  }
2109
2398
 
2110
- const targetSection = activeSection === 'current-run' ? 'run-files' : activeSection;
2111
- if (targetSection !== activeSection) {
2112
- setSectionFocus((previous) => ({ ...previous, runs: targetSection }));
2113
- }
2114
-
2399
+ const targetSection = activeSection;
2115
2400
  const targetRows = rowsBySection[targetSection] || [];
2116
2401
  if (targetRows.length === 0) {
2117
2402
  return;
@@ -2129,24 +2414,22 @@ function createDashboardApp(deps) {
2129
2414
  return;
2130
2415
  }
2131
2416
 
2132
- if (ui.view === 'runs' && (key.return || key.enter)) {
2133
- if (activeSection === 'pending' || activeSection === 'completed') {
2134
- const rowsForSection = rowsBySection[activeSection] || [];
2135
- const selectedRow = getSelectedRow(rowsForSection, selectionBySection[activeSection] || 0);
2136
- if (selectedRow?.kind === 'group' && selectedRow.expandable) {
2137
- setExpandedGroups((previous) => ({
2138
- ...previous,
2139
- [selectedRow.key]: !previous[selectedRow.key]
2140
- }));
2141
- }
2417
+ if (key.return || key.enter) {
2418
+ const rowsForSection = rowsBySection[activeSection] || [];
2419
+ const selectedRow = getSelectedRow(rowsForSection, selectionBySection[activeSection] || 0);
2420
+ if (selectedRow?.kind === 'group' && selectedRow.expandable) {
2421
+ setExpandedGroups((previous) => ({
2422
+ ...previous,
2423
+ [selectedRow.key]: !previous[selectedRow.key]
2424
+ }));
2142
2425
  }
2143
2426
  return;
2144
2427
  }
2145
2428
 
2146
- if (input === 'v' && ui.view === 'runs') {
2429
+ if (input === 'v') {
2147
2430
  const target = selectedFocusedFile || previewTarget;
2148
2431
  if (!target) {
2149
- setStatusLine('Select a file row first (run files, pending, or completed).');
2432
+ setStatusLine('Select a file row first.');
2150
2433
  return;
2151
2434
  }
2152
2435
 
@@ -2184,7 +2467,7 @@ function createDashboardApp(deps) {
2184
2467
  return;
2185
2468
  }
2186
2469
 
2187
- if (input === 'o' && ui.view === 'runs') {
2470
+ if (input === 'o') {
2188
2471
  const target = selectedFocusedFile || previewTarget;
2189
2472
  const result = openFileWithDefaultApp(target?.path);
2190
2473
  setStatusLine(result.message);
@@ -2196,21 +2479,38 @@ function createDashboardApp(deps) {
2196
2479
  }, [refresh]);
2197
2480
 
2198
2481
  useEffect(() => {
2199
- setSelectionBySection((previous) => ({
2200
- ...previous,
2201
- 'run-files': clampIndex(previous['run-files'] || 0, runFileRows.length),
2202
- pending: clampIndex(previous.pending || 0, pendingRows.length),
2203
- completed: clampIndex(previous.completed || 0, completedRows.length)
2204
- }));
2205
- }, [activeFlow, runFileRows.length, pendingRows.length, completedRows.length, snapshot?.generatedAt]);
2482
+ setSelectionBySection((previous) => {
2483
+ let changed = false;
2484
+ const next = { ...previous };
2485
+
2486
+ for (const [sectionKey, sectionRows] of Object.entries(rowsBySection)) {
2487
+ const previousValue = Number.isFinite(previous[sectionKey]) ? previous[sectionKey] : 0;
2488
+ const clampedValue = clampIndex(previousValue, sectionRows.length);
2489
+ if (previousValue !== clampedValue) {
2490
+ next[sectionKey] = clampedValue;
2491
+ changed = true;
2492
+ } else if (!(sectionKey in next)) {
2493
+ next[sectionKey] = clampedValue;
2494
+ changed = true;
2495
+ }
2496
+ }
2497
+
2498
+ return changed ? next : previous;
2499
+ });
2500
+ }, [activeFlow, rowLengthSignature, snapshot?.generatedAt]);
2206
2501
 
2207
2502
  useEffect(() => {
2208
- if (ui.view !== 'runs') {
2209
- setPreviewOpen(false);
2210
- setOverlayPreviewOpen(false);
2211
- setPreviewScroll(0);
2212
- setPaneFocus('main');
2213
- }
2503
+ setDeferredTabsReady(false);
2504
+ const timer = setTimeout(() => {
2505
+ setDeferredTabsReady(true);
2506
+ }, 0);
2507
+ return () => {
2508
+ clearTimeout(timer);
2509
+ };
2510
+ }, [activeFlow, snapshot?.generatedAt]);
2511
+
2512
+ useEffect(() => {
2513
+ setPaneFocus('main');
2214
2514
  }, [ui.view]);
2215
2515
 
2216
2516
  useEffect(() => {
@@ -2326,14 +2626,15 @@ function createDashboardApp(deps) {
2326
2626
  const fullWidth = Math.max(40, cols - 1);
2327
2627
  const showFooterHelpLine = rows >= 10;
2328
2628
  const showErrorPanel = Boolean(error) && rows >= 18;
2629
+ const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp;
2329
2630
  const showErrorInline = Boolean(error) && !showErrorPanel;
2330
2631
  const densePanels = rows <= 28 || cols <= 120;
2331
2632
 
2332
- const reservedRows = 2 + (showFooterHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
2633
+ const reservedRows = 2 + (showFooterHelpLine ? 1 : 0) + (showGlobalErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
2333
2634
  const contentRowsBudget = Math.max(4, rows - reservedRows);
2334
2635
  const ultraCompact = rows <= 14;
2335
2636
  const panelTitles = getPanelTitles(activeFlow, snapshot);
2336
- const splitPreviewLayout = ui.view === 'runs' && previewOpen && !overlayPreviewOpen && !ui.showHelp && cols >= 110 && rows >= 16;
2637
+ const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && cols >= 110 && rows >= 16;
2337
2638
  const mainPaneWidth = splitPreviewLayout
2338
2639
  ? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
2339
2640
  : fullWidth;
@@ -2343,19 +2644,17 @@ function createDashboardApp(deps) {
2343
2644
  const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
2344
2645
  const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
2345
2646
 
2346
- const runFileLines = buildInteractiveRowsLines(
2347
- runFileRows,
2348
- selectionBySection['run-files'] || 0,
2349
- icons,
2350
- mainCompactWidth,
2351
- ui.view === 'runs' && focusedSection === 'run-files' && paneFocus === 'main'
2352
- );
2353
- const pendingLines = buildInteractiveRowsLines(
2354
- pendingRows,
2355
- selectionBySection.pending || 0,
2356
- icons,
2357
- mainCompactWidth,
2358
- ui.view === 'runs' && focusedSection === 'pending' && paneFocus === 'main'
2647
+ const sectionLines = Object.fromEntries(
2648
+ Object.entries(rowsBySection).map(([sectionKey, sectionRows]) => [
2649
+ sectionKey,
2650
+ buildInteractiveRowsLines(
2651
+ sectionRows,
2652
+ selectionBySection[sectionKey] || 0,
2653
+ icons,
2654
+ mainCompactWidth,
2655
+ paneFocus === 'main' && focusedSection === sectionKey
2656
+ )
2657
+ ])
2359
2658
  );
2360
2659
  const effectivePreviewTarget = previewTarget || selectedFocusedFile;
2361
2660
  const previewLines = previewOpen
@@ -2366,12 +2665,14 @@ function createDashboardApp(deps) {
2366
2665
 
2367
2666
  const shortcutsOverlayLines = buildHelpOverlayLines({
2368
2667
  view: ui.view,
2668
+ flow: activeFlow,
2369
2669
  previewOpen,
2370
2670
  paneFocus,
2371
2671
  availableFlowCount: availableFlowIds.length,
2372
2672
  showErrorSection: showErrorPanel
2373
2673
  });
2374
2674
  const quickHelpText = buildQuickHelpText(ui.view, {
2675
+ flow: activeFlow,
2375
2676
  previewOpen,
2376
2677
  paneFocus,
2377
2678
  availableFlowCount: availableFlowIds.length
@@ -2387,7 +2688,7 @@ function createDashboardApp(deps) {
2387
2688
  borderColor: 'cyan'
2388
2689
  }
2389
2690
  ];
2390
- } else if (ui.view === 'runs' && previewOpen && overlayPreviewOpen) {
2691
+ } else if (previewOpen && overlayPreviewOpen) {
2391
2692
  panelCandidates = [
2392
2693
  {
2393
2694
  key: 'preview-overlay',
@@ -2396,45 +2697,42 @@ function createDashboardApp(deps) {
2396
2697
  borderColor: 'magenta'
2397
2698
  }
2398
2699
  ];
2399
- } else if (ui.view === 'overview') {
2700
+ } else if (ui.view === 'intents') {
2400
2701
  panelCandidates = [
2401
2702
  {
2402
2703
  key: 'intent-status',
2403
2704
  title: 'Intents',
2404
- lines: buildOverviewIntentLines(snapshot, mainCompactWidth, activeFlow, overviewIntentFilter),
2705
+ lines: sectionLines['intent-status'],
2405
2706
  borderColor: 'yellow'
2406
- },
2707
+ }
2708
+ ];
2709
+ } else if (ui.view === 'completed') {
2710
+ panelCandidates = [
2407
2711
  {
2408
2712
  key: 'completed-runs',
2409
2713
  title: panelTitles.completed,
2410
- lines: buildCompletedLines(snapshot, mainCompactWidth, activeFlow),
2714
+ lines: sectionLines['completed-runs'],
2411
2715
  borderColor: 'blue'
2412
- },
2716
+ }
2717
+ ];
2718
+ } else if (ui.view === 'health') {
2719
+ panelCandidates = [
2413
2720
  {
2414
2721
  key: 'standards',
2415
2722
  title: 'Standards',
2416
- lines: buildOverviewStandardsLines(snapshot, mainCompactWidth, activeFlow),
2723
+ lines: sectionLines.standards,
2417
2724
  borderColor: 'blue'
2418
2725
  },
2419
- {
2420
- key: 'project',
2421
- title: 'Project + Workspace',
2422
- lines: buildOverviewProjectLines(snapshot, mainCompactWidth, activeFlow),
2423
- borderColor: 'green'
2424
- }
2425
- ];
2426
- } else if (ui.view === 'health') {
2427
- panelCandidates = [
2428
2726
  {
2429
2727
  key: 'stats',
2430
2728
  title: 'Stats',
2431
- lines: buildStatsLines(snapshot, mainCompactWidth, activeFlow),
2729
+ lines: sectionLines.stats,
2432
2730
  borderColor: 'magenta'
2433
2731
  },
2434
2732
  {
2435
2733
  key: 'warnings',
2436
2734
  title: 'Warnings',
2437
- lines: buildWarningsLines(snapshot, mainCompactWidth),
2735
+ lines: sectionLines.warnings,
2438
2736
  borderColor: 'red'
2439
2737
  }
2440
2738
  ];
@@ -2443,47 +2741,36 @@ function createDashboardApp(deps) {
2443
2741
  panelCandidates.push({
2444
2742
  key: 'error-details',
2445
2743
  title: 'Error Details',
2446
- lines: buildErrorLines(error, mainCompactWidth),
2744
+ lines: sectionLines['error-details'],
2447
2745
  borderColor: 'red'
2448
2746
  });
2449
2747
  }
2450
2748
  } else {
2451
- const includeInlinePreviewPanel = previewOpen && !splitPreviewLayout;
2452
2749
  panelCandidates = [
2453
2750
  {
2454
2751
  key: 'current-run',
2455
2752
  title: panelTitles.current,
2456
- lines: buildCurrentRunLines(snapshot, mainCompactWidth, activeFlow),
2753
+ lines: sectionLines['current-run'],
2457
2754
  borderColor: 'green'
2458
- },
2459
- {
2460
- key: 'run-files',
2461
- title: panelTitles.files,
2462
- lines: runFileLines,
2463
- borderColor: 'yellow'
2464
- },
2465
- includeInlinePreviewPanel
2466
- ? {
2467
- key: 'preview',
2468
- title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
2469
- lines: previewLines,
2470
- borderColor: 'magenta'
2471
- }
2472
- : null,
2473
- {
2474
- key: 'pending',
2475
- title: panelTitles.pending,
2476
- lines: pendingLines,
2477
- borderColor: 'yellow'
2478
2755
  }
2479
2756
  ];
2480
2757
  }
2481
2758
 
2759
+ if (!ui.showHelp && previewOpen && !overlayPreviewOpen && !splitPreviewLayout) {
2760
+ panelCandidates.push({
2761
+ key: 'preview',
2762
+ title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
2763
+ lines: previewLines,
2764
+ borderColor: 'magenta'
2765
+ });
2766
+ }
2767
+
2482
2768
  if (ultraCompact && !splitPreviewLayout) {
2483
2769
  if (previewOpen) {
2484
- panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === 'current-run' || panel.key === 'preview'));
2770
+ panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === focusedSection || panel.key === 'preview'));
2485
2771
  } else {
2486
- panelCandidates = [panelCandidates[0]];
2772
+ const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
2773
+ panelCandidates = [focusedPanel || panelCandidates[0]];
2487
2774
  }
2488
2775
  }
2489
2776
 
@@ -2502,7 +2789,7 @@ function createDashboardApp(deps) {
2502
2789
  });
2503
2790
 
2504
2791
  let contentNode;
2505
- if (splitPreviewLayout && ui.view === 'runs' && !overlayPreviewOpen) {
2792
+ if (splitPreviewLayout && !overlayPreviewOpen) {
2506
2793
  const previewPanel = {
2507
2794
  key: 'preview-split',
2508
2795
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
@@ -2564,11 +2851,11 @@ function createDashboardApp(deps) {
2564
2851
  { flexDirection: 'column', width: fullWidth },
2565
2852
  React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
2566
2853
  React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
2567
- React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons }),
2854
+ React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons, flow: activeFlow }),
2568
2855
  showErrorInline
2569
2856
  ? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
2570
2857
  : null,
2571
- showErrorPanel
2858
+ showGlobalErrorPanel
2572
2859
  ? React.createElement(SectionPanel, {
2573
2860
  title: 'Errors',
2574
2861
  lines: buildErrorLines(error, Math.max(18, fullWidth - 4)),
@@ -1,15 +1,18 @@
1
1
  function createInitialUIState() {
2
2
  return {
3
3
  view: 'runs',
4
- showHelp: true
4
+ showHelp: false
5
5
  };
6
6
  }
7
7
 
8
8
  function cycleView(current) {
9
9
  if (current === 'runs') {
10
- return 'overview';
10
+ return 'intents';
11
11
  }
12
- if (current === 'overview') {
12
+ if (current === 'intents') {
13
+ return 'completed';
14
+ }
15
+ if (current === 'completed') {
13
16
  return 'health';
14
17
  }
15
18
  return 'runs';
@@ -19,10 +22,13 @@ function cycleViewBackward(current) {
19
22
  if (current === 'runs') {
20
23
  return 'health';
21
24
  }
22
- if (current === 'overview') {
25
+ if (current === 'intents') {
23
26
  return 'runs';
24
27
  }
25
- return 'overview';
28
+ if (current === 'completed') {
29
+ return 'intents';
30
+ }
31
+ return 'completed';
26
32
  }
27
33
 
28
34
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
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": {