specsmd 0.1.36 → 0.1.38

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.
@@ -802,7 +802,7 @@ function getSectionOrderForView(view) {
802
802
  if (view === 'health') {
803
803
  return ['standards', 'stats', 'warnings', 'error-details'];
804
804
  }
805
- return ['current-run'];
805
+ return ['current-run', 'run-files'];
806
806
  }
807
807
 
808
808
  function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
@@ -906,7 +906,8 @@ function collectSimpleSpecFiles(spec) {
906
906
  }));
907
907
  }
908
908
 
909
- function getRunFileEntries(snapshot, flow) {
909
+ function getRunFileEntries(snapshot, flow, options = {}) {
910
+ const includeBacklog = options.includeBacklog !== false;
910
911
  const effectiveFlow = getEffectiveFlow(flow, snapshot);
911
912
  const entries = [];
912
913
  const seenPaths = new Set();
@@ -917,6 +918,10 @@ function getRunFileEntries(snapshot, flow) {
917
918
  pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
918
919
  }
919
920
 
921
+ if (!includeBacklog) {
922
+ return entries;
923
+ }
924
+
920
925
  const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
921
926
  for (const pendingBolt of pendingBolts) {
922
927
  for (const file of collectAidlcBoltFiles(pendingBolt)) {
@@ -963,6 +968,10 @@ function getRunFileEntries(snapshot, flow) {
963
968
  pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
964
969
  }
965
970
 
971
+ if (!includeBacklog) {
972
+ return entries;
973
+ }
974
+
966
975
  const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
967
976
  for (const pendingSpec of pendingSpecs) {
968
977
  for (const file of collectSimpleSpecFiles(pendingSpec)) {
@@ -985,6 +994,10 @@ function getRunFileEntries(snapshot, flow) {
985
994
  pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
986
995
  }
987
996
 
997
+ if (!includeBacklog) {
998
+ return entries;
999
+ }
1000
+
988
1001
  const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
989
1002
  for (const pendingItem of pendingItems) {
990
1003
  pushFileEntry(entries, seenPaths, {
@@ -1132,6 +1145,73 @@ function buildRunFileGroups(fileEntries) {
1132
1145
  return groups;
1133
1146
  }
1134
1147
 
1148
+ function getFileEntityLabel(fileEntry, fallbackIndex = 0) {
1149
+ const rawLabel = typeof fileEntry?.label === 'string' ? fileEntry.label : '';
1150
+ if (rawLabel.includes('/')) {
1151
+ return rawLabel.split('/')[0] || `item-${fallbackIndex + 1}`;
1152
+ }
1153
+
1154
+ const filePath = typeof fileEntry?.path === 'string' ? fileEntry.path : '';
1155
+ if (filePath !== '') {
1156
+ const parentDir = path.basename(path.dirname(filePath));
1157
+ if (parentDir && parentDir !== '.' && parentDir !== path.sep) {
1158
+ return parentDir;
1159
+ }
1160
+
1161
+ const baseName = path.basename(filePath);
1162
+ if (baseName) {
1163
+ return baseName;
1164
+ }
1165
+ }
1166
+
1167
+ return `item-${fallbackIndex + 1}`;
1168
+ }
1169
+
1170
+ function buildRunFileEntityGroups(snapshot, flow, options = {}) {
1171
+ const order = ['active', 'upcoming', 'completed', 'intent', 'other'];
1172
+ const rankByScope = new Map(order.map((scope, index) => [scope, index]));
1173
+ const entries = filterExistingFiles(getRunFileEntries(snapshot, flow, options));
1174
+ const groupsByEntity = new Map();
1175
+
1176
+ for (let index = 0; index < entries.length; index += 1) {
1177
+ const fileEntry = entries[index];
1178
+ const entity = getFileEntityLabel(fileEntry, index);
1179
+ const scope = order.includes(fileEntry?.scope) ? fileEntry.scope : 'other';
1180
+ const scopeRank = rankByScope.get(scope) ?? rankByScope.get('other');
1181
+
1182
+ if (!groupsByEntity.has(entity)) {
1183
+ groupsByEntity.set(entity, {
1184
+ entity,
1185
+ files: [],
1186
+ scope,
1187
+ scopeRank
1188
+ });
1189
+ }
1190
+
1191
+ const group = groupsByEntity.get(entity);
1192
+ group.files.push(fileEntry);
1193
+
1194
+ if (scopeRank < group.scopeRank) {
1195
+ group.scopeRank = scopeRank;
1196
+ group.scope = scope;
1197
+ }
1198
+ }
1199
+
1200
+ return Array.from(groupsByEntity.values())
1201
+ .sort((a, b) => {
1202
+ if (a.scopeRank !== b.scopeRank) {
1203
+ return a.scopeRank - b.scopeRank;
1204
+ }
1205
+ return String(a.entity).localeCompare(String(b.entity));
1206
+ })
1207
+ .map((group) => ({
1208
+ key: `run-files:entity:${group.entity}`,
1209
+ label: `${group.entity} [${formatScope(group.scope)}] (${group.files.length})`,
1210
+ files: filterExistingFiles(group.files)
1211
+ }))
1212
+ .filter((group) => group.files.length > 0);
1213
+ }
1214
+
1135
1215
  function normalizeInfoLine(line) {
1136
1216
  const normalized = normalizePanelLine(line);
1137
1217
  return {
@@ -1611,11 +1691,14 @@ function buildQuickHelpText(view, options = {}) {
1611
1691
 
1612
1692
  if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health') {
1613
1693
  if (previewOpen) {
1614
- parts.push('tab pane', '↑/↓ nav/scroll', 'v close');
1694
+ parts.push('tab pane', '↑/↓ nav/scroll', 'v/space close');
1615
1695
  } else {
1616
- parts.push('↑/↓ navigate', 'enter expand', 'v preview');
1696
+ parts.push('↑/↓ navigate', 'enter expand', 'v/space preview');
1617
1697
  }
1618
1698
  }
1699
+ if (view === 'runs') {
1700
+ parts.push('a current', 'f files');
1701
+ }
1619
1702
  parts.push(`tab1 ${activeLabel}`);
1620
1703
 
1621
1704
  if (availableFlowCount > 1) {
@@ -1651,9 +1734,10 @@ function buildHelpOverlayLines(options = {}) {
1651
1734
  { text: '', color: undefined, bold: false },
1652
1735
  { text: 'Tab 1 Active', color: 'yellow', bold: true },
1653
1736
  `a focus active ${itemLabel}`,
1737
+ `f focus ${itemLabel} files`,
1654
1738
  'up/down or j/k move selection',
1655
1739
  'enter expand/collapse selected folder row',
1656
- 'v preview selected file',
1740
+ 'v or space preview selected file',
1657
1741
  'v twice quickly opens fullscreen preview overlay',
1658
1742
  'tab switch focus between main and preview panes',
1659
1743
  'o open selected file in system default app'
@@ -1982,6 +2066,7 @@ function createDashboardApp(deps) {
1982
2066
  });
1983
2067
  const [selectionBySection, setSelectionBySection] = useState({
1984
2068
  'current-run': 0,
2069
+ 'run-files': 0,
1985
2070
  'intent-status': 0,
1986
2071
  'completed-runs': 0,
1987
2072
  standards: 0,
@@ -2043,6 +2128,20 @@ function createDashboardApp(deps) {
2043
2128
  currentExpandedGroups
2044
2129
  );
2045
2130
  const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
2131
+ const runFileGroups = buildRunFileEntityGroups(snapshot, activeFlow, {
2132
+ includeBacklog: shouldHydrateSecondaryTabs
2133
+ });
2134
+ const runFileExpandedGroups = { ...expandedGroups };
2135
+ for (const group of runFileGroups) {
2136
+ if (runFileExpandedGroups[group.key] == null) {
2137
+ runFileExpandedGroups[group.key] = true;
2138
+ }
2139
+ }
2140
+ const runFileRows = toExpandableRows(
2141
+ runFileGroups,
2142
+ getNoFileMessage(effectiveFlow),
2143
+ runFileExpandedGroups
2144
+ );
2046
2145
  const intentRows = shouldHydrateSecondaryTabs
2047
2146
  ? [
2048
2147
  {
@@ -2098,6 +2197,7 @@ function createDashboardApp(deps) {
2098
2197
 
2099
2198
  const rowsBySection = {
2100
2199
  'current-run': currentRunRows,
2200
+ 'run-files': runFileRows,
2101
2201
  'intent-status': intentRows,
2102
2202
  'completed-runs': completedRows,
2103
2203
  standards: standardsRows,
@@ -2231,6 +2331,7 @@ function createDashboardApp(deps) {
2231
2331
  });
2232
2332
  setSelectionBySection({
2233
2333
  'current-run': 0,
2334
+ 'run-files': 0,
2234
2335
  'intent-status': 0,
2235
2336
  'completed-runs': 0,
2236
2337
  standards: 0,
@@ -2268,6 +2369,7 @@ function createDashboardApp(deps) {
2268
2369
  });
2269
2370
  setSelectionBySection({
2270
2371
  'current-run': 0,
2372
+ 'run-files': 0,
2271
2373
  'intent-status': 0,
2272
2374
  'completed-runs': 0,
2273
2375
  standards: 0,
@@ -2340,6 +2442,11 @@ function createDashboardApp(deps) {
2340
2442
  setPaneFocus('main');
2341
2443
  return;
2342
2444
  }
2445
+ if (input === 'f') {
2446
+ setSectionFocus((previous) => ({ ...previous, runs: 'run-files' }));
2447
+ setPaneFocus('main');
2448
+ return;
2449
+ }
2343
2450
  } else if (ui.view === 'intents') {
2344
2451
  if (input === 'i') {
2345
2452
  setSectionFocus((previous) => ({ ...previous, intents: 'intent-status' }));
@@ -2426,7 +2533,7 @@ function createDashboardApp(deps) {
2426
2533
  return;
2427
2534
  }
2428
2535
 
2429
- if (input === 'v') {
2536
+ if (input === 'v' || input === ' ' || key.space) {
2430
2537
  const target = selectedFocusedFile || previewTarget;
2431
2538
  if (!target) {
2432
2539
  setStatusLine('Select a file row first.');
@@ -2500,14 +2607,19 @@ function createDashboardApp(deps) {
2500
2607
  }, [activeFlow, rowLengthSignature, snapshot?.generatedAt]);
2501
2608
 
2502
2609
  useEffect(() => {
2610
+ if (ui.view !== 'runs') {
2611
+ setDeferredTabsReady(true);
2612
+ return undefined;
2613
+ }
2614
+
2503
2615
  setDeferredTabsReady(false);
2504
2616
  const timer = setTimeout(() => {
2505
2617
  setDeferredTabsReady(true);
2506
- }, 0);
2618
+ }, 250);
2507
2619
  return () => {
2508
2620
  clearTimeout(timer);
2509
2621
  };
2510
- }, [activeFlow, snapshot?.generatedAt]);
2622
+ }, [activeFlow, snapshot?.generatedAt, ui.view]);
2511
2623
 
2512
2624
  useEffect(() => {
2513
2625
  setPaneFocus('main');
@@ -2624,13 +2736,21 @@ function createDashboardApp(deps) {
2624
2736
  const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
2625
2737
 
2626
2738
  const fullWidth = Math.max(40, cols - 1);
2739
+ const showFlowBar = availableFlowIds.length > 1;
2627
2740
  const showFooterHelpLine = rows >= 10;
2628
2741
  const showErrorPanel = Boolean(error) && rows >= 18;
2629
2742
  const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp;
2630
2743
  const showErrorInline = Boolean(error) && !showErrorPanel;
2744
+ const showStatusLine = statusLine !== '';
2631
2745
  const densePanels = rows <= 28 || cols <= 120;
2632
2746
 
2633
- const reservedRows = 2 + (showFooterHelpLine ? 1 : 0) + (showGlobalErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
2747
+ const reservedRows =
2748
+ 2 +
2749
+ (showFlowBar ? 1 : 0) +
2750
+ (showFooterHelpLine ? 1 : 0) +
2751
+ (showGlobalErrorPanel ? 5 : 0) +
2752
+ (showErrorInline ? 1 : 0) +
2753
+ (showStatusLine ? 1 : 0);
2634
2754
  const contentRowsBudget = Math.max(4, rows - reservedRows);
2635
2755
  const ultraCompact = rows <= 14;
2636
2756
  const panelTitles = getPanelTitles(activeFlow, snapshot);
@@ -2752,6 +2872,12 @@ function createDashboardApp(deps) {
2752
2872
  title: panelTitles.current,
2753
2873
  lines: sectionLines['current-run'],
2754
2874
  borderColor: 'green'
2875
+ },
2876
+ {
2877
+ key: 'run-files',
2878
+ title: panelTitles.files,
2879
+ lines: sectionLines['run-files'],
2880
+ borderColor: 'yellow'
2755
2881
  }
2756
2882
  ];
2757
2883
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
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": {