specsmd 0.1.56 → 0.1.58
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/tui/app.js +85 -3056
- package/lib/dashboard/tui/file-entries.js +382 -0
- package/lib/dashboard/tui/flow-builders.js +991 -0
- package/lib/dashboard/tui/git-builders.js +218 -0
- package/lib/dashboard/tui/helpers.js +236 -0
- package/lib/dashboard/tui/overlays.js +231 -0
- package/lib/dashboard/tui/preview.js +145 -0
- package/lib/dashboard/tui/row-builders.js +794 -0
- package/lib/dashboard/tui/sections.js +45 -0
- package/lib/dashboard/tui/worktree-builders.js +229 -0
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,3062 +1,84 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
4
|
-
const stringWidthModule = require('string-width');
|
|
5
|
-
const sliceAnsiModule = require('slice-ansi');
|
|
6
1
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
7
2
|
const { createInitialUIState } = require('./store');
|
|
8
3
|
|
|
9
|
-
const stringWidth = typeof stringWidthModule === 'function'
|
|
10
|
-
? stringWidthModule
|
|
11
|
-
: stringWidthModule.default;
|
|
12
|
-
const sliceAnsi = typeof sliceAnsiModule === 'function'
|
|
13
|
-
? sliceAnsiModule
|
|
14
|
-
: sliceAnsiModule.default;
|
|
15
4
|
const {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (typeof error === 'string') {
|
|
29
|
-
return {
|
|
30
|
-
code: defaultCode,
|
|
31
|
-
message: error
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof error === 'object') {
|
|
36
|
-
return {
|
|
37
|
-
code: error.code || defaultCode,
|
|
38
|
-
message: error.message || 'Unknown dashboard error.',
|
|
39
|
-
details: error.details,
|
|
40
|
-
path: error.path,
|
|
41
|
-
hint: error.hint
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
code: defaultCode,
|
|
47
|
-
message: String(error)
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function safeJsonHash(value) {
|
|
52
|
-
try {
|
|
53
|
-
return JSON.stringify(value, (key, nestedValue) => {
|
|
54
|
-
if (key === 'generatedAt') {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
return nestedValue;
|
|
58
|
-
});
|
|
59
|
-
} catch {
|
|
60
|
-
return String(value);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function resolveIconSet() {
|
|
65
|
-
const mode = (process.env.SPECSMD_ICON_SET || 'auto').toLowerCase();
|
|
66
|
-
|
|
67
|
-
const ascii = {
|
|
68
|
-
runs: '[R]',
|
|
69
|
-
overview: '[O]',
|
|
70
|
-
health: '[H]',
|
|
71
|
-
git: '[G]',
|
|
72
|
-
runFile: '*',
|
|
73
|
-
activeFile: '>',
|
|
74
|
-
groupCollapsed: '>',
|
|
75
|
-
groupExpanded: 'v'
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const nerd = {
|
|
79
|
-
runs: '',
|
|
80
|
-
overview: '',
|
|
81
|
-
health: '',
|
|
82
|
-
git: '',
|
|
83
|
-
runFile: '',
|
|
84
|
-
activeFile: '',
|
|
85
|
-
groupCollapsed: '',
|
|
86
|
-
groupExpanded: ''
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (mode === 'ascii') {
|
|
90
|
-
return ascii;
|
|
91
|
-
}
|
|
92
|
-
if (mode === 'nerd') {
|
|
93
|
-
return nerd;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const locale = `${process.env.LC_ALL || ''}${process.env.LC_CTYPE || ''}${process.env.LANG || ''}`;
|
|
97
|
-
const isUtf8 = /utf-?8/i.test(locale);
|
|
98
|
-
const looksLikeVsCodeTerminal = (process.env.TERM_PROGRAM || '').toLowerCase().includes('vscode');
|
|
99
|
-
|
|
100
|
-
return isUtf8 && looksLikeVsCodeTerminal ? nerd : ascii;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function truncate(value, width) {
|
|
104
|
-
const text = String(value ?? '');
|
|
105
|
-
if (!Number.isFinite(width)) {
|
|
106
|
-
return text;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const safeWidth = Math.max(0, Math.floor(width));
|
|
110
|
-
if (safeWidth === 0) {
|
|
111
|
-
return '';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (stringWidth(text) <= safeWidth) {
|
|
115
|
-
return text;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (safeWidth <= 3) {
|
|
119
|
-
return sliceAnsi(text, 0, safeWidth);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const ellipsis = '...';
|
|
123
|
-
const bodyWidth = Math.max(0, safeWidth - stringWidth(ellipsis));
|
|
124
|
-
return `${sliceAnsi(text, 0, bodyWidth)}${ellipsis}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function resolveFrameWidth(columns) {
|
|
128
|
-
const safeColumns = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 120;
|
|
129
|
-
return safeColumns > 24 ? safeColumns - 1 : safeColumns;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function normalizePanelLine(line) {
|
|
133
|
-
if (line && typeof line === 'object' && !Array.isArray(line)) {
|
|
134
|
-
return {
|
|
135
|
-
text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
|
|
136
|
-
color: line.color,
|
|
137
|
-
bold: Boolean(line.bold),
|
|
138
|
-
selected: Boolean(line.selected),
|
|
139
|
-
loading: Boolean(line.loading)
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
text: String(line ?? ''),
|
|
145
|
-
color: undefined,
|
|
146
|
-
bold: false,
|
|
147
|
-
selected: false,
|
|
148
|
-
loading: false
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function fitLines(lines, maxLines, width) {
|
|
153
|
-
const safeLines = (Array.isArray(lines) ? lines : []).map((line) => {
|
|
154
|
-
const normalized = normalizePanelLine(line);
|
|
155
|
-
return {
|
|
156
|
-
...normalized,
|
|
157
|
-
text: truncate(normalized.text, width)
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (safeLines.length <= maxLines) {
|
|
162
|
-
return safeLines;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const selectedIndex = safeLines.findIndex((line) => line.selected);
|
|
166
|
-
if (selectedIndex >= 0) {
|
|
167
|
-
const windowSize = Math.max(1, maxLines);
|
|
168
|
-
let start = selectedIndex - Math.floor(windowSize / 2);
|
|
169
|
-
start = Math.max(0, start);
|
|
170
|
-
start = Math.min(start, Math.max(0, safeLines.length - windowSize));
|
|
171
|
-
return safeLines.slice(start, start + windowSize);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
175
|
-
visible.push({
|
|
176
|
-
text: truncate(`... +${safeLines.length - visible.length} more`, width),
|
|
177
|
-
color: 'gray',
|
|
178
|
-
bold: false
|
|
179
|
-
});
|
|
180
|
-
return visible;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function formatTime(value) {
|
|
184
|
-
if (!value) {
|
|
185
|
-
return 'n/a';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const date = new Date(value);
|
|
189
|
-
if (Number.isNaN(date.getTime())) {
|
|
190
|
-
return value;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return date.toLocaleTimeString();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function buildShortStats(snapshot, flow) {
|
|
197
|
-
if (!snapshot?.initialized) {
|
|
198
|
-
if (flow === 'aidlc') {
|
|
199
|
-
return 'init: waiting for memory-bank scan';
|
|
200
|
-
}
|
|
201
|
-
if (flow === 'simple') {
|
|
202
|
-
return 'init: waiting for specs scan';
|
|
203
|
-
}
|
|
204
|
-
return 'init: waiting for state.yaml';
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const stats = snapshot?.stats || {};
|
|
208
|
-
|
|
209
|
-
if (flow === 'aidlc') {
|
|
210
|
-
return `bolts ${stats.activeBoltsCount || 0}/${stats.completedBolts || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | stories ${stats.completedStories || 0}/${stats.totalStories || 0}`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (flow === 'simple') {
|
|
214
|
-
return `specs ${stats.completedSpecs || 0}/${stats.totalSpecs || 0} | tasks ${stats.completedTasks || 0}/${stats.totalTasks || 0} | active ${stats.activeSpecsCount || 0}`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return `runs ${stats.activeRunsCount || 0}/${stats.completedRuns || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | work ${stats.completedWorkItems || 0}/${stats.totalWorkItems || 0}`;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, width, worktreeLabel = null) {
|
|
221
|
-
const projectName = snapshot?.project?.name || 'Unnamed project';
|
|
222
|
-
const shortStats = buildShortStats(snapshot, flow);
|
|
223
|
-
const worktreeSegment = worktreeLabel ? ` | wt:${worktreeLabel}` : '';
|
|
224
|
-
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'}${worktreeSegment} | ${view} | ${formatTime(lastRefreshAt)}`;
|
|
225
|
-
|
|
226
|
-
return truncate(line, width);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function buildErrorLines(error, width) {
|
|
230
|
-
if (!error) {
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const lines = [`[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`];
|
|
235
|
-
|
|
236
|
-
if (error.details) {
|
|
237
|
-
lines.push(`details: ${error.details}`);
|
|
238
|
-
}
|
|
239
|
-
if (error.path) {
|
|
240
|
-
lines.push(`path: ${error.path}`);
|
|
241
|
-
}
|
|
242
|
-
if (error.hint) {
|
|
243
|
-
lines.push(`hint: ${error.hint}`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return lines.map((line) => truncate(line, width));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function getCurrentRun(snapshot) {
|
|
250
|
-
const activeRuns = Array.isArray(snapshot?.activeRuns) ? [...snapshot.activeRuns] : [];
|
|
251
|
-
if (activeRuns.length === 0) {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
activeRuns.sort((a, b) => {
|
|
256
|
-
const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
|
|
257
|
-
const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
|
|
258
|
-
if (bTime !== aTime) {
|
|
259
|
-
return bTime - aTime;
|
|
260
|
-
}
|
|
261
|
-
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
return activeRuns[0] || null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function normalizeToken(value) {
|
|
268
|
-
if (typeof value !== 'string') {
|
|
269
|
-
return '';
|
|
270
|
-
}
|
|
271
|
-
return value.toLowerCase().trim().replace(/[\s-]+/g, '_');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function getCurrentFireWorkItem(run) {
|
|
275
|
-
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
276
|
-
if (workItems.length === 0) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
return workItems.find((item) => item.id === run.currentItem)
|
|
280
|
-
|| workItems.find((item) => normalizeToken(item?.status) === 'in_progress')
|
|
281
|
-
|| workItems[0]
|
|
282
|
-
|| null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function readFileTextSafe(filePath) {
|
|
286
|
-
try {
|
|
287
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
288
|
-
} catch {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function extractFrontmatterBlock(content) {
|
|
294
|
-
if (typeof content !== 'string') {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
298
|
-
return match ? match[1] : null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function extractFrontmatterValue(frontmatterBlock, key) {
|
|
302
|
-
if (typeof frontmatterBlock !== 'string' || typeof key !== 'string' || key === '') {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
307
|
-
const expression = new RegExp(`^${escapedKey}\\s*:\\s*(.+)$`, 'mi');
|
|
308
|
-
const match = frontmatterBlock.match(expression);
|
|
309
|
-
if (!match) {
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const raw = String(match[1] || '').trim();
|
|
314
|
-
if (raw === '') {
|
|
315
|
-
return '';
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return raw
|
|
319
|
-
.replace(/^["']/, '')
|
|
320
|
-
.replace(/["']$/, '')
|
|
321
|
-
.trim();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const FIRE_AWAITING_APPROVAL_STATES = new Set([
|
|
325
|
-
'awaiting_approval',
|
|
326
|
-
'waiting',
|
|
327
|
-
'pending_approval',
|
|
328
|
-
'approval_needed',
|
|
329
|
-
'approval_required',
|
|
330
|
-
'checkpoint_pending'
|
|
331
|
-
]);
|
|
332
|
-
|
|
333
|
-
const FIRE_APPROVED_STATES = new Set([
|
|
334
|
-
'approved',
|
|
335
|
-
'confirmed',
|
|
336
|
-
'accepted',
|
|
337
|
-
'resumed',
|
|
338
|
-
'done',
|
|
339
|
-
'completed',
|
|
340
|
-
'cleared',
|
|
341
|
-
'none',
|
|
342
|
-
'not_required',
|
|
343
|
-
'skipped'
|
|
344
|
-
]);
|
|
345
|
-
|
|
346
|
-
function parseFirePlanCheckpointMetadata(run) {
|
|
347
|
-
if (!run || typeof run.folderPath !== 'string' || run.folderPath.trim() === '') {
|
|
348
|
-
return { hasPlan: false, checkpointState: null, checkpoint: null };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const planPath = path.join(run.folderPath, 'plan.md');
|
|
352
|
-
if (!fileExists(planPath)) {
|
|
353
|
-
return { hasPlan: false, checkpointState: null, checkpoint: null };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const content = readFileTextSafe(planPath);
|
|
357
|
-
const frontmatter = extractFrontmatterBlock(content);
|
|
358
|
-
if (!frontmatter) {
|
|
359
|
-
return { hasPlan: true, checkpointState: null, checkpoint: null };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const checkpointState = normalizeToken(
|
|
363
|
-
extractFrontmatterValue(frontmatter, 'checkpoint_state')
|
|
364
|
-
|| extractFrontmatterValue(frontmatter, 'checkpointState')
|
|
365
|
-
|| extractFrontmatterValue(frontmatter, 'approval_state')
|
|
366
|
-
|| extractFrontmatterValue(frontmatter, 'approvalState')
|
|
367
|
-
|| ''
|
|
368
|
-
) || null;
|
|
369
|
-
const checkpoint = extractFrontmatterValue(frontmatter, 'current_checkpoint')
|
|
370
|
-
|| extractFrontmatterValue(frontmatter, 'currentCheckpoint')
|
|
371
|
-
|| extractFrontmatterValue(frontmatter, 'checkpoint')
|
|
372
|
-
|| null;
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
hasPlan: true,
|
|
376
|
-
checkpointState,
|
|
377
|
-
checkpoint
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function resolveFireApprovalState(run, currentWorkItem) {
|
|
382
|
-
const itemState = normalizeToken(
|
|
383
|
-
currentWorkItem?.checkpointState
|
|
384
|
-
|| currentWorkItem?.checkpoint_state
|
|
385
|
-
|| currentWorkItem?.approvalState
|
|
386
|
-
|| currentWorkItem?.approval_state
|
|
387
|
-
|| ''
|
|
388
|
-
);
|
|
389
|
-
const runState = normalizeToken(
|
|
390
|
-
run?.checkpointState
|
|
391
|
-
|| run?.checkpoint_state
|
|
392
|
-
|| run?.approvalState
|
|
393
|
-
|| run?.approval_state
|
|
394
|
-
|| ''
|
|
395
|
-
);
|
|
396
|
-
const planState = parseFirePlanCheckpointMetadata(run);
|
|
397
|
-
const state = itemState || runState || planState.checkpointState || null;
|
|
398
|
-
const checkpoint = currentWorkItem?.currentCheckpoint
|
|
399
|
-
|| currentWorkItem?.current_checkpoint
|
|
400
|
-
|| run?.currentCheckpoint
|
|
401
|
-
|| run?.current_checkpoint
|
|
402
|
-
|| planState.checkpoint
|
|
403
|
-
|| null;
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
state,
|
|
407
|
-
checkpoint,
|
|
408
|
-
source: itemState
|
|
409
|
-
? 'item-state'
|
|
410
|
-
: (runState
|
|
411
|
-
? 'run-state'
|
|
412
|
-
: (planState.checkpointState ? 'plan-frontmatter' : null))
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function getFireRunApprovalGate(run, currentWorkItem) {
|
|
417
|
-
const mode = normalizeToken(currentWorkItem?.mode);
|
|
418
|
-
const status = normalizeToken(currentWorkItem?.status);
|
|
419
|
-
if (!['confirm', 'validate'].includes(mode) || status !== 'in_progress') {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const phase = normalizeToken(getCurrentPhaseLabel(run, currentWorkItem));
|
|
424
|
-
if (phase !== 'plan') {
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const resolvedApproval = resolveFireApprovalState(run, currentWorkItem);
|
|
429
|
-
if (!resolvedApproval.state) {
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (FIRE_APPROVED_STATES.has(resolvedApproval.state)) {
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (!FIRE_AWAITING_APPROVAL_STATES.has(resolvedApproval.state)) {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const modeLabel = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
442
|
-
const itemId = String(currentWorkItem?.id || run.currentItem || 'unknown-item');
|
|
443
|
-
const checkpointLabel = String(resolvedApproval.checkpoint || 'plan').replace(/[_\s]+/g, '-');
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
flow: 'fire',
|
|
447
|
-
title: 'Approval Needed',
|
|
448
|
-
message: `${run.id}: ${itemId} (${modeLabel}) is waiting at ${checkpointLabel} checkpoint`,
|
|
449
|
-
checkpoint: checkpointLabel,
|
|
450
|
-
source: resolvedApproval.source
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function isFireRunAwaitingApproval(run, currentWorkItem) {
|
|
455
|
-
return Boolean(getFireRunApprovalGate(run, currentWorkItem));
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function detectFireRunApprovalGate(snapshot) {
|
|
459
|
-
const run = getCurrentRun(snapshot);
|
|
460
|
-
if (!run) {
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
465
|
-
if (!currentWorkItem) {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return getFireRunApprovalGate(run, currentWorkItem);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function normalizeStageName(stage) {
|
|
473
|
-
return normalizeToken(stage).replace(/_/g, '-');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function getAidlcCheckpointSignalFiles(boltType, stageName) {
|
|
477
|
-
const normalizedType = normalizeToken(boltType).replace(/_/g, '-');
|
|
478
|
-
const normalizedStage = normalizeStageName(stageName);
|
|
479
|
-
|
|
480
|
-
if (normalizedType === 'simple-construction-bolt') {
|
|
481
|
-
if (normalizedStage === 'plan') return ['implementation-plan.md'];
|
|
482
|
-
if (normalizedStage === 'implement') return ['implementation-walkthrough.md'];
|
|
483
|
-
if (normalizedStage === 'test') return ['test-walkthrough.md'];
|
|
484
|
-
return [];
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (normalizedType === 'ddd-construction-bolt') {
|
|
488
|
-
if (normalizedStage === 'model') return ['ddd-01-domain-model.md'];
|
|
489
|
-
if (normalizedStage === 'design') return ['ddd-02-technical-design.md'];
|
|
490
|
-
if (normalizedStage === 'implement') return ['implementation-walkthrough.md'];
|
|
491
|
-
if (normalizedStage === 'test') return ['ddd-03-test-report.md'];
|
|
492
|
-
return [];
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (normalizedType === 'spike-bolt') {
|
|
496
|
-
if (normalizedStage === 'explore') return ['spike-exploration.md'];
|
|
497
|
-
if (normalizedStage === 'document') return ['spike-report.md'];
|
|
498
|
-
return [];
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return [];
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function hasAidlcCheckpointSignal(bolt, stageName) {
|
|
505
|
-
const fileNames = Array.isArray(bolt?.files) ? bolt.files : [];
|
|
506
|
-
const lowerNames = new Set(fileNames.map((name) => String(name || '').toLowerCase()));
|
|
507
|
-
const expectedFiles = getAidlcCheckpointSignalFiles(bolt?.type, stageName)
|
|
508
|
-
.map((name) => String(name).toLowerCase());
|
|
509
|
-
|
|
510
|
-
for (const expectedFile of expectedFiles) {
|
|
511
|
-
if (lowerNames.has(expectedFile)) {
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (normalizeStageName(stageName) === 'adr') {
|
|
517
|
-
for (const name of lowerNames) {
|
|
518
|
-
if (/^adr-[\w-]+\.md$/.test(name)) {
|
|
519
|
-
return true;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function isAidlcBoltAwaitingApproval(bolt) {
|
|
528
|
-
if (!bolt || normalizeToken(bolt.status) !== 'in_progress') {
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const currentStage = normalizeStageName(bolt.currentStage);
|
|
533
|
-
if (!currentStage) {
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
538
|
-
const stageMeta = stages.find((stage) => normalizeStageName(stage?.name) === currentStage);
|
|
539
|
-
if (normalizeToken(stageMeta?.status) === 'completed') {
|
|
540
|
-
return false;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return hasAidlcCheckpointSignal(bolt, currentStage);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function detectAidlcBoltApprovalGate(snapshot) {
|
|
547
|
-
const bolt = getCurrentBolt(snapshot);
|
|
548
|
-
if (!bolt) {
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (!isAidlcBoltAwaitingApproval(bolt)) {
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return {
|
|
557
|
-
flow: 'aidlc',
|
|
558
|
-
title: 'Approval Needed',
|
|
559
|
-
message: `${bolt.id}: ${bolt.currentStage || 'current'} stage is waiting for confirmation`
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function detectDashboardApprovalGate(snapshot, flow) {
|
|
564
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
565
|
-
if (effectiveFlow === 'fire') {
|
|
566
|
-
return detectFireRunApprovalGate(snapshot);
|
|
567
|
-
}
|
|
568
|
-
if (effectiveFlow === 'aidlc') {
|
|
569
|
-
return detectAidlcBoltApprovalGate(snapshot);
|
|
570
|
-
}
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function getCurrentPhaseLabel(run, currentWorkItem) {
|
|
575
|
-
const phase = currentWorkItem?.currentPhase || '';
|
|
576
|
-
if (typeof phase === 'string' && phase !== '') {
|
|
577
|
-
return phase.toLowerCase();
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (run?.hasTestReport) {
|
|
581
|
-
return 'review';
|
|
582
|
-
}
|
|
583
|
-
if (run?.hasPlan) {
|
|
584
|
-
return 'execute';
|
|
585
|
-
}
|
|
586
|
-
return 'plan';
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function buildPhaseTrack(currentPhase) {
|
|
590
|
-
const order = ['plan', 'execute', 'test', 'review'];
|
|
591
|
-
const labels = ['P', 'E', 'T', 'R'];
|
|
592
|
-
const currentIndex = Math.max(0, order.indexOf(currentPhase));
|
|
593
|
-
return labels.map((label, index) => (index === currentIndex ? `[${label}]` : ` ${label} `)).join(' - ');
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function buildFireCurrentRunLines(snapshot, width) {
|
|
597
|
-
const run = getCurrentRun(snapshot);
|
|
598
|
-
if (!run) {
|
|
599
|
-
return [truncate('No active run', width)];
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
603
|
-
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
604
|
-
const currentWorkItem = workItems.find((item) => item.id === run.currentItem) || workItems.find((item) => item.status === 'in_progress') || workItems[0];
|
|
605
|
-
|
|
606
|
-
const itemId = currentWorkItem?.id || run.currentItem || 'n/a';
|
|
607
|
-
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
608
|
-
const status = currentWorkItem?.status || 'pending';
|
|
609
|
-
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
610
|
-
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
611
|
-
|
|
612
|
-
const lines = [
|
|
613
|
-
`${run.id} [${run.scope}] ${completed}/${workItems.length} items done`,
|
|
614
|
-
`work item: ${itemId}`,
|
|
615
|
-
`mode: ${mode} | status: ${status}`,
|
|
616
|
-
`phase: ${phaseTrack}`
|
|
617
|
-
];
|
|
618
|
-
|
|
619
|
-
return lines.map((line) => truncate(line, width));
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function buildFirePendingLines(snapshot, width) {
|
|
623
|
-
const pending = snapshot?.pendingItems || [];
|
|
624
|
-
if (pending.length === 0) {
|
|
625
|
-
return [truncate('No pending work items', width)];
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
return pending.map((item) => {
|
|
629
|
-
const deps = item.dependencies && item.dependencies.length > 0 ? ` deps:${item.dependencies.join(',')}` : '';
|
|
630
|
-
return truncate(`${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`, width);
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function buildFireCompletedLines(snapshot, width) {
|
|
635
|
-
const completedRuns = snapshot?.completedRuns || [];
|
|
636
|
-
if (completedRuns.length === 0) {
|
|
637
|
-
return [truncate('No completed runs yet', width)];
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return completedRuns.map((run) => {
|
|
641
|
-
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
642
|
-
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
643
|
-
return truncate(`${run.id} [${run.scope}] ${completed}/${workItems.length} done at ${run.completedAt || 'unknown'}`, width);
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function buildFireStatsLines(snapshot, width) {
|
|
648
|
-
if (!snapshot?.initialized) {
|
|
649
|
-
return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const stats = snapshot.stats;
|
|
653
|
-
return [
|
|
654
|
-
`intents: ${stats.completedIntents}/${stats.totalIntents} done | in_progress: ${stats.inProgressIntents} | blocked: ${stats.blockedIntents}`,
|
|
655
|
-
`work items: ${stats.completedWorkItems}/${stats.totalWorkItems} done | in_progress: ${stats.inProgressWorkItems} | pending: ${stats.pendingWorkItems} | blocked: ${stats.blockedWorkItems}`,
|
|
656
|
-
`runs: ${stats.activeRunsCount} active | ${stats.completedRuns} completed | ${stats.totalRuns} total`
|
|
657
|
-
].map((line) => truncate(line, width));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function buildWarningsLines(snapshot, width) {
|
|
661
|
-
const warnings = snapshot?.warnings || [];
|
|
662
|
-
if (warnings.length === 0) {
|
|
663
|
-
return [truncate('No warnings', width)];
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
return warnings.map((warning) => truncate(warning, width));
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function buildFireOverviewProjectLines(snapshot, width) {
|
|
670
|
-
if (!snapshot?.initialized) {
|
|
671
|
-
return [
|
|
672
|
-
truncate('FIRE folder detected, but state.yaml is missing.', width),
|
|
673
|
-
truncate('Initialize project context and this view will populate.', width)
|
|
674
|
-
];
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const project = snapshot.project || {};
|
|
678
|
-
const workspace = snapshot.workspace || {};
|
|
679
|
-
|
|
680
|
-
return [
|
|
681
|
-
`project: ${project.name || 'unknown'} | fire_version: ${project.fireVersion || snapshot.version || '0.0.0'}`,
|
|
682
|
-
`workspace: ${workspace.type || 'unknown'} / ${workspace.structure || 'unknown'}`,
|
|
683
|
-
`autonomy: ${workspace.autonomyBias || 'unknown'} | run scope pref: ${workspace.runScopePreference || 'unknown'}`
|
|
684
|
-
].map((line) => truncate(line, width));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function buildFireOverviewIntentLines(snapshot, width) {
|
|
688
|
-
const intents = snapshot?.intents || [];
|
|
689
|
-
if (intents.length === 0) {
|
|
690
|
-
return [truncate('No intents found', width)];
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return intents.map((intent) => {
|
|
694
|
-
const workItems = Array.isArray(intent.workItems) ? intent.workItems : [];
|
|
695
|
-
const done = workItems.filter((item) => item.status === 'completed').length;
|
|
696
|
-
return truncate(`${intent.id}: ${intent.status} (${done}/${workItems.length} work items)`, width);
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function buildFireOverviewStandardsLines(snapshot, width) {
|
|
701
|
-
const expected = ['constitution', 'tech-stack', 'coding-standards', 'testing-standards', 'system-architecture'];
|
|
702
|
-
const actual = new Set((snapshot?.standards || []).map((item) => item.type));
|
|
703
|
-
|
|
704
|
-
return expected.map((name) => {
|
|
705
|
-
const marker = actual.has(name) ? '[x]' : '[ ]';
|
|
706
|
-
return truncate(`${marker} ${name}.md`, width);
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function getEffectiveFlow(flow, snapshot) {
|
|
711
|
-
const explicitFlow = typeof flow === 'string' && flow !== '' ? flow : null;
|
|
712
|
-
const snapshotFlow = typeof snapshot?.flow === 'string' && snapshot.flow !== '' ? snapshot.flow : null;
|
|
713
|
-
return (snapshotFlow || explicitFlow || 'fire').toLowerCase();
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function getCurrentBolt(snapshot) {
|
|
717
|
-
const activeBolts = Array.isArray(snapshot?.activeBolts) ? [...snapshot.activeBolts] : [];
|
|
718
|
-
if (activeBolts.length === 0) {
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
activeBolts.sort((a, b) => {
|
|
723
|
-
const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
|
|
724
|
-
const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
|
|
725
|
-
if (bTime !== aTime) {
|
|
726
|
-
return bTime - aTime;
|
|
727
|
-
}
|
|
728
|
-
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
return activeBolts[0] || null;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function buildAidlcStageTrack(bolt) {
|
|
735
|
-
const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
|
|
736
|
-
if (stages.length === 0) {
|
|
737
|
-
return 'n/a';
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
return stages.map((stage) => {
|
|
741
|
-
const label = String(stage?.name || '?').charAt(0).toUpperCase();
|
|
742
|
-
if (stage?.status === 'completed') {
|
|
743
|
-
return `[${label}]`;
|
|
744
|
-
}
|
|
745
|
-
if (stage?.status === 'in_progress') {
|
|
746
|
-
return `<${label}>`;
|
|
747
|
-
}
|
|
748
|
-
return ` ${label} `;
|
|
749
|
-
}).join('-');
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function buildAidlcCurrentRunLines(snapshot, width) {
|
|
753
|
-
const bolt = getCurrentBolt(snapshot);
|
|
754
|
-
if (!bolt) {
|
|
755
|
-
return [truncate('No active bolt', width)];
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
759
|
-
const completedStages = stages.filter((stage) => stage.status === 'completed').length;
|
|
760
|
-
const phaseTrack = buildAidlcStageTrack(bolt);
|
|
761
|
-
const location = `${bolt.intent || 'unknown-intent'} / ${bolt.unit || 'unknown-unit'}`;
|
|
762
|
-
|
|
763
|
-
const lines = [
|
|
764
|
-
`${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages done`,
|
|
765
|
-
`scope: ${location}`,
|
|
766
|
-
`stage: ${bolt.currentStage || 'n/a'} | status: ${bolt.status}`,
|
|
767
|
-
`phase: ${phaseTrack}`
|
|
768
|
-
];
|
|
769
|
-
|
|
770
|
-
return lines.map((line) => truncate(line, width));
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function buildAidlcPendingLines(snapshot, width) {
|
|
774
|
-
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
775
|
-
if (pendingBolts.length === 0) {
|
|
776
|
-
return [truncate('No queued bolts', width)];
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return pendingBolts.map((bolt) => {
|
|
780
|
-
const deps = Array.isArray(bolt.blockedBy) && bolt.blockedBy.length > 0
|
|
781
|
-
? ` blocked_by:${bolt.blockedBy.join(',')}`
|
|
782
|
-
: '';
|
|
783
|
-
const location = `${bolt.intent || 'unknown'}/${bolt.unit || 'unknown'}`;
|
|
784
|
-
return truncate(`${bolt.id} (${bolt.status}) in ${location}${deps}`, width);
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function buildAidlcCompletedLines(snapshot, width) {
|
|
789
|
-
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
790
|
-
if (completedBolts.length === 0) {
|
|
791
|
-
return [truncate('No completed bolts yet', width)];
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return completedBolts.map((bolt) =>
|
|
795
|
-
truncate(`${bolt.id} [${bolt.type}] done at ${bolt.completedAt || 'unknown'}`, width)
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function buildAidlcStatsLines(snapshot, width) {
|
|
800
|
-
const stats = snapshot?.stats || {};
|
|
801
|
-
|
|
802
|
-
return [
|
|
803
|
-
`intents: ${stats.completedIntents || 0}/${stats.totalIntents || 0} done | in_progress: ${stats.inProgressIntents || 0} | blocked: ${stats.blockedIntents || 0}`,
|
|
804
|
-
`stories: ${stats.completedStories || 0}/${stats.totalStories || 0} done | in_progress: ${stats.inProgressStories || 0} | pending: ${stats.pendingStories || 0} | blocked: ${stats.blockedStories || 0}`,
|
|
805
|
-
`bolts: ${stats.activeBoltsCount || 0} active | ${stats.queuedBolts || 0} queued | ${stats.blockedBolts || 0} blocked | ${stats.completedBolts || 0} done`
|
|
806
|
-
].map((line) => truncate(line, width));
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function buildAidlcOverviewProjectLines(snapshot, width) {
|
|
810
|
-
const project = snapshot?.project || {};
|
|
811
|
-
const stats = snapshot?.stats || {};
|
|
812
|
-
|
|
813
|
-
return [
|
|
814
|
-
`project: ${project.name || 'unknown'} | project_type: ${project.projectType || 'unknown'}`,
|
|
815
|
-
`memory-bank: intents ${stats.totalIntents || 0} | units ${stats.totalUnits || 0} | stories ${stats.totalStories || 0}`,
|
|
816
|
-
`progress: ${stats.progressPercent || 0}% stories complete | standards: ${(snapshot?.standards || []).length}`
|
|
817
|
-
].map((line) => truncate(line, width));
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function buildAidlcOverviewIntentLines(snapshot, width) {
|
|
821
|
-
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
822
|
-
if (intents.length === 0) {
|
|
823
|
-
return [truncate('No intents found', width)];
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
return intents.map((intent) => {
|
|
827
|
-
return truncate(
|
|
828
|
-
`${intent.id}: ${intent.status} (${intent.completedStories || 0}/${intent.storyCount || 0} stories, ${intent.completedUnits || 0}/${intent.unitCount || 0} units)`,
|
|
829
|
-
width
|
|
830
|
-
);
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function buildAidlcOverviewStandardsLines(snapshot, width) {
|
|
835
|
-
const standards = Array.isArray(snapshot?.standards) ? snapshot.standards : [];
|
|
836
|
-
if (standards.length === 0) {
|
|
837
|
-
return [truncate('No standards found under memory-bank/standards', width)];
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
return standards.map((standard) =>
|
|
841
|
-
truncate(`[x] ${standard.name || standard.type || 'unknown'}.md`, width)
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function getCurrentSpec(snapshot) {
|
|
846
|
-
const specs = Array.isArray(snapshot?.activeSpecs) ? snapshot.activeSpecs : [];
|
|
847
|
-
if (specs.length === 0) {
|
|
848
|
-
return null;
|
|
849
|
-
}
|
|
850
|
-
return specs[0] || null;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function simplePhaseIndex(state) {
|
|
854
|
-
if (state === 'requirements_pending') {
|
|
855
|
-
return 0;
|
|
856
|
-
}
|
|
857
|
-
if (state === 'design_pending') {
|
|
858
|
-
return 1;
|
|
859
|
-
}
|
|
860
|
-
return 2;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function buildSimplePhaseTrack(spec) {
|
|
864
|
-
if (spec?.state === 'completed') {
|
|
865
|
-
return '[R] - [D] - [T]';
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const labels = ['R', 'D', 'T'];
|
|
869
|
-
const current = simplePhaseIndex(spec?.state);
|
|
870
|
-
return labels.map((label, index) => (index === current ? `[${label}]` : ` ${label} `)).join(' - ');
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function buildSimpleCurrentRunLines(snapshot, width) {
|
|
874
|
-
const spec = getCurrentSpec(snapshot);
|
|
875
|
-
if (!spec) {
|
|
876
|
-
return [truncate('No active spec', width)];
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const files = [
|
|
880
|
-
spec.hasRequirements ? 'req' : '-',
|
|
881
|
-
spec.hasDesign ? 'design' : '-',
|
|
882
|
-
spec.hasTasks ? 'tasks' : '-'
|
|
883
|
-
].join('/');
|
|
884
|
-
|
|
885
|
-
const lines = [
|
|
886
|
-
`${spec.name} [${spec.state}] ${spec.tasksCompleted}/${spec.tasksTotal} tasks done`,
|
|
887
|
-
`phase: ${spec.phase}`,
|
|
888
|
-
`files: ${files}`,
|
|
889
|
-
`track: ${buildSimplePhaseTrack(spec)}`
|
|
890
|
-
];
|
|
891
|
-
|
|
892
|
-
return lines.map((line) => truncate(line, width));
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function buildSimplePendingLines(snapshot, width) {
|
|
896
|
-
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
897
|
-
if (pendingSpecs.length === 0) {
|
|
898
|
-
return [truncate('No pending specs', width)];
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
return pendingSpecs.map((spec) =>
|
|
902
|
-
truncate(`${spec.name} (${spec.state}) ${spec.tasksCompleted}/${spec.tasksTotal} tasks`, width)
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function buildSimpleCompletedLines(snapshot, width) {
|
|
907
|
-
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
908
|
-
if (completedSpecs.length === 0) {
|
|
909
|
-
return [truncate('No completed specs yet', width)];
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
return completedSpecs.map((spec) =>
|
|
913
|
-
truncate(`${spec.name} done at ${spec.updatedAt || 'unknown'} (${spec.tasksCompleted}/${spec.tasksTotal})`, width)
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function buildSimpleStatsLines(snapshot, width) {
|
|
918
|
-
const stats = snapshot?.stats || {};
|
|
919
|
-
|
|
920
|
-
return [
|
|
921
|
-
`specs: ${stats.completedSpecs || 0}/${stats.totalSpecs || 0} complete | in_progress: ${stats.inProgressSpecs || 0} | pending: ${stats.pendingSpecs || 0}`,
|
|
922
|
-
`pipeline: ready ${stats.readySpecs || 0} | design_pending ${stats.designPendingSpecs || 0} | tasks_pending ${stats.tasksPendingSpecs || 0}`,
|
|
923
|
-
`tasks: ${stats.completedTasks || 0}/${stats.totalTasks || 0} complete | pending: ${stats.pendingTasks || 0} | optional: ${stats.optionalTasks || 0}`
|
|
924
|
-
].map((line) => truncate(line, width));
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function buildSimpleOverviewProjectLines(snapshot, width) {
|
|
928
|
-
const project = snapshot?.project || {};
|
|
929
|
-
const stats = snapshot?.stats || {};
|
|
930
|
-
|
|
931
|
-
return [
|
|
932
|
-
`project: ${project.name || 'unknown'} | simple flow`,
|
|
933
|
-
`specs: ${stats.totalSpecs || 0} total | active: ${stats.activeSpecsCount || 0} | completed: ${stats.completedSpecs || 0}`,
|
|
934
|
-
`tasks: ${stats.completedTasks || 0}/${stats.totalTasks || 0} complete (${stats.progressPercent || 0}%)`
|
|
935
|
-
].map((line) => truncate(line, width));
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function buildSimpleOverviewIntentLines(snapshot, width) {
|
|
939
|
-
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
940
|
-
if (specs.length === 0) {
|
|
941
|
-
return [truncate('No specs found', width)];
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
return specs.map((spec) =>
|
|
945
|
-
truncate(`${spec.name}: ${spec.state} (${spec.tasksCompleted}/${spec.tasksTotal} tasks)`, width)
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function buildSimpleOverviewStandardsLines(snapshot, width) {
|
|
950
|
-
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
951
|
-
if (specs.length === 0) {
|
|
952
|
-
return [truncate('No spec artifacts found', width)];
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const reqCount = specs.filter((spec) => spec.hasRequirements).length;
|
|
956
|
-
const designCount = specs.filter((spec) => spec.hasDesign).length;
|
|
957
|
-
const tasksCount = specs.filter((spec) => spec.hasTasks).length;
|
|
958
|
-
const total = specs.length;
|
|
959
|
-
|
|
960
|
-
return [
|
|
961
|
-
`[x] requirements.md coverage ${reqCount}/${total}`,
|
|
962
|
-
`[x] design.md coverage ${designCount}/${total}`,
|
|
963
|
-
`[x] tasks.md coverage ${tasksCount}/${total}`
|
|
964
|
-
].map((line) => truncate(line, width));
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
function buildCurrentRunLines(snapshot, width, flow) {
|
|
968
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
969
|
-
if (effectiveFlow === 'aidlc') {
|
|
970
|
-
return buildAidlcCurrentRunLines(snapshot, width);
|
|
971
|
-
}
|
|
972
|
-
if (effectiveFlow === 'simple') {
|
|
973
|
-
return buildSimpleCurrentRunLines(snapshot, width);
|
|
974
|
-
}
|
|
975
|
-
return buildFireCurrentRunLines(snapshot, width);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function buildPendingLines(snapshot, width, flow) {
|
|
979
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
980
|
-
if (effectiveFlow === 'aidlc') {
|
|
981
|
-
return buildAidlcPendingLines(snapshot, width);
|
|
982
|
-
}
|
|
983
|
-
if (effectiveFlow === 'simple') {
|
|
984
|
-
return buildSimplePendingLines(snapshot, width);
|
|
985
|
-
}
|
|
986
|
-
return buildFirePendingLines(snapshot, width);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function buildCompletedLines(snapshot, width, flow) {
|
|
990
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
991
|
-
if (effectiveFlow === 'aidlc') {
|
|
992
|
-
return buildAidlcCompletedLines(snapshot, width);
|
|
993
|
-
}
|
|
994
|
-
if (effectiveFlow === 'simple') {
|
|
995
|
-
return buildSimpleCompletedLines(snapshot, width);
|
|
996
|
-
}
|
|
997
|
-
return buildFireCompletedLines(snapshot, width);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function buildStatsLines(snapshot, width, flow) {
|
|
1001
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1002
|
-
if (!snapshot?.initialized) {
|
|
1003
|
-
if (effectiveFlow === 'aidlc') {
|
|
1004
|
-
return [truncate('Waiting for memory-bank initialization.', width)];
|
|
1005
|
-
}
|
|
1006
|
-
if (effectiveFlow === 'simple') {
|
|
1007
|
-
return [truncate('Waiting for specs/ initialization.', width)];
|
|
1008
|
-
}
|
|
1009
|
-
return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (effectiveFlow === 'aidlc') {
|
|
1013
|
-
return buildAidlcStatsLines(snapshot, width);
|
|
1014
|
-
}
|
|
1015
|
-
if (effectiveFlow === 'simple') {
|
|
1016
|
-
return buildSimpleStatsLines(snapshot, width);
|
|
1017
|
-
}
|
|
1018
|
-
return buildFireStatsLines(snapshot, width);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function buildOverviewProjectLines(snapshot, width, flow) {
|
|
1022
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1023
|
-
if (effectiveFlow === 'aidlc') {
|
|
1024
|
-
return buildAidlcOverviewProjectLines(snapshot, width);
|
|
1025
|
-
}
|
|
1026
|
-
if (effectiveFlow === 'simple') {
|
|
1027
|
-
return buildSimpleOverviewProjectLines(snapshot, width);
|
|
1028
|
-
}
|
|
1029
|
-
return buildFireOverviewProjectLines(snapshot, width);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function listOverviewIntentEntries(snapshot, flow) {
|
|
1033
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1034
|
-
if (effectiveFlow === 'aidlc') {
|
|
1035
|
-
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
1036
|
-
return intents.map((intent) => ({
|
|
1037
|
-
id: intent?.id || 'unknown',
|
|
1038
|
-
status: intent?.status || 'pending',
|
|
1039
|
-
line: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${intent?.completedStories || 0}/${intent?.storyCount || 0} stories, ${intent?.completedUnits || 0}/${intent?.unitCount || 0} units)`
|
|
1040
|
-
}));
|
|
1041
|
-
}
|
|
1042
|
-
if (effectiveFlow === 'simple') {
|
|
1043
|
-
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
1044
|
-
return specs.map((spec) => ({
|
|
1045
|
-
id: spec?.name || 'unknown',
|
|
1046
|
-
status: spec?.state || 'pending',
|
|
1047
|
-
line: `${spec?.name || 'unknown'}: ${spec?.state || 'pending'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks)`
|
|
1048
|
-
}));
|
|
1049
|
-
}
|
|
1050
|
-
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
1051
|
-
return intents.map((intent) => {
|
|
1052
|
-
const workItems = Array.isArray(intent?.workItems) ? intent.workItems : [];
|
|
1053
|
-
const done = workItems.filter((item) => item.status === 'completed').length;
|
|
1054
|
-
return {
|
|
1055
|
-
id: intent?.id || 'unknown',
|
|
1056
|
-
status: intent?.status || 'pending',
|
|
1057
|
-
line: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${done}/${workItems.length} work items)`
|
|
1058
|
-
};
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function buildOverviewIntentLines(snapshot, width, flow, filter = 'next') {
|
|
1063
|
-
const entries = listOverviewIntentEntries(snapshot, flow);
|
|
1064
|
-
const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
|
|
1065
|
-
const isNextFilter = normalizedFilter === 'next';
|
|
1066
|
-
const nextLabel = isNextFilter ? '[NEXT]' : ' next ';
|
|
1067
|
-
const completedLabel = !isNextFilter ? '[COMPLETED]' : ' completed ';
|
|
1068
|
-
|
|
1069
|
-
const filtered = entries.filter((entry) => {
|
|
1070
|
-
if (normalizedFilter === 'completed') {
|
|
1071
|
-
return entry.status === 'completed';
|
|
1072
|
-
}
|
|
1073
|
-
return entry.status !== 'completed';
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
const lines = [{
|
|
1077
|
-
text: truncate(`filter ${nextLabel} | ${completedLabel} (←/→ or n/x)`, width),
|
|
1078
|
-
color: 'cyan',
|
|
1079
|
-
bold: true
|
|
1080
|
-
}];
|
|
1081
|
-
|
|
1082
|
-
if (filtered.length === 0) {
|
|
1083
|
-
lines.push({
|
|
1084
|
-
text: truncate(
|
|
1085
|
-
normalizedFilter === 'completed' ? 'No completed intents yet' : 'No upcoming intents',
|
|
1086
|
-
width
|
|
1087
|
-
),
|
|
1088
|
-
color: 'gray',
|
|
1089
|
-
bold: false
|
|
1090
|
-
});
|
|
1091
|
-
return lines;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
lines.push(...filtered.map((entry) => truncate(entry.line, width)));
|
|
1095
|
-
return lines;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function buildOverviewStandardsLines(snapshot, width, flow) {
|
|
1099
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1100
|
-
if (effectiveFlow === 'aidlc') {
|
|
1101
|
-
return buildAidlcOverviewStandardsLines(snapshot, width);
|
|
1102
|
-
}
|
|
1103
|
-
if (effectiveFlow === 'simple') {
|
|
1104
|
-
return buildSimpleOverviewStandardsLines(snapshot, width);
|
|
1105
|
-
}
|
|
1106
|
-
return buildFireOverviewStandardsLines(snapshot, width);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function getPanelTitles(flow, snapshot) {
|
|
1110
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1111
|
-
if (effectiveFlow === 'aidlc') {
|
|
1112
|
-
return {
|
|
1113
|
-
current: 'Current Bolt',
|
|
1114
|
-
files: 'Bolt Files',
|
|
1115
|
-
pending: 'Queued Bolts',
|
|
1116
|
-
completed: 'Recent Completed Bolts',
|
|
1117
|
-
otherWorktrees: 'Other Worktrees: Active Bolts'
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
if (effectiveFlow === 'simple') {
|
|
1121
|
-
return {
|
|
1122
|
-
current: 'Current Spec',
|
|
1123
|
-
files: 'Spec Files',
|
|
1124
|
-
pending: 'Pending Specs',
|
|
1125
|
-
completed: 'Recent Completed Specs',
|
|
1126
|
-
otherWorktrees: 'Other Worktrees: Active Specs'
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
return {
|
|
1130
|
-
current: 'Current Run',
|
|
1131
|
-
files: 'Run Files',
|
|
1132
|
-
pending: 'Pending Queue',
|
|
1133
|
-
completed: 'Recent Completed Runs',
|
|
1134
|
-
otherWorktrees: 'Other Worktrees: Active Runs',
|
|
1135
|
-
git: 'Git Changes'
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function getGitChangesSnapshot(snapshot) {
|
|
1140
|
-
const gitChanges = snapshot?.gitChanges;
|
|
1141
|
-
if (!gitChanges || typeof gitChanges !== 'object') {
|
|
1142
|
-
return {
|
|
1143
|
-
available: false,
|
|
1144
|
-
branch: '(unavailable)',
|
|
1145
|
-
upstream: null,
|
|
1146
|
-
ahead: 0,
|
|
1147
|
-
behind: 0,
|
|
1148
|
-
counts: {
|
|
1149
|
-
total: 0,
|
|
1150
|
-
staged: 0,
|
|
1151
|
-
unstaged: 0,
|
|
1152
|
-
untracked: 0,
|
|
1153
|
-
conflicted: 0
|
|
1154
|
-
},
|
|
1155
|
-
staged: [],
|
|
1156
|
-
unstaged: [],
|
|
1157
|
-
untracked: [],
|
|
1158
|
-
conflicted: [],
|
|
1159
|
-
clean: true
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
return {
|
|
1163
|
-
...gitChanges,
|
|
1164
|
-
counts: gitChanges.counts || {
|
|
1165
|
-
total: 0,
|
|
1166
|
-
staged: 0,
|
|
1167
|
-
unstaged: 0,
|
|
1168
|
-
untracked: 0,
|
|
1169
|
-
conflicted: 0
|
|
1170
|
-
},
|
|
1171
|
-
staged: Array.isArray(gitChanges.staged) ? gitChanges.staged : [],
|
|
1172
|
-
unstaged: Array.isArray(gitChanges.unstaged) ? gitChanges.unstaged : [],
|
|
1173
|
-
untracked: Array.isArray(gitChanges.untracked) ? gitChanges.untracked : [],
|
|
1174
|
-
conflicted: Array.isArray(gitChanges.conflicted) ? gitChanges.conflicted : []
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function readGitCommandLines(repoRoot, args, options = {}) {
|
|
1179
|
-
if (typeof repoRoot !== 'string' || repoRoot.trim() === '' || !Array.isArray(args) || args.length === 0) {
|
|
1180
|
-
return [];
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
const acceptedStatuses = Array.isArray(options.acceptedStatuses) && options.acceptedStatuses.length > 0
|
|
1184
|
-
? options.acceptedStatuses
|
|
1185
|
-
: [0];
|
|
1186
|
-
|
|
1187
|
-
const result = spawnSync('git', args, {
|
|
1188
|
-
cwd: repoRoot,
|
|
1189
|
-
encoding: 'utf8',
|
|
1190
|
-
maxBuffer: 8 * 1024 * 1024
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
if (result.error) {
|
|
1194
|
-
return [];
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
if (typeof result.status === 'number' && !acceptedStatuses.includes(result.status)) {
|
|
1198
|
-
return [];
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const lines = String(result.stdout || '')
|
|
1202
|
-
.split(/\r?\n/)
|
|
1203
|
-
.map((line) => line.trim())
|
|
1204
|
-
.filter(Boolean);
|
|
1205
|
-
|
|
1206
|
-
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : null;
|
|
1207
|
-
if (limit == null || lines.length <= limit) {
|
|
1208
|
-
return lines;
|
|
1209
|
-
}
|
|
1210
|
-
return lines.slice(0, limit);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function buildGitStatusPanelLines(snapshot) {
|
|
1214
|
-
const git = getGitChangesSnapshot(snapshot);
|
|
1215
|
-
if (!git.available) {
|
|
1216
|
-
return [{
|
|
1217
|
-
text: 'Repository unavailable in selected worktree',
|
|
1218
|
-
color: 'red',
|
|
1219
|
-
bold: true
|
|
1220
|
-
}];
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
const tracking = git.upstream
|
|
1224
|
-
? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
|
|
1225
|
-
: 'no upstream';
|
|
1226
|
-
|
|
1227
|
-
return [
|
|
1228
|
-
{
|
|
1229
|
-
text: `branch: ${git.branch}${git.detached ? ' [detached]' : ''}`,
|
|
1230
|
-
color: 'green',
|
|
1231
|
-
bold: true
|
|
1232
|
-
},
|
|
1233
|
-
{
|
|
1234
|
-
text: `tracking: ${tracking}`,
|
|
1235
|
-
color: 'gray',
|
|
1236
|
-
bold: false
|
|
1237
|
-
},
|
|
1238
|
-
{
|
|
1239
|
-
text: `changes: ${git.counts.total || 0} total`,
|
|
1240
|
-
color: 'gray',
|
|
1241
|
-
bold: false
|
|
1242
|
-
},
|
|
1243
|
-
{
|
|
1244
|
-
text: `staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0}`,
|
|
1245
|
-
color: 'yellow',
|
|
1246
|
-
bold: false
|
|
1247
|
-
},
|
|
1248
|
-
{
|
|
1249
|
-
text: `untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
|
|
1250
|
-
color: 'yellow',
|
|
1251
|
-
bold: false
|
|
1252
|
-
}
|
|
1253
|
-
];
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
function buildGitCommitRows(snapshot) {
|
|
1257
|
-
const git = getGitChangesSnapshot(snapshot);
|
|
1258
|
-
if (!git.available) {
|
|
1259
|
-
return [{
|
|
1260
|
-
kind: 'info',
|
|
1261
|
-
key: 'git:commits:unavailable',
|
|
1262
|
-
label: 'No commit history (git unavailable)',
|
|
1263
|
-
selectable: false
|
|
1264
|
-
}];
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
const commitLines = readGitCommandLines(git.rootPath, [
|
|
1268
|
-
'-c',
|
|
1269
|
-
'color.ui=false',
|
|
1270
|
-
'log',
|
|
1271
|
-
'--date=relative',
|
|
1272
|
-
'--pretty=format:%h %s',
|
|
1273
|
-
'--max-count=30'
|
|
1274
|
-
], { limit: 30 });
|
|
1275
|
-
|
|
1276
|
-
if (commitLines.length === 0) {
|
|
1277
|
-
return [{
|
|
1278
|
-
kind: 'info',
|
|
1279
|
-
key: 'git:commits:empty',
|
|
1280
|
-
label: 'No commits found',
|
|
1281
|
-
selectable: false
|
|
1282
|
-
}];
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
return commitLines.map((line, index) => {
|
|
1286
|
-
const firstSpace = line.indexOf(' ');
|
|
1287
|
-
const commitHash = firstSpace > 0 ? line.slice(0, firstSpace) : '';
|
|
1288
|
-
const message = firstSpace > 0 ? line.slice(firstSpace + 1) : line;
|
|
1289
|
-
const label = commitHash ? `${commitHash} ${message}` : message;
|
|
1290
|
-
|
|
1291
|
-
return {
|
|
1292
|
-
kind: 'git-commit',
|
|
1293
|
-
key: `git:commit:${commitHash || index}:${index}`,
|
|
1294
|
-
label,
|
|
1295
|
-
commitHash,
|
|
1296
|
-
repoRoot: git.rootPath,
|
|
1297
|
-
previewType: 'git-commit-diff',
|
|
1298
|
-
selectable: true
|
|
1299
|
-
};
|
|
1300
|
-
});
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function getDashboardWorktreeMeta(snapshot) {
|
|
1304
|
-
if (!snapshot || typeof snapshot !== 'object') {
|
|
1305
|
-
return null;
|
|
1306
|
-
}
|
|
1307
|
-
const meta = snapshot.dashboardWorktrees;
|
|
1308
|
-
if (!meta || typeof meta !== 'object') {
|
|
1309
|
-
return null;
|
|
1310
|
-
}
|
|
1311
|
-
const items = Array.isArray(meta.items) ? meta.items : [];
|
|
1312
|
-
if (items.length === 0) {
|
|
1313
|
-
return null;
|
|
1314
|
-
}
|
|
1315
|
-
return {
|
|
1316
|
-
...meta,
|
|
1317
|
-
items
|
|
1318
|
-
};
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
function getWorktreeItems(snapshot) {
|
|
1322
|
-
return getDashboardWorktreeMeta(snapshot)?.items || [];
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
function getSelectedWorktree(snapshot) {
|
|
1326
|
-
const meta = getDashboardWorktreeMeta(snapshot);
|
|
1327
|
-
if (!meta) {
|
|
1328
|
-
return null;
|
|
1329
|
-
}
|
|
1330
|
-
return meta.items.find((item) => item.id === meta.selectedWorktreeId) || null;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function hasMultipleWorktrees(snapshot) {
|
|
1334
|
-
return getWorktreeItems(snapshot).length > 1;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
function isSelectedWorktreeMain(snapshot) {
|
|
1338
|
-
const selected = getSelectedWorktree(snapshot);
|
|
1339
|
-
return Boolean(selected?.isMainBranch);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
function getWorktreeDisplayName(worktree) {
|
|
1343
|
-
if (!worktree || typeof worktree !== 'object') {
|
|
1344
|
-
return 'unknown';
|
|
1345
|
-
}
|
|
1346
|
-
if (typeof worktree.displayBranch === 'string' && worktree.displayBranch.trim() !== '') {
|
|
1347
|
-
return worktree.displayBranch;
|
|
1348
|
-
}
|
|
1349
|
-
if (typeof worktree.branch === 'string' && worktree.branch.trim() !== '') {
|
|
1350
|
-
return worktree.branch;
|
|
1351
|
-
}
|
|
1352
|
-
if (typeof worktree.name === 'string' && worktree.name.trim() !== '') {
|
|
1353
|
-
return worktree.name;
|
|
1354
|
-
}
|
|
1355
|
-
return path.basename(worktree.path || '') || 'unknown';
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
function buildWorktreeRows(snapshot, flow) {
|
|
1359
|
-
const meta = getDashboardWorktreeMeta(snapshot);
|
|
1360
|
-
if (!meta) {
|
|
1361
|
-
return [{
|
|
1362
|
-
kind: 'info',
|
|
1363
|
-
key: 'worktrees:none',
|
|
1364
|
-
label: 'No git worktrees detected',
|
|
1365
|
-
selectable: false
|
|
1366
|
-
}];
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1370
|
-
const entityLabel = effectiveFlow === 'aidlc'
|
|
1371
|
-
? 'active bolts'
|
|
1372
|
-
: (effectiveFlow === 'simple' ? 'active specs' : 'active runs');
|
|
1373
|
-
|
|
1374
|
-
const rows = [];
|
|
1375
|
-
for (const item of meta.items) {
|
|
1376
|
-
const currentLabel = item.isSelected ? '[CURRENT] ' : '';
|
|
1377
|
-
const mainLabel = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
|
|
1378
|
-
const availabilityLabel = item.flowAvailable ? '' : ' (flow unavailable)';
|
|
1379
|
-
const statusLabel = item.status === 'loading'
|
|
1380
|
-
? ' loading...'
|
|
1381
|
-
: (item.status === 'error' ? ' error' : ` ${item.activeCount || 0} ${entityLabel}`);
|
|
1382
|
-
const scopeLabel = item.name ? ` (${item.name})` : '';
|
|
1383
|
-
|
|
1384
|
-
rows.push({
|
|
1385
|
-
kind: 'info',
|
|
1386
|
-
key: `worktree:item:${item.id}`,
|
|
1387
|
-
label: `${currentLabel}${mainLabel}${getWorktreeDisplayName(item)}${scopeLabel}${availabilityLabel}${statusLabel}`,
|
|
1388
|
-
color: item.isSelected ? 'green' : (item.flowAvailable ? 'gray' : 'red'),
|
|
1389
|
-
bold: item.isSelected,
|
|
1390
|
-
selectable: true
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
return rows;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function buildOtherWorktreeActiveGroups(snapshot, flow) {
|
|
1398
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1399
|
-
if (effectiveFlow === 'simple') {
|
|
1400
|
-
return [];
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
const meta = getDashboardWorktreeMeta(snapshot);
|
|
1404
|
-
if (!meta) {
|
|
1405
|
-
return [];
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
const selectedWorktree = getSelectedWorktree(snapshot);
|
|
1409
|
-
if (!selectedWorktree || !selectedWorktree.isMainBranch) {
|
|
1410
|
-
return [];
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
const groups = [];
|
|
1414
|
-
const otherItems = meta.items.filter((item) => item.id !== meta.selectedWorktreeId);
|
|
1415
|
-
for (const item of otherItems) {
|
|
1416
|
-
if (!item.flowAvailable || item.status === 'unavailable' || item.status === 'error') {
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
if (effectiveFlow === 'aidlc') {
|
|
1421
|
-
const activeBolts = Array.isArray(item.activity?.activeBolts) ? item.activity.activeBolts : [];
|
|
1422
|
-
for (const bolt of activeBolts) {
|
|
1423
|
-
const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
|
|
1424
|
-
const completedStages = stages.filter((stage) => stage?.status === 'completed').length;
|
|
1425
|
-
groups.push({
|
|
1426
|
-
key: `other:wt:${item.id}:bolt:${bolt.id}`,
|
|
1427
|
-
label: `[WT ${getWorktreeDisplayName(item)}] ${bolt.id} [${bolt.type || 'bolt'}] ${completedStages}/${stages.length || 0} stages`,
|
|
1428
|
-
files: collectAidlcBoltFiles(bolt).map((file) => ({ ...file, scope: 'active' }))
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
const activeRuns = Array.isArray(item.activity?.activeRuns) ? item.activity.activeRuns : [];
|
|
1435
|
-
for (const run of activeRuns) {
|
|
1436
|
-
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
1437
|
-
const completed = workItems.filter((workItem) => workItem?.status === 'completed').length;
|
|
1438
|
-
groups.push({
|
|
1439
|
-
key: `other:wt:${item.id}:run:${run.id}`,
|
|
1440
|
-
label: `[WT ${getWorktreeDisplayName(item)}] ${run.id} [${run.scope || 'single'}] ${completed}/${workItems.length} items`,
|
|
1441
|
-
files: collectFireRunFiles(run).map((file) => ({ ...file, scope: 'active' }))
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
return groups;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function getOtherWorktreeEmptyMessage(snapshot, flow) {
|
|
1450
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1451
|
-
if (!hasMultipleWorktrees(snapshot)) {
|
|
1452
|
-
return 'No additional worktrees';
|
|
1453
|
-
}
|
|
1454
|
-
if (!isSelectedWorktreeMain(snapshot)) {
|
|
1455
|
-
return 'Switch to main worktree to view active items from other worktrees';
|
|
1456
|
-
}
|
|
1457
|
-
if (effectiveFlow === 'aidlc') {
|
|
1458
|
-
return 'No active bolts in other worktrees';
|
|
1459
|
-
}
|
|
1460
|
-
if (effectiveFlow === 'simple') {
|
|
1461
|
-
return 'No active specs in other worktrees';
|
|
1462
|
-
}
|
|
1463
|
-
return 'No active runs in other worktrees';
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function getSectionOrderForView(view, options = {}) {
|
|
1467
|
-
const includeWorktrees = options.includeWorktrees === true;
|
|
1468
|
-
const includeOtherWorktrees = options.includeOtherWorktrees === true;
|
|
1469
|
-
|
|
1470
|
-
if (view === 'intents') {
|
|
1471
|
-
return ['intent-status'];
|
|
1472
|
-
}
|
|
1473
|
-
if (view === 'completed') {
|
|
1474
|
-
return ['completed-runs'];
|
|
1475
|
-
}
|
|
1476
|
-
if (view === 'health') {
|
|
1477
|
-
return ['standards', 'stats', 'warnings', 'error-details'];
|
|
1478
|
-
}
|
|
1479
|
-
if (view === 'git') {
|
|
1480
|
-
return ['git-status', 'git-changes', 'git-commits', 'git-diff'];
|
|
1481
|
-
}
|
|
1482
|
-
const sections = [];
|
|
1483
|
-
if (includeWorktrees) {
|
|
1484
|
-
sections.push('worktrees');
|
|
1485
|
-
}
|
|
1486
|
-
sections.push('current-run', 'run-files');
|
|
1487
|
-
if (includeOtherWorktrees) {
|
|
1488
|
-
sections.push('other-worktrees-active');
|
|
1489
|
-
}
|
|
1490
|
-
return sections;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
|
|
1494
|
-
const order = Array.isArray(availableSections) && availableSections.length > 0
|
|
1495
|
-
? availableSections
|
|
1496
|
-
: getSectionOrderForView(view);
|
|
1497
|
-
if (order.length === 0) {
|
|
1498
|
-
return currentSectionKey;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
const currentIndex = order.indexOf(currentSectionKey);
|
|
1502
|
-
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
1503
|
-
const nextIndex = (safeIndex + direction + order.length) % order.length;
|
|
1504
|
-
return order[nextIndex];
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function fileExists(filePath) {
|
|
1508
|
-
try {
|
|
1509
|
-
return fs.statSync(filePath).isFile();
|
|
1510
|
-
} catch {
|
|
1511
|
-
return false;
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function listMarkdownFiles(dirPath) {
|
|
1516
|
-
try {
|
|
1517
|
-
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
1518
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
1519
|
-
.map((entry) => entry.name)
|
|
1520
|
-
.sort((a, b) => a.localeCompare(b));
|
|
1521
|
-
} catch {
|
|
1522
|
-
return [];
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
function pushFileEntry(entries, seenPaths, candidate) {
|
|
1527
|
-
if (!candidate || typeof candidate.path !== 'string' || typeof candidate.label !== 'string') {
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
if (!fileExists(candidate.path)) {
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
if (seenPaths.has(candidate.path)) {
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
seenPaths.add(candidate.path);
|
|
1540
|
-
entries.push({
|
|
1541
|
-
path: candidate.path,
|
|
1542
|
-
label: candidate.label,
|
|
1543
|
-
scope: candidate.scope || 'other'
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function buildIntentScopedLabel(snapshot, intentId, filePath, fallbackName = 'file.md') {
|
|
1548
|
-
const safeIntentId = typeof intentId === 'string' && intentId.trim() !== ''
|
|
1549
|
-
? intentId
|
|
1550
|
-
: '';
|
|
1551
|
-
const safeFallback = typeof fallbackName === 'string' && fallbackName.trim() !== ''
|
|
1552
|
-
? fallbackName
|
|
1553
|
-
: 'file.md';
|
|
1554
|
-
|
|
1555
|
-
if (typeof filePath === 'string' && filePath.trim() !== '') {
|
|
1556
|
-
if (safeIntentId && typeof snapshot?.rootPath === 'string' && snapshot.rootPath.trim() !== '') {
|
|
1557
|
-
const intentPath = path.join(snapshot.rootPath, 'intents', safeIntentId);
|
|
1558
|
-
const relativePath = path.relative(intentPath, filePath);
|
|
1559
|
-
if (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
1560
|
-
return `${safeIntentId}/${relativePath.split(path.sep).join('/')}`;
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
const basename = path.basename(filePath);
|
|
1565
|
-
return safeIntentId ? `${safeIntentId}/${basename}` : basename;
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
return safeIntentId ? `${safeIntentId}/${safeFallback}` : safeFallback;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
function findIntentIdForWorkItem(snapshot, workItemId) {
|
|
1572
|
-
if (typeof workItemId !== 'string' || workItemId.trim() === '') {
|
|
1573
|
-
return '';
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
1577
|
-
for (const intent of intents) {
|
|
1578
|
-
const items = Array.isArray(intent?.workItems) ? intent.workItems : [];
|
|
1579
|
-
if (items.some((item) => item?.id === workItemId)) {
|
|
1580
|
-
return intent?.id || '';
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
return '';
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function resolveFireWorkItemPath(snapshot, intentId, workItemId, explicitPath) {
|
|
1588
|
-
if (typeof explicitPath === 'string' && explicitPath.trim() !== '') {
|
|
1589
|
-
return explicitPath;
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
if (typeof snapshot?.rootPath !== 'string' || snapshot.rootPath.trim() === '') {
|
|
1593
|
-
return null;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
if (typeof workItemId !== 'string' || workItemId.trim() === '') {
|
|
1597
|
-
return null;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
const safeIntentId = typeof intentId === 'string' && intentId.trim() !== ''
|
|
1601
|
-
? intentId
|
|
1602
|
-
: findIntentIdForWorkItem(snapshot, workItemId);
|
|
1603
|
-
|
|
1604
|
-
if (!safeIntentId) {
|
|
1605
|
-
return null;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
return path.join(snapshot.rootPath, 'intents', safeIntentId, 'work-items', `${workItemId}.md`);
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
function collectFireRunFiles(run) {
|
|
1612
|
-
if (!run || typeof run.folderPath !== 'string') {
|
|
1613
|
-
return [];
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
const names = ['run.md'];
|
|
1617
|
-
if (run.hasPlan) names.push('plan.md');
|
|
1618
|
-
if (run.hasTestReport) names.push('test-report.md');
|
|
1619
|
-
if (run.hasWalkthrough) names.push('walkthrough.md');
|
|
1620
|
-
|
|
1621
|
-
return names.map((fileName) => ({
|
|
1622
|
-
label: `${run.id}/${fileName}`,
|
|
1623
|
-
path: path.join(run.folderPath, fileName)
|
|
1624
|
-
}));
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
function collectAidlcBoltFiles(bolt) {
|
|
1628
|
-
if (!bolt || typeof bolt.path !== 'string') {
|
|
1629
|
-
return [];
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
const fileNames = Array.isArray(bolt.files) && bolt.files.length > 0
|
|
1633
|
-
? bolt.files
|
|
1634
|
-
: listMarkdownFiles(bolt.path);
|
|
1635
|
-
|
|
1636
|
-
return fileNames.map((fileName) => ({
|
|
1637
|
-
label: `${bolt.id}/${fileName}`,
|
|
1638
|
-
path: path.join(bolt.path, fileName)
|
|
1639
|
-
}));
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
function collectSimpleSpecFiles(spec) {
|
|
1643
|
-
if (!spec || typeof spec.path !== 'string') {
|
|
1644
|
-
return [];
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
const names = [];
|
|
1648
|
-
if (spec.hasRequirements) names.push('requirements.md');
|
|
1649
|
-
if (spec.hasDesign) names.push('design.md');
|
|
1650
|
-
if (spec.hasTasks) names.push('tasks.md');
|
|
1651
|
-
|
|
1652
|
-
return names.map((fileName) => ({
|
|
1653
|
-
label: `${spec.name}/${fileName}`,
|
|
1654
|
-
path: path.join(spec.path, fileName)
|
|
1655
|
-
}));
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
function getRunFileEntries(snapshot, flow, options = {}) {
|
|
1659
|
-
const includeBacklog = options.includeBacklog !== false;
|
|
1660
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1661
|
-
const entries = [];
|
|
1662
|
-
const seenPaths = new Set();
|
|
1663
|
-
|
|
1664
|
-
if (effectiveFlow === 'aidlc') {
|
|
1665
|
-
const bolt = getCurrentBolt(snapshot);
|
|
1666
|
-
for (const file of collectAidlcBoltFiles(bolt)) {
|
|
1667
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
if (!includeBacklog) {
|
|
1671
|
-
return entries;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
1675
|
-
for (const pendingBolt of pendingBolts) {
|
|
1676
|
-
for (const file of collectAidlcBoltFiles(pendingBolt)) {
|
|
1677
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
1682
|
-
for (const completedBolt of completedBolts) {
|
|
1683
|
-
for (const file of collectAidlcBoltFiles(completedBolt)) {
|
|
1684
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
const intentIds = new Set([
|
|
1689
|
-
...pendingBolts.map((item) => item?.intent).filter(Boolean),
|
|
1690
|
-
...completedBolts.map((item) => item?.intent).filter(Boolean)
|
|
1691
|
-
]);
|
|
1692
|
-
|
|
1693
|
-
for (const intentId of intentIds) {
|
|
1694
|
-
const intentPath = path.join(snapshot?.rootPath || '', 'intents', intentId);
|
|
1695
|
-
pushFileEntry(entries, seenPaths, {
|
|
1696
|
-
label: `${intentId}/requirements.md`,
|
|
1697
|
-
path: path.join(intentPath, 'requirements.md'),
|
|
1698
|
-
scope: 'intent'
|
|
1699
|
-
});
|
|
1700
|
-
pushFileEntry(entries, seenPaths, {
|
|
1701
|
-
label: `${intentId}/system-context.md`,
|
|
1702
|
-
path: path.join(intentPath, 'system-context.md'),
|
|
1703
|
-
scope: 'intent'
|
|
1704
|
-
});
|
|
1705
|
-
pushFileEntry(entries, seenPaths, {
|
|
1706
|
-
label: `${intentId}/units.md`,
|
|
1707
|
-
path: path.join(intentPath, 'units.md'),
|
|
1708
|
-
scope: 'intent'
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
return entries;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
if (effectiveFlow === 'simple') {
|
|
1715
|
-
const spec = getCurrentSpec(snapshot);
|
|
1716
|
-
for (const file of collectSimpleSpecFiles(spec)) {
|
|
1717
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
if (!includeBacklog) {
|
|
1721
|
-
return entries;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
1725
|
-
for (const pendingSpec of pendingSpecs) {
|
|
1726
|
-
for (const file of collectSimpleSpecFiles(pendingSpec)) {
|
|
1727
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
1732
|
-
for (const completedSpec of completedSpecs) {
|
|
1733
|
-
for (const file of collectSimpleSpecFiles(completedSpec)) {
|
|
1734
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
return entries;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
const run = getCurrentRun(snapshot);
|
|
1742
|
-
for (const file of collectFireRunFiles(run)) {
|
|
1743
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
if (!includeBacklog) {
|
|
1747
|
-
return entries;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
1751
|
-
for (const pendingItem of pendingItems) {
|
|
1752
|
-
pushFileEntry(entries, seenPaths, {
|
|
1753
|
-
label: buildIntentScopedLabel(
|
|
1754
|
-
snapshot,
|
|
1755
|
-
pendingItem?.intentId,
|
|
1756
|
-
pendingItem?.filePath,
|
|
1757
|
-
`${pendingItem?.id || 'work-item'}.md`
|
|
1758
|
-
),
|
|
1759
|
-
path: pendingItem?.filePath,
|
|
1760
|
-
scope: 'upcoming'
|
|
1761
|
-
});
|
|
1762
|
-
|
|
1763
|
-
if (pendingItem?.intentId) {
|
|
1764
|
-
pushFileEntry(entries, seenPaths, {
|
|
1765
|
-
label: buildIntentScopedLabel(
|
|
1766
|
-
snapshot,
|
|
1767
|
-
pendingItem.intentId,
|
|
1768
|
-
path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
|
|
1769
|
-
'brief.md'
|
|
1770
|
-
),
|
|
1771
|
-
path: path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
|
|
1772
|
-
scope: 'intent'
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
1778
|
-
for (const completedRun of completedRuns) {
|
|
1779
|
-
for (const file of collectFireRunFiles(completedRun)) {
|
|
1780
|
-
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
const completedIntents = Array.isArray(snapshot?.intents)
|
|
1785
|
-
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
1786
|
-
: [];
|
|
1787
|
-
for (const intent of completedIntents) {
|
|
1788
|
-
pushFileEntry(entries, seenPaths, {
|
|
1789
|
-
label: buildIntentScopedLabel(
|
|
1790
|
-
snapshot,
|
|
1791
|
-
intent?.id,
|
|
1792
|
-
path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
1793
|
-
'brief.md'
|
|
1794
|
-
),
|
|
1795
|
-
path: path.join(snapshot?.rootPath || '', 'intents', intent.id, 'brief.md'),
|
|
1796
|
-
scope: 'intent'
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
return entries;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
function clampIndex(value, length) {
|
|
1804
|
-
if (!Number.isFinite(value)) {
|
|
1805
|
-
return 0;
|
|
1806
|
-
}
|
|
1807
|
-
if (!Number.isFinite(length) || length <= 0) {
|
|
1808
|
-
return 0;
|
|
1809
|
-
}
|
|
1810
|
-
return Math.max(0, Math.min(length - 1, Math.floor(value)));
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
function getNoFileMessage(flow) {
|
|
1814
|
-
return `No selectable files for ${String(flow || 'flow').toUpperCase()}`;
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
function formatScope(scope) {
|
|
1818
|
-
if (scope === 'active') return 'ACTIVE';
|
|
1819
|
-
if (scope === 'upcoming') return 'UPNEXT';
|
|
1820
|
-
if (scope === 'completed') return 'DONE';
|
|
1821
|
-
if (scope === 'intent') return 'INTENT';
|
|
1822
|
-
if (scope === 'staged') return 'STAGED';
|
|
1823
|
-
if (scope === 'unstaged') return 'UNSTAGED';
|
|
1824
|
-
if (scope === 'untracked') return 'UNTRACKED';
|
|
1825
|
-
if (scope === 'conflicted') return 'CONFLICT';
|
|
1826
|
-
return 'FILE';
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
function getNoPendingMessage(flow) {
|
|
1830
|
-
if (flow === 'aidlc') return 'No queued bolts';
|
|
1831
|
-
if (flow === 'simple') return 'No pending specs';
|
|
1832
|
-
return 'No pending work items';
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
function getNoCompletedMessage(flow) {
|
|
1836
|
-
if (flow === 'aidlc') return 'No completed bolts yet';
|
|
1837
|
-
if (flow === 'simple') return 'No completed specs yet';
|
|
1838
|
-
return 'No completed runs yet';
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
function getNoCurrentMessage(flow) {
|
|
1842
|
-
if (flow === 'aidlc') return 'No active bolt';
|
|
1843
|
-
if (flow === 'simple') return 'No active spec';
|
|
1844
|
-
return 'No active run';
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
function buildFireCurrentRunGroups(snapshot) {
|
|
1848
|
-
const run = getCurrentRun(snapshot);
|
|
1849
|
-
if (!run) {
|
|
1850
|
-
return [];
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
1854
|
-
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
1855
|
-
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
1856
|
-
const awaitingApproval = isFireRunAwaitingApproval(run, currentWorkItem);
|
|
1857
|
-
|
|
1858
|
-
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
1859
|
-
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
1860
|
-
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
1861
|
-
const status = currentWorkItem?.status || 'pending';
|
|
1862
|
-
const statusTag = status === 'in_progress' ? 'current' : status;
|
|
1863
|
-
|
|
1864
|
-
const runIntentId = typeof run?.intent === 'string' ? run.intent : '';
|
|
1865
|
-
const currentWorkItemFiles = workItems.map((item, index) => {
|
|
1866
|
-
const itemId = typeof item?.id === 'string' && item.id !== '' ? item.id : `work-item-${index + 1}`;
|
|
1867
|
-
const intentId = typeof item?.intent === 'string' && item.intent !== ''
|
|
1868
|
-
? item.intent
|
|
1869
|
-
: (runIntentId || findIntentIdForWorkItem(snapshot, itemId));
|
|
1870
|
-
const filePath = resolveFireWorkItemPath(snapshot, intentId, itemId, item?.filePath);
|
|
1871
|
-
if (!filePath) {
|
|
1872
|
-
return null;
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
const itemMode = String(item?.mode || 'confirm').toUpperCase();
|
|
1876
|
-
const itemStatus = item?.status || 'pending';
|
|
1877
|
-
const isCurrent = Boolean(currentWorkItem?.id) && itemId === currentWorkItem.id;
|
|
1878
|
-
const itemScope = isCurrent
|
|
1879
|
-
? 'active'
|
|
1880
|
-
: (itemStatus === 'completed' ? 'completed' : 'upcoming');
|
|
1881
|
-
const itemStatusTag = isCurrent ? 'current' : itemStatus;
|
|
1882
|
-
const labelPath = buildIntentScopedLabel(snapshot, intentId, filePath, `${itemId}.md`);
|
|
1883
|
-
|
|
1884
|
-
return {
|
|
1885
|
-
label: `${labelPath} [${itemMode}] [${itemStatusTag}]`,
|
|
1886
|
-
path: filePath,
|
|
1887
|
-
scope: itemScope
|
|
1888
|
-
};
|
|
1889
|
-
}).filter(Boolean);
|
|
1890
|
-
|
|
1891
|
-
const currentRunFiles = collectFireRunFiles(run).map((fileEntry) => ({
|
|
1892
|
-
...fileEntry,
|
|
1893
|
-
label: path.basename(fileEntry.path || fileEntry.label || ''),
|
|
1894
|
-
scope: 'active'
|
|
1895
|
-
}));
|
|
1896
|
-
|
|
1897
|
-
return [
|
|
1898
|
-
{
|
|
1899
|
-
key: `current:run:${run.id}:summary`,
|
|
1900
|
-
label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
1901
|
-
files: []
|
|
1902
|
-
},
|
|
1903
|
-
{
|
|
1904
|
-
key: `current:run:${run.id}:work-items`,
|
|
1905
|
-
label: `WORK ITEMS (${currentWorkItemFiles.length})`,
|
|
1906
|
-
files: filterExistingFiles(currentWorkItemFiles)
|
|
1907
|
-
},
|
|
1908
|
-
{
|
|
1909
|
-
key: `current:run:${run.id}:run-files`,
|
|
1910
|
-
label: 'RUN FILES',
|
|
1911
|
-
files: filterExistingFiles(currentRunFiles)
|
|
1912
|
-
}
|
|
1913
|
-
];
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
function buildCurrentGroups(snapshot, flow) {
|
|
1917
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
1918
|
-
|
|
1919
|
-
if (effectiveFlow === 'aidlc') {
|
|
1920
|
-
const bolt = getCurrentBolt(snapshot);
|
|
1921
|
-
if (!bolt) {
|
|
1922
|
-
return [];
|
|
1923
|
-
}
|
|
1924
|
-
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
1925
|
-
const completedStages = stages.filter((stage) => stage.status === 'completed').length;
|
|
1926
|
-
const awaitingApproval = isAidlcBoltAwaitingApproval(bolt);
|
|
1927
|
-
return [{
|
|
1928
|
-
key: `current:bolt:${bolt.id}`,
|
|
1929
|
-
label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
1930
|
-
files: filterExistingFiles([
|
|
1931
|
-
...collectAidlcBoltFiles(bolt),
|
|
1932
|
-
...collectAidlcIntentContextFiles(snapshot, bolt.intent)
|
|
1933
|
-
])
|
|
1934
|
-
}];
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
if (effectiveFlow === 'simple') {
|
|
1938
|
-
const spec = getCurrentSpec(snapshot);
|
|
1939
|
-
if (!spec) {
|
|
1940
|
-
return [];
|
|
1941
|
-
}
|
|
1942
|
-
return [{
|
|
1943
|
-
key: `current:spec:${spec.name}`,
|
|
1944
|
-
label: `${spec.name} [${spec.state}] ${spec.tasksCompleted}/${spec.tasksTotal} tasks`,
|
|
1945
|
-
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
1946
|
-
}];
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
return buildFireCurrentRunGroups(snapshot);
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
function buildRunFileGroups(fileEntries) {
|
|
1953
|
-
const order = ['active', 'upcoming', 'completed', 'intent', 'other'];
|
|
1954
|
-
const buckets = new Map(order.map((scope) => [scope, []]));
|
|
1955
|
-
|
|
1956
|
-
for (const fileEntry of Array.isArray(fileEntries) ? fileEntries : []) {
|
|
1957
|
-
const scope = order.includes(fileEntry?.scope) ? fileEntry.scope : 'other';
|
|
1958
|
-
buckets.get(scope).push(fileEntry);
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
const groups = [];
|
|
1962
|
-
for (const scope of order) {
|
|
1963
|
-
const files = buckets.get(scope) || [];
|
|
1964
|
-
if (files.length === 0) {
|
|
1965
|
-
continue;
|
|
1966
|
-
}
|
|
1967
|
-
groups.push({
|
|
1968
|
-
key: `run-files:scope:${scope}`,
|
|
1969
|
-
label: `${formatScope(scope)} files (${files.length})`,
|
|
1970
|
-
files: filterExistingFiles(files)
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
return groups;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
function getFileEntityLabel(fileEntry, fallbackIndex = 0) {
|
|
1977
|
-
const rawLabel = typeof fileEntry?.label === 'string' ? fileEntry.label : '';
|
|
1978
|
-
if (rawLabel.includes('/')) {
|
|
1979
|
-
return rawLabel.split('/')[0] || `item-${fallbackIndex + 1}`;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
const filePath = typeof fileEntry?.path === 'string' ? fileEntry.path : '';
|
|
1983
|
-
if (filePath !== '') {
|
|
1984
|
-
const parentDir = path.basename(path.dirname(filePath));
|
|
1985
|
-
if (parentDir && parentDir !== '.' && parentDir !== path.sep) {
|
|
1986
|
-
return parentDir;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
const baseName = path.basename(filePath);
|
|
1990
|
-
if (baseName) {
|
|
1991
|
-
return baseName;
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
return `item-${fallbackIndex + 1}`;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
function buildRunFileEntityGroups(snapshot, flow, options = {}) {
|
|
1999
|
-
const order = ['active', 'upcoming', 'completed', 'intent', 'other'];
|
|
2000
|
-
const rankByScope = new Map(order.map((scope, index) => [scope, index]));
|
|
2001
|
-
const entries = filterExistingFiles(getRunFileEntries(snapshot, flow, options));
|
|
2002
|
-
const groupsByEntity = new Map();
|
|
2003
|
-
|
|
2004
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
2005
|
-
const fileEntry = entries[index];
|
|
2006
|
-
const entity = getFileEntityLabel(fileEntry, index);
|
|
2007
|
-
const scope = order.includes(fileEntry?.scope) ? fileEntry.scope : 'other';
|
|
2008
|
-
const scopeRank = rankByScope.get(scope) ?? rankByScope.get('other');
|
|
2009
|
-
|
|
2010
|
-
if (!groupsByEntity.has(entity)) {
|
|
2011
|
-
groupsByEntity.set(entity, {
|
|
2012
|
-
entity,
|
|
2013
|
-
files: [],
|
|
2014
|
-
scope,
|
|
2015
|
-
scopeRank
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
const group = groupsByEntity.get(entity);
|
|
2020
|
-
group.files.push(fileEntry);
|
|
2021
|
-
|
|
2022
|
-
if (scopeRank < group.scopeRank) {
|
|
2023
|
-
group.scopeRank = scopeRank;
|
|
2024
|
-
group.scope = scope;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
return Array.from(groupsByEntity.values())
|
|
2029
|
-
.sort((a, b) => {
|
|
2030
|
-
if (a.scopeRank !== b.scopeRank) {
|
|
2031
|
-
return a.scopeRank - b.scopeRank;
|
|
2032
|
-
}
|
|
2033
|
-
return String(a.entity).localeCompare(String(b.entity));
|
|
2034
|
-
})
|
|
2035
|
-
.map((group) => ({
|
|
2036
|
-
key: `run-files:entity:${group.entity}`,
|
|
2037
|
-
label: `${group.entity} [${formatScope(group.scope)}] (${group.files.length})`,
|
|
2038
|
-
files: filterExistingFiles(group.files)
|
|
2039
|
-
}))
|
|
2040
|
-
.filter((group) => group.files.length > 0);
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
function normalizeInfoLine(line) {
|
|
2044
|
-
const normalized = normalizePanelLine(line);
|
|
2045
|
-
return {
|
|
2046
|
-
label: normalized.text,
|
|
2047
|
-
color: normalized.color,
|
|
2048
|
-
bold: normalized.bold
|
|
2049
|
-
};
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
function toInfoRows(lines, keyPrefix, emptyLabel = 'No data') {
|
|
2053
|
-
const safe = Array.isArray(lines) ? lines : [];
|
|
2054
|
-
if (safe.length === 0) {
|
|
2055
|
-
return [{
|
|
2056
|
-
kind: 'info',
|
|
2057
|
-
key: `${keyPrefix}:empty`,
|
|
2058
|
-
label: emptyLabel,
|
|
2059
|
-
selectable: false
|
|
2060
|
-
}];
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
return safe.map((line, index) => {
|
|
2064
|
-
const normalized = normalizeInfoLine(line);
|
|
2065
|
-
return {
|
|
2066
|
-
kind: 'info',
|
|
2067
|
-
key: `${keyPrefix}:${index}`,
|
|
2068
|
-
label: normalized.label,
|
|
2069
|
-
color: normalized.color,
|
|
2070
|
-
bold: normalized.bold,
|
|
2071
|
-
selectable: true
|
|
2072
|
-
};
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
function toLoadingRows(label, keyPrefix = 'loading') {
|
|
2077
|
-
return [{
|
|
2078
|
-
kind: 'loading',
|
|
2079
|
-
key: `${keyPrefix}:row`,
|
|
2080
|
-
label: typeof label === 'string' && label !== '' ? label : 'Loading...',
|
|
2081
|
-
selectable: false
|
|
2082
|
-
}];
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
function buildOverviewIntentGroups(snapshot, flow, filter = 'next') {
|
|
2086
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
2087
|
-
const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
|
|
2088
|
-
const isIncluded = (status) => {
|
|
2089
|
-
if (normalizedFilter === 'completed') {
|
|
2090
|
-
return status === 'completed';
|
|
2091
|
-
}
|
|
2092
|
-
return status !== 'completed';
|
|
2093
|
-
};
|
|
2094
|
-
|
|
2095
|
-
if (effectiveFlow === 'aidlc') {
|
|
2096
|
-
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
2097
|
-
return intents
|
|
2098
|
-
.filter((intent) => isIncluded(intent?.status || 'pending'))
|
|
2099
|
-
.map((intent, index) => ({
|
|
2100
|
-
key: `overview:intent:${intent?.id || index}`,
|
|
2101
|
-
label: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${intent?.completedStories || 0}/${intent?.storyCount || 0} stories, ${intent?.completedUnits || 0}/${intent?.unitCount || 0} units)`,
|
|
2102
|
-
files: filterExistingFiles(collectAidlcIntentContextFiles(snapshot, intent?.id))
|
|
2103
|
-
}));
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
if (effectiveFlow === 'simple') {
|
|
2107
|
-
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
2108
|
-
return specs
|
|
2109
|
-
.filter((spec) => isIncluded(spec?.state || 'pending'))
|
|
2110
|
-
.map((spec, index) => ({
|
|
2111
|
-
key: `overview:spec:${spec?.name || index}`,
|
|
2112
|
-
label: `${spec?.name || 'unknown'}: ${spec?.state || 'pending'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks)`,
|
|
2113
|
-
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
2114
|
-
}));
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
2118
|
-
return intents
|
|
2119
|
-
.filter((intent) => isIncluded(intent?.status || 'pending'))
|
|
2120
|
-
.map((intent, index) => {
|
|
2121
|
-
const workItems = Array.isArray(intent?.workItems) ? intent.workItems : [];
|
|
2122
|
-
const done = workItems.filter((item) => item.status === 'completed').length;
|
|
2123
|
-
const files = [{
|
|
2124
|
-
label: buildIntentScopedLabel(snapshot, intent?.id, intent?.filePath, 'brief.md'),
|
|
2125
|
-
path: intent?.filePath,
|
|
2126
|
-
scope: 'intent'
|
|
2127
|
-
}, ...workItems.map((item) => ({
|
|
2128
|
-
label: buildIntentScopedLabel(
|
|
2129
|
-
snapshot,
|
|
2130
|
-
intent?.id,
|
|
2131
|
-
item?.filePath,
|
|
2132
|
-
`${item?.id || 'work-item'}.md`
|
|
2133
|
-
),
|
|
2134
|
-
path: item?.filePath,
|
|
2135
|
-
scope: item?.status === 'completed' ? 'completed' : 'upcoming'
|
|
2136
|
-
}))];
|
|
2137
|
-
return {
|
|
2138
|
-
key: `overview:intent:${intent?.id || index}`,
|
|
2139
|
-
label: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${done}/${workItems.length} work items)`,
|
|
2140
|
-
files: filterExistingFiles(files)
|
|
2141
|
-
};
|
|
2142
|
-
});
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
function buildStandardsRows(snapshot, flow) {
|
|
2146
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
2147
|
-
if (effectiveFlow === 'simple') {
|
|
2148
|
-
return [{
|
|
2149
|
-
kind: 'info',
|
|
2150
|
-
key: 'standards:empty:simple',
|
|
2151
|
-
label: 'No standards for SIMPLE flow',
|
|
2152
|
-
selectable: false
|
|
2153
|
-
}];
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
const standards = Array.isArray(snapshot?.standards) ? snapshot.standards : [];
|
|
2157
|
-
const files = filterExistingFiles(standards.map((standard, index) => ({
|
|
2158
|
-
label: `${standard?.name || standard?.type || `standard-${index}`}.md`,
|
|
2159
|
-
path: standard?.filePath,
|
|
2160
|
-
scope: 'file'
|
|
2161
|
-
})));
|
|
2162
|
-
|
|
2163
|
-
if (files.length === 0) {
|
|
2164
|
-
return [{
|
|
2165
|
-
kind: 'info',
|
|
2166
|
-
key: 'standards:empty',
|
|
2167
|
-
label: 'No standards found',
|
|
2168
|
-
selectable: false
|
|
2169
|
-
}];
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
return files.map((file, index) => ({
|
|
2173
|
-
kind: 'file',
|
|
2174
|
-
key: `standards:file:${file.path}:${index}`,
|
|
2175
|
-
label: file.label,
|
|
2176
|
-
path: file.path,
|
|
2177
|
-
scope: 'file',
|
|
2178
|
-
selectable: true
|
|
2179
|
-
}));
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
function buildProjectGroups(snapshot, flow) {
|
|
2183
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
2184
|
-
const files = [];
|
|
2185
|
-
|
|
2186
|
-
if (effectiveFlow === 'aidlc') {
|
|
2187
|
-
files.push({
|
|
2188
|
-
label: 'memory-bank/project.yaml',
|
|
2189
|
-
path: path.join(snapshot?.rootPath || '', 'project.yaml'),
|
|
2190
|
-
scope: 'file'
|
|
2191
|
-
});
|
|
2192
|
-
} else if (effectiveFlow === 'simple') {
|
|
2193
|
-
files.push({
|
|
2194
|
-
label: 'package.json',
|
|
2195
|
-
path: path.join(snapshot?.workspacePath || '', 'package.json'),
|
|
2196
|
-
scope: 'file'
|
|
2197
|
-
});
|
|
2198
|
-
} else {
|
|
2199
|
-
files.push({
|
|
2200
|
-
label: '.specs-fire/state.yaml',
|
|
2201
|
-
path: path.join(snapshot?.rootPath || '', 'state.yaml'),
|
|
2202
|
-
scope: 'file'
|
|
2203
|
-
});
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
const projectName = snapshot?.project?.name || 'unknown-project';
|
|
2207
|
-
return [{
|
|
2208
|
-
key: `project:${projectName}`,
|
|
2209
|
-
label: `project ${projectName}`,
|
|
2210
|
-
files: filterExistingFiles(files)
|
|
2211
|
-
}];
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
function collectAidlcIntentContextFiles(snapshot, intentId) {
|
|
2215
|
-
if (!snapshot || typeof intentId !== 'string' || intentId.trim() === '') {
|
|
2216
|
-
return [];
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
const intentPath = path.join(snapshot.rootPath || '', 'intents', intentId);
|
|
2220
|
-
return [
|
|
2221
|
-
{
|
|
2222
|
-
label: `${intentId}/requirements.md`,
|
|
2223
|
-
path: path.join(intentPath, 'requirements.md'),
|
|
2224
|
-
scope: 'intent'
|
|
2225
|
-
},
|
|
2226
|
-
{
|
|
2227
|
-
label: `${intentId}/system-context.md`,
|
|
2228
|
-
path: path.join(intentPath, 'system-context.md'),
|
|
2229
|
-
scope: 'intent'
|
|
2230
|
-
},
|
|
2231
|
-
{
|
|
2232
|
-
label: `${intentId}/units.md`,
|
|
2233
|
-
path: path.join(intentPath, 'units.md'),
|
|
2234
|
-
scope: 'intent'
|
|
2235
|
-
}
|
|
2236
|
-
];
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
function filterExistingFiles(files) {
|
|
2240
|
-
return (Array.isArray(files) ? files : []).filter((file) => {
|
|
2241
|
-
if (!file || typeof file.path !== 'string' || typeof file.label !== 'string') {
|
|
2242
|
-
return false;
|
|
2243
|
-
}
|
|
2244
|
-
if (file.allowMissing === true) {
|
|
2245
|
-
return true;
|
|
2246
|
-
}
|
|
2247
|
-
return fileExists(file.path);
|
|
2248
|
-
});
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
function buildPendingGroups(snapshot, flow) {
|
|
2252
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
2253
|
-
|
|
2254
|
-
if (effectiveFlow === 'aidlc') {
|
|
2255
|
-
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
2256
|
-
return pendingBolts.map((bolt, index) => {
|
|
2257
|
-
const deps = Array.isArray(bolt?.blockedBy) && bolt.blockedBy.length > 0
|
|
2258
|
-
? ` blocked_by:${bolt.blockedBy.join(',')}`
|
|
2259
|
-
: '';
|
|
2260
|
-
const location = `${bolt?.intent || 'unknown'}/${bolt?.unit || 'unknown'}`;
|
|
2261
|
-
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
2262
|
-
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
2263
|
-
return {
|
|
2264
|
-
key: `pending:bolt:${bolt?.id || index}`,
|
|
2265
|
-
label: `${bolt?.id || 'unknown'} (${bolt?.status || 'pending'}) in ${location}${deps}`,
|
|
2266
|
-
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
2267
|
-
};
|
|
2268
|
-
});
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
if (effectiveFlow === 'simple') {
|
|
2272
|
-
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
2273
|
-
return pendingSpecs.map((spec, index) => ({
|
|
2274
|
-
key: `pending:spec:${spec?.name || index}`,
|
|
2275
|
-
label: `${spec?.name || 'unknown'} (${spec?.state || 'pending'}) ${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks`,
|
|
2276
|
-
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
2277
|
-
}));
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
2281
|
-
return pendingItems.map((item, index) => {
|
|
2282
|
-
const deps = Array.isArray(item?.dependencies) && item.dependencies.length > 0
|
|
2283
|
-
? ` deps:${item.dependencies.join(',')}`
|
|
2284
|
-
: '';
|
|
2285
|
-
const intentTitle = item?.intentTitle || item?.intentId || 'unknown-intent';
|
|
2286
|
-
const files = [];
|
|
2287
|
-
|
|
2288
|
-
if (item?.filePath) {
|
|
2289
|
-
files.push({
|
|
2290
|
-
label: buildIntentScopedLabel(
|
|
2291
|
-
snapshot,
|
|
2292
|
-
item?.intentId,
|
|
2293
|
-
item?.filePath,
|
|
2294
|
-
`${item?.id || 'work-item'}.md`
|
|
2295
|
-
),
|
|
2296
|
-
path: item.filePath,
|
|
2297
|
-
scope: 'upcoming'
|
|
2298
|
-
});
|
|
2299
|
-
}
|
|
2300
|
-
if (item?.intentId) {
|
|
2301
|
-
files.push({
|
|
2302
|
-
label: buildIntentScopedLabel(
|
|
2303
|
-
snapshot,
|
|
2304
|
-
item.intentId,
|
|
2305
|
-
path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
|
|
2306
|
-
'brief.md'
|
|
2307
|
-
),
|
|
2308
|
-
path: path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
|
|
2309
|
-
scope: 'intent'
|
|
2310
|
-
});
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
return {
|
|
2314
|
-
key: `pending:item:${item?.intentId || 'intent'}:${item?.id || index}`,
|
|
2315
|
-
label: `${item?.id || 'work-item'} (${item?.mode || 'confirm'}/${item?.complexity || 'medium'}) in ${intentTitle}${deps}`,
|
|
2316
|
-
files: filterExistingFiles(files)
|
|
2317
|
-
};
|
|
2318
|
-
});
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
function buildCompletedGroups(snapshot, flow) {
|
|
2322
|
-
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
2323
|
-
|
|
2324
|
-
if (effectiveFlow === 'aidlc') {
|
|
2325
|
-
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
2326
|
-
return completedBolts.map((bolt, index) => {
|
|
2327
|
-
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
2328
|
-
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
2329
|
-
return {
|
|
2330
|
-
key: `completed:bolt:${bolt?.id || index}`,
|
|
2331
|
-
label: `${bolt?.id || 'unknown'} [${bolt?.type || 'bolt'}] done at ${bolt?.completedAt || 'unknown'}`,
|
|
2332
|
-
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
2333
|
-
};
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
if (effectiveFlow === 'simple') {
|
|
2338
|
-
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
2339
|
-
return completedSpecs.map((spec, index) => ({
|
|
2340
|
-
key: `completed:spec:${spec?.name || index}`,
|
|
2341
|
-
label: `${spec?.name || 'unknown'} done at ${spec?.updatedAt || 'unknown'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0})`,
|
|
2342
|
-
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
2343
|
-
}));
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
const groups = [];
|
|
2347
|
-
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
2348
|
-
for (let index = 0; index < completedRuns.length; index += 1) {
|
|
2349
|
-
const run = completedRuns[index];
|
|
2350
|
-
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
2351
|
-
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
2352
|
-
groups.push({
|
|
2353
|
-
key: `completed:run:${run?.id || index}`,
|
|
2354
|
-
label: `${run?.id || 'run'} [${run?.scope || 'single'}] ${completed}/${workItems.length} done at ${run?.completedAt || 'unknown'}`,
|
|
2355
|
-
files: filterExistingFiles(collectFireRunFiles(run).map((file) => ({ ...file, scope: 'completed' })))
|
|
2356
|
-
});
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
const completedIntents = Array.isArray(snapshot?.intents)
|
|
2360
|
-
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
2361
|
-
: [];
|
|
2362
|
-
for (let index = 0; index < completedIntents.length; index += 1) {
|
|
2363
|
-
const intent = completedIntents[index];
|
|
2364
|
-
groups.push({
|
|
2365
|
-
key: `completed:intent:${intent?.id || index}`,
|
|
2366
|
-
label: `intent ${intent?.id || 'unknown'} [completed]`,
|
|
2367
|
-
files: filterExistingFiles([{
|
|
2368
|
-
label: buildIntentScopedLabel(
|
|
2369
|
-
snapshot,
|
|
2370
|
-
intent?.id,
|
|
2371
|
-
path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
2372
|
-
'brief.md'
|
|
2373
|
-
),
|
|
2374
|
-
path: path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
2375
|
-
scope: 'intent'
|
|
2376
|
-
}])
|
|
2377
|
-
});
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
return groups;
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
function buildGitChangeGroups(snapshot) {
|
|
2384
|
-
const git = getGitChangesSnapshot(snapshot);
|
|
2385
|
-
|
|
2386
|
-
if (!git.available) {
|
|
2387
|
-
return [];
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
const makeFiles = (items, scope) => items.map((item) => ({
|
|
2391
|
-
label: item.relativePath,
|
|
2392
|
-
path: item.path || path.join(git.rootPath || snapshot?.workspacePath || '', item.relativePath || ''),
|
|
2393
|
-
scope,
|
|
2394
|
-
allowMissing: true,
|
|
2395
|
-
previewType: 'git-diff',
|
|
2396
|
-
repoRoot: git.rootPath || snapshot?.workspacePath || '',
|
|
2397
|
-
relativePath: item.relativePath || '',
|
|
2398
|
-
bucket: item.bucket || scope
|
|
2399
|
-
}));
|
|
2400
|
-
|
|
2401
|
-
const groups = [];
|
|
2402
|
-
groups.push({
|
|
2403
|
-
key: 'git:staged',
|
|
2404
|
-
label: `staged (${git.counts.staged || 0})`,
|
|
2405
|
-
files: makeFiles(git.staged, 'staged')
|
|
2406
|
-
});
|
|
2407
|
-
groups.push({
|
|
2408
|
-
key: 'git:unstaged',
|
|
2409
|
-
label: `unstaged (${git.counts.unstaged || 0})`,
|
|
2410
|
-
files: makeFiles(git.unstaged, 'unstaged')
|
|
2411
|
-
});
|
|
2412
|
-
groups.push({
|
|
2413
|
-
key: 'git:untracked',
|
|
2414
|
-
label: `untracked (${git.counts.untracked || 0})`,
|
|
2415
|
-
files: makeFiles(git.untracked, 'untracked')
|
|
2416
|
-
});
|
|
2417
|
-
groups.push({
|
|
2418
|
-
key: 'git:conflicted',
|
|
2419
|
-
label: `conflicts (${git.counts.conflicted || 0})`,
|
|
2420
|
-
files: makeFiles(git.conflicted, 'conflicted')
|
|
2421
|
-
});
|
|
2422
|
-
|
|
2423
|
-
return groups;
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
2427
|
-
if (!Array.isArray(groups) || groups.length === 0) {
|
|
2428
|
-
return [{
|
|
2429
|
-
kind: 'info',
|
|
2430
|
-
key: 'section:empty',
|
|
2431
|
-
label: emptyLabel,
|
|
2432
|
-
selectable: false
|
|
2433
|
-
}];
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
const rows = [];
|
|
2437
|
-
|
|
2438
|
-
for (const group of groups) {
|
|
2439
|
-
const files = filterExistingFiles(group?.files);
|
|
2440
|
-
const expandable = files.length > 0;
|
|
2441
|
-
const expanded = expandable && Boolean(expandedGroups?.[group.key]);
|
|
2442
|
-
|
|
2443
|
-
rows.push({
|
|
2444
|
-
kind: 'group',
|
|
2445
|
-
key: group.key,
|
|
2446
|
-
label: group.label,
|
|
2447
|
-
expandable,
|
|
2448
|
-
expanded,
|
|
2449
|
-
selectable: true
|
|
2450
|
-
});
|
|
2451
|
-
|
|
2452
|
-
if (expanded) {
|
|
2453
|
-
for (let index = 0; index < files.length; index += 1) {
|
|
2454
|
-
const file = files[index];
|
|
2455
|
-
rows.push({
|
|
2456
|
-
kind: file.previewType === 'git-diff' ? 'git-file' : 'file',
|
|
2457
|
-
key: `${group.key}:file:${file.path}:${index}`,
|
|
2458
|
-
label: file.label,
|
|
2459
|
-
path: file.path,
|
|
2460
|
-
scope: file.scope || 'file',
|
|
2461
|
-
selectable: true,
|
|
2462
|
-
previewType: file.previewType,
|
|
2463
|
-
repoRoot: file.repoRoot,
|
|
2464
|
-
relativePath: file.relativePath,
|
|
2465
|
-
bucket: file.bucket
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
return rows;
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2474
|
-
function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedSection) {
|
|
2475
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2476
|
-
return [{ text: '', color: undefined, bold: false, selected: false }];
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
const clampedIndex = clampIndex(selectedIndex, rows.length);
|
|
2480
|
-
|
|
2481
|
-
return rows.map((row, index) => {
|
|
2482
|
-
const selectable = row?.selectable !== false;
|
|
2483
|
-
const isSelected = selectable && index === clampedIndex;
|
|
2484
|
-
const cursor = isSelected
|
|
2485
|
-
? (isFocusedSection ? (icons.activeFile || '>') : '•')
|
|
2486
|
-
: ' ';
|
|
2487
|
-
|
|
2488
|
-
if (row.kind === 'group') {
|
|
2489
|
-
const marker = row.expandable
|
|
2490
|
-
? (row.expanded ? (icons.groupExpanded || 'v') : (icons.groupCollapsed || '>'))
|
|
2491
|
-
: '-';
|
|
2492
|
-
return {
|
|
2493
|
-
text: truncate(`${cursor} ${marker} ${row.label}`, width),
|
|
2494
|
-
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : undefined,
|
|
2495
|
-
bold: isSelected,
|
|
2496
|
-
selected: isSelected
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
if (row.kind === 'file' || row.kind === 'git-file' || row.kind === 'git-commit') {
|
|
2501
|
-
const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
|
|
2502
|
-
return {
|
|
2503
|
-
text: truncate(`${cursor} ${icons.runFile} ${scope}${row.label}`, width),
|
|
2504
|
-
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : 'gray',
|
|
2505
|
-
bold: isSelected,
|
|
2506
|
-
selected: isSelected
|
|
2507
|
-
};
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
if (row.kind === 'loading') {
|
|
2511
|
-
return {
|
|
2512
|
-
text: truncate(row.label || 'Loading...', width),
|
|
2513
|
-
color: 'cyan',
|
|
2514
|
-
bold: false,
|
|
2515
|
-
selected: false,
|
|
2516
|
-
loading: true
|
|
2517
|
-
};
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
return {
|
|
2521
|
-
text: truncate(`${isSelected ? `${cursor} ` : ' '}${row.label || ''}`, width),
|
|
2522
|
-
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : (row.color || 'gray'),
|
|
2523
|
-
bold: isSelected || Boolean(row.bold),
|
|
2524
|
-
selected: isSelected
|
|
2525
|
-
};
|
|
2526
|
-
});
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
function getSelectedRow(rows, selectedIndex) {
|
|
2530
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2531
|
-
return null;
|
|
2532
|
-
}
|
|
2533
|
-
return rows[clampIndex(selectedIndex, rows.length)] || null;
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
function rowToFileEntry(row) {
|
|
2537
|
-
if (!row) {
|
|
2538
|
-
return null;
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
if (row.kind === 'git-commit') {
|
|
2542
|
-
const commitHash = typeof row.commitHash === 'string' ? row.commitHash : '';
|
|
2543
|
-
if (commitHash === '') {
|
|
2544
|
-
return null;
|
|
2545
|
-
}
|
|
2546
|
-
return {
|
|
2547
|
-
label: row.label || commitHash,
|
|
2548
|
-
path: commitHash,
|
|
2549
|
-
scope: 'commit',
|
|
2550
|
-
previewType: row.previewType || 'git-commit-diff',
|
|
2551
|
-
repoRoot: row.repoRoot,
|
|
2552
|
-
commitHash
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
if ((row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
|
|
2557
|
-
return null;
|
|
2558
|
-
}
|
|
2559
|
-
return {
|
|
2560
|
-
label: row.label || path.basename(row.path),
|
|
2561
|
-
path: row.path,
|
|
2562
|
-
scope: row.scope || 'file',
|
|
2563
|
-
previewType: row.previewType,
|
|
2564
|
-
repoRoot: row.repoRoot,
|
|
2565
|
-
relativePath: row.relativePath,
|
|
2566
|
-
bucket: row.bucket
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
function firstFileEntryFromRows(rows) {
|
|
2571
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2572
|
-
return null;
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
for (const row of rows) {
|
|
2576
|
-
const entry = rowToFileEntry(row);
|
|
2577
|
-
if (entry) {
|
|
2578
|
-
return entry;
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
return null;
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
function rowToWorktreeId(row) {
|
|
2586
|
-
if (!row || typeof row.key !== 'string') {
|
|
2587
|
-
return null;
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
const prefix = 'worktree:item:';
|
|
2591
|
-
if (!row.key.startsWith(prefix)) {
|
|
2592
|
-
return null;
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
const worktreeId = row.key.slice(prefix.length).trim();
|
|
2596
|
-
return worktreeId === '' ? null : worktreeId;
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
function moveRowSelection(rows, currentIndex, direction) {
|
|
2600
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2601
|
-
return 0;
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
const clamped = clampIndex(currentIndex, rows.length);
|
|
2605
|
-
const step = direction >= 0 ? 1 : -1;
|
|
2606
|
-
let next = clamped + step;
|
|
2607
|
-
|
|
2608
|
-
while (next >= 0 && next < rows.length) {
|
|
2609
|
-
if (rows[next]?.selectable !== false) {
|
|
2610
|
-
return next;
|
|
2611
|
-
}
|
|
2612
|
-
next += step;
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
return clamped;
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
function openFileWithDefaultApp(filePath) {
|
|
2619
|
-
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
2620
|
-
return {
|
|
2621
|
-
ok: false,
|
|
2622
|
-
message: 'No file selected to open.'
|
|
2623
|
-
};
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
if (!fileExists(filePath)) {
|
|
2627
|
-
return {
|
|
2628
|
-
ok: false,
|
|
2629
|
-
message: `File not found: ${filePath}`
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
let command = null;
|
|
2634
|
-
let args = [];
|
|
2635
|
-
|
|
2636
|
-
if (process.platform === 'darwin') {
|
|
2637
|
-
command = 'open';
|
|
2638
|
-
args = [filePath];
|
|
2639
|
-
} else if (process.platform === 'win32') {
|
|
2640
|
-
command = 'cmd';
|
|
2641
|
-
args = ['/c', 'start', '', filePath];
|
|
2642
|
-
} else {
|
|
2643
|
-
command = 'xdg-open';
|
|
2644
|
-
args = [filePath];
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
const result = spawnSync(command, args, { stdio: 'ignore' });
|
|
2648
|
-
if (result.error) {
|
|
2649
|
-
return {
|
|
2650
|
-
ok: false,
|
|
2651
|
-
message: `Unable to open file: ${result.error.message}`
|
|
2652
|
-
};
|
|
2653
|
-
}
|
|
2654
|
-
if (typeof result.status === 'number' && result.status !== 0) {
|
|
2655
|
-
return {
|
|
2656
|
-
ok: false,
|
|
2657
|
-
message: `Open command failed with exit code ${result.status}.`
|
|
2658
|
-
};
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
return {
|
|
2662
|
-
ok: true,
|
|
2663
|
-
message: `Opened ${filePath}`
|
|
2664
|
-
};
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
function buildQuickHelpText(view, options = {}) {
|
|
2668
|
-
const {
|
|
2669
|
-
flow = 'fire',
|
|
2670
|
-
previewOpen = false,
|
|
2671
|
-
availableFlowCount = 1,
|
|
2672
|
-
hasWorktrees = false
|
|
2673
|
-
} = options;
|
|
2674
|
-
const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
|
|
2675
|
-
const isSimple = String(flow || '').toLowerCase() === 'simple';
|
|
2676
|
-
const activeLabel = isAidlc ? 'active bolt' : (isSimple ? 'active spec' : 'active run');
|
|
2677
|
-
|
|
2678
|
-
const parts = ['1/2/3/4/5 tabs', 'g/G sections'];
|
|
2679
|
-
|
|
2680
|
-
if (view === 'runs' || view === 'intents' || view === 'completed' || view === 'health' || view === 'git') {
|
|
2681
|
-
if (previewOpen) {
|
|
2682
|
-
parts.push('tab pane', '↑/↓ nav/scroll', 'v/space close');
|
|
2683
|
-
} else {
|
|
2684
|
-
parts.push('↑/↓ navigate', 'enter expand', 'v/space preview');
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
if (view === 'runs') {
|
|
2688
|
-
if (hasWorktrees) {
|
|
2689
|
-
parts.push('b worktrees', 'u others');
|
|
2690
|
-
}
|
|
2691
|
-
parts.push('a current', 'f files');
|
|
2692
|
-
} else if (view === 'git') {
|
|
2693
|
-
parts.push('6 status', '7 files', '8 commits', '- diff');
|
|
2694
|
-
}
|
|
2695
|
-
parts.push(`tab1 ${activeLabel}`);
|
|
2696
|
-
|
|
2697
|
-
if (availableFlowCount > 1) {
|
|
2698
|
-
parts.push('[/] flow');
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
parts.push('r refresh', '? shortcuts', 'q quit');
|
|
2702
|
-
return parts.join(' | ');
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
function buildGitCommandStrip(view, options = {}) {
|
|
2706
|
-
const {
|
|
2707
|
-
hasWorktrees = false,
|
|
2708
|
-
previewOpen = false
|
|
2709
|
-
} = options;
|
|
2710
|
-
|
|
2711
|
-
const parts = [];
|
|
2712
|
-
|
|
2713
|
-
if (view === 'runs') {
|
|
2714
|
-
if (hasWorktrees) {
|
|
2715
|
-
parts.push('b worktrees');
|
|
2716
|
-
}
|
|
2717
|
-
parts.push('a current', 'f files', 'enter expand');
|
|
2718
|
-
} else if (view === 'intents') {
|
|
2719
|
-
parts.push('n next', 'x completed', 'enter expand');
|
|
2720
|
-
} else if (view === 'completed') {
|
|
2721
|
-
parts.push('c completed', 'enter expand');
|
|
2722
|
-
} else if (view === 'health') {
|
|
2723
|
-
parts.push('s standards', 't stats', 'w warnings');
|
|
2724
|
-
} else if (view === 'git') {
|
|
2725
|
-
parts.push('6 status', '7 files', '8 commits', '- diff', 'space preview');
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
if (previewOpen) {
|
|
2729
|
-
parts.push('tab pane', 'j/k scroll');
|
|
2730
|
-
} else {
|
|
2731
|
-
parts.push('v preview');
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
parts.push('1-5 views', 'g/G panels', 'r refresh', '? help', 'q quit');
|
|
2735
|
-
return parts.join(' | ');
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
function buildGitCommandLogLine(options = {}) {
|
|
2739
|
-
const {
|
|
2740
|
-
statusLine = '',
|
|
2741
|
-
activeFlow = 'fire',
|
|
2742
|
-
watchEnabled = true,
|
|
2743
|
-
watchStatus = 'watching',
|
|
2744
|
-
selectedWorktreeLabel = null
|
|
2745
|
-
} = options;
|
|
2746
|
-
|
|
2747
|
-
if (typeof statusLine === 'string' && statusLine.trim() !== '') {
|
|
2748
|
-
return `Command Log | ${statusLine}`;
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
const watchLabel = watchEnabled ? watchStatus : 'off';
|
|
2752
|
-
const worktreeSegment = selectedWorktreeLabel ? ` | wt:${selectedWorktreeLabel}` : '';
|
|
2753
|
-
return `Command Log | flow:${String(activeFlow || 'fire').toUpperCase()} | watch:${watchLabel}${worktreeSegment} | ready`;
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
function buildHelpOverlayLines(options = {}) {
|
|
2757
|
-
const {
|
|
2758
|
-
view = 'runs',
|
|
2759
|
-
flow = 'fire',
|
|
2760
|
-
previewOpen = false,
|
|
2761
|
-
paneFocus = 'main',
|
|
2762
|
-
availableFlowCount = 1,
|
|
2763
|
-
showErrorSection = false,
|
|
2764
|
-
hasWorktrees = false
|
|
2765
|
-
} = options;
|
|
2766
|
-
const isAidlc = String(flow || '').toLowerCase() === 'aidlc';
|
|
2767
|
-
const isSimple = String(flow || '').toLowerCase() === 'simple';
|
|
2768
|
-
const itemLabel = isAidlc ? 'bolt' : (isSimple ? 'spec' : 'run');
|
|
2769
|
-
const itemPlural = isAidlc ? 'bolts' : (isSimple ? 'specs' : 'runs');
|
|
2770
|
-
|
|
2771
|
-
const lines = [
|
|
2772
|
-
{ text: 'Global', color: 'cyan', bold: true },
|
|
2773
|
-
'q or Ctrl+C quit',
|
|
2774
|
-
'r refresh snapshot',
|
|
2775
|
-
`1 active ${itemLabel} | 2 intents | 3 completed ${itemPlural} | 4 standards/health | 5 git changes`,
|
|
2776
|
-
'g next section | G previous section',
|
|
2777
|
-
'h/? toggle this shortcuts overlay',
|
|
2778
|
-
'esc close overlays (help/preview/fullscreen)',
|
|
2779
|
-
{ text: '', color: undefined, bold: false },
|
|
2780
|
-
{ text: 'Tab 1 Active', color: 'yellow', bold: true },
|
|
2781
|
-
...(hasWorktrees ? ['b focus worktrees section', 'u focus other-worktrees section'] : []),
|
|
2782
|
-
`a focus active ${itemLabel}`,
|
|
2783
|
-
`f focus ${itemLabel} files`,
|
|
2784
|
-
'up/down or j/k move selection',
|
|
2785
|
-
'enter expand/collapse selected folder row',
|
|
2786
|
-
'v or space preview selected file',
|
|
2787
|
-
'v twice quickly opens fullscreen preview overlay',
|
|
2788
|
-
'tab switch focus between main and preview panes',
|
|
2789
|
-
'o open selected file in system default app'
|
|
2790
|
-
];
|
|
2791
|
-
|
|
2792
|
-
if (previewOpen) {
|
|
2793
|
-
lines.push(`preview is open (focus: ${paneFocus})`);
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
if (availableFlowCount > 1) {
|
|
2797
|
-
lines.push('[/] (and m) switch flow');
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
lines.push(
|
|
2801
|
-
{ text: '', color: undefined, bold: false },
|
|
2802
|
-
{ text: 'Tab 2 Intents', color: 'green', bold: true },
|
|
2803
|
-
'i focus intents',
|
|
2804
|
-
'n next intents | x completed intents',
|
|
2805
|
-
'left/right toggles next/completed when intents is focused',
|
|
2806
|
-
{ text: '', color: undefined, bold: false },
|
|
2807
|
-
{ text: 'Tab 3 Completed', color: 'blue', bold: true },
|
|
2808
|
-
'c focus completed items',
|
|
2809
|
-
{ text: '', color: undefined, bold: false },
|
|
2810
|
-
{ text: 'Tab 4 Standards/Health', color: 'magenta', bold: true },
|
|
2811
|
-
`s standards | t stats | w warnings${showErrorSection ? ' | e errors' : ''}`,
|
|
2812
|
-
{ text: '', color: undefined, bold: false },
|
|
2813
|
-
{ text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
|
|
2814
|
-
'7 files: select changed files and preview per-file diffs',
|
|
2815
|
-
'8 commits: select a commit to preview the full commit diff',
|
|
2816
|
-
'6 status | 7 files | 8 commits | - diff',
|
|
2817
|
-
{ text: '', color: undefined, bold: false },
|
|
2818
|
-
{ text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
|
|
2819
|
-
);
|
|
2820
|
-
|
|
2821
|
-
return lines;
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
function buildWorktreeOverlayLines(snapshot, selectedIndex, width) {
|
|
2825
|
-
const meta = getDashboardWorktreeMeta(snapshot);
|
|
2826
|
-
if (!meta) {
|
|
2827
|
-
return [{
|
|
2828
|
-
text: truncate('No worktrees available', width),
|
|
2829
|
-
color: 'gray',
|
|
2830
|
-
bold: false
|
|
2831
|
-
}];
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
const items = Array.isArray(meta.items) ? meta.items : [];
|
|
2835
|
-
const clampedIndex = clampIndex(selectedIndex, items.length || 1);
|
|
2836
|
-
const lines = [{
|
|
2837
|
-
text: truncate('Use ↑/↓ and Enter to switch. Esc closes.', width),
|
|
2838
|
-
color: 'gray',
|
|
2839
|
-
bold: false
|
|
2840
|
-
}];
|
|
2841
|
-
|
|
2842
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
2843
|
-
const item = items[index];
|
|
2844
|
-
const marker = index === clampedIndex ? '>' : ' ';
|
|
2845
|
-
const current = item.isSelected ? '[CURRENT] ' : '';
|
|
2846
|
-
const main = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
|
|
2847
|
-
const status = item.status === 'loading'
|
|
2848
|
-
? 'loading'
|
|
2849
|
-
: (item.status === 'error'
|
|
2850
|
-
? 'error'
|
|
2851
|
-
: (item.flowAvailable ? `${item.activeCount || 0} active` : 'flow unavailable'));
|
|
2852
|
-
const pathLabel = item.path ? path.basename(item.path) : 'unknown';
|
|
2853
|
-
lines.push({
|
|
2854
|
-
text: truncate(`${marker} ${current}${main}${getWorktreeDisplayName(item)} (${pathLabel}) | ${status}`, width),
|
|
2855
|
-
color: index === clampedIndex ? 'green' : (item.isSelected ? 'cyan' : 'gray'),
|
|
2856
|
-
bold: index === clampedIndex || item.isSelected
|
|
2857
|
-
});
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
if (meta.hasPendingScans) {
|
|
2861
|
-
lines.push({
|
|
2862
|
-
text: truncate('Background scan in progress for additional worktrees...', width),
|
|
2863
|
-
color: 'yellow',
|
|
2864
|
-
bold: false
|
|
2865
|
-
});
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
return lines;
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
function colorizeMarkdownLine(line, inCodeBlock) {
|
|
2872
|
-
const text = String(line ?? '');
|
|
2873
|
-
|
|
2874
|
-
if (/^\s*```/.test(text)) {
|
|
2875
|
-
return {
|
|
2876
|
-
color: 'magenta',
|
|
2877
|
-
bold: true,
|
|
2878
|
-
togglesCodeBlock: true
|
|
2879
|
-
};
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
if (/^\s{0,3}#{1,6}\s+/.test(text)) {
|
|
2883
|
-
return {
|
|
2884
|
-
color: 'cyan',
|
|
2885
|
-
bold: true,
|
|
2886
|
-
togglesCodeBlock: false
|
|
2887
|
-
};
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
if (/^\s*[-*+]\s+\[[ xX]\]/.test(text) || /^\s*[-*+]\s+/.test(text) || /^\s*\d+\.\s+/.test(text)) {
|
|
2891
|
-
return {
|
|
2892
|
-
color: 'yellow',
|
|
2893
|
-
bold: false,
|
|
2894
|
-
togglesCodeBlock: false
|
|
2895
|
-
};
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
if (/^\s*>\s+/.test(text)) {
|
|
2899
|
-
return {
|
|
2900
|
-
color: 'gray',
|
|
2901
|
-
bold: false,
|
|
2902
|
-
togglesCodeBlock: false
|
|
2903
|
-
};
|
|
2904
|
-
}
|
|
2905
|
-
|
|
2906
|
-
if (/^\s*---\s*$/.test(text)) {
|
|
2907
|
-
return {
|
|
2908
|
-
color: 'yellow',
|
|
2909
|
-
bold: false,
|
|
2910
|
-
togglesCodeBlock: false
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
if (inCodeBlock) {
|
|
2915
|
-
return {
|
|
2916
|
-
color: 'green',
|
|
2917
|
-
bold: false,
|
|
2918
|
-
togglesCodeBlock: false
|
|
2919
|
-
};
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
return {
|
|
2923
|
-
color: undefined,
|
|
2924
|
-
bold: false,
|
|
2925
|
-
togglesCodeBlock: false
|
|
2926
|
-
};
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
|
|
2930
|
-
const fullDocument = options?.fullDocument === true;
|
|
2931
|
-
|
|
2932
|
-
if (!fileEntry || typeof fileEntry.path !== 'string') {
|
|
2933
|
-
return [{ text: truncate('No file selected', width), color: 'gray', bold: false }];
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
const isGitFilePreview = fileEntry.previewType === 'git-diff';
|
|
2937
|
-
const isGitCommitPreview = fileEntry.previewType === 'git-commit-diff';
|
|
2938
|
-
const isGitPreview = isGitFilePreview || isGitCommitPreview;
|
|
2939
|
-
let rawLines = [];
|
|
2940
|
-
if (isGitPreview) {
|
|
2941
|
-
const diffText = isGitCommitPreview
|
|
2942
|
-
? loadGitCommitPreview(fileEntry)
|
|
2943
|
-
: loadGitDiffPreview(fileEntry);
|
|
2944
|
-
rawLines = String(diffText || '').split(/\r?\n/);
|
|
2945
|
-
} else {
|
|
2946
|
-
let content;
|
|
2947
|
-
try {
|
|
2948
|
-
content = fs.readFileSync(fileEntry.path, 'utf8');
|
|
2949
|
-
} catch (error) {
|
|
2950
|
-
return [{
|
|
2951
|
-
text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
|
|
2952
|
-
color: 'red',
|
|
2953
|
-
bold: false
|
|
2954
|
-
}];
|
|
2955
|
-
}
|
|
2956
|
-
rawLines = String(content).split(/\r?\n/);
|
|
2957
|
-
}
|
|
2958
|
-
|
|
2959
|
-
const headLine = {
|
|
2960
|
-
text: truncate(
|
|
2961
|
-
isGitCommitPreview
|
|
2962
|
-
? `commit: ${fileEntry.commitHash || fileEntry.path}`
|
|
2963
|
-
: `${isGitPreview ? 'diff' : 'file'}: ${fileEntry.path}`,
|
|
2964
|
-
width
|
|
2965
|
-
),
|
|
2966
|
-
color: 'cyan',
|
|
2967
|
-
bold: true
|
|
2968
|
-
};
|
|
2969
|
-
|
|
2970
|
-
const cappedLines = fullDocument ? rawLines : rawLines.slice(0, 300);
|
|
2971
|
-
const hiddenLineCount = fullDocument ? 0 : Math.max(0, rawLines.length - cappedLines.length);
|
|
2972
|
-
let inCodeBlock = false;
|
|
2973
|
-
|
|
2974
|
-
const highlighted = cappedLines.map((rawLine, index) => {
|
|
2975
|
-
const prefixedLine = `${String(index + 1).padStart(4, ' ')} | ${rawLine}`;
|
|
2976
|
-
let color;
|
|
2977
|
-
let bold;
|
|
2978
|
-
let togglesCodeBlock = false;
|
|
2979
|
-
|
|
2980
|
-
if (isGitPreview) {
|
|
2981
|
-
if (rawLine.startsWith('+++ ') || rawLine.startsWith('--- ') || rawLine.startsWith('diff --git')) {
|
|
2982
|
-
color = 'cyan';
|
|
2983
|
-
bold = true;
|
|
2984
|
-
} else if (rawLine.startsWith('@@')) {
|
|
2985
|
-
color = 'magenta';
|
|
2986
|
-
bold = true;
|
|
2987
|
-
} else if (rawLine.startsWith('+')) {
|
|
2988
|
-
color = 'green';
|
|
2989
|
-
bold = false;
|
|
2990
|
-
} else if (rawLine.startsWith('-')) {
|
|
2991
|
-
color = 'red';
|
|
2992
|
-
bold = false;
|
|
2993
|
-
} else {
|
|
2994
|
-
color = undefined;
|
|
2995
|
-
bold = false;
|
|
2996
|
-
}
|
|
2997
|
-
} else {
|
|
2998
|
-
const markdownStyle = colorizeMarkdownLine(rawLine, inCodeBlock);
|
|
2999
|
-
color = markdownStyle.color;
|
|
3000
|
-
bold = markdownStyle.bold;
|
|
3001
|
-
togglesCodeBlock = markdownStyle.togglesCodeBlock;
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
if (togglesCodeBlock) {
|
|
3005
|
-
inCodeBlock = !inCodeBlock;
|
|
3006
|
-
}
|
|
3007
|
-
return {
|
|
3008
|
-
text: truncate(prefixedLine, width),
|
|
3009
|
-
color,
|
|
3010
|
-
bold
|
|
3011
|
-
};
|
|
3012
|
-
});
|
|
3013
|
-
|
|
3014
|
-
if (hiddenLineCount > 0) {
|
|
3015
|
-
highlighted.push({
|
|
3016
|
-
text: truncate(`... ${hiddenLineCount} additional lines hidden`, width),
|
|
3017
|
-
color: 'gray',
|
|
3018
|
-
bold: false
|
|
3019
|
-
});
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
const clampedOffset = clampIndex(scrollOffset, highlighted.length);
|
|
3023
|
-
const body = highlighted.slice(clampedOffset);
|
|
5
|
+
stringWidth,
|
|
6
|
+
toDashboardError,
|
|
7
|
+
safeJsonHash,
|
|
8
|
+
resolveIconSet,
|
|
9
|
+
truncate,
|
|
10
|
+
resolveFrameWidth,
|
|
11
|
+
fitLines,
|
|
12
|
+
clampIndex
|
|
13
|
+
} = require('./helpers');
|
|
3024
14
|
|
|
3025
|
-
|
|
3026
|
-
|
|
15
|
+
const {
|
|
16
|
+
getEffectiveFlow,
|
|
17
|
+
detectDashboardApprovalGate,
|
|
18
|
+
detectFireRunApprovalGate,
|
|
19
|
+
detectAidlcBoltApprovalGate,
|
|
20
|
+
buildHeaderLine,
|
|
21
|
+
buildErrorLines,
|
|
22
|
+
buildStatsLines,
|
|
23
|
+
buildWarningsLines,
|
|
24
|
+
getPanelTitles
|
|
25
|
+
} = require('./flow-builders');
|
|
3027
26
|
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
27
|
+
const {
|
|
28
|
+
getGitChangesSnapshot,
|
|
29
|
+
buildGitStatusPanelLines,
|
|
30
|
+
buildGitCommitRows,
|
|
31
|
+
buildGitChangeGroups
|
|
32
|
+
} = require('./git-builders');
|
|
3033
33
|
|
|
3034
|
-
|
|
3035
|
-
|
|
34
|
+
const {
|
|
35
|
+
hasMultipleWorktrees,
|
|
36
|
+
getWorktreeItems,
|
|
37
|
+
getSelectedWorktree,
|
|
38
|
+
getWorktreeDisplayName,
|
|
39
|
+
buildWorktreeRows,
|
|
40
|
+
buildOtherWorktreeActiveGroups,
|
|
41
|
+
getOtherWorktreeEmptyMessage,
|
|
42
|
+
buildWorktreeOverlayLines
|
|
43
|
+
} = require('./worktree-builders');
|
|
44
|
+
|
|
45
|
+
const { getSectionOrderForView, cycleSection } = require('./sections');
|
|
3036
46
|
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
47
|
+
const {
|
|
48
|
+
getNoCurrentMessage,
|
|
49
|
+
getNoFileMessage,
|
|
50
|
+
getNoCompletedMessage
|
|
51
|
+
} = require('./file-entries');
|
|
3040
52
|
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
53
|
+
const {
|
|
54
|
+
buildCurrentGroups,
|
|
55
|
+
toExpandableRows,
|
|
56
|
+
buildRunFileEntityGroups,
|
|
57
|
+
buildOverviewIntentGroups,
|
|
58
|
+
buildCompletedGroups,
|
|
59
|
+
buildStandardsRows,
|
|
60
|
+
toInfoRows,
|
|
61
|
+
toLoadingRows,
|
|
62
|
+
buildInteractiveRowsLines,
|
|
63
|
+
getSelectedRow,
|
|
64
|
+
rowToFileEntry,
|
|
65
|
+
firstFileEntryFromRows,
|
|
66
|
+
rowToWorktreeId,
|
|
67
|
+
moveRowSelection,
|
|
68
|
+
openFileWithDefaultApp
|
|
69
|
+
} = require('./row-builders');
|
|
3049
70
|
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
}
|
|
71
|
+
const {
|
|
72
|
+
buildQuickHelpText,
|
|
73
|
+
buildGitCommandStrip,
|
|
74
|
+
buildGitCommandLogLine,
|
|
75
|
+
buildHelpOverlayLines
|
|
76
|
+
} = require('./overlays');
|
|
3057
77
|
|
|
3058
|
-
|
|
3059
|
-
|
|
78
|
+
const {
|
|
79
|
+
buildPreviewLines,
|
|
80
|
+
allocateSingleColumnPanels
|
|
81
|
+
} = require('./preview');
|
|
3060
82
|
|
|
3061
83
|
function createDashboardApp(deps) {
|
|
3062
84
|
const {
|
|
@@ -3475,10 +497,10 @@ function createDashboardApp(deps) {
|
|
|
3475
497
|
'git-changes': gitRows,
|
|
3476
498
|
'git-commits': gitCommitRows
|
|
3477
499
|
};
|
|
3478
|
-
const
|
|
500
|
+
const worktreeItemsList = getWorktreeItems(snapshot);
|
|
3479
501
|
const selectedWorktree = getSelectedWorktree(snapshot);
|
|
3480
502
|
const selectedWorktreeLabel = selectedWorktree ? getWorktreeDisplayName(selectedWorktree) : null;
|
|
3481
|
-
const worktreeWatchSignature = `${snapshot?.dashboardWorktrees?.selectedWorktreeId || ''}|${
|
|
503
|
+
const worktreeWatchSignature = `${snapshot?.dashboardWorktrees?.selectedWorktreeId || ''}|${worktreeItemsList
|
|
3482
504
|
.map((item) => `${item.id}:${item.status}:${item.activeCount}:${item.flowAvailable ? '1' : '0'}`)
|
|
3483
505
|
.join(',')}`;
|
|
3484
506
|
const rowLengthSignature = Object.entries(rowsBySection)
|
|
@@ -3566,7 +588,7 @@ function createDashboardApp(deps) {
|
|
|
3566
588
|
return false;
|
|
3567
589
|
}
|
|
3568
590
|
|
|
3569
|
-
const nextItem =
|
|
591
|
+
const nextItem = worktreeItemsList.find((item) => item.id === normalizedNextId);
|
|
3570
592
|
if (!nextItem) {
|
|
3571
593
|
setStatusLine('Selected worktree is no longer available.');
|
|
3572
594
|
return false;
|
|
@@ -3591,7 +613,7 @@ function createDashboardApp(deps) {
|
|
|
3591
613
|
}
|
|
3592
614
|
|
|
3593
615
|
return true;
|
|
3594
|
-
}, [refresh, selectedWorktreeId,
|
|
616
|
+
}, [refresh, selectedWorktreeId, worktreeItemsList]);
|
|
3595
617
|
|
|
3596
618
|
useInput((input, key) => {
|
|
3597
619
|
if ((key.ctrl && input === 'c') || input === 'q') {
|
|
@@ -3630,12 +652,12 @@ function createDashboardApp(deps) {
|
|
|
3630
652
|
}
|
|
3631
653
|
|
|
3632
654
|
if (key.downArrow || input === 'j') {
|
|
3633
|
-
setWorktreeOverlayIndex((previous) => Math.min(Math.max(0,
|
|
655
|
+
setWorktreeOverlayIndex((previous) => Math.min(Math.max(0, worktreeItemsList.length - 1), previous + 1));
|
|
3634
656
|
return;
|
|
3635
657
|
}
|
|
3636
658
|
|
|
3637
659
|
if (key.return || key.enter) {
|
|
3638
|
-
const selectedOverlayItem =
|
|
660
|
+
const selectedOverlayItem = worktreeItemsList[clampIndex(worktreeOverlayIndex, worktreeItemsList.length || 1)];
|
|
3639
661
|
switchToWorktree(selectedOverlayItem?.id || '', { forceRefresh: true });
|
|
3640
662
|
setWorktreeOverlayOpen(false);
|
|
3641
663
|
return;
|
|
@@ -4070,6 +1092,13 @@ function createDashboardApp(deps) {
|
|
|
4070
1092
|
setPreviewScroll(0);
|
|
4071
1093
|
}, [previewOpen, overlayPreviewOpen, paneFocus, selectedFocusedFile?.path, previewTarget?.path]);
|
|
4072
1094
|
|
|
1095
|
+
useEffect(() => {
|
|
1096
|
+
if (ui.view !== 'git') {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
setPreviewScroll(0);
|
|
1100
|
+
}, [ui.view, focusedSection, selectedGitFile?.path, selectedGitCommit?.commitHash]);
|
|
1101
|
+
|
|
4073
1102
|
useEffect(() => {
|
|
4074
1103
|
if (statusLine === '') {
|
|
4075
1104
|
return undefined;
|
|
@@ -4235,8 +1264,8 @@ function createDashboardApp(deps) {
|
|
|
4235
1264
|
: [];
|
|
4236
1265
|
const gitInlineDiffTarget = (
|
|
4237
1266
|
focusedSection === 'git-commits'
|
|
4238
|
-
? (selectedGitCommit || selectedGitFile || firstGitFile
|
|
4239
|
-
: (selectedGitFile || firstGitFile
|
|
1267
|
+
? (selectedGitCommit || selectedGitFile || firstGitFile)
|
|
1268
|
+
: (selectedGitFile || firstGitFile)
|
|
4240
1269
|
) || null;
|
|
4241
1270
|
const gitInlineDiffLines = ui.view === 'git'
|
|
4242
1271
|
? buildPreviewLines(gitInlineDiffTarget, compactWidth, previewOpen && paneFocus === 'preview' ? previewScroll : 0, {
|