specsmd 0.1.26 → 0.1.28
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/aidlc/parser.js +581 -0
- package/lib/dashboard/index.js +80 -46
- package/lib/dashboard/simple/parser.js +293 -0
- package/lib/dashboard/tui/app.js +601 -48
- package/lib/dashboard/tui/store.js +11 -0
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
2
|
-
const { createInitialUIState, cycleView, cycleRunFilter } = require('./store');
|
|
2
|
+
const { createInitialUIState, cycleView, cycleViewBackward, cycleRunFilter } = require('./store');
|
|
3
3
|
|
|
4
4
|
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
5
5
|
if (!error) {
|
|
@@ -45,6 +45,37 @@ function safeJsonHash(value) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function resolveIconSet() {
|
|
49
|
+
const mode = (process.env.SPECSMD_ICON_SET || 'auto').toLowerCase();
|
|
50
|
+
|
|
51
|
+
const ascii = {
|
|
52
|
+
runs: '[R]',
|
|
53
|
+
overview: '[O]',
|
|
54
|
+
health: '[H]',
|
|
55
|
+
runFile: '*'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const nerd = {
|
|
59
|
+
runs: '',
|
|
60
|
+
overview: '',
|
|
61
|
+
health: '',
|
|
62
|
+
runFile: ''
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (mode === 'ascii') {
|
|
66
|
+
return ascii;
|
|
67
|
+
}
|
|
68
|
+
if (mode === 'nerd') {
|
|
69
|
+
return nerd;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const locale = `${process.env.LC_ALL || ''}${process.env.LC_CTYPE || ''}${process.env.LANG || ''}`;
|
|
73
|
+
const isUtf8 = /utf-?8/i.test(locale);
|
|
74
|
+
const looksLikeVsCodeTerminal = (process.env.TERM_PROGRAM || '').toLowerCase().includes('vscode');
|
|
75
|
+
|
|
76
|
+
return isUtf8 && looksLikeVsCodeTerminal ? nerd : ascii;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
function truncate(value, width) {
|
|
49
80
|
const text = String(value ?? '');
|
|
50
81
|
if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
|
|
@@ -83,18 +114,33 @@ function formatTime(value) {
|
|
|
83
114
|
return date.toLocaleTimeString();
|
|
84
115
|
}
|
|
85
116
|
|
|
86
|
-
function buildShortStats(snapshot) {
|
|
117
|
+
function buildShortStats(snapshot, flow) {
|
|
87
118
|
if (!snapshot?.initialized) {
|
|
119
|
+
if (flow === 'aidlc') {
|
|
120
|
+
return 'init: waiting for memory-bank scan';
|
|
121
|
+
}
|
|
122
|
+
if (flow === 'simple') {
|
|
123
|
+
return 'init: waiting for specs scan';
|
|
124
|
+
}
|
|
88
125
|
return 'init: waiting for state.yaml';
|
|
89
126
|
}
|
|
90
127
|
|
|
91
|
-
const stats = snapshot
|
|
92
|
-
|
|
128
|
+
const stats = snapshot?.stats || {};
|
|
129
|
+
|
|
130
|
+
if (flow === 'aidlc') {
|
|
131
|
+
return `bolts ${stats.activeBoltsCount || 0}/${stats.completedBolts || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | stories ${stats.completedStories || 0}/${stats.totalStories || 0}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (flow === 'simple') {
|
|
135
|
+
return `specs ${stats.completedSpecs || 0}/${stats.totalSpecs || 0} | tasks ${stats.completedTasks || 0}/${stats.totalTasks || 0} | active ${stats.activeSpecsCount || 0}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return `runs ${stats.activeRunsCount || 0}/${stats.completedRuns || 0} | intents ${stats.completedIntents || 0}/${stats.totalIntents || 0} | work ${stats.completedWorkItems || 0}/${stats.totalWorkItems || 0}`;
|
|
93
139
|
}
|
|
94
140
|
|
|
95
141
|
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
|
|
96
|
-
const projectName = snapshot?.project?.name || 'Unnamed
|
|
97
|
-
const shortStats = buildShortStats(snapshot);
|
|
142
|
+
const projectName = snapshot?.project?.name || 'Unnamed project';
|
|
143
|
+
const shortStats = buildShortStats(snapshot, flow);
|
|
98
144
|
|
|
99
145
|
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}/${runFilter} | ${formatTime(lastRefreshAt)}`;
|
|
100
146
|
|
|
@@ -121,31 +167,87 @@ function buildErrorLines(error, width) {
|
|
|
121
167
|
return lines.map((line) => truncate(line, width));
|
|
122
168
|
}
|
|
123
169
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
function getCurrentRun(snapshot) {
|
|
171
|
+
const activeRuns = Array.isArray(snapshot?.activeRuns) ? [...snapshot.activeRuns] : [];
|
|
172
|
+
if (activeRuns.length === 0) {
|
|
173
|
+
return null;
|
|
127
174
|
}
|
|
128
175
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
activeRuns.sort((a, b) => {
|
|
177
|
+
const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
|
|
178
|
+
const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
|
|
179
|
+
if (bTime !== aTime) {
|
|
180
|
+
return bTime - aTime;
|
|
181
|
+
}
|
|
182
|
+
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return activeRuns[0] || null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getCurrentPhaseLabel(run, currentWorkItem) {
|
|
189
|
+
const phase = currentWorkItem?.currentPhase || '';
|
|
190
|
+
if (typeof phase === 'string' && phase !== '') {
|
|
191
|
+
return phase.toLowerCase();
|
|
132
192
|
}
|
|
133
193
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
194
|
+
if (run?.hasTestReport) {
|
|
195
|
+
return 'review';
|
|
196
|
+
}
|
|
197
|
+
if (run?.hasPlan) {
|
|
198
|
+
return 'execute';
|
|
199
|
+
}
|
|
200
|
+
return 'plan';
|
|
201
|
+
}
|
|
140
202
|
|
|
141
|
-
|
|
142
|
-
|
|
203
|
+
function buildPhaseTrack(currentPhase) {
|
|
204
|
+
const order = ['plan', 'execute', 'test', 'review'];
|
|
205
|
+
const labels = ['P', 'E', 'T', 'R'];
|
|
206
|
+
const currentIndex = Math.max(0, order.indexOf(currentPhase));
|
|
207
|
+
return labels.map((label, index) => (index === currentIndex ? `[${label}]` : ` ${label} `)).join(' - ');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildFireCurrentRunLines(snapshot, width) {
|
|
211
|
+
const run = getCurrentRun(snapshot);
|
|
212
|
+
if (!run) {
|
|
213
|
+
return [truncate('No active run', width)];
|
|
143
214
|
}
|
|
144
215
|
|
|
216
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
217
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
218
|
+
const currentWorkItem = workItems.find((item) => item.id === run.currentItem) || workItems.find((item) => item.status === 'in_progress') || workItems[0];
|
|
219
|
+
|
|
220
|
+
const itemId = currentWorkItem?.id || run.currentItem || 'n/a';
|
|
221
|
+
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
222
|
+
const status = currentWorkItem?.status || 'pending';
|
|
223
|
+
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
224
|
+
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
225
|
+
|
|
226
|
+
const lines = [
|
|
227
|
+
`${run.id} [${run.scope}] ${completed}/${workItems.length} items done`,
|
|
228
|
+
`work item: ${itemId}`,
|
|
229
|
+
`mode: ${mode} | status: ${status}`,
|
|
230
|
+
`phase: ${phaseTrack}`
|
|
231
|
+
];
|
|
232
|
+
|
|
145
233
|
return lines.map((line) => truncate(line, width));
|
|
146
234
|
}
|
|
147
235
|
|
|
148
|
-
function
|
|
236
|
+
function buildFireRunFilesLines(snapshot, width, icons) {
|
|
237
|
+
const run = getCurrentRun(snapshot);
|
|
238
|
+
if (!run) {
|
|
239
|
+
return [truncate('No run files (no active run)', width)];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const files = ['run.md'];
|
|
243
|
+
if (run.hasPlan) files.push('plan.md');
|
|
244
|
+
if (run.hasTestReport) files.push('test-report.md');
|
|
245
|
+
if (run.hasWalkthrough) files.push('walkthrough.md');
|
|
246
|
+
|
|
247
|
+
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildFirePendingLines(snapshot, runFilter, width) {
|
|
149
251
|
if (runFilter === 'completed') {
|
|
150
252
|
return [truncate('Hidden by run filter: completed', width)];
|
|
151
253
|
}
|
|
@@ -161,7 +263,7 @@ function buildPendingLines(snapshot, runFilter, width) {
|
|
|
161
263
|
});
|
|
162
264
|
}
|
|
163
265
|
|
|
164
|
-
function
|
|
266
|
+
function buildFireCompletedLines(snapshot, runFilter, width) {
|
|
165
267
|
if (runFilter === 'active') {
|
|
166
268
|
return [truncate('Hidden by run filter: active', width)];
|
|
167
269
|
}
|
|
@@ -178,7 +280,7 @@ function buildCompletedLines(snapshot, runFilter, width) {
|
|
|
178
280
|
});
|
|
179
281
|
}
|
|
180
282
|
|
|
181
|
-
function
|
|
283
|
+
function buildFireStatsLines(snapshot, width) {
|
|
182
284
|
if (!snapshot?.initialized) {
|
|
183
285
|
return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
|
|
184
286
|
}
|
|
@@ -200,7 +302,7 @@ function buildWarningsLines(snapshot, width) {
|
|
|
200
302
|
return warnings.map((warning) => truncate(warning, width));
|
|
201
303
|
}
|
|
202
304
|
|
|
203
|
-
function
|
|
305
|
+
function buildFireOverviewProjectLines(snapshot, width) {
|
|
204
306
|
if (!snapshot?.initialized) {
|
|
205
307
|
return [
|
|
206
308
|
truncate('FIRE folder detected, but state.yaml is missing.', width),
|
|
@@ -218,7 +320,7 @@ function buildOverviewProjectLines(snapshot, width) {
|
|
|
218
320
|
].map((line) => truncate(line, width));
|
|
219
321
|
}
|
|
220
322
|
|
|
221
|
-
function
|
|
323
|
+
function buildFireOverviewIntentLines(snapshot, width) {
|
|
222
324
|
const intents = snapshot?.intents || [];
|
|
223
325
|
if (intents.length === 0) {
|
|
224
326
|
return [truncate('No intents found', width)];
|
|
@@ -231,7 +333,7 @@ function buildOverviewIntentLines(snapshot, width) {
|
|
|
231
333
|
});
|
|
232
334
|
}
|
|
233
335
|
|
|
234
|
-
function
|
|
336
|
+
function buildFireOverviewStandardsLines(snapshot, width) {
|
|
235
337
|
const expected = ['constitution', 'tech-stack', 'coding-standards', 'testing-standards', 'system-architecture'];
|
|
236
338
|
const actual = new Set((snapshot?.standards || []).map((item) => item.type));
|
|
237
339
|
|
|
@@ -241,6 +343,435 @@ function buildOverviewStandardsLines(snapshot, width) {
|
|
|
241
343
|
});
|
|
242
344
|
}
|
|
243
345
|
|
|
346
|
+
function getEffectiveFlow(flow, snapshot) {
|
|
347
|
+
const explicitFlow = typeof flow === 'string' && flow !== '' ? flow : null;
|
|
348
|
+
const snapshotFlow = typeof snapshot?.flow === 'string' && snapshot.flow !== '' ? snapshot.flow : null;
|
|
349
|
+
return (snapshotFlow || explicitFlow || 'fire').toLowerCase();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getCurrentBolt(snapshot) {
|
|
353
|
+
const activeBolts = Array.isArray(snapshot?.activeBolts) ? [...snapshot.activeBolts] : [];
|
|
354
|
+
if (activeBolts.length === 0) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
activeBolts.sort((a, b) => {
|
|
359
|
+
const aTime = a?.startedAt ? Date.parse(a.startedAt) : 0;
|
|
360
|
+
const bTime = b?.startedAt ? Date.parse(b.startedAt) : 0;
|
|
361
|
+
if (bTime !== aTime) {
|
|
362
|
+
return bTime - aTime;
|
|
363
|
+
}
|
|
364
|
+
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return activeBolts[0] || null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildAidlcStageTrack(bolt) {
|
|
371
|
+
const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
|
|
372
|
+
if (stages.length === 0) {
|
|
373
|
+
return 'n/a';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return stages.map((stage) => {
|
|
377
|
+
const label = String(stage?.name || '?').charAt(0).toUpperCase();
|
|
378
|
+
if (stage?.status === 'completed') {
|
|
379
|
+
return `[${label}]`;
|
|
380
|
+
}
|
|
381
|
+
if (stage?.status === 'in_progress') {
|
|
382
|
+
return `<${label}>`;
|
|
383
|
+
}
|
|
384
|
+
return ` ${label} `;
|
|
385
|
+
}).join('-');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildAidlcCurrentRunLines(snapshot, width) {
|
|
389
|
+
const bolt = getCurrentBolt(snapshot);
|
|
390
|
+
if (!bolt) {
|
|
391
|
+
return [truncate('No active bolt', width)];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
395
|
+
const completedStages = stages.filter((stage) => stage.status === 'completed').length;
|
|
396
|
+
const phaseTrack = buildAidlcStageTrack(bolt);
|
|
397
|
+
const location = `${bolt.intent || 'unknown-intent'} / ${bolt.unit || 'unknown-unit'}`;
|
|
398
|
+
|
|
399
|
+
const lines = [
|
|
400
|
+
`${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages done`,
|
|
401
|
+
`scope: ${location}`,
|
|
402
|
+
`stage: ${bolt.currentStage || 'n/a'} | status: ${bolt.status}`,
|
|
403
|
+
`phase: ${phaseTrack}`
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
return lines.map((line) => truncate(line, width));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildAidlcRunFilesLines(snapshot, width, icons) {
|
|
410
|
+
const bolt = getCurrentBolt(snapshot);
|
|
411
|
+
if (!bolt) {
|
|
412
|
+
return [truncate('No bolt files (no active bolt)', width)];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const files = Array.isArray(bolt.files) ? bolt.files : [];
|
|
416
|
+
if (files.length === 0) {
|
|
417
|
+
return [truncate('No markdown files found in active bolt', width)];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildAidlcPendingLines(snapshot, runFilter, width) {
|
|
424
|
+
if (runFilter === 'completed') {
|
|
425
|
+
return [truncate('Hidden by run filter: completed', width)];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
429
|
+
if (pendingBolts.length === 0) {
|
|
430
|
+
return [truncate('No queued bolts', width)];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return pendingBolts.map((bolt) => {
|
|
434
|
+
const deps = Array.isArray(bolt.blockedBy) && bolt.blockedBy.length > 0
|
|
435
|
+
? ` blocked_by:${bolt.blockedBy.join(',')}`
|
|
436
|
+
: '';
|
|
437
|
+
const location = `${bolt.intent || 'unknown'}/${bolt.unit || 'unknown'}`;
|
|
438
|
+
return truncate(`${bolt.id} (${bolt.status}) in ${location}${deps}`, width);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildAidlcCompletedLines(snapshot, runFilter, width) {
|
|
443
|
+
if (runFilter === 'active') {
|
|
444
|
+
return [truncate('Hidden by run filter: active', width)];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
448
|
+
if (completedBolts.length === 0) {
|
|
449
|
+
return [truncate('No completed bolts yet', width)];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return completedBolts.map((bolt) =>
|
|
453
|
+
truncate(`${bolt.id} [${bolt.type}] done at ${bolt.completedAt || 'unknown'}`, width)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function buildAidlcStatsLines(snapshot, width) {
|
|
458
|
+
const stats = snapshot?.stats || {};
|
|
459
|
+
|
|
460
|
+
return [
|
|
461
|
+
`intents: ${stats.completedIntents || 0}/${stats.totalIntents || 0} done | in_progress: ${stats.inProgressIntents || 0} | blocked: ${stats.blockedIntents || 0}`,
|
|
462
|
+
`stories: ${stats.completedStories || 0}/${stats.totalStories || 0} done | in_progress: ${stats.inProgressStories || 0} | pending: ${stats.pendingStories || 0} | blocked: ${stats.blockedStories || 0}`,
|
|
463
|
+
`bolts: ${stats.activeBoltsCount || 0} active | ${stats.queuedBolts || 0} queued | ${stats.blockedBolts || 0} blocked | ${stats.completedBolts || 0} done`
|
|
464
|
+
].map((line) => truncate(line, width));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function buildAidlcOverviewProjectLines(snapshot, width) {
|
|
468
|
+
const project = snapshot?.project || {};
|
|
469
|
+
const stats = snapshot?.stats || {};
|
|
470
|
+
|
|
471
|
+
return [
|
|
472
|
+
`project: ${project.name || 'unknown'} | project_type: ${project.projectType || 'unknown'}`,
|
|
473
|
+
`memory-bank: intents ${stats.totalIntents || 0} | units ${stats.totalUnits || 0} | stories ${stats.totalStories || 0}`,
|
|
474
|
+
`progress: ${stats.progressPercent || 0}% stories complete | standards: ${(snapshot?.standards || []).length}`
|
|
475
|
+
].map((line) => truncate(line, width));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildAidlcOverviewIntentLines(snapshot, width) {
|
|
479
|
+
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
480
|
+
if (intents.length === 0) {
|
|
481
|
+
return [truncate('No intents found', width)];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return intents.map((intent) => {
|
|
485
|
+
return truncate(
|
|
486
|
+
`${intent.id}: ${intent.status} (${intent.completedStories || 0}/${intent.storyCount || 0} stories, ${intent.completedUnits || 0}/${intent.unitCount || 0} units)`,
|
|
487
|
+
width
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function buildAidlcOverviewStandardsLines(snapshot, width) {
|
|
493
|
+
const standards = Array.isArray(snapshot?.standards) ? snapshot.standards : [];
|
|
494
|
+
if (standards.length === 0) {
|
|
495
|
+
return [truncate('No standards found under memory-bank/standards', width)];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return standards.map((standard) =>
|
|
499
|
+
truncate(`[x] ${standard.name || standard.type || 'unknown'}.md`, width)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function getCurrentSpec(snapshot) {
|
|
504
|
+
const specs = Array.isArray(snapshot?.activeSpecs) ? snapshot.activeSpecs : [];
|
|
505
|
+
if (specs.length === 0) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return specs[0] || null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function simplePhaseIndex(state) {
|
|
512
|
+
if (state === 'requirements_pending') {
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
if (state === 'design_pending') {
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
return 2;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function buildSimplePhaseTrack(spec) {
|
|
522
|
+
if (spec?.state === 'completed') {
|
|
523
|
+
return '[R] - [D] - [T]';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const labels = ['R', 'D', 'T'];
|
|
527
|
+
const current = simplePhaseIndex(spec?.state);
|
|
528
|
+
return labels.map((label, index) => (index === current ? `[${label}]` : ` ${label} `)).join(' - ');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function buildSimpleCurrentRunLines(snapshot, width) {
|
|
532
|
+
const spec = getCurrentSpec(snapshot);
|
|
533
|
+
if (!spec) {
|
|
534
|
+
return [truncate('No active spec', width)];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const files = [
|
|
538
|
+
spec.hasRequirements ? 'req' : '-',
|
|
539
|
+
spec.hasDesign ? 'design' : '-',
|
|
540
|
+
spec.hasTasks ? 'tasks' : '-'
|
|
541
|
+
].join('/');
|
|
542
|
+
|
|
543
|
+
const lines = [
|
|
544
|
+
`${spec.name} [${spec.state}] ${spec.tasksCompleted}/${spec.tasksTotal} tasks done`,
|
|
545
|
+
`phase: ${spec.phase}`,
|
|
546
|
+
`files: ${files}`,
|
|
547
|
+
`track: ${buildSimplePhaseTrack(spec)}`
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
return lines.map((line) => truncate(line, width));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function buildSimpleRunFilesLines(snapshot, width, icons) {
|
|
554
|
+
const spec = getCurrentSpec(snapshot);
|
|
555
|
+
if (!spec) {
|
|
556
|
+
return [truncate('No spec files (no active spec)', width)];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const files = [];
|
|
560
|
+
if (spec.hasRequirements) files.push('requirements.md');
|
|
561
|
+
if (spec.hasDesign) files.push('design.md');
|
|
562
|
+
if (spec.hasTasks) files.push('tasks.md');
|
|
563
|
+
|
|
564
|
+
if (files.length === 0) {
|
|
565
|
+
return [truncate('No files found in active spec folder', width)];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return files.map((file) => truncate(`${icons.runFile} ${file}`, width));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function buildSimplePendingLines(snapshot, runFilter, width) {
|
|
572
|
+
if (runFilter === 'completed') {
|
|
573
|
+
return [truncate('Hidden by run filter: completed', width)];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
577
|
+
if (pendingSpecs.length === 0) {
|
|
578
|
+
return [truncate('No pending specs', width)];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return pendingSpecs.map((spec) =>
|
|
582
|
+
truncate(`${spec.name} (${spec.state}) ${spec.tasksCompleted}/${spec.tasksTotal} tasks`, width)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function buildSimpleCompletedLines(snapshot, runFilter, width) {
|
|
587
|
+
if (runFilter === 'active') {
|
|
588
|
+
return [truncate('Hidden by run filter: active', width)];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
592
|
+
if (completedSpecs.length === 0) {
|
|
593
|
+
return [truncate('No completed specs yet', width)];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return completedSpecs.map((spec) =>
|
|
597
|
+
truncate(`${spec.name} done at ${spec.updatedAt || 'unknown'} (${spec.tasksCompleted}/${spec.tasksTotal})`, width)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function buildSimpleStatsLines(snapshot, width) {
|
|
602
|
+
const stats = snapshot?.stats || {};
|
|
603
|
+
|
|
604
|
+
return [
|
|
605
|
+
`specs: ${stats.completedSpecs || 0}/${stats.totalSpecs || 0} complete | in_progress: ${stats.inProgressSpecs || 0} | pending: ${stats.pendingSpecs || 0}`,
|
|
606
|
+
`pipeline: ready ${stats.readySpecs || 0} | design_pending ${stats.designPendingSpecs || 0} | tasks_pending ${stats.tasksPendingSpecs || 0}`,
|
|
607
|
+
`tasks: ${stats.completedTasks || 0}/${stats.totalTasks || 0} complete | pending: ${stats.pendingTasks || 0} | optional: ${stats.optionalTasks || 0}`
|
|
608
|
+
].map((line) => truncate(line, width));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function buildSimpleOverviewProjectLines(snapshot, width) {
|
|
612
|
+
const project = snapshot?.project || {};
|
|
613
|
+
const stats = snapshot?.stats || {};
|
|
614
|
+
|
|
615
|
+
return [
|
|
616
|
+
`project: ${project.name || 'unknown'} | simple flow`,
|
|
617
|
+
`specs: ${stats.totalSpecs || 0} total | active: ${stats.activeSpecsCount || 0} | completed: ${stats.completedSpecs || 0}`,
|
|
618
|
+
`tasks: ${stats.completedTasks || 0}/${stats.totalTasks || 0} complete (${stats.progressPercent || 0}%)`
|
|
619
|
+
].map((line) => truncate(line, width));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function buildSimpleOverviewIntentLines(snapshot, width) {
|
|
623
|
+
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
624
|
+
if (specs.length === 0) {
|
|
625
|
+
return [truncate('No specs found', width)];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return specs.map((spec) =>
|
|
629
|
+
truncate(`${spec.name}: ${spec.state} (${spec.tasksCompleted}/${spec.tasksTotal} tasks)`, width)
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function buildSimpleOverviewStandardsLines(snapshot, width) {
|
|
634
|
+
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
635
|
+
if (specs.length === 0) {
|
|
636
|
+
return [truncate('No spec artifacts found', width)];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const reqCount = specs.filter((spec) => spec.hasRequirements).length;
|
|
640
|
+
const designCount = specs.filter((spec) => spec.hasDesign).length;
|
|
641
|
+
const tasksCount = specs.filter((spec) => spec.hasTasks).length;
|
|
642
|
+
const total = specs.length;
|
|
643
|
+
|
|
644
|
+
return [
|
|
645
|
+
`[x] requirements.md coverage ${reqCount}/${total}`,
|
|
646
|
+
`[x] design.md coverage ${designCount}/${total}`,
|
|
647
|
+
`[x] tasks.md coverage ${tasksCount}/${total}`
|
|
648
|
+
].map((line) => truncate(line, width));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function buildCurrentRunLines(snapshot, width, flow) {
|
|
652
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
653
|
+
if (effectiveFlow === 'aidlc') {
|
|
654
|
+
return buildAidlcCurrentRunLines(snapshot, width);
|
|
655
|
+
}
|
|
656
|
+
if (effectiveFlow === 'simple') {
|
|
657
|
+
return buildSimpleCurrentRunLines(snapshot, width);
|
|
658
|
+
}
|
|
659
|
+
return buildFireCurrentRunLines(snapshot, width);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function buildRunFilesLines(snapshot, width, icons, flow) {
|
|
663
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
664
|
+
if (effectiveFlow === 'aidlc') {
|
|
665
|
+
return buildAidlcRunFilesLines(snapshot, width, icons);
|
|
666
|
+
}
|
|
667
|
+
if (effectiveFlow === 'simple') {
|
|
668
|
+
return buildSimpleRunFilesLines(snapshot, width, icons);
|
|
669
|
+
}
|
|
670
|
+
return buildFireRunFilesLines(snapshot, width, icons);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function buildPendingLines(snapshot, runFilter, width, flow) {
|
|
674
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
675
|
+
if (effectiveFlow === 'aidlc') {
|
|
676
|
+
return buildAidlcPendingLines(snapshot, runFilter, width);
|
|
677
|
+
}
|
|
678
|
+
if (effectiveFlow === 'simple') {
|
|
679
|
+
return buildSimplePendingLines(snapshot, runFilter, width);
|
|
680
|
+
}
|
|
681
|
+
return buildFirePendingLines(snapshot, runFilter, width);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function buildCompletedLines(snapshot, runFilter, width, flow) {
|
|
685
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
686
|
+
if (effectiveFlow === 'aidlc') {
|
|
687
|
+
return buildAidlcCompletedLines(snapshot, runFilter, width);
|
|
688
|
+
}
|
|
689
|
+
if (effectiveFlow === 'simple') {
|
|
690
|
+
return buildSimpleCompletedLines(snapshot, runFilter, width);
|
|
691
|
+
}
|
|
692
|
+
return buildFireCompletedLines(snapshot, runFilter, width);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function buildStatsLines(snapshot, width, flow) {
|
|
696
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
697
|
+
if (!snapshot?.initialized) {
|
|
698
|
+
if (effectiveFlow === 'aidlc') {
|
|
699
|
+
return [truncate('Waiting for memory-bank initialization.', width)];
|
|
700
|
+
}
|
|
701
|
+
if (effectiveFlow === 'simple') {
|
|
702
|
+
return [truncate('Waiting for specs/ initialization.', width)];
|
|
703
|
+
}
|
|
704
|
+
return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (effectiveFlow === 'aidlc') {
|
|
708
|
+
return buildAidlcStatsLines(snapshot, width);
|
|
709
|
+
}
|
|
710
|
+
if (effectiveFlow === 'simple') {
|
|
711
|
+
return buildSimpleStatsLines(snapshot, width);
|
|
712
|
+
}
|
|
713
|
+
return buildFireStatsLines(snapshot, width);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function buildOverviewProjectLines(snapshot, width, flow) {
|
|
717
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
718
|
+
if (effectiveFlow === 'aidlc') {
|
|
719
|
+
return buildAidlcOverviewProjectLines(snapshot, width);
|
|
720
|
+
}
|
|
721
|
+
if (effectiveFlow === 'simple') {
|
|
722
|
+
return buildSimpleOverviewProjectLines(snapshot, width);
|
|
723
|
+
}
|
|
724
|
+
return buildFireOverviewProjectLines(snapshot, width);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function buildOverviewIntentLines(snapshot, width, flow) {
|
|
728
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
729
|
+
if (effectiveFlow === 'aidlc') {
|
|
730
|
+
return buildAidlcOverviewIntentLines(snapshot, width);
|
|
731
|
+
}
|
|
732
|
+
if (effectiveFlow === 'simple') {
|
|
733
|
+
return buildSimpleOverviewIntentLines(snapshot, width);
|
|
734
|
+
}
|
|
735
|
+
return buildFireOverviewIntentLines(snapshot, width);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function buildOverviewStandardsLines(snapshot, width, flow) {
|
|
739
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
740
|
+
if (effectiveFlow === 'aidlc') {
|
|
741
|
+
return buildAidlcOverviewStandardsLines(snapshot, width);
|
|
742
|
+
}
|
|
743
|
+
if (effectiveFlow === 'simple') {
|
|
744
|
+
return buildSimpleOverviewStandardsLines(snapshot, width);
|
|
745
|
+
}
|
|
746
|
+
return buildFireOverviewStandardsLines(snapshot, width);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function getPanelTitles(flow, snapshot) {
|
|
750
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
751
|
+
if (effectiveFlow === 'aidlc') {
|
|
752
|
+
return {
|
|
753
|
+
current: 'Current Bolt',
|
|
754
|
+
files: 'Bolt Files',
|
|
755
|
+
pending: 'Queued Bolts',
|
|
756
|
+
completed: 'Recent Completed Bolts'
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (effectiveFlow === 'simple') {
|
|
760
|
+
return {
|
|
761
|
+
current: 'Current Spec',
|
|
762
|
+
files: 'Spec Files',
|
|
763
|
+
pending: 'Pending Specs',
|
|
764
|
+
completed: 'Recent Completed Specs'
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
current: 'Current Run',
|
|
769
|
+
files: 'Run Files',
|
|
770
|
+
pending: 'Pending Queue',
|
|
771
|
+
completed: 'Recent Completed Runs'
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
244
775
|
function allocateSingleColumnPanels(candidates, rowsBudget) {
|
|
245
776
|
const filtered = (candidates || []).filter(Boolean);
|
|
246
777
|
if (filtered.length === 0) {
|
|
@@ -298,7 +829,8 @@ function createDashboardApp(deps) {
|
|
|
298
829
|
width,
|
|
299
830
|
maxLines,
|
|
300
831
|
borderColor,
|
|
301
|
-
marginBottom
|
|
832
|
+
marginBottom,
|
|
833
|
+
dense
|
|
302
834
|
} = props;
|
|
303
835
|
|
|
304
836
|
const contentWidth = Math.max(18, width - 4);
|
|
@@ -308,9 +840,9 @@ function createDashboardApp(deps) {
|
|
|
308
840
|
Box,
|
|
309
841
|
{
|
|
310
842
|
flexDirection: 'column',
|
|
311
|
-
borderStyle: 'round',
|
|
843
|
+
borderStyle: dense ? 'single' : 'round',
|
|
312
844
|
borderColor: borderColor || 'gray',
|
|
313
|
-
paddingX: 1,
|
|
845
|
+
paddingX: dense ? 0 : 1,
|
|
314
846
|
width,
|
|
315
847
|
marginBottom: marginBottom || 0
|
|
316
848
|
},
|
|
@@ -320,11 +852,11 @@ function createDashboardApp(deps) {
|
|
|
320
852
|
}
|
|
321
853
|
|
|
322
854
|
function TabsBar(props) {
|
|
323
|
-
const { view, width } = props;
|
|
855
|
+
const { view, width, icons } = props;
|
|
324
856
|
const tabs = [
|
|
325
|
-
{ id: 'runs', label:
|
|
326
|
-
{ id: 'overview', label:
|
|
327
|
-
{ id: 'health', label:
|
|
857
|
+
{ id: 'runs', label: ` 1 ${icons.runs} RUNS ` },
|
|
858
|
+
{ id: 'overview', label: ` 2 ${icons.overview} OVERVIEW ` },
|
|
859
|
+
{ id: 'health', label: ` 3 ${icons.health} HEALTH ` }
|
|
328
860
|
];
|
|
329
861
|
|
|
330
862
|
return React.createElement(
|
|
@@ -363,6 +895,7 @@ function createDashboardApp(deps) {
|
|
|
363
895
|
columns: stdout?.columns || process.stdout.columns || 120,
|
|
364
896
|
rows: stdout?.rows || process.stdout.rows || 40
|
|
365
897
|
}));
|
|
898
|
+
const icons = resolveIconSet();
|
|
366
899
|
|
|
367
900
|
const refresh = useCallback(async () => {
|
|
368
901
|
const now = new Date().toISOString();
|
|
@@ -447,6 +980,16 @@ function createDashboardApp(deps) {
|
|
|
447
980
|
return;
|
|
448
981
|
}
|
|
449
982
|
|
|
983
|
+
if (key.rightArrow) {
|
|
984
|
+
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (key.leftArrow) {
|
|
989
|
+
setUi((previous) => ({ ...previous, view: cycleViewBackward(previous.view) }));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
450
993
|
if (input === 'f') {
|
|
451
994
|
setUi((previous) => ({ ...previous, runFilter: cycleRunFilter(previous.runFilter) }));
|
|
452
995
|
}
|
|
@@ -530,10 +1073,12 @@ function createDashboardApp(deps) {
|
|
|
530
1073
|
const showHelpLine = ui.showHelp && rows >= 14;
|
|
531
1074
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
532
1075
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
1076
|
+
const densePanels = rows <= 28 || cols <= 120;
|
|
533
1077
|
|
|
534
1078
|
const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
|
|
535
1079
|
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
536
1080
|
const ultraCompact = rows <= 14;
|
|
1081
|
+
const panelTitles = getPanelTitles(flow, snapshot);
|
|
537
1082
|
|
|
538
1083
|
let panelCandidates;
|
|
539
1084
|
if (ui.view === 'overview') {
|
|
@@ -541,19 +1086,19 @@ function createDashboardApp(deps) {
|
|
|
541
1086
|
{
|
|
542
1087
|
key: 'project',
|
|
543
1088
|
title: 'Project + Workspace',
|
|
544
|
-
lines: buildOverviewProjectLines(snapshot, compactWidth),
|
|
1089
|
+
lines: buildOverviewProjectLines(snapshot, compactWidth, flow),
|
|
545
1090
|
borderColor: 'green'
|
|
546
1091
|
},
|
|
547
1092
|
{
|
|
548
1093
|
key: 'intent-status',
|
|
549
1094
|
title: 'Intent Status',
|
|
550
|
-
lines: buildOverviewIntentLines(snapshot, compactWidth),
|
|
1095
|
+
lines: buildOverviewIntentLines(snapshot, compactWidth, flow),
|
|
551
1096
|
borderColor: 'yellow'
|
|
552
1097
|
},
|
|
553
1098
|
{
|
|
554
1099
|
key: 'standards',
|
|
555
1100
|
title: 'Standards',
|
|
556
|
-
lines: buildOverviewStandardsLines(snapshot, compactWidth),
|
|
1101
|
+
lines: buildOverviewStandardsLines(snapshot, compactWidth, flow),
|
|
557
1102
|
borderColor: 'blue'
|
|
558
1103
|
}
|
|
559
1104
|
];
|
|
@@ -562,7 +1107,7 @@ function createDashboardApp(deps) {
|
|
|
562
1107
|
{
|
|
563
1108
|
key: 'stats',
|
|
564
1109
|
title: 'Stats',
|
|
565
|
-
lines: buildStatsLines(snapshot, compactWidth),
|
|
1110
|
+
lines: buildStatsLines(snapshot, compactWidth, flow),
|
|
566
1111
|
borderColor: 'magenta'
|
|
567
1112
|
},
|
|
568
1113
|
{
|
|
@@ -584,21 +1129,27 @@ function createDashboardApp(deps) {
|
|
|
584
1129
|
} else {
|
|
585
1130
|
panelCandidates = [
|
|
586
1131
|
{
|
|
587
|
-
key: '
|
|
588
|
-
title:
|
|
589
|
-
lines:
|
|
1132
|
+
key: 'current-run',
|
|
1133
|
+
title: panelTitles.current,
|
|
1134
|
+
lines: buildCurrentRunLines(snapshot, compactWidth, flow),
|
|
590
1135
|
borderColor: 'green'
|
|
591
1136
|
},
|
|
1137
|
+
{
|
|
1138
|
+
key: 'run-files',
|
|
1139
|
+
title: panelTitles.files,
|
|
1140
|
+
lines: buildRunFilesLines(snapshot, compactWidth, icons, flow),
|
|
1141
|
+
borderColor: 'yellow'
|
|
1142
|
+
},
|
|
592
1143
|
{
|
|
593
1144
|
key: 'pending',
|
|
594
|
-
title:
|
|
595
|
-
lines: buildPendingLines(snapshot, ui.runFilter, compactWidth),
|
|
1145
|
+
title: panelTitles.pending,
|
|
1146
|
+
lines: buildPendingLines(snapshot, ui.runFilter, compactWidth, flow),
|
|
596
1147
|
borderColor: 'yellow'
|
|
597
1148
|
},
|
|
598
1149
|
{
|
|
599
1150
|
key: 'completed',
|
|
600
|
-
title:
|
|
601
|
-
lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth),
|
|
1151
|
+
title: panelTitles.completed,
|
|
1152
|
+
lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth, flow),
|
|
602
1153
|
borderColor: 'blue'
|
|
603
1154
|
}
|
|
604
1155
|
];
|
|
@@ -610,13 +1161,13 @@ function createDashboardApp(deps) {
|
|
|
610
1161
|
|
|
611
1162
|
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
612
1163
|
|
|
613
|
-
const helpText = 'q quit | r refresh | h/? help | tab
|
|
1164
|
+
const helpText = 'q quit | r refresh | h/? help | ←/→ or tab switch views | 1 runs | 2 overview | 3 health | f run filter';
|
|
614
1165
|
|
|
615
1166
|
return React.createElement(
|
|
616
1167
|
Box,
|
|
617
1168
|
{ flexDirection: 'column', width: fullWidth },
|
|
618
1169
|
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, ui.view, ui.runFilter, fullWidth)),
|
|
619
|
-
React.createElement(TabsBar, { view: ui.view, width: fullWidth }),
|
|
1170
|
+
React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons }),
|
|
620
1171
|
showErrorInline
|
|
621
1172
|
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
622
1173
|
: null,
|
|
@@ -627,7 +1178,8 @@ function createDashboardApp(deps) {
|
|
|
627
1178
|
width: fullWidth,
|
|
628
1179
|
maxLines: 2,
|
|
629
1180
|
borderColor: 'red',
|
|
630
|
-
marginBottom: 1
|
|
1181
|
+
marginBottom: densePanels ? 0 : 1,
|
|
1182
|
+
dense: densePanels
|
|
631
1183
|
})
|
|
632
1184
|
: null,
|
|
633
1185
|
...panels.map((panel, index) => React.createElement(SectionPanel, {
|
|
@@ -637,7 +1189,8 @@ function createDashboardApp(deps) {
|
|
|
637
1189
|
width: fullWidth,
|
|
638
1190
|
maxLines: panel.maxLines,
|
|
639
1191
|
borderColor: panel.borderColor,
|
|
640
|
-
marginBottom: index === panels.length - 1 ? 0 : 1
|
|
1192
|
+
marginBottom: densePanels ? 0 : (index === panels.length - 1 ? 0 : 1),
|
|
1193
|
+
dense: densePanels
|
|
641
1194
|
})),
|
|
642
1195
|
showHelpLine
|
|
643
1196
|
? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
|