specsmd 0.1.24 → 0.1.26
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 +514 -31
- package/lib/dashboard/tui/store.js +3 -0
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
2
2
|
const { createInitialUIState, cycleView, cycleRunFilter } = require('./store');
|
|
3
|
-
const { formatDashboardText } = require('./renderer');
|
|
4
3
|
|
|
5
4
|
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
6
5
|
if (!error) {
|
|
@@ -33,6 +32,248 @@ function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
function safeJsonHash(value) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.stringify(value, (key, nestedValue) => {
|
|
38
|
+
if (key === 'generatedAt') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return nestedValue;
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function truncate(value, width) {
|
|
49
|
+
const text = String(value ?? '');
|
|
50
|
+
if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (width <= 3) {
|
|
55
|
+
return text.slice(0, width);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `${text.slice(0, width - 3)}...`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fitLines(lines, maxLines, width) {
|
|
62
|
+
const safeLines = (Array.isArray(lines) ? lines : []).map((line) => truncate(line, width));
|
|
63
|
+
|
|
64
|
+
if (safeLines.length <= maxLines) {
|
|
65
|
+
return safeLines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
69
|
+
visible.push(truncate(`... +${safeLines.length - visible.length} more`, width));
|
|
70
|
+
return visible;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatTime(value) {
|
|
74
|
+
if (!value) {
|
|
75
|
+
return 'n/a';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const date = new Date(value);
|
|
79
|
+
if (Number.isNaN(date.getTime())) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return date.toLocaleTimeString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildShortStats(snapshot) {
|
|
87
|
+
if (!snapshot?.initialized) {
|
|
88
|
+
return 'init: waiting for state.yaml';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stats = snapshot.stats;
|
|
92
|
+
return `runs ${stats.activeRunsCount}/${stats.completedRuns} | intents ${stats.completedIntents}/${stats.totalIntents} | work ${stats.completedWorkItems}/${stats.totalWorkItems}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildHeaderLine(snapshot, flow, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
|
|
96
|
+
const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
|
|
97
|
+
const shortStats = buildShortStats(snapshot);
|
|
98
|
+
|
|
99
|
+
const line = `${flow.toUpperCase()} | ${projectName} | ${shortStats} | watch:${watchEnabled ? watchStatus : 'off'} | ${view}/${runFilter} | ${formatTime(lastRefreshAt)}`;
|
|
100
|
+
|
|
101
|
+
return truncate(line, width);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildErrorLines(error, width) {
|
|
105
|
+
if (!error) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = [`[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`];
|
|
110
|
+
|
|
111
|
+
if (error.details) {
|
|
112
|
+
lines.push(`details: ${error.details}`);
|
|
113
|
+
}
|
|
114
|
+
if (error.path) {
|
|
115
|
+
lines.push(`path: ${error.path}`);
|
|
116
|
+
}
|
|
117
|
+
if (error.hint) {
|
|
118
|
+
lines.push(`hint: ${error.hint}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return lines.map((line) => truncate(line, width));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildActiveRunLines(snapshot, runFilter, width) {
|
|
125
|
+
if (runFilter === 'completed') {
|
|
126
|
+
return [truncate('Hidden by run filter: completed', width)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const activeRuns = snapshot?.activeRuns || [];
|
|
130
|
+
if (activeRuns.length === 0) {
|
|
131
|
+
return [truncate('No active runs', width)];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lines = [];
|
|
135
|
+
for (const run of activeRuns) {
|
|
136
|
+
const currentItem = run.currentItem || 'n/a';
|
|
137
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
138
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
139
|
+
const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
|
|
140
|
+
|
|
141
|
+
lines.push(`${run.id} [${run.scope}] current: ${currentItem}`);
|
|
142
|
+
lines.push(`progress: ${completed}/${workItems.length} done, ${inProgress} active`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return lines.map((line) => truncate(line, width));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildPendingLines(snapshot, runFilter, width) {
|
|
149
|
+
if (runFilter === 'completed') {
|
|
150
|
+
return [truncate('Hidden by run filter: completed', width)];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pending = snapshot?.pendingItems || [];
|
|
154
|
+
if (pending.length === 0) {
|
|
155
|
+
return [truncate('No pending work items', width)];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return pending.map((item) => {
|
|
159
|
+
const deps = item.dependencies && item.dependencies.length > 0 ? ` deps:${item.dependencies.join(',')}` : '';
|
|
160
|
+
return truncate(`${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`, width);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildCompletedLines(snapshot, runFilter, width) {
|
|
165
|
+
if (runFilter === 'active') {
|
|
166
|
+
return [truncate('Hidden by run filter: active', width)];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const completedRuns = snapshot?.completedRuns || [];
|
|
170
|
+
if (completedRuns.length === 0) {
|
|
171
|
+
return [truncate('No completed runs yet', width)];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return completedRuns.map((run) => {
|
|
175
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
176
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
177
|
+
return truncate(`${run.id} [${run.scope}] ${completed}/${workItems.length} done at ${run.completedAt || 'unknown'}`, width);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildStatsLines(snapshot, width) {
|
|
182
|
+
if (!snapshot?.initialized) {
|
|
183
|
+
return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const stats = snapshot.stats;
|
|
187
|
+
return [
|
|
188
|
+
`intents: ${stats.completedIntents}/${stats.totalIntents} done | in_progress: ${stats.inProgressIntents} | blocked: ${stats.blockedIntents}`,
|
|
189
|
+
`work items: ${stats.completedWorkItems}/${stats.totalWorkItems} done | in_progress: ${stats.inProgressWorkItems} | pending: ${stats.pendingWorkItems} | blocked: ${stats.blockedWorkItems}`,
|
|
190
|
+
`runs: ${stats.activeRunsCount} active | ${stats.completedRuns} completed | ${stats.totalRuns} total`
|
|
191
|
+
].map((line) => truncate(line, width));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildWarningsLines(snapshot, width) {
|
|
195
|
+
const warnings = snapshot?.warnings || [];
|
|
196
|
+
if (warnings.length === 0) {
|
|
197
|
+
return [truncate('No warnings', width)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return warnings.map((warning) => truncate(warning, width));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildOverviewProjectLines(snapshot, width) {
|
|
204
|
+
if (!snapshot?.initialized) {
|
|
205
|
+
return [
|
|
206
|
+
truncate('FIRE folder detected, but state.yaml is missing.', width),
|
|
207
|
+
truncate('Initialize project context and this view will populate.', width)
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const project = snapshot.project || {};
|
|
212
|
+
const workspace = snapshot.workspace || {};
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
`project: ${project.name || 'unknown'} | fire_version: ${project.fireVersion || snapshot.version || '0.0.0'}`,
|
|
216
|
+
`workspace: ${workspace.type || 'unknown'} / ${workspace.structure || 'unknown'}`,
|
|
217
|
+
`autonomy: ${workspace.autonomyBias || 'unknown'} | run scope pref: ${workspace.runScopePreference || 'unknown'}`
|
|
218
|
+
].map((line) => truncate(line, width));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildOverviewIntentLines(snapshot, width) {
|
|
222
|
+
const intents = snapshot?.intents || [];
|
|
223
|
+
if (intents.length === 0) {
|
|
224
|
+
return [truncate('No intents found', width)];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return intents.map((intent) => {
|
|
228
|
+
const workItems = Array.isArray(intent.workItems) ? intent.workItems : [];
|
|
229
|
+
const done = workItems.filter((item) => item.status === 'completed').length;
|
|
230
|
+
return truncate(`${intent.id}: ${intent.status} (${done}/${workItems.length} work items)`, width);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildOverviewStandardsLines(snapshot, width) {
|
|
235
|
+
const expected = ['constitution', 'tech-stack', 'coding-standards', 'testing-standards', 'system-architecture'];
|
|
236
|
+
const actual = new Set((snapshot?.standards || []).map((item) => item.type));
|
|
237
|
+
|
|
238
|
+
return expected.map((name) => {
|
|
239
|
+
const marker = actual.has(name) ? '[x]' : '[ ]';
|
|
240
|
+
return truncate(`${marker} ${name}.md`, width);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function allocateSingleColumnPanels(candidates, rowsBudget) {
|
|
245
|
+
const filtered = (candidates || []).filter(Boolean);
|
|
246
|
+
if (filtered.length === 0) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const selected = [];
|
|
251
|
+
let remaining = Math.max(4, rowsBudget);
|
|
252
|
+
|
|
253
|
+
for (const panel of filtered) {
|
|
254
|
+
const margin = selected.length > 0 ? 1 : 0;
|
|
255
|
+
const minimumRows = 4 + margin;
|
|
256
|
+
|
|
257
|
+
if (remaining >= minimumRows || selected.length === 0) {
|
|
258
|
+
selected.push({
|
|
259
|
+
...panel,
|
|
260
|
+
maxLines: 1
|
|
261
|
+
});
|
|
262
|
+
remaining -= minimumRows;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let index = 0;
|
|
267
|
+
while (remaining > 0 && selected.length > 0) {
|
|
268
|
+
const panelIndex = index % selected.length;
|
|
269
|
+
selected[panelIndex].maxLines += 1;
|
|
270
|
+
remaining -= 1;
|
|
271
|
+
index += 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return selected;
|
|
275
|
+
}
|
|
276
|
+
|
|
36
277
|
function createDashboardApp(deps) {
|
|
37
278
|
const {
|
|
38
279
|
React,
|
|
@@ -47,33 +288,126 @@ function createDashboardApp(deps) {
|
|
|
47
288
|
initialError
|
|
48
289
|
} = deps;
|
|
49
290
|
|
|
50
|
-
const { Box, Text, useApp, useInput } = ink;
|
|
51
|
-
const { useState, useEffect, useCallback } = React;
|
|
291
|
+
const { Box, Text, useApp, useInput, useStdout } = ink;
|
|
292
|
+
const { useState, useEffect, useCallback, useRef } = React;
|
|
293
|
+
|
|
294
|
+
function SectionPanel(props) {
|
|
295
|
+
const {
|
|
296
|
+
title,
|
|
297
|
+
lines,
|
|
298
|
+
width,
|
|
299
|
+
maxLines,
|
|
300
|
+
borderColor,
|
|
301
|
+
marginBottom
|
|
302
|
+
} = props;
|
|
303
|
+
|
|
304
|
+
const contentWidth = Math.max(18, width - 4);
|
|
305
|
+
const visibleLines = fitLines(lines, maxLines, contentWidth);
|
|
306
|
+
|
|
307
|
+
return React.createElement(
|
|
308
|
+
Box,
|
|
309
|
+
{
|
|
310
|
+
flexDirection: 'column',
|
|
311
|
+
borderStyle: 'round',
|
|
312
|
+
borderColor: borderColor || 'gray',
|
|
313
|
+
paddingX: 1,
|
|
314
|
+
width,
|
|
315
|
+
marginBottom: marginBottom || 0
|
|
316
|
+
},
|
|
317
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
|
|
318
|
+
...visibleLines.map((line, index) => React.createElement(Text, { key: `${title}-${index}` }, line))
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function TabsBar(props) {
|
|
323
|
+
const { view, width } = props;
|
|
324
|
+
const tabs = [
|
|
325
|
+
{ id: 'runs', label: ' 1 RUNS ' },
|
|
326
|
+
{ id: 'overview', label: ' 2 OVERVIEW ' },
|
|
327
|
+
{ id: 'health', label: ' 3 HEALTH ' }
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
return React.createElement(
|
|
331
|
+
Box,
|
|
332
|
+
{ width, flexWrap: 'nowrap' },
|
|
333
|
+
...tabs.map((tab) => {
|
|
334
|
+
const isActive = tab.id === view;
|
|
335
|
+
return React.createElement(
|
|
336
|
+
Text,
|
|
337
|
+
{
|
|
338
|
+
key: tab.id,
|
|
339
|
+
bold: isActive,
|
|
340
|
+
color: isActive ? 'black' : 'gray',
|
|
341
|
+
backgroundColor: isActive ? 'cyan' : undefined
|
|
342
|
+
},
|
|
343
|
+
tab.label
|
|
344
|
+
);
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
}
|
|
52
348
|
|
|
53
349
|
function DashboardApp() {
|
|
54
350
|
const { exit } = useApp();
|
|
351
|
+
const { stdout } = useStdout();
|
|
352
|
+
|
|
353
|
+
const initialNormalizedError = initialError ? toDashboardError(initialError) : null;
|
|
354
|
+
const snapshotHashRef = useRef(safeJsonHash(initialSnapshot || null));
|
|
355
|
+
const errorHashRef = useRef(initialNormalizedError ? safeJsonHash(initialNormalizedError) : null);
|
|
55
356
|
|
|
56
357
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
57
|
-
const [error, setError] = useState(
|
|
358
|
+
const [error, setError] = useState(initialNormalizedError);
|
|
58
359
|
const [ui, setUi] = useState(createInitialUIState());
|
|
59
360
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
60
361
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
362
|
+
const [terminalSize, setTerminalSize] = useState(() => ({
|
|
363
|
+
columns: stdout?.columns || process.stdout.columns || 120,
|
|
364
|
+
rows: stdout?.rows || process.stdout.rows || 40
|
|
365
|
+
}));
|
|
61
366
|
|
|
62
367
|
const refresh = useCallback(async () => {
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
|
|
63
370
|
try {
|
|
64
371
|
const result = await parseSnapshot();
|
|
65
372
|
|
|
66
373
|
if (result?.ok) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
374
|
+
const nextSnapshot = result.snapshot || null;
|
|
375
|
+
const nextSnapshotHash = safeJsonHash(nextSnapshot);
|
|
376
|
+
|
|
377
|
+
if (nextSnapshotHash !== snapshotHashRef.current) {
|
|
378
|
+
snapshotHashRef.current = nextSnapshotHash;
|
|
379
|
+
setSnapshot(nextSnapshot);
|
|
380
|
+
setLastRefreshAt(now);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (errorHashRef.current !== null) {
|
|
384
|
+
errorHashRef.current = null;
|
|
385
|
+
setError(null);
|
|
386
|
+
setLastRefreshAt(now);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (watchEnabled) {
|
|
390
|
+
setWatchStatus((previous) => (previous === 'watching' ? previous : 'watching'));
|
|
391
|
+
}
|
|
70
392
|
} else {
|
|
71
|
-
|
|
393
|
+
const nextError = toDashboardError(result?.error, 'PARSE_ERROR');
|
|
394
|
+
const nextErrorHash = safeJsonHash(nextError);
|
|
395
|
+
|
|
396
|
+
if (nextErrorHash !== errorHashRef.current) {
|
|
397
|
+
errorHashRef.current = nextErrorHash;
|
|
398
|
+
setError(nextError);
|
|
399
|
+
setLastRefreshAt(now);
|
|
400
|
+
}
|
|
72
401
|
}
|
|
73
402
|
} catch (refreshError) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
403
|
+
const nextError = toDashboardError(refreshError, 'REFRESH_FAILED');
|
|
404
|
+
const nextErrorHash = safeJsonHash(nextError);
|
|
405
|
+
|
|
406
|
+
if (nextErrorHash !== errorHashRef.current) {
|
|
407
|
+
errorHashRef.current = nextErrorHash;
|
|
408
|
+
setError(nextError);
|
|
409
|
+
setLastRefreshAt(now);
|
|
410
|
+
}
|
|
77
411
|
}
|
|
78
412
|
}, [parseSnapshot, watchEnabled]);
|
|
79
413
|
|
|
@@ -103,6 +437,11 @@ function createDashboardApp(deps) {
|
|
|
103
437
|
return;
|
|
104
438
|
}
|
|
105
439
|
|
|
440
|
+
if (input === '3') {
|
|
441
|
+
setUi((previous) => ({ ...previous, view: 'health' }));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
106
445
|
if (key.tab) {
|
|
107
446
|
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
108
447
|
return;
|
|
@@ -117,6 +456,34 @@ function createDashboardApp(deps) {
|
|
|
117
456
|
void refresh();
|
|
118
457
|
}, [refresh]);
|
|
119
458
|
|
|
459
|
+
useEffect(() => {
|
|
460
|
+
if (!stdout || typeof stdout.on !== 'function') {
|
|
461
|
+
setTerminalSize({
|
|
462
|
+
columns: process.stdout.columns || 120,
|
|
463
|
+
rows: process.stdout.rows || 40
|
|
464
|
+
});
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const updateSize = () => {
|
|
469
|
+
setTerminalSize({
|
|
470
|
+
columns: stdout.columns || process.stdout.columns || 120,
|
|
471
|
+
rows: stdout.rows || process.stdout.rows || 40
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
updateSize();
|
|
476
|
+
stdout.on('resize', updateSize);
|
|
477
|
+
|
|
478
|
+
return () => {
|
|
479
|
+
if (typeof stdout.off === 'function') {
|
|
480
|
+
stdout.off('resize', updateSize);
|
|
481
|
+
} else if (typeof stdout.removeListener === 'function') {
|
|
482
|
+
stdout.removeListener('resize', updateSize);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}, [stdout]);
|
|
486
|
+
|
|
120
487
|
useEffect(() => {
|
|
121
488
|
if (!watchEnabled) {
|
|
122
489
|
return undefined;
|
|
@@ -124,20 +491,29 @@ function createDashboardApp(deps) {
|
|
|
124
491
|
|
|
125
492
|
const runtime = createWatchRuntime({
|
|
126
493
|
rootPath: rootPath || `${workspacePath}/.specs-fire`,
|
|
127
|
-
debounceMs:
|
|
494
|
+
debounceMs: 200,
|
|
128
495
|
onRefresh: () => {
|
|
129
496
|
void refresh();
|
|
130
497
|
},
|
|
131
498
|
onError: (watchError) => {
|
|
132
|
-
|
|
133
|
-
|
|
499
|
+
const now = new Date().toISOString();
|
|
500
|
+
setWatchStatus((previous) => (previous === 'reconnecting' ? previous : 'reconnecting'));
|
|
501
|
+
|
|
502
|
+
const nextError = toDashboardError(watchError, 'WATCH_ERROR');
|
|
503
|
+
const nextErrorHash = safeJsonHash(nextError);
|
|
504
|
+
if (nextErrorHash !== errorHashRef.current) {
|
|
505
|
+
errorHashRef.current = nextErrorHash;
|
|
506
|
+
setError(nextError);
|
|
507
|
+
setLastRefreshAt(now);
|
|
508
|
+
}
|
|
134
509
|
}
|
|
135
510
|
});
|
|
136
511
|
|
|
137
512
|
runtime.start();
|
|
513
|
+
const fallbackIntervalMs = Math.max(refreshMs, 5000);
|
|
138
514
|
const interval = setInterval(() => {
|
|
139
515
|
void refresh();
|
|
140
|
-
},
|
|
516
|
+
}, fallbackIntervalMs);
|
|
141
517
|
|
|
142
518
|
return () => {
|
|
143
519
|
clearInterval(interval);
|
|
@@ -145,24 +521,127 @@ function createDashboardApp(deps) {
|
|
|
145
521
|
};
|
|
146
522
|
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath]);
|
|
147
523
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
524
|
+
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
525
|
+
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
526
|
+
|
|
527
|
+
const fullWidth = Math.max(40, cols - 1);
|
|
528
|
+
const compactWidth = Math.max(18, fullWidth - 4);
|
|
529
|
+
|
|
530
|
+
const showHelpLine = ui.showHelp && rows >= 14;
|
|
531
|
+
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
532
|
+
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
533
|
+
|
|
534
|
+
const reservedRows = 2 + (showHelpLine ? 1 : 0) + (showErrorPanel ? 5 : 0) + (showErrorInline ? 1 : 0);
|
|
535
|
+
const contentRowsBudget = Math.max(4, rows - reservedRows);
|
|
536
|
+
const ultraCompact = rows <= 14;
|
|
537
|
+
|
|
538
|
+
let panelCandidates;
|
|
539
|
+
if (ui.view === 'overview') {
|
|
540
|
+
panelCandidates = [
|
|
541
|
+
{
|
|
542
|
+
key: 'project',
|
|
543
|
+
title: 'Project + Workspace',
|
|
544
|
+
lines: buildOverviewProjectLines(snapshot, compactWidth),
|
|
545
|
+
borderColor: 'green'
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
key: 'intent-status',
|
|
549
|
+
title: 'Intent Status',
|
|
550
|
+
lines: buildOverviewIntentLines(snapshot, compactWidth),
|
|
551
|
+
borderColor: 'yellow'
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
key: 'standards',
|
|
555
|
+
title: 'Standards',
|
|
556
|
+
lines: buildOverviewStandardsLines(snapshot, compactWidth),
|
|
557
|
+
borderColor: 'blue'
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
} else if (ui.view === 'health') {
|
|
561
|
+
panelCandidates = [
|
|
562
|
+
{
|
|
563
|
+
key: 'stats',
|
|
564
|
+
title: 'Stats',
|
|
565
|
+
lines: buildStatsLines(snapshot, compactWidth),
|
|
566
|
+
borderColor: 'magenta'
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
key: 'warnings',
|
|
570
|
+
title: 'Warnings',
|
|
571
|
+
lines: buildWarningsLines(snapshot, compactWidth),
|
|
572
|
+
borderColor: 'red'
|
|
573
|
+
}
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
if (error && showErrorPanel) {
|
|
577
|
+
panelCandidates.push({
|
|
578
|
+
key: 'error-details',
|
|
579
|
+
title: 'Error Details',
|
|
580
|
+
lines: buildErrorLines(error, compactWidth),
|
|
581
|
+
borderColor: 'red'
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
panelCandidates = [
|
|
586
|
+
{
|
|
587
|
+
key: 'active-runs',
|
|
588
|
+
title: 'Active Runs',
|
|
589
|
+
lines: buildActiveRunLines(snapshot, ui.runFilter, compactWidth),
|
|
590
|
+
borderColor: 'green'
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
key: 'pending',
|
|
594
|
+
title: 'Pending Queue',
|
|
595
|
+
lines: buildPendingLines(snapshot, ui.runFilter, compactWidth),
|
|
596
|
+
borderColor: 'yellow'
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
key: 'completed',
|
|
600
|
+
title: 'Recent Completed Runs',
|
|
601
|
+
lines: buildCompletedLines(snapshot, ui.runFilter, compactWidth),
|
|
602
|
+
borderColor: 'blue'
|
|
603
|
+
}
|
|
604
|
+
];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (ultraCompact) {
|
|
608
|
+
panelCandidates = [panelCandidates[0]];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
|
|
612
|
+
|
|
613
|
+
const helpText = 'q quit | r refresh | h/? help | tab next view | 1 runs | 2 overview | 3 health | f run filter';
|
|
161
614
|
|
|
162
615
|
return React.createElement(
|
|
163
616
|
Box,
|
|
164
|
-
{ flexDirection: 'column' },
|
|
165
|
-
React.createElement(Text,
|
|
617
|
+
{ flexDirection: 'column', width: fullWidth },
|
|
618
|
+
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 }),
|
|
620
|
+
showErrorInline
|
|
621
|
+
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
622
|
+
: null,
|
|
623
|
+
showErrorPanel
|
|
624
|
+
? React.createElement(SectionPanel, {
|
|
625
|
+
title: 'Errors',
|
|
626
|
+
lines: buildErrorLines(error, compactWidth),
|
|
627
|
+
width: fullWidth,
|
|
628
|
+
maxLines: 2,
|
|
629
|
+
borderColor: 'red',
|
|
630
|
+
marginBottom: 1
|
|
631
|
+
})
|
|
632
|
+
: null,
|
|
633
|
+
...panels.map((panel, index) => React.createElement(SectionPanel, {
|
|
634
|
+
key: panel.key,
|
|
635
|
+
title: panel.title,
|
|
636
|
+
lines: panel.lines,
|
|
637
|
+
width: fullWidth,
|
|
638
|
+
maxLines: panel.maxLines,
|
|
639
|
+
borderColor: panel.borderColor,
|
|
640
|
+
marginBottom: index === panels.length - 1 ? 0 : 1
|
|
641
|
+
})),
|
|
642
|
+
showHelpLine
|
|
643
|
+
? React.createElement(Text, { color: 'gray' }, truncate(helpText, fullWidth))
|
|
644
|
+
: null
|
|
166
645
|
);
|
|
167
646
|
}
|
|
168
647
|
|
|
@@ -171,5 +650,9 @@ function createDashboardApp(deps) {
|
|
|
171
650
|
|
|
172
651
|
module.exports = {
|
|
173
652
|
createDashboardApp,
|
|
174
|
-
toDashboardError
|
|
653
|
+
toDashboardError,
|
|
654
|
+
truncate,
|
|
655
|
+
fitLines,
|
|
656
|
+
safeJsonHash,
|
|
657
|
+
allocateSingleColumnPanels
|
|
175
658
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
|
|
5
5
|
"main": "lib/installer.js",
|
|
6
6
|
"bin": {
|