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.
@@ -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, cycleView, cycleViewBackward } = require('./store');
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 buildSelectableRunFileLines(fileEntries, selectedIndex, icons, width, flow) {
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 [truncate(getNoFileMessage(flow), width)];
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
- const clampedIndex = clampIndex(selectedIndex, fileEntries.length);
965
- return fileEntries.map((file, index) => {
966
- const isSelected = index === clampedIndex;
967
- const prefix = isSelected ? '>' : ' ';
968
- const scope = formatScope(file.scope);
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
- text: truncate(`${prefix} ${icons.runFile} [${scope}] ${file.label}`, width),
971
- color: isSelected ? 'cyan' : undefined,
972
- bold: isSelected,
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: borderColor || 'gray',
1535
+ borderColor: panelBorderColor,
1163
1536
  paddingX: dense ? 0 : 1,
1164
1537
  width,
1165
1538
  marginBottom: marginBottom || 0
1166
1539
  },
1167
- React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
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 ? 'black' : 'gray',
1199
- backgroundColor: isActive ? 'cyan' : undefined
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 [selectedFileIndex, setSelectedFileIndex] = useState(0);
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 clampedSelectedFileIndex = clampIndex(selectedFileIndex, runFileEntries.length);
1278
- const selectedFile = runFileEntries[clampedSelectedFileIndex] || null;
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
- return;
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
- setSelectedFileIndex((previous) => clampIndex(previous, runFileEntries.length));
1450
- if (runFileEntries.length === 0) {
1451
- setPreviewOpen(false);
1452
- setPreviewScroll(0);
1453
- }
1454
- }, [activeFlow, runFileEntries.length, snapshot?.generatedAt]);
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 runFileLines = buildSelectableRunFileLines(runFileEntries, clampedSelectedFileIndex, icons, compactWidth, activeFlow);
1548
- const previewLines = previewOpen ? buildPreviewLines(selectedFile, compactWidth, previewScroll) : [];
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 === 'overview') {
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, compactWidth, activeFlow),
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, compactWidth, activeFlow),
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, compactWidth, activeFlow),
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, compactWidth, activeFlow),
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, compactWidth),
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, compactWidth),
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, compactWidth, activeFlow),
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
- previewOpen
2288
+ includeInlinePreviewPanel
1611
2289
  ? {
1612
2290
  key: 'preview',
1613
- title: `Preview: ${selectedFile?.label || 'unknown'}`,
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: buildPendingLines(snapshot, compactWidth, activeFlow),
2299
+ lines: pendingLines,
1622
2300
  borderColor: 'yellow'
1623
2301
  },
1624
2302
  {
1625
2303
  key: 'completed',
1626
2304
  title: panelTitles.completed,
1627
- lines: buildCompletedLines(snapshot, compactWidth, activeFlow),
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 previewHint = previewOpen ? ' | ↑/↓ scroll preview' : ' | ↑/↓ select file | v preview';
1644
- const helpText = `q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health${previewHint}${flowSwitchHint}`;
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, compactWidth),
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
- ...panels.map((panel, index) => React.createElement(SectionPanel, {
1667
- key: panel.key,
1668
- title: panel.title,
1669
- lines: panel.lines,
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.32",
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": {