specsmd 0.1.38 → 0.1.40

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,
@@ -1,9 +1,18 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
+ const stringWidthModule = require('string-width');
5
+ const sliceAnsiModule = require('slice-ansi');
4
6
  const { createWatchRuntime } = require('../runtime/watch-runtime');
5
7
  const { createInitialUIState } = require('./store');
6
8
 
9
+ const stringWidth = typeof stringWidthModule === 'function'
10
+ ? stringWidthModule
11
+ : stringWidthModule.default;
12
+ const sliceAnsi = typeof sliceAnsiModule === 'function'
13
+ ? sliceAnsiModule
14
+ : sliceAnsiModule.default;
15
+
7
16
  function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
8
17
  if (!error) {
9
18
  return {
@@ -87,15 +96,26 @@ function resolveIconSet() {
87
96
 
88
97
  function truncate(value, width) {
89
98
  const text = String(value ?? '');
90
- if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
99
+ if (!Number.isFinite(width)) {
91
100
  return text;
92
101
  }
93
102
 
94
- if (width <= 3) {
95
- return text.slice(0, width);
103
+ const safeWidth = Math.max(0, Math.floor(width));
104
+ if (safeWidth === 0) {
105
+ return '';
106
+ }
107
+
108
+ if (stringWidth(text) <= safeWidth) {
109
+ return text;
96
110
  }
97
111
 
98
- return `${text.slice(0, width - 3)}...`;
112
+ if (safeWidth <= 3) {
113
+ return sliceAnsi(text, 0, safeWidth);
114
+ }
115
+
116
+ const ellipsis = '...';
117
+ const bodyWidth = Math.max(0, safeWidth - stringWidth(ellipsis));
118
+ return `${sliceAnsi(text, 0, bodyWidth)}${ellipsis}`;
99
119
  }
100
120
 
101
121
  function normalizePanelLine(line) {
@@ -104,7 +124,8 @@ function normalizePanelLine(line) {
104
124
  text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
105
125
  color: line.color,
106
126
  bold: Boolean(line.bold),
107
- selected: Boolean(line.selected)
127
+ selected: Boolean(line.selected),
128
+ loading: Boolean(line.loading)
108
129
  };
109
130
  }
110
131
 
@@ -112,7 +133,8 @@ function normalizePanelLine(line) {
112
133
  text: String(line ?? ''),
113
134
  color: undefined,
114
135
  bold: false,
115
- selected: false
136
+ selected: false,
137
+ loading: false
116
138
  };
117
139
  }
118
140
 
@@ -1245,6 +1267,15 @@ function toInfoRows(lines, keyPrefix, emptyLabel = 'No data') {
1245
1267
  });
1246
1268
  }
1247
1269
 
1270
+ function toLoadingRows(label, keyPrefix = 'loading') {
1271
+ return [{
1272
+ kind: 'loading',
1273
+ key: `${keyPrefix}:row`,
1274
+ label: typeof label === 'string' && label !== '' ? label : 'Loading...',
1275
+ selectable: false
1276
+ }];
1277
+ }
1278
+
1248
1279
  function buildOverviewIntentGroups(snapshot, flow, filter = 'next') {
1249
1280
  const effectiveFlow = getEffectiveFlow(flow, snapshot);
1250
1281
  const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
@@ -1582,6 +1613,16 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
1582
1613
  };
1583
1614
  }
1584
1615
 
