specsmd 0.1.51 → 0.1.52
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.
- package/lib/dashboard/git/changes.js +330 -0
- package/lib/dashboard/index.js +3 -1
- package/lib/dashboard/tui/app.js +239 -34
- package/lib/dashboard/tui/store.js +8 -2
- package/package.json +1 -1
|
@@ -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
|
+
};
|
package/lib/dashboard/index.js
CHANGED
|
@@ -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
|
};
|
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -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: '',
|
|
@@ -1120,7 +1123,47 @@ function getPanelTitles(flow, snapshot) {
|
|
|
1120
1123
|
files: 'Run Files',
|
|
1121
1124
|
pending: 'Pending Queue',
|
|
1122
1125
|
completed: 'Recent Completed Runs',
|
|
1123
|
-
otherWorktrees: 'Other Worktrees: Active Runs'
|
|
1126
|
+
otherWorktrees: 'Other Worktrees: Active Runs',
|
|
1127
|
+
git: 'Git Changes'
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function getGitChangesSnapshot(snapshot) {
|
|
1132
|
+
const gitChanges = snapshot?.gitChanges;
|
|
1133
|
+
if (!gitChanges || typeof gitChanges !== 'object') {
|
|
1134
|
+
return {
|
|
1135
|
+
available: false,
|
|
1136
|
+
branch: '(unavailable)',
|
|
1137
|
+
upstream: null,
|
|
1138
|
+
ahead: 0,
|
|
1139
|
+
behind: 0,
|
|
1140
|
+
counts: {
|
|
1141
|
+
total: 0,
|
|
1142
|
+
staged: 0,
|
|
1143
|
+
unstaged: 0,
|
|
1144
|
+
untracked: 0,
|
|
1145
|
+
conflicted: 0
|
|
1146
|
+
},
|
|
1147
|
+
staged: [],
|
|
1148
|
+
unstaged: [],
|
|
1149
|
+
untracked: [],
|
|
1150
|
+
conflicted: [],
|
|
1151
|
+
clean: true
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
...gitChanges,
|
|
1156
|
+
counts: gitChanges.counts || {
|
|
1157
|
+
total: 0,
|
|
1158
|
+
staged: 0,
|
|
1159
|
+
unstaged: 0,
|
|
1160
|
+
untracked: 0,
|
|
1161
|
+
conflicted: 0
|
|
1162
|
+
},
|
|
1163
|
+
staged: Array.isArray(gitChanges.staged) ? gitChanges.staged : [],
|
|
1164
|
+
unstaged: Array.isArray(gitChanges.unstaged) ? gitChanges.unstaged : [],
|
|
1165
|
+
untracked: Array.isArray(gitChanges.untracked) ? gitChanges.untracked : [],
|
|
1166
|
+
conflicted: Array.isArray(gitChanges.conflicted) ? gitChanges.conflicted : []
|
|
1124
1167
|
};
|
|
1125
1168
|
}
|
|
1126
1169
|
|
|
@@ -1300,6 +1343,9 @@ function getSectionOrderForView(view, options = {}) {
|
|
|
1300
1343
|
if (view === 'health') {
|
|
1301
1344
|
return ['standards', 'stats', 'warnings', 'error-details'];
|
|
1302
1345
|
}
|
|
1346
|
+
if (view === 'git') {
|
|
1347
|
+
return ['git-changes'];
|
|
1348
|
+
}
|
|
1303
1349
|
const sections = [];
|
|
1304
1350
|
if (includeWorktrees) {
|
|
1305
1351
|
sections.push('worktrees');
|
|
@@ -1640,6 +1686,10 @@ function formatScope(scope) {
|
|
|
1640
1686
|
if (scope === 'upcoming') return 'UPNEXT';
|
|
1641
1687
|
if (scope === 'completed') return 'DONE';
|
|
1642
1688
|
if (scope === 'intent') return 'INTENT';
|
|
1689
|
+
if (scope === 'staged') return 'STAGED';
|
|
1690
|
+
if (scope === 'unstaged') return 'UNSTAGED';
|
|
1691
|
+
if (scope === 'untracked') return 'UNTRACKED';
|
|
1692
|
+
if (scope === 'conflicted') return 'CONFLICT';
|
|
1643
1693
|
return 'FILE';
|
|
1644
1694
|
}
|
|
1645
1695
|
|
|
@@ -2054,9 +2104,15 @@ function collectAidlcIntentContextFiles(snapshot, intentId) {
|
|
|
2054
2104
|
}
|
|
2055
2105
|
|
|
2056
2106
|
function filterExistingFiles(files) {
|
|
2057
|
-
return (Array.isArray(files) ? files : []).filter((file) =>
|
|
2058
|
-
file
|
|
2059
|
-
|
|
2107
|
+
return (Array.isArray(files) ? files : []).filter((file) => {
|
|
2108
|
+
if (!file || typeof file.path !== 'string' || typeof file.label !== 'string') {
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
if (file.allowMissing === true) {
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2114
|
+
return fileExists(file.path);
|
|
2115
|
+
});
|
|
2060
2116
|
}
|
|
2061
2117
|
|
|
2062
2118
|
function buildPendingGroups(snapshot, flow) {
|
|
@@ -2191,6 +2247,49 @@ function buildCompletedGroups(snapshot, flow) {
|
|
|
2191
2247
|
return groups;
|
|
2192
2248
|
}
|
|
2193
2249
|
|
|
2250
|
+
function buildGitChangeGroups(snapshot) {
|
|
2251
|
+
const git = getGitChangesSnapshot(snapshot);
|
|
2252
|
+
|
|
2253
|
+
if (!git.available) {
|
|
2254
|
+
return [];
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
const makeFiles = (items, scope) => items.map((item) => ({
|
|
2258
|
+
label: item.relativePath,
|
|
2259
|
+
path: item.path || path.join(git.rootPath || snapshot?.workspacePath || '', item.relativePath || ''),
|
|
2260
|
+
scope,
|
|
2261
|
+
allowMissing: true,
|
|
2262
|
+
previewType: 'git-diff',
|
|
2263
|
+
repoRoot: git.rootPath || snapshot?.workspacePath || '',
|
|
2264
|
+
relativePath: item.relativePath || '',
|
|
2265
|
+
bucket: item.bucket || scope
|
|
2266
|
+
}));
|
|
2267
|
+
|
|
2268
|
+
const groups = [];
|
|
2269
|
+
groups.push({
|
|
2270
|
+
key: 'git:staged',
|
|
2271
|
+
label: `staged (${git.counts.staged || 0})`,
|
|
2272
|
+
files: makeFiles(git.staged, 'staged')
|
|
2273
|
+
});
|
|
2274
|
+
groups.push({
|
|
2275
|
+
key: 'git:unstaged',
|
|
2276
|
+
label: `unstaged (${git.counts.unstaged || 0})`,
|
|
2277
|
+
files: makeFiles(git.unstaged, 'unstaged')
|
|
2278
|
+
});
|
|
2279
|
+
groups.push({
|
|
2280
|
+
key: 'git:untracked',
|
|
2281
|
+
label: `untracked (${git.counts.untracked || 0})`,
|
|
2282
|
+
files: makeFiles(git.untracked, 'untracked')
|
|
2283
|
+
});
|
|
2284
|
+
groups.push({
|
|
2285
|
+
key: 'git:conflicted',
|
|
2286
|
+
label: `conflicts (${git.counts.conflicted || 0})`,
|
|
2287
|
+
files: makeFiles(git.conflicted, 'conflicted')
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
return groups;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2194
2293
|
function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
2195
2294
|
if (!Array.isArray(groups) || groups.length === 0) {
|
|
2196
2295
|
return [{
|
|
@@ -2221,12 +2320,16 @@ function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
|
2221
2320
|
for (let index = 0; index < files.length; index += 1) {
|
|
2222
2321
|
const file = files[index];
|
|
2223
2322
|
rows.push({
|
|
2224
|
-
kind: 'file',
|
|
2323
|
+
kind: file.previewType === 'git-diff' ? 'git-file' : 'file',
|
|
2225
2324
|
key: `${group.key}:file:${file.path}:${index}`,
|
|
2226
2325
|
label: file.label,
|
|
2227
2326
|
path: file.path,
|
|
2228
2327
|
scope: file.scope || 'file',
|
|
2229
|
-
selectable: true
|
|
2328
|
+
selectable: true,
|
|
2329
|
+
previewType: file.previewType,
|
|
2330
|
+
repoRoot: file.repoRoot,
|
|
2331
|
+
relativePath: file.relativePath,
|
|
2332
|
+
bucket: file.bucket
|
|
2230
2333
|
});
|
|
2231
2334
|
}
|
|
2232
2335
|
}
|
|
@@ -2261,7 +2364,7 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
|
|
|
2261
2364
|
};
|
|
2262
2365
|
}
|
|
2263
2366
|
|
|
2264
|
-
if (row.kind === 'file') {
|
|
2367
|
+
if (row.kind === 'file' || row.kind === 'git-file') {
|
|
2265
2368
|
const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
|
|
2266
2369
|
return {
|
|
2267
2370
|
text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
|
|
@@ -2298,13 +2401,17 @@ function getSelectedRow(rows, selectedIndex) {
|
|
|
2298
2401
|
}
|
|
2299
2402
|
|
|
2300
2403
|
function rowToFileEntry(row) {
|
|
2301
|
-
if (!row || row.kind !== 'file' || typeof row.path !== 'string') {
|
|
2404
|
+
if (!row || (row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
|
|
2302
2405
|
return null;
|
|
2303
2406
|
}
|
|
2304
2407
|
return {
|
|
2305
2408
|
label: row.label || path.basename(row.path),
|
|
2306
2409
|
path: row.path,
|
|
2307
|
-
scope: row.scope || 'file'
|
|
2410
|
+
scope: row.scope || 'file',
|
|
2411
|
+
previewType: row.previewType,
|
|
2412
|
+
repoRoot: row.repoRoot,
|
|
2413
|
+
relativePath: row.relativePath,
|
|
2414
|
+
bucket: row.bucket
|
|
2308
2415
|
};
|
|
2309
2416
|
}
|
|
2310
2417
|
|
|
@@ -2401,9 +2508,9 @@ function buildQuickHelpText(view, options = {}) {
|
|
|
2401
2508
|
const isSimple = String(flow || '').toLowerCase() === 'simple';
|
|
2402
2509
|
const activeLabel = isAidlc ? 'active bolt' : (isSimple ? 'active spec' : 'active run');
|
|
2403
2510
|
|
|
2404
|
-
const parts = ['1/2/3/4 tabs', 'g/G sections'];
|
|
2511
|
+
const parts = ['1/2/3/4/5 tabs', 'g/G sections'];
|
|
2405
2512
|
|
|
2406
|
-
if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health') {
|
|
2513
|
+
if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health' || view === 'git') {
|
|
2407
2514
|
if (previewOpen) {
|
|
2408
2515
|
parts.push('tab pane', '↑/↓ nav/scroll', 'v/space close');
|
|
2409
2516
|
} else {
|
|
@@ -2445,7 +2552,7 @@ function buildHelpOverlayLines(options = {}) {
|
|
|
2445
2552
|
{ text: 'Global', color: 'cyan', bold: true },
|
|
2446
2553
|
'q or Ctrl+C quit',
|
|
2447
2554
|
'r refresh snapshot',
|
|
2448
|
-
`1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health`,
|
|
2555
|
+
`1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health | 5 git changes`,
|
|
2449
2556
|
'g next section | G previous section',
|
|
2450
2557
|
'h/? toggle this shortcuts overlay',
|
|
2451
2558
|
'esc close overlays (help/preview/fullscreen)',
|
|
@@ -2483,6 +2590,9 @@ function buildHelpOverlayLines(options = {}) {
|
|
|
2483
2590
|
{ text: 'Tab 4 Standards/Health', color: 'magenta', bold: true },
|
|
2484
2591
|
`s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
|
|
2485
2592
|
{ text: '', color: undefined, bold: false },
|
|
2593
|
+
{ text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
|
|
2594
|
+
'select changed files and preview diffs',
|
|
2595
|
+
{ text: '', color: undefined, bold: false },
|
|
2486
2596
|
{ text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
|
|
2487
2597
|
);
|
|
2488
2598
|
|
|
@@ -2601,20 +2711,27 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
|
2601
2711
|
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
2602
2712
|
}
|
|
2603
2713
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2714
|
+
const isGitPreview = fileEntry.previewType === 'git-diff';
|
|
2715
|
+
let rawLines = [];
|
|
2716
|
+
if (isGitPreview) {
|
|
2717
|
+
const diffText = loadGitDiffPreview(fileEntry);
|
|
2718
|
+
rawLines = String(diffText || '').split(/\r?\n/);
|
|
2719
|
+
} else {
|
|
2720
|
+
let content;
|
|
2721
|
+
try {
|
|
2722
|
+
content = fs.readFileSync(fileEntry.path, 'utf8');
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
return [{
|
|
2725
|
+
text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
|
|
2726
|
+
color: 'red',
|
|
2727
|
+
bold: false
|
|
2728
|
+
}];
|
|
2729
|
+
}
|
|
2730
|
+
rawLines = String(content).split(/\r?\n/);
|
|
2613
2731
|
}
|
|
2614
2732
|
|
|
2615
|
-
const rawLines = String(content).split(/\r?\n/);
|
|
2616
2733
|
const headLine = {
|
|
2617
|
-
text: truncate(
|
|
2734
|
+
text: truncate(`${isGitPreview ? 'diff' : 'file'}: ${fileEntry.path}`, width),
|
|
2618
2735
|
color: 'cyan',
|
|
2619
2736
|
bold: true
|
|
2620
2737
|
};
|
|
@@ -2625,7 +2742,34 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
|
2625
2742
|
|
|
2626
2743
|
const highlighted = cappedLines.map((rawLine, index) => {
|
|
2627
2744
|
const prefixedLine = `${String(index + 1).padStart(4, ' ')} | ${rawLine}`;
|
|
2628
|
-
|
|
2745
|
+
let color;
|
|
2746
|
+
let bold;
|
|
2747
|
+
let togglesCodeBlock = false;
|
|
2748
|
+
|
|
2749
|
+
if (isGitPreview) {
|
|
2750
|
+
if (rawLine.startsWith('+++ ') || rawLine.startsWith('--- ') || rawLine.startsWith('diff --git')) {
|
|
2751
|
+
color = 'cyan';
|
|
2752
|
+
bold = true;
|
|
2753
|
+
} else if (rawLine.startsWith('@@')) {
|
|
2754
|
+
color = 'magenta';
|
|
2755
|
+
bold = true;
|
|
2756
|
+
} else if (rawLine.startsWith('+')) {
|
|
2757
|
+
color = 'green';
|
|
2758
|
+
bold = false;
|
|
2759
|
+
} else if (rawLine.startsWith('-')) {
|
|
2760
|
+
color = 'red';
|
|
2761
|
+
bold = false;
|
|
2762
|
+
} else {
|
|
2763
|
+
color = undefined;
|
|
2764
|
+
bold = false;
|
|
2765
|
+
}
|
|
2766
|
+
} else {
|
|
2767
|
+
const markdownStyle = colorizeMarkdownLine(rawLine, inCodeBlock);
|
|
2768
|
+
color = markdownStyle.color;
|
|
2769
|
+
bold = markdownStyle.bold;
|
|
2770
|
+
togglesCodeBlock = markdownStyle.togglesCodeBlock;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2629
2773
|
if (togglesCodeBlock) {
|
|
2630
2774
|
inCodeBlock = !inCodeBlock;
|
|
2631
2775
|
}
|
|
@@ -2772,7 +2916,8 @@ function createDashboardApp(deps) {
|
|
|
2772
2916
|
{ id: 'runs', label: `1 ${icons.runs} ${primaryLabel}` },
|
|
2773
2917
|
{ id: 'intents', label: `2 ${icons.overview} INTENTS` },
|
|
2774
2918
|
{ id: 'completed', label: `3 ${icons.runs} ${completedLabel}` },
|
|
2775
|
-
{ id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` }
|
|
2919
|
+
{ id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` },
|
|
2920
|
+
{ id: 'git', label: `5 ${icons.git} GIT CHANGES` }
|
|
2776
2921
|
];
|
|
2777
2922
|
const maxWidth = Math.max(8, Math.floor(width));
|
|
2778
2923
|
const segments = [];
|
|
@@ -2902,7 +3047,8 @@ function createDashboardApp(deps) {
|
|
|
2902
3047
|
runs: 'current-run',
|
|
2903
3048
|
intents: 'intent-status',
|
|
2904
3049
|
completed: 'completed-runs',
|
|
2905
|
-
health: 'standards'
|
|
3050
|
+
health: 'standards',
|
|
3051
|
+
git: 'git-changes'
|
|
2906
3052
|
});
|
|
2907
3053
|
const [selectionBySection, setSelectionBySection] = useState({
|
|
2908
3054
|
worktrees: 0,
|
|
@@ -2914,7 +3060,8 @@ function createDashboardApp(deps) {
|
|
|
2914
3060
|
standards: 0,
|
|
2915
3061
|
stats: 0,
|
|
2916
3062
|
warnings: 0,
|
|
2917
|
-
'error-details': 0
|
|
3063
|
+
'error-details': 0,
|
|
3064
|
+
'git-changes': 0
|
|
2918
3065
|
});
|
|
2919
3066
|
const [expandedGroups, setExpandedGroups] = useState({});
|
|
2920
3067
|
const [previewTarget, setPreviewTarget] = useState(null);
|
|
@@ -3068,6 +3215,37 @@ function createDashboardApp(deps) {
|
|
|
3068
3215
|
'No error details'
|
|
3069
3216
|
)
|
|
3070
3217
|
: toLoadingRows('Loading error details...', 'error-loading');
|
|
3218
|
+
const gitRows = shouldHydrateSecondaryTabs
|
|
3219
|
+
? (() => {
|
|
3220
|
+
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(
|
|
3242
|
+
buildGitChangeGroups(snapshot),
|
|
3243
|
+
git.available ? 'Working tree clean' : 'No git changes',
|
|
3244
|
+
expandedGroups
|
|
3245
|
+
);
|
|
3246
|
+
return [...headerRows, ...groups];
|
|
3247
|
+
})()
|
|
3248
|
+
: toLoadingRows('Loading git changes...', 'git-loading');
|
|
3071
3249
|
|
|
3072
3250
|
const rowsBySection = {
|
|
3073
3251
|
worktrees: worktreeRows,
|
|
@@ -3079,7 +3257,8 @@ function createDashboardApp(deps) {
|
|
|
3079
3257
|
standards: standardsRows,
|
|
3080
3258
|
stats: statsRows,
|
|
3081
3259
|
warnings: warningsRows,
|
|
3082
|
-
'error-details': errorDetailsRows
|
|
3260
|
+
'error-details': errorDetailsRows,
|
|
3261
|
+
'git-changes': gitRows
|
|
3083
3262
|
};
|
|
3084
3263
|
const worktreeItems = getWorktreeItems(snapshot);
|
|
3085
3264
|
const selectedWorktree = getSelectedWorktree(snapshot);
|
|
@@ -3269,6 +3448,12 @@ function createDashboardApp(deps) {
|
|
|
3269
3448
|
return;
|
|
3270
3449
|
}
|
|
3271
3450
|
|
|
3451
|
+
if (input === '5') {
|
|
3452
|
+
setUi((previous) => ({ ...previous, view: 'git' }));
|
|
3453
|
+
setPaneFocus('main');
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3272
3457
|
if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
|
|
3273
3458
|
snapshotHashRef.current = safeJsonHash(null);
|
|
3274
3459
|
errorHashRef.current = null;
|
|
@@ -3291,13 +3476,15 @@ function createDashboardApp(deps) {
|
|
|
3291
3476
|
standards: 0,
|
|
3292
3477
|
stats: 0,
|
|
3293
3478
|
warnings: 0,
|
|
3294
|
-
'error-details': 0
|
|
3479
|
+
'error-details': 0,
|
|
3480
|
+
'git-changes': 0
|
|
3295
3481
|
});
|
|
3296
3482
|
setSectionFocus({
|
|
3297
3483
|
runs: 'current-run',
|
|
3298
3484
|
intents: 'intent-status',
|
|
3299
3485
|
completed: 'completed-runs',
|
|
3300
|
-
health: 'standards'
|
|
3486
|
+
health: 'standards',
|
|
3487
|
+
git: 'git-changes'
|
|
3301
3488
|
});
|
|
3302
3489
|
setOverviewIntentFilter('next');
|
|
3303
3490
|
setExpandedGroups({});
|
|
@@ -3332,13 +3519,15 @@ function createDashboardApp(deps) {
|
|
|
3332
3519
|
standards: 0,
|
|
3333
3520
|
stats: 0,
|
|
3334
3521
|
warnings: 0,
|
|
3335
|
-
'error-details': 0
|
|
3522
|
+
'error-details': 0,
|
|
3523
|
+
'git-changes': 0
|
|
3336
3524
|
});
|
|
3337
3525
|
setSectionFocus({
|
|
3338
3526
|
runs: 'current-run',
|
|
3339
3527
|
intents: 'intent-status',
|
|
3340
3528
|
completed: 'completed-runs',
|
|
3341
|
-
health: 'standards'
|
|
3529
|
+
health: 'standards',
|
|
3530
|
+
git: 'git-changes'
|
|
3342
3531
|
});
|
|
3343
3532
|
setOverviewIntentFilter('next');
|
|
3344
3533
|
setExpandedGroups({});
|
|
@@ -3442,6 +3631,11 @@ function createDashboardApp(deps) {
|
|
|
3442
3631
|
setSectionFocus((previous) => ({ ...previous, health: 'error-details' }));
|
|
3443
3632
|
return;
|
|
3444
3633
|
}
|
|
3634
|
+
} else if (ui.view === 'git') {
|
|
3635
|
+
if (input === 'd') {
|
|
3636
|
+
setSectionFocus((previous) => ({ ...previous, git: 'git-changes' }));
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3445
3639
|
}
|
|
3446
3640
|
|
|
3447
3641
|
if (key.escape) {
|
|
@@ -3717,7 +3911,9 @@ function createDashboardApp(deps) {
|
|
|
3717
3911
|
});
|
|
3718
3912
|
|
|
3719
3913
|
runtime.start();
|
|
3720
|
-
const fallbackIntervalMs =
|
|
3914
|
+
const fallbackIntervalMs = ui.view === 'git'
|
|
3915
|
+
? Math.max(refreshMs, 1000)
|
|
3916
|
+
: Math.max(refreshMs, 5000);
|
|
3721
3917
|
const interval = setInterval(() => {
|
|
3722
3918
|
void refresh();
|
|
3723
3919
|
}, fallbackIntervalMs);
|
|
@@ -3726,7 +3922,7 @@ function createDashboardApp(deps) {
|
|
|
3726
3922
|
clearInterval(interval);
|
|
3727
3923
|
void runtime.close();
|
|
3728
3924
|
};
|
|
3729
|
-
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, worktreeWatchSignature, selectedWorktreeId]);
|
|
3925
|
+
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, ui.view, worktreeWatchSignature, selectedWorktreeId]);
|
|
3730
3926
|
|
|
3731
3927
|
useEffect(() => {
|
|
3732
3928
|
if (!stdout || typeof stdout.write !== 'function') {
|
|
@@ -3885,6 +4081,15 @@ function createDashboardApp(deps) {
|
|
|
3885
4081
|
borderColor: 'red'
|
|
3886
4082
|
});
|
|
3887
4083
|
}
|
|
4084
|
+
} else if (ui.view === 'git') {
|
|
4085
|
+
panelCandidates = [
|
|
4086
|
+
{
|
|
4087
|
+
key: 'git-changes',
|
|
4088
|
+
title: panelTitles.git || 'Git Changes',
|
|
4089
|
+
lines: sectionLines['git-changes'],
|
|
4090
|
+
borderColor: 'yellow'
|
|
4091
|
+
}
|
|
4092
|
+
];
|
|
3888
4093
|
} else {
|
|
3889
4094
|
panelCandidates = [];
|
|
3890
4095
|
if (worktreeSectionEnabled) {
|
|
@@ -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 '
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.52",
|
|
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": {
|