specsmd 0.1.55 → 0.1.56

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.
@@ -324,7 +324,39 @@ function loadGitDiffPreview(changeEntry) {
324
324
  return result.stdout;
325
325
  }
326
326
 
327
+ function loadGitCommitPreview(changeEntry) {
328
+ const repoRoot = typeof changeEntry?.repoRoot === 'string'
329
+ ? changeEntry.repoRoot
330
+ : (typeof changeEntry?.workspacePath === 'string' ? findGitRoot(changeEntry.workspacePath) : null);
331
+ const commitHash = typeof changeEntry?.commitHash === 'string'
332
+ ? changeEntry.commitHash.trim()
333
+ : '';
334
+
335
+ if (!repoRoot) {
336
+ return '[git] repository is unavailable for commit preview.';
337
+ }
338
+ if (commitHash === '') {
339
+ return '[git] no commit selected.';
340
+ }
341
+
342
+ const result = runGit(
343
+ ['-c', 'color.ui=false', '--no-pager', 'show', '--patch', '--stat', '--no-ext-diff', commitHash],
344
+ repoRoot
345
+ );
346
+ if (!result.ok) {
347
+ return `[git] unable to load commit diff: ${result.error}`;
348
+ }
349
+
350
+ const output = result.stdout.trim();
351
+ if (output === '') {
352
+ return '[git] no commit output for this selection.';
353
+ }
354
+
355
+ return result.stdout;
356
+ }
357
+
327
358
  module.exports = {
328
359
  listGitChanges,
329
- loadGitDiffPreview
360
+ loadGitDiffPreview,
361
+ loadGitCommitPreview
330
362
  };
@@ -5,7 +5,6 @@ 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');
9
8
 
10
9
  const stringWidth = typeof stringWidthModule === 'function'
11
10
  ? stringWidthModule
@@ -13,6 +12,10 @@ const stringWidth = typeof stringWidthModule === 'function'
13
12
  const sliceAnsi = typeof sliceAnsiModule === 'function'
14
13
  ? sliceAnsiModule
15
14
  : sliceAnsiModule.default;
15
+ const {
16
+ loadGitDiffPreview,
17
+ loadGitCommitPreview
18
+ } = require('../git/changes');
16
19
 
17
20
  function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
18
21
  if (!error) {
@@ -1250,135 +1253,51 @@ function buildGitStatusPanelLines(snapshot) {
1250
1253
  ];
1251
1254
  }
1252
1255
 
1253
- function buildGitBranchesPanelLines(snapshot) {
1256
+ function buildGitCommitRows(snapshot) {
1254
1257
  const git = getGitChangesSnapshot(snapshot);
1255
1258
  if (!git.available) {
1256
1259
  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
1260
+ kind: 'info',
1261
+ key: 'git:commits:unavailable',
1262
+ label: 'No commit history (git unavailable)',
1263
+ selectable: false
1306
1264
  }];
1307
1265
  }
1308
1266
 
1309
- const reflogLines = readGitCommandLines(git.rootPath, [
1267
+ const commitLines = readGitCommandLines(git.rootPath, [
1310
1268
  '-c',
1311
1269
  'color.ui=false',
1312
- 'reflog',
1270
+ 'log',
1313
1271
  '--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
- }
1272
+ '--pretty=format:%h %s',
1273
+ '--max-count=30'
1274
+ ], { limit: 30 });
1331
1275
 
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) {
1276
+ if (commitLines.length === 0) {
1343
1277
  return [{
1344
- text: 'No stash info (git unavailable)',
1345
- color: 'gray',
1346
- bold: false
1278
+ kind: 'info',
1279
+ key: 'git:commits:empty',
1280
+ label: 'No commits found',
1281
+ selectable: false
1347
1282
  }];
1348
1283
  }
1349
1284
 
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
- }
1285
+ return commitLines.map((line, index) => {
1286
+ const firstSpace = line.indexOf(' ');
1287
+ const commitHash = firstSpace > 0 ? line.slice(0, firstSpace) : '';
1288
+ const message = firstSpace > 0 ? line.slice(firstSpace + 1) : line;
1289
+ const label = commitHash ? `${commitHash} ${message}` : message;
1375
1290
 
1376
- lines.push(...stashLines.map((line) => ({
1377
- text: line,
1378
- color: 'gray',
1379
- bold: false
1380
- })));
1381
- return lines;
1291
+ return {
1292
+ kind: 'git-commit',
1293
+ key: `git:commit:${commitHash || index}:${index}`,
1294
+ label,
1295
+ commitHash,
1296
+ repoRoot: git.rootPath,
1297
+ previewType: 'git-commit-diff',
1298
+ selectable: true
1299
+ };
1300
+ });
1382
1301
  }
