specsmd 0.1.46 → 0.1.47

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'
1124
+ };
1125
+ }
1126
+
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
1121
1142
  };
1122
1143
  }
1123
1144
 
1124
- function getSectionOrderForView(view) {
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) {
@@ -2203,7 +2380,8 @@ function buildQuickHelpText(view, options = {}) {
2203
2380
  const {
2204
2381
  flow = 'fire',
2205
2382
  previewOpen = false,
2206
- availableFlowCount = 1
2383
+ availableFlowCount = 1,
2384
+ hasWorktrees = false
2207
2385
  } = options;
2208
2386
  const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
2209
2387
  const isSimple = String(flow || '').toLowerCase() === 'simple';
@@ -2219,7 +2397,13 @@ function buildQuickHelpText(view, options = {}) {
2219
2397
  }
2220
2398
  }
2221
2399
  if (view === 'runs') {
2400
+ if (hasWorktrees) {
2401
+ parts.push('b worktrees', 'u others');
2402
+ }
2222
2403
  parts.push('a current', 'f files');
2404
+ if (hasWorktrees) {
2405
+ parts.push('w worktree');
2406
+ }
2223
2407
  }
2224
2408
  parts.push(`tab1 ${activeLabel}`);
2225
2409
 
@@ -2238,7 +2422,8 @@ function buildHelpOverlayLines(options = {}) {
2238
2422
  previewOpen = false,
2239
2423
  paneFocus = 'main',
2240
2424
  availableFlowCount = 1,
2241
- showErrorSection = false
2425
+ showErrorSection = false,
2426
+ hasWorktrees = false
2242
2427
  } = options;
2243
2428
  const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
2244
2429
  const isSimple = String(flow || '').toLowerCase() === 'simple';
@@ -2255,8 +2440,10 @@ function buildHelpOverlayLines(options = {}) {
2255
2440
  'esc close overlays (help/preview/fullscreen)',
2256
2441
  { text: '', color: undefined, bold: false },
2257
2442
  { text: 'Tab 1 Active', color: 'yellow', bold: true },
2443
+ ...(hasWorktrees ? ['b focus worktrees section', 'u focus other-worktrees section'] : []),
2258
2444
  `a focus active ${itemLabel}`,
2259
2445
  `f focus ${itemLabel} files`,
2446
+ ...(hasWorktrees ? ['w open worktree switcher'] : []),
2260
2447
  'up/down or j/k move selection',
2261
2448
  'enter expand/collapse selected folder row',
2262
2449
  'v or space preview selected file',
@@ -2292,6 +2479,53 @@ function buildHelpOverlayLines(options = {}) {
2292
2479
  return lines;
2293
2480
  }
2294
2481
 
2482
+ function buildWorktreeOverlayLines(snapshot, selectedIndex, width) {
2483
+ const meta = getDashboardWorktreeMeta(snapshot);
2484
+ if (!meta) {
2485
+ return [{
2486
+ text: truncate('No worktrees available', width),
2487
+ color: 'gray',
2488
+ bold: false
2489
+ }];
2490
+ }
2491
+
2492
+ const items = Array.isArray(meta.items) ? meta.items : [];
2493
+ const clampedIndex = clampIndex(selectedIndex, items.length || 1);
2494
+ const lines = [{
2495
+ text: truncate('Use ↑/↓ and Enter to switch. Esc closes.', width),
2496
+ color: 'gray',
2497
+ bold: false
2498
+ }];
2499
+
2500
+ for (let index = 0; index < items.length; index += 1) {
2501
+ const item = items[index];
2502
+ const marker = index === clampedIndex ? '>' : ' ';
2503
+ const current = item.isSelected ? '[CURRENT] ' : '';
2504
+ const main = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
2505
+ const status = item.status === 'loading'
2506
+ ? 'loading'
2507
+ : (item.status === 'error'
2508
+ ? 'error'
2509
+ : (item.flowAvailable ? `${item.activeCount || 0} active` : 'flow unavailable'));
2510
+ const pathLabel = item.path ? path.basename(item.path) : 'unknown';
2511
+ lines.push({
2512
+ text: truncate(`${marker} ${current}${main}${getWorktreeDisplayName(item)} (${pathLabel}) | ${status}`, width),
2513
+ color: index === clampedIndex ? 'green' : (item.isSelected ? 'cyan' : 'gray'),
2514
+ bold: index === clampedIndex || item.isSelected
2515
+ });
2516
+ }
2517
+
2518
+ if (meta.hasPendingScans) {
2519
+ lines.push({
2520
+ text: truncate('Background scan in progress for additional worktrees...', width),
2521
+ color: 'yellow',
2522
+ bold: false
2523
+ });
2524
+ }
2525
+
2526
+ return lines;
2527
+ }
2528
+
2295
2529
  function colorizeMarkdownLine(line, inCodeBlock) {
2296
2530
  const text = String(line ?? '');
2297
2531
 
@@ -2451,6 +2685,7 @@ function createDashboardApp(deps) {
2451
2685
  flow,
2452
2686
  availableFlows,
2453
2687
  resolveRootPathForFlow,
2688
+ resolveRootPathsForFlow,
2454
2689
  refreshMs,
2455
2690
  watchEnabled,
2456
2691
  initialSnapshot,
@@ -2660,8 +2895,10 @@ function createDashboardApp(deps) {
2660
2895
  health: 'standards'
2661
2896
  });
2662
2897
  const [selectionBySection, setSelectionBySection] = useState({
2898
+ worktrees: 0,
2663
2899
  'current-run': 0,
2664
2900
  'run-files': 0,
2901
+ 'other-worktrees-active': 0,
2665
2902
  'intent-status': 0,
2666
2903
  'completed-runs': 0,
2667
2904
  standards: 0,
@@ -2676,6 +2913,11 @@ function createDashboardApp(deps) {
2676
2913
  const [previewOpen, setPreviewOpen] = useState(false);
2677
2914
  const [paneFocus, setPaneFocus] = useState('main');
2678
2915
  const [overlayPreviewOpen, setOverlayPreviewOpen] = useState(false);
2916
+ const [worktreeOverlayOpen, setWorktreeOverlayOpen] = useState(false);
2917
+ const [worktreeOverlayIndex, setWorktreeOverlayIndex] = useState(0);
2918
+ const [selectedWorktreeId, setSelectedWorktreeId] = useState(
2919
+ initialSnapshot?.dashboardWorktrees?.selectedWorktreeId || null
2920
+ );
2679
2921
  const [previewScroll, setPreviewScroll] = useState(0);
2680
2922
  const [statusLine, setStatusLine] = useState('');
2681
2923
  const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
@@ -2685,9 +2927,9 @@ function createDashboardApp(deps) {
2685
2927
  rows: stdout?.rows || process.stdout.rows || 40
2686
2928
  }));
2687
2929
  const icons = resolveIconSet();
2688
- const parseSnapshotForActiveFlow = useCallback(async (flowId) => {
2930
+ const parseSnapshotForActiveFlow = useCallback(async (flowId, context = {}) => {
2689
2931
  if (typeof parseSnapshotForFlow === 'function') {
2690
- return parseSnapshotForFlow(flowId);
2932
+ return parseSnapshotForFlow(flowId, context);
2691
2933
  }
2692
2934
  if (typeof parseSnapshot === 'function') {
2693
2935
  return parseSnapshot();
@@ -2703,10 +2945,18 @@ function createDashboardApp(deps) {
2703
2945
 
2704
2946
  const previewVisibleRows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
2705
2947
  const showErrorPanelForSections = Boolean(error) && previewVisibleRows >= 18;
2948
+ const worktreeSectionEnabled = hasMultipleWorktrees(snapshot);
2949
+ const otherWorktreesSectionEnabled = worktreeSectionEnabled
2950
+ && isSelectedWorktreeMain(snapshot)
2951
+ && getEffectiveFlow(activeFlow, snapshot) !== 'simple';
2952
+
2706
2953
  const getAvailableSections = useCallback((viewId) => {
2707
- const base = getSectionOrderForView(viewId);
2954
+ const base = getSectionOrderForView(viewId, {
2955
+ includeWorktrees: worktreeSectionEnabled,
2956
+ includeOtherWorktrees: otherWorktreesSectionEnabled
2957
+ });
2708
2958
  return base.filter((sectionKey) => sectionKey !== 'error-details' || showErrorPanelForSections);
2709
- }, [showErrorPanelForSections]);
2959
+ }, [showErrorPanelForSections, worktreeSectionEnabled, otherWorktreesSectionEnabled]);
2710
2960
 
2711
2961
  const effectiveFlow = getEffectiveFlow(activeFlow, snapshot);
2712
2962
  const approvalGate = detectDashboardApprovalGate(snapshot, activeFlow);
@@ -2739,6 +2989,8 @@ function createDashboardApp(deps) {
2739
2989
  ...currentRunRowsBase
2740
2990
  ]
2741
2991
  : currentRunRowsBase;
2992
+ const worktreeRows = buildWorktreeRows(snapshot, activeFlow);
2993
+
2742
2994
  const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
2743
2995
  const runFileGroups = buildRunFileEntityGroups(snapshot, activeFlow, {
2744
2996
  includeBacklog: shouldHydrateSecondaryTabs
@@ -2754,6 +3006,12 @@ function createDashboardApp(deps) {
2754
3006
  getNoFileMessage(effectiveFlow),
2755
3007
  runFileExpandedGroups
2756
3008
  );
3009
+ const otherWorktreeGroups = buildOtherWorktreeActiveGroups(snapshot, activeFlow);
3010
+ const otherWorktreeRows = toExpandableRows(
3011
+ otherWorktreeGroups,
3012
+ getOtherWorktreeEmptyMessage(snapshot, activeFlow),
3013
+ expandedGroups
3014
+ );
2757
3015
  const intentRows = shouldHydrateSecondaryTabs
2758
3016
  ? [
2759
3017
  {
@@ -2804,8 +3062,10 @@ function createDashboardApp(deps) {
2804
3062
  : toLoadingRows('Loading error details...', 'error-loading');
2805
3063
 
2806
3064
  const rowsBySection = {
3065
+ worktrees: worktreeRows,
2807
3066
  'current-run': currentRunRows,
2808
3067
  'run-files': runFileRows,
3068
+ 'other-worktrees-active': otherWorktreeRows,
2809
3069
  'intent-status': intentRows,
2810
3070
  'completed-runs': completedRows,
2811
3071
  standards: standardsRows,
@@ -2813,6 +3073,12 @@ function createDashboardApp(deps) {
2813
3073
  warnings: warningsRows,
2814
3074
  'error-details': errorDetailsRows
2815
3075
  };
3076
+ const worktreeItems = getWorktreeItems(snapshot);
3077
+ const selectedWorktree = getSelectedWorktree(snapshot);
3078
+ const selectedWorktreeLabel = selectedWorktree ? getWorktreeDisplayName(selectedWorktree) : null;
3079
+ const worktreeWatchSignature = `${snapshot?.dashboardWorktrees?.selectedWorktreeId || ''}|${worktreeItems
3080
+ .map((item) => `${item.id}:${item.status}:${item.activeCount}:${item.flowAvailable ? '1' : '0'}`)
3081
+ .join(',')}`;
2816
3082
  const rowLengthSignature = Object.entries(rowsBySection)
2817
3083
  .map(([key, rowsForSection]) => `${key}:${Array.isArray(rowsForSection) ? rowsForSection.length : 0}`)
2818
3084
  .join('|');
@@ -2831,7 +3097,9 @@ function createDashboardApp(deps) {
2831
3097
  const now = new Date().toISOString();
2832
3098
 
2833
3099
  try {
2834
- const result = await parseSnapshotForActiveFlow(activeFlow);
3100
+ const result = await parseSnapshotForActiveFlow(activeFlow, {
3101
+ selectedWorktreeId
3102
+ });
2835
3103
 
2836
3104
  if (result?.ok) {
2837
3105
  const nextSnapshot = result.snapshot
@@ -2845,6 +3113,11 @@ function createDashboardApp(deps) {
2845
3113
  setLastRefreshAt(now);
2846
3114
  }
2847
3115
 
3116
+ const nextSelectedWorktreeId = nextSnapshot?.dashboardWorktrees?.selectedWorktreeId;
3117
+ if (typeof nextSelectedWorktreeId === 'string' && nextSelectedWorktreeId !== '' && nextSelectedWorktreeId !== selectedWorktreeId) {
3118
+ setSelectedWorktreeId(nextSelectedWorktreeId);
3119
+ }
3120
+
2848
3121
  if (errorHashRef.current !== null) {
2849
3122
  errorHashRef.current = null;
2850
3123
  setError(null);
@@ -2874,7 +3147,7 @@ function createDashboardApp(deps) {
2874
3147
  setLastRefreshAt(now);
2875
3148
  }
2876
3149
  }
2877
- }, [activeFlow, parseSnapshotForActiveFlow, watchEnabled]);
3150
+ }, [activeFlow, parseSnapshotForActiveFlow, selectedWorktreeId, watchEnabled]);
2878
3151
 
2879
3152
  useInput((input, key) => {
2880
3153
  if ((key.ctrl && input === 'c') || input === 'q') {
@@ -2901,6 +3174,44 @@ function createDashboardApp(deps) {
2901
3174
  return;
2902
3175
  }
2903
3176
 
3177
+ if (worktreeOverlayOpen) {
3178
+ if (key.escape) {
3179
+ setWorktreeOverlayOpen(false);
3180
+ return;
3181
+ }
3182
+
3183
+ if (key.upArrow || input === 'k') {
3184
+ setWorktreeOverlayIndex((previous) => Math.max(0, previous - 1));
3185
+ return;
3186
+ }
3187
+
3188
+ if (key.downArrow || input === 'j') {
3189
+ setWorktreeOverlayIndex((previous) => Math.min(Math.max(0, worktreeItems.length - 1), previous + 1));
3190
+ return;
3191
+ }
3192
+
3193
+ if (key.return || key.enter) {
3194
+ const selectedOverlayItem = worktreeItems[clampIndex(worktreeOverlayIndex, worktreeItems.length || 1)];
3195
+ if (!selectedOverlayItem) {
3196
+ setStatusLine('No worktree selected.');
3197
+ setWorktreeOverlayOpen(false);
3198
+ return;
3199
+ }
3200
+ setSelectedWorktreeId(selectedOverlayItem.id);
3201
+ setWorktreeOverlayOpen(false);
3202
+ setPreviewTarget(null);
3203
+ setPreviewOpen(false);
3204
+ setOverlayPreviewOpen(false);
3205
+ setPreviewScroll(0);
3206
+ setPaneFocus('main');
3207
+ setStatusLine(`Switched to worktree: ${getWorktreeDisplayName(selectedOverlayItem)}`);
3208
+ void refresh();
3209
+ return;
3210
+ }
3211
+
3212
+ return;
3213
+ }
3214
+
2904
3215
  if (input === '1') {
2905
3216
  setUi((previous) => ({ ...previous, view: 'runs' }));
2906
3217
  setPaneFocus('main');
@@ -2925,6 +3236,13 @@ function createDashboardApp(deps) {
2925
3236
  return;
2926
3237
  }
2927
3238
 
3239
+ if (ui.view === 'runs' && input === 'w' && worktreeSectionEnabled) {
3240
+ setWorktreeOverlayIndex(clampIndex(worktreeItems.findIndex((item) => item.id === selectedWorktreeId), worktreeItems.length || 1));
3241
+ setWorktreeOverlayOpen(true);
3242
+ setPaneFocus('main');
3243
+ return;
3244
+ }
3245
+
2928
3246
  if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
2929
3247
  snapshotHashRef.current = safeJsonHash(null);
2930
3248
  errorHashRef.current = null;
@@ -2938,8 +3256,10 @@ function createDashboardApp(deps) {
2938
3256
  return availableFlowIds[nextIndex];
2939
3257
  });
2940
3258
  setSelectionBySection({
3259
+ worktrees: 0,
2941
3260
  'current-run': 0,
2942
3261
  'run-files': 0,
3262
+ 'other-worktrees-active': 0,
2943
3263
  'intent-status': 0,
2944
3264
  'completed-runs': 0,
2945
3265
  standards: 0,
@@ -2958,6 +3278,7 @@ function createDashboardApp(deps) {
2958
3278
  setPreviewTarget(null);
2959
3279
  setPreviewOpen(false);
2960
3280
  setOverlayPreviewOpen(false);
3281
+ setWorktreeOverlayOpen(false);
2961
3282
  setPreviewScroll(0);
2962
3283
  setPaneFocus('main');
2963
3284
  return;
@@ -2976,8 +3297,10 @@ function createDashboardApp(deps) {
2976
3297
  return availableFlowIds[nextIndex];
2977
3298
  });
2978
3299
  setSelectionBySection({
3300
+ worktrees: 0,
2979
3301
  'current-run': 0,
2980
3302
  'run-files': 0,
3303
+ 'other-worktrees-active': 0,
2981
3304
  'intent-status': 0,
2982
3305
  'completed-runs': 0,
2983
3306
  standards: 0,
@@ -2996,6 +3319,7 @@ function createDashboardApp(deps) {
2996
3319
  setPreviewTarget(null);
2997
3320
  setPreviewOpen(false);
2998
3321
  setOverlayPreviewOpen(false);
3322
+ setWorktreeOverlayOpen(false);
2999
3323
  setPreviewScroll(0);
3000
3324
  setPaneFocus('main');
3001
3325
  return;
@@ -3045,6 +3369,11 @@ function createDashboardApp(deps) {
3045
3369
  }
3046
3370
 
3047
3371
  if (ui.view === 'runs') {
3372
+ if (input === 'b' && worktreeSectionEnabled) {
3373
+ setSectionFocus((previous) => ({ ...previous, runs: 'worktrees' }));
3374
+ setPaneFocus('main');
3375
+ return;
3376
+ }
3048
3377
  if (input === 'a') {
3049
3378
  setSectionFocus((previous) => ({ ...previous, runs: 'current-run' }));
3050
3379
  setPaneFocus('main');
@@ -3055,6 +3384,11 @@ function createDashboardApp(deps) {
3055
3384
  setPaneFocus('main');
3056
3385
  return;
3057
3386
  }
3387
+ if (input === 'u' && otherWorktreesSectionEnabled) {
3388
+ setSectionFocus((previous) => ({ ...previous, runs: 'other-worktrees-active' }));
3389
+ setPaneFocus('main');
3390
+ return;
3391
+ }
3058
3392
  } else if (ui.view === 'intents') {
3059
3393
  if (input === 'i') {
3060
3394
  setSectionFocus((previous) => ({ ...previous, intents: 'intent-status' }));
@@ -3193,6 +3527,28 @@ function createDashboardApp(deps) {
3193
3527
  void refresh();
3194
3528
  }, [refresh]);
3195
3529
 
3530
+ useEffect(() => {
3531
+ const snapshotSelected = snapshot?.dashboardWorktrees?.selectedWorktreeId;
3532
+ if (typeof snapshotSelected !== 'string' || snapshotSelected === '') {
3533
+ return;
3534
+ }
3535
+ if (snapshotSelected !== selectedWorktreeId) {
3536
+ setSelectedWorktreeId(snapshotSelected);
3537
+ }
3538
+ }, [snapshot?.dashboardWorktrees?.selectedWorktreeId, selectedWorktreeId]);
3539
+
3540
+ useEffect(() => {
3541
+ if (!snapshot?.dashboardWorktrees?.hasPendingScans) {
3542
+ return undefined;
3543
+ }
3544
+ const timer = setTimeout(() => {
3545
+ void refresh();
3546
+ }, 350);
3547
+ return () => {
3548
+ clearTimeout(timer);
3549
+ };
3550
+ }, [snapshot?.dashboardWorktrees?.hasPendingScans, snapshot?.generatedAt, refresh]);
3551
+
3196
3552
  useEffect(() => {
3197
3553
  setSelectionBySection((previous) => {
3198
3554
  let changed = false;
@@ -3226,6 +3582,7 @@ function createDashboardApp(deps) {
3226
3582
 
3227
3583
  useEffect(() => {
3228
3584
  setPaneFocus('main');
3585
+ setWorktreeOverlayOpen(false);
3229
3586
  }, [ui.view]);
3230
3587
 
3231
3588
  useEffect(() => {
@@ -3295,12 +3652,17 @@ function createDashboardApp(deps) {
3295
3652
  return undefined;
3296
3653
  }
3297
3654
 
3298
- const watchRootPath = resolveRootPathForFlow
3655
+ const resolvedRootCandidates = typeof resolveRootPathsForFlow === 'function'
3656
+ ? resolveRootPathsForFlow(activeFlow, snapshot?.dashboardWorktrees, selectedWorktreeId)
3657
+ : null;
3658
+ const candidateRoots = Array.isArray(resolvedRootCandidates) ? resolvedRootCandidates : [];
3659
+ const fallbackRoot = resolveRootPathForFlow
3299
3660
  ? resolveRootPathForFlow(activeFlow)
3300
3661
  : (rootPath || `${workspacePath}/.specs-fire`);
3662
+ const watchRoots = candidateRoots.length > 0 ? candidateRoots : [fallbackRoot];
3301
3663
 
3302
3664
  const runtime = createWatchRuntime({
3303
- rootPath: watchRootPath,
3665
+ rootPaths: watchRoots,
3304
3666
  debounceMs: 200,
3305
3667
  onRefresh: () => {
3306
3668
  void refresh();
@@ -3329,7 +3691,7 @@ function createDashboardApp(deps) {
3329
3691
  clearInterval(interval);
3330
3692
  void runtime.close();
3331
3693
  };
3332
- }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, activeFlow]);
3694
+ }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, worktreeWatchSignature, selectedWorktreeId]);
3333
3695
 
3334
3696
  useEffect(() => {
3335
3697
  if (!stdout || typeof stdout.write !== 'function') {
@@ -3348,9 +3710,9 @@ function createDashboardApp(deps) {
3348
3710
  const showFlowBar = availableFlowIds.length > 1;
3349
3711
  const showFooterHelpLine = rows >= 10;
3350
3712
  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;
3713
+ const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp && !worktreeOverlayOpen;
3714
+ const showErrorInline = Boolean(error) && !showErrorPanel && !worktreeOverlayOpen;
3715
+ const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp && !worktreeOverlayOpen;
3354
3716
  const showStatusLine = statusLine !== '';
3355
3717
  const densePanels = rows <= 28 || cols <= 120;
3356
3718
 
@@ -3366,7 +3728,7 @@ function createDashboardApp(deps) {
3366
3728
  const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
3367
3729
  const ultraCompact = rows <= 14;
3368
3730
  const panelTitles = getPanelTitles(activeFlow, snapshot);
3369
- const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && cols >= 110 && rows >= 16;
3731
+ const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && !worktreeOverlayOpen && cols >= 110 && rows >= 16;
3370
3732
  const mainPaneWidth = splitPreviewLayout
3371
3733
  ? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
3372
3734
  : fullWidth;
@@ -3401,13 +3763,15 @@ function createDashboardApp(deps) {
3401
3763
  previewOpen,
3402
3764
  paneFocus,
3403
3765
  availableFlowCount: availableFlowIds.length,
3404
- showErrorSection: showErrorPanel
3766
+ showErrorSection: showErrorPanel,
3767
+ hasWorktrees: worktreeSectionEnabled
3405
3768
  });
3406
3769
  const quickHelpText = buildQuickHelpText(ui.view, {
3407
3770
  flow: activeFlow,
3408
3771
  previewOpen,
3409
3772
  paneFocus,
3410
- availableFlowCount: availableFlowIds.length
3773
+ availableFlowCount: availableFlowIds.length,
3774
+ hasWorktrees: worktreeSectionEnabled
3411
3775
  });
3412
3776
 
3413
3777
  let panelCandidates;
@@ -3420,6 +3784,15 @@ function createDashboardApp(deps) {
3420
3784
  borderColor: 'cyan'
3421
3785
  }
3422
3786
  ];
3787
+ } else if (worktreeOverlayOpen) {
3788
+ panelCandidates = [
3789
+ {
3790
+ key: 'worktree-overlay',
3791
+ title: 'Switch Worktree',
3792
+ lines: buildWorktreeOverlayLines(snapshot, worktreeOverlayIndex, Math.max(18, fullWidth - 4)),
3793
+ borderColor: 'yellow'
3794
+ }
3795
+ ];
3423
3796
  } else if (previewOpen && overlayPreviewOpen) {
3424
3797
  panelCandidates = [
3425
3798
  {
@@ -3478,7 +3851,16 @@ function createDashboardApp(deps) {
3478
3851
  });
3479
3852
  }
3480
3853
  } else {
3481
- panelCandidates = [
3854
+ panelCandidates = [];
3855
+ if (worktreeSectionEnabled) {
3856
+ panelCandidates.push({
3857
+ key: 'worktrees',
3858
+ title: 'Worktrees',
3859
+ lines: sectionLines.worktrees,
3860
+ borderColor: 'magenta'
3861
+ });
3862
+ }
3863
+ panelCandidates.push(
3482
3864
  {
3483
3865
  key: 'current-run',
3484
3866
  title: panelTitles.current,
@@ -3491,7 +3873,15 @@ function createDashboardApp(deps) {
3491
3873
  lines: sectionLines['run-files'],
3492
3874
  borderColor: 'yellow'
3493
3875
  }
3494
- ];
3876
+ );
3877
+ if (otherWorktreesSectionEnabled) {
3878
+ panelCandidates.push({
3879
+ key: 'other-worktrees-active',
3880
+ title: panelTitles.otherWorktrees,
3881
+ lines: sectionLines['other-worktrees-active'],
3882
+ borderColor: 'blue'
3883
+ });
3884
+ }
3495
3885
  }
3496
3886
 
3497
3887
  if (!ui.showHelp && previewOpen && !overlayPreviewOpen && !splitPreviewLayout) {
@@ -3577,7 +3967,7 @@ function createDashboardApp(deps) {
3577
3967
  panel,
3578
3968
  index,
3579
3969
  fullWidth,
3580
- ui.showHelp
3970
+ (ui.showHelp || worktreeOverlayOpen)
3581
3971
  ? true
3582
3972
  : ((panel.key === 'preview' || panel.key === 'preview-overlay')
3583
3973
  ? paneFocus === 'preview'
@@ -3588,7 +3978,11 @@ function createDashboardApp(deps) {
3588
3978
  return React.createElement(
3589
3979
  Box,
3590
3980
  { flexDirection: 'column', width: fullWidth },
3591
- React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
3981
+ React.createElement(
3982
+ Text,
3983
+ { color: 'cyan' },
3984
+ buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth, selectedWorktreeLabel)
3985
+ ),
3592
3986
  React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
3593
3987
  React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons, flow: activeFlow }),
3594
3988
  showApprovalBanner