bullswarm 0.18.3 → 0.18.5
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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/workflow/dashboard.js +34 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.4 — reliable auto-follow tail
|
|
4
|
+
|
|
5
|
+
- Timestamp-aligned mobile auto-follow now preserves the newest workflow
|
|
6
|
+
milestone instead of replacing it with a spurious newer-rows marker.
|
|
7
|
+
|
|
3
8
|
## 0.18.3 — mobile timeline polish
|
|
4
9
|
|
|
5
10
|
- Auto-follow now begins at a timestamped milestone instead of exposing an
|
package/package.json
CHANGED
|
@@ -559,18 +559,20 @@ function renderWorkflowOverviewPanel(model, width, height, spinnerFrame, timelin
|
|
|
559
559
|
const timelineRows = Math.max(1, contentRows - liveRows - nextRows);
|
|
560
560
|
const maxTimelineScroll = Math.max(0, timeline.lines.length - timelineRows);
|
|
561
561
|
const scroll = clamp(timelineScroll, 0, maxTimelineScroll);
|
|
562
|
-
|
|
562
|
+
const end = Math.max(0, timeline.lines.length - scroll);
|
|
563
|
+
let start = Math.max(0, end - timelineRows);
|
|
563
564
|
if (start > 0) {
|
|
564
|
-
|
|
565
|
+
start = Math.max(0, end - Math.max(0, timelineRows - 1));
|
|
566
|
+
while (start < end && !/^\d{2}:\d{2}\s/.test(timeline.lines[start])) start += 1;
|
|
565
567
|
}
|
|
566
|
-
|
|
567
|
-
let visibleTimeline = timeline.lines.slice(start, start + historyRows);
|
|
568
|
+
let visibleTimeline = timeline.lines.slice(start, end);
|
|
568
569
|
if (start > 0) {
|
|
569
570
|
visibleTimeline.unshift(dimText(`↑ ${start} earlier timeline rows`, inner));
|
|
570
571
|
}
|
|
571
|
-
const end = start + historyRows;
|
|
572
572
|
if (end < timeline.lines.length && visibleTimeline.length) {
|
|
573
|
-
|
|
573
|
+
const marker = dimText(`↓ ${timeline.lines.length - end} newer timeline rows`, inner);
|
|
574
|
+
if (visibleTimeline.length >= timelineRows) visibleTimeline[visibleTimeline.length - 1] = marker;
|
|
575
|
+
else visibleTimeline.push(marker);
|
|
574
576
|
}
|
|
575
577
|
const visibleLive = live.lines.slice(0, liveRows);
|
|
576
578
|
const visibleNext = next.slice(0, nextRows);
|
|
@@ -1266,7 +1268,8 @@ function detailRow(bullswarmDir, token) {
|
|
|
1266
1268
|
}
|
|
1267
1269
|
|
|
1268
1270
|
export async function runDashboard(bullswarmDir, {
|
|
1269
|
-
input = process.stdin, output = process.stdout, refreshMs = 1000,
|
|
1271
|
+
input = process.stdin, output = process.stdout, refreshMs = 1000,
|
|
1272
|
+
spinnerMs = 400, token = null,
|
|
1270
1273
|
} = {}) {
|
|
1271
1274
|
if ((!input.isTTY || !output.isTTY) && !token) throw new Error('workflow dashboard requires a TTY, or pass a run ID for a static text tree');
|
|
1272
1275
|
if ((!input.isTTY || !output.isTTY) && token) {
|
|
@@ -1280,6 +1283,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
1280
1283
|
let lastGoodRow = null;
|
|
1281
1284
|
let rows = dashboardRows(bullswarmDir);
|
|
1282
1285
|
let selectedRunId = token ? detailRow(bullswarmDir, token).runId : (rows[selected]?.runId ?? null);
|
|
1286
|
+
let lastPaintedFrame = null;
|
|
1283
1287
|
const ui = {
|
|
1284
1288
|
focus: 0,
|
|
1285
1289
|
phaseIndex: null,
|
|
@@ -1295,6 +1299,24 @@ export async function runDashboard(bullswarmDir, {
|
|
|
1295
1299
|
mobileTimeline: true,
|
|
1296
1300
|
spinnerFrame: 0,
|
|
1297
1301
|
};
|
|
1302
|
+
// Clearing the entire alternate screen for every spinner frame produces a
|
|
1303
|
+
// visible blank flash on slower terminals, especially mobile SSH sessions.
|
|
1304
|
+
// Enter/clear the alternate screen once, then repaint each row in place.
|
|
1305
|
+
// Padding to the terminal height also removes remnants when switching from
|
|
1306
|
+
// a taller detail view to a shorter picker view.
|
|
1307
|
+
const writeFrame = (text) => {
|
|
1308
|
+
const clearPrefix = `${ESC}2J${ESC}H`;
|
|
1309
|
+
const source = String(text ?? '').startsWith(clearPrefix)
|
|
1310
|
+
? String(text ?? '').slice(clearPrefix.length)
|
|
1311
|
+
: String(text ?? '');
|
|
1312
|
+
const height = Math.max(1, Number(output.rows) || source.split('\n').length);
|
|
1313
|
+
const lines = source.split('\n').slice(0, height);
|
|
1314
|
+
while (lines.length < height) lines.push('');
|
|
1315
|
+
const frame = `${ESC}H${lines.map((line) => `${line}${ESC}K`).join('\n')}`;
|
|
1316
|
+
if (frame === lastPaintedFrame) return;
|
|
1317
|
+
lastPaintedFrame = frame;
|
|
1318
|
+
output.write(frame);
|
|
1319
|
+
};
|
|
1298
1320
|
const paintUnsafe = () => {
|
|
1299
1321
|
if (selected >= rows.length) selected = Math.max(0, rows.length - 1);
|
|
1300
1322
|
if (detail && selectedRunId) {
|
|
@@ -1309,7 +1331,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
1309
1331
|
});
|
|
1310
1332
|
ui.phaseIndex = model.phaseIndex;
|
|
1311
1333
|
ui.agentIndex = model.agentIndex;
|
|
1312
|
-
|
|
1334
|
+
writeFrame(renderWorkflowTui(row, {
|
|
1313
1335
|
width: output.columns,
|
|
1314
1336
|
height: output.rows,
|
|
1315
1337
|
...ui,
|
|
@@ -1317,7 +1339,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
1317
1339
|
}));
|
|
1318
1340
|
return;
|
|
1319
1341
|
}
|
|
1320
|
-
|
|
1342
|
+
writeFrame(renderDashboard({ rows, selected, message }));
|
|
1321
1343
|
};
|
|
1322
1344
|
// A render error must never kill the TUI or strand the terminal in
|
|
1323
1345
|
// alt-screen raw mode (crash observed 2026-08-29 at detailRow via the
|
|
@@ -1325,7 +1347,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
1325
1347
|
const paint = () => {
|
|
1326
1348
|
try { paintUnsafe(); } catch (err) {
|
|
1327
1349
|
message = `display error: ${err.message}`;
|
|
1328
|
-
try {
|
|
1350
|
+
try { writeFrame(renderDashboard({ rows, selected, message })); } catch { /* keep the loop alive */ }
|
|
1329
1351
|
}
|
|
1330
1352
|
};
|
|
1331
1353
|
const refresh = () => {
|
|
@@ -1337,13 +1359,13 @@ export async function runDashboard(bullswarmDir, {
|
|
|
1337
1359
|
};
|
|
1338
1360
|
input.setRawMode?.(true);
|
|
1339
1361
|
input.resume();
|
|
1340
|
-
output.write(`${ESC}?1049h${ESC}?25l`);
|
|
1362
|
+
output.write(`${ESC}?1049h${ESC}?25l${ESC}2J${ESC}H`);
|
|
1341
1363
|
paint();
|
|
1342
1364
|
const timer = setInterval(refresh, refreshMs);
|
|
1343
1365
|
const spinnerTimer = setInterval(() => {
|
|
1344
1366
|
ui.spinnerFrame = (ui.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
1345
1367
|
if (detail) paint();
|
|
1346
|
-
},
|
|
1368
|
+
}, Math.max(50, Number(spinnerMs) || 400));
|
|
1347
1369
|
return new Promise((resolve) => {
|
|
1348
1370
|
const finish = () => {
|
|
1349
1371
|
clearInterval(timer);
|