specsmd 0.1.51 → 0.1.53

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.
@@ -0,0 +1,330 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ function runGit(args, cwd, options = {}) {
6
+ const acceptedStatuses = Array.isArray(options.acceptedStatuses) && options.acceptedStatuses.length > 0
7
+ ? options.acceptedStatuses
8
+ : [0];
9
+ const result = spawnSync('git', args, {
10
+ cwd,
11
+ encoding: 'utf8',
12
+ maxBuffer: 10 * 1024 * 1024
13
+ });
14
+
15
+ if (result.error) {
16
+ return {
17
+ ok: false,
18
+ error: result.error.message || String(result.error),
19
+ stdout: '',
20
+ stderr: ''
21
+ };
22
+ }
23
+
24
+ if (typeof result.status === 'number' && !acceptedStatuses.includes(result.status)) {
25
+ return {
26
+ ok: false,
27
+ error: String(result.stderr || '').trim() || `git exited with code ${result.status}`,
28
+ stdout: String(result.stdout || ''),
29
+ stderr: String(result.stderr || '')
30
+ };
31
+ }
32
+
33
+ return {
34
+ ok: true,
35
+ error: null,
36
+ stdout: String(result.stdout || ''),
37
+ stderr: String(result.stderr || '')
38
+ };
39
+ }
40
+
41
+ function findGitRoot(worktreePath) {
42
+ if (typeof worktreePath !== 'string' || worktreePath.trim() === '') {
43
+ return null;
44
+ }
45
+
46
+ const probe = runGit(['rev-parse', '--show-toplevel'], worktreePath);
47
+ if (!probe.ok) {
48
+ return null;
49
+ }
50
+
51
+ const root = probe.stdout.trim();
52
+ return root === '' ? null : root;
53
+ }
54
+
55
+ function parseBranchSummary(line) {
56
+ const raw = String(line || '').replace(/^##\s*/, '').trim();
57
+ if (raw === '') {
58
+ return {
59
+ branch: '(unknown)',
60
+ upstream: null,
61
+ ahead: 0,
62
+ behind: 0,
63
+ detached: false
64
+ };
65
+ }
66
+
67
+ if (raw.startsWith('HEAD ')) {
68
+ return {
69
+ branch: '(detached)',
70
+ upstream: null,
71
+ ahead: 0,
72
+ behind: 0,
73
+ detached: true
74
+ };
75
+ }
76
+
77
+ const [branchPart, trackingPartRaw] = raw.split(/\s+/, 2);
78
+ let branch = branchPart;
79
+ let upstream = null;
80
+ if (branch.includes('...')) {
81
+ const [name, remote] = branch.split('...', 2);
82
+ branch = name || '(unknown)';
83
+ upstream = remote || null;
84
+ }
85
+
86
+ const trackingPart = typeof trackingPartRaw === 'string' ? trackingPartRaw : '';
87
+ const aheadMatch = trackingPart.match(/ahead\s+(\d+)/);
88
+ const behindMatch = trackingPart.match(/behind\s+(\d+)/);
89
+ const ahead = aheadMatch ? Number.parseInt(aheadMatch[1], 10) : 0;
90
+ const behind = behindMatch ? Number.parseInt(behindMatch[1], 10) : 0;
91
+
92
+ return {
93
+ branch: branch || '(unknown)',
94
+ upstream,
95
+ ahead: Number.isFinite(ahead) ? ahead : 0,
96
+ behind: Number.isFinite(behind) ? behind : 0,
97
+ detached: false
98
+ };
99
+ }
100
+
101
+ function parseStatusEntry(line, repoRoot) {
102
+ const raw = String(line || '');
103
+ if (raw.trim() === '' || raw.startsWith('## ')) {
104
+ return null;
105
+ }
106
+
107
+ const code = raw.slice(0, 2);
108
+ const statusX = code.charAt(0);
109
+ const statusY = code.charAt(1);
110
+ const remainder = raw.length > 3 ? raw.slice(3) : '';
111
+
112
+ let relativePath = remainder.trim();
113
+ if (relativePath.includes(' -> ')) {
114
+ const parts = relativePath.split(' -> ');
115
+ relativePath = parts[parts.length - 1].trim();
116
+ }
117
+
118
+ const absolutePath = relativePath === ''
119
+ ? ''
120
+ : path.join(repoRoot, relativePath);
121
+ const isUntracked = code === '??';
122
+ const isConflicted = statusX === 'U'
123
+ || statusY === 'U'
124
+ || code === 'AA'
125
+ || code === 'DD';
126
+ const isStaged = !isUntracked && statusX !== ' ';
127
+ const isUnstaged = !isUntracked && statusY !== ' ';
128
+
129
+ return {
130
+ code,
131
+ statusX,
132
+ statusY,
133
+ relativePath,
134
+ absolutePath,
135
+ staged: isStaged,
136
+ unstaged: isUnstaged,
137
+ untracked: isUntracked,
138
+ conflicted: isConflicted
139
+ };
140
+ }
141
+
142
+ function buildBucketItem(entry, bucket) {
143
+ return {
144
+ key: `${bucket}:${entry.relativePath}`,
145
+ bucket,
146
+ code: entry.code,
147
+ path: entry.absolutePath,
148
+ relativePath: entry.relativePath,
149
+ label: entry.relativePath
150
+ };
151
+ }
152
+
153
+ function listGitChanges(worktreePath) {
154
+ const gitRoot = findGitRoot(worktreePath);
155
+ if (!gitRoot) {
156
+ return {
157
+ available: false,
158
+ rootPath: null,
159
+ branch: '(not a git repo)',
160
+ upstream: null,
161
+ ahead: 0,
162
+ behind: 0,
163
+ detached: false,
164
+ clean: true,
165
+ counts: {
166
+ total: 0,
167
+ staged: 0,
168
+ unstaged: 0,
169
+ untracked: 0,
170
+ conflicted: 0
171
+ },
172
+ staged: [],
173
+ unstaged: [],
174
+ untracked: [],
175
+ conflicted: [],
176
+ generatedAt: new Date().toISOString()
177
+ };
178
+ }
179
+
180
+ const statusResult = runGit(
181
+ ['-c', 'color.ui=false', '-c', 'core.quotepath=false', 'status', '--porcelain', '--branch', '--untracked-files=all'],
182
+ gitRoot
183
+ );
184
+
185
+ if (!statusResult.ok) {
186
+ return {
187
+ available: false,
188
+ rootPath: gitRoot,
189
+ branch: '(status unavailable)',
190
+ upstream: null,
191
+ ahead: 0,
192
+ behind: 0,
193
+ detached: false,
194
+ clean: true,
195
+ counts: {
196
+ total: 0,
197
+ staged: 0,
198
+ unstaged: 0,
199
+ untracked: 0,
200
+ conflicted: 0
201
+ },
202
+ staged: [],
203
+ unstaged: [],
204
+ untracked: [],
205
+ conflicted: [],
206
+ error: statusResult.error,
207
+ generatedAt: new Date().toISOString()
208
+ };
209
+ }
210
+
211
+ const lines = statusResult.stdout.split(/\r?\n/).filter(Boolean);
212
+ const branchInfo = parseBranchSummary(lines[0] || '');
213
+
214
+ const staged = [];
215
+ const unstaged = [];
216
+ const untracked = [];
217
+ const conflicted = [];
218
+
219
+ for (const line of lines.slice(1)) {
220
+ const entry = parseStatusEntry(line, gitRoot);
221
+ if (!entry || entry.relativePath === '') {
222
+ continue;
223
+ }
224
+ if (entry.conflicted) {
225
+ conflicted.push(buildBucketItem(entry, 'conflicted'));
226
+ }
227
+ if (entry.staged) {
228
+ staged.push(buildBucketItem(entry, 'staged'));
229
+ }
230
+ if (entry.unstaged) {
231
+ unstaged.push(buildBucketItem(entry, 'unstaged'));
232
+ }
233
+ if (entry.untracked) {
234
+ untracked.push(buildBucketItem(entry, 'untracked'));
235
+ }
236
+ }
237
+
238
+ const uniqueCount = new Set([
239
+ ...staged.map((item) => item.relativePath),
240
+ ...unstaged.map((item) => item.relativePath),
241
+ ...untracked.map((item) => item.relativePath),
242
+ ...conflicted.map((item) => item.relativePath)
243
+ ]).size;
244
+
245
+ return {
246
+ available: true,
247
+ rootPath: gitRoot,
248
+ branch: branchInfo.branch,
249
+ upstream: branchInfo.upstream,
250
+ ahead: branchInfo.ahead,
251
+ behind: branchInfo.behind,
252
+ detached: branchInfo.detached,
253
+ clean: uniqueCount === 0,
254
+ counts: {
255
+ total: uniqueCount,
256
+ staged: staged.length,
257
+ unstaged: unstaged.length,
258
+ untracked: untracked.length,
259
+ conflicted: conflicted.length
260
+ },
261
+ staged,
262
+ unstaged,
263
+ untracked,
264
+ conflicted,
265
+ generatedAt: new Date().toISOString()
266
+ };
267
+ }
268
+
269
+ function readUntrackedFileDiff(repoRoot, absolutePath) {
270
+ const exists = typeof absolutePath === 'string' && absolutePath !== '' && fs.existsSync(absolutePath);
271
+ if (!exists) {
272
+ return '';
273
+ }
274
+
275
+ const result = runGit(
276
+ ['-c', 'color.ui=always', '--no-pager', 'diff', '--no-index', '--', '/dev/null', absolutePath],
277
+ repoRoot,
278
+ { acceptedStatuses: [0, 1] }
279
+ );
280
+ if (!result.ok) {
281
+ return '';
282
+ }
283
+ return result.stdout;
284
+ }
285
+
286
+ function loadGitDiffPreview(changeEntry) {
287
+ const bucket = typeof changeEntry?.bucket === 'string' ? changeEntry.bucket : 'unstaged';
288
+ const repoRoot = typeof changeEntry?.repoRoot === 'string'
289
+ ? changeEntry.repoRoot
290
+ : (typeof changeEntry?.workspacePath === 'string' ? findGitRoot(changeEntry.workspacePath) : null);
291
+ const relativePath = typeof changeEntry?.relativePath === 'string' ? changeEntry.relativePath : '';
292
+ const absolutePath = typeof changeEntry?.path === 'string' ? changeEntry.path : '';
293
+
294
+ if (!repoRoot) {
295
+ return '[git] repository is unavailable for preview.';
296
+ }
297
+ if (relativePath === '') {
298
+ return '[git] no file selected.';
299
+ }
300
+
301
+ if (bucket === 'untracked') {
302
+ const rawDiff = readUntrackedFileDiff(repoRoot, absolutePath);
303
+ if (rawDiff.trim() !== '') {
304
+ return rawDiff;
305
+ }
306
+ }
307
+
308
+ const args = ['-c', 'color.ui=always', '--no-pager', 'diff'];
309
+ if (bucket === 'staged') {
310
+ args.push('--cached');
311
+ }
312
+ args.push('--', relativePath);
313
+
314
+ const result = runGit(args, repoRoot);
315
+ if (!result.ok) {
316
+ return `[git] unable to load diff: ${result.error}`;
317
+ }
318
+
319
+ const output = result.stdout.trim();
320
+ if (output === '') {
321
+ return '[git] no diff output for this file.';
322
+ }
323
+
324
+ return result.stdout;
325
+ }
326
+
327
+ module.exports = {
328
+ listGitChanges,
329
+ loadGitDiffPreview
330
+ };
@@ -8,6 +8,7 @@ const { parseSimpleDashboard } = require('./simple/parser');
8
8
  const { formatDashboardText } = require('./tui/renderer');
9
9
  const { createDashboardApp } = require('./tui/app');
10
10
  const { discoverGitWorktrees, pickWorktree, pathExistsAsDirectory } = require('./git/worktrees');
11
+ const { listGitChanges } = require('./git/changes');
11
12
 
12
13
  function parseRefreshMs(raw) {
13
14
  const parsed = Number.parseInt(String(raw || '1000'), 10);
@@ -602,7 +603,8 @@ async function runFlowDashboard(options, flow, availableFlows = []) {
602
603
  snapshot: {
603
604
  ...selectedResult.snapshot,
604
605
  workspacePath: selectedWorktree?.path || selectedResult.snapshot?.workspacePath || workspacePath,
605
- dashboardWorktrees: envelope
606
+ dashboardWorktrees: envelope,
607
+ gitChanges: listGitChanges(selectedWorktree?.path || workspacePath)
606
608
  }
607
609
  };
608
610
  };
@@ -5,6 +5,7 @@ const stringWidthModule = require('string-width');
5
5
  const sliceAnsiModule = require('slice-ansi');
6
6
  const { createWatchRuntime } = require('../runtime/watch-runtime');
7
7
  const { createInitialUIState } = require('./store');
8
+ const { loadGitDiffPreview } = require('../git/changes');
8
9
 
9
10
  const stringWidth = typeof stringWidthModule === 'function'
10
11
  ? stringWidthModule
@@ -64,6 +65,7 @@ function resolveIconSet() {
64
65
  runs: '[R]',
65
66
  overview: '[O]',
66
67
  health: '[H]',
68
+ git: '[G]',
67
69
  runFile: '*',
68
70
  activeFile: '>',
69
71
  groupCollapsed: '>',
@@ -74,6 +76,7 @@ function resolveIconSet() {
74
76
  runs: '󰑮',
75
77
  overview: '󰍉',
76
78
  health: '󰓦',
79
+ git: '󰊢',
77
80
  runFile: '󰈔',
78
81
  activeFile: '󰜴',
79
82
  groupCollapsed: '󰐕',
@@ -118,6 +121,11 @@ function truncate(value, width) {
118
121
  return `${sliceAnsi(text, 0, bodyWidth)}${ellipsis}`;
119
122
  }
120
123
 
124
+ function resolveFrameWidth(columns) {
125
+ const safeColumns = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 120;
126
+ return safeColumns > 24 ? safeColumns - 1 : safeColumns;
127
+ }
128
+
121
129
  function normalizePanelLine(line) {
122
130
  if (line && typeof line === 'object' && !Array.isArray(line)) {
123
131
  return {
@@ -1120,7 +1128,47 @@ function getPanelTitles(flow, snapshot) {
1120
1128
  files: 'Run Files',
1121
1129
  pending: 'Pending Queue',
1122
1130
  completed: 'Recent Completed Runs',
1123
- otherWorktrees: 'Other Worktrees: Active Runs'
1131
+ otherWorktrees: 'Other Worktrees: Active Runs',
1132
+ git: 'Git Changes'
1133
+ };
1134
+ }
1135
+
1136
+ function getGitChangesSnapshot(snapshot) {
1137
+ const gitChanges = snapshot?.gitChanges;
1138
+ if (!gitChanges || typeof gitChanges !== 'object') {
1139
+ return {
1140
+ available: false,
1141
+ branch: '(unavailable)',
1142
+ upstream: null,
1143
+ ahead: 0,
1144
+ behind: 0,
1145
+ counts: {
1146
+ total: 0,
1147
+ staged: 0,
1148
+ unstaged: 0,
1149
+ untracked: 0,
1150
+ conflicted: 0
1151
+ },
1152
+ staged: [],
1153
+ unstaged: [],
1154
+ untracked: [],
1155
+ conflicted: [],
1156
+ clean: true
1157
+ };
1158
+ }
1159
+ return {
1160
+ ...gitChanges,
1161
+ counts: gitChanges.counts || {
1162
+ total: 0,
1163
+ staged: 0,
1164
+ unstaged: 0,
1165
+ untracked: 0,
1166
+ conflicted: 0
1167
+ },
1168
+ staged: Array.isArray(gitChanges.staged) ? gitChanges.staged : [],
1169
+ unstaged: Array.isArray(gitChanges.unstaged) ? gitChanges.unstaged : [],
1170
+ untracked: Array.isArray(gitChanges.untracked) ? gitChanges.untracked : [],
1171
+ conflicted: Array.isArray(gitChanges.conflicted) ? gitChanges.conflicted : []
1124
1172
  };
1125
1173
  }
1126
1174
 
@@ -1300,6 +1348,9 @@ function getSectionOrderForView(view, options = {}) {
1300
1348
  if (view === 'health') {
1301
1349
  return ['standards', 'stats', 'warnings', 'error-details'];
1302
1350
  }
1351
+ if (view === 'git') {
1352
+ return ['git-changes'];
1353
+ }
1303
1354
  const sections = [];
1304
1355
  if (includeWorktrees) {
1305
1356
  sections.push('worktrees');
@@ -1640,6 +1691,10 @@ function formatScope(scope) {
1640
1691
  if (scope === 'upcoming') return 'UPNEXT';
1641
1692
  if (scope === 'completed') return 'DONE';
1642
1693
  if (scope === 'intent') return 'INTENT';
1694
+ if (scope === 'staged') return 'STAGED';
1695
+ if (scope === 'unstaged') return 'UNSTAGED';
1696
+ if (scope === 'untracked') return 'UNTRACKED';
1697
+ if (scope === 'conflicted') return 'CONFLICT';
1643
1698
  return 'FILE';
1644
1699
  }
1645
1700
 
@@ -2054,9 +2109,15 @@ function collectAidlcIntentContextFiles(snapshot, intentId) {
2054
2109
  }
2055
2110
 
2056
2111
  function filterExistingFiles(files) {
2057
- return (Array.isArray(files) ? files : []).filter((file) =>
2058
- file && typeof file.path === 'string' && typeof file.label === 'string' && fileExists(file.path)
2059
- );
2112
+ return (Array.isArray(files) ? files : []).filter((file) => {
2113
+ if (!file || typeof file.path !== 'string' || typeof file.label !== 'string') {
2114
+ return false;
2115
+ }
2116
+ if (file.allowMissing === true) {
2117
+ return true;
2118
+ }
2119
+ return fileExists(file.path);
2120
+ });
2060
2121
  }
2061
2122
 
2062
2123
  function buildPendingGroups(snapshot, flow) {
@@ -2191,6 +2252,49 @@ function buildCompletedGroups(snapshot, flow) {
2191
2252
  return groups;
2192
2253
  }
2193
2254
 
2255
+ function buildGitChangeGroups(snapshot) {
2256
+ const git = getGitChangesSnapshot(snapshot);
2257
+
2258
+ if (!git.available) {
2259
+ return [];
2260
+ }
2261
+
2262
+ const makeFiles = (items, scope) => items.map((item) => ({
2263
+ label: item.relativePath,
2264
+ path: item.path || path.join(git.rootPath || snapshot?.workspacePath || '', item.relativePath || ''),
2265
+ scope,
2266
+ allowMissing: true,
2267
+ previewType: 'git-diff',
2268
+ repoRoot: git.rootPath || snapshot?.workspacePath || '',
2269
+ relativePath: item.relativePath || '',
2270
+ bucket: item.bucket || scope
2271
+ }));
2272
+
2273
+ const groups = [];
2274
+ groups.push({
2275
+ key: 'git:staged',
2276
+ label: `staged (${git.counts.staged || 0})`,
2277
+ files: makeFiles(git.staged, 'staged')
2278
+ });
2279
+ groups.push({
2280
+ key: 'git:unstaged',
2281
+ label: `unstaged (${git.counts.unstaged || 0})`,
2282
+ files: makeFiles(git.unstaged, 'unstaged')
2283
+ });
2284
+ groups.push({
2285
+ key: 'git:untracked',
2286
+ label: `untracked (${git.counts.untracked || 0})`,
2287
+ files: makeFiles(git.untracked, 'untracked')
2288
+ });
2289
+ groups.push({
2290
+ key: 'git:conflicted',
2291
+ label: `conflicts (${git.counts.conflicted || 0})`,
2292
+ files: makeFiles(git.conflicted, 'conflicted')
2293
+ });
2294
+
2295
+ return groups;
2296
+ }
2297
+
2194
2298
  function toExpandableRows(groups, emptyLabel, expandedGroups) {
2195
2299
  if (!Array.isArray(groups) || groups.length === 0) {
2196
2300
  return [{
@@ -2221,12 +2325,16 @@ function toExpandableRows(groups, emptyLabel, expandedGroups) {
2221
2325
  for (let index = 0; index < files.length; index += 1) {
2222
2326
  const file = files[index];
2223
2327
  rows.push({
2224
- kind: 'file',
2328
+ kind: file.previewType === 'git-diff' ? 'git-file' : 'file',
2225
2329
  key: `${group.key}:file:${file.path}:${index}`,
2226
2330
  label: file.label,
2227
2331
  path: file.path,
2228
2332
  scope: file.scope || 'file',
2229
- selectable: true
2333
+ selectable: true,
2334
+ previewType: file.previewType,
2335
+ repoRoot: file.repoRoot,
2336
+ relativePath: file.relativePath,
2337
+ bucket: file.bucket
2230
2338
  });
2231
2339
  }
2232
2340
  }
@@ -2261,7 +2369,7 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
2261
2369
  };
2262
2370
  }
2263
2371
 
2264
- if (row.kind === 'file') {
2372
+ if (row.kind === 'file' || row.kind === 'git-file') {
2265
2373
  const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
2266
2374
  return {
2267
2375
  text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
@@ -2298,13 +2406,17 @@ function getSelectedRow(rows, selectedIndex) {
2298
2406
  }
2299
2407
 
2300
2408
  function rowToFileEntry(row) {
2301
- if (!row || row.kind !== 'file' || typeof row.path !== 'string') {
2409
+ if (!row || (row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
2302
2410
  return null;
2303
2411
  }
2304
2412
  return {
2305
2413
  label: row.label || path.basename(row.path),
2306
2414
  path: row.path,
2307
- scope: row.scope || 'file'
2415
+ scope: row.scope || 'file',
2416
+ previewType: row.previewType,
2417
+ repoRoot: row.repoRoot,
2418
+ relativePath: row.relativePath,
2419
+ bucket: row.bucket
2308
2420
  };
2309
2421
  }
2310
2422
 
@@ -2401,9 +2513,9 @@ function buildQuickHelpText(view, options = {}) {
2401
2513
  const isSimple = String(flow || '').toLowerCase() === 'simple';
2402
2514
  const activeLabel = isAidlc ? 'active bolt' : (isSimple ? 'active spec' : 'active run');
2403
2515
 
2404
- const parts = ['1/2/3/4 tabs', 'g/G sections'];
2516
+ const parts = ['1/2/3/4/5 tabs', 'g/G sections'];
2405
2517
 
2406
- if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health') {
2518
+ if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health' || view === 'git') {
2407
2519
  if (previewOpen) {
2408
2520
  parts.push('tab pane', '↑/↓ nav/scroll', 'v/space close');
2409
2521
  } else {
@@ -2415,6 +2527,8 @@ function buildQuickHelpText(view, options = {}) {
2415
2527
  parts.push('b worktrees', 'u others');
2416
2528
  }
2417
2529
  parts.push('a current', 'f files');
2530
+ } else if (view === 'git') {
2531
+ parts.push('d changes', 'c/A/p/P git(soon)');
2418
2532
  }
2419
2533
  parts.push(`tab1 ${activeLabel}`);
2420
2534
 
@@ -2426,6 +2540,57 @@ function buildQuickHelpText(view, options = {}) {
2426
2540
  return parts.join(' | ');
2427
2541
  }
2428
2542
 
2543
+ function buildLazyGitCommandStrip(view, options = {}) {
2544
+ const {
2545
+ hasWorktrees = false,
2546
+ previewOpen = false
2547
+ } = options;
2548
+
2549
+ const parts = [];
2550
+
2551
+ if (view === 'runs') {
2552
+ if (hasWorktrees) {
2553
+ parts.push('b worktrees');
2554
+ }
2555
+ parts.push('a current', 'f files', 'enter expand');
2556
+ } else if (view === 'intents') {
2557
+ parts.push('n next', 'x completed', 'enter expand');
2558
+ } else if (view === 'completed') {
2559
+ parts.push('c completed', 'enter expand');
2560
+ } else if (view === 'health') {
2561
+ parts.push('s standards', 't stats', 'w warnings');
2562
+ } else if (view === 'git') {
2563
+ parts.push('d changes', 'space preview', 'c commit (soon)', 'A amend (soon)', 'p push (soon)', 'P pull (soon)');
2564
+ }
2565
+
2566
+ if (previewOpen) {
2567
+ parts.push('tab pane', 'j/k scroll');
2568
+ } else {
2569
+ parts.push('v preview');
2570
+ }
2571
+
2572
+ parts.push('1-5 views', 'g/G panels', 'r refresh', '? help', 'q quit');
2573
+ return parts.join(' | ');
2574
+ }
2575
+
2576
+ function buildLazyGitCommandLogLine(options = {}) {
2577
+ const {
2578
+ statusLine = '',
2579
+ activeFlow = 'fire',
2580
+ watchEnabled = true,
2581
+ watchStatus = 'watching',
2582
+ selectedWorktreeLabel = null
2583
+ } = options;
2584
+
2585
+ if (typeof statusLine === 'string' && statusLine.trim() !== '') {
2586
+ return `Command Log | ${statusLine}`;
2587
+ }
2588
+
2589
+ const watchLabel = watchEnabled ? watchStatus : 'off';
2590
+ const worktreeSegment = selectedWorktreeLabel ? ` | wt:${selectedWorktreeLabel}` : '';
2591
+ return `Command Log | flow:${String(activeFlow || 'fire').toUpperCase()} | watch:${watchLabel}${worktreeSegment} | ready`;
2592
+ }
2593
+
2429
2594
  function buildHelpOverlayLines(options = {}) {
2430
2595
  const {
2431
2596
  view = 'runs',
@@ -2445,7 +2610,7 @@ function buildHelpOverlayLines(options = {}) {
2445
2610
  { text: 'Global', color: 'cyan', bold: true },
2446
2611
  'q or Ctrl+C quit',
2447
2612
  'r refresh snapshot',
2448
- `1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health`,
2613
+ `1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health | 5 git changes`,
2449
2614
  'g next section | G previous section',
2450
2615
  'h/? toggle this shortcuts overlay',
2451
2616
  'esc close overlays (help/preview/fullscreen)',
@@ -2483,6 +2648,10 @@ function buildHelpOverlayLines(options = {}) {
2483
2648
  { text: 'Tab 4 Standards/Health', color: 'magenta', bold: true },
2484
2649
  `s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
2485
2650
  { text: '', color: undefined, bold: false },
2651
+ { text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
2652
+ 'select changed files and preview diffs',
2653
+ 'commit/push/pull shortcuts are shown in footer for LazyGit-style hierarchy',
2654
+ { text: '', color: undefined, bold: false },
2486
2655
  { text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
2487
2656
  );
2488
2657
 
@@ -2601,20 +2770,27 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
2601
2770
  return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
2602
2771
  }
2603
2772
 
2604
- let content;
2605
- try {
2606
- content = fs.readFileSync(fileEntry.path, 'utf8');
2607
- } catch (error) {
2608
- return [{
2609
- text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
2610
- color: 'red',
2611
- bold: false
2612
- }];
2773
+ const isGitPreview = fileEntry.previewType === 'git-diff';
2774
+ let rawLines = [];
2775
+ if (isGitPreview) {
2776
+ const diffText = loadGitDiffPreview(fileEntry);
2777
+ rawLines = String(diffText || '').split(/\r?\n/);
2778
+ } else {
2779
+ let content;
2780
+ try {
2781
+ content = fs.readFileSync(fileEntry.path, 'utf8');
2782
+ } catch (error) {
2783
+ return [{
2784
+ text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
2785
+ color: 'red',
2786
+ bold: false
2787
+ }];
2788
+ }
2789
+ rawLines = String(content).split(/\r?\n/);
2613
2790
  }
2614
2791
 
2615
- const rawLines = String(content).split(/\r?\n/);
2616
2792
  const headLine = {
2617
- text: truncate(`file: ${fileEntry.path}`, width),
2793
+ text: truncate(`${isGitPreview ? 'diff' : 'file'}: ${fileEntry.path}`, width),
2618
2794
  color: 'cyan',
2619
2795
  bold: true
2620
2796
  };
@@ -2625,7 +2801,34 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
2625
2801
 
2626
2802
  const highlighted = cappedLines.map((rawLine, index) => {
2627
2803
  const prefixedLine = `${String(index + 1).padStart(4, ' ')} | ${rawLine}`;
2628
- const { color, bold, togglesCodeBlock } = colorizeMarkdownLine(rawLine, inCodeBlock);
2804
+ let color;
2805
+ let bold;
2806
+ let togglesCodeBlock = false;
2807
+
2808
+ if (isGitPreview) {
2809
+ if (rawLine.startsWith('+++ ') || rawLine.startsWith('--- ') || rawLine.startsWith('diff --git')) {
2810
+ color = 'cyan';
2811
+ bold = true;
2812
+ } else if (rawLine.startsWith('@@')) {
2813
+ color = 'magenta';
2814
+ bold = true;
2815
+ } else if (rawLine.startsWith('+')) {
2816
+ color = 'green';
2817
+ bold = false;
2818
+ } else if (rawLine.startsWith('-')) {
2819
+ color = 'red';
2820
+ bold = false;
2821
+ } else {
2822
+ color = undefined;
2823
+ bold = false;
2824
+ }
2825
+ } else {
2826
+ const markdownStyle = colorizeMarkdownLine(rawLine, inCodeBlock);
2827
+ color = markdownStyle.color;
2828
+ bold = markdownStyle.bold;
2829
+ togglesCodeBlock = markdownStyle.togglesCodeBlock;
2830
+ }
2831
+
2629
2832
  if (togglesCodeBlock) {
2630
2833
  inCodeBlock = !inCodeBlock;
2631
2834
  }
@@ -2720,7 +2923,7 @@ function createDashboardApp(deps) {
2720
2923
  focused
2721
2924
  } = props;
2722
2925
 
2723
- const contentWidth = Math.max(18, width - 4);
2926
+ const contentWidth = Math.max(1, width - (dense ? 2 : 4));
2724
2927
  const visibleLines = fitLines(lines, maxLines, contentWidth);
2725
2928
  const panelBorderColor = focused ? 'cyan' : (borderColor || 'gray');
2726
2929
  const titleColor = focused ? 'black' : 'cyan';
@@ -2772,7 +2975,8 @@ function createDashboardApp(deps) {
2772
2975
  { id: 'runs', label: `1 ${icons.runs} ${primaryLabel}` },
2773
2976
  { id: 'intents', label: `2 ${icons.overview} INTENTS` },
2774
2977
  { id: 'completed', label: `3 ${icons.runs} ${completedLabel}` },
2775
- { id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` }
2978
+ { id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` },
2979
+ { id: 'git', label: `5 ${icons.git} GIT CHANGES` }
2776
2980
  ];
2777
2981
  const maxWidth = Math.max(8, Math.floor(width));
2778
2982
  const segments = [];
@@ -2902,7 +3106,8 @@ function createDashboardApp(deps) {
2902
3106
  runs: 'current-run',
2903
3107
  intents: 'intent-status',
2904
3108
  completed: 'completed-runs',
2905
- health: 'standards'
3109
+ health: 'standards',
3110
+ git: 'git-changes'
2906
3111
  });
2907
3112
  const [selectionBySection, setSelectionBySection] = useState({
2908
3113
  worktrees: 0,
@@ -2914,7 +3119,8 @@ function createDashboardApp(deps) {
2914
3119
  standards: 0,
2915
3120
  stats: 0,
2916
3121
  warnings: 0,
2917
- 'error-details': 0
3122
+ 'error-details': 0,
3123
+ 'git-changes': 0
2918
3124
  });
2919
3125
  const [expandedGroups, setExpandedGroups] = useState({});
2920
3126
  const [previewTarget, setPreviewTarget] = useState(null);
@@ -3068,6 +3274,37 @@ function createDashboardApp(deps) {
3068
3274
  'No error details'
3069
3275
  )
3070
3276
  : toLoadingRows('Loading error details...', 'error-loading');
3277
+ const gitRows = shouldHydrateSecondaryTabs
3278
+ ? (() => {
3279
+ const git = getGitChangesSnapshot(snapshot);
3280
+ const tracking = git.upstream
3281
+ ? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
3282
+ : 'no upstream';
3283
+ const headerRows = [{
3284
+ kind: 'info',
3285
+ key: 'git:branch',
3286
+ label: git.available
3287
+ ? `branch ${git.branch}${git.detached ? ' [detached]' : ''} | ${tracking}`
3288
+ : 'git: repository unavailable in selected worktree',
3289
+ color: git.available ? 'cyan' : 'red',
3290
+ bold: true,
3291
+ selectable: false
3292
+ }, {
3293
+ kind: 'info',
3294
+ key: 'git:counts',
3295
+ label: `changes ${git.counts.total || 0} | staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0} | untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
3296
+ color: 'gray',
3297
+ bold: false,
3298
+ selectable: false
3299
+ }];
3300
+ const groups = toExpandableRows(
3301
+ buildGitChangeGroups(snapshot),
3302
+ git.available ? 'Working tree clean' : 'No git changes',
3303
+ expandedGroups
3304
+ );
3305
+ return [...headerRows, ...groups];
3306
+ })()
3307
+ : toLoadingRows('Loading git changes...', 'git-loading');
3071
3308
 
3072
3309
  const rowsBySection = {
3073
3310
  worktrees: worktreeRows,
@@ -3079,7 +3316,8 @@ function createDashboardApp(deps) {
3079
3316
  standards: standardsRows,
3080
3317
  stats: statsRows,
3081
3318
  warnings: warningsRows,
3082
- 'error-details': errorDetailsRows
3319
+ 'error-details': errorDetailsRows,
3320
+ 'git-changes': gitRows
3083
3321
  };
3084
3322
  const worktreeItems = getWorktreeItems(snapshot);
3085
3323
  const selectedWorktree = getSelectedWorktree(snapshot);
@@ -3269,6 +3507,12 @@ function createDashboardApp(deps) {
3269
3507
  return;
3270
3508
  }
3271
3509
 
3510
+ if (input === '5') {
3511
+ setUi((previous) => ({ ...previous, view: 'git' }));
3512
+ setPaneFocus('main');
3513
+ return;
3514
+ }
3515
+
3272
3516
  if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
3273
3517
  snapshotHashRef.current = safeJsonHash(null);
3274
3518
  errorHashRef.current = null;
@@ -3291,13 +3535,15 @@ function createDashboardApp(deps) {
3291
3535
  standards: 0,
3292
3536
  stats: 0,
3293
3537
  warnings: 0,
3294
- 'error-details': 0
3538
+ 'error-details': 0,
3539
+ 'git-changes': 0
3295
3540
  });
3296
3541
  setSectionFocus({
3297
3542
  runs: 'current-run',
3298
3543
  intents: 'intent-status',
3299
3544
  completed: 'completed-runs',
3300
- health: 'standards'
3545
+ health: 'standards',
3546
+ git: 'git-changes'
3301
3547
  });
3302
3548
  setOverviewIntentFilter('next');
3303
3549
  setExpandedGroups({});
@@ -3332,13 +3578,15 @@ function createDashboardApp(deps) {
3332
3578
  standards: 0,
3333
3579
  stats: 0,
3334
3580
  warnings: 0,
3335
- 'error-details': 0
3581
+ 'error-details': 0,
3582
+ 'git-changes': 0
3336
3583
  });
3337
3584
  setSectionFocus({
3338
3585
  runs: 'current-run',
3339
3586
  intents: 'intent-status',
3340
3587
  completed: 'completed-runs',
3341
- health: 'standards'
3588
+ health: 'standards',
3589
+ git: 'git-changes'
3342
3590
  });
3343
3591
  setOverviewIntentFilter('next');
3344
3592
  setExpandedGroups({});
@@ -3442,6 +3690,27 @@ function createDashboardApp(deps) {
3442
3690
  setSectionFocus((previous) => ({ ...previous, health: 'error-details' }));
3443
3691
  return;
3444
3692
  }
3693
+ } else if (ui.view === 'git') {
3694
+ if (input === 'd') {
3695
+ setSectionFocus((previous) => ({ ...previous, git: 'git-changes' }));
3696
+ return;
3697
+ }
3698
+ if (input === 'c') {
3699
+ setStatusLine('Git commit action is not wired yet (footer mirrors LazyGit command hierarchy).');
3700
+ return;
3701
+ }
3702
+ if (input === 'A') {
3703
+ setStatusLine('Git amend action is not wired yet (footer mirrors LazyGit command hierarchy).');
3704
+ return;
3705
+ }
3706
+ if (input === 'p') {
3707
+ setStatusLine('Git push action is not wired yet (footer mirrors LazyGit command hierarchy).');
3708
+ return;
3709
+ }
3710
+ if (input === 'P') {
3711
+ setStatusLine('Git pull action is not wired yet (footer mirrors LazyGit command hierarchy).');
3712
+ return;
3713
+ }
3445
3714
  }
3446
3715
 
3447
3716
  if (key.escape) {
@@ -3651,16 +3920,16 @@ function createDashboardApp(deps) {
3651
3920
  useEffect(() => {
3652
3921
  if (!stdout || typeof stdout.on !== 'function') {
3653
3922
  setTerminalSize({
3654
- columns: process.stdout.columns || 120,
3655
- rows: process.stdout.rows || 40
3923
+ columns: Math.max(1, process.stdout.columns || 120),
3924
+ rows: Math.max(1, process.stdout.rows || 40)
3656
3925
  });
3657
3926
  return undefined;
3658
3927
  }
3659
3928
 
3660
3929
  const updateSize = () => {
3661
3930
  setTerminalSize({
3662
- columns: stdout.columns || process.stdout.columns || 120,
3663
- rows: stdout.rows || process.stdout.rows || 40
3931
+ columns: Math.max(1, stdout.columns || process.stdout.columns || 120),
3932
+ rows: Math.max(1, stdout.rows || process.stdout.rows || 40)
3664
3933
  });
3665
3934
 
3666
3935
  // Resize in some terminals can leave stale frame rows behind.
@@ -3672,6 +3941,9 @@ function createDashboardApp(deps) {
3672
3941
 
3673
3942
  updateSize();
3674
3943
  stdout.on('resize', updateSize);
3944
+ if (process.stdout !== stdout && typeof process.stdout.on === 'function') {
3945
+ process.stdout.on('resize', updateSize);
3946
+ }
3675
3947
 
3676
3948
  return () => {
3677
3949
  if (typeof stdout.off === 'function') {
@@ -3679,6 +3951,13 @@ function createDashboardApp(deps) {
3679
3951
  } else if (typeof stdout.removeListener === 'function') {
3680
3952
  stdout.removeListener('resize', updateSize);
3681
3953
  }
3954
+ if (process.stdout !== stdout) {
3955
+ if (typeof process.stdout.off === 'function') {
3956
+ process.stdout.off('resize', updateSize);
3957
+ } else if (typeof process.stdout.removeListener === 'function') {
3958
+ process.stdout.removeListener('resize', updateSize);
3959
+ }
3960
+ }
3682
3961
  };
3683
3962
  }, [stdout]);
3684
3963
 
@@ -3717,7 +3996,9 @@ function createDashboardApp(deps) {
3717
3996
  });
3718
3997
 
3719
3998
  runtime.start();
3720
- const fallbackIntervalMs = Math.max(refreshMs, 5000);
3999
+ const fallbackIntervalMs = ui.view === 'git'
4000
+ ? Math.max(refreshMs, 1000)
4001
+ : Math.max(refreshMs, 5000);
3721
4002
  const interval = setInterval(() => {
3722
4003
  void refresh();
3723
4004
  }, fallbackIntervalMs);
@@ -3726,7 +4007,7 @@ function createDashboardApp(deps) {
3726
4007
  clearInterval(interval);
3727
4008
  void runtime.close();
3728
4009
  };
3729
- }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, worktreeWatchSignature, selectedWorktreeId]);
4010
+ }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, ui.view, worktreeWatchSignature, selectedWorktreeId]);
3730
4011
 
3731
4012
  useEffect(() => {
3732
4013
  if (!stdout || typeof stdout.write !== 'function') {
@@ -3741,37 +4022,31 @@ function createDashboardApp(deps) {
3741
4022
  const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
3742
4023
  const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
3743
4024
 
3744
- const fullWidth = Math.max(40, cols - 1);
4025
+ const fullWidth = resolveFrameWidth(cols);
3745
4026
  const showFlowBar = availableFlowIds.length > 1;
3746
- const showFooterHelpLine = rows >= 10;
4027
+ const showCommandLogLine = rows >= 8;
4028
+ const showCommandStrip = rows >= 9;
3747
4029
  const showErrorPanel = Boolean(error) && rows >= 18;
3748
4030
  const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp && !worktreeOverlayOpen;
3749
4031
  const showErrorInline = Boolean(error) && !showErrorPanel && !worktreeOverlayOpen;
3750
4032
  const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp && !worktreeOverlayOpen;
3751
- const showStatusLine = statusLine !== '';
4033
+ const showLegacyStatusLine = statusLine !== '' && !showCommandLogLine;
3752
4034
  const densePanels = rows <= 28 || cols <= 120;
3753
4035
 
3754
4036
  const reservedRows =
3755
4037
  2 +
3756
4038
  (showFlowBar ? 1 : 0) +
3757
4039
  (showApprovalBanner ? 1 : 0) +
3758
- (showFooterHelpLine ? 1 : 0) +
4040
+ (showCommandLogLine ? 1 : 0) +
4041
+ (showCommandStrip ? 1 : 0) +
3759
4042
  (showGlobalErrorPanel ? 5 : 0) +
3760
4043
  (showErrorInline ? 1 : 0) +
3761
- (showStatusLine ? 1 : 0);
4044
+ (showLegacyStatusLine ? 1 : 0);
3762
4045
  const frameSafetyRows = 2;
3763
4046
  const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
3764
4047
  const ultraCompact = rows <= 14;
3765
4048
  const panelTitles = getPanelTitles(activeFlow, snapshot);
3766
- const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && !worktreeOverlayOpen && cols >= 110 && rows >= 16;
3767
- const mainPaneWidth = splitPreviewLayout
3768
- ? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
3769
- : fullWidth;
3770
- const previewPaneWidth = splitPreviewLayout
3771
- ? Math.max(30, fullWidth - mainPaneWidth - 1)
3772
- : fullWidth;
3773
- const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
3774
- const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
4049
+ const compactWidth = Math.max(18, fullWidth - 4);
3775
4050
 
3776
4051
  const sectionLines = Object.fromEntries(
3777
4052
  Object.entries(rowsBySection).map(([sectionKey, sectionRows]) => [
@@ -3780,14 +4055,14 @@ function createDashboardApp(deps) {
3780
4055
  sectionRows,
3781
4056
  selectionBySection[sectionKey] || 0,
3782
4057
  icons,
3783
- mainCompactWidth,
4058
+ compactWidth,
3784
4059
  paneFocus === 'main' && focusedSection === sectionKey
3785
4060
  )
3786
4061
  ])
3787
4062
  );
3788
4063
  const effectivePreviewTarget = previewTarget || selectedFocusedFile;
3789
4064
  const previewLines = previewOpen
3790
- ? buildPreviewLines(effectivePreviewTarget, previewCompactWidth, previewScroll, {
4065
+ ? buildPreviewLines(effectivePreviewTarget, compactWidth, previewScroll, {
3791
4066
  fullDocument: overlayPreviewOpen
3792
4067
  })
3793
4068
  : [];
@@ -3808,6 +4083,17 @@ function createDashboardApp(deps) {
3808
4083
  availableFlowCount: availableFlowIds.length,
3809
4084
  hasWorktrees: worktreeSectionEnabled
3810
4085
  });
4086
+ const commandStripText = buildLazyGitCommandStrip(ui.view, {
4087
+ hasWorktrees: worktreeSectionEnabled,
4088
+ previewOpen
4089
+ });
4090
+ const commandLogLine = buildLazyGitCommandLogLine({
4091
+ statusLine,
4092
+ activeFlow,
4093
+ watchEnabled,
4094
+ watchStatus,
4095
+ selectedWorktreeLabel
4096
+ });
3811
4097
 
3812
4098
  let panelCandidates;
3813
4099
  if (ui.showHelp) {
@@ -3885,6 +4171,15 @@ function createDashboardApp(deps) {
3885
4171
  borderColor: 'red'
3886
4172
  });
3887
4173
  }
4174
+ } else if (ui.view === 'git') {
4175
+ panelCandidates = [
4176
+ {
4177
+ key: 'git-changes',
4178
+ title: panelTitles.git || 'Git Changes',
4179
+ lines: sectionLines['git-changes'],
4180
+ borderColor: 'yellow'
4181
+ }
4182
+ ];
3888
4183
  } else {
3889
4184
  panelCandidates = [];
3890
4185
  if (worktreeSectionEnabled) {
@@ -3919,7 +4214,7 @@ function createDashboardApp(deps) {
3919
4214
  }
3920
4215
  }
3921
4216
 
3922
- if (!ui.showHelp && previewOpen && !overlayPreviewOpen && !splitPreviewLayout) {
4217
+ if (!ui.showHelp && previewOpen && !overlayPreviewOpen) {
3923
4218
  panelCandidates.push({
3924
4219
  key: 'preview',
3925
4220
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
@@ -3928,7 +4223,7 @@ function createDashboardApp(deps) {
3928
4223
  });
3929
4224
  }
3930
4225
 
3931
- if (ultraCompact && !splitPreviewLayout) {
4226
+ if (ultraCompact) {
3932
4227
  if (previewOpen) {
3933
4228
  panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === focusedSection || panel.key === 'preview'));
3934
4229
  } else {
@@ -3938,6 +4233,12 @@ function createDashboardApp(deps) {
3938
4233
  }
3939
4234
 
3940
4235
  const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
4236
+ const lazyGitHierarchyLayout = !ui.showHelp
4237
+ && !worktreeOverlayOpen
4238
+ && !overlayPreviewOpen
4239
+ && !ultraCompact
4240
+ && fullWidth >= 96
4241
+ && panelCandidates.length > 1;
3941
4242
 
3942
4243
  const renderPanel = (panel, index, width, isFocused) => React.createElement(SectionPanel, {
3943
4244
  key: panel.key,
@@ -3952,14 +4253,21 @@ function createDashboardApp(deps) {
3952
4253
  });
3953
4254
 
3954
4255
  let contentNode;
3955
- if (splitPreviewLayout && !overlayPreviewOpen) {
3956
- const previewBodyLines = Math.max(1, contentRowsBudget - 3);
3957
- const previewPanel = {
3958
- key: 'preview-split',
3959
- title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
3960
- lines: previewLines,
3961
- borderColor: 'magenta',
3962
- maxLines: previewBodyLines
4256
+ if (lazyGitHierarchyLayout) {
4257
+ const preferredRightPanel = previewOpen && !overlayPreviewOpen
4258
+ ? panelCandidates.find((panel) => panel?.key === 'preview')
4259
+ : null;
4260
+ const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4261
+ const rightPanelBase = preferredRightPanel
4262
+ || focusedPanel
4263
+ || panelCandidates[panelCandidates.length - 1];
4264
+ const leftCandidates = panelCandidates.filter((panel) => panel?.key !== rightPanelBase?.key);
4265
+ const leftWidth = Math.max(30, Math.min(Math.floor(fullWidth * 0.38), fullWidth - 36));
4266
+ const rightWidth = Math.max(34, fullWidth - leftWidth - 1);
4267
+ const leftPanels = allocateSingleColumnPanels(leftCandidates, contentRowsBudget);
4268
+ const rightPanel = {
4269
+ ...rightPanelBase,
4270
+ maxLines: Math.max(4, contentRowsBudget)
3963
4271
  };
3964
4272
 
3965
4273
  contentNode = React.createElement(
@@ -3967,33 +4275,39 @@ function createDashboardApp(deps) {
3967
4275
  { width: fullWidth, flexDirection: 'row' },
3968
4276
  React.createElement(
3969
4277
  Box,
3970
- { width: mainPaneWidth, flexDirection: 'column' },
3971
- ...panels.map((panel, index) => React.createElement(SectionPanel, {
4278
+ { width: leftWidth, flexDirection: 'column' },
4279
+ ...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
3972
4280
  key: panel.key,
3973
4281
  title: panel.title,
3974
4282
  lines: panel.lines,
3975
- width: mainPaneWidth,
4283
+ width: leftWidth,
3976
4284
  maxLines: panel.maxLines,
3977
4285
  borderColor: panel.borderColor,
3978
- marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
4286
+ marginBottom: densePanels ? 0 : (index === leftPanels.length - 1 ? 0 : 1),
3979
4287
  dense: densePanels,
3980
4288
  focused: paneFocus === 'main' && panel.key === focusedSection
3981
4289
  }))
3982
4290
  ),
3983
- React.createElement(Box, { width: 1 }, React.createElement(Text, null, ' ')),
3984
4291
  React.createElement(
3985
4292
  Box,
3986
- { width: previewPaneWidth, flexDirection: 'column' },
4293
+ { width: 1, justifyContent: 'center' },
4294
+ React.createElement(Text, { color: 'gray' }, '│')
4295
+ ),
4296
+ React.createElement(
4297
+ Box,
4298
+ { width: rightWidth, flexDirection: 'column' },
3987
4299
  React.createElement(SectionPanel, {
3988
- key: previewPanel.key,
3989
- title: previewPanel.title,
3990
- lines: previewPanel.lines,
3991
- width: previewPaneWidth,
3992
- maxLines: previewPanel.maxLines,
3993
- borderColor: previewPanel.borderColor,
4300
+ key: rightPanel.key,
4301
+ title: rightPanel.title,
4302
+ lines: rightPanel.lines,
4303
+ width: rightWidth,
4304
+ maxLines: rightPanel.maxLines,
4305
+ borderColor: rightPanel.borderColor,
3994
4306
  marginBottom: 0,
3995
4307
  dense: densePanels,
3996
- focused: paneFocus === 'preview'
4308
+ focused: rightPanel.key === 'preview'
4309
+ ? paneFocus === 'preview'
4310
+ : (paneFocus === 'main' && rightPanel.key === focusedSection)
3997
4311
  })
3998
4312
  )
3999
4313
  );
@@ -4043,12 +4357,25 @@ function createDashboardApp(deps) {
4043
4357
  })
4044
4358
  : null,
4045
4359
  ...(Array.isArray(contentNode) ? contentNode : [contentNode]),
4046
- statusLine !== ''
4360
+ showLegacyStatusLine
4047
4361
  ? React.createElement(Text, { color: 'yellow' }, truncate(statusLine, fullWidth))
4048
4362
  : null,
4049
- showFooterHelpLine
4050
- ? React.createElement(Text, { color: 'gray' }, truncate(quickHelpText, fullWidth))
4051
- : null
4363
+ showCommandLogLine
4364
+ ? React.createElement(
4365
+ Text,
4366
+ { color: 'white', backgroundColor: 'gray', bold: true },
4367
+ truncate(commandLogLine, fullWidth)
4368
+ )
4369
+ : null,
4370
+ showCommandStrip
4371
+ ? React.createElement(
4372
+ Text,
4373
+ { color: 'white', backgroundColor: 'blue', bold: true },
4374
+ truncate(commandStripText, fullWidth)
4375
+ )
4376
+ : (rows >= 10
4377
+ ? React.createElement(Text, { color: 'gray' }, truncate(quickHelpText, fullWidth))
4378
+ : null)
4052
4379
  );
4053
4380
  }
4054
4381
 
@@ -15,12 +15,15 @@ function cycleView(current) {
15
15
  if (current === 'completed') {
16
16
  return 'health';
17
17
  }
18
+ if (current === 'health') {
19
+ return 'git';
20
+ }
18
21
  return 'runs';
19
22
  }
20
23
 
21
24
  function cycleViewBackward(current) {
22
25
  if (current === 'runs') {
23
- return 'health';
26
+ return 'git';
24
27
  }
25
28
  if (current === 'intents') {
26
29
  return 'runs';
@@ -28,7 +31,10 @@ function cycleViewBackward(current) {
28
31
  if (current === 'completed') {
29
32
  return 'intents';
30
33
  }
31
- return 'completed';
34
+ if (current === 'health') {
35
+ return 'completed';
36
+ }
37
+ return 'health';
32
38
  }
33
39
 
34
40
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
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": {