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.
- package/lib/dashboard/index.js +7 -0
- package/lib/dashboard/tui/app.js +123 -27
- package/package.json +2 -1
package/lib/dashboard/index.js
CHANGED
|
@@ -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,
|
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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) =>
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
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:
|
|
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.
|
|
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",
|