specsmd 0.1.46 → 0.1.50

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.
@@ -206,11 +206,11 @@ function buildShortStats(snapshot, flow) {
206
206
  return `runs ${stats.activeRunsCount || 0}/${stats.completedRuns || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | work ${stats.completedWorkItems || 0}/${stats.totalWorkItems || 0}`;
207
207
  }
208
208
 
209
- function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, width) {
209
+ function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, width, worktreeLabel = null) {
210
210
  const projectName = snapshot?.project?.name || 'Unnamed project';
211
211
  const shortStats = buildShortStats(snapshot, flow);
212
-
213
- const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view} | ${formatTime(lastRefreshAt)}`;
212
+ const worktreeSegment = worktreeLabel ? ` | wt:${worktreeLabel}` : '';
213
+ const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'}${worktreeSegment} | ${view} | ${formatTime(lastRefreshAt)}`;
214
214
 
215
215
  return truncate(line, width);
216
216
  }
@@ -1102,7 +1102,8 @@ function getPanelTitles(flow, snapshot) {
1102
1102
  current: 'Current Bolt',
1103
1103
  files: 'Bolt Files',
1104
1104
  pending: 'Queued Bolts',
1105
- completed: 'Recent Completed Bolts'
1105
+ completed: 'Recent Completed Bolts',
1106
+ otherWorktrees: 'Other Worktrees: Active Bolts'
1106
1107
  };
1107
1108
  }
1108
1109
  if (effectiveFlow === 'simple') {
@@ -1110,18 +1111,186 @@ function getPanelTitles(flow, snapshot) {
1110
1111
  current: 'Current Spec',
1111
1112
  files: 'Spec Files',
1112
1113
  pending: 'Pending Specs',
1113
- completed: 'Recent Completed Specs'
1114
+ completed: 'Recent Completed Specs',
1115
+ otherWorktrees: 'Other Worktrees: Active Specs'
1114
1116
  };
1115
1117
  }
1116
1118
  return {
1117
1119
  current: 'Current Run',
1118
1120
  files: 'Run Files',
1119
1121
  pending: 'Pending Queue',
1120
- completed: 'Recent Completed Runs'
1122
+ completed: 'Recent Completed Runs',
1123
+ otherWorktrees: 'Other Worktrees: Active Runs'
1121
1124
  };
1122
1125
  }
1123
1126
 
