specsmd 0.1.32 → 0.1.34

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') {
@@ -682,15 +689,70 @@ function buildOverviewProjectLines(snapshot, width, flow) {
682
689
  return buildFireOverviewProjectLines(snapshot, width);
683
690
  }
684
691
 
685
- function buildOverviewIntentLines(snapshot, width, flow) {
692
+ function listOverviewIntentEntries(snapshot, flow) {
686
693
  const effectiveFlow = getEffectiveFlow(flow, snapshot);
687
694
  if (effectiveFlow === 'aidlc') {
688
- return buildAidlcOverviewIntentLines(snapshot, width);
695
+ const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
696
+ return intents.map((intent) => ({
697
+ id: intent?.id || 'unknown',
698
+ status: intent?.status || 'pending',
699
+ line: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${intent?.completedStories || 0}/${intent?.storyCount || 0} stories, ${intent?.completedUnits || 0}/${intent?.unitCount || 0} units)`
700
+ }));
689
701
  }
690
702
  if (effectiveFlow === 'simple') {
691
- return buildSimpleOverviewIntentLines(snapshot, width);
703
+ const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
704
+ return specs.map((spec) => ({
705
+ id: spec?.name || 'unknown',
706
+ status: spec?.state || 'pending',
707
+ line: `${spec?.name || 'unknown'}: ${spec?.state || 'pending'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks)`
708
+ }));
692
709
  }
693
- return buildFireOverviewIntentLines(snapshot, width);
710
+ const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
711
+ return intents.map((intent) => {
712
+ const workItems = Array.isArray(intent?.workItems) ? intent.workItems : [];
713
+ const done = workItems.filter((item) => item.status === 'completed').length;
714
+ return {
715
+ id: intent?.id || 'unknown',
716
+ status: intent?.status || 'pending',
717
+ line: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${done}/${workItems.length} work items)`
718
+ };
719
+ });
720
+ }
721
+
722
+ function buildOverviewIntentLines(snapshot, width, flow, filter = 'next') {
723
+ const entries = listOverviewIntentEntries(snapshot, flow);
724
+ const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
725
+ const isNextFilter = normalizedFilter === 'next';
726
+ const nextLabel = isNextFilter ? '[NEXT]' : ' next ';
727
+ const completedLabel = !isNextFilter ? '[COMPLETED]' : ' completed ';
728
+
729
+ const filtered = entries.filter((entry) => {
730
+ if (normalizedFilter === 'completed') {
731
+ return entry.status === 'completed';
732
+ }
733
+ return entry.status !== 'completed';
734
+ });
735
+
736
+ const lines = [{
737
+ text: truncate(`filter ${nextLabel} | ${completedLabel} (←/→ or n/x)`, width),
738
+ color: 'cyan',
739
+ bold: true
740
+ }];
741
+
742
+ if (filtered.length === 0) {
743
+ lines.push({
744
+ text: truncate(
745
+ normalizedFilter === 'completed' ? 'No completed intents yet' : 'No upcoming intents',
746
+ width
747
+ ),
748
+ color: 'gray',
749
+ bold: false
750
+ });
751
+ return lines;
752
+ }
753
+
754
+ lines.push(...filtered.map((entry) => truncate(entry.line, width)));
755
+ return lines;
694
756
  }
695
757
 
696
758
  function buildOverviewStandardsLines(snapshot, width, flow) {
@@ -730,6 +792,30 @@ function getPanelTitles(flow, snapshot) {
730
792
  };
731
793
  }
732
794
 
795
+ function getSectionOrderForView(view) {
796
+ if (view === 'overview') {
797
+ return ['intent-status', 'completed-runs', 'standards', 'project'];
798
+ }
799
+ if (view === 'health') {
800
+ return ['stats', 'warnings', 'error-details'];
801
+ }
802
+ return ['current-run', 'run-files', 'pending'];
803
+ }
804
+
805
+ function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
806
+ const order = Array.isArray(availableSections) && availableSections.length > 0
807
+ ? availableSections
808
+ : getSectionOrderForView(view);
809
+ if (order.length === 0) {
810
+ return currentSectionKey;
811
+ }
812
+
813
+ const currentIndex = order.indexOf(currentSectionKey);
814
+ const safeIndex = currentIndex >= 0 ? currentIndex : 0;
815
+ const nextIndex = (safeIndex + direction + order.length) % order.length;
816
+ return order[nextIndex];
817
+ }
818
+
733
819
  function fileExists(filePath) {
734
820
  try {
735
821
  return fs.statSync(filePath).isFile();
@@ -956,25 +1042,361 @@ function formatScope(scope) {
956
1042
  return 'FILE';
957
1043
  }
958
1044
 
959
- function buildSelectableRunFileLines(fileEntries, selectedIndex, icons, width, flow) {
1045
+ function getNoPendingMessage(flow) {
1046
+ if (flow === 'aidlc') return 'No queued bolts';
1047
+ if (flow === 'simple') return 'No pending specs';
1048
+ return 'No pending work items';
1049
+ }
1050
+
1051
+ function getNoCompletedMessage(flow) {
1052
+ if (flow === 'aidlc') return 'No completed bolts yet';
1053
+ if (flow === 'simple') return 'No completed specs yet';
1054
+ return 'No completed runs yet';
1055
+ }
1056
+
1057
+ function toRunFileRows(fileEntries, flow) {
960
1058
  if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
961
- return [truncate(getNoFileMessage(flow), width)];
1059
+ return [{
1060
+ kind: 'info',
1061
+ key: 'run-files:empty',
1062
+ label: getNoFileMessage(flow),
1063
+ selectable: false
1064
+ }];
1065
+ }
1066
+
1067
+ return fileEntries.map((file, index) => ({
1068
+ kind: 'file',
1069
+ key: `run-files:${file.path}:${index}`,
1070
+ label: file.label,
1071
+ path: file.path,
1072
+ scope: file.scope || 'file',
1073
+ selectable: true
1074
+ }));
1075
+ }
1076
+
1077
+ function collectAidlcIntentContextFiles(snapshot, intentId) {
1078
+ if (!snapshot || typeof intentId !== 'string' || intentId.trim() === '') {
1079
+ return [];
1080
+ }
1081
+
1082
+ const intentPath = path.join(snapshot.rootPath || '', 'intents', intentId);
1083
+ return [
1084
+ {
1085
+ label: `${intentId}/requirements.md`,
1086
+ path: path.join(intentPath, 'requirements.md'),
1087
+ scope: 'intent'
1088
+ },
1089
+ {
1090
+ label: `${intentId}/system-context.md`,
1091
+ path: path.join(intentPath, 'system-context.md'),
1092
+ scope: 'intent'
1093
+ },
1094
+ {
1095
+ label: `${intentId}/units.md`,
1096
+ path: path.join(intentPath, 'units.md'),
1097
+ scope: 'intent'
1098
+ }
1099
+ ];
1100
+ }
1101
+
1102
+ function filterExistingFiles(files) {
1103
+ return (Array.isArray(files) ? files : []).filter((file) =>
1104
+ file && typeof file.path === 'string' && typeof file.label === 'string' && fileExists(file.path)
1105
+ );
1106
+ }
1107
+
1108
+ function buildPendingGroups(snapshot, flow) {
1109
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1110
+
1111
+ if (effectiveFlow === 'aidlc') {
1112
+ const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
1113
+ return pendingBolts.map((bolt, index) => {
1114
+ const deps = Array.isArray(bolt?.blockedBy) && bolt.blockedBy.length > 0
1115
+ ? ` blocked_by:${bolt.blockedBy.join(',')}`
1116
+ : '';
1117
+ const location = `${bolt?.intent || 'unknown'}/${bolt?.unit || 'unknown'}`;
1118
+ const boltFiles = collectAidlcBoltFiles(bolt);
1119
+ const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
1120
+ return {
1121
+ key: `pending:bolt:${bolt?.id || index}`,
1122
+ label: `${bolt?.id || 'unknown'} (${bolt?.status || 'pending'}) in ${location}${deps}`,
1123
+ files: filterExistingFiles([...boltFiles, ...intentFiles])
1124
+ };
1125
+ });
1126
+ }
1127
+
1128
+ if (effectiveFlow === 'simple') {
1129
+ const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
1130
+ return pendingSpecs.map((spec, index) => ({
1131
+ key: `pending:spec:${spec?.name || index}`,
1132
+ label: `${spec?.name || 'unknown'} (${spec?.state || 'pending'}) ${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks`,
1133
+ files: filterExistingFiles(collectSimpleSpecFiles(spec))
1134
+ }));
1135
+ }
1136
+
1137
+ const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
1138
+ return pendingItems.map((item, index) => {
1139
+ const deps = Array.isArray(item?.dependencies) && item.dependencies.length > 0
1140
+ ? ` deps:${item.dependencies.join(',')}`
1141
+ : '';
1142
+ const intentTitle = item?.intentTitle || item?.intentId || 'unknown-intent';
1143
+ const files = [];
1144
+
1145
+ if (item?.filePath) {
1146
+ files.push({
1147
+ label: `${item?.intentId || 'intent'}/${item?.id || 'work-item'}.md`,
1148
+ path: item.filePath,
1149
+ scope: 'upcoming'
1150
+ });
1151
+ }
1152
+ if (item?.intentId) {
1153
+ files.push({
1154
+ label: `${item.intentId}/brief.md`,
1155
+ path: path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
1156
+ scope: 'intent'
1157
+ });
1158
+ }
1159
+
1160
+ return {
1161
+ key: `pending:item:${item?.intentId || 'intent'}:${item?.id || index}`,
1162
+ label: `${item?.id || 'work-item'} (${item?.mode || 'confirm'}/${item?.complexity || 'medium'}) in ${intentTitle}${deps}`,
1163
+ files: filterExistingFiles(files)
1164
+ };
1165
+ });
1166
+ }
1167
+
1168
+ function buildCompletedGroups(snapshot, flow) {
1169
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1170
+
1171
+ if (effectiveFlow === 'aidlc') {
1172
+ const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
1173
+ return completedBolts.map((bolt, index) => {
1174
+ const boltFiles = collectAidlcBoltFiles(bolt);
1175
+ const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
1176
+ return {
1177
+ key: `completed:bolt:${bolt?.id || index}`,
1178
+ label: `${bolt?.id || 'unknown'} [${bolt?.type || 'bolt'}] done at ${bolt?.completedAt || 'unknown'}`,
1179
+ files: filterExistingFiles([...boltFiles, ...intentFiles])
1180
+ };
1181
+ });
1182
+ }
1183
+
1184
+ if (effectiveFlow === 'simple') {
1185
+ const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
1186
+ return completedSpecs.map((spec, index) => ({
1187
+ key: `completed:spec:${spec?.name || index}`,
1188
+ label: `${spec?.name || 'unknown'} done at ${spec?.updatedAt || 'unknown'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0})`,
1189
+ files: filterExistingFiles(collectSimpleSpecFiles(spec))
1190
+ }));
1191
+ }
1192
+
1193
+ const groups = [];
1194
+ const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
1195
+ for (let index = 0; index < completedRuns.length; index += 1) {
1196
+ const run = completedRuns[index];
1197
+ const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
1198
+ const completed = workItems.filter((item) => item.status === 'completed').length;
1199
+ groups.push({
1200
+ key: `completed:run:${run?.id || index}`,
1201
+ label: `${run?.id || 'run'} [${run?.scope || 'single'}] ${completed}/${workItems.length} done at ${run?.completedAt || 'unknown'}`,
1202
+ files: filterExistingFiles(collectFireRunFiles(run).map((file) => ({ ...file, scope: 'completed' })))
1203
+ });
962
1204
  }
963
1205
 
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);
1206
+ const completedIntents = Array.isArray(snapshot?.intents)
1207
+ ? snapshot.intents.filter((intent) => intent?.status === 'completed')
1208
+ : [];
1209
+ for (let index = 0; index < completedIntents.length; index += 1) {
1210
+ const intent = completedIntents[index];
1211
+ groups.push({
1212
+ key: `completed:intent:${intent?.id || index}`,
1213
+ label: `intent ${intent?.id || 'unknown'} [completed]`,
1214
+ files: filterExistingFiles([{
1215
+ label: `${intent?.id || 'intent'}/brief.md`,
1216
+ path: path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
1217
+ scope: 'intent'
1218
+ }])
1219
+ });
1220
+ }
1221
+
1222
+ return groups;
1223
+ }
1224
+
1225
+ function toExpandableRows(groups, emptyLabel, expandedGroups) {
1226
+ if (!Array.isArray(groups) || groups.length === 0) {
1227
+ return [{
1228
+ kind: 'info',
1229
+ key: 'section:empty',
1230
+ label: emptyLabel,
1231
+ selectable: false
1232
+ }];
1233
+ }
1234
+
1235
+ const rows = [];
1236
+
1237
+ for (const group of groups) {
1238
+ const files = filterExistingFiles(group?.files);
1239
+ const expandable = files.length > 0;
1240
+ const expanded = expandable && Boolean(expandedGroups?.[group.key]);
1241
+
1242
+ rows.push({
1243
+ kind: 'group',
1244
+ key: group.key,
1245
+ label: group.label,
1246
+ expandable,
1247
+ expanded,
1248
+ selectable: true
1249
+ });
1250
+
1251
+ if (expanded) {
1252
+ for (let index = 0; index < files.length; index += 1) {
1253
+ const file = files[index];
1254
+ rows.push({
1255
+ kind: 'file',
1256
+ key: `${group.key}:file:${file.path}:${index}`,
1257
+ label: file.label,
1258
+ path: file.path,
1259
+ scope: file.scope || 'file',
1260
+ selectable: true
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ return rows;
1267
+ }
1268
+
1269
+ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedSection) {
1270
+ if (!Array.isArray(rows) || rows.length === 0) {
1271
+ return [{ text: '', color: undefined, bold: false, selected: false }];
1272
+ }
1273
+
1274
+ const clampedIndex = clampIndex(selectedIndex, rows.length);
1275
+
1276
+ return rows.map((row, index) => {
1277
+ const selectable = row?.selectable !== false;
1278
+ const isSelected = selectable && index === clampedIndex;
1279
+ const cursor = isSelected
1280
+ ? (isFocusedSection ? (icons.activeFile || '>') : '•')
1281
+ : ' ';
1282
+
1283
+ if (row.kind === 'group') {
1284
+ const marker = row.expandable
1285
+ ? (row.expanded ? (icons.groupExpanded || 'v') : (icons.groupCollapsed || '>'))
1286
+ : '-';
1287
+ return {
1288
+ text: truncate(`${cursor} ${marker} ${row.label}`, width),
1289
+ color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : undefined,
1290
+ bold: isSelected,
1291
+ selected: isSelected
1292
+ };
1293
+ }
1294
+
1295
+ if (row.kind === 'file') {
1296
+ const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
1297
+ return {
1298
+ text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
1299
+ color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : 'gray',
1300
+ bold: isSelected,
1301
+ selected: isSelected
1302
+ };
1303
+ }
1304
+
969
1305
  return {
970
- text: truncate(`${prefix} ${icons.runFile} [${scope}] ${file.label}`, width),
971
- color: isSelected ? 'cyan' : undefined,
972
- bold: isSelected,
973
- selected: isSelected
1306
+ text: truncate(` ${row.label || ''}`, width),
1307
+ color: 'gray',
1308
+ bold: false,
1309
+ selected: false
974
1310
  };
975
1311
  });
976
1312
  }
977
1313
 
1314
+ function getSelectedRow(rows, selectedIndex) {
1315
+ if (!Array.isArray(rows) || rows.length === 0) {
1316
+ return null;
1317
+ }
1318
+ return rows[clampIndex(selectedIndex, rows.length)] || null;
1319
+ }
1320
+
1321
+ function rowToFileEntry(row) {
1322
+ if (!row || row.kind !== 'file' || typeof row.path !== 'string') {
1323
+ return null;
1324
+ }
1325
+ return {
1326
+ label: row.label || path.basename(row.path),
1327
+ path: row.path,
1328
+ scope: row.scope || 'file'
1329
+ };
1330
+ }
1331
+
1332
+ function moveRowSelection(rows, currentIndex, direction) {
1333
+ if (!Array.isArray(rows) || rows.length === 0) {
1334
+ return 0;
1335
+ }
1336
+
1337
+ const clamped = clampIndex(currentIndex, rows.length);
1338
+ const step = direction >= 0 ? 1 : -1;
1339
+ let next = clamped + step;
1340
+
1341
+ while (next >= 0 && next < rows.length) {
1342
+ if (rows[next]?.selectable !== false) {
1343
+ return next;
1344
+ }
1345
+ next += step;
1346
+ }
1347
+
1348
+ return clamped;
1349
+ }
1350
+
1351
+ function openFileWithDefaultApp(filePath) {
1352
+ if (typeof filePath !== 'string' || filePath.trim() === '') {
1353
+ return {
1354
+ ok: false,
1355
+ message: 'No file selected to open.'
1356
+ };
1357
+ }
1358
+
1359
+ if (!fileExists(filePath)) {
1360
+ return {
1361
+ ok: false,
1362
+ message: `File not found: ${filePath}`
1363
+ };
1364
+ }
1365
+
1366
+ let command = null;
1367
+ let args = [];
1368
+
1369
+ if (process.platform === 'darwin') {
1370
+ command = 'open';
1371
+ args = [filePath];
1372
+ } else if (process.platform === 'win32') {
1373
+ command = 'cmd';
1374
+ args = ['/c', 'start', '', filePath];
1375
+ } else {
1376
+ command = 'xdg-open';
1377
+ args = [filePath];
1378
+ }
1379
+
1380
+ const result = spawnSync(command, args, { stdio: 'ignore' });
1381
+ if (result.error) {
1382
+ return {
1383
+ ok: false,
1384
+ message: `Unable to open file: ${result.error.message}`
1385
+ };
1386
+ }
1387
+ if (typeof result.status === 'number' && result.status !== 0) {
1388
+ return {
1389
+ ok: false,
1390
+ message: `Open command failed with exit code ${result.status}.`
1391
+ };
1392
+ }
1393
+
1394
+ return {
1395
+ ok: true,
1396
+ message: `Opened ${filePath}`
1397
+ };
1398
+ }
1399
+
978
1400
  function colorizeMarkdownLine(line, inCodeBlock) {
979
1401
  const text = String(line ?? '');
980
1402
 
@@ -1033,7 +1455,9 @@ function colorizeMarkdownLine(line, inCodeBlock) {
1033
1455
  };
1034
1456
  }
1035
1457
 
1036
- function buildPreviewLines(fileEntry, width, scrollOffset) {
1458
+ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
1459
+ const fullDocument = options?.fullDocument === true;
1460
+
1037
1461
  if (!fileEntry || typeof fileEntry.path !== 'string') {
1038
1462
  return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
1039
1463
  }
@@ -1056,8 +1480,8 @@ function buildPreviewLines(fileEntry, width, scrollOffset) {
1056
1480
  bold: true
1057
1481
  };
1058
1482
 
1059
- const cappedLines = rawLines.slice(0, 300);
1060
- const hiddenLineCount = Math.max(0, rawLines.length - cappedLines.length);
1483
+ const cappedLines = fullDocument ? rawLines : rawLines.slice(0, 300);
1484
+ const hiddenLineCount = fullDocument ? 0 : Math.max(0, rawLines.length - cappedLines.length);
1061
1485
  let inCodeBlock = false;
1062
1486
 
1063
1487
  const highlighted = cappedLines.map((rawLine, index) => {
@@ -1148,23 +1572,31 @@ function createDashboardApp(deps) {
1148
1572
  maxLines,
1149
1573
  borderColor,
1150
1574
  marginBottom,
1151
- dense
1575
+ dense,
1576
+ focused
1152
1577
  } = props;
1153
1578
 
1154
1579
  const contentWidth = Math.max(18, width - 4);
1155
1580
  const visibleLines = fitLines(lines, maxLines, contentWidth);
1581
+ const panelBorderColor = focused ? 'cyan' : (borderColor || 'gray');
1582
+ const titleColor = focused ? 'black' : 'cyan';
1583
+ const titleBackground = focused ? 'cyan' : undefined;
1156
1584
 
1157
1585
  return React.createElement(
1158
1586
  Box,
1159
1587
  {
1160
1588
  flexDirection: 'column',
1161
1589
  borderStyle: dense ? 'single' : 'round',
1162
- borderColor: borderColor || 'gray',
1590
+ borderColor: panelBorderColor,
1163
1591
  paddingX: dense ? 0 : 1,
1164
1592
  width,
1165
1593
  marginBottom: marginBottom || 0
1166
1594
  },
1167
- React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
1595
+ React.createElement(
1596
+ Text,
1597
+ { bold: true, color: titleColor, backgroundColor: titleBackground },
1598
+ truncate(title, contentWidth)
1599
+ ),
1168
1600
  ...visibleLines.map((line, index) => React.createElement(
1169
1601
  Text,
1170
1602
  {
@@ -1195,8 +1627,8 @@ function createDashboardApp(deps) {
1195
1627
  {
1196
1628
  key: tab.id,
1197
1629
  bold: isActive,
1198
- color: isActive ? 'black' : 'gray',
1199
- backgroundColor: isActive ? 'cyan' : undefined
1630
+ color: isActive ? 'white' : 'gray',
1631
+ backgroundColor: isActive ? 'blue' : undefined
1200
1632
  },
1201
1633
  tab.label
1202
1634
  );
@@ -1243,14 +1675,30 @@ function createDashboardApp(deps) {
1243
1675
  const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
1244
1676
  const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
1245
1677
  const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
1678
+ const lastVPressRef = useRef(0);
1246
1679
 
1247
1680
  const [activeFlow, setActiveFlow] = useState(fallbackFlow);
1248
1681
  const [snapshot, setSnapshot] = useState(initialSnapshot || null);
1249
1682
  const [error, setError] = useState(initialNormalizedError);
1250
1683
  const [ui, setUi] = useState(createInitialUIState());
1251
- const [selectedFileIndex, setSelectedFileIndex] = useState(0);
1684
+ const [sectionFocus, setSectionFocus] = useState({
1685
+ runs: 'run-files',
1686
+ overview: 'project',
1687
+ health: 'stats'
1688
+ });
1689
+ const [selectionBySection, setSelectionBySection] = useState({
1690
+ 'run-files': 0,
1691
+ pending: 0,
1692
+ completed: 0
1693
+ });
1694
+ const [expandedGroups, setExpandedGroups] = useState({});
1695
+ const [previewTarget, setPreviewTarget] = useState(null);
1696
+ const [overviewIntentFilter, setOverviewIntentFilter] = useState('next');
1252
1697
  const [previewOpen, setPreviewOpen] = useState(false);
1698
+ const [paneFocus, setPaneFocus] = useState('main');
1699
+ const [overlayPreviewOpen, setOverlayPreviewOpen] = useState(false);
1253
1700
  const [previewScroll, setPreviewScroll] = useState(0);
1701
+ const [statusLine, setStatusLine] = useState('');
1254
1702
  const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
1255
1703
  const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
1256
1704
  const [terminalSize, setTerminalSize] = useState(() => ({
@@ -1273,9 +1721,42 @@ function createDashboardApp(deps) {
1273
1721
  }
1274
1722
  };
1275
1723
  }, [parseSnapshotForFlow, parseSnapshot]);
1724
+
1725
+ const previewVisibleRows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
1726
+ const showErrorPanelForSections = Boolean(error) && previewVisibleRows >= 18;
1727
+ const getAvailableSections = useCallback((viewId) => {
1728
+ const base = getSectionOrderForView(viewId);
1729
+ return base.filter((sectionKey) => sectionKey !== 'error-details' || showErrorPanelForSections);
1730
+ }, [showErrorPanelForSections]);
1731
+
1276
1732
  const runFileEntries = getRunFileEntries(snapshot, activeFlow);
1277
- const clampedSelectedFileIndex = clampIndex(selectedFileIndex, runFileEntries.length);
1278
- const selectedFile = runFileEntries[clampedSelectedFileIndex] || null;
1733
+ const runFileRows = toRunFileRows(runFileEntries, activeFlow);
1734
+ const pendingRows = toExpandableRows(
1735
+ buildPendingGroups(snapshot, activeFlow),
1736
+ getNoPendingMessage(getEffectiveFlow(activeFlow, snapshot)),
1737
+ expandedGroups
1738
+ );
1739
+ const completedRows = toExpandableRows(
1740
+ buildCompletedGroups(snapshot, activeFlow),
1741
+ getNoCompletedMessage(getEffectiveFlow(activeFlow, snapshot)),
1742
+ expandedGroups
1743
+ );
1744
+
1745
+ const rowsBySection = {
1746
+ 'run-files': runFileRows,
1747
+ pending: pendingRows,
1748
+ completed: completedRows
1749
+ };
1750
+
1751
+ const currentSectionOrder = getAvailableSections(ui.view);
1752
+ const focusedSection = currentSectionOrder.includes(sectionFocus[ui.view])
1753
+ ? sectionFocus[ui.view]
1754
+ : (currentSectionOrder[0] || 'current-run');
1755
+
1756
+ const focusedRows = rowsBySection[focusedSection] || [];
1757
+ const focusedIndex = selectionBySection[focusedSection] || 0;
1758
+ const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
1759
+ const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
1279
1760
 
1280
1761
  const refresh = useCallback(async () => {
1281
1762
  const now = new Date().toISOString();
@@ -1337,41 +1818,6 @@ function createDashboardApp(deps) {
1337
1818
  return;
1338
1819
  }
1339
1820
 
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
1821
  if (input === 'h' || input === '?') {
1376
1822
  setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }));
1377
1823
  return;
@@ -1379,31 +1825,19 @@ function createDashboardApp(deps) {
1379
1825
 
1380
1826
  if (input === '1') {
1381
1827
  setUi((previous) => ({ ...previous, view: 'runs' }));
1828
+ setPaneFocus('main');
1382
1829
  return;
1383
1830
  }
1384
1831
 
1385
1832
  if (input === '2') {
1386
1833
  setUi((previous) => ({ ...previous, view: 'overview' }));
1834
+ setPaneFocus('main');
1387
1835
  return;
1388
1836
  }
1389
1837
 
1390
1838
  if (input === '3') {
1391
1839
  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) }));
1840
+ setPaneFocus('main');
1407
1841
  return;
1408
1842
  }
1409
1843
 
@@ -1419,8 +1853,23 @@ function createDashboardApp(deps) {
1419
1853
  : 0;
1420
1854
  return availableFlowIds[nextIndex];
1421
1855
  });
1856
+ setSelectionBySection({
1857
+ 'run-files': 0,
1858
+ pending: 0,
1859
+ completed: 0
1860
+ });
1861
+ setSectionFocus({
1862
+ runs: 'run-files',
1863
+ overview: 'project',
1864
+ health: 'stats'
1865
+ });
1866
+ setOverviewIntentFilter('next');
1867
+ setExpandedGroups({});
1868
+ setPreviewTarget(null);
1422
1869
  setPreviewOpen(false);
1870
+ setOverlayPreviewOpen(false);
1423
1871
  setPreviewScroll(0);
1872
+ setPaneFocus('main');
1424
1873
  return;
1425
1874
  }
1426
1875
 
@@ -1436,8 +1885,225 @@ function createDashboardApp(deps) {
1436
1885
  : 0;
1437
1886
  return availableFlowIds[nextIndex];
1438
1887
  });
1888
+ setSelectionBySection({
1889
+ 'run-files': 0,
1890
+ pending: 0,
1891
+ completed: 0
1892
+ });
1893
+ setSectionFocus({
1894
+ runs: 'run-files',
1895
+ overview: 'project',
1896
+ health: 'stats'
1897
+ });
1898
+ setOverviewIntentFilter('next');
1899
+ setExpandedGroups({});
1900
+ setPreviewTarget(null);
1901
+ setPreviewOpen(false);
1902
+ setOverlayPreviewOpen(false);
1903
+ setPreviewScroll(0);
1904
+ setPaneFocus('main');
1905
+ return;
1906
+ }
1907
+
1908
+ const availableSections = getAvailableSections(ui.view);
1909
+ const activeSection = availableSections.includes(sectionFocus[ui.view])
1910
+ ? sectionFocus[ui.view]
1911
+ : (availableSections[0] || 'current-run');
1912
+
1913
+ if (key.tab && ui.view === 'runs' && previewOpen) {
1914
+ setPaneFocus((previous) => (previous === 'main' ? 'preview' : 'main'));
1915
+ return;
1916
+ }
1917
+
1918
+ if (ui.view === 'overview' && activeSection === 'intent-status') {
1919
+ if (input === 'n') {
1920
+ setOverviewIntentFilter('next');
1921
+ return;
1922
+ }
1923
+ if (input === 'x') {
1924
+ setOverviewIntentFilter('completed');
1925
+ return;
1926
+ }
1927
+ if (key.rightArrow || key.leftArrow) {
1928
+ setOverviewIntentFilter((previous) => (previous === 'completed' ? 'next' : 'completed'));
1929
+ return;
1930
+ }
1931
+ }
1932
+
1933
+ if (input === 'g' || key.rightArrow) {
1934
+ setSectionFocus((previous) => ({
1935
+ ...previous,
1936
+ [ui.view]: cycleSection(ui.view, activeSection, 1, availableSections)
1937
+ }));
1938
+ setPaneFocus('main');
1939
+ return;
1940
+ }
1941
+
1942
+ if (input === 'G' || key.leftArrow) {
1943
+ setSectionFocus((previous) => ({
1944
+ ...previous,
1945
+ [ui.view]: cycleSection(ui.view, activeSection, -1, availableSections)
1946
+ }));
1947
+ setPaneFocus('main');
1948
+ return;
1949
+ }
1950
+
1951
+ if (ui.view === 'runs') {
1952
+ if (input === 'a') {
1953
+ setSectionFocus((previous) => ({ ...previous, runs: 'current-run' }));
1954
+ setPaneFocus('main');
1955
+ return;
1956
+ }
1957
+ if (input === 'f') {
1958
+ setSectionFocus((previous) => ({ ...previous, runs: 'run-files' }));
1959
+ setPaneFocus('main');
1960
+ return;
1961
+ }
1962
+ if (input === 'p') {
1963
+ setSectionFocus((previous) => ({ ...previous, runs: 'pending' }));
1964
+ setPaneFocus('main');
1965
+ return;
1966
+ }
1967
+ } else if (ui.view === 'overview') {
1968
+ if (input === 'p') {
1969
+ setSectionFocus((previous) => ({ ...previous, overview: 'project' }));
1970
+ return;
1971
+ }
1972
+ if (input === 'i') {
1973
+ setSectionFocus((previous) => ({ ...previous, overview: 'intent-status' }));
1974
+ return;
1975
+ }
1976
+ if (input === 's') {
1977
+ setSectionFocus((previous) => ({ ...previous, overview: 'standards' }));
1978
+ return;
1979
+ }
1980
+ if (input === 'c') {
1981
+ setSectionFocus((previous) => ({ ...previous, overview: 'completed-runs' }));
1982
+ return;
1983
+ }
1984
+ } else if (ui.view === 'health') {
1985
+ if (input === 't') {
1986
+ setSectionFocus((previous) => ({ ...previous, health: 'stats' }));
1987
+ return;
1988
+ }
1989
+ if (input === 'w') {
1990
+ setSectionFocus((previous) => ({ ...previous, health: 'warnings' }));
1991
+ return;
1992
+ }
1993
+ if (input === 'e' && showErrorPanelForSections) {
1994
+ setSectionFocus((previous) => ({ ...previous, health: 'error-details' }));
1995
+ return;
1996
+ }
1997
+ }
1998
+
1999
+ if (key.escape) {
2000
+ if (overlayPreviewOpen) {
2001
+ setOverlayPreviewOpen(false);
2002
+ setPaneFocus('preview');
2003
+ return;
2004
+ }
2005
+ if (previewOpen) {
2006
+ setPreviewOpen(false);
2007
+ setPreviewScroll(0);
2008
+ setPaneFocus('main');
2009
+ return;
2010
+ }
2011
+ }
2012
+
2013
+ if (ui.view === 'runs' && (key.upArrow || key.downArrow || input === 'j' || input === 'k')) {
2014
+ const moveDown = key.downArrow || input === 'j';
2015
+ const moveUp = key.upArrow || input === 'k';
2016
+
2017
+ if (overlayPreviewOpen || (previewOpen && paneFocus === 'preview')) {
2018
+ if (moveDown) {
2019
+ setPreviewScroll((previous) => previous + 1);
2020
+ } else if (moveUp) {
2021
+ setPreviewScroll((previous) => Math.max(0, previous - 1));
2022
+ }
2023
+ return;
2024
+ }
2025
+
2026
+ const targetSection = activeSection === 'current-run' ? 'run-files' : activeSection;
2027
+ if (targetSection !== activeSection) {
2028
+ setSectionFocus((previous) => ({ ...previous, runs: targetSection }));
2029
+ }
2030
+
2031
+ const targetRows = rowsBySection[targetSection] || [];
2032
+ if (targetRows.length === 0) {
2033
+ return;
2034
+ }
2035
+
2036
+ const currentIndex = selectionBySection[targetSection] || 0;
2037
+ const nextIndex = moveDown
2038
+ ? moveRowSelection(targetRows, currentIndex, 1)
2039
+ : moveRowSelection(targetRows, currentIndex, -1);
2040
+
2041
+ setSelectionBySection((previous) => ({
2042
+ ...previous,
2043
+ [targetSection]: nextIndex
2044
+ }));
2045
+ return;
2046
+ }
2047
+
2048
+ if (ui.view === 'runs' && (key.return || key.enter)) {
2049
+ if (activeSection === 'pending' || activeSection === 'completed') {
2050
+ const rowsForSection = rowsBySection[activeSection] || [];
2051
+ const selectedRow = getSelectedRow(rowsForSection, selectionBySection[activeSection] || 0);
2052
+ if (selectedRow?.kind === 'group' && selectedRow.expandable) {
2053
+ setExpandedGroups((previous) => ({
2054
+ ...previous,
2055
+ [selectedRow.key]: !previous[selectedRow.key]
2056
+ }));
2057
+ }
2058
+ }
2059
+ return;
2060
+ }
2061
+
2062
+ if (input === 'v' && ui.view === 'runs') {
2063
+ const target = selectedFocusedFile || previewTarget;
2064
+ if (!target) {
2065
+ setStatusLine('Select a file row first (run files, pending, or completed).');
2066
+ return;
2067
+ }
2068
+
2069
+ const now = Date.now();
2070
+ const isDoublePress = (now - lastVPressRef.current) <= 320;
2071
+ lastVPressRef.current = now;
2072
+
2073
+ if (isDoublePress) {
2074
+ setPreviewTarget(target);
2075
+ setPreviewOpen(true);
2076
+ setOverlayPreviewOpen(true);
2077
+ setPreviewScroll(0);
2078
+ setPaneFocus('preview');
2079
+ return;
2080
+ }
2081
+
2082
+ if (!previewOpen) {
2083
+ setPreviewTarget(target);
2084
+ setPreviewOpen(true);
2085
+ setOverlayPreviewOpen(false);
2086
+ setPreviewScroll(0);
2087
+ setPaneFocus('main');
2088
+ return;
2089
+ }
2090
+
2091
+ if (overlayPreviewOpen) {
2092
+ setOverlayPreviewOpen(false);
2093
+ setPaneFocus('preview');
2094
+ return;
2095
+ }
2096
+
1439
2097
  setPreviewOpen(false);
1440
2098
  setPreviewScroll(0);
2099
+ setPaneFocus('main');
2100
+ return;
2101
+ }
2102
+
2103
+ if (input === 'o' && ui.view === 'runs') {
2104
+ const target = selectedFocusedFile || previewTarget;
2105
+ const result = openFileWithDefaultApp(target?.path);
2106
+ setStatusLine(result.message);
1441
2107
  }
1442
2108
  });
1443
2109
 
@@ -1446,20 +2112,51 @@ function createDashboardApp(deps) {
1446
2112
  }, [refresh]);
1447
2113
 
1448
2114
  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]);
2115
+ setSelectionBySection((previous) => ({
2116
+ ...previous,
2117
+ 'run-files': clampIndex(previous['run-files'] || 0, runFileRows.length),
2118
+ pending: clampIndex(previous.pending || 0, pendingRows.length),
2119
+ completed: clampIndex(previous.completed || 0, completedRows.length)
2120
+ }));
2121
+ }, [activeFlow, runFileRows.length, pendingRows.length, completedRows.length, snapshot?.generatedAt]);
1455
2122
 