1616
+ if (row.kind === 'loading') {
1617
+ return {
1618
+ text: truncate(row.label || 'Loading...', width),
1619
+ color: 'cyan',
1620
+ bold: false,
1621
+ selected: false,
1622
+ loading: true
1623
+ };
1624
+ }
1625
+
1585
1626
  return {
1586
1627
  text: truncate(`${isSelected ? `${cursor} ` : ' '}${row.label || ''}`, width),
1587
1628
  color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : (row.color || 'gray'),
@@ -1921,6 +1962,7 @@ function createDashboardApp(deps) {
1921
1962
  const {
1922
1963
  React,
1923
1964
  ink,
1965
+ inkUi,
1924
1966
  parseSnapshot,
1925
1967
  parseSnapshotForFlow,
1926
1968
  workspacePath,
@@ -1936,6 +1978,9 @@ function createDashboardApp(deps) {
1936
1978
 
1937
1979
  const { Box, Text, useApp, useInput, useStdout } = ink;
1938
1980
  const { useState, useEffect, useCallback, useRef } = React;
1981
+ const Spinner = inkUi && typeof inkUi.Spinner === 'function'
1982
+ ? inkUi.Spinner
1983
+ : null;
1939
1984
 
1940
1985
  function SectionPanel(props) {
1941
1986
  const {
@@ -1970,15 +2015,25 @@ function createDashboardApp(deps) {
1970
2015
  { bold: true, color: titleColor, backgroundColor: titleBackground },
1971
2016
  truncate(title, contentWidth)
1972
2017
  ),
1973
- ...visibleLines.map((line, index) => React.createElement(
1974
- Text,
1975
- {
1976
- key: `${title}-${index}`,
1977
- color: line.color,
1978
- bold: line.bold
1979
- },
1980
- line.text
1981
- ))
2018
+ ...visibleLines.map((line, index) => {
2019
+ if (line.loading && Spinner) {
2020
+ return React.createElement(
2021
+ Box,
2022
+ { key: `${title}-${index}` },
2023
+ React.createElement(Spinner, { label: truncate(line.text, contentWidth) })
2024
+ );
2025
+ }
2026
+
2027
+ return React.createElement(
2028
+ Text,
2029
+ {
2030
+ key: `${title}-${index}`,
2031
+ color: line.color,
2032
+ bold: line.bold
2033
+ },
2034
+ line.text
2035
+ );
2036
+ })
1982
2037
  );
1983
2038
  }
1984
2039
 
@@ -2158,42 +2213,42 @@ function createDashboardApp(deps) {
2158
2213
  expandedGroups
2159
2214
  )
2160
2215
  ]
2161
- : toInfoRows(['Loading intents...'], 'intent-loading');
2216
+ : toLoadingRows('Loading intents...', 'intent-loading');
2162
2217
  const completedRows = shouldHydrateSecondaryTabs
2163
2218
  ? toExpandableRows(
2164
2219
  buildCompletedGroups(snapshot, activeFlow),
2165
2220
  getNoCompletedMessage(effectiveFlow),
2166
2221
  expandedGroups
2167
2222
  )
2168
- : toInfoRows(['Loading completed items...'], 'completed-loading');
2223
+ : toLoadingRows('Loading completed items...', 'completed-loading');
2169
2224
  const standardsRows = shouldHydrateSecondaryTabs
2170
2225
  ? toExpandableRows(
2171
2226
  buildStandardsGroups(snapshot, activeFlow),
2172
2227
  effectiveFlow === 'simple' ? 'No standards for SIMPLE flow' : 'No standards found',
2173
2228
  expandedGroups
2174
2229
  )
2175
- : toInfoRows(['Loading standards...'], 'standards-loading');
2230
+ : toLoadingRows('Loading standards...', 'standards-loading');
2176
2231
  const statsRows = shouldHydrateSecondaryTabs
2177
2232
  ? toInfoRows(
2178
2233
  buildStatsLines(snapshot, 200, activeFlow),
2179
2234
  'stats',
2180
2235
  'No stats available'
2181
2236
  )
2182
- : toInfoRows(['Loading stats...'], 'stats-loading');
2237
+ : toLoadingRows('Loading stats...', 'stats-loading');
2183
2238
  const warningsRows = shouldHydrateSecondaryTabs
2184
2239
  ? toInfoRows(
2185
2240
  buildWarningsLines(snapshot, 200),
2186
2241
  'warnings',
2187
2242
  'No warnings'
2188
2243
  )
2189
- : toInfoRows(['Loading warnings...'], 'warnings-loading');
2244
+ : toLoadingRows('Loading warnings...', 'warnings-loading');
2190
2245
  const errorDetailsRows = shouldHydrateSecondaryTabs
2191
2246
  ? toInfoRows(
2192
2247
  buildErrorLines(error, 200),
2193
2248
  'error-details',
2194
2249
  'No error details'
2195
2250
  )
2196
- : toInfoRows(['Loading error details...'], 'error-loading');
2251
+ : toLoadingRows('Loading error details...', 'error-loading');
2197
2252
 
2198
2253
  const rowsBySection = {
2199
2254
  'current-run': currentRunRows,
@@ -2751,7 +2806,8 @@ function createDashboardApp(deps) {
2751
2806
  (showGlobalErrorPanel ? 5 : 0) +
2752
2807
  (showErrorInline ? 1 : 0) +
2753
2808
  (showStatusLine ? 1 : 0);
2754
- const contentRowsBudget = Math.max(4, rows - reservedRows);
2809
+ const frameSafetyRows = 2;
2810
+ const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
2755
2811
  const ultraCompact = rows <= 14;
2756
2812
  const panelTitles = getPanelTitles(activeFlow, snapshot);
2757
2813
  const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && cols >= 110 && rows >= 16;
@@ -2916,12 +2972,13 @@ function createDashboardApp(deps) {
2916
2972
 
2917
2973
  let contentNode;
2918
2974
  if (splitPreviewLayout && !overlayPreviewOpen) {
2975
+ const previewBodyLines = Math.max(1, contentRowsBudget - 3);
2919
2976
  const previewPanel = {
2920
2977
  key: 'preview-split',
2921
2978
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
2922
2979
  lines: previewLines,
2923
2980
  borderColor: 'magenta',
2924
- maxLines: Math.max(4, contentRowsBudget)
2981
+ maxLines: previewBodyLines
2925
2982
  };
2926
2983
 
2927
2984
  contentNode = React.createElement(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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",