1124
- function getSectionOrderForView(view) {
1127
+ function getDashboardWorktreeMeta(snapshot) {
1128
+ if (!snapshot || typeof snapshot !== 'object') {
1129
+ return null;
1130
+ }
1131
+ const meta = snapshot.dashboardWorktrees;
1132
+ if (!meta || typeof meta !== 'object') {
1133
+ return null;
1134
+ }
1135
+ const items = Array.isArray(meta.items) ? meta.items : [];
1136
+ if (items.length === 0) {
1137
+ return null;
1138
+ }
1139
+ return {
1140
+ ...meta,
1141
+ items
1142
+ };
1143
+ }
1144
+
1145
+ function getWorktreeItems(snapshot) {
1146
+ return getDashboardWorktreeMeta(snapshot)?.items || [];
1147
+ }
1148
+
1149
+ function getSelectedWorktree(snapshot) {
1150
+ const meta = getDashboardWorktreeMeta(snapshot);
1151
+ if (!meta) {
1152
+ return null;
1153
+ }
1154
+ return meta.items.find((item) => item.id === meta.selectedWorktreeId) || null;
1155
+ }
1156
+
1157
+ function hasMultipleWorktrees(snapshot) {
1158
+ return getWorktreeItems(snapshot).length > 1;
1159
+ }
1160
+
1161
+ function isSelectedWorktreeMain(snapshot) {
1162
+ const selected = getSelectedWorktree(snapshot);
1163
+ return Boolean(selected?.isMainBranch);
1164
+ }
1165
+
1166
+ function getWorktreeDisplayName(worktree) {
1167
+ if (!worktree || typeof worktree !== 'object') {
1168
+ return 'unknown';
1169
+ }
1170
+ if (typeof worktree.displayBranch === 'string' && worktree.displayBranch.trim() !== '') {
1171
+ return worktree.displayBranch;
1172
+ }
1173
+ if (typeof worktree.branch === 'string' && worktree.branch.trim() !== '') {
1174
+ return worktree.branch;
1175
+ }
1176
+ if (typeof worktree.name === 'string' && worktree.name.trim() !== '') {
1177
+ return worktree.name;
1178
+ }
1179
+ return path.basename(worktree.path || '') || 'unknown';
1180
+ }
1181
+
1182
+ function buildWorktreeRows(snapshot, flow) {
1183
+ const meta = getDashboardWorktreeMeta(snapshot);
1184
+ if (!meta) {
1185
+ return [{
1186
+ kind: 'info',
1187
+ key: 'worktrees:none',
1188
+ label: 'No git worktrees detected',
1189
+ selectable: false
1190
+ }];
1191
+ }
1192
+
1193
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1194
+ const entityLabel = effectiveFlow === 'aidlc'
1195
+ ? 'active bolts'
1196
+ : (effectiveFlow === 'simple' ? 'active specs' : 'active runs');
1197
+
1198
+ const rows = [];
1199
+ for (const item of meta.items) {
1200
+ const currentLabel = item.isSelected ? '[CURRENT] ' : '';
1201
+ const mainLabel = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
1202
+ const availabilityLabel = item.flowAvailable ? '' : ' (flow unavailable)';
1203
+ const statusLabel = item.status === 'loading'
1204
+ ? ' loading...'
1205
+ : (item.status === 'error' ? ' error' : ` ${item.activeCount || 0} ${entityLabel}`);
1206
+ const scopeLabel = item.name ? ` (${item.name})` : '';
1207
+
1208
+ rows.push({
1209
+ kind: 'info',
1210
+ key: `worktree:item:${item.id}`,
1211
+ label: `${currentLabel}${mainLabel}${getWorktreeDisplayName(item)}${scopeLabel}${availabilityLabel}${statusLabel}`,
1212
+ color: item.isSelected ? 'green' : (item.flowAvailable ? 'gray' : 'red'),
1213
+ bold: item.isSelected,
1214
+ selectable: true
1215
+ });
1216
+ }
1217
+
1218
+ return rows;
1219
+ }
1220
+
1221
+ function buildOtherWorktreeActiveGroups(snapshot, flow) {
1222
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1223
+ if (effectiveFlow === 'simple') {
1224
+ return [];
1225
+ }
1226
+
1227
+ const meta = getDashboardWorktreeMeta(snapshot);
1228
+ if (!meta) {
1229
+ return [];
1230
+ }
1231
+
1232
+ const selectedWorktree = getSelectedWorktree(snapshot);
1233
+ if (!selectedWorktree || !selectedWorktree.isMainBranch) {
1234
+ return [];
1235
+ }
1236
+
1237
+ const groups = [];
1238
+ const otherItems = meta.items.filter((item) => item.id !== meta.selectedWorktreeId);
1239
+ for (const item of otherItems) {
1240
+ if (!item.flowAvailable || item.status === 'unavailable' || item.status === 'error') {
1241
+ continue;
1242
+ }
1243
+
1244
+ if (effectiveFlow === 'aidlc') {
1245
+ const activeBolts = Array.isArray(item.activity?.activeBolts) ? item.activity.activeBolts : [];
1246
+ for (const bolt of activeBolts) {
1247
+ const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
1248
+ const completedStages = stages.filter((stage) => stage?.status === 'completed').length;
1249
+ groups.push({
1250
+ key: `other:wt:${item.id}:bolt:${bolt.id}`,
1251
+ label: `[WT ${getWorktreeDisplayName(item)}] ${bolt.id} [${bolt.type || 'bolt'}] ${completedStages}/${stages.length || 0} stages`,
1252
+ files: collectAidlcBoltFiles(bolt).map((file) => ({ ...file, scope: 'active' }))
1253
+ });
1254
+ }
1255
+ continue;
1256
+ }
1257
+
1258
+ const activeRuns = Array.isArray(item.activity?.activeRuns) ? item.activity.activeRuns : [];
1259
+ for (const run of activeRuns) {
1260
+ const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
1261
+ const completed = workItems.filter((workItem) => workItem?.status === 'completed').length;
1262
+ groups.push({
1263
+ key: `other:wt:${item.id}:run:${run.id}`,
1264
+ label: `[WT ${getWorktreeDisplayName(item)}] ${run.id} [${run.scope || 'single'}] ${completed}/${workItems.length} items`,
1265
+ files: collectFireRunFiles(run).map((file) => ({ ...file, scope: 'active' }))
1266
+ });
1267
+ }
1268
+ }
1269
+
1270
+ return groups;
1271
+ }
1272
+
1273
+ function getOtherWorktreeEmptyMessage(snapshot, flow) {
1274
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
1275
+ if (!hasMultipleWorktrees(snapshot)) {
1276
+ return 'No additional worktrees';
1277
+ }
1278
+ if (!isSelectedWorktreeMain(snapshot)) {
1279
+ return 'Switch to main worktree to view active items from other worktrees';
1280
+ }
1281
+ if (effectiveFlow === 'aidlc') {
1282
+ return 'No active bolts in other worktrees';
1283
+ }
1284
+ if (effectiveFlow === 'simple') {
1285
+ return 'No active specs in other worktrees';
1286
+ }
1287
+ return 'No active runs in other worktrees';
1288
+ }
1289
+
1290
+ function getSectionOrderForView(view, options = {}) {
1291
+ const includeWorktrees = options.includeWorktrees === true;
1292
+ const includeOtherWorktrees = options.includeOtherWorktrees === true;
1293
+
1125
1294
  if (view === 'intents') {
1126
1295
  return ['intent-status'];
1127
1296
  }
@@ -1131,7 +1300,15 @@ function getSectionOrderForView(view) {
1131
1300
  if (view === 'health') {
1132
1301
  return ['standards', 'stats', 'warnings', 'error-details'];
1133
1302
  }
1134
- return ['current-run', 'run-files'];
1303
+ const sections = [];
1304
+ if (includeWorktrees) {
1305
+ sections.push('worktrees');
1306
+ }
1307
+ sections.push('current-run', 'run-files');
1308
+ if (includeOtherWorktrees) {
1309
+ sections.push('other-worktrees-active');
1310
+ }
1311
+ return sections;
1135
1312
  }
1136
1313
 
1137
1314
  function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
@@ -2131,6 +2308,20 @@ function rowToFileEntry(row) {
2131
2308
  };
2132
2309
  }
2133
2310
 
2311
+ function rowToWorktreeId(row) {
2312
+ if (!row || typeof row.key !== 'string') {
2313
+ return null;
2314
+ }
2315
+
2316
+ const prefix = 'worktree:item:';
2317
+ if (!row.key.startsWith(prefix)) {
2318
+ return null;
2319
+ }
2320
+
2321
+ const worktreeId = row.key.slice(prefix.length).trim();
2322
+ return worktreeId === '' ? null : worktreeId;
2323
+ }
2324
+
2134
2325
  function moveRowSelection(rows, currentIndex, direction) {
2135
2326
  if (!Array.isArray(rows) || rows.length === 0) {
2136
2327
  return 0;
@@ -2203,7 +2394,8 @@ function buildQuickHelpText(view, options = {}) {
2203
2394
  const {
2204
2395
  flow = 'fire',
2205
2396
  previewOpen = false,
2206
- availableFlowCount = 1
2397
+ availableFlowCount = 1,
2398
+ hasWorktrees = false
2207
2399
  } = options;
2208
2400
  const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
2209
2401
  const isSimple = String(flow || '').toLowerCase() === 'simple';
@@ -2219,7 +2411,13 @@ function buildQuickHelpText(view, options = {}) {
2219
2411
  }
2220
2412
  }
2221
2413
  if (view === 'runs') {
2414
+ if (hasWorktrees) {
2415
+ parts.push('b worktrees', 'u others');
2416
+ }
2222
2417
  parts.push('a current', 'f files');
2418
+ if (hasWorktrees) {
2419
+ parts.push('w worktree');
2420
+ }
2223
2421
  }
2224
2422
  parts.push(`tab1 ${activeLabel}`);
2225
2423
 
@@ -2238,7 +2436,8 @@ function buildHelpOverlayLines(options = {}) {
2238
2436
  previewOpen = false,
2239
2437
  paneFocus = 'main',
2240
2438
  availableFlowCount = 1,
2241
- showErrorSection = false
2439
+ showErrorSection = false,
2440
+ hasWorktrees = false
2242
2441
  } = options;
2243
2442
  const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
2244
2443
  const isSimple = String(flow || '').toLowerCase() === 'simple';
@@ -2255,8 +2454,10 @@ function buildHelpOverlayLines(options = {}) {
2255
2454
  'esc close overlays (help/preview/fullscreen)',
2256
2455
  { text: '', color: undefined, bold: false },
2257
2456
  { text: 'Tab 1 Active', color: 'yellow', bold: true },
2457
+ ...(hasWorktrees ? ['b focus worktrees section', 'u focus other-worktrees section'] : []),
2258
2458
  `a focus active ${itemLabel}`,
2259
2459
  `f focus ${itemLabel} files`,
2460
+ ...(hasWorktrees ? ['w open worktree switcher'] : []),
2260
2461
  'up/down or j/k move selection',
2261
2462
  'enter expand/collapse selected folder row',
2262
2463
  'v or space preview selected file',
@@ -2292,6 +2493,53 @@ function buildHelpOverlayLines(options = {}) {
2292
2493
  return lines;
2293
2494
  }
2294
2495
 
2496
+ function buildWorktreeOverlayLines(snapshot, selectedIndex, width) {
2497
+ const meta = getDashboardWorktreeMeta(snapshot);
2498
+ if (!meta) {
2499
+ return [{
2500
+ text: truncate('No worktrees available', width),
2501
+ color: 'gray',
2502
+ bold: false
2503
+ }];
2504
+ }
2505
+
2506
+ const items = Array.isArray(meta.items) ? meta.items : [];
2507
+ const clampedIndex = clampIndex(selectedIndex, items.length || 1);
2508
+ const lines = [{
2509
+ text: truncate('Use ↑/↓ and Enter to switch. Esc closes.', width),
2510
+ color: 'gray',
2511
+ bold: false
2512
+ }];
2513
+
2514
+ for (let index = 0; index < items.length; index += 1) {
2515
+ const item = items[index];
2516
+ const marker = index === clampedIndex ? '>' : ' ';
2517
+ const current = item.isSelected ? '[CURRENT] ' : '';
2518
+ const main = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
2519
+ const status = item.status === 'loading'
2520
+ ? 'loading'
2521
+ : (item.status === 'error'
2522
+ ? 'error'
2523
+ : (item.flowAvailable ? `${item.activeCount || 0} active` : 'flow unavailable'));
2524
+ const pathLabel = item.path ? path.basename(item.path) : 'unknown';
2525
+ lines.push({
2526
+ text: truncate(`${marker} ${current}${main}${getWorktreeDisplayName(item)} (${pathLabel}) | ${status}`, width),
2527
+ color: index === clampedIndex ? 'green' : (item.isSelected ? 'cyan' : 'gray'),
2528
+ bold: index === clampedIndex || item.isSelected
2529
+ });
2530
+ }
2531
+
2532
+ if (meta.hasPendingScans) {
2533
+ lines.push({
2534
+ text: truncate('Background scan in progress for additional worktrees...', width),
2535
+ color: 'yellow',
2536
+ bold: false
2537
+ });
2538
+ }
2539
+
2540
+ return lines;
2541
+ }
2542
+
2295
2543
  function colorizeMarkdownLine(line, inCodeBlock) {
2296
2544
  const text = String(line ?? '');
2297
2545
 
@@ -2451,6 +2699,7 @@ function createDashboardApp(deps) {
2451
2699
  flow,
2452
2700
  availableFlows,
2453
2701
  resolveRootPathForFlow,
2702
+ resolveRootPathsForFlow,
2454
2703
  refreshMs,
2455
2704
  watchEnabled,
2456
2705
  initialSnapshot,
@@ -2660,8 +2909,10 @@ function createDashboardApp(deps) {
2660
2909
  health: 'standards'
2661
2910
  });
2662
2911
  const [selectionBySection, setSelectionBySection] = useState({
2912
+ worktrees: 0,
2663
2913
  'current-run': 0,
2664
2914
  'run-files': 0,
2915
+ 'other-worktrees-active': 0,
2665
2916
  'intent-status': 0,
2666
2917
  'completed-runs': 0,
2667
2918
  standards: 0,
@@ -2676,6 +2927,11 @@ function createDashboardApp(deps) {
2676
2927
  const [previewOpen, setPreviewOpen] = useState(false);
2677
2928
  const [paneFocus, setPaneFocus] = useState('main');
2678
2929
  const [overlayPreviewOpen, setOverlayPreviewOpen] = useState(false);
2930
+ const [worktreeOverlayOpen, setWorktreeOverlayOpen] = useState(false);
2931
+ const [worktreeOverlayIndex, setWorktreeOverlayIndex] = useState(0);
2932
+ const [selectedWorktreeId, setSelectedWorktreeId] = useState(
2933
+ initialSnapshot?.dashboardWorktrees?.selectedWorktreeId || null
2934
+ );
2679
2935
  const [previewScroll, setPreviewScroll] = useState(0);
2680
2936
  const [statusLine, setStatusLine] = useState('');
2681
2937
  const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
@@ -2685,9 +2941,9 @@ function createDashboardApp(deps) {
2685
2941
  rows: stdout?.rows || process.stdout.rows || 40
2686
2942
  }));
2687
2943
  const icons = resolveIconSet();
2688
- const parseSnapshotForActiveFlow = useCallback(async (flowId) => {
2944
+ const parseSnapshotForActiveFlow = useCallback(async (flowId, context = {}) => {
2689
2945
  if (typeof parseSnapshotForFlow === 'function') {
2690
- return parseSnapshotForFlow(flowId);
2946
+ return parseSnapshotForFlow(flowId, context);
2691
2947
  }
2692
2948
  if (typeof parseSnapshot === 'function') {
2693
2949
  return parseSnapshot();
@@ -2703,10 +2959,18 @@ function createDashboardApp(deps) {
2703
2959
 
2704
2960
  const previewVisibleRows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
2705
2961
  const showErrorPanelForSections = Boolean(error) && previewVisibleRows >= 18;
2962
+ const worktreeSectionEnabled = hasMultipleWorktrees(snapshot);
2963
+ const otherWorktreesSectionEnabled = worktreeSectionEnabled
2964
+ && isSelectedWorktreeMain(snapshot)
2965
+ && getEffectiveFlow(activeFlow, snapshot) !== 'simple';
2966
+
2706
2967
  const getAvailableSections = useCallback((viewId) => {
2707
- const base = getSectionOrderForView(viewId);
2968
+ const base = getSectionOrderForView(viewId, {
2969
+ includeWorktrees: worktreeSectionEnabled,
2970
+ includeOtherWorktrees: otherWorktreesSectionEnabled
2971
+ });
2708
2972
  return base.filter((sectionKey) => sectionKey !== 'error-details' || showErrorPanelForSections);
2709
- }, [showErrorPanelForSections]);
2973
+ }, [showErrorPanelForSections, worktreeSectionEnabled, otherWorktreesSectionEnabled]);
2710
2974
 
2711
2975
  const effectiveFlow = getEffectiveFlow(activeFlow, snapshot);
2712
2976
  const approvalGate = detectDashboardApprovalGate(snapshot, activeFlow);
@@ -2739,6 +3003,8 @@ function createDashboardApp(deps) {
2739
3003
  ...currentRunRowsBase
2740
3004
  ]
2741
3005
  : currentRunRowsBase;
3006
+ const worktreeRows = buildWorktreeRows(snapshot, activeFlow);
3007
+
2742
3008
  const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
2743
3009
  const runFileGroups = buildRunFileEntityGroups(snapshot, activeFlow, {
2744
3010
  includeBacklog: shouldHydrateSecondaryTabs
@@ -2754,6 +3020,12 @@ function createDashboardApp(deps) {
2754
3020
  getNoFileMessage(effectiveFlow),
2755
3021
  runFileExpandedGroups
2756
3022
  );
3023
+ const otherWorktreeGroups = buildOtherWorktreeActiveGroups(snapshot, activeFlow);
3024
+ const otherWorktreeRows = toExpandableRows(
3025
+ otherWorktreeGroups,
3026
+ getOtherWorktreeEmptyMessage(snapshot, activeFlow),
3027
+ expandedGroups
3028
+ );
2757
3029
  const intentRows = shouldHydrateSecondaryTabs
2758
3030
  ? [
2759
3031
  {
@@ -2804,8 +3076,10 @@ function createDashboardApp(deps) {
2804
3076
  : toLoadingRows('Loading error details...', 'error-loading');
2805
3077
 
2806
3078
  const rowsBySection = {
3079
+ worktrees: worktreeRows,
2807
3080
  'current-run': currentRunRows,
2808
3081
  'run-files': runFileRows,
3082
+ 'other-worktrees-active': otherWorktreeRows,
2809
3083
  'intent-status': intentRows,
2810
3084
  'completed-runs': completedRows,
2811
3085
  standards: standardsRows,
@@ -2813,6 +3087,12 @@ function createDashboardApp(deps) {
2813
3087
  warnings: warningsRows,
2814
3088
  'error-details': errorDetailsRows
2815
3089
  };
3090
+ const worktreeItems = getWorktreeItems(snapshot);
3091
+ const selectedWorktree = getSelectedWorktree(snapshot);
3092
+ const selectedWorktreeLabel = selectedWorktree ? getWorktreeDisplayName(selectedWorktree) : null;
3093
+ const worktreeWatchSignature = `${snapshot?.dashboardWorktrees?.selectedWorktreeId || ''}|${worktreeItems
3094
+ .map((item) => `${item.id}:${item.status}:${item.activeCount}:${item.flowAvailable ? '1' : '0'}`)
3095
+ .join(',')}`;
2816
3096
  const rowLengthSignature = Object.entries(rowsBySection)
2817
3097
  .map(([key, rowsForSection]) => `${key}:${Array.isArray(rowsForSection) ? rowsForSection.length : 0}`)
2818
3098
  .join('|');
@@ -2827,11 +3107,16 @@ function createDashboardApp(deps) {
2827
3107
  const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
2828
3108
  const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
2829
3109
 
2830
- const refresh = useCallback(async () => {
3110
+ const refresh = useCallback(async (overrideSelectedWorktreeId = null) => {
2831
3111
  const now = new Date().toISOString();
3112
+ const requestedWorktreeId = typeof overrideSelectedWorktreeId === 'string' && overrideSelectedWorktreeId.trim() !== ''
3113
+ ? overrideSelectedWorktreeId.trim()
3114
+ : selectedWorktreeId;
2832
3115
 
2833
3116
  try {
2834
- const result = await parseSnapshotForActiveFlow(activeFlow);
3117
+ const result = await parseSnapshotForActiveFlow(activeFlow, {
3118
+ selectedWorktreeId: requestedWorktreeId
3119
+ });
2835
3120
 
2836
3121
  if (result?.ok) {
2837
3122
  const nextSnapshot = result.snapshot
@@ -2845,6 +3130,11 @@ function createDashboardApp(deps) {
2845
3130
  setLastRefreshAt(now);
2846
3131
  }
2847
3132
 
3133
+ const nextSelectedWorktreeId = nextSnapshot?.dashboardWorktrees?.selectedWorktreeId;
3134
+ if (typeof nextSelectedWorktreeId === 'string' && nextSelectedWorktreeId !== '' && nextSelectedWorktreeId !== selectedWorktreeId) {
3135
+ setSelectedWorktreeId(nextSelectedWorktreeId);
3136
+ }
3137
+
2848
3138
  if (errorHashRef.current !== null) {
2849
3139
  errorHashRef.current = null;
2850
3140
  setError(null);
@@ -2874,7 +3164,41 @@ function createDashboardApp(deps) {
2874
3164
  setLastRefreshAt(now);
2875
3165
  }
2876
3166
  }
2877
- }, [activeFlow, parseSnapshotForActiveFlow, watchEnabled]);
3167
+ }, [activeFlow, parseSnapshotForActiveFlow, selectedWorktreeId, watchEnabled]);
3168
+
3169
+ const switchToWorktree = useCallback((nextWorktreeId, options = {}) => {
3170
+ const normalizedNextId = typeof nextWorktreeId === 'string' ? nextWorktreeId.trim() : '';
3171
+ if (normalizedNextId === '') {
3172
+ setStatusLine('No worktree selected.');
3173
+ return false;
3174
+ }
3175
+
3176
+ const nextItem = worktreeItems.find((item) => item.id === normalizedNextId);
3177
+ if (!nextItem) {
3178
+ setStatusLine('Selected worktree is no longer available.');
3179
+ return false;
3180
+ }
3181
+
3182
+ if (!nextItem.flowAvailable) {
3183
+ setStatusLine(`Flow is unavailable in worktree: ${getWorktreeDisplayName(nextItem)}`);
3184
+ return false;
3185
+ }
3186
+
3187
+ const changed = normalizedNextId !== selectedWorktreeId;
3188
+ setSelectedWorktreeId(normalizedNextId);
3189
+ setPreviewTarget(null);
3190
+ setPreviewOpen(false);
3191
+ setOverlayPreviewOpen(false);
3192
+ setPreviewScroll(0);
3193
+ setPaneFocus('main');
3194
+
3195
+ if (changed || options.forceRefresh) {
3196
+ setStatusLine(`Switched to worktree: ${getWorktreeDisplayName(nextItem)}`);
3197
+ void refresh(normalizedNextId);
3198
+ }
3199
+
3200
+ return true;
3201
+ }, [refresh, selectedWorktreeId, worktreeItems]);
2878
3202
 
2879
3203
  useInput((input, key) => {
2880
3204
  if ((key.ctrl && input === 'c') || input === 'q') {
@@ -2901,6 +3225,32 @@ function createDashboardApp(deps) {
2901
3225
  return;
2902
3226
  }
2903
3227
 
3228
+ if (worktreeOverlayOpen) {
3229
+ if (key.escape) {
3230
+ setWorktreeOverlayOpen(false);
3231
+ return;
3232
+ }
3233
+
3234
+ if (key.upArrow || input === 'k') {
3235
+ setWorktreeOverlayIndex((previous) => Math.max(0, previous - 1));
3236
+ return;
3237
+ }
3238
+
3239
+ if (key.downArrow || input === 'j') {
3240
+ setWorktreeOverlayIndex((previous) => Math.min(Math.max(0, worktreeItems.length - 1), previous + 1));
3241
+ return;
3242
+ }
3243
+
3244
+ if (key.return || key.enter) {
3245
+ const selectedOverlayItem = worktreeItems[clampIndex(worktreeOverlayIndex, worktreeItems.length || 1)];
3246
+ switchToWorktree(selectedOverlayItem?.id || '', { forceRefresh: true });
3247
+ setWorktreeOverlayOpen(false);
3248
+ return;
3249
+ }
3250
+
3251
+ return;
3252
+ }
3253
+
2904
3254
  if (input === '1') {
2905
3255
  setUi((previous) => ({ ...previous, view: 'runs' }));
2906
3256
  setPaneFocus('main');
@@ -2925,6 +3275,13 @@ function createDashboardApp(deps) {
2925
3275
  return;
2926
3276
  }
2927
3277
 
3278
+ if (ui.view === 'runs' && input === 'w' && worktreeSectionEnabled) {
3279
+ setWorktreeOverlayIndex(clampIndex(worktreeItems.findIndex((item) => item.id === selectedWorktreeId), worktreeItems.length || 1));
3280
+ setWorktreeOverlayOpen(true);
3281
+ setPaneFocus('main');
3282
+ return;
3283
+ }
3284
+
2928
3285
  if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
2929
3286
  snapshotHashRef.current = safeJsonHash(null);
2930
3287
  errorHashRef.current = null;
@@ -2938,8 +3295,10 @@ function createDashboardApp(deps) {
2938
3295
  return availableFlowIds[nextIndex];
2939
3296
  });
2940
3297
  setSelectionBySection({
3298
+ worktrees: 0,
2941
3299
  'current-run': 0,
2942
3300
  'run-files': 0,
3301
+ 'other-worktrees-active': 0,
2943
3302
  'intent-status': 0,
2944
3303
  'completed-runs': 0,
2945
3304
  standards: 0,
@@ -2958,6 +3317,7 @@ function createDashboardApp(deps) {
2958
3317
  setPreviewTarget(null);
2959
3318
  setPreviewOpen(false);
2960
3319
  setOverlayPreviewOpen(false);
3320
+ setWorktreeOverlayOpen(false);
2961
3321
  setPreviewScroll(0);
2962
3322
  setPaneFocus('main');
2963
3323
  return;
@@ -2976,8 +3336,10 @@ function createDashboardApp(deps) {
2976
3336
  return availableFlowIds[nextIndex];
2977
3337
  });
2978
3338
  setSelectionBySection({
3339
+ worktrees: 0,
2979
3340
  'current-run': 0,
2980
3341
  'run-files': 0,
3342
+ 'other-worktrees-active': 0,
2981
3343
  'intent-status': 0,
2982
3344
  'completed-runs': 0,
2983
3345
  standards: 0,
@@ -2996,6 +3358,7 @@ function createDashboardApp(deps) {
2996
3358
  setPreviewTarget(null);
2997
3359
  setPreviewOpen(false);
2998
3360
  setOverlayPreviewOpen(false);
3361
+ setWorktreeOverlayOpen(false);
2999
3362
  setPreviewScroll(0);
3000
3363
  setPaneFocus('main');
3001
3364
  return;
@@ -3045,6 +3408,11 @@ function createDashboardApp(deps) {
3045
3408
  }
3046
3409
 
3047
3410
  if (ui.view === 'runs') {
3411
+ if (input === 'b' && worktreeSectionEnabled) {
3412
+ setSectionFocus((previous) => ({ ...previous, runs: 'worktrees' }));
3413
+ setPaneFocus('main');
3414
+ return;
3415
+ }
3048
3416
  if (input === 'a') {
3049
3417
  setSectionFocus((previous) => ({ ...previous, runs: 'current-run' }));
3050
3418
  setPaneFocus('main');
@@ -3055,6 +3423,11 @@ function createDashboardApp(deps) {
3055
3423
  setPaneFocus('main');
3056
3424
  return;
3057
3425
  }
3426
+ if (input === 'u' && otherWorktreesSectionEnabled) {
3427
+ setSectionFocus((previous) => ({ ...previous, runs: 'other-worktrees-active' }));
3428
+ setPaneFocus('main');
3429
+ return;
3430
+ }
3058
3431
  } else if (ui.view === 'intents') {
3059
3432
  if (input === 'i') {
3060
3433
  setSectionFocus((previous) => ({ ...previous, intents: 'intent-status' }));
@@ -3126,6 +3499,15 @@ function createDashboardApp(deps) {
3126
3499
  ...previous,
3127
3500
  [targetSection]: nextIndex
3128
3501
  }));
3502
+
3503
+ if (targetSection === 'worktrees' && worktreeSectionEnabled) {
3504
+ const nextRow = getSelectedRow(targetRows, nextIndex);
3505
+ const nextWorktreeId = rowToWorktreeId(nextRow);
3506
+ if (nextWorktreeId && nextWorktreeId !== selectedWorktreeId) {
3507
+ switchToWorktree(nextWorktreeId);
3508
+ }
3509
+ }
3510
+
3129
3511
  return;
3130
3512
  }
3131
3513
 
@@ -3193,6 +3575,28 @@ function createDashboardApp(deps) {
3193
3575
  void refresh();
3194
3576
  }, [refresh]);
3195
3577
 
3578
+ useEffect(() => {
3579
+ const snapshotSelected = snapshot?.dashboardWorktrees?.selectedWorktreeId;
3580
+ if (typeof snapshotSelected !== 'string' || snapshotSelected === '') {
3581
+ return;
3582
+ }
3583
+ if (snapshotSelected !== selectedWorktreeId) {
3584
+ setSelectedWorktreeId(snapshotSelected);
3585
+ }
3586
+ }, [snapshot?.dashboardWorktrees?.selectedWorktreeId, selectedWorktreeId]);
3587
+
3588
+ useEffect(() => {
3589
+ if (!snapshot?.dashboardWorktrees?.hasPendingScans) {
3590
+ return undefined;
3591
+ }
3592
+ const timer = setTimeout(() => {
3593
+ void refresh();
3594
+ }, 350);
3595
+ return () => {
3596
+ clearTimeout(timer);
3597
+ };
3598
+ }, [snapshot?.dashboardWorktrees?.hasPendingScans, snapshot?.generatedAt, refresh]);
3599
+
3196
3600
  useEffect(() => {
3197
3601
  setSelectionBySection((previous) => {
3198
3602
  let changed = false;
@@ -3226,6 +3630,7 @@ function createDashboardApp(deps) {
3226
3630
 
3227
3631
  useEffect(() => {
3228
3632
  setPaneFocus('main');
3633
+ setWorktreeOverlayOpen(false);
3229
3634
  }, [ui.view]);
3230
3635
 
3231
3636
  useEffect(() => {
@@ -3295,12 +3700,17 @@ function createDashboardApp(deps) {
3295
3700
  return undefined;
3296
3701
  }
3297
3702
 
3298
- const watchRootPath = resolveRootPathForFlow
3703
+ const resolvedRootCandidates = typeof resolveRootPathsForFlow === 'function'
3704
+ ? resolveRootPathsForFlow(activeFlow, snapshot?.dashboardWorktrees, selectedWorktreeId)
3705
+ : null;
3706
+ const candidateRoots = Array.isArray(resolvedRootCandidates) ? resolvedRootCandidates : [];
3707
+ const fallbackRoot = resolveRootPathForFlow
3299
3708
  ? resolveRootPathForFlow(activeFlow)
3300
3709
  : (rootPath || `${workspacePath}/.specs-fire`);
3710
+ const watchRoots = candidateRoots.length > 0 ? candidateRoots : [fallbackRoot];
3301
3711
 
3302
3712
  const runtime = createWatchRuntime({
3303
- rootPath: watchRootPath,
3713
+ rootPaths: watchRoots,
3304
3714
  debounceMs: 200,
3305
3715
  onRefresh: () => {
3306
3716
  void refresh();
@@ -3329,7 +3739,7 @@ function createDashboardApp(deps) {
3329
3739
  clearInterval(interval);
3330
3740
  void runtime.close();
3331
3741
  };
3332
- }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
3742
+ }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, worktreeWatchSignature, selectedWorktreeId]);
3333
3743
 
3334
3744
  useEffect(() => {
3335
3745
  if (!stdout || typeof stdout.write !== 'function') {
@@ -3348,9 +3758,9 @@ function createDashboardApp(deps) {
3348
3758
  const showFlowBar = availableFlowIds.length > 1;
3349
3759
  const showFooterHelpLine = rows >= 10;
3350
3760
  const showErrorPanel = Boolean(error) && rows >= 18;
3351
- const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp;
3352
- const showErrorInline = Boolean(error) && !showErrorPanel;
3353
- const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp;
3761
+ const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp && !worktreeOverlayOpen;
3762
+ const showErrorInline = Boolean(error) && !showErrorPanel && !worktreeOverlayOpen;
3763
+ const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp && !worktreeOverlayOpen;
3354
3764
  const showStatusLine = statusLine !== '';
3355
3765
  const densePanels = rows <= 28 || cols <= 120;
3356
3766
 
@@ -3366,7 +3776,7 @@ function createDashboardApp(deps) {
3366
3776
  const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
3367
3777
  const ultraCompact = rows <= 14;
3368
3778
  const panelTitles = getPanelTitles(activeFlow, snapshot);
3369
- const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && cols >= 110 && rows >= 16;
3779
+ const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && !worktreeOverlayOpen && cols >= 110 && rows >= 16;
3370
3780
  const mainPaneWidth = splitPreviewLayout
3371
3781
  ? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
3372
3782
  : fullWidth;
@@ -3401,13 +3811,15 @@ function createDashboardApp(deps) {
3401
3811
  previewOpen,
3402
3812
  paneFocus,
3403
3813
  availableFlowCount: availableFlowIds.length,
3404
- showErrorSection: showErrorPanel
3814
+ showErrorSection: showErrorPanel,
3815
+ hasWorktrees: worktreeSectionEnabled
3405
3816
  });
3406
3817
  const quickHelpText = buildQuickHelpText(ui.view, {
3407
3818
  flow: activeFlow,
3408
3819
  previewOpen,
3409
3820
  paneFocus,
3410
- availableFlowCount: availableFlowIds.length
3821
+ availableFlowCount: availableFlowIds.length,
3822
+ hasWorktrees: worktreeSectionEnabled
3411
3823
  });
3412
3824
 
3413
3825
  let panelCandidates;
@@ -3420,6 +3832,15 @@ function createDashboardApp(deps) {
3420
3832
  borderColor: 'cyan'
3421
3833
  }
3422
3834
  ];
3835
+ } else if (worktreeOverlayOpen) {
3836
+ panelCandidates = [
3837
+ {
3838
+ key: 'worktree-overlay',
3839
+ title: 'Switch Worktree',
3840
+ lines: buildWorktreeOverlayLines(snapshot, worktreeOverlayIndex, Math.max(18, fullWidth - 4)),
3841
+ borderColor: 'yellow'
3842
+ }
3843
+ ];
3423
3844
  } else if (previewOpen && overlayPreviewOpen) {
3424
3845
  panelCandidates = [
3425
3846
  {
@@ -3478,7 +3899,16 @@ function createDashboardApp(deps) {
3478
3899
  });
3479
3900
  }
3480
3901
  } else {
3481
- panelCandidates = [
3902
+ panelCandidates = [];
3903
+ if (worktreeSectionEnabled) {
3904
+ panelCandidates.push({
3905
+ key: 'worktrees',
3906
+ title: 'Worktrees',
3907
+ lines: sectionLines.worktrees,
3908
+ borderColor: 'magenta'
3909
+ });
3910
+ }
3911
+ panelCandidates.push(
3482
3912
  {
3483
3913
  key: 'current-run',
3484
3914
  title: panelTitles.current,
@@ -3491,7 +3921,15 @@ function createDashboardApp(deps) {
3491
3921
  lines: sectionLines['run-files'],
3492
3922
  borderColor: 'yellow'
3493
3923
  }
3494
- ];
3924
+ );
3925
+ if (otherWorktreesSectionEnabled) {
3926
+ panelCandidates.push({
3927
+ key: 'other-worktrees-active',
3928
+ title: panelTitles.otherWorktrees,
3929
+ lines: sectionLines['other-worktrees-active'],
3930
+ borderColor: 'blue'
3931
+ });
3932
+ }
3495
3933
  }
3496
3934
 
3497
3935
  if (!ui.showHelp && previewOpen && !overlayPreviewOpen && !splitPreviewLayout) {
@@ -3577,7 +4015,7 @@ function createDashboardApp(deps) {
3577
4015
  panel,
3578
4016
  index,
3579
4017
  fullWidth,
3580
- ui.showHelp
4018
+ (ui.showHelp || worktreeOverlayOpen)
3581
4019
  ? true
3582
4020
  : ((panel.key === 'preview' || panel.key === 'preview-overlay')
3583
4021
  ? paneFocus === 'preview'
@@ -3588,7 +4026,11 @@ function createDashboardApp(deps) {
3588
4026
  return React.createElement(
3589
4027
  Box,
3590
4028
  { flexDirection: 'column', width: fullWidth },
3591
- React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
4029
+ React.createElement(
4030
+ Text,
4031
+ { color: 'cyan' },
4032
+ buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth, selectedWorktreeLabel)
4033
+ ),
3592
4034
  React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
3593
4035
  React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons, flow: activeFlow }),
3594
4036
  showApprovalBanner