specsmd 0.1.39 → 0.1.41

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.
@@ -172,12 +172,19 @@ async function runFlowDashboard(options, flow, availableFlows = []) {
172
172
  }
173
173
 
174
174
  const ink = await import('ink');
175
+ let inkUi = null;
176
+ try {
177
+ inkUi = await import('@inkjs/ui');
178
+ } catch {
179
+ inkUi = null;
180
+ }
175
181
  const reactNamespace = await import('react');
176
182
  const React = reactNamespace.default || reactNamespace;
177
183
 
178
184
  const App = createDashboardApp({
179
185
  React,
180
186
  ink,
187
+ inkUi,
181
188
  parseSnapshotForFlow,
182
189
  workspacePath,
183
190
  flow,
@@ -124,7 +124,8 @@ function normalizePanelLine(line) {
124
124
  text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
125
125
  color: line.color,
126
126
  bold: Boolean(line.bold),
127
- selected: Boolean(line.selected)
127
+ selected: Boolean(line.selected),
128
+ loading: Boolean(line.loading)
128
129
  };
129
130
  }
130
131
 
@@ -132,7 +133,8 @@ function normalizePanelLine(line) {
132
133
  text: String(line ?? ''),
133
134
  color: undefined,
134
135
  bold: false,
135
- selected: false
136
+ selected: false,
137
+ loading: false
136
138
  };
137
139
  }
138
140
 
@@ -879,6 +881,30 @@ function pushFileEntry(entries, seenPaths, candidate) {
879
881
  });
880
882
  }
881
883
 