1383
1302
 
1384
1303
  function getDashboardWorktreeMeta(snapshot) {
@@ -1558,7 +1477,7 @@ function getSectionOrderForView(view, options = {}) {
1558
1477
  return ['standards', 'stats', 'warnings', 'error-details'];
1559
1478
  }
1560
1479
  if (view === 'git') {
1561
- return ['git-status', 'git-changes', 'git-branches', 'git-commits', 'git-stash', 'git-diff'];
1480
+ return ['git-status', 'git-changes', 'git-commits', 'git-diff'];
1562
1481
  }
1563
1482
  const sections = [];
1564
1483
  if (includeWorktrees) {
@@ -2578,7 +2497,7 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
2578
2497
  };
2579
2498
  }
2580
2499
 
2581
- if (row.kind === 'file' || row.kind === 'git-file') {
2500
+ if (row.kind === 'file' || row.kind === 'git-file' || row.kind === 'git-commit') {
2582
2501
  const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
2583
2502
  return {
2584
2503
  text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
@@ -2615,7 +2534,26 @@ function getSelectedRow(rows, selectedIndex) {
2615
2534
  }
2616
2535
 
2617
2536
  function rowToFileEntry(row) {
2618
- if (!row || (row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
2537
+ if (!row) {
2538
+ return null;
2539
+ }
2540
+
2541
+ if (row.kind === 'git-commit') {
2542
+ const commitHash = typeof row.commitHash === 'string' ? row.commitHash : '';
2543
+ if (commitHash === '') {
2544
+ return null;
2545
+ }
2546
+ return {
2547
+ label: row.label || commitHash,
2548
+ path: commitHash,
2549
+ scope: 'commit',
2550
+ previewType: row.previewType || 'git-commit-diff',
2551
+ repoRoot: row.repoRoot,
2552
+ commitHash
2553
+ };
2554
+ }
2555
+
2556
+ if ((row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
2619
2557
  return null;
2620
2558
  }
2621
2559
  return {
@@ -2752,7 +2690,7 @@ function buildQuickHelpText(view, options = {}) {
2752
2690
  }
2753
2691
  parts.push('a current', 'f files');
2754
2692
  } else if (view === 'git') {
2755
- parts.push('6 status', '7 files', '8 branches', '9 commits', '0 stash', '- diff');
2693
+ parts.push('6 status', '7 files', '8 commits', '- diff');
2756
2694
  }
2757
2695
  parts.push(`tab1 ${activeLabel}`);
2758
2696
 
@@ -2784,7 +2722,7 @@ function buildGitCommandStrip(view, options = {}) {
2784
2722
  } else if (view === 'health') {
2785
2723
  parts.push('s standards', 't stats', 'w warnings');
2786
2724
  } else if (view === 'git') {
2787
- parts.push('6 status', '7 files', '8 branches', '9 commits', '0 stash', '- diff', 'space preview');
2725
+ parts.push('6 status', '7 files', '8 commits', '- diff', 'space preview');
2788
2726
  }
2789
2727
 
2790
2728
  if (previewOpen) {
@@ -2873,8 +2811,9 @@ function buildHelpOverlayLines(options = {}) {
2873
2811
  `s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
2874
2812
  { text: '', color: undefined, bold: false },
2875
2813
  { text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
2876
- 'select changed files and preview diffs',
2877
- '6 status | 7 files | 8 branches | 9 commits | 0 stash | - diff',
2814
+ '7 files: select changed files and preview per-file diffs',
2815
+ '8 commits: select a commit to preview the full commit diff',
2816
+ '6 status | 7 files | 8 commits | - diff',
2878
2817
  { text: '', color: undefined, bold: false },
2879
2818
  { text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
2880
2819
  );
@@ -2994,10 +2933,14 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
2994
2933
  return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
2995
2934
  }
2996
2935
 
2997
- const isGitPreview = fileEntry.previewType === 'git-diff';
2936
+ const isGitFilePreview = fileEntry.previewType === 'git-diff';
2937
+ const isGitCommitPreview = fileEntry.previewType === 'git-commit-diff';
2938
+ const isGitPreview = isGitFilePreview || isGitCommitPreview;
2998
2939
  let rawLines = [];
2999
2940
  if (isGitPreview) {
3000
- const diffText = loadGitDiffPreview(fileEntry);
2941
+ const diffText = isGitCommitPreview
2942
+ ? loadGitCommitPreview(fileEntry)
2943
+ : loadGitDiffPreview(fileEntry);
3001
2944
  rawLines = String(diffText || '').split(/\r?\n/);
3002
2945
  } else {
3003
2946
  let content;
@@ -3014,7 +2957,12 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
3014
2957
  }
3015
2958
 
3016
2959
  const headLine = {
3017
- text: truncate(`${isGitPreview ? 'diff' : 'file'}: ${fileEntry.path}`, width),
2960
+ text: truncate(
2961
+ isGitCommitPreview
2962
+ ? `commit: ${fileEntry.commitHash || fileEntry.path}`
2963
+ : `${isGitPreview ? 'diff' : 'file'}: ${fileEntry.path}`,
2964
+ width
2965
+ ),
3018
2966
  color: 'cyan',
3019
2967
  bold: true
3020
2968
  };
@@ -3331,7 +3279,7 @@ function createDashboardApp(deps) {
3331
3279
  intents: 'intent-status',
3332
3280
  completed: 'completed-runs',
3333
3281
  health: 'standards',
3334
- git: 'git-changes'
3282
+ git: 'git-status'
3335
3283
  });
3336
3284
  const [selectionBySection, setSelectionBySection] = useState({
3337
3285
  worktrees: 0,
@@ -3344,7 +3292,8 @@ function createDashboardApp(deps) {
3344
3292
  stats: 0,
3345
3293
  warnings: 0,
3346
3294
  'error-details': 0,
3347
- 'git-changes': 0
3295
+ 'git-changes': 0,
3296
+ 'git-commits': 0
3348
3297
  });
3349
3298
  const [expandedGroups, setExpandedGroups] = useState({});
3350
3299
  const [previewTarget, setPreviewTarget] = useState(null);
@@ -3508,6 +3457,9 @@ function createDashboardApp(deps) {
3508
3457
  );
3509
3458
  })()
3510
3459
  : toLoadingRows('Loading git changes...', 'git-loading');
3460
+ const gitCommitRows = shouldHydrateSecondaryTabs
3461
+ ? buildGitCommitRows(snapshot)
3462
+ : toLoadingRows('Loading commit history...', 'git-commits-loading');
3511
3463
 
3512
3464
  const rowsBySection = {
3513
3465
  worktrees: worktreeRows,
@@ -3520,7 +3472,8 @@ function createDashboardApp(deps) {
3520
3472
  stats: statsRows,
3521
3473
  warnings: warningsRows,
3522
3474
  'error-details': errorDetailsRows,
3523
- 'git-changes': gitRows
3475
+ 'git-changes': gitRows,
3476
+ 'git-commits': gitCommitRows
3524
3477
  };
3525
3478
  const worktreeItems = getWorktreeItems(snapshot);
3526
3479
  const selectedWorktree = getSelectedWorktree(snapshot);
@@ -3543,6 +3496,8 @@ function createDashboardApp(deps) {
3543
3496
  const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
3544
3497
  const selectedGitRow = getSelectedRow(gitRows, selectionBySection['git-changes'] || 0);
3545
3498
  const selectedGitFile = rowToFileEntry(selectedGitRow);
3499
+ const selectedGitCommitRow = getSelectedRow(gitCommitRows, selectionBySection['git-commits'] || 0);
3500
+ const selectedGitCommit = rowToFileEntry(selectedGitCommitRow);
3546
3501
  const firstGitFile = firstFileEntryFromRows(gitRows);
3547
3502
 
3548
3503
  const refresh = useCallback(async (overrideSelectedWorktreeId = null) => {
@@ -3742,14 +3697,15 @@ function createDashboardApp(deps) {
3742
3697
  stats: 0,
3743
3698
  warnings: 0,
3744
3699
  'error-details': 0,
3745
- 'git-changes': 0
3700
+ 'git-changes': 0,
3701
+ 'git-commits': 0
3746
3702
  });
3747
3703
  setSectionFocus({
3748
3704
  runs: 'current-run',
3749
3705
  intents: 'intent-status',
3750
3706
  completed: 'completed-runs',
3751
3707
  health: 'standards',
3752
- git: 'git-changes'
3708
+ git: 'git-status'
3753
3709
  });
3754
3710
  setOverviewIntentFilter('next');
3755
3711
  setExpandedGroups({});
@@ -3785,14 +3741,15 @@ function createDashboardApp(deps) {
3785
3741
  stats: 0,
3786
3742
  warnings: 0,
3787
3743
  'error-details': 0,
3788
- 'git-changes': 0
3744
+ 'git-changes': 0,
3745
+ 'git-commits': 0
3789
3746
  });
3790
3747
  setSectionFocus({
3791
3748
  runs: 'current-run',
3792
3749
  intents: 'intent-status',
3793
3750
  completed: 'completed-runs',
3794
3751
  health: 'standards',
3795
- git: 'git-changes'
3752
+ git: 'git-status'
3796
3753
  });
3797
3754
  setOverviewIntentFilter('next');
3798
3755
  setExpandedGroups({});
@@ -3908,20 +3865,10 @@ function createDashboardApp(deps) {
3908
3865
  return;
3909
3866
  }
3910
3867
  if (input === '8') {
3911
- setSectionFocus((previous) => ({ ...previous, git: 'git-branches' }));
3912
- setPaneFocus('main');
3913
- return;
3914
- }
3915
- if (input === '9') {
3916
3868
  setSectionFocus((previous) => ({ ...previous, git: 'git-commits' }));
3917
3869
  setPaneFocus('main');
3918
3870
  return;
3919
3871
  }
3920
- if (input === '0') {
3921
- setSectionFocus((previous) => ({ ...previous, git: 'git-stash' }));
3922
- setPaneFocus('main');
3923
- return;
3924
- }
3925
3872
  if (input === '-') {
3926
3873
  setSectionFocus((previous) => ({ ...previous, git: 'git-diff' }));
3927
3874
  setPaneFocus('main');
@@ -4038,6 +3985,10 @@ function createDashboardApp(deps) {
4038
3985
 
4039
3986
  if (input === 'o') {
4040
3987
  const target = selectedFocusedFile || previewTarget;
3988
+ if (target?.previewType === 'git-commit-diff') {
3989
+ setStatusLine('Commit entries cannot be opened as files.');
3990
+ return;
3991
+ }
4041
3992
  const result = openFileWithDefaultApp(target?.path);
4042
3993
  setStatusLine(result.message);
4043
3994
  }
@@ -4282,16 +4233,17 @@ function createDashboardApp(deps) {
4282
4233
  fullDocument: overlayPreviewOpen
4283
4234
  })
4284
4235
  : [];
4285
- const gitInlineDiffTarget = selectedGitFile || firstGitFile || previewTarget || null;
4236
+ const gitInlineDiffTarget = (
4237
+ focusedSection === 'git-commits'
4238
+ ? (selectedGitCommit || selectedGitFile || firstGitFile || previewTarget)
4239
+ : (selectedGitFile || firstGitFile || previewTarget)
4240
+ ) || null;
4286
4241
  const gitInlineDiffLines = ui.view === 'git'
4287
4242
  ? buildPreviewLines(gitInlineDiffTarget, compactWidth, previewOpen && paneFocus === 'preview' ? previewScroll : 0, {
4288
4243
  fullDocument: false
4289
4244
  })
4290
4245
  : [];
4291
4246
  const gitStatusPanelLines = ui.view === 'git' ? buildGitStatusPanelLines(snapshot) : [];
4292
- const gitBranchesPanelLines = ui.view === 'git' ? buildGitBranchesPanelLines(snapshot) : [];
4293
- const gitCommitsPanelLines = ui.view === 'git' ? buildGitCommitsPanelLines(snapshot) : [];
4294
- const gitStashPanelLines = ui.view === 'git' ? buildGitStashPanelLines(snapshot) : [];
4295
4247
 
4296
4248
  const shortcutsOverlayLines = buildHelpOverlayLines({
4297
4249
  view: ui.view,
@@ -4411,27 +4363,15 @@ function createDashboardApp(deps) {
4411
4363
  lines: sectionLines['git-changes'],
4412
4364
  borderColor: 'yellow'
4413
4365
  },
4414
- {
4415
- key: 'git-branches',
4416
- title: '[8]-Local branches',
4417
- lines: gitBranchesPanelLines,
4418
- borderColor: 'magenta'
4419
- },
4420
4366
  {
4421
4367
  key: 'git-commits',
4422
- title: '[9]-Commits',
4423
- lines: gitCommitsPanelLines,
4368
+ title: '[8]-Commits',
4369
+ lines: sectionLines['git-commits'],
4424
4370
  borderColor: 'cyan'
4425
4371
  },
4426
- {
4427
- key: 'git-stash',
4428
- title: '[0]-Stash',
4429
- lines: gitStashPanelLines,
4430
- borderColor: 'blue'
4431
- },
4432
4372
  {
4433
4373
  key: 'git-diff',
4434
- title: '[-]-Unstaged changes',
4374
+ title: focusedSection === 'git-commits' ? '[-]-Selected commit diff' : '[-]-Unstaged changes',
4435
4375
  lines: gitInlineDiffLines,
4436
4376
  borderColor: 'yellow'
4437
4377
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
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": {