specsmd 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dashboard/git/changes.js +330 -0
- package/lib/dashboard/index.js +3 -1
- package/lib/dashboard/tui/app.js +409 -82
- 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: '',
|
|
@@ -118,6 +121,11 @@ function truncate(value, width) {
|
|
|
118
121
|
return `${sliceAnsi(text, 0, bodyWidth)}${ellipsis}`;
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
function resolveFrameWidth(columns) {
|
|
125
|
+
const safeColumns = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 120;
|
|
126
|
+
return safeColumns > 24 ? safeColumns - 1 : safeColumns;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
function normalizePanelLine(line) {
|
|
122
130
|
if (line && typeof line === 'object' && !Array.isArray(line)) {
|
|
123
131
|
return {
|
|
@@ -1120,7 +1128,47 @@ function getPanelTitles(flow, snapshot) {
|
|
|
1120
1128
|
files: 'Run Files',
|
|
1121
1129
|
pending: 'Pending Queue',
|
|
1122
1130
|
completed: 'Recent Completed Runs',
|
|
1123
|
-
otherWorktrees: 'Other Worktrees: Active Runs'
|
|
1131
|
+
otherWorktrees: 'Other Worktrees: Active Runs',
|
|
1132
|
+
git: 'Git Changes'
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function getGitChangesSnapshot(snapshot) {
|
|
1137
|
+
const gitChanges = snapshot?.gitChanges;
|
|
1138
|
+
if (!gitChanges || typeof gitChanges !== 'object') {
|
|
1139
|
+
return {
|
|
1140
|
+
available: false,
|
|
1141
|
+
branch: '(unavailable)',
|
|
1142
|
+
upstream: null,
|
|
1143
|
+
ahead: 0,
|
|
1144
|
+
behind: 0,
|
|
1145
|
+
counts: {
|
|
1146
|
+
total: 0,
|
|
1147
|
+
staged: 0,
|
|
1148
|
+
unstaged: 0,
|
|
1149
|
+
untracked: 0,
|
|
1150
|
+
conflicted: 0
|
|
1151
|
+
},
|
|
1152
|
+
staged: [],
|
|
1153
|
+
unstaged: [],
|
|
1154
|
+
untracked: [],
|
|
1155
|
+
conflicted: [],
|
|
1156
|
+
clean: true
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
...gitChanges,
|
|
1161
|
+
counts: gitChanges.counts || {
|
|
1162
|
+
total: 0,
|
|
1163
|
+
staged: 0,
|
|
1164
|
+
unstaged: 0,
|
|
1165
|
+
untracked: 0,
|
|
1166
|
+
conflicted: 0
|
|
1167
|
+
},
|
|
1168
|
+
staged: Array.isArray(gitChanges.staged) ? gitChanges.staged : [],
|
|
1169
|
+
unstaged: Array.isArray(gitChanges.unstaged) ? gitChanges.unstaged : [],
|
|
1170
|
+
untracked: Array.isArray(gitChanges.untracked) ? gitChanges.untracked : [],
|
|
1171
|
+
conflicted: Array.isArray(gitChanges.conflicted) ? gitChanges.conflicted : []
|
|
1124
1172
|
};
|
|
1125
1173
|
}
|
|
1126
1174
|
|
|
@@ -1300,6 +1348,9 @@ function getSectionOrderForView(view, options = {}) {
|
|
|
1300
1348
|
if (view === 'health') {
|
|
1301
1349
|
return ['standards', 'stats', 'warnings', 'error-details'];
|
|
1302
1350
|
}
|
|
1351
|
+
if (view === 'git') {
|
|
1352
|
+
return ['git-changes'];
|
|
1353
|
+
}
|
|
1303
1354
|
const sections = [];
|
|
1304
1355
|
if (includeWorktrees) {
|
|
1305
1356
|
sections.push('worktrees');
|
|
@@ -1640,6 +1691,10 @@ function formatScope(scope) {
|
|
|
1640
1691
|
if (scope === 'upcoming') return 'UPNEXT';
|
|
1641
1692
|
if (scope === 'completed') return 'DONE';
|
|
1642
1693
|
if (scope === 'intent') return 'INTENT';
|
|
1694
|
+
if (scope === 'staged') return 'STAGED';
|
|
1695
|
+
if (scope === 'unstaged') return 'UNSTAGED';
|
|
1696
|
+
if (scope === 'untracked') return 'UNTRACKED';
|
|
1697
|
+
if (scope === 'conflicted') return 'CONFLICT';
|
|
1643
1698
|
return 'FILE';
|
|
1644
1699
|
}
|
|
1645
1700
|
|
|
@@ -2054,9 +2109,15 @@ function collectAidlcIntentContextFiles(snapshot, intentId) {
|
|
|
2054
2109
|
}
|
|
2055
2110
|
|
|
2056
2111
|
function filterExistingFiles(files) {
|
|
2057
|
-
return (Array.isArray(files) ? files : []).filter((file) =>
|
|
2058
|
-
file
|
|
2059
|
-
|
|
2112
|
+
return (Array.isArray(files) ? files : []).filter((file) => {
|
|
2113
|
+
if (!file || typeof file.path !== 'string' || typeof file.label !== 'string') {
|
|
2114
|
+
return false;
|
|
2115
|
+
}
|
|
2116
|
+
if (file.allowMissing === true) {
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
return fileExists(file.path);
|
|
2120
|
+
});
|
|
2060
2121
|
}
|
|
2061
2122
|
|
|
2062
2123
|
function buildPendingGroups(snapshot, flow) {
|
|
@@ -2191,6 +2252,49 @@ function buildCompletedGroups(snapshot, flow) {
|
|
|
2191
2252
|
return groups;
|
|
2192
2253
|
}
|
|
2193
2254
|
|
|
2255
|
+
function buildGitChangeGroups(snapshot) {
|
|
2256
|
+
const git = getGitChangesSnapshot(snapshot);
|
|
2257
|
+
|
|
2258
|
+
if (!git.available) {
|
|
2259
|
+
return [];
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const makeFiles = (items, scope) => items.map((item) => ({
|
|
2263
|
+
label: item.relativePath,
|
|
2264
|
+
path: item.path || path.join(git.rootPath || snapshot?.workspacePath || '', item.relativePath || ''),
|
|
2265
|
+
scope,
|
|
2266
|
+
allowMissing: true,
|
|
2267
|
+
previewType: 'git-diff',
|
|
2268
|
+
repoRoot: git.rootPath || snapshot?.workspacePath || '',
|
|
2269
|
+
relativePath: item.relativePath || '',
|
|
2270
|
+
bucket: item.bucket || scope
|
|
2271
|
+
}));
|
|
2272
|
+
|
|
2273
|
+
const groups = [];
|
|
2274
|
+
groups.push({
|
|
2275
|
+
key: 'git:staged',
|
|
2276
|
+
label: `staged (${git.counts.staged || 0})`,
|
|
2277
|
+
files: makeFiles(git.staged, 'staged')
|
|
2278
|
+
});
|
|
2279
|
+
groups.push({
|
|
2280
|
+
key: 'git:unstaged',
|
|
2281
|
+
label: `unstaged (${git.counts.unstaged || 0})`,
|
|
2282
|
+
files: makeFiles(git.unstaged, 'unstaged')
|
|
2283
|
+
});
|
|
2284
|
+
groups.push({
|
|
2285
|
+
key: 'git:untracked',
|
|
2286
|
+
label: `untracked (${git.counts.untracked || 0})`,
|
|
2287
|
+
files: makeFiles(git.untracked, 'untracked')
|
|
2288
|
+
});
|
|
2289
|
+
groups.push({
|
|
2290
|
+
key: 'git:conflicted',
|
|
2291
|
+
label: `conflicts (${git.counts.conflicted || 0})`,
|
|
2292
|
+
files: makeFiles(git.conflicted, 'conflicted')
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
return groups;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2194
2298
|
function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
2195
2299
|
if (!Array.isArray(groups) || groups.length === 0) {
|
|
2196
2300
|
return [{
|
|
@@ -2221,12 +2325,16 @@ function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
|
2221
2325
|
for (let index = 0; index < files.length; index += 1) {
|
|
2222
2326
|
const file = files[index];
|
|
2223
2327
|
rows.push({
|
|
2224
|
-
kind: 'file',
|
|
2328
|
+
kind: file.previewType === 'git-diff' ? 'git-file' : 'file',
|
|
2225
2329
|
key: `${group.key}:file:${file.path}:${index}`,
|
|
2226
2330
|
label: file.label,
|
|
2227
2331
|
path: file.path,
|
|
2228
2332
|
scope: file.scope || 'file',
|
|
2229
|
-
selectable: true
|
|
2333
|
+
selectable: true,
|
|
2334
|
+
previewType: file.previewType,
|
|
2335
|
+
repoRoot: file.repoRoot,
|
|
2336
|
+
relativePath: file.relativePath,
|
|
2337
|
+
bucket: file.bucket
|
|
2230
2338
|
});
|
|
2231
2339
|
}
|
|
2232
2340
|
}
|
|
@@ -2261,7 +2369,7 @@ function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedS
|
|
|
2261
2369
|
};
|
|
2262
2370
|
}
|
|
2263
2371
|
|
|
2264
|
-
if (row.kind === 'file') {
|
|
2372
|
+
if (row.kind === 'file' || row.kind === 'git-file') {
|
|
2265
2373
|
const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
|
|
2266
2374
|
return {
|
|
2267
2375
|
text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
|
|
@@ -2298,13 +2406,17 @@ function getSelectedRow(rows, selectedIndex) {
|
|
|
2298
2406
|
}
|
|
2299
2407
|
|
|
2300
2408
|
function rowToFileEntry(row) {
|
|
2301
|
-
if (!row || row.kind !== 'file' || typeof row.path !== 'string') {
|
|
2409
|
+
if (!row || (row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
|
|
2302
2410
|
return null;
|
|
2303
2411
|
}
|
|
2304
2412
|
return {
|
|
2305
2413
|
label: row.label || path.basename(row.path),
|
|
2306
2414
|
path: row.path,
|
|
2307
|
-
scope: row.scope || 'file'
|
|
2415
|
+
scope: row.scope || 'file',
|
|
2416
|
+
previewType: row.previewType,
|
|
2417
|
+
repoRoot: row.repoRoot,
|
|
2418
|
+
relativePath: row.relativePath,
|
|
2419
|
+
bucket: row.bucket
|
|
2308
2420
|
};
|
|
2309
2421
|
}
|
|
2310
2422
|
|
|
@@ -2401,9 +2513,9 @@ function buildQuickHelpText(view, options = {}) {
|
|
|
2401
2513
|
const isSimple = String(flow || '').toLowerCase() === 'simple';
|
|
2402
2514
|
const activeLabel = isAidlc ? 'active bolt' : (isSimple ? 'active spec' : 'active run');
|
|
2403
2515
|
|
|
2404
|
-
const parts = ['1/2/3/4 tabs', 'g/G sections'];
|
|
2516
|
+
const parts = ['1/2/3/4/5 tabs', 'g/G sections'];
|
|
2405
2517
|
|
|
2406
|
-
if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health') {
|
|
2518
|
+
if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health' || view === 'git') {
|
|
2407
2519
|
if (previewOpen) {
|
|
2408
2520
|
parts.push('tab pane', '↑/↓ nav/scroll', 'v/space close');
|
|
2409
2521
|
} else {
|
|
@@ -2415,6 +2527,8 @@ function buildQuickHelpText(view, options = {}) {
|
|
|
2415
2527
|
parts.push('b worktrees', 'u others');
|
|
2416
2528
|
}
|
|
2417
2529
|
parts.push('a current', 'f files');
|
|
2530
|
+
} else if (view === 'git') {
|
|
2531
|
+
parts.push('d changes', 'c/A/p/P git(soon)');
|
|
2418
2532
|
}
|
|
2419
2533
|
parts.push(`tab1 ${activeLabel}`);
|
|
2420
2534
|
|
|
@@ -2426,6 +2540,57 @@ function buildQuickHelpText(view, options = {}) {
|
|
|
2426
2540
|
return parts.join(' | ');
|
|
2427
2541
|
}
|
|
2428
2542
|
|
|
2543
|
+
function buildLazyGitCommandStrip(view, options = {}) {
|
|
2544
|
+
const {
|
|
2545
|
+
hasWorktrees = false,
|
|
2546
|
+
previewOpen = false
|
|
2547
|
+
} = options;
|
|
2548
|
+
|
|
2549
|
+
const parts = [];
|
|
2550
|
+
|
|
2551
|
+
if (view === 'runs') {
|
|
2552
|
+
if (hasWorktrees) {
|
|
2553
|
+
parts.push('b worktrees');
|
|
2554
|
+
}
|
|
2555
|
+
parts.push('a current', 'f files', 'enter expand');
|
|
2556
|
+
} else if (view === 'intents') {
|
|
2557
|
+
parts.push('n next', 'x completed', 'enter expand');
|
|
2558
|
+
} else if (view === 'completed') {
|
|
2559
|
+
parts.push('c completed', 'enter expand');
|
|
2560
|
+
} else if (view === 'health') {
|
|
2561
|
+
parts.push('s standards', 't stats', 'w warnings');
|
|
2562
|
+
} else if (view === 'git') {
|
|
2563
|
+
parts.push('d changes', 'space preview', 'c commit (soon)', 'A amend (soon)', 'p push (soon)', 'P pull (soon)');
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (previewOpen) {
|
|
2567
|
+
parts.push('tab pane', 'j/k scroll');
|
|
2568
|
+
} else {
|
|
2569
|
+
parts.push('v preview');
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
parts.push('1-5 views', 'g/G panels', 'r refresh', '? help', 'q quit');
|
|
2573
|
+
return parts.join(' | ');
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
function buildLazyGitCommandLogLine(options = {}) {
|
|
2577
|
+
const {
|
|
2578
|
+
statusLine = '',
|
|
2579
|
+
activeFlow = 'fire',
|
|
2580
|
+
watchEnabled = true,
|
|
2581
|
+
watchStatus = 'watching',
|
|
2582
|
+
selectedWorktreeLabel = null
|
|
2583
|
+
} = options;
|
|
2584
|
+
|
|
2585
|
+
if (typeof statusLine === 'string' && statusLine.trim() !== '') {
|
|
2586
|
+
return `Command Log | ${statusLine}`;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
const watchLabel = watchEnabled ? watchStatus : 'off';
|
|
2590
|
+
const worktreeSegment = selectedWorktreeLabel ? ` | wt:${selectedWorktreeLabel}` : '';
|
|
2591
|
+
return `Command Log | flow:${String(activeFlow || 'fire').toUpperCase()} | watch:${watchLabel}${worktreeSegment} | ready`;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2429
2594
|
function buildHelpOverlayLines(options = {}) {
|
|
2430
2595
|
const {
|
|
2431
2596
|
view = 'runs',
|
|
@@ -2445,7 +2610,7 @@ function buildHelpOverlayLines(options = {}) {
|
|
|
2445
2610
|
{ text: 'Global', color: 'cyan', bold: true },
|
|
2446
2611
|
'q or Ctrl+C quit',
|
|
2447
2612
|
'r refresh snapshot',
|
|
2448
|
-
`1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health`,
|
|
2613
|
+
`1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health | 5 git changes`,
|
|
2449
2614
|
'g next section | G previous section',
|
|
2450
2615
|
'h/? toggle this shortcuts overlay',
|
|
2451
2616
|
'esc close overlays (help/preview/fullscreen)',
|
|
@@ -2483,6 +2648,10 @@ function buildHelpOverlayLines(options = {}) {
|
|
|
2483
2648
|
{ text: 'Tab 4 Standards/Health', color: 'magenta', bold: true },
|
|
2484
2649
|
`s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
|
|
2485
2650
|
{ text: '', color: undefined, bold: false },
|
|
2651
|
+
{ text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
|
|
2652
|
+
'select changed files and preview diffs',
|
|
2653
|
+
'commit/push/pull shortcuts are shown in footer for LazyGit-style hierarchy',
|
|
2654
|
+
{ text: '', color: undefined, bold: false },
|
|
2486
2655
|
{ text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
|
|
2487
2656
|
);
|
|
2488
2657
|
|
|
@@ -2601,20 +2770,27 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
|
2601
2770
|
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
2602
2771
|
}
|
|
2603
2772
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2773
|
+
const isGitPreview = fileEntry.previewType === 'git-diff';
|
|
2774
|
+
let rawLines = [];
|
|
2775
|
+
if (isGitPreview) {
|
|
2776
|
+
const diffText = loadGitDiffPreview(fileEntry);
|
|
2777
|
+
rawLines = String(diffText || '').split(/\r?\n/);
|
|
2778
|
+
} else {
|
|
2779
|
+
let content;
|
|
2780
|
+
try {
|
|
2781
|
+
content = fs.readFileSync(fileEntry.path, 'utf8');
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
return [{
|
|
2784
|
+
text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
|
|
2785
|
+
color: 'red',
|
|
2786
|
+
bold: false
|
|
2787
|
+
}];
|
|
2788
|
+
}
|
|
2789
|
+
rawLines = String(content).split(/\r?\n/);
|
|
2613
2790
|
}
|
|
2614
2791
|
|
|
2615
|
-
const rawLines = String(content).split(/\r?\n/);
|
|
2616
2792
|
const headLine = {
|
|
2617
|
-
text: truncate(
|
|
2793
|
+
text: truncate(`${isGitPreview ? 'diff' : 'file'}: ${fileEntry.path}`, width),
|
|
2618
2794
|
color: 'cyan',
|
|
2619
2795
|
bold: true
|
|
2620
2796
|
};
|
|
@@ -2625,7 +2801,34 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
|
2625
2801
|
|
|
2626
2802
|
const highlighted = cappedLines.map((rawLine, index) => {
|
|
2627
2803
|
const prefixedLine = `${String(index + 1).padStart(4, ' ')} | ${rawLine}`;
|
|
2628
|
-
|
|
2804
|
+
let color;
|
|
2805
|
+
let bold;
|
|
2806
|
+
let togglesCodeBlock = false;
|
|
2807
|
+
|
|
2808
|
+
if (isGitPreview) {
|
|
2809
|
+
if (rawLine.startsWith('+++ ') || rawLine.startsWith('--- ') || rawLine.startsWith('diff --git')) {
|
|
2810
|
+
color = 'cyan';
|
|
2811
|
+
bold = true;
|
|
2812
|
+
} else if (rawLine.startsWith('@@')) {
|
|
2813
|
+
color = 'magenta';
|
|
2814
|
+
bold = true;
|
|
2815
|
+
} else if (rawLine.startsWith('+')) {
|
|
2816
|
+
color = 'green';
|
|
2817
|
+
bold = false;
|
|
2818
|
+
} else if (rawLine.startsWith('-')) {
|
|
2819
|
+
color = 'red';
|
|
2820
|
+
bold = false;
|
|
2821
|
+
} else {
|
|
2822
|
+
color = undefined;
|
|
2823
|
+
bold = false;
|
|
2824
|
+
}
|
|
2825
|
+
} else {
|
|
2826
|
+
const markdownStyle = colorizeMarkdownLine(rawLine, inCodeBlock);
|
|
2827
|
+
color = markdownStyle.color;
|
|
2828
|
+
bold = markdownStyle.bold;
|
|
2829
|
+
togglesCodeBlock = markdownStyle.togglesCodeBlock;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2629
2832
|
if (togglesCodeBlock) {
|
|
2630
2833
|
inCodeBlock = !inCodeBlock;
|
|
2631
2834
|
}
|
|
@@ -2720,7 +2923,7 @@ function createDashboardApp(deps) {
|
|
|
2720
2923
|
focused
|
|
2721
2924
|
} = props;
|
|
2722
2925
|
|
|
2723
|
-
const contentWidth = Math.max(
|
|
2926
|
+
const contentWidth = Math.max(1, width - (dense ? 2 : 4));
|
|
2724
2927
|
const visibleLines = fitLines(lines, maxLines, contentWidth);
|
|
2725
2928
|
const panelBorderColor = focused ? 'cyan' : (borderColor || 'gray');
|
|
2726
2929
|
const titleColor = focused ? 'black' : 'cyan';
|
|
@@ -2772,7 +2975,8 @@ function createDashboardApp(deps) {
|
|
|
2772
2975
|
{ id: 'runs', label: `1 ${icons.runs} ${primaryLabel}` },
|
|
2773
2976
|
{ id: 'intents', label: `2 ${icons.overview} INTENTS` },
|
|
2774
2977
|
{ id: 'completed', label: `3 ${icons.runs} ${completedLabel}` },
|
|
2775
|
-
{ id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` }
|
|
2978
|
+
{ id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` },
|
|
2979
|
+
{ id: 'git', label: `5 ${icons.git} GIT CHANGES` }
|
|
2776
2980
|
];
|
|
2777
2981
|
const maxWidth = Math.max(8, Math.floor(width));
|
|
2778
2982
|
const segments = [];
|
|
@@ -2902,7 +3106,8 @@ function createDashboardApp(deps) {
|
|
|
2902
3106
|
runs: 'current-run',
|
|
2903
3107
|
intents: 'intent-status',
|
|
2904
3108
|
completed: 'completed-runs',
|
|
2905
|
-
health: 'standards'
|
|
3109
|
+
health: 'standards',
|
|
3110
|
+
git: 'git-changes'
|
|
2906
3111
|
});
|
|
2907
3112
|
const [selectionBySection, setSelectionBySection] = useState({
|
|
2908
3113
|
worktrees: 0,
|
|
@@ -2914,7 +3119,8 @@ function createDashboardApp(deps) {
|
|
|
2914
3119
|
standards: 0,
|
|
2915
3120
|
stats: 0,
|
|
2916
3121
|
warnings: 0,
|
|
2917
|
-
'error-details': 0
|
|
3122
|
+
'error-details': 0,
|
|
3123
|
+
'git-changes': 0
|
|
2918
3124
|
});
|
|
2919
3125
|
const [expandedGroups, setExpandedGroups] = useState({});
|
|
2920
3126
|
const [previewTarget, setPreviewTarget] = useState(null);
|
|
@@ -3068,6 +3274,37 @@ function createDashboardApp(deps) {
|
|
|
3068
3274
|
'No error details'
|
|
3069
3275
|
)
|
|
3070
3276
|
: toLoadingRows('Loading error details...', 'error-loading');
|
|
3277
|
+
const gitRows = shouldHydrateSecondaryTabs
|
|
3278
|
+
? (() => {
|
|
3279
|
+
const git = getGitChangesSnapshot(snapshot);
|
|
3280
|
+
const tracking = git.upstream
|
|
3281
|
+
? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
|
|
3282
|
+
: 'no upstream';
|
|
3283
|
+
const headerRows = [{
|
|
3284
|
+
kind: 'info',
|
|
3285
|
+
key: 'git:branch',
|
|
3286
|
+
label: git.available
|
|
3287
|
+
? `branch ${git.branch}${git.detached ? ' [detached]' : ''} | ${tracking}`
|
|
3288
|
+
: 'git: repository unavailable in selected worktree',
|
|
3289
|
+
color: git.available ? 'cyan' : 'red',
|
|
3290
|
+
bold: true,
|
|
3291
|
+
selectable: false
|
|
3292
|
+
}, {
|
|
3293
|
+
kind: 'info',
|
|
3294
|
+
key: 'git:counts',
|
|
3295
|
+
label: `changes ${git.counts.total || 0} | staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0} | untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
|
|
3296
|
+
color: 'gray',
|
|
3297
|
+
bold: false,
|
|
3298
|
+
selectable: false
|
|
3299
|
+
}];
|
|
3300
|
+
const groups = toExpandableRows(
|
|
3301
|
+
buildGitChangeGroups(snapshot),
|
|
3302
|
+
git.available ? 'Working tree clean' : 'No git changes',
|
|
3303
|
+
expandedGroups
|
|
3304
|
+
);
|
|
3305
|
+
return [...headerRows, ...groups];
|
|
3306
|
+
})()
|
|
3307
|
+
: toLoadingRows('Loading git changes...', 'git-loading');
|
|
3071
3308
|
|
|
3072
3309
|
const rowsBySection = {
|
|
3073
3310
|
worktrees: worktreeRows,
|
|
@@ -3079,7 +3316,8 @@ function createDashboardApp(deps) {
|
|
|
3079
3316
|
standards: standardsRows,
|
|
3080
3317
|
stats: statsRows,
|
|
3081
3318
|
warnings: warningsRows,
|
|
3082
|
-
'error-details': errorDetailsRows
|
|
3319
|
+
'error-details': errorDetailsRows,
|
|
3320
|
+
'git-changes': gitRows
|
|
3083
3321
|
};
|
|
3084
3322
|
const worktreeItems = getWorktreeItems(snapshot);
|
|
3085
3323
|
const selectedWorktree = getSelectedWorktree(snapshot);
|
|
@@ -3269,6 +3507,12 @@ function createDashboardApp(deps) {
|
|
|
3269
3507
|
return;
|
|
3270
3508
|
}
|
|
3271
3509
|
|
|
3510
|
+
if (input === '5') {
|
|
3511
|
+
setUi((previous) => ({ ...previous, view: 'git' }));
|
|
3512
|
+
setPaneFocus('main');
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3272
3516
|
if ((input === ']' || input === 'm') && availableFlowIds.length > 1) {
|
|
3273
3517
|
snapshotHashRef.current = safeJsonHash(null);
|
|
3274
3518
|
errorHashRef.current = null;
|
|
@@ -3291,13 +3535,15 @@ function createDashboardApp(deps) {
|
|
|
3291
3535
|
standards: 0,
|
|
3292
3536
|
stats: 0,
|
|
3293
3537
|
warnings: 0,
|
|
3294
|
-
'error-details': 0
|
|
3538
|
+
'error-details': 0,
|
|
3539
|
+
'git-changes': 0
|
|
3295
3540
|
});
|
|
3296
3541
|
setSectionFocus({
|
|
3297
3542
|
runs: 'current-run',
|
|
3298
3543
|
intents: 'intent-status',
|
|
3299
3544
|
completed: 'completed-runs',
|
|
3300
|
-
health: 'standards'
|
|
3545
|
+
health: 'standards',
|
|
3546
|
+
git: 'git-changes'
|
|
3301
3547
|
});
|
|
3302
3548
|
setOverviewIntentFilter('next');
|
|
3303
3549
|
setExpandedGroups({});
|
|
@@ -3332,13 +3578,15 @@ function createDashboardApp(deps) {
|
|
|
3332
3578
|
standards: 0,
|
|
3333
3579
|
stats: 0,
|
|
3334
3580
|
warnings: 0,
|
|
3335
|
-
'error-details': 0
|
|
3581
|
+
'error-details': 0,
|
|
3582
|
+
'git-changes': 0
|
|
3336
3583
|
});
|
|
3337
3584
|
setSectionFocus({
|
|
3338
3585
|
runs: 'current-run',
|
|
3339
3586
|
intents: 'intent-status',
|
|
3340
3587
|
completed: 'completed-runs',
|
|
3341
|
-
health: 'standards'
|
|
3588
|
+
health: 'standards',
|
|
3589
|
+
git: 'git-changes'
|
|
3342
3590
|
});
|
|
3343
3591
|
setOverviewIntentFilter('next');
|
|
3344
3592
|
setExpandedGroups({});
|
|
@@ -3442,6 +3690,27 @@ function createDashboardApp(deps) {
|
|
|
3442
3690
|
setSectionFocus((previous) => ({ ...previous, health: 'error-details' }));
|
|
3443
3691
|
return;
|
|
3444
3692
|
}
|
|
3693
|
+
} else if (ui.view === 'git') {
|
|
3694
|
+
if (input === 'd') {
|
|
3695
|
+
setSectionFocus((previous) => ({ ...previous, git: 'git-changes' }));
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (input === 'c') {
|
|
3699
|
+
setStatusLine('Git commit action is not wired yet (footer mirrors LazyGit command hierarchy).');
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
if (input === 'A') {
|
|
3703
|
+
setStatusLine('Git amend action is not wired yet (footer mirrors LazyGit command hierarchy).');
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
if (input === 'p') {
|
|
3707
|
+
setStatusLine('Git push action is not wired yet (footer mirrors LazyGit command hierarchy).');
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
if (input === 'P') {
|
|
3711
|
+
setStatusLine('Git pull action is not wired yet (footer mirrors LazyGit command hierarchy).');
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3445
3714
|
}
|
|
3446
3715
|
|
|
3447
3716
|
if (key.escape) {
|
|
@@ -3651,16 +3920,16 @@ function createDashboardApp(deps) {
|
|
|
3651
3920
|
useEffect(() => {
|
|
3652
3921
|
if (!stdout || typeof stdout.on !== 'function') {
|
|
3653
3922
|
setTerminalSize({
|
|
3654
|
-
columns: process.stdout.columns || 120,
|
|
3655
|
-
rows: process.stdout.rows || 40
|
|
3923
|
+
columns: Math.max(1, process.stdout.columns || 120),
|
|
3924
|
+
rows: Math.max(1, process.stdout.rows || 40)
|
|
3656
3925
|
});
|
|
3657
3926
|
return undefined;
|
|
3658
3927
|
}
|
|
3659
3928
|
|
|
3660
3929
|
const updateSize = () => {
|
|
3661
3930
|
setTerminalSize({
|
|
3662
|
-
columns: stdout.columns || process.stdout.columns || 120,
|
|
3663
|
-
rows: stdout.rows || process.stdout.rows || 40
|
|
3931
|
+
columns: Math.max(1, stdout.columns || process.stdout.columns || 120),
|
|
3932
|
+
rows: Math.max(1, stdout.rows || process.stdout.rows || 40)
|
|
3664
3933
|
});
|
|
3665
3934
|
|
|
3666
3935
|
// Resize in some terminals can leave stale frame rows behind.
|
|
@@ -3672,6 +3941,9 @@ function createDashboardApp(deps) {
|
|
|
3672
3941
|
|
|
3673
3942
|
updateSize();
|
|
3674
3943
|
stdout.on('resize', updateSize);
|
|
3944
|
+
if (process.stdout !== stdout && typeof process.stdout.on === 'function') {
|
|
3945
|
+
process.stdout.on('resize', updateSize);
|
|
3946
|
+
}
|
|
3675
3947
|
|
|
3676
3948
|
return () => {
|
|
3677
3949
|
if (typeof stdout.off === 'function') {
|
|
@@ -3679,6 +3951,13 @@ function createDashboardApp(deps) {
|
|
|
3679
3951
|
} else if (typeof stdout.removeListener === 'function') {
|
|
3680
3952
|
stdout.removeListener('resize', updateSize);
|
|
3681
3953
|
}
|
|
3954
|
+
if (process.stdout !== stdout) {
|
|
3955
|
+
if (typeof process.stdout.off === 'function') {
|
|
3956
|
+
process.stdout.off('resize', updateSize);
|
|
3957
|
+
} else if (typeof process.stdout.removeListener === 'function') {
|
|
3958
|
+
process.stdout.removeListener('resize', updateSize);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3682
3961
|
};
|
|
3683
3962
|
}, [stdout]);
|
|
3684
3963
|
|
|
@@ -3717,7 +3996,9 @@ function createDashboardApp(deps) {
|
|
|
3717
3996
|
});
|
|
3718
3997
|
|
|
3719
3998
|
runtime.start();
|
|
3720
|
-
const fallbackIntervalMs =
|
|
3999
|
+
const fallbackIntervalMs = ui.view === 'git'
|
|
4000
|
+
? Math.max(refreshMs, 1000)
|
|
4001
|
+
: Math.max(refreshMs, 5000);
|
|
3721
4002
|
const interval = setInterval(() => {
|
|
3722
4003
|
void refresh();
|
|
3723
4004
|
}, fallbackIntervalMs);
|
|
@@ -3726,7 +4007,7 @@ function createDashboardApp(deps) {
|
|
|
3726
4007
|
clearInterval(interval);
|
|
3727
4008
|
void runtime.close();
|
|
3728
4009
|
};
|
|
3729
|
-
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, worktreeWatchSignature, selectedWorktreeId]);
|
|
4010
|
+
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath, resolveRootPathForFlow, resolveRootPathsForFlow, activeFlow, ui.view, worktreeWatchSignature, selectedWorktreeId]);
|
|
3730
4011
|
|
|
3731
4012
|
useEffect(() => {
|
|
3732
4013
|
if (!stdout || typeof stdout.write !== 'function') {
|
|
@@ -3741,37 +4022,31 @@ function createDashboardApp(deps) {
|
|
|
3741
4022
|
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
3742
4023
|
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
3743
4024
|
|
|
3744
|
-
const fullWidth =
|
|
4025
|
+
const fullWidth = resolveFrameWidth(cols);
|
|
3745
4026
|
const showFlowBar = availableFlowIds.length > 1;
|
|
3746
|
-
const
|
|
4027
|
+
const showCommandLogLine = rows >= 8;
|
|
4028
|
+
const showCommandStrip = rows >= 9;
|
|
3747
4029
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
3748
4030
|
const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp && !worktreeOverlayOpen;
|
|
3749
4031
|
const showErrorInline = Boolean(error) && !showErrorPanel && !worktreeOverlayOpen;
|
|
3750
4032
|
const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp && !worktreeOverlayOpen;
|
|
3751
|
-
const
|
|
4033
|
+
const showLegacyStatusLine = statusLine !== '' && !showCommandLogLine;
|
|
3752
4034
|
const densePanels = rows <= 28 || cols <= 120;
|
|
3753
4035
|
|
|
3754
4036
|
const reservedRows =
|
|
3755
4037
|
2 +
|
|
3756
4038
|
(showFlowBar ? 1 : 0) +
|
|
3757
4039
|
(showApprovalBanner ? 1 : 0) +
|
|
3758
|
-
(
|
|
4040
|
+
(showCommandLogLine ? 1 : 0) +
|
|
4041
|
+
(showCommandStrip ? 1 : 0) +
|
|
3759
4042
|
(showGlobalErrorPanel ? 5 : 0) +
|
|
3760
4043
|
(showErrorInline ? 1 : 0) +
|
|
3761
|
-
(
|
|
4044
|
+
(showLegacyStatusLine ? 1 : 0);
|
|
3762
4045
|
const frameSafetyRows = 2;
|
|
3763
4046
|
const contentRowsBudget = Math.max(4, rows - reservedRows - frameSafetyRows);
|
|
3764
4047
|
const ultraCompact = rows <= 14;
|
|
3765
4048
|
const panelTitles = getPanelTitles(activeFlow, snapshot);
|
|
3766
|
-
const
|
|
3767
|
-
const mainPaneWidth = splitPreviewLayout
|
|
3768
|
-
? Math.max(34, Math.floor((fullWidth - 1) * 0.52))
|
|
3769
|
-
: fullWidth;
|
|
3770
|
-
const previewPaneWidth = splitPreviewLayout
|
|
3771
|
-
? Math.max(30, fullWidth - mainPaneWidth - 1)
|
|
3772
|
-
: fullWidth;
|
|
3773
|
-
const mainCompactWidth = Math.max(18, mainPaneWidth - 4);
|
|
3774
|
-
const previewCompactWidth = Math.max(18, previewPaneWidth - 4);
|
|
4049
|
+
const compactWidth = Math.max(18, fullWidth - 4);
|
|
3775
4050
|
|
|
3776
4051
|
const sectionLines = Object.fromEntries(
|
|
3777
4052
|
Object.entries(rowsBySection).map(([sectionKey, sectionRows]) => [
|
|
@@ -3780,14 +4055,14 @@ function createDashboardApp(deps) {
|
|
|
3780
4055
|
sectionRows,
|
|
3781
4056
|
selectionBySection[sectionKey] || 0,
|
|
3782
4057
|
icons,
|
|
3783
|
-
|
|
4058
|
+
compactWidth,
|
|
3784
4059
|
paneFocus === 'main' && focusedSection === sectionKey
|
|
3785
4060
|
)
|
|
3786
4061
|
])
|
|
3787
4062
|
);
|
|
3788
4063
|
const effectivePreviewTarget = previewTarget || selectedFocusedFile;
|
|
3789
4064
|
const previewLines = previewOpen
|
|
3790
|
-
? buildPreviewLines(effectivePreviewTarget,
|
|
4065
|
+
? buildPreviewLines(effectivePreviewTarget, compactWidth, previewScroll, {
|
|
3791
4066
|
fullDocument: overlayPreviewOpen
|
|
3792
4067
|
})
|
|
3793
4068
|
: [];
|
|
@@ -3808,6 +4083,17 @@ function createDashboardApp(deps) {
|
|
|
3808
4083
|
availableFlowCount: availableFlowIds.length,
|
|
3809
4084
|
hasWorktrees: worktreeSectionEnabled
|
|
3810
4085
|
});
|
|
4086
|
+
const commandStripText = buildLazyGitCommandStrip(ui.view, {
|
|
4087
|
+
hasWorktrees: worktreeSectionEnabled,
|
|
4088
|
+
previewOpen
|
|
4089
|
+
});
|
|
4090
|
+
const commandLogLine = buildLazyGitCommandLogLine({
|
|
4091
|
+
statusLine,
|
|
4092
|
+
activeFlow,
|
|
4093
|
+
watchEnabled,
|
|
4094
|
+
watchStatus,
|
|
4095
|
+
selectedWorktreeLabel
|
|
4096
|
+
});
|
|
3811
4097
|
|
|
3812
4098
|
let panelCandidates;
|
|
3813
4099
|
if (ui.showHelp) {
|
|
@@ -3885,6 +4171,15 @@ function createDashboardApp(deps) {
|
|
|
3885
4171
|
borderColor: 'red'
|
|
3886
4172
|
});
|
|
3887
4173
|
}
|
|
4174
|
+
} else if (ui.view === 'git') {
|
|
4175
|
+
panelCandidates = [
|
|
4176
|
+
{
|
|
4177
|
+
key: 'git-changes',
|
|
4178
|
+
title: panelTitles.git || 'Git Changes',
|
|
4179
|
+
lines: sectionLines['git-changes'],
|
|
4180
|
+
borderColor: 'yellow'
|
|
4181
|
+
}
|
|
4182
|
+
];
|
|
3888
4183
|
} else {
|
|
3889
4184
|
panelCandidates = [];
|
|
3890
4185
|
if (worktreeSectionEnabled) {
|
|
@@ -3919,7 +4214,7 @@ function createDashboardApp(deps) {
|
|
|
3919
4214
|
}
|
|
3920
4215
|
}
|
|
3921
4216
|
|
|
3922
|
-
if (!ui.showHelp && previewOpen && !overlayPreviewOpen
|
|
4217
|
+
if (!ui.showHelp && previewOpen && !overlayPreviewOpen) {
|
|
3923
4218
|
panelCandidates.push({
|
|
3924
4219
|
key: 'preview',
|
|
3925
4220
|
title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
|
|
@@ -3928,7 +4223,7 @@ function createDashboardApp(deps) {
|
|
|
3928
4223
|
});
|
|
3929
4224
|
}
|
|
3930
4225
|
|
|
3931
|
-
if (ultraCompact
|
|
4226
|
+
if (ultraCompact) {
|
|
3932
4227
|
if (previewOpen) {
|
|
3933
4228
|
panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === focusedSection || panel.key === 'preview'));
|
|
3934
4229
|
} else {
|
|
@@ -3938,6 +4233,12 @@ function createDashboardApp(deps) {
|
|
|
3938
4233
|
}
|
|
3939
4234
|
|
|
3940
4235
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
4236
|
+
const lazyGitHierarchyLayout = !ui.showHelp
|
|
4237
|
+
&& !worktreeOverlayOpen
|
|
4238
|
+
&& !overlayPreviewOpen
|
|
4239
|
+
&& !ultraCompact
|
|
4240
|
+
&& fullWidth >= 96
|
|
4241
|
+
&& panelCandidates.length > 1;
|
|
3941
4242
|
|
|
3942
4243
|
const renderPanel = (panel, index, width, isFocused) => React.createElement(SectionPanel, {
|
|
3943
4244
|
key: panel.key,
|
|
@@ -3952,14 +4253,21 @@ function createDashboardApp(deps) {
|
|
|
3952
4253
|
});
|
|
3953
4254
|
|
|
3954
4255
|
let contentNode;
|
|
3955
|
-
if (
|
|
3956
|
-
const
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
4256
|
+
if (lazyGitHierarchyLayout) {
|
|
4257
|
+
const preferredRightPanel = previewOpen && !overlayPreviewOpen
|
|
4258
|
+
? panelCandidates.find((panel) => panel?.key === 'preview')
|
|
4259
|
+
: null;
|
|
4260
|
+
const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
|
|
4261
|
+
const rightPanelBase = preferredRightPanel
|
|
4262
|
+
|| focusedPanel
|
|
4263
|
+
|| panelCandidates[panelCandidates.length - 1];
|
|
4264
|
+
const leftCandidates = panelCandidates.filter((panel) => panel?.key !== rightPanelBase?.key);
|
|
4265
|
+
const leftWidth = Math.max(30, Math.min(Math.floor(fullWidth * 0.38), fullWidth - 36));
|
|
4266
|
+
const rightWidth = Math.max(34, fullWidth - leftWidth - 1);
|
|
4267
|
+
const leftPanels = allocateSingleColumnPanels(leftCandidates, contentRowsBudget);
|
|
4268
|
+
const rightPanel = {
|
|
4269
|
+
...rightPanelBase,
|
|
4270
|
+
maxLines: Math.max(4, contentRowsBudget)
|
|
3963
4271
|
};
|
|
3964
4272
|
|
|
3965
4273
|
contentNode = React.createElement(
|
|
@@ -3967,33 +4275,39 @@ function createDashboardApp(deps) {
|
|
|
3967
4275
|
{ width: fullWidth, flexDirection: 'row' },
|
|
3968
4276
|
React.createElement(
|
|
3969
4277
|
Box,
|
|
3970
|
-
{ width:
|
|
3971
|
-
...
|
|
4278
|
+
{ width: leftWidth, flexDirection: 'column' },
|
|
4279
|
+
...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
|
|
3972
4280
|
key: panel.key,
|
|
3973
4281
|
title: panel.title,
|
|
3974
4282
|
lines: panel.lines,
|
|
3975
|
-
width:
|
|
4283
|
+
width: leftWidth,
|
|
3976
4284
|
maxLines: panel.maxLines,
|
|
3977
4285
|
borderColor: panel.borderColor,
|
|
3978
|
-
marginBottom: densePanels ? 0 : (index ===
|
|
4286
|
+
marginBottom: densePanels ? 0 : (index === leftPanels.length - 1 ? 0 : 1),
|
|
3979
4287
|
dense: densePanels,
|
|
3980
4288
|
focused: paneFocus === 'main' && panel.key === focusedSection
|
|
3981
4289
|
}))
|
|
3982
4290
|
),
|
|
3983
|
-
React.createElement(Box, { width: 1 }, React.createElement(Text, null, ' ')),
|
|
3984
4291
|
React.createElement(
|
|
3985
4292
|
Box,
|
|
3986
|
-
{ width:
|
|
4293
|
+
{ width: 1, justifyContent: 'center' },
|
|
4294
|
+
React.createElement(Text, { color: 'gray' }, '│')
|
|
4295
|
+
),
|
|
4296
|
+
React.createElement(
|
|
4297
|
+
Box,
|
|
4298
|
+
{ width: rightWidth, flexDirection: 'column' },
|
|
3987
4299
|
React.createElement(SectionPanel, {
|
|
3988
|
-
key:
|
|
3989
|
-
title:
|
|
3990
|
-
lines:
|
|
3991
|
-
width:
|
|
3992
|
-
maxLines:
|
|
3993
|
-
borderColor:
|
|
4300
|
+
key: rightPanel.key,
|
|
4301
|
+
title: rightPanel.title,
|
|
4302
|
+
lines: rightPanel.lines,
|
|
4303
|
+
width: rightWidth,
|
|
4304
|
+
maxLines: rightPanel.maxLines,
|
|
4305
|
+
borderColor: rightPanel.borderColor,
|
|
3994
4306
|
marginBottom: 0,
|
|
3995
4307
|
dense: densePanels,
|
|
3996
|
-
focused:
|
|
4308
|
+
focused: rightPanel.key === 'preview'
|
|
4309
|
+
? paneFocus === 'preview'
|
|
4310
|
+
: (paneFocus === 'main' && rightPanel.key === focusedSection)
|
|
3997
4311
|
})
|
|
3998
4312
|
)
|
|
3999
4313
|
);
|
|
@@ -4043,12 +4357,25 @@ function createDashboardApp(deps) {
|
|
|
4043
4357
|
})
|
|
4044
4358
|
: null,
|
|
4045
4359
|
...(Array.isArray(contentNode) ? contentNode : [contentNode]),
|
|
4046
|
-
|
|
4360
|
+
showLegacyStatusLine
|
|
4047
4361
|
? React.createElement(Text, { color: 'yellow' }, truncate(statusLine, fullWidth))
|
|
4048
4362
|
: null,
|
|
4049
|
-
|
|
4050
|
-
? React.createElement(
|
|
4051
|
-
|
|
4363
|
+
showCommandLogLine
|
|
4364
|
+
? React.createElement(
|
|
4365
|
+
Text,
|
|
4366
|
+
{ color: 'white', backgroundColor: 'gray', bold: true },
|
|
4367
|
+
truncate(commandLogLine, fullWidth)
|
|
4368
|
+
)
|
|
4369
|
+
: null,
|
|
4370
|
+
showCommandStrip
|
|
4371
|
+
? React.createElement(
|
|
4372
|
+
Text,
|
|
4373
|
+
{ color: 'white', backgroundColor: 'blue', bold: true },
|
|
4374
|
+
truncate(commandStripText, fullWidth)
|
|
4375
|
+
)
|
|
4376
|
+
: (rows >= 10
|
|
4377
|
+
? React.createElement(Text, { color: 'gray' }, truncate(quickHelpText, fullWidth))
|
|
4378
|
+
: null)
|
|
4052
4379
|
);
|
|
4053
4380
|
}
|
|
4054
4381
|
|
|
@@ -15,12 +15,15 @@ function cycleView(current) {
|
|
|
15
15
|
if (current === 'completed') {
|
|
16
16
|
return 'health';
|
|
17
17
|
}
|
|
18
|
+
if (current === 'health') {
|
|
19
|
+
return 'git';
|
|
20
|
+
}
|
|
18
21
|
return 'runs';
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function cycleViewBackward(current) {
|
|
22
25
|
if (current === 'runs') {
|
|
23
|
-
return '
|
|
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.53",
|
|
4
4
|
"description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
|
|
5
5
|
"main": "lib/installer.js",
|
|
6
6
|
"bin": {
|