specsmd 0.1.24 → 0.1.25
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 +404 -12
- 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,204 @@ function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
function truncate(value, width) {
|
|
36
|
+
const text = String(value ?? '');
|
|
37
|
+
if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (width <= 3) {
|
|
42
|
+
return text.slice(0, width);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `${text.slice(0, width - 3)}...`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fitLines(lines, maxLines, width) {
|
|
49
|
+
const safeLines = (Array.isArray(lines) ? lines : [])
|
|
50
|
+
.map((line) => truncate(line, width));
|
|
51
|
+
|
|
52
|
+
if (safeLines.length <= maxLines) {
|
|
53
|
+
return safeLines;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
|
|
57
|
+
visible.push(truncate(`... +${safeLines.length - visible.length} more`, width));
|
|
58
|
+
return visible;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatTime(value) {
|
|
62
|
+
if (!value) {
|
|
63
|
+
return 'n/a';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const date = new Date(value);
|
|
67
|
+
if (Number.isNaN(date.getTime())) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return date.toLocaleTimeString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildHeaderLines(snapshot, flow, workspacePath, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
|
|
75
|
+
const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
`specsmd dashboard | ${flow.toUpperCase()} | ${projectName}`,
|
|
79
|
+
`path: ${workspacePath}`,
|
|
80
|
+
`updated: ${formatTime(lastRefreshAt)} | watch: ${watchEnabled ? watchStatus : 'off'} | view: ${view} | filter: ${runFilter}`
|
|
81
|
+
].map((line) => truncate(line, width));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildErrorLines(error, width) {
|
|
85
|
+
if (!error) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const lines = [
|
|
90
|
+
`[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
if (error.details) {
|
|
94
|
+
lines.push(`details: ${error.details}`);
|
|
95
|
+
}
|
|
96
|
+
if (error.path) {
|
|
97
|
+
lines.push(`path: ${error.path}`);
|
|
98
|
+
}
|
|
99
|
+
if (error.hint) {
|
|
100
|
+
lines.push(`hint: ${error.hint}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines.map((line) => truncate(line, width));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildActiveRunLines(snapshot, runFilter, width) {
|
|
107
|
+
if (runFilter === 'completed') {
|
|
108
|
+
return [truncate('Hidden by active filter: completed', width)];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const activeRuns = snapshot?.activeRuns || [];
|
|
112
|
+
if (activeRuns.length === 0) {
|
|
113
|
+
return [truncate('No active runs', width)];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const lines = [];
|
|
117
|
+
for (const run of activeRuns) {
|
|
118
|
+
const currentItem = run.currentItem || 'n/a';
|
|
119
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
120
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
121
|
+
const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
|
|
122
|
+
const artifacts = [
|
|
123
|
+
run.hasPlan ? 'plan' : null,
|
|
124
|
+
run.hasWalkthrough ? 'walkthrough' : null,
|
|
125
|
+
run.hasTestReport ? 'test-report' : null
|
|
126
|
+
].filter(Boolean).join(', ') || 'none';
|
|
127
|
+
|
|
128
|
+
lines.push(`${run.id} [${run.scope}] current: ${currentItem}`);
|
|
129
|
+
lines.push(`progress ${completed}/${workItems.length} done, ${inProgress} active | artifacts: ${artifacts}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return lines.map((line) => truncate(line, width));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildPendingLines(snapshot, runFilter, width) {
|
|
136
|
+
if (runFilter === 'completed') {
|
|
137
|
+
return [truncate('Hidden by active filter: completed', width)];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const pending = snapshot?.pendingItems || [];
|
|
141
|
+
if (pending.length === 0) {
|
|
142
|
+
return [truncate('No pending work items', width)];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return pending.map((item) => {
|
|
146
|
+
const deps = item.dependencies && item.dependencies.length > 0
|
|
147
|
+
? ` deps:${item.dependencies.join(',')}`
|
|
148
|
+
: '';
|
|
149
|
+
return truncate(`${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`, width);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildCompletedLines(snapshot, runFilter, width) {
|
|
154
|
+
if (runFilter === 'active') {
|
|
155
|
+
return [truncate('Hidden by active filter: active', width)];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const completedRuns = snapshot?.completedRuns || [];
|
|
159
|
+
if (completedRuns.length === 0) {
|
|
160
|
+
return [truncate('No completed runs yet', width)];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return completedRuns.map((run) => {
|
|
164
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
165
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
166
|
+
return truncate(`${run.id} [${run.scope}] ${completed}/${workItems.length} done at ${run.completedAt || 'unknown'}`, width);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildStatsLines(snapshot, width) {
|
|
171
|
+
if (!snapshot?.initialized) {
|
|
172
|
+
return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stats = snapshot.stats;
|
|
176
|
+
return [
|
|
177
|
+
`intents: ${stats.completedIntents}/${stats.totalIntents} done | in_progress: ${stats.inProgressIntents} | blocked: ${stats.blockedIntents}`,
|
|
178
|
+
`work items: ${stats.completedWorkItems}/${stats.totalWorkItems} done | in_progress: ${stats.inProgressWorkItems} | pending: ${stats.pendingWorkItems} | blocked: ${stats.blockedWorkItems}`,
|
|
179
|
+
`runs: ${stats.activeRunsCount} active | ${stats.completedRuns} completed | ${stats.totalRuns} total`
|
|
180
|
+
].map((line) => truncate(line, width));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildOverviewProjectLines(snapshot, width) {
|
|
184
|
+
if (!snapshot?.initialized) {
|
|
185
|
+
return [
|
|
186
|
+
truncate('FIRE folder detected, but state.yaml is missing.', width),
|
|
187
|
+
truncate('Initialize project context and this view will populate.', width)
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const project = snapshot.project || {};
|
|
192
|
+
const workspace = snapshot.workspace || {};
|
|
193
|
+
|
|
194
|
+
return [
|
|
195
|
+
`project: ${project.name || 'unknown'} | fire_version: ${project.fireVersion || snapshot.version || '0.0.0'}`,
|
|
196
|
+
`workspace: ${workspace.type || 'unknown'} / ${workspace.structure || 'unknown'}`,
|
|
197
|
+
`autonomy: ${workspace.autonomyBias || 'unknown'} | run scope pref: ${workspace.runScopePreference || 'unknown'}`
|
|
198
|
+
].map((line) => truncate(line, width));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildOverviewIntentLines(snapshot, width) {
|
|
202
|
+
const intents = snapshot?.intents || [];
|
|
203
|
+
if (intents.length === 0) {
|
|
204
|
+
return [truncate('No intents found', width)];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return intents.map((intent) => {
|
|
208
|
+
const workItems = Array.isArray(intent.workItems) ? intent.workItems : [];
|
|
209
|
+
const done = workItems.filter((item) => item.status === 'completed').length;
|
|
210
|
+
return truncate(`${intent.id}: ${intent.status} (${done}/${workItems.length} work items)`, width);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildOverviewStandardsLines(snapshot, width) {
|
|
215
|
+
const expected = ['constitution', 'tech-stack', 'coding-standards', 'testing-standards', 'system-architecture'];
|
|
216
|
+
const actual = new Set((snapshot?.standards || []).map((item) => item.type));
|
|
217
|
+
|
|
218
|
+
return expected.map((name) => {
|
|
219
|
+
const marker = actual.has(name) ? '[x]' : '[ ]';
|
|
220
|
+
return truncate(`${marker} ${name}.md`, width);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildWarningsLines(snapshot, width) {
|
|
225
|
+
const warnings = snapshot?.warnings || [];
|
|
226
|
+
if (warnings.length === 0) {
|
|
227
|
+
return [truncate('No warnings', width)];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return warnings.map((warning) => truncate(warning, width));
|
|
231
|
+
}
|
|
232
|
+
|
|
36
233
|
function createDashboardApp(deps) {
|
|
37
234
|
const {
|
|
38
235
|
React,
|
|
@@ -47,17 +244,52 @@ function createDashboardApp(deps) {
|
|
|
47
244
|
initialError
|
|
48
245
|
} = deps;
|
|
49
246
|
|
|
50
|
-
const { Box, Text, useApp, useInput } = ink;
|
|
247
|
+
const { Box, Text, useApp, useInput, useStdout } = ink;
|
|
51
248
|
const { useState, useEffect, useCallback } = React;
|
|
52
249
|
|
|
250
|
+
function SectionPanel(props) {
|
|
251
|
+
const {
|
|
252
|
+
title,
|
|
253
|
+
lines,
|
|
254
|
+
width,
|
|
255
|
+
maxLines,
|
|
256
|
+
borderColor,
|
|
257
|
+
marginRight,
|
|
258
|
+
marginBottom
|
|
259
|
+
} = props;
|
|
260
|
+
|
|
261
|
+
const contentWidth = Math.max(18, width - 4);
|
|
262
|
+
const visibleLines = fitLines(lines, maxLines, contentWidth);
|
|
263
|
+
|
|
264
|
+
return React.createElement(
|
|
265
|
+
Box,
|
|
266
|
+
{
|
|
267
|
+
flexDirection: 'column',
|
|
268
|
+
borderStyle: 'round',
|
|
269
|
+
borderColor: borderColor || 'gray',
|
|
270
|
+
paddingX: 1,
|
|
271
|
+
width,
|
|
272
|
+
marginRight: marginRight || 0,
|
|
273
|
+
marginBottom: marginBottom || 0
|
|
274
|
+
},
|
|
275
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
|
|
276
|
+
...visibleLines.map((line, index) => React.createElement(Text, { key: `${title}-${index}` }, line))
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
53
280
|
function DashboardApp() {
|
|
54
281
|
const { exit } = useApp();
|
|
282
|
+
const { stdout } = useStdout();
|
|
55
283
|
|
|
56
284
|
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
57
285
|
const [error, setError] = useState(initialError ? toDashboardError(initialError) : null);
|
|
58
286
|
const [ui, setUi] = useState(createInitialUIState());
|
|
59
287
|
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
60
288
|
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
289
|
+
const [terminalSize, setTerminalSize] = useState(() => ({
|
|
290
|
+
columns: stdout?.columns || process.stdout.columns || 120,
|
|
291
|
+
rows: stdout?.rows || process.stdout.rows || 40
|
|
292
|
+
}));
|
|
61
293
|
|
|
62
294
|
const refresh = useCallback(async () => {
|
|
63
295
|
try {
|
|
@@ -117,6 +349,34 @@ function createDashboardApp(deps) {
|
|
|
117
349
|
void refresh();
|
|
118
350
|
}, [refresh]);
|
|
119
351
|
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
if (!stdout || typeof stdout.on !== 'function') {
|
|
354
|
+
setTerminalSize({
|
|
355
|
+
columns: process.stdout.columns || 120,
|
|
356
|
+
rows: process.stdout.rows || 40
|
|
357
|
+
});
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const updateSize = () => {
|
|
362
|
+
setTerminalSize({
|
|
363
|
+
columns: stdout.columns || process.stdout.columns || 120,
|
|
364
|
+
rows: stdout.rows || process.stdout.rows || 40
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
updateSize();
|
|
369
|
+
stdout.on('resize', updateSize);
|
|
370
|
+
|
|
371
|
+
return () => {
|
|
372
|
+
if (typeof stdout.off === 'function') {
|
|
373
|
+
stdout.off('resize', updateSize);
|
|
374
|
+
} else if (typeof stdout.removeListener === 'function') {
|
|
375
|
+
stdout.removeListener('resize', updateSize);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}, [stdout]);
|
|
379
|
+
|
|
120
380
|
useEffect(() => {
|
|
121
381
|
if (!watchEnabled) {
|
|
122
382
|
return undefined;
|
|
@@ -145,24 +405,154 @@ function createDashboardApp(deps) {
|
|
|
145
405
|
};
|
|
146
406
|
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath]);
|
|
147
407
|
|
|
148
|
-
const
|
|
408
|
+
const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
|
|
409
|
+
const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
|
|
410
|
+
|
|
411
|
+
const compact = cols < 110;
|
|
412
|
+
const veryCompact = cols < 80 || rows < 22;
|
|
413
|
+
const contentAreaHeight = Math.max(12, rows - (ui.showHelp ? 7 : 5));
|
|
414
|
+
const sectionLineLimit = compact
|
|
415
|
+
? Math.max(2, Math.floor(contentAreaHeight / 5))
|
|
416
|
+
: Math.max(3, Math.floor(contentAreaHeight / 4));
|
|
417
|
+
|
|
418
|
+
const fullWidth = Math.max(40, cols - 1);
|
|
419
|
+
const leftWidth = compact ? fullWidth : Math.max(28, Math.floor((fullWidth - 1) / 2));
|
|
420
|
+
const rightWidth = compact ? fullWidth : Math.max(28, fullWidth - leftWidth - 1);
|
|
421
|
+
|
|
422
|
+
const headerLines = buildHeaderLines(
|
|
149
423
|
snapshot,
|
|
150
|
-
error,
|
|
151
424
|
flow,
|
|
152
425
|
workspacePath,
|
|
153
|
-
view: ui.view,
|
|
154
|
-
runFilter: ui.runFilter,
|
|
155
426
|
watchEnabled,
|
|
156
427
|
watchStatus,
|
|
157
|
-
showHelp: ui.showHelp,
|
|
158
428
|
lastRefreshAt,
|
|
159
|
-
|
|
160
|
-
|
|
429
|
+
ui.view,
|
|
430
|
+
ui.runFilter,
|
|
431
|
+
fullWidth - 4
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const helpLines = ui.showHelp
|
|
435
|
+
? ['q quit | r refresh | h/? help | tab switch view | 1 runs | 2 overview | f run filter']
|
|
436
|
+
: ['press h to show shortcuts'];
|
|
437
|
+
|
|
438
|
+
const errorLines = buildErrorLines(error, fullWidth - 4);
|
|
439
|
+
|
|
440
|
+
const leftPanels = [];
|
|
441
|
+
const rightPanels = [];
|
|
442
|
+
|
|
443
|
+
if (ui.view === 'overview') {
|
|
444
|
+
leftPanels.push({
|
|
445
|
+
title: 'Project + Workspace',
|
|
446
|
+
lines: buildOverviewProjectLines(snapshot, leftWidth - 4),
|
|
447
|
+
borderColor: 'green'
|
|
448
|
+
});
|
|
449
|
+
leftPanels.push({
|
|
450
|
+
title: 'Intent Status',
|
|
451
|
+
lines: buildOverviewIntentLines(snapshot, leftWidth - 4),
|
|
452
|
+
borderColor: 'yellow'
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
rightPanels.push({
|
|
456
|
+
title: 'Stats',
|
|
457
|
+
lines: buildStatsLines(snapshot, rightWidth - 4),
|
|
458
|
+
borderColor: 'magenta'
|
|
459
|
+
});
|
|
460
|
+
rightPanels.push({
|
|
461
|
+
title: 'Standards',
|
|
462
|
+
lines: buildOverviewStandardsLines(snapshot, rightWidth - 4),
|
|
463
|
+
borderColor: 'blue'
|
|
464
|
+
});
|
|
465
|
+
rightPanels.push({
|
|
466
|
+
title: 'Warnings',
|
|
467
|
+
lines: buildWarningsLines(snapshot, rightWidth - 4),
|
|
468
|
+
borderColor: 'red'
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
leftPanels.push({
|
|
472
|
+
title: 'Active Runs',
|
|
473
|
+
lines: buildActiveRunLines(snapshot, ui.runFilter, leftWidth - 4),
|
|
474
|
+
borderColor: 'green'
|
|
475
|
+
});
|
|
476
|
+
leftPanels.push({
|
|
477
|
+
title: 'Pending Queue',
|
|
478
|
+
lines: buildPendingLines(snapshot, ui.runFilter, leftWidth - 4),
|
|
479
|
+
borderColor: 'yellow'
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
rightPanels.push({
|
|
483
|
+
title: 'Recent Completed Runs',
|
|
484
|
+
lines: buildCompletedLines(snapshot, ui.runFilter, rightWidth - 4),
|
|
485
|
+
borderColor: 'blue'
|
|
486
|
+
});
|
|
487
|
+
rightPanels.push({
|
|
488
|
+
title: 'Stats',
|
|
489
|
+
lines: buildStatsLines(snapshot, rightWidth - 4),
|
|
490
|
+
borderColor: 'magenta'
|
|
491
|
+
});
|
|
492
|
+
rightPanels.push({
|
|
493
|
+
title: 'Warnings',
|
|
494
|
+
lines: buildWarningsLines(snapshot, rightWidth - 4),
|
|
495
|
+
borderColor: 'red'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
161
498
|
|
|
162
499
|
return React.createElement(
|
|
163
500
|
Box,
|
|
164
|
-
{ flexDirection: 'column' },
|
|
165
|
-
React.createElement(
|
|
501
|
+
{ flexDirection: 'column', width: fullWidth },
|
|
502
|
+
React.createElement(SectionPanel, {
|
|
503
|
+
title: veryCompact ? 'specsmd dashboard' : 'specsmd dashboard / FIRE',
|
|
504
|
+
lines: headerLines,
|
|
505
|
+
width: fullWidth,
|
|
506
|
+
maxLines: veryCompact ? 2 : 3,
|
|
507
|
+
borderColor: 'cyan',
|
|
508
|
+
marginBottom: 1
|
|
509
|
+
}),
|
|
510
|
+
error ? React.createElement(SectionPanel, {
|
|
511
|
+
title: 'Errors',
|
|
512
|
+
lines: errorLines,
|
|
513
|
+
width: fullWidth,
|
|
514
|
+
maxLines: Math.max(2, sectionLineLimit),
|
|
515
|
+
borderColor: 'red',
|
|
516
|
+
marginBottom: 1
|
|
517
|
+
}) : null,
|
|
518
|
+
React.createElement(
|
|
519
|
+
Box,
|
|
520
|
+
{ flexDirection: compact ? 'column' : 'row', width: fullWidth },
|
|
521
|
+
React.createElement(
|
|
522
|
+
Box,
|
|
523
|
+
{ flexDirection: 'column', width: leftWidth, marginRight: compact ? 0 : 1 },
|
|
524
|
+
...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
|
|
525
|
+
key: `left-${panel.title}`,
|
|
526
|
+
title: panel.title,
|
|
527
|
+
lines: panel.lines,
|
|
528
|
+
width: leftWidth,
|
|
529
|
+
maxLines: sectionLineLimit,
|
|
530
|
+
borderColor: panel.borderColor,
|
|
531
|
+
marginBottom: index === leftPanels.length - 1 ? 0 : 1
|
|
532
|
+
}))
|
|
533
|
+
),
|
|
534
|
+
React.createElement(
|
|
535
|
+
Box,
|
|
536
|
+
{ flexDirection: 'column', width: rightWidth },
|
|
537
|
+
...rightPanels.map((panel, index) => React.createElement(SectionPanel, {
|
|
538
|
+
key: `right-${panel.title}`,
|
|
539
|
+
title: panel.title,
|
|
540
|
+
lines: panel.lines,
|
|
541
|
+
width: rightWidth,
|
|
542
|
+
maxLines: sectionLineLimit,
|
|
543
|
+
borderColor: panel.borderColor,
|
|
544
|
+
marginBottom: index === rightPanels.length - 1 ? 0 : 1
|
|
545
|
+
}))
|
|
546
|
+
)
|
|
547
|
+
),
|
|
548
|
+
React.createElement(SectionPanel, {
|
|
549
|
+
title: 'Help',
|
|
550
|
+
lines: helpLines,
|
|
551
|
+
width: fullWidth,
|
|
552
|
+
maxLines: 2,
|
|
553
|
+
borderColor: 'gray',
|
|
554
|
+
marginTop: 1
|
|
555
|
+
})
|
|
166
556
|
);
|
|
167
557
|
}
|
|
168
558
|
|
|
@@ -171,5 +561,7 @@ function createDashboardApp(deps) {
|
|
|
171
561
|
|
|
172
562
|
module.exports = {
|
|
173
563
|
createDashboardApp,
|
|
174
|
-
toDashboardError
|
|
564
|
+
toDashboardError,
|
|
565
|
+
truncate,
|
|
566
|
+
fitLines
|
|
175
567
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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": {
|