specsmd 0.1.52 → 0.1.54

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.
@@ -121,6 +121,11 @@ function truncate(value, width) {
121
121
  return `${sliceAnsi(text, 0, bodyWidth)}${ellipsis}`;
122
122
  }
123
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
+
124
129
  function normalizePanelLine(line) {
125
130
  if (line && typeof line === 'object' && !Array.isArray(line)) {
126
131
  return {
@@ -1167,6 +1172,215 @@ function getGitChangesSnapshot(snapshot) {
1167
1172
  };
1168
1173
  }
1169
1174
 
1175
+ function readGitCommandLines(repoRoot, args, options = {}) {
1176
+ if (typeof repoRoot !== 'string' || repoRoot.trim() === '' || !Array.isArray(args) || args.length === 0) {
1177
+ return [];
1178
+ }
1179
+
1180
+ const acceptedStatuses = Array.isArray(options.acceptedStatuses) && options.acceptedStatuses.length > 0
1181
+ ? options.acceptedStatuses
1182
+ : [0];
1183
+
1184
+ const result = spawnSync('git', args, {
1185
+ cwd: repoRoot,
1186
+ encoding: 'utf8',
1187
+ maxBuffer: 8 * 1024 * 1024
1188
+ });
1189
+
1190
+ if (result.error) {
1191
+ return [];
1192
+ }
1193
+
1194
+ if (typeof result.status === 'number' && !acceptedStatuses.includes(result.status)) {
1195
+ return [];
1196
+ }
1197
+
1198
+ const lines = String(result.stdout || '')
1199
+ .split(/\r?\n/)
1200
+ .map((line) => line.trim())
1201
+ .filter(Boolean);
1202
+
1203
+ const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : null;
1204
+ if (limit == null || lines.length <= limit) {
1205
+ return lines;
1206
+ }
1207
+ return lines.slice(0, limit);
1208
+ }
1209
+
1210
+ function buildGitStatusPanelLines(snapshot) {
1211
+ const git = getGitChangesSnapshot(snapshot);
1212
+ if (!git.available) {
1213
+ return [{
1214
+ text: 'Repository unavailable in selected worktree',
1215
+ color: 'red',
1216
+ bold: true
1217
+ }];
1218
+ }
1219
+
1220
+ const tracking = git.upstream
1221
+ ? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
1222
+ : 'no upstream';
1223
+
1224
+ return [
1225
+ {
1226
+ text: `branch: ${git.branch}${git.detached ? ' [detached]' : ''}`,
1227
+ color: 'green',
1228
+ bold: true
1229
+ },
1230
+ {
1231
+ text: `tracking: ${tracking}`,
1232
+ color: 'gray',
1233
+ bold: false
1234
+ },
1235
+ {
1236
+ text: `changes: ${git.counts.total || 0} total`,
1237
+ color: 'gray',
1238
+ bold: false
1239
+ },
1240
+ {
1241
+ text: `staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0}`,
1242
+ color: 'yellow',
1243
+ bold: false
1244
+ },
1245
+ {
1246
+ text: `untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
1247
+ color: 'yellow',
1248
+ bold: false
1249
+ }
1250
+ ];
1251
+ }
1252
+
1253
+ function buildGitBranchesPanelLines(snapshot) {
1254
+ const git = getGitChangesSnapshot(snapshot);
1255
+ if (!git.available) {
1256
+ return [{
1257
+ text: 'No branch list (git unavailable)',
1258
+ color: 'gray',
1259
+ bold: false
1260
+ }];
1261
+ }
1262
+
1263
+ const branchLines = readGitCommandLines(git.rootPath, [
1264
+ '-c',
1265
+ 'color.ui=false',
1266
+ 'for-each-ref',
1267
+ '--sort=-committerdate',
1268
+ '--format=%(if)%(HEAD)%(then)*%(else) %(end) %(refname:short)',
1269
+ 'refs/heads'
1270
+ ], { limit: 8 });
1271
+
1272
+ const lines = [{
1273
+ text: 'Local branches · Remotes · Tags',
1274
+ color: 'cyan',
1275
+ bold: true
1276
+ }];
1277
+
1278
+ if (branchLines.length === 0) {
1279
+ lines.push({
1280
+ text: 'No local branches found',
1281
+ color: 'gray',
1282
+ bold: false
1283
+ });
1284
+ return lines;
1285
+ }
1286
+
1287
+ for (const line of branchLines) {
1288
+ const isCurrent = line.startsWith('*');
1289
+ lines.push({
1290
+ text: line,
1291
+ color: isCurrent ? 'green' : 'gray',
1292
+ bold: isCurrent
1293
+ });
1294
+ }
1295
+
1296
+ return lines;
1297
+ }
1298
+
1299
+ function buildGitCommitsPanelLines(snapshot) {
1300
+ const git = getGitChangesSnapshot(snapshot);
1301
+ if (!git.available) {
1302
+ return [{
1303
+ text: 'No commit log (git unavailable)',
1304
+ color: 'gray',
1305
+ bold: false
1306
+ }];
1307
+ }
1308
+
1309
+ const reflogLines = readGitCommandLines(git.rootPath, [
1310
+ '-c',
1311
+ 'color.ui=false',
1312
+ 'reflog',
1313
+ '--date=relative',
1314
+ '--pretty=format:%h %gs'
1315
+ ], { limit: 8 });
1316
+
1317
+ const lines = [{
1318
+ text: 'Commits · Reflog',
1319
+ color: 'cyan',
1320
+ bold: true
1321
+ }];
1322
+
1323
+ if (reflogLines.length === 0) {
1324
+ lines.push({
1325
+ text: 'No reflog entries',
1326
+ color: 'gray',
1327
+ bold: false
1328
+ });
1329
+ return lines;
1330
+ }
1331
+
1332
+ lines.push(...reflogLines.map((line) => ({
1333
+ text: line,
1334
+ color: 'gray',
1335
+ bold: false
1336
+ })));
1337
+ return lines;
1338
+ }
1339
+
1340
+ function buildGitStashPanelLines(snapshot) {
1341
+ const git = getGitChangesSnapshot(snapshot);
1342
+ if (!git.available) {
1343
+ return [{
1344
+ text: 'No stash info (git unavailable)',
1345
+ color: 'gray',
1346
+ bold: false
1347
+ }];
1348
+ }
1349
+
1350
+ const stashLines = readGitCommandLines(git.rootPath, [
1351
+ '-c',
1352
+ 'color.ui=false',
1353
+ 'stash',
1354
+ 'list',
1355
+ '--pretty=format:%gd %s'
1356
+ ], {
1357
+ acceptedStatuses: [0],
1358
+ limit: 4
1359
+ });
1360
+
1361
+ const lines = [{
1362
+ text: 'Stash',
1363
+ color: 'cyan',
1364
+ bold: true
1365
+ }];
1366
+
1367
+ if (stashLines.length === 0) {
1368
+ lines.push({
1369
+ text: 'No stashes',
1370
+ color: 'gray',
1371
+ bold: false
1372
+ });
1373
+ return lines;
1374
+ }
1375
+
1376
+ lines.push(...stashLines.map((line) => ({
1377
+ text: line,
1378
+ color: 'gray',
1379
+ bold: false
1380
+ })));
1381
+ return lines;
1382
+ }
1383
+
1170
1384
  function getDashboardWorktreeMeta(snapshot) {
1171
1385
  if (!snapshot || typeof snapshot !== 'object') {
1172
1386
  return null;
@@ -2415,6 +2629,21 @@ function rowToFileEntry(row) {
2415
2629
  };
2416
2630
  }
2417
2631
 
2632
+ function firstFileEntryFromRows(rows) {
2633
+ if (!Array.isArray(rows) || rows.length === 0) {
2634
+ return null;
2635
+ }
2636
+
2637
+ for (const row of rows) {
2638
+ const entry = rowToFileEntry(row);
2639
+ if (entry) {
2640
+ return entry;
2641
+ }
2642
+ }
2643
+
2644
+ return null;
2645
+ }
2646
+
2418
2647
  function rowToWorktreeId(row) {
2419
2648
  if (!row || typeof row.key !== 'string') {
2420
2649
  return null;
@@ -2522,6 +2751,8 @@ function buildQuickHelpText(view, options = {}) {
2522
2751
  parts.push('b worktrees', 'u others');
2523
2752
  }
2524
2753
  parts.push('a current', 'f files');
2754
+ } else if (view === 'git') {
2755
+ parts.push('d changes', 'c/A/p/P git(soon)');
2525
2756
  }
2526
2757
  parts.push(`tab1 ${activeLabel}`);
2527
2758
 
@@ -2533,6 +2764,57 @@ function buildQuickHelpText(view, options = {}) {
2533
2764
  return parts.join(' | ');
2534
2765
  }
2535
2766
 
2767
+ function buildLazyGitCommandStrip(view, options = {}) {
2768
+ const {
2769
+ hasWorktrees = false,
2770
+ previewOpen = false
2771
+ } = options;
2772
+
2773
+ const parts = [];
2774
+
2775
+ if (view === 'runs') {
2776
+ if (hasWorktrees) {
2777
+ parts.push('b worktrees');
2778
+ }
2779
+ parts.push('a current', 'f files', 'enter expand');
2780
+ } else if (view === 'intents') {
2781
+ parts.push('n next', 'x completed', 'enter expand');
2782
+ } else if (view === 'completed') {
2783
+ parts.push('c completed', 'enter expand');
2784
+ } else if (view === 'health') {
2785
+ parts.push('s standards', 't stats', 'w warnings');
2786
+ } else if (view === 'git') {
2787
+ parts.push('d changes', 'space preview', 'c commit (soon)', 'A amend (soon)', 'p push (soon)', 'P pull (soon)');
2788
+ }
2789
+
2790
+ if (previewOpen) {
2791
+ parts.push('tab pane', 'j/k scroll');
2792
+ } else {
2793
+ parts.push('v preview');
2794
+ }
2795
+
2796
+ parts.push('1-5 views', 'g/G panels', 'r refresh', '? help', 'q quit');
2797
+ return parts.join(' | ');
2798
+ }
2799
+
2800
+ function buildLazyGitCommandLogLine(options = {}) {
2801
+ const {
2802
+ statusLine = '',
2803
+ activeFlow = 'fire',
2804
+ watchEnabled = true,
2805
+ watchStatus = 'watching',
2806
+ selectedWorktreeLabel = null
2807
+ } = options;
2808
+
2809
+ if (typeof statusLine === 'string' && statusLine.trim() !== '') {
2810
+ return `Command Log | ${statusLine}`;
2811
+ }
2812
+
2813
+ const watchLabel = watchEnabled ? watchStatus : 'off';
2814
+ const worktreeSegment = selectedWorktreeLabel ? ` | wt:${selectedWorktreeLabel}` : '';
2815
+ return `Command Log | flow:${String(activeFlow || 'fire').toUpperCase()} | watch:${watchLabel}${worktreeSegment} | ready`;
2816
+ }
2817
+
2536
2818
  function buildHelpOverlayLines(options = {}) {
2537
2819
  const {
2538
2820
  view = 'runs',
@@ -2592,6 +2874,7 @@ function buildHelpOverlayLines(options = {}) {
2592
2874
  { text: '', color: undefined, bold: false },
2593
2875
  { text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
2594
2876
  'select changed files and preview diffs',
2877
+ 'commit/push/pull shortcuts are shown in footer for LazyGit-style hierarchy',
2595
2878
  { text: '', color: undefined, bold: false },
2596
2879
  { text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
2597
2880
  );
@@ -2864,7 +3147,7 @@ function createDashboardApp(deps) {
2864
3147
  focused
2865
3148
  } = props;
2866
3149
 
2867
- const contentWidth = Math.max(18, width - 4);
3150
+ const contentWidth = Math.max(1, width - (dense ? 2 : 4));
2868
3151
  const visibleLines = fitLines(lines, maxLines, contentWidth);
2869
3152
  const panelBorderColor = focused ? 'cyan' : (borderColor || 'gray');
2870
3153
  const titleColor = focused ? 'black' : 'cyan';
@@ -3218,32 +3501,11 @@ function createDashboardApp(deps) {
3218
3501
  const gitRows = shouldHydrateSecondaryTabs
3219
3502
  ? (() => {
3220
3503
  const git = getGitChangesSnapshot(snapshot);
3221
- const tracking = git.upstream
3222
- ? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
3223
- : 'no upstream';
3224
- const headerRows = [{
3225
- kind: 'info',
3226
- key: 'git:branch',
3227
- label: git.available
3228
- ? `branch ${git.branch}${git.detached ? ' [detached]' : ''} | ${tracking}`
3229
- : 'git: repository unavailable in selected worktree',
3230
- color: git.available ? 'cyan' : 'red',
3231
- bold: true,
3232
- selectable: false
3233
- }, {
3234
- kind: 'info',
3235
- key: 'git:counts',
3236
- 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}`,
3237
- color: 'gray',
3238
- bold: false,
3239
- selectable: false
3240
- }];
3241
- const groups = toExpandableRows(
3504
+ return toExpandableRows(
3242
3505
  buildGitChangeGroups(snapshot),
3243
3506
  git.available ? 'Working tree clean' : 'No git changes',
3244
3507
  expandedGroups
3245
3508
  );
3246
- return [...headerRows, ...groups];
3247
3509
  })()
3248
3510
  : toLoadingRows('Loading git changes...', 'git-loading');
3249
3511
 
@@ -3279,6 +3541,9 @@ function createDashboardApp(deps) {
3279
3541
  const focusedIndex = selectionBySection[focusedSection] || 0;
3280
3542
  const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
3281
3543
  const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
3544
+ const selectedGitRow = getSelectedRow(gitRows, selectionBySection['git-changes'] || 0);
3545
+ const selectedGitFile = rowToFileEntry(selectedGitRow);
3546
+ const firstGitFile = firstFileEntryFromRows(gitRows);
3282
3547
 
3283
3548
  const refresh = useCallback(async (overrideSelectedWorktreeId = null) => {
3284
3549
  const now = new Date().toISOString();
@@ -3636,6 +3901,22 @@ function createDashboardApp(deps) {
3636
3901
  setSectionFocus((previous) => ({ ...previous, git: 'git-changes' }));
3637
3902
  return;
3638
3903
  }
3904
+ if (input === 'c') {
3905
+ setStatusLine('Git commit action is not wired yet (footer mirrors LazyGit command hierarchy).');
3906
+ return;
3907
+ }
3908
+ if (input === 'A') {
3909
+ setStatusLine('Git amend action is not wired yet (footer mirrors LazyGit command hierarchy).');
3910
+ return;
3911
+ }
3912
+ if (input === 'p') {
3913
+ setStatusLine('Git push action is not wired yet (footer mirrors LazyGit command hierarchy).');
3914
+ return;
3915
+ }
3916
+ if (input === 'P') {
3917
+ setStatusLine('Git pull action is not wired yet (footer mirrors LazyGit command hierarchy).');
3918
+ return;
3919
+ }
3639
3920
  }
3640
3921
 
3641
3922
  if (key.escape) {
@@ -3845,16 +4126,16 @@ function createDashboardApp(deps) {
3845
4126
  useEffect(() => {
3846
4127
  if (!stdout || typeof stdout.on !== 'function') {
3847
4128
  setTerminalSize({
3848
- columns: process.stdout.columns || 120,
3849
- rows: process.stdout.rows || 40
4129
+ columns: Math.max(1, process.stdout.columns || 120),
4130
+ rows: Math.max(1, process.stdout.rows || 40)
3850
4131
  });
3851
4132
  return undefined;
3852
4133
  }
3853
4134
 
3854
4135
  const updateSize = () => {
3855
4136
  setTerminalSize({
3856
- columns: stdout.columns || process.stdout.columns || 120,
3857
- rows: stdout.rows || process.stdout.rows || 40
4137
+ columns: Math.max(1, stdout.columns || process.stdout.columns || 120),
4138
+ rows: Math.max(1, stdout.rows || process.stdout.rows || 40)
3858
4139
  });
3859
4140
 
3860
4141
  // Resize in some terminals can leave stale frame rows behind.
@@ -3866,6 +4147,9 @@ function createDashboardApp(deps) {
3866
4147
 
3867
4148
  updateSize();
3868
4149
  stdout.on('resize', updateSize);
4150
+ if (process.stdout !== stdout && typeof process.stdout.on === 'function') {
4151
+ process.stdout.on('resize', updateSize);
4152
+ }
3869
4153
 
3870
4154
  return () => {
3871
4155
  if (typeof stdout.off === 'function') {
@@ -3873,6 +4157,13 @@ function createDashboardApp(deps) {
3873
4157
  } else if (typeof stdout.removeListener === 'function') {
3874
4158
  stdout.removeListener('resize', updateSize);
3875
4159
  }
4160
+ if (process.stdout !== stdout) {
4161
+ if (typeof process.stdout.off === 'function') {
4162
+ process.stdout.off('resize', updateSize);
4163
+ } else if (typeof process.stdout.removeListener === 'function') {
4164
+ process.stdout.removeListener('resize', updateSize);
4165
+ }
4166
+ }
3876
4167
  };
3877
4168
  }, [stdout]);
3878
4169
 
@@ -3937,37 +4228,31 @@ function createDashboardApp(deps) {
3937
4228
  const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
3938
4229
  const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
3939
4230
 
3940
- const fullWidth = Math.max(40, cols - 1);
4231
+ const fullWidth = resolveFrameWidth(cols);
3941
4232
  const showFlowBar = availableFlowIds.length > 1;
3942
- const showFooterHelpLine = rows >= 10;
4233
+ const showCommandLogLine = rows >= 8;
4234
+ const showCommandStrip = rows >= 9;
3943
4235
  const showErrorPanel = Boolean(error) && rows >= 18;
3944
4236
  const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp && !worktreeOverlayOpen;
3945
4237
  const showErrorInline = Boolean(error) && !showErrorPanel && !worktreeOverlayOpen;
3946
4238
  const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp && !worktreeOverlayOpen;
3947
- const showStatusLine = statusLine !== '';
4239
+ const showLegacyStatusLine = statusLine !== '' && !showCommandLogLine;
3948
4240
  const densePanels = rows <= 28 || cols <= 120;
3949
4241
 
3950
4242
  const reservedRows =
3951
4243
  2 +
3952
4244
  (showFlowBar ? 1 : 0) +
3953
4245
  (showApprovalBanner ? 1 : 0) +
3954
- (showFooterHelpLine ? 1 : 0) +
4246
+ (showCommandLogLine ? 1 : 0) +
4247
+ (showCommandStrip ? 1 : 0) +
3955
4248
  (showGlobalErrorPanel ? 5 : 0) +
3956
4249
  (showErrorInline ? 1 : 0) +
3957
- (showStatusLine ? 1 : 0);
4250
+ (showLegacyStatusLine ? 1 : 0);
3958
4251
  const frameSafetyRows = 2;
3959
4252
  const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
3960
4253
  const ultraCompact = rows <= 14;
3961
4254
  const panelTitles = getPanelTitles(activeFlow, snapshot);
3962
- const splitPreviewLayout = previewOpen && !overlayPreviewOpen && !ui.showHelp && !worktreeOverlayOpen && cols >= 110 && rows >= 16;
3963
- const mainPaneWidth = splitPreviewLayout
3964
- ? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
3965
- : fullWidth;
3966
- const previewPaneWidth = splitPreviewLayout
3967
- ? Math.max(30, fullWidth - mainPaneWidth - 1)
3968
- : fullWidth;
3969
- const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
3970
- const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
4255
+ const compactWidth = Math.max(18, fullWidth - 4);
3971
4256
 
3972
4257
  const sectionLines = Object.fromEntries(
3973
4258
  Object.entries(rowsBySection).map(([sectionKey, sectionRows]) => [
@@ -3976,17 +4261,27 @@ function createDashboardApp(deps) {
3976
4261
  sectionRows,
3977
4262
  selectionBySection[sectionKey] || 0,
3978
4263
  icons,
3979
- mainCompactWidth,
4264
+ compactWidth,
3980
4265
  paneFocus === 'main' && focusedSection === sectionKey
3981
4266
  )
3982
4267
  ])
3983
4268
  );
3984
4269
  const effectivePreviewTarget = previewTarget || selectedFocusedFile;
3985
4270
  const previewLines = previewOpen
3986
- ? buildPreviewLines(effectivePreviewTarget, previewCompactWidth, previewScroll, {
4271
+ ? buildPreviewLines(effectivePreviewTarget, compactWidth, previewScroll, {
3987
4272
  fullDocument: overlayPreviewOpen
3988
4273
  })
3989
4274
  : [];
4275
+ const gitInlineDiffTarget = selectedGitFile || firstGitFile || previewTarget || null;
4276
+ const gitInlineDiffLines = ui.view === 'git'
4277
+ ? buildPreviewLines(gitInlineDiffTarget, compactWidth, previewOpen && paneFocus === 'preview' ? previewScroll : 0, {
4278
+ fullDocument: false
4279
+ })
4280
+ : [];
4281
+ const gitStatusPanelLines = ui.view === 'git' ? buildGitStatusPanelLines(snapshot) : [];
4282
+ const gitBranchesPanelLines = ui.view === 'git' ? buildGitBranchesPanelLines(snapshot) : [];
4283
+ const gitCommitsPanelLines = ui.view === 'git' ? buildGitCommitsPanelLines(snapshot) : [];
4284
+ const gitStashPanelLines = ui.view === 'git' ? buildGitStashPanelLines(snapshot) : [];
3990
4285
 
3991
4286
  const shortcutsOverlayLines = buildHelpOverlayLines({
3992
4287
  view: ui.view,
@@ -4004,6 +4299,17 @@ function createDashboardApp(deps) {
4004
4299
  availableFlowCount: availableFlowIds.length,
4005
4300
  hasWorktrees: worktreeSectionEnabled
4006
4301
  });
4302
+ const commandStripText = buildLazyGitCommandStrip(ui.view, {
4303
+ hasWorktrees: worktreeSectionEnabled,
4304
+ previewOpen
4305
+ });
4306
+ const commandLogLine = buildLazyGitCommandLogLine({
4307
+ statusLine,
4308
+ activeFlow,
4309
+ watchEnabled,
4310
+ watchStatus,
4311
+ selectedWorktreeLabel
4312
+ });
4007
4313
 
4008
4314
  let panelCandidates;
4009
4315
  if (ui.showHelp) {
@@ -4083,11 +4389,41 @@ function createDashboardApp(deps) {
4083
4389
  }
4084
4390
  } else if (ui.view === 'git') {
4085
4391
  panelCandidates = [
4392
+ {
4393
+ key: 'git-status',
4394
+ title: '[1]-Status',
4395
+ lines: gitStatusPanelLines,
4396
+ borderColor: 'green'
4397
+ },
4086
4398
  {
4087
4399
  key: 'git-changes',
4088
- title: panelTitles.git || 'Git Changes',
4400
+ title: '[2]-Files',
4089
4401
  lines: sectionLines['git-changes'],
4090
4402
  borderColor: 'yellow'
4403
+ },
4404
+ {
4405
+ key: 'git-branches',
4406
+ title: '[3]-Local branches',
4407
+ lines: gitBranchesPanelLines,
4408
+ borderColor: 'magenta'
4409
+ },
4410
+ {
4411
+ key: 'git-commits',
4412
+ title: '[4]-Commits',
4413
+ lines: gitCommitsPanelLines,
4414
+ borderColor: 'cyan'
4415
+ },
4416
+ {
4417
+ key: 'git-stash',
4418
+ title: '[5]-Stash',
4419
+ lines: gitStashPanelLines,
4420
+ borderColor: 'blue'
4421
+ },
4422
+ {
4423
+ key: 'git-diff',
4424
+ title: '[0]-Unstaged changes',
4425
+ lines: gitInlineDiffLines,
4426
+ borderColor: 'yellow'
4091
4427
  }
4092
4428
  ];
4093
4429
  } else {
@@ -4124,7 +4460,7 @@ function createDashboardApp(deps) {
4124
4460
  }
4125
4461
  }
4126
4462
 
4127
- if (!ui.showHelp && previewOpen && !overlayPreviewOpen && !splitPreviewLayout) {
4463
+ if (!ui.showHelp && previewOpen && !overlayPreviewOpen && ui.view !== 'git') {
4128
4464
  panelCandidates.push({
4129
4465
  key: 'preview',
4130
4466
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
@@ -4133,9 +4469,15 @@ function createDashboardApp(deps) {
4133
4469
  });
4134
4470
  }
4135
4471
 
4136
- if (ultraCompact && !splitPreviewLayout) {
4472
+ if (ultraCompact) {
4137
4473
  if (previewOpen) {
4138
- panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === focusedSection || panel.key === 'preview'));
4474
+ panelCandidates = panelCandidates.filter((panel) =>
4475
+ panel && (
4476
+ panel.key === focusedSection
4477
+ || panel.key === 'preview'
4478
+ || (ui.view === 'git' && panel.key === 'git-diff')
4479
+ )
4480
+ );
4139
4481
  } else {
4140
4482
  const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4141
4483
  panelCandidates = [focusedPanel || panelCandidates[0]];
@@ -4143,6 +4485,13 @@ function createDashboardApp(deps) {
4143
4485
  }
4144
4486
 
4145
4487
  const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
4488
+ const lazyGitHierarchyLayout = ui.view === 'git'
4489
+ && !ui.showHelp
4490
+ && !worktreeOverlayOpen
4491
+ && !overlayPreviewOpen
4492
+ && !ultraCompact
4493
+ && fullWidth >= 96
4494
+ && panelCandidates.length > 1;
4146
4495
 
4147
4496
  const renderPanel = (panel, index, width, isFocused) => React.createElement(SectionPanel, {
4148
4497
  key: panel.key,
@@ -4157,14 +4506,22 @@ function createDashboardApp(deps) {
4157
4506
  });
4158
4507
 
4159
4508
  let contentNode;
4160
- if (splitPreviewLayout && !overlayPreviewOpen) {
4161
- const previewBodyLines = Math.max(1, contentRowsBudget - 3);
4162
- const previewPanel = {
4163
- key: 'preview-split',
4164
- title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
4165
- lines: previewLines,
4166
- borderColor: 'magenta',
4167
- maxLines: previewBodyLines
4509
+ if (lazyGitHierarchyLayout) {
4510
+ const preferredRightPanel = panelCandidates.find((panel) => panel?.key === 'git-diff')
4511
+ || (previewOpen && !overlayPreviewOpen
4512
+ ? panelCandidates.find((panel) => panel?.key === 'preview')
4513
+ : null);
4514
+ const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4515
+ const rightPanelBase = preferredRightPanel
4516
+ || focusedPanel
4517
+ || panelCandidates[panelCandidates.length - 1];
4518
+ const leftCandidates = panelCandidates.filter((panel) => panel?.key !== rightPanelBase?.key);
4519
+ const leftWidth = Math.max(30, Math.min(Math.floor(fullWidth * 0.38), fullWidth - 36));
4520
+ const rightWidth = Math.max(34, fullWidth - leftWidth - 1);
4521
+ const leftPanels = allocateSingleColumnPanels(leftCandidates, contentRowsBudget);
4522
+ const rightPanel = {
4523
+ ...rightPanelBase,
4524
+ maxLines: Math.max(4, contentRowsBudget)
4168
4525
  };
4169
4526
 
4170
4527
  contentNode = React.createElement(
@@ -4172,33 +4529,41 @@ function createDashboardApp(deps) {
4172
4529
  { width: fullWidth, flexDirection: 'row' },
4173
4530
  React.createElement(
4174
4531
  Box,
4175
- { width: mainPaneWidth, flexDirection: 'column' },
4176
- ...panels.map((panel, index) => React.createElement(SectionPanel, {
4532
+ { width: leftWidth, flexDirection: 'column' },
4533
+ ...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
4177
4534
  key: panel.key,
4178
4535
  title: panel.title,
4179
4536
  lines: panel.lines,
4180
- width: mainPaneWidth,
4537
+ width: leftWidth,
4181
4538
  maxLines: panel.maxLines,
4182
4539
  borderColor: panel.borderColor,
4183
- marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
4540
+ marginBottom: densePanels ? 0 : (index === leftPanels.length - 1 ? 0 : 1),
4184
4541
  dense: densePanels,
4185
4542
  focused: paneFocus === 'main' && panel.key === focusedSection
4186
4543
  }))
4187
4544
  ),
4188
- React.createElement(Box, { width: 1 }, React.createElement(Text, null, ' ')),
4189
4545
  React.createElement(
4190
4546
  Box,
4191
- { width: previewPaneWidth, flexDirection: 'column' },
4547
+ { width: 1, justifyContent: 'center' },
4548
+ React.createElement(Text, { color: 'gray' }, '│')
4549
+ ),
4550
+ React.createElement(
4551
+ Box,
4552
+ { width: rightWidth, flexDirection: 'column' },
4192
4553
  React.createElement(SectionPanel, {
4193
- key: previewPanel.key,
4194
- title: previewPanel.title,
4195
- lines: previewPanel.lines,
4196
- width: previewPaneWidth,
4197
- maxLines: previewPanel.maxLines,
4198
- borderColor: previewPanel.borderColor,
4554
+ key: rightPanel.key,
4555
+ title: rightPanel.title,
4556
+ lines: rightPanel.lines,
4557
+ width: rightWidth,
4558
+ maxLines: rightPanel.maxLines,
4559
+ borderColor: rightPanel.borderColor,
4199
4560
  marginBottom: 0,
4200
4561
  dense: densePanels,
4201
- focused: paneFocus === 'preview'
4562
+ focused: rightPanel.key === 'preview'
4563
+ ? paneFocus === 'preview'
4564
+ : (rightPanel.key === 'git-diff'
4565
+ ? true
4566
+ : (paneFocus === 'main' && rightPanel.key === focusedSection))
4202
4567
  })
4203
4568
  )
4204
4569
  );
@@ -4248,12 +4613,25 @@ function createDashboardApp(deps) {
4248
4613
  })
4249
4614
  : null,
4250
4615
  ...(Array.isArray(contentNode) ? contentNode : [contentNode]),
4251
- statusLine !== ''
4616
+ showLegacyStatusLine
4252
4617
  ? React.createElement(Text, { color: 'yellow' }, truncate(statusLine, fullWidth))
4253
4618
  : null,
4254
- showFooterHelpLine
4255
- ? React.createElement(Text, { color: 'gray' }, truncate(quickHelpText, fullWidth))
4256
- : null
4619
+ showCommandLogLine
4620
+ ? React.createElement(
4621
+ Text,
4622
+ { color: 'white', backgroundColor: 'gray', bold: true },
4623
+ truncate(commandLogLine, fullWidth)
4624
+ )
4625
+ : null,
4626
+ showCommandStrip
4627
+ ? React.createElement(
4628
+ Text,
4629
+ { color: 'white', backgroundColor: 'blue', bold: true },
4630
+ truncate(commandStripText, fullWidth)
4631
+ )
4632
+ : (rows >= 10
4633
+ ? React.createElement(Text, { color: 'gray' }, truncate(quickHelpText, fullWidth))
4634
+ : null)
4257
4635
  );
4258
4636
  }
4259
4637
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
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": {