1456
2123
  useEffect(() => {
1457
2124
  if (ui.view !== 'runs') {
1458
2125
  setPreviewOpen(false);
2126
+ setOverlayPreviewOpen(false);
1459
2127
  setPreviewScroll(0);
2128
+ setPaneFocus('main');
1460
2129
  }
1461
2130
  }, [ui.view]);
1462
2131
 
2132
+ useEffect(() => {
2133
+ if (!previewOpen || overlayPreviewOpen || paneFocus !== 'main') {
2134
+ return;
2135
+ }
2136
+ if (!selectedFocusedFile?.path) {
2137
+ return;
2138
+ }
2139
+ if (previewTarget?.path === selectedFocusedFile.path) {
2140
+ return;
2141
+ }
2142
+ setPreviewTarget(selectedFocusedFile);
2143
+ setPreviewScroll(0);
2144
+ }, [previewOpen, overlayPreviewOpen, paneFocus, selectedFocusedFile?.path, previewTarget?.path]);
2145
+
2146
+ useEffect(() => {
2147
+ if (statusLine === '') {
2148
+ return undefined;
2149
+ }
2150
+
2151
+ const timeout = setTimeout(() => {
2152
+ setStatusLine('');
2153
+ }, 3500);
2154
+
2155
+ return () => {
2156
+ clearTimeout(timeout);
2157
+ };
2158
+ }, [statusLine]);
2159
+
1463
2160
  useEffect(() => {
1464
2161
  if (!stdout || typeof stdout.on !== 'function') {
1465
2162
  setTerminalSize({
@@ -1529,12 +2226,20 @@ function createDashboardApp(deps) {
1529
2226
  };
1530
2227
  }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
1531
2228
 
2229
+ useEffect(() => {
2230
+ if (!stdout || typeof stdout.write !== 'function') {
2231
+ return;
2232
+ }
2233
+ if (stdout.isTTY === false) {
2234
+ return;
2235
+ }
2236
+ stdout.write('\u001B[2J\u001B[3J\u001B[H');
2237
+ }, [stdout]);
2238
+
1532
2239
  const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
1533
2240
  const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
1534
2241
 
1535
2242
  const fullWidth = Math.max(40, cols - 1);
1536
- const compactWidth = Math.max(18, fullWidth - 4);
1537
-
1538
2243
  const showHelpLine = ui.showHelp && rows >= 14;
1539
2244
  const showErrorPanel = Boolean(error) && rows >= 18;
1540
2245
  const showErrorInline = Boolean(error) && !showErrorPanel;
@@ -1544,29 +2249,72 @@ function createDashboardApp(deps) {
1544
2249
  const contentRowsBudget = Math.max(4, rows - reservedRows);
1545
2250
  const ultraCompact = rows <= 14;
1546
2251
  const panelTitles = getPanelTitles(activeFlow, snapshot);
1547
- const runFileLines = buildSelectableRunFileLines(runFileEntries, clampedSelectedFileIndex, icons, compactWidth, activeFlow);
1548
- const previewLines = previewOpen ? buildPreviewLines(selectedFile, compactWidth, previewScroll) : [];
2252
+ const splitPreviewLayout = ui.view === 'runs' && previewOpen && !overlayPreviewOpen && cols >= 110 && rows >= 16;
2253
+ const mainPaneWidth = splitPreviewLayout
2254
+ ? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
2255
+ : fullWidth;
2256
+ const previewPaneWidth = splitPreviewLayout
2257
+ ? Math.max(30, fullWidth - mainPaneWidth - 1)
2258
+ : fullWidth;
2259
+ const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
2260
+ const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
2261
+
2262
+ const runFileLines = buildInteractiveRowsLines(
2263
+ runFileRows,
2264
+ selectionBySection['run-files'] || 0,
2265
+ icons,
2266
+ mainCompactWidth,
2267
+ ui.view === 'runs' && focusedSection === 'run-files' && paneFocus === 'main'
2268
+ );
2269
+ const pendingLines = buildInteractiveRowsLines(
2270
+ pendingRows,
2271
+ selectionBySection.pending || 0,
2272
+ icons,
2273
+ mainCompactWidth,
2274
+ ui.view === 'runs' && focusedSection === 'pending' && paneFocus === 'main'
2275
+ );
2276
+ const effectivePreviewTarget = previewTarget || selectedFocusedFile;
2277
+ const previewLines = previewOpen
2278
+ ? buildPreviewLines(effectivePreviewTarget, previewCompactWidth, previewScroll, {
2279
+ fullDocument: overlayPreviewOpen
2280
+ })
2281
+ : [];
1549
2282
 
1550
2283
  let panelCandidates;
1551
- if (ui.view === 'overview') {
2284
+ if (ui.view === 'runs' && previewOpen && overlayPreviewOpen) {
1552
2285
  panelCandidates = [
1553
2286
  {
1554
- key: 'project',
1555
- title: 'Project + Workspace',
1556
- lines: buildOverviewProjectLines(snapshot, compactWidth, activeFlow),
1557
- borderColor: 'green'
1558
- },
2287
+ key: 'preview-overlay',
2288
+ title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
2289
+ lines: previewLines,
2290
+ borderColor: 'magenta'
2291
+ }
2292
+ ];
2293
+ } else if (ui.view === 'overview') {
2294
+ panelCandidates = [
1559
2295
  {
1560
2296
  key: 'intent-status',
1561
- title: 'Intent Status',
1562
- lines: buildOverviewIntentLines(snapshot, compactWidth, activeFlow),
2297
+ title: 'Intents',
2298
+ lines: buildOverviewIntentLines(snapshot, mainCompactWidth, activeFlow, overviewIntentFilter),
1563
2299
  borderColor: 'yellow'
1564
2300
  },
2301
+ {
2302
+ key: 'completed-runs',
2303
+ title: panelTitles.completed,
2304
+ lines: buildCompletedLines(snapshot, mainCompactWidth, activeFlow),
2305
+ borderColor: 'blue'
2306
+ },
1565
2307
  {
1566
2308
  key: 'standards',
1567
2309
  title: 'Standards',
1568
- lines: buildOverviewStandardsLines(snapshot, compactWidth, activeFlow),
2310
+ lines: buildOverviewStandardsLines(snapshot, mainCompactWidth, activeFlow),
1569
2311
  borderColor: 'blue'
2312
+ },
2313
+ {
2314
+ key: 'project',
2315
+ title: 'Project + Workspace',
2316
+ lines: buildOverviewProjectLines(snapshot, mainCompactWidth, activeFlow),
2317
+ borderColor: 'green'
1570
2318
  }
1571
2319
  ];
1572
2320
  } else if (ui.view === 'health') {
@@ -1574,13 +2322,13 @@ function createDashboardApp(deps) {
1574
2322
  {
1575
2323
  key: 'stats',
1576
2324
  title: 'Stats',
1577
- lines: buildStatsLines(snapshot, compactWidth, activeFlow),
2325
+ lines: buildStatsLines(snapshot, mainCompactWidth, activeFlow),
1578
2326
  borderColor: 'magenta'
1579
2327
  },
1580
2328
  {
1581
2329
  key: 'warnings',
1582
2330
  title: 'Warnings',
1583
- lines: buildWarningsLines(snapshot, compactWidth),
2331
+ lines: buildWarningsLines(snapshot, mainCompactWidth),
1584
2332
  borderColor: 'red'
1585
2333
  }
1586
2334
  ];
@@ -1589,16 +2337,17 @@ function createDashboardApp(deps) {
1589
2337
  panelCandidates.push({
1590
2338
  key: 'error-details',
1591
2339
  title: 'Error Details',
1592
- lines: buildErrorLines(error, compactWidth),
2340
+ lines: buildErrorLines(error, mainCompactWidth),
1593
2341
  borderColor: 'red'
1594
2342
  });
1595
2343
  }
1596
2344
  } else {
2345
+ const includeInlinePreviewPanel = previewOpen && !splitPreviewLayout;
1597
2346
  panelCandidates = [
1598
2347
  {
1599
2348
  key: 'current-run',
1600
2349
  title: panelTitles.current,
1601
- lines: buildCurrentRunLines(snapshot, compactWidth, activeFlow),
2350
+ lines: buildCurrentRunLines(snapshot, mainCompactWidth, activeFlow),
1602
2351
  borderColor: 'green'
1603
2352
  },
1604
2353
  {
@@ -1607,10 +2356,10 @@ function createDashboardApp(deps) {
1607
2356
  lines: runFileLines,
1608
2357
  borderColor: 'yellow'
1609
2358
  },
1610
- previewOpen
2359
+ includeInlinePreviewPanel
1611
2360
  ? {
1612
2361
  key: 'preview',
1613
- title: `Preview: ${selectedFile?.label || 'unknown'}`,
2362
+ title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
1614
2363
  lines: previewLines,
1615
2364
  borderColor: 'magenta'
1616
2365
  }
@@ -1618,19 +2367,13 @@ function createDashboardApp(deps) {
1618
2367
  {
1619
2368
  key: 'pending',
1620
2369
  title: panelTitles.pending,
1621
- lines: buildPendingLines(snapshot, compactWidth, activeFlow),
2370
+ lines: pendingLines,
1622
2371
  borderColor: 'yellow'
1623
- },
1624
- {
1625
- key: 'completed',
1626
- title: panelTitles.completed,
1627
- lines: buildCompletedLines(snapshot, compactWidth, activeFlow),
1628
- borderColor: 'blue'
1629
2372
  }
1630
2373
  ];
1631
2374
  }
1632
2375
 
1633
- if (ultraCompact) {
2376
+ if (ultraCompact && !splitPreviewLayout) {
1634
2377
  if (previewOpen) {
1635
2378
  panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === 'current-run' || panel.key === 'preview'));
1636
2379
  } else {
@@ -1640,8 +2383,83 @@ function createDashboardApp(deps) {
1640
2383
 
1641
2384
  const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
1642
2385
  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}`;
2386
+ const sectionHint = ui.view === 'runs'
2387
+ ? ' | a active | f files | p pending'
2388
+ : (ui.view === 'overview' ? ' | i intents | c completed | s standards | p project | n/x intent filter' : ' | t stats | w warnings | e errors');
2389
+ const previewHint = ui.view === 'runs'
2390
+ ? (previewOpen
2391
+ ? ` | tab ${paneFocus === 'preview' ? 'main' : 'preview'} | ↑/↓ ${paneFocus === 'preview' ? 'scroll' : 'navigate'} | v close | vv fullscreen`
2392
+ : ' | ↑/↓ navigate | enter expand | v preview | vv fullscreen | o open')
2393
+ : '';
2394
+ const helpText = `q quit | r refresh | h/? help | 1 runs | 2 overview | 3 health | g/G section${sectionHint}${previewHint}${flowSwitchHint}`;
2395
+
2396
+ const renderPanel = (panel, index, width, isFocused) => React.createElement(SectionPanel, {
2397
+ key: panel.key,
2398
+ title: panel.title,
2399
+ lines: panel.lines,
2400
+ width,
2401
+ maxLines: panel.maxLines,
2402
+ borderColor: panel.borderColor,
2403
+ marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
2404
+ dense: densePanels,
2405
+ focused: isFocused
2406
+ });
2407
+
2408
+ let contentNode;
2409
+ if (splitPreviewLayout && ui.view === 'runs' && !overlayPreviewOpen) {
2410
+ const previewPanel = {
2411
+ key: 'preview-split',
2412
+ title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
2413
+ lines: previewLines,
2414
+ borderColor: 'magenta',
2415
+ maxLines: Math.max(4, contentRowsBudget)
2416
+ };
2417
+
2418
+ contentNode = React.createElement(
2419
+ Box,
2420
+ { width: fullWidth, flexDirection: 'row' },
2421
+ React.createElement(
2422
+ Box,
2423
+ { width: mainPaneWidth, flexDirection: 'column' },
2424
+ ...panels.map((panel, index) => React.createElement(SectionPanel, {
2425
+ key: panel.key,
2426
+ title: panel.title,
2427
+ lines: panel.lines,
2428
+ width: mainPaneWidth,
2429
+ maxLines: panel.maxLines,
2430
+ borderColor: panel.borderColor,
2431
+ marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
2432
+ dense: densePanels,
2433
+ focused: paneFocus === 'main' && panel.key === focusedSection
2434
+ }))
2435
+ ),
2436
+ React.createElement(Box, { width: 1 }, React.createElement(Text, null, ' ')),
2437
+ React.createElement(
2438
+ Box,
2439
+ { width: previewPaneWidth, flexDirection: 'column' },
2440
+ React.createElement(SectionPanel, {
2441
+ key: previewPanel.key,
2442
+ title: previewPanel.title,
2443
+ lines: previewPanel.lines,
2444
+ width: previewPaneWidth,
2445
+ maxLines: previewPanel.maxLines,
2446
+ borderColor: previewPanel.borderColor,
2447
+ marginBottom: 0,
2448
+ dense: densePanels,
2449
+ focused: paneFocus === 'preview'
2450
+ })
2451
+ )
2452
+ );
2453
+ } else {
2454
+ contentNode = panels.map((panel, index) => renderPanel(
2455
+ panel,
2456
+ index,
2457
+ fullWidth,
2458
+ (panel.key === 'preview' || panel.key === 'preview-overlay')
2459
+ ? paneFocus === 'preview'
2460
+ : (paneFocus === 'main' && panel.key === focusedSection)
2461
+ ));
2462
+ }
1645
2463
 
1646
2464
  return React.createElement(
1647
2465
  Box,
@@ -1655,24 +2473,19 @@ function createDashboardApp(deps) {
1655
2473
  showErrorPanel
1656
2474
  ? React.createElement(SectionPanel, {
1657
2475
  title: 'Errors',
1658
- lines: buildErrorLines(error, compactWidth),
2476
+ lines: buildErrorLines(error, Math.max(18, fullWidth - 4)),
1659
2477
  width: fullWidth,
1660
2478
  maxLines: 2,
1661
2479
  borderColor: 'red',
1662
2480
  marginBottom: densePanels ? 0 : 1,
1663
- dense: densePanels
2481
+ dense: densePanels,
2482
+ focused: paneFocus === 'main' && focusedSection === 'error-details'
1664
2483
  })
1665
2484
  : 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
- })),
2485
+ ...(Array.isArray(contentNode) ? contentNode : [contentNode]),
2486
+ statusLine !== ''
2487
+ ? React.createElement(Text, { color: 'yellow' }, truncate(statusLine, fullWidth))
2488
+ : null,
1676
2489
  showHelpLine
1677
2490
  ? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
1678
2491
  : null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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": {