884
+ function buildIntentScopedLabel(snapshot, intentId, filePath, fallbackName = 'file.md') {
885
+ const safeIntentId = typeof intentId === 'string' && intentId.trim() !== ''
886
+ ? intentId
887
+ : '';
888
+ const safeFallback = typeof fallbackName === 'string' && fallbackName.trim() !== ''
889
+ ? fallbackName
890
+ : 'file.md';
891
+
892
+ if (typeof filePath === 'string' && filePath.trim() !== '') {
893
+ if (safeIntentId && typeof snapshot?.rootPath === 'string' && snapshot.rootPath.trim() !== '') {
894
+ const intentPath = path.join(snapshot.rootPath, 'intents', safeIntentId);
895
+ const relativePath = path.relative(intentPath, filePath);
896
+ if (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
897
+ return `${safeIntentId}/${relativePath.split(path.sep).join('/')}`;
898
+ }
899
+ }
900
+
901
+ const basename = path.basename(filePath);
902
+ return safeIntentId ? `${safeIntentId}/${basename}` : basename;
903
+ }
904
+
905
+ return safeIntentId ? `${safeIntentId}/${safeFallback}` : safeFallback;
906
+ }
907
+
882
908
  function collectFireRunFiles(run) {
883
909
  if (!run || typeof run.folderPath !== 'string') {
884
910
  return [];
@@ -1021,14 +1047,24 @@ function getRunFileEntries(snapshot, flow, options = {}) {
1021
1047
  const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
1022
1048
  for (const pendingItem of pendingItems) {
1023
1049
  pushFileEntry(entries, seenPaths, {
1024
- label: `${pendingItem?.intentId || 'intent'}/${pendingItem?.id || 'work-item'}.md`,
1050
+ label: buildIntentScopedLabel(
1051
+ snapshot,
1052
+ pendingItem?.intentId,
1053
+ pendingItem?.filePath,
1054
+ `${pendingItem?.id || 'work-item'}.md`
1055
+ ),
1025
1056
  path: pendingItem?.filePath,
1026
1057
  scope: 'upcoming'
1027
1058
  });
1028
1059
 
1029
1060
  if (pendingItem?.intentId) {
1030
1061
  pushFileEntry(entries, seenPaths, {
1031
- label: `${pendingItem.intentId}/brief.md`,
1062
+ label: buildIntentScopedLabel(
1063
+ snapshot,
1064
+ pendingItem.intentId,
1065
+ path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
1066
+ 'brief.md'
1067
+ ),
1032
1068
  path: path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
1033
1069
  scope: 'intent'
1034
1070
  });
@@ -1047,7 +1083,12 @@ function getRunFileEntries(snapshot, flow, options = {}) {
1047
1083
  : [];
1048
1084
  for (const intent of completedIntents) {
1049
1085
  pushFileEntry(entries, seenPaths, {
1050
- label: `${intent.id}/brief.md`,
1086
+ label: buildIntentScopedLabel(
1087
+ snapshot,
1088
+ intent?.id,
1089
+ path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
1090
+ 'brief.md'
1091
+ ),
1051
1092
  path: path.join(snapshot?.rootPath || '', 'intents', intent.id, 'brief.md'),
1052
1093
  scope: 'intent'
1053
1094
  });
@@ -1265,6 +1306,15 @@ function toInfoRows(lines, keyPrefix, emptyLabel = 'No data') {
1265
1306
  });
1266
1307
  }
1267
1308
 
1309
+ function toLoadingRows(label, keyPrefix = 'loading') {
1310
+ return [{
1311
+ kind: 'loading',
1312
+ key: `${keyPrefix}:row`,
1313
+ label: typeof label === 'string' && label !== '' ? label : 'Loading...',
1314
+ selectable: false
1315
+ }];
1316
+ }
1317
+
1268
1318
  function buildOverviewIntentGroups(snapshot, flow, filter = 'next') {
1269
1319
  const effectiveFlow = getEffectiveFlow(flow, snapshot);
1270
1320
  const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
@@ -1304,11 +1354,16 @@ function buildOverviewIntentGroups(snapshot, flow, filter = 'next') {
1304
1354
  const workItems = Array.isArray(intent?.workItems) ? intent.workItems : [];
1305
1355
  const done = workItems.filter((item) => item.status === 'completed').length;
1306
1356
  const files = [{
1307
- label: `${intent?.id || 'intent'}/brief.md`,
1357
+ label: buildIntentScopedLabel(snapshot, intent?.id, intent?.filePath, 'brief.md'),
1308
1358
  path: intent?.filePath,
1309
1359
  scope: 'intent'
1310
1360
  }, ...workItems.map((item) => ({
1311
- label: `${intent?.id || 'intent'}/${item?.id || 'work-item'}.md`,
1361
+ label: buildIntentScopedLabel(
1362
+ snapshot,
1363
+ intent?.id,
1364
+ item?.filePath,
1365
+ `${item?.id || 'work-item'}.md`
1366
+ ),
1312
1367
  path: item?.filePath,
1313
1368
  scope: item?.status === 'completed' ? 'completed' : 'upcoming'
1314
1369
  }))];
@@ -1444,14 +1499,24 @@ function buildPendingGroups(snapshot, flow) {
1444
1499
 
1445
1500
  if (item?.filePath) {
1446
1501
  files.push({
1447
- label: `${item?.intentId || 'intent'}/${item?.id || 'work-item'}.md`,
1502
+ label: buildIntentScopedLabel(
1503
+ snapshot,
1504
+ item?.intentId,
1505
+ item?.filePath,
1506
+ `${item?.id || 'work-item'}.md`
1507
+ ),
1448
1508
  path: item.filePath,
1449
1509
  scope: 'upcoming'
1450
1510
  });
1451
1511
  }
1452
1512
  if (item?.intentId) {
1453
1513
  files.push({
1454
- label: `${item.intentId}/brief.md`,
1514
+ label: buildIntentScopedLabel(
1515
+ snapshot,
1516
+ item.intentId,
1517
+ path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
1518
+ 'brief.md'
1519
+ ),
1455
1520
  path: path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
1456
1521
  scope: 'intent'
1457
1522
  });
@@ -1512,7 +1577,12 @@ function buildCompletedGroups(snapshot, flow) {
1512
1577
  key: `completed:intent:${intent?.id || index}`,
1513
1578
  label: `intent ${intent?.id || 'unknown'} [completed]`,
1514
1579
  files: filterExistingFiles([{
1515
- label: `${intent?.id || 'intent'}/brief.md`,
1580
+ label: buildIntentScopedLabel(
1581
+ snapshot,
1582
+ intent?.id,
1583
+ path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
1584
+ 'brief.md'
1585
+ ),
1516
1586
  path: path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
1517
1587
  scope: 'intent'
1518
1588
  }])
@@ -1602,6 +1672,16 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
1602
1672
  };
1603
1673
  }
1604
1674
 
1675
+ if (row.kind === 'loading') {
1676
+ return {
1677
+ text: truncate(row.label || 'Loading...', width),
1678
+ color: 'cyan',
1679
+ bold: false,
1680
+ selected: false,
1681
+ loading: true
1682
+ };
1683
+ }
1684
+
1605
1685
  return {
1606
1686
  text: truncate(`${isSelected ? `${cursor} ` : ' '}${row.label || ''}`, width),
1607
1687
  color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : (row.color || 'gray'),
@@ -1941,6 +2021,7 @@ function createDashboardApp(deps) {
1941
2021
  const {
1942
2022
  React,
1943
2023
  ink,
2024
+ inkUi,
1944
2025
  parseSnapshot,
1945
2026
  parseSnapshotForFlow,
1946
2027
  workspacePath,
@@ -1956,6 +2037,9 @@ function createDashboardApp(deps) {
1956
2037
 
1957
2038
  const { Box, Text, useApp, useInput, useStdout } = ink;
1958
2039
  const { useState, useEffect, useCallback, useRef } = React;
2040
+ const Spinner = inkUi && typeof inkUi.Spinner === 'function'
2041
+ ? inkUi.Spinner
2042
+ : null;
1959
2043
 
1960
2044
  function SectionPanel(props) {
1961
2045
  const {
@@ -1990,15 +2074,25 @@ function createDashboardApp(deps) {
1990
2074
  { bold: true, color: titleColor, backgroundColor: titleBackground },
1991
2075
  truncate(title, contentWidth)
1992
2076
  ),
1993
- ...visibleLines.map((line, index) => React.createElement(
1994
- Text,
1995
- {
1996
- key: `${title}-${index}`,
1997
- color: line.color,
1998
- bold: line.bold
1999
- },
2000
- line.text
2001
- ))
2077
+ ...visibleLines.map((line, index) => {
2078
+ if (line.loading && Spinner) {
2079
+ return React.createElement(
2080
+ Box,
2081
+ { key: `${title}-${index}` },
2082
+ React.createElement(Spinner, { label: truncate(line.text, contentWidth) })
2083
+ );
2084
+ }
2085
+
2086
+ return React.createElement(
2087
+ Text,
2088
+ {
2089
+ key: `${title}-${index}`,
2090
+ color: line.color,
2091
+ bold: line.bold
2092
+ },
2093
+ line.text
2094
+ );
2095
+ })
2002
2096
  );
2003
2097
  }
2004
2098
 
@@ -2178,42 +2272,42 @@ function createDashboardApp(deps) {
2178
2272
  expandedGroups
2179
2273
  )
2180
2274
  ]
2181
- : toInfoRows(['Loading intents...'], 'intent-loading');
2275
+ : toLoadingRows('Loading intents...', 'intent-loading');
2182
2276
  const completedRows = shouldHydrateSecondaryTabs
2183
2277
  ? toExpandableRows(
2184
2278
  buildCompletedGroups(snapshot, activeFlow),
2185
2279
  getNoCompletedMessage(effectiveFlow),
2186
2280
  expandedGroups
2187
2281
  )
2188
- : toInfoRows(['Loading completed items...'], 'completed-loading');
2282
+ : toLoadingRows('Loading completed items...', 'completed-loading');
2189
2283
  const standardsRows = shouldHydrateSecondaryTabs
2190
2284
  ? toExpandableRows(
2191
2285
  buildStandardsGroups(snapshot, activeFlow),
2192
2286
  effectiveFlow === 'simple' ? 'No standards for SIMPLE flow' : 'No standards found',
2193
2287
  expandedGroups
2194
2288
  )
2195
- : toInfoRows(['Loading standards...'], 'standards-loading');
2289
+ : toLoadingRows('Loading standards...', 'standards-loading');
2196
2290
  const statsRows = shouldHydrateSecondaryTabs
2197
2291
  ? toInfoRows(
2198
2292
  buildStatsLines(snapshot, 200, activeFlow),
2199
2293
  'stats',
2200
2294
  'No stats available'
2201
2295
  )
2202
- : toInfoRows(['Loading stats...'], 'stats-loading');
2296
+ : toLoadingRows('Loading stats...', 'stats-loading');
2203
2297
  const warningsRows = shouldHydrateSecondaryTabs
2204
2298
  ? toInfoRows(
2205
2299
  buildWarningsLines(snapshot, 200),
2206
2300
  'warnings',
2207
2301
  'No warnings'
2208
2302
  )
2209
- : toInfoRows(['Loading warnings...'], 'warnings-loading');
2303
+ : toLoadingRows('Loading warnings...', 'warnings-loading');
2210
2304
  const errorDetailsRows = shouldHydrateSecondaryTabs
2211
2305
  ? toInfoRows(
2212
2306
  buildErrorLines(error, 200),
2213
2307
  'error-details',
2214
2308
  'No error details'
2215
2309
  )
2216
- : toInfoRows(['Loading error details...'], 'error-loading');
2310
+ : toLoadingRows('Loading error details...', 'error-loading');
2217
2311
 
2218
2312
  const rowsBySection = {
2219
2313
  'current-run': currentRunRows,
@@ -2771,7 +2865,8 @@ function createDashboardApp(deps) {
2771
2865
  (showGlobalErrorPanel ? 5 : 0) +
2772
2866
  (showErrorInline ? 1 : 0) +
2773
2867
  (showStatusLine ? 1 : 0);
2774
- const contentRowsBudget = Math.max(4, rows - reservedRows);
2868
+ const frameSafetyRows = 2;
2869
+ const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
2775
2870
  const ultraCompact = rows <= 14;
2776
2871
  const panelTitles = getPanelTitles(activeFlow, snapshot);
2777
2872
  const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && cols >= 110 && rows >= 16;
@@ -2936,12 +3031,13 @@ function createDashboardApp(deps) {
2936
3031
 
2937
3032
  let contentNode;
2938
3033
  if (splitPreviewLayout && !overlayPreviewOpen) {
3034
+ const previewBodyLines = Math.max(1, contentRowsBudget - 3);
2939
3035
  const previewPanel = {
2940
3036
  key: 'preview-split',
2941
3037
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
2942
3038
  lines: previewLines,
2943
3039
  borderColor: 'magenta',
2944
- maxLines: Math.max(4, contentRowsBudget)
3040
+ maxLines: previewBodyLines
2945
3041
  };
2946
3042
 
2947
3043
  contentNode = React.createElement(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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": {
@@ -40,6 +40,7 @@
40
40
  "README.md"
41
41
  ],
42
42
  "dependencies": {
43
+ "@inkjs/ui": "^2.0.0",
43
44
  "chalk": "^4.1.2",
44
45
  "chokidar": "^4.0.3",
45
46
  "commander": "^11.1.0",