specsmd 0.1.53 → 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.
@@ -1172,6 +1172,215 @@ function getGitChangesSnapshot(snapshot) {
1172
1172
  };
1173
1173
  }
1174
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
+
1175
1384
  function getDashboardWorktreeMeta(snapshot) {
1176
1385
  if (!snapshot || typeof snapshot !== 'object') {
1177
1386
  return null;
@@ -2420,6 +2629,21 @@ function rowToFileEntry(row) {
2420
2629
  };
2421
2630
  }
2422
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
+
2423
2647
  function rowToWorktreeId(row) {
2424
2648
  if (!row || typeof row.key !== 'string') {
2425
2649
  return null;
@@ -3277,32 +3501,11 @@ function createDashboardApp(deps) {
3277
3501
  const gitRows = shouldHydrateSecondaryTabs
3278
3502
  ? (() => {
3279
3503
  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(
3504
+ return toExpandableRows(
3301
3505
  buildGitChangeGroups(snapshot),
3302
3506
  git.available ? 'Working tree clean' : 'No git changes',
3303
3507
  expandedGroups
3304
3508
  );
3305
- return [...headerRows, ...groups];
3306
3509
  })()
3307
3510
  : toLoadingRows('Loading git changes...', 'git-loading');
3308
3511
 
@@ -3338,6 +3541,9 @@ function createDashboardApp(deps) {
3338
3541
  const focusedIndex = selectionBySection[focusedSection] || 0;
3339
3542
  const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
3340
3543
  const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
3544
+ const selectedGitRow = getSelectedRow(gitRows, selectionBySection['git-changes'] || 0);
3545
+ const selectedGitFile = rowToFileEntry(selectedGitRow);
3546
+ const firstGitFile = firstFileEntryFromRows(gitRows);
3341
3547
 
3342
3548
  const refresh = useCallback(async (overrideSelectedWorktreeId = null) => {
3343
3549
  const now = new Date().toISOString();
@@ -4066,6 +4272,16 @@ function createDashboardApp(deps) {
4066
4272
  fullDocument: overlayPreviewOpen
4067
4273
  })
4068
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) : [];
4069
4285
 
4070
4286
  const shortcutsOverlayLines = buildHelpOverlayLines({
4071
4287
  view: ui.view,
@@ -4173,11 +4389,41 @@ function createDashboardApp(deps) {
4173
4389
  }
4174
4390
  } else if (ui.view === 'git') {
4175
4391
  panelCandidates = [
4392
+ {
4393
+ key: 'git-status',
4394
+ title: '[1]-Status',
4395
+ lines: gitStatusPanelLines,
4396
+ borderColor: 'green'
4397
+ },
4176
4398
  {
4177
4399
  key: 'git-changes',
4178
- title: panelTitles.git || 'Git Changes',
4400
+ title: '[2]-Files',
4179
4401
  lines: sectionLines['git-changes'],
4180
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'
4181
4427
  }
4182
4428
  ];
4183
4429
  } else {
@@ -4214,7 +4460,7 @@ function createDashboardApp(deps) {
4214
4460
  }
4215
4461
  }
4216
4462
 
4217
- if (!ui.showHelp && previewOpen && !overlayPreviewOpen) {
4463
+ if (!ui.showHelp && previewOpen && !overlayPreviewOpen && ui.view !== 'git') {
4218
4464
  panelCandidates.push({
4219
4465
  key: 'preview',
4220
4466
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
@@ -4225,7 +4471,13 @@ function createDashboardApp(deps) {
4225
4471
 
4226
4472
  if (ultraCompact) {
4227
4473
  if (previewOpen) {
4228
- 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
+ );
4229
4481
  } else {
4230
4482
  const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4231
4483
  panelCandidates = [focusedPanel || panelCandidates[0]];
@@ -4233,7 +4485,8 @@ function createDashboardApp(deps) {
4233
4485
  }
4234
4486
 
4235
4487
  const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
4236
- const lazyGitHierarchyLayout = !ui.showHelp
4488
+ const lazyGitHierarchyLayout = ui.view === 'git'
4489
+ && !ui.showHelp
4237
4490
  && !worktreeOverlayOpen
4238
4491
  && !overlayPreviewOpen
4239
4492
  && !ultraCompact
@@ -4254,9 +4507,10 @@ function createDashboardApp(deps) {
4254
4507
 
4255
4508
  let contentNode;
4256
4509
  if (lazyGitHierarchyLayout) {
4257
- const preferredRightPanel = previewOpen && !overlayPreviewOpen
4258
- ? panelCandidates.find((panel) => panel?.key === 'preview')
4259
- : null;
4510
+ const preferredRightPanel = panelCandidates.find((panel) => panel?.key === 'git-diff')
4511
+ || (previewOpen && !overlayPreviewOpen
4512
+ ? panelCandidates.find((panel) => panel?.key === 'preview')
4513
+ : null);
4260
4514
  const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4261
4515
  const rightPanelBase = preferredRightPanel
4262
4516
  || focusedPanel
@@ -4307,7 +4561,9 @@ function createDashboardApp(deps) {
4307
4561
  dense: densePanels,
4308
4562
  focused: rightPanel.key === 'preview'
4309
4563
  ? paneFocus === 'preview'
4310
- : (paneFocus === 'main' && rightPanel.key === focusedSection)
4564
+ : (rightPanel.key === 'git-diff'
4565
+ ? true
4566
+ : (paneFocus === 'main' && rightPanel.key === focusedSection))
4311
4567
  })
4312
4568
  )
4313
4569
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.53",
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": {