specsmd 0.1.32 → 0.1.33
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/tui/app.js +860 -112
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
3
4
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
4
|
-
const { createInitialUIState
|
|
5
|
+
const { createInitialUIState } = require('./store');
|
|
5
6
|
|
|
6
7
|
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
7
8
|
if (!error) {
|
|
@@ -54,14 +55,20 @@ function resolveIconSet() {
|
|
|
54
55
|
runs: '[R]',
|
|
55
56
|
overview: '[O]',
|
|
56
57
|
health: '[H]',
|
|
57
|
-
runFile: '*'
|
|
58
|
+
runFile: '*',
|
|
59
|
+
activeFile: '>',
|
|
60
|
+
groupCollapsed: '>',
|
|
61
|
+
groupExpanded: 'v'
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
const nerd = {
|
|
61
65
|
runs: '',
|
|
62
66
|
overview: '',
|
|
63
67
|
health: '',
|
|
64
|
-
runFile: ''
|
|
68
|
+
runFile: '',
|
|
69
|
+
activeFile: '',
|
|
70
|
+
groupCollapsed: '',
|
|
71
|
+
groupExpanded: ''
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
if (mode === 'ascii') {
|
|
@@ -730,6 +737,30 @@ function getPanelTitles(flow, snapshot) {
|
|
|
730
737
|
};
|
|
731
738
|
}
|
|
732
739
|
|
|
740
|
+
function getSectionOrderForView(view) {
|
|
741
|
+
if (view === 'overview') {
|
|
742
|
+
return ['project', 'intent-status', 'standards'];
|
|
743
|
+
}
|
|
744
|
+
if (view === 'health') {
|
|
745
|
+
return ['stats', 'warnings', 'error-details'];
|
|
746
|
+
}
|
|
747
|
+
return ['current-run', 'run-files', 'pending', 'completed'];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
|
|
751
|
+
const order = Array.isArray(availableSections) && availableSections.length > 0
|
|
752
|
+
? availableSections
|
|
753
|
+
: getSectionOrderForView(view);
|
|
754
|
+
if (order.length === 0) {
|
|
755
|
+
return currentSectionKey;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const currentIndex = order.indexOf(currentSectionKey);
|
|
759
|
+
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
760
|
+
const nextIndex = (safeIndex + direction + order.length) % order.length;
|
|
761
|
+
return order[nextIndex];
|
|
762
|
+
}
|
|
763
|
+
|
|
733
764
|
function fileExists(filePath) {
|
|
734
765
|
try {
|
|
735
766
|
return fs.statSync(filePath).isFile();
|
|
@@ -956,25 +987,361 @@ function formatScope(scope) {
|
|
|
956
987
|
return 'FILE';
|
|
957
988
|
}
|
|
958
989
|
|
|
959
|
-
function
|
|
990
|
+
function getNoPendingMessage(flow) {
|
|
991
|
+
if (flow === 'aidlc') return 'No queued bolts';
|
|
992
|
+
if (flow === 'simple') return 'No pending specs';
|
|
993
|
+
return 'No pending work items';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function getNoCompletedMessage(flow) {
|
|
997
|
+
if (flow === 'aidlc') return 'No completed bolts yet';
|
|
998
|
+
if (flow === 'simple') return 'No completed specs yet';
|
|
999
|
+
return 'No completed runs yet';
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function toRunFileRows(fileEntries, flow) {
|
|
960
1003
|
if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
|
|
961
|
-
return [
|
|
1004
|
+
return [{
|
|
1005
|
+
kind: 'info',
|
|
1006
|
+
key: 'run-files:empty',
|
|
1007
|
+
label: getNoFileMessage(flow),
|
|
1008
|
+
selectable: false
|
|
1009
|
+
}];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return fileEntries.map((file, index) => ({
|
|
1013
|
+
kind: 'file',
|
|
1014
|
+
key: `run-files:${file.path}:${index}`,
|
|
1015
|
+
label: file.label,
|
|
1016
|
+
path: file.path,
|
|
1017
|
+
scope: file.scope || 'file',
|
|
1018
|
+
selectable: true
|
|
1019
|
+
}));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function collectAidlcIntentContextFiles(snapshot, intentId) {
|
|
1023
|
+
if (!snapshot || typeof intentId !== 'string' || intentId.trim() === '') {
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const intentPath = path.join(snapshot.rootPath || '', 'intents', intentId);
|
|
1028
|
+
return [
|
|
1029
|
+
{
|
|
1030
|
+
label: `${intentId}/requirements.md`,
|
|
1031
|
+
path: path.join(intentPath, 'requirements.md'),
|
|
1032
|
+
scope: 'intent'
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
label: `${intentId}/system-context.md`,
|
|
1036
|
+
path: path.join(intentPath, 'system-context.md'),
|
|
1037
|
+
scope: 'intent'
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
label: `${intentId}/units.md`,
|
|
1041
|
+
path: path.join(intentPath, 'units.md'),
|
|
1042
|
+
scope: 'intent'
|
|
1043
|
+
}
|
|
1044
|
+
];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function filterExistingFiles(files) {
|
|
1048
|
+
return (Array.isArray(files) ? files : []).filter((file) =>
|
|
1049
|
+
file && typeof file.path === 'string' && typeof file.label === 'string' && fileExists(file.path)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function buildPendingGroups(snapshot, flow) {
|
|
1054
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1055
|
+
|
|
1056
|
+
if (effectiveFlow === 'aidlc') {
|
|
1057
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
1058
|
+
return pendingBolts.map((bolt, index) => {
|
|
1059
|
+
const deps = Array.isArray(bolt?.blockedBy) && bolt.blockedBy.length > 0
|
|
1060
|
+
? ` blocked_by:${bolt.blockedBy.join(',')}`
|
|
1061
|
+
: '';
|
|
1062
|
+
const location = `${bolt?.intent || 'unknown'}/${bolt?.unit || 'unknown'}`;
|
|
1063
|
+
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
1064
|
+
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
1065
|
+
return {
|
|
1066
|
+
key: `pending:bolt:${bolt?.id || index}`,
|
|
1067
|
+
label: `${bolt?.id || 'unknown'} (${bolt?.status || 'pending'}) in ${location}${deps}`,
|
|
1068
|
+
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
962
1071
|
}
|
|
963
1072
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1073
|
+
if (effectiveFlow === 'simple') {
|
|
1074
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
1075
|
+
return pendingSpecs.map((spec, index) => ({
|
|
1076
|
+
key: `pending:spec:${spec?.name || index}`,
|
|
1077
|
+
label: `${spec?.name || 'unknown'} (${spec?.state || 'pending'}) ${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks`,
|
|
1078
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
1079
|
+
}));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
1083
|
+
return pendingItems.map((item, index) => {
|
|
1084
|
+
const deps = Array.isArray(item?.dependencies) && item.dependencies.length > 0
|
|
1085
|
+
? ` deps:${item.dependencies.join(',')}`
|
|
1086
|
+
: '';
|
|
1087
|
+
const intentTitle = item?.intentTitle || item?.intentId || 'unknown-intent';
|
|
1088
|
+
const files = [];
|
|
1089
|
+
|
|
1090
|
+
if (item?.filePath) {
|
|
1091
|
+
files.push({
|
|
1092
|
+
label: `${item?.intentId || 'intent'}/${item?.id || 'work-item'}.md`,
|
|
1093
|
+
path: item.filePath,
|
|
1094
|
+
scope: 'upcoming'
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (item?.intentId) {
|
|
1098
|
+
files.push({
|
|
1099
|
+
label: `${item.intentId}/brief.md`,
|
|
1100
|
+
path: path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
|
|
1101
|
+
scope: 'intent'
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
969
1105
|
return {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
selected: isSelected
|
|
1106
|
+
key: `pending:item:${item?.intentId || 'intent'}:${item?.id || index}`,
|
|
1107
|
+
label: `${item?.id || 'work-item'} (${item?.mode || 'confirm'}/${item?.complexity || 'medium'}) in ${intentTitle}${deps}`,
|
|
1108
|
+
files: filterExistingFiles(files)
|
|
974
1109
|
};
|
|
975
1110
|
});
|
|
976
1111
|
}
|
|
977
1112
|
|
|
1113
|
+
function buildCompletedGroups(snapshot, flow) {
|
|
1114
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1115
|
+
|
|
1116
|
+
if (effectiveFlow === 'aidlc') {
|
|
1117
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
1118
|
+
return completedBolts.map((bolt, index) => {
|
|
1119
|
+
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
1120
|
+
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
1121
|
+
return {
|
|
1122
|
+
key: `completed:bolt:${bolt?.id || index}`,
|
|
1123
|
+
label: `${bolt?.id || 'unknown'} [${bolt?.type || 'bolt'}] done at ${bolt?.completedAt || 'unknown'}`,
|
|
1124
|
+
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (effectiveFlow === 'simple') {
|
|
1130
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
1131
|
+
return completedSpecs.map((spec, index) => ({
|
|
1132
|
+
key: `completed:spec:${spec?.name || index}`,
|
|
1133
|
+
label: `${spec?.name || 'unknown'} done at ${spec?.updatedAt || 'unknown'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0})`,
|
|
1134
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
1135
|
+
}));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const groups = [];
|
|
1139
|
+
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
1140
|
+
for (let index = 0; index < completedRuns.length; index += 1) {
|
|
1141
|
+
const run = completedRuns[index];
|
|
1142
|
+
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
1143
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
1144
|
+
groups.push({
|
|
1145
|
+
key: `completed:run:${run?.id || index}`,
|
|
1146
|
+
label: `${run?.id || 'run'} [${run?.scope || 'single'}] ${completed}/${workItems.length} done at ${run?.completedAt || 'unknown'}`,
|
|
1147
|
+
files: filterExistingFiles(collectFireRunFiles(run).map((file) => ({ ...file, scope: 'completed' })))
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const completedIntents = Array.isArray(snapshot?.intents)
|
|
1152
|
+
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
1153
|
+
: [];
|
|
1154
|
+
for (let index = 0; index < completedIntents.length; index += 1) {
|
|
1155
|
+
const intent = completedIntents[index];
|
|
1156
|
+
groups.push({
|
|
1157
|
+
key: `completed:intent:${intent?.id || index}`,
|
|
1158
|
+
label: `intent ${intent?.id || 'unknown'} [completed]`,
|
|
1159
|
+
files: filterExistingFiles([{
|
|
1160
|
+
label: `${intent?.id || 'intent'}/brief.md`,
|
|
1161
|
+
path: path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
1162
|
+
scope: 'intent'
|
|
1163
|
+
}])
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return groups;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
1171
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1172
|
+
return [{
|
|
1173
|
+
kind: 'info',
|
|
1174
|
+
key: 'section:empty',
|
|
1175
|
+
label: emptyLabel,
|
|
1176
|
+
selectable: false
|
|
1177
|
+
}];
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const rows = [];
|
|
1181
|
+
|
|
1182
|
+
for (const group of groups) {
|
|
1183
|
+
const files = filterExistingFiles(group?.files);
|
|
1184
|
+
const expandable = files.length > 0;
|
|
1185
|
+
const expanded = expandable && Boolean(expandedGroups?.[group.key]);
|
|
1186
|
+
|
|
1187
|
+
rows.push({
|
|
1188
|
+
kind: 'group',
|
|
1189
|
+
key: group.key,
|
|
1190
|
+
label: group.label,
|
|
1191
|
+
expandable,
|
|
1192
|
+
expanded,
|
|
1193
|
+
selectable: true
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
if (expanded) {
|
|
1197
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
1198
|
+
const file = files[index];
|
|
1199
|
+
rows.push({
|
|
1200
|
+
kind: 'file',
|
|
1201
|
+
key: `${group.key}:file:${file.path}:${index}`,
|
|
1202
|
+
label: file.label,
|
|
1203
|
+
path: file.path,
|
|
1204
|
+
scope: file.scope || 'file',
|
|
1205
|
+
selectable: true
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return rows;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedSection) {
|
|
1215
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1216
|
+
return [{ text: '', color: undefined, bold: false, selected: false }];
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const clampedIndex = clampIndex(selectedIndex, rows.length);
|
|
1220
|
+
|
|
1221
|
+
return rows.map((row, index) => {
|
|
1222
|
+
const selectable = row?.selectable !== false;
|
|
1223
|
+
const isSelected = selectable && index === clampedIndex;
|
|
1224
|
+
const cursor = isSelected
|
|
1225
|
+
? (isFocusedSection ? (icons.activeFile || '>') : '•')
|
|
1226
|
+
: ' ';
|
|
1227
|
+
|
|
1228
|
+
if (row.kind === 'group') {
|
|
1229
|
+
const marker = row.expandable
|
|
1230
|
+
? (row.expanded ? (icons.groupExpanded || 'v') : (icons.groupCollapsed || '>'))
|
|
1231
|
+
: '-';
|
|
1232
|
+
return {
|
|
1233
|
+
text: truncate(`${cursor} ${marker} ${row.label}`, width),
|
|
1234
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : undefined,
|
|
1235
|
+
bold: isSelected,
|
|
1236
|
+
selected: isSelected
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (row.kind === 'file') {
|
|
1241
|
+
const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
|
|
1242
|
+
return {
|
|
1243
|
+
text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
|
|
1244
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : 'gray',
|
|
1245
|
+
bold: isSelected,
|
|
1246
|
+
selected: isSelected
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
text: truncate(` ${row.label || ''}`, width),
|
|
1252
|
+
color: 'gray',
|
|
1253
|
+
bold: false,
|
|
1254
|
+
selected: false
|
|
1255
|
+
};
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function getSelectedRow(rows, selectedIndex) {
|
|
1260
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
return rows[clampIndex(selectedIndex, rows.length)] || null;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function rowToFileEntry(row) {
|
|
1267
|
+
if (!row || row.kind !== 'file' || typeof row.path !== 'string') {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
label: row.label || path.basename(row.path),
|
|
1272
|
+
path: row.path,
|
|
1273
|
+
scope: row.scope || 'file'
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function moveRowSelection(rows, currentIndex, direction) {
|
|
1278
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1279
|
+
return 0;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const clamped = clampIndex(currentIndex, rows.length);
|
|
1283
|
+
const step = direction >= 0 ? 1 : -1;
|
|
1284
|
+
let next = clamped + step;
|
|
1285
|
+
|
|
1286
|
+
while (next >= 0 && next < rows.length) {
|
|
1287
|
+
if (rows[next]?.selectable !== false) {
|
|
1288
|
+
return next;
|
|
1289
|
+
}
|
|
1290
|
+
next += step;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return clamped;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function openFileWithDefaultApp(filePath) {
|
|
1297
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
1298
|
+
return {
|
|
1299
|
+
ok: false,
|
|
1300
|
+
message: 'No file selected to open.'
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (!fileExists(filePath)) {
|
|
1305
|
+
return {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
message: `File not found: ${filePath}`
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
let command = null;
|
|
1312
|
+
let args = [];
|
|
1313
|
+
|
|
1314
|
+
if (process.platform === 'darwin') {
|
|
1315
|
+
command = 'open';
|
|
1316
|
+
args = [filePath];
|
|
1317
|
+
} else if (process.platform === 'win32') {
|
|
1318
|
+
command = 'cmd';
|
|
1319
|
+
args = ['/c', 'start', '', filePath];
|
|
1320
|
+
} else {
|
|
1321
|
+
command = 'xdg-open';
|
|
1322
|
+
args = [filePath];
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const result = spawnSync(command, args, { stdio: 'ignore' });
|
|
1326
|
+
if (result.error) {
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
message: `Unable to open file: ${result.error.message}`
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
1333
|
+
return {
|
|
1334
|
+
ok: false,
|
|
1335
|
+
message: `Open command failed with exit code ${result.status}.`
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return {
|
|
1340
|
+
ok: true,
|
|
1341
|
+
message: `Opened ${filePath}`
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
978
1345
|
function colorizeMarkdownLine(line, inCodeBlock) {
|
|
979
1346
|
const text = String(line ?? '');
|
|
980
1347
|
|
|
@@ -1033,7 +1400,9 @@ function colorizeMarkdownLine(line, inCodeBlock) {
|
|
|
1033
1400
|
};
|
|
1034
1401
|
}
|
|
1035
1402
|
|
|
1036
|
-
function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
1403
|
+
function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
1404
|
+
const fullDocument = options?.fullDocument === true;
|
|
1405
|
+
|
|
1037
1406
|
if (!fileEntry || typeof fileEntry.path !== 'string') {
|
|
1038
1407
|
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
1039
1408
|
}
|
|
@@ -1056,8 +1425,8 @@ function buildPreviewLines(fileEntry, width, scrollOffset) {
|
|
|
1056
1425
|
bold: true
|
|
1057
1426
|
};
|
|
1058
1427
|
|
|
1059
|
-
const cappedLines = rawLines.slice(0, 300);
|
|
1060
|
-
const hiddenLineCount = Math.max(0, rawLines.length - cappedLines.length);
|
|
1428
|
+
const cappedLines = fullDocument ? rawLines : rawLines.slice(0, 300);
|
|
1429
|
+
const hiddenLineCount = fullDocument ? 0 : Math.max(0, rawLines.length - cappedLines.length);
|
|
1061
1430
|
let inCodeBlock = false;
|
|
1062
1431
|
|
|
1063
1432
|
const highlighted = cappedLines.map((rawLine, index) => {
|
|
@@ -1148,23 +1517,31 @@ function createDashboardApp(deps) {
|
|
|
1148
1517
|
maxLines,
|
|
1149
1518
|
borderColor,
|
|
1150
1519
|
marginBottom,
|
|
1151
|
-
dense
|
|
1520
|
+
dense,
|
|
1521
|
+
focused
|
|
1152
1522
|
} = props;
|
|
1153
1523
|
|
|
1154
1524
|
const contentWidth = Math.max(18, width - 4);
|
|
1155
1525
|
const visibleLines = fitLines(lines, maxLines, contentWidth);
|
|
1526
|
+
const panelBorderColor = focused ? 'cyan' : (borderColor || 'gray');
|
|
1527
|
+
const titleColor = focused ? 'black' : 'cyan';
|
|
1528
|
+
const titleBackground = focused ? 'cyan' : undefined;
|
|
1156
1529
|
|
|
1157
1530
|
return React.createElement(
|
|
1158
1531
|
Box,
|
|
1159
1532
|
{
|
|
1160
1533
|
flexDirection: 'column',
|
|
1161
1534
|
borderStyle: dense ? 'single' : 'round',
|
|
1162
|
-
borderColor:
|
|
1535
|
+
borderColor: panelBorderColor,
|
|
1163
1536
|
paddingX: dense ? 0 : 1,
|
|
1164
1537
|
width,
|
|
1165
1538
|
marginBottom: marginBottom || 0
|
|
1166
1539
|
},
|
|
1167
|
-
React.createElement(
|
|
1540
|
+
React.createElement(
|
|
1541
|
+
Text,
|
|
1542
|
+
{ bold: true, color: titleColor, backgroundColor: titleBackground },
|
|
1543
|
+
truncate(title, contentWidth)
|
|
1544
|
+
),
|
|
1168
1545
|
...visibleLines.map((line, index) => React.createElement(
|
|
1169
1546
|
Text,
|
|
1170
1547
|
{
|
|
@@ -1195,8 +1572,8 @@ function createDashboardApp(deps) {
|
|
|
1195
1572
|
{
|
|
1196
1573
|
key: tab.id,
|
|
1197
1574
|
bold: isActive,
|
|
1198
|
-
color: isActive ? '
|
|
1199
|
-
backgroundColor: isActive ? '
|
|
1575
|
+
color: isActive ? 'white' : 'gray',
|
|
1576
|
+
backgroundColor: isActive ? 'blue' : undefined
|
|
1200
1577
|
},
|
|
1201
1578
|
tab.label
|
|
1202
1579
|
);
|
|
@@ -1243,14 +1620,29 @@ function createDashboardApp(deps) {
|
|
|
1243
1620
|
const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
|
|
1244
1621
|
const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
|
|
1245
1622
|
const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
|
|
1623
|
+
const lastVPressRef = useRef(0);
|
|
1246
1624
|
|
|
1247
1625
|
const [activeFlow, setActiveFlow] = useState(fallbackFlow);
|
|
1248
1626
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
1249
1627
|
const [error, setError] = useState(initialNormalizedError);
|
|
1250
1628
|
const [ui, setUi] = useState(createInitialUIState());
|
|
1251
|
-
const [
|
|
1629
|
+
const [sectionFocus, setSectionFocus] = useState({
|
|
1630
|
+
runs: 'run-files',
|
|
1631
|
+
overview: 'project',
|
|
1632
|
+
health: 'stats'
|
|
1633
|
+
});
|
|
1634
|
+
const [selectionBySection, setSelectionBySection] = useState({
|
|
1635
|
+
'run-files': 0,
|
|
1636
|
+
pending: 0,
|
|
1637
|
+
completed: 0
|
|
1638
|
+
});
|
|
1639
|
+
const [expandedGroups, setExpandedGroups] = useState({});
|
|
1640
|
+
const [previewTarget, setPreviewTarget] = useState(null);
|
|
1252
1641
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
1642
|
+
const [paneFocus, setPaneFocus] = useState('main');
|
|
1643
|
+
const [overlayPreviewOpen, setOverlayPreviewOpen] = useState(false);
|
|
1253
1644
|
const [previewScroll, setPreviewScroll] = useState(0);
|
|
1645
|
+
const [statusLine, setStatusLine] = useState('');
|
|
1254
1646
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
1255
1647
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
1256
1648
|
const [terminalSize, setTerminalSize] = useState(() => ({
|
|
@@ -1273,9 +1665,42 @@ function createDashboardApp(deps) {
|
|
|
1273
1665
|
}
|
|
1274
1666
|
};
|
|
1275
1667
|
}, [parseSnapshotForFlow, parseSnapshot]);
|
|
1668
|
+
|
|
1669
|
+
const previewVisibleRows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
1670
|
+
const showErrorPanelForSections = Boolean(error) && previewVisibleRows >= 18;
|
|
1671
|
+
const getAvailableSections = useCallback((viewId) => {
|
|
1672
|
+
const base = getSectionOrderForView(viewId);
|
|
1673
|
+
return base.filter((sectionKey) => sectionKey !== 'error-details' || showErrorPanelForSections);
|
|
1674
|
+
}, [showErrorPanelForSections]);
|
|
1675
|
+
|
|
1276
1676
|
const runFileEntries = getRunFileEntries(snapshot, activeFlow);
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1677
|
+
const runFileRows = toRunFileRows(runFileEntries, activeFlow);
|
|
1678
|
+
const pendingRows = toExpandableRows(
|
|
1679
|
+
buildPendingGroups(snapshot, activeFlow),
|
|
1680
|
+
getNoPendingMessage(getEffectiveFlow(activeFlow, snapshot)),
|
|
1681
|
+
expandedGroups
|
|
1682
|
+
);
|
|
1683
|
+
const completedRows = toExpandableRows(
|
|
1684
|
+
buildCompletedGroups(snapshot, activeFlow),
|
|
1685
|
+
getNoCompletedMessage(getEffectiveFlow(activeFlow, snapshot)),
|
|
1686
|
+
expandedGroups
|
|
1687
|
+
);
|
|
1688
|
+
|
|
1689
|
+
const rowsBySection = {
|
|
1690
|
+
'run-files': runFileRows,
|
|
1691
|
+
pending: pendingRows,
|
|
1692
|
+
completed: completedRows
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
const currentSectionOrder = getAvailableSections(ui.view);
|
|
1696
|
+
const focusedSection = currentSectionOrder.includes(sectionFocus[ui.view])
|
|
1697
|
+
? sectionFocus[ui.view]
|
|
1698
|
+
: (currentSectionOrder[0] || 'current-run');
|
|
1699
|
+
|
|
1700
|
+
const focusedRows = rowsBySection[focusedSection] || [];
|
|
1701
|
+
const focusedIndex = selectionBySection[focusedSection] || 0;
|
|
1702
|
+
const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
|
|
1703
|
+
const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
|
|
1279
1704
|
|
|
1280
1705
|
const refresh = useCallback(async () => {
|
|
1281
1706
|
const now = new Date().toISOString();
|
|
@@ -1337,41 +1762,6 @@ function createDashboardApp(deps) {
|
|
|
1337
1762
|
return;
|
|
1338
1763
|
}
|
|
1339
1764
|
|
|
1340
|
-
if (input === 'v' && ui.view === 'runs') {
|
|
1341
|
-
if (selectedFile) {
|
|
1342
|
-
setPreviewOpen((previous) => !previous);
|
|
1343
|
-
setPreviewScroll(0);
|
|
1344
|
-
}
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
if (key.escape && previewOpen) {
|
|
1349
|
-
setPreviewOpen(false);
|
|
1350
|
-
setPreviewScroll(0);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
|
|
1355
|
-
const moveDown = key.downArrow || input === 'j';
|
|
1356
|
-
const moveUp = key.upArrow || input === 'k';
|
|
1357
|
-
|
|
1358
|
-
if (previewOpen) {
|
|
1359
|
-
if (moveDown) {
|
|
1360
|
-
setPreviewScroll((previous) => previous + 1);
|
|
1361
|
-
} else if (moveUp) {
|
|
1362
|
-
setPreviewScroll((previous) => Math.max(0, previous - 1));
|
|
1363
|
-
}
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
if (moveDown) {
|
|
1368
|
-
setSelectedFileIndex((previous) => clampIndex(previous + 1, runFileEntries.length));
|
|
1369
|
-
} else if (moveUp) {
|
|
1370
|
-
setSelectedFileIndex((previous) => clampIndex(previous - 1, runFileEntries.length));
|
|
1371
|
-
}
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
1765
|
if (input === 'h' || input === '?') {
|
|
1376
1766
|
setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }));
|
|
1377
1767
|
return;
|
|
@@ -1379,31 +1769,19 @@ function createDashboardApp(deps) {
|
|
|
1379
1769
|
|
|
1380
1770
|
if (input === '1') {
|
|
1381
1771
|
setUi((previous) => ({ ...previous, view: 'runs' }));
|
|
1772
|
+
setPaneFocus('main');
|
|
1382
1773
|
return;
|
|
1383
1774
|
}
|
|
1384
1775
|
|
|
1385
1776
|
if (input === '2') {
|
|
1386
1777
|
setUi((previous) => ({ ...previous, view: 'overview' }));
|
|
1778
|
+
setPaneFocus('main');
|
|
1387
1779
|
return;
|
|
1388
1780
|
}
|
|
1389
1781
|
|
|
1390
1782
|
if (input === '3') {
|
|
1391
1783
|
setUi((previous) => ({ ...previous, view: 'health' }));
|
|
1392
|
-
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
if (key.tab) {
|
|
1396
|
-
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
if (key.rightArrow) {
|
|
1401
|
-
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
if (key.leftArrow) {
|
|
1406
|
-
setUi((previous) => ({ ...previous, view: cycleViewBackward(previous.view) }));
|
|
1784
|
+
setPaneFocus('main');
|
|
1407
1785
|
return;
|
|
1408
1786
|
}
|
|
1409
1787
|
|
|
@@ -1419,8 +1797,22 @@ function createDashboardApp(deps) {
|
|
|
1419
1797
|
: 0;
|
|
1420
1798
|
return availableFlowIds[nextIndex];
|
|
1421
1799
|
});
|
|
1800
|
+
setSelectionBySection({
|
|
1801
|
+
'run-files': 0,
|
|
1802
|
+
pending: 0,
|
|
1803
|
+
completed: 0
|
|
1804
|
+
});
|
|
1805
|
+
setSectionFocus({
|
|
1806
|
+
runs: 'run-files',
|
|
1807
|
+
overview: 'project',
|
|
1808
|
+
health: 'stats'
|
|
1809
|
+
});
|
|
1810
|
+
setExpandedGroups({});
|
|
1811
|
+
setPreviewTarget(null);
|
|
1422
1812
|
setPreviewOpen(false);
|
|
1813
|
+
setOverlayPreviewOpen(false);
|
|
1423
1814
|
setPreviewScroll(0);
|
|
1815
|
+
setPaneFocus('main');
|
|
1424
1816
|
return;
|
|
1425
1817
|
}
|
|
1426
1818
|
|
|
@@ -1436,8 +1828,210 @@ function createDashboardApp(deps) {
|
|
|
1436
1828
|
: 0;
|
|
1437
1829
|
return availableFlowIds[nextIndex];
|
|
1438
1830
|
});
|
|
1831
|
+
setSelectionBySection({
|
|
1832
|
+
'run-files': 0,
|
|
1833
|
+
pending: 0,
|
|
1834
|
+
completed: 0
|
|
1835
|
+
});
|
|
1836
|
+
setSectionFocus({
|
|
1837
|
+
runs: 'run-files',
|
|
1838
|
+
overview: 'project',
|
|
1839
|
+
health: 'stats'
|
|
1840
|
+
});
|
|
1841
|
+
setExpandedGroups({});
|
|
1842
|
+
setPreviewTarget(null);
|
|
1439
1843
|
setPreviewOpen(false);
|
|
1844
|
+
setOverlayPreviewOpen(false);
|
|
1440
1845
|
setPreviewScroll(0);
|
|
1846
|
+
setPaneFocus('main');
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
const availableSections = getAvailableSections(ui.view);
|
|
1851
|
+
const activeSection = availableSections.includes(sectionFocus[ui.view])
|
|
1852
|
+
? sectionFocus[ui.view]
|
|
1853
|
+
: (availableSections[0] || 'current-run');
|
|
1854
|
+
|
|
1855
|
+
if (key.tab && ui.view === 'runs' && previewOpen) {
|
|
1856
|
+
setPaneFocus((previous) => (previous === 'main' ? 'preview' : 'main'));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (key.rightArrow || input === 'g') {
|
|
1861
|
+
setSectionFocus((previous) => ({
|
|
1862
|
+
...previous,
|
|
1863
|
+
[ui.view]: cycleSection(ui.view, activeSection, 1, availableSections)
|
|
1864
|
+
}));
|
|
1865
|
+
setPaneFocus('main');
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (key.leftArrow || input === 'G') {
|
|
1870
|
+
setSectionFocus((previous) => ({
|
|
1871
|
+
...previous,
|
|
1872
|
+
[ui.view]: cycleSection(ui.view, activeSection, -1, availableSections)
|
|
1873
|
+
}));
|
|
1874
|
+
setPaneFocus('main');
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (ui.view === 'runs') {
|
|
1879
|
+
if (input === 'a') {
|
|
1880
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'current-run' }));
|
|
1881
|
+
setPaneFocus('main');
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (input === 'f') {
|
|
1885
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'run-files' }));
|
|
1886
|
+
setPaneFocus('main');
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (input === 'p') {
|
|
1890
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'pending' }));
|
|
1891
|
+
setPaneFocus('main');
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (input === 'c') {
|
|
1895
|
+
setSectionFocus((previous) => ({ ...previous, runs: 'completed' }));
|
|
1896
|
+
setPaneFocus('main');
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
} else if (ui.view === 'overview') {
|
|
1900
|
+
if (input === 'p') {
|
|
1901
|
+
setSectionFocus((previous) => ({ ...previous, overview: 'project' }));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (input === 'i') {
|
|
1905
|
+
setSectionFocus((previous) => ({ ...previous, overview: 'intent-status' }));
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (input === 's') {
|
|
1909
|
+
setSectionFocus((previous) => ({ ...previous, overview: 'standards' }));
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
} else if (ui.view === 'health') {
|
|
1913
|
+
if (input === 't') {
|
|
1914
|
+
setSectionFocus((previous) => ({ ...previous, health: 'stats' }));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (input === 'w') {
|
|
1918
|
+
setSectionFocus((previous) => ({ ...previous, health: 'warnings' }));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (input === 'e' && showErrorPanelForSections) {
|
|
1922
|
+
setSectionFocus((previous) => ({ ...previous, health: 'error-details' }));
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
if (key.escape) {
|
|
1928
|
+
if (overlayPreviewOpen) {
|
|
1929
|
+
setOverlayPreviewOpen(false);
|
|
1930
|
+
setPaneFocus('preview');
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
if (previewOpen) {
|
|
1934
|
+
setPreviewOpen(false);
|
|
1935
|
+
setPreviewScroll(0);
|
|
1936
|
+
setPaneFocus('main');
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
|
|
1942
|
+
const moveDown = key.downArrow || input === 'j';
|
|
1943
|
+
const moveUp = key.upArrow || input === 'k';
|
|
1944
|
+
|
|
1945
|
+
if (overlayPreviewOpen || (previewOpen && paneFocus === 'preview')) {
|
|
1946
|
+
if (moveDown) {
|
|
1947
|
+
setPreviewScroll((previous) => previous + 1);
|
|
1948
|
+
} else if (moveUp) {
|
|
1949
|
+
setPreviewScroll((previous) => Math.max(0, previous - 1));
|
|
1950
|
+
}
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const targetSection = activeSection === 'current-run' ? 'run-files' : activeSection;
|
|
1955
|
+
if (targetSection !== activeSection) {
|
|
1956
|
+
setSectionFocus((previous) => ({ ...previous, runs: targetSection }));
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const targetRows = rowsBySection[targetSection] || [];
|
|
1960
|
+
if (targetRows.length === 0) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const currentIndex = selectionBySection[targetSection] || 0;
|
|
1965
|
+
const nextIndex = moveDown
|
|
1966
|
+
? moveRowSelection(targetRows, currentIndex, 1)
|
|
1967
|
+
: moveRowSelection(targetRows, currentIndex, -1);
|
|
1968
|
+
|
|
1969
|
+
setSelectionBySection((previous) => ({
|
|
1970
|
+
...previous,
|
|
1971
|
+
[targetSection]: nextIndex
|
|
1972
|
+
}));
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (ui.view === 'runs' && (key.return || key.enter)) {
|
|
1977
|
+
if (activeSection === 'pending' || activeSection === 'completed') {
|
|
1978
|
+
const rowsForSection = rowsBySection[activeSection] || [];
|
|
1979
|
+
const selectedRow = getSelectedRow(rowsForSection, selectionBySection[activeSection] || 0);
|
|
1980
|
+
if (selectedRow?.kind === 'group' && selectedRow.expandable) {
|
|
1981
|
+
setExpandedGroups((previous) => ({
|
|
1982
|
+
...previous,
|
|
1983
|
+
[selectedRow.key]: !previous[selectedRow.key]
|
|
1984
|
+
}));
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (input === 'v' && ui.view === 'runs') {
|
|
1991
|
+
const target = selectedFocusedFile || previewTarget;
|
|
1992
|
+
if (!target) {
|
|
1993
|
+
setStatusLine('Select a file row first (run files, pending, or completed).');
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const now = Date.now();
|
|
1998
|
+
const isDoublePress = (now - lastVPressRef.current) <= 320;
|
|
1999
|
+
lastVPressRef.current = now;
|
|
2000
|
+
|
|
2001
|
+
if (isDoublePress) {
|
|
2002
|
+
setPreviewTarget(target);
|
|
2003
|
+
setPreviewOpen(true);
|
|
2004
|
+
setOverlayPreviewOpen(true);
|
|
2005
|
+
setPreviewScroll(0);
|
|
2006
|
+
setPaneFocus('preview');
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
if (!previewOpen) {
|
|
2011
|
+
setPreviewTarget(target);
|
|
2012
|
+
setPreviewOpen(true);
|
|
2013
|
+
setOverlayPreviewOpen(false);
|
|
2014
|
+
setPreviewScroll(0);
|
|
2015
|
+
setPaneFocus('main');
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (overlayPreviewOpen) {
|
|
2020
|
+
setOverlayPreviewOpen(false);
|
|
2021
|
+
setPaneFocus('preview');
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
setPreviewOpen(false);
|
|
2026
|
+
setPreviewScroll(0);
|
|
2027
|
+
setPaneFocus('main');
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
if (input === 'o' && ui.view === 'runs') {
|
|
2032
|
+
const target = selectedFocusedFile || previewTarget;
|
|
2033
|
+
const result = openFileWithDefaultApp(target?.path);
|
|
2034
|
+
setStatusLine(result.message);
|
|
1441
2035
|
}
|
|
1442
2036
|
});
|
|
1443
2037
|
|
|
@@ -1446,20 +2040,51 @@ function createDashboardApp(deps) {
|
|
|
1446
2040
|
}, [refresh]);
|
|
1447
2041
|
|
|
1448
2042
|
useEffect(() => {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2043
|
+
setSelectionBySection((previous) => ({
|
|
2044
|
+
...previous,
|
|
2045
|
+
'run-files': clampIndex(previous['run-files'] || 0, runFileRows.length),
|
|
2046
|
+
pending: clampIndex(previous.pending || 0, pendingRows.length),
|
|
2047
|
+
completed: clampIndex(previous.completed || 0, completedRows.length)
|
|
2048
|
+
}));
|
|
2049
|
+
}, [activeFlow, runFileRows.length, pendingRows.length, completedRows.length, snapshot?.generatedAt]);
|
|
1455
2050
|
|
|
1456
2051
|
useEffect(() => {
|
|
1457
2052
|
if (ui.view !== 'runs') {
|
|
1458
2053
|
setPreviewOpen(false);
|
|
2054
|
+
setOverlayPreviewOpen(false);
|
|
1459
2055
|
setPreviewScroll(0);
|
|
2056
|
+
setPaneFocus('main');
|
|
1460
2057
|
}
|
|
1461
2058
|
}, [ui.view]);
|
|
1462
2059
|
|
|
2060
|
+
useEffect(() => {
|
|
2061
|
+
if (!previewOpen || overlayPreviewOpen || paneFocus !== 'main') {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
if (!selectedFocusedFile?.path) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (previewTarget?.path === selectedFocusedFile.path) {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
setPreviewTarget(selectedFocusedFile);
|
|
2071
|
+
setPreviewScroll(0);
|
|
2072
|
+
}, [previewOpen, overlayPreviewOpen, paneFocus, selectedFocusedFile?.path, previewTarget?.path]);
|
|
2073
|
+
|
|
2074
|
+
useEffect(() => {
|
|
2075
|
+
if (statusLine === '') {
|
|
2076
|
+
return undefined;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const timeout = setTimeout(() => {
|
|
2080
|
+
setStatusLine('');
|
|
2081
|
+
}, 3500);
|
|
2082
|
+
|
|
2083
|
+
return () => {
|
|
2084
|
+
clearTimeout(timeout);
|
|
2085
|
+
};
|
|
2086
|
+
}, [statusLine]);
|
|
2087
|
+
|
|
1463
2088
|
useEffect(() => {
|
|
1464
2089
|
if (!stdout || typeof stdout.on !== 'function') {
|
|
1465
2090
|
setTerminalSize({
|
|
@@ -1529,12 +2154,20 @@ function createDashboardApp(deps) {
|
|
|
1529
2154
|
};
|
|
1530
2155
|
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
|
|
1531
2156
|
|
|
2157
|
+
useEffect(() => {
|
|
2158
|
+
if (!stdout || typeof stdout.write !== 'function') {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (stdout.isTTY === false) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
stdout.write('\u001B[2J\u001B[3J\u001B[H');
|
|
2165
|
+
}, [stdout]);
|
|
2166
|
+
|
|
1532
2167
|
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
1533
2168
|
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
1534
2169
|
|
|
1535
2170
|
const fullWidth = Math.max(40, cols - 1);
|
|
1536
|
-
const compactWidth = Math.max(18, fullWidth - 4);
|
|
1537
|
-
|
|
1538
2171
|
const showHelpLine = ui.showHelp && rows >= 14;
|
|
1539
2172
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
1540
2173
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
@@ -1544,28 +2177,72 @@ function createDashboardApp(deps) {
|
|
|
1544
2177
|
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
1545
2178
|
const ultraCompact = rows <= 14;
|
|
1546
2179
|
const panelTitles = getPanelTitles(activeFlow, snapshot);
|
|
1547
|
-
const
|
|
1548
|
-
const
|
|
2180
|
+
const splitPreviewLayout = ui.view === 'runs' && previewOpen && !overlayPreviewOpen && cols >= 110 && rows >= 16;
|
|
2181
|
+
const mainPaneWidth = splitPreviewLayout
|
|
2182
|
+
? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
|
|
2183
|
+
: fullWidth;
|
|
2184
|
+
const previewPaneWidth = splitPreviewLayout
|
|
2185
|
+
? Math.max(30, fullWidth - mainPaneWidth - 1)
|
|
2186
|
+
: fullWidth;
|
|
2187
|
+
const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
|
|
2188
|
+
const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
|
|
2189
|
+
|
|
2190
|
+
const runFileLines = buildInteractiveRowsLines(
|
|
2191
|
+
runFileRows,
|
|
2192
|
+
selectionBySection['run-files'] || 0,
|
|
2193
|
+
icons,
|
|
2194
|
+
mainCompactWidth,
|
|
2195
|
+
ui.view === 'runs' && focusedSection === 'run-files' && paneFocus === 'main'
|
|
2196
|
+
);
|
|
2197
|
+
const pendingLines = buildInteractiveRowsLines(
|
|
2198
|
+
pendingRows,
|
|
2199
|
+
selectionBySection.pending || 0,
|
|
2200
|
+
icons,
|
|
2201
|
+
mainCompactWidth,
|
|
2202
|
+
ui.view === 'runs' && focusedSection === 'pending' && paneFocus === 'main'
|
|
2203
|
+
);
|
|
2204
|
+
const completedLines = buildInteractiveRowsLines(
|
|
2205
|
+
completedRows,
|
|
2206
|
+
selectionBySection.completed || 0,
|
|
2207
|
+
icons,
|
|
2208
|
+
mainCompactWidth,
|
|
2209
|
+
ui.view === 'runs' && focusedSection === 'completed' && paneFocus === 'main'
|
|
2210
|
+
);
|
|
2211
|
+
const effectivePreviewTarget = previewTarget || selectedFocusedFile;
|
|
2212
|
+
const previewLines = previewOpen
|
|
2213
|
+
? buildPreviewLines(effectivePreviewTarget, previewCompactWidth, previewScroll, {
|
|
2214
|
+
fullDocument: overlayPreviewOpen
|
|
2215
|
+
})
|
|
2216
|
+
: [];
|
|
1549
2217
|
|
|
1550
2218
|
let panelCandidates;
|
|
1551
|
-
if (ui.view === '
|
|
2219
|
+
if (ui.view === 'runs' && previewOpen && overlayPreviewOpen) {
|
|
2220
|
+
panelCandidates = [
|
|
2221
|
+
{
|
|
2222
|
+
key: 'preview-overlay',
|
|
2223
|
+
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
2224
|
+
lines: previewLines,
|
|
2225
|
+
borderColor: 'magenta'
|
|
2226
|
+
}
|
|
2227
|
+
];
|
|
2228
|
+
} else if (ui.view === 'overview') {
|
|
1552
2229
|
panelCandidates = [
|
|
1553
2230
|
{
|
|
1554
2231
|
key: 'project',
|
|
1555
2232
|
title: 'Project + Workspace',
|
|
1556
|
-
lines: buildOverviewProjectLines(snapshot,
|
|
2233
|
+
lines: buildOverviewProjectLines(snapshot, mainCompactWidth, activeFlow),
|
|
1557
2234
|
borderColor: 'green'
|
|
1558
2235
|
},
|
|
1559
2236
|
{
|
|
1560
2237
|
key: 'intent-status',
|
|
1561
2238
|
title: 'Intent Status',
|
|
1562
|
-
lines: buildOverviewIntentLines(snapshot,
|
|
2239
|
+
lines: buildOverviewIntentLines(snapshot, mainCompactWidth, activeFlow),
|
|
1563
2240
|
borderColor: 'yellow'
|
|
1564
2241
|
},
|
|
1565
2242
|
{
|
|
1566
2243
|
key: 'standards',
|
|
1567
2244
|
title: 'Standards',
|
|
1568
|
-
lines: buildOverviewStandardsLines(snapshot,
|
|
2245
|
+
lines: buildOverviewStandardsLines(snapshot, mainCompactWidth, activeFlow),
|
|
1569
2246
|
borderColor: 'blue'
|
|
1570
2247
|
}
|
|
1571
2248
|
];
|
|
@@ -1574,13 +2251,13 @@ function createDashboardApp(deps) {
|
|
|
1574
2251
|
{
|
|
1575
2252
|
key: 'stats',
|
|
1576
2253
|
title: 'Stats',
|
|
1577
|
-
lines: buildStatsLines(snapshot,
|
|
2254
|
+
lines: buildStatsLines(snapshot, mainCompactWidth, activeFlow),
|
|
1578
2255
|
borderColor: 'magenta'
|
|
1579
2256
|
},
|
|
1580
2257
|
{
|
|
1581
2258
|
key: 'warnings',
|
|
1582
2259
|
title: 'Warnings',
|
|
1583
|
-
lines: buildWarningsLines(snapshot,
|
|
2260
|
+
lines: buildWarningsLines(snapshot, mainCompactWidth),
|
|
1584
2261
|
borderColor: 'red'
|
|
1585
2262
|
}
|
|
1586
2263
|
];
|
|
@@ -1589,16 +2266,17 @@ function createDashboardApp(deps) {
|
|
|
1589
2266
|
panelCandidates.push({
|
|
1590
2267
|
key: 'error-details',
|
|
1591
2268
|
title: 'Error Details',
|
|
1592
|
-
lines: buildErrorLines(error,
|
|
2269
|
+
lines: buildErrorLines(error, mainCompactWidth),
|
|
1593
2270
|
borderColor: 'red'
|
|
1594
2271
|
});
|
|
1595
2272
|
}
|
|
1596
2273
|
} else {
|
|
2274
|
+
const includeInlinePreviewPanel = previewOpen && !splitPreviewLayout;
|
|
1597
2275
|
panelCandidates = [
|
|
1598
2276
|
{
|
|
1599
2277
|
key: 'current-run',
|
|
1600
2278
|
title: panelTitles.current,
|
|
1601
|
-
lines: buildCurrentRunLines(snapshot,
|
|
2279
|
+
lines: buildCurrentRunLines(snapshot, mainCompactWidth, activeFlow),
|
|
1602
2280
|
borderColor: 'green'
|
|
1603
2281
|
},
|
|
1604
2282
|
{
|
|
@@ -1607,10 +2285,10 @@ function createDashboardApp(deps) {
|
|
|
1607
2285
|
lines: runFileLines,
|
|
1608
2286
|
borderColor: 'yellow'
|
|
1609
2287
|
},
|
|
1610
|
-
|
|
2288
|
+
includeInlinePreviewPanel
|
|
1611
2289
|
? {
|
|
1612
2290
|
key: 'preview',
|
|
1613
|
-
title: `Preview: ${
|
|
2291
|
+
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
1614
2292
|
lines: previewLines,
|
|
1615
2293
|
borderColor: 'magenta'
|
|
1616
2294
|
}
|
|
@@ -1618,19 +2296,19 @@ function createDashboardApp(deps) {
|
|
|
1618
2296
|
{
|
|
1619
2297
|
key: 'pending',
|
|
1620
2298
|
title: panelTitles.pending,
|
|
1621
|
-
lines:
|
|
2299
|
+
lines: pendingLines,
|
|
1622
2300
|
borderColor: 'yellow'
|
|
1623
2301
|
},
|
|
1624
2302
|
{
|
|
1625
2303
|
key: 'completed',
|
|
1626
2304
|
title: panelTitles.completed,
|
|
1627
|
-
lines:
|
|
2305
|
+
lines: completedLines,
|
|
1628
2306
|
borderColor: 'blue'
|
|
1629
2307
|
}
|
|
1630
2308
|
];
|
|
1631
2309
|
}
|
|
1632
2310
|
|
|
1633
|
-
if (ultraCompact) {
|
|
2311
|
+
if (ultraCompact && !splitPreviewLayout) {
|
|
1634
2312
|
if (previewOpen) {
|
|
1635
2313
|
panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === 'current-run' || panel.key === 'preview'));
|
|
1636
2314
|
} else {
|
|
@@ -1640,8 +2318,83 @@ function createDashboardApp(deps) {
|
|
|
1640
2318
|
|
|
1641
2319
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
1642
2320
|
const flowSwitchHint = availableFlowIds.length > 1 ? ' | [ or ] switch flow' : '';
|
|
1643
|
-
const
|
|
1644
|
-
|
|
2321
|
+
const sectionHint = ui.view === 'runs'
|
|
2322
|
+
? ' | a active | f files | p pending | c done'
|
|
2323
|
+
: (ui.view === 'overview' ? ' | p project | i intents | s standards' : ' | t stats | w warnings | e errors');
|
|
2324
|
+
const previewHint = ui.view === 'runs'
|
|
2325
|
+
? (previewOpen
|
|
2326
|
+
? ` | tab ${paneFocus === 'preview' ? 'main' : 'preview'} | ↑/↓ ${paneFocus === 'preview' ? 'scroll' : 'navigate'} | v close | vv fullscreen`
|
|
2327
|
+
: ' | ↑/↓ navigate | enter expand | v preview | vv fullscreen | o open')
|
|
2328
|
+
: '';
|
|
2329
|
+
const helpText = `q quit | r refresh | h/? help | 1 runs | 2 overview | 3 health | g/G section${sectionHint}${previewHint}${flowSwitchHint}`;
|
|
2330
|
+
|
|
2331
|
+
const renderPanel = (panel, index, width, isFocused) => React.createElement(SectionPanel, {
|
|
2332
|
+
key: panel.key,
|
|
2333
|
+
title: panel.title,
|
|
2334
|
+
lines: panel.lines,
|
|
2335
|
+
width,
|
|
2336
|
+
maxLines: panel.maxLines,
|
|
2337
|
+
borderColor: panel.borderColor,
|
|
2338
|
+
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
2339
|
+
dense: densePanels,
|
|
2340
|
+
focused: isFocused
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
let contentNode;
|
|
2344
|
+
if (splitPreviewLayout && ui.view === 'runs' && !overlayPreviewOpen) {
|
|
2345
|
+
const previewPanel = {
|
|
2346
|
+
key: 'preview-split',
|
|
2347
|
+
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
2348
|
+
lines: previewLines,
|
|
2349
|
+
borderColor: 'magenta',
|
|
2350
|
+
maxLines: Math.max(4, contentRowsBudget)
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
contentNode = React.createElement(
|
|
2354
|
+
Box,
|
|
2355
|
+
{ width: fullWidth, flexDirection: 'row' },
|
|
2356
|
+
React.createElement(
|
|
2357
|
+
Box,
|
|
2358
|
+
{ width: mainPaneWidth, flexDirection: 'column' },
|
|
2359
|
+
...panels.map((panel, index) => React.createElement(SectionPanel, {
|
|
2360
|
+
key: panel.key,
|
|
2361
|
+
title: panel.title,
|
|
2362
|
+
lines: panel.lines,
|
|
2363
|
+
width: mainPaneWidth,
|
|
2364
|
+
maxLines: panel.maxLines,
|
|
2365
|
+
borderColor: panel.borderColor,
|
|
2366
|
+
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
2367
|
+
dense: densePanels,
|
|
2368
|
+
focused: paneFocus === 'main' && panel.key === focusedSection
|
|
2369
|
+
}))
|
|
2370
|
+
),
|
|
2371
|
+
React.createElement(Box, { width: 1 }, React.createElement(Text, null, ' ')),
|
|
2372
|
+
React.createElement(
|
|
2373
|
+
Box,
|
|
2374
|
+
{ width: previewPaneWidth, flexDirection: 'column' },
|
|
2375
|
+
React.createElement(SectionPanel, {
|
|
2376
|
+
key: previewPanel.key,
|
|
2377
|
+
title: previewPanel.title,
|
|
2378
|
+
lines: previewPanel.lines,
|
|
2379
|
+
width: previewPaneWidth,
|
|
2380
|
+
maxLines: previewPanel.maxLines,
|
|
2381
|
+
borderColor: previewPanel.borderColor,
|
|
2382
|
+
marginBottom: 0,
|
|
2383
|
+
dense: densePanels,
|
|
2384
|
+
focused: paneFocus === 'preview'
|
|
2385
|
+
})
|
|
2386
|
+
)
|
|
2387
|
+
);
|
|
2388
|
+
} else {
|
|
2389
|
+
contentNode = panels.map((panel, index) => renderPanel(
|
|
2390
|
+
panel,
|
|
2391
|
+
index,
|
|
2392
|
+
fullWidth,
|
|
2393
|
+
(panel.key === 'preview' || panel.key === 'preview-overlay')
|
|
2394
|
+
? paneFocus === 'preview'
|
|
2395
|
+
: (paneFocus === 'main' && panel.key === focusedSection)
|
|
2396
|
+
));
|
|
2397
|
+
}
|
|
1645
2398
|
|
|
1646
2399
|
return React.createElement(
|
|
1647
2400
|
Box,
|
|
@@ -1655,24 +2408,19 @@ function createDashboardApp(deps) {
|
|
|
1655
2408
|
showErrorPanel
|
|
1656
2409
|
? React.createElement(SectionPanel, {
|
|
1657
2410
|
title: 'Errors',
|
|
1658
|
-
lines: buildErrorLines(error,
|
|
2411
|
+
lines: buildErrorLines(error, Math.max(18, fullWidth - 4)),
|
|
1659
2412
|
width: fullWidth,
|
|
1660
2413
|
maxLines: 2,
|
|
1661
2414
|
borderColor: 'red',
|
|
1662
2415
|
marginBottom: densePanels ? 0 : 1,
|
|
1663
|
-
dense: densePanels
|
|
2416
|
+
dense: densePanels,
|
|
2417
|
+
focused: paneFocus === 'main' && focusedSection === 'error-details'
|
|
1664
2418
|
})
|
|
1665
2419
|
: null,
|
|
1666
|
-
...
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
width: fullWidth,
|
|
1671
|
-
maxLines: panel.maxLines,
|
|
1672
|
-
borderColor: panel.borderColor,
|
|
1673
|
-
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
1674
|
-
dense: densePanels
|
|
1675
|
-
})),
|
|
2420
|
+
...(Array.isArray(contentNode) ? contentNode : [contentNode]),
|
|
2421
|
+
statusLine !== ''
|
|
2422
|
+
? React.createElement(Text, { color: 'yellow' }, truncate(statusLine, fullWidth))
|
|
2423
|
+
: null,
|
|
1676
2424
|
showHelpLine
|
|
1677
2425
|
? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
|
|
1678
2426
|
: null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
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": {
|