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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "Route work across coding-agent CLI subscriptions — paced by live quota meters, verified by content, never trusting exit codes.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- let start = Math.max(0, timeline.lines.length - timelineRows - scroll);
562
+ const end = Math.max(0, timeline.lines.length - scroll);
563
+ let start = Math.max(0, end - timelineRows);
563
564
  if (start > 0) {
564
- while (start < timeline.lines.length && !/^\d{2}:\d{2}\s/.test(timeline.lines[start])) start += 1;
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
- const historyRows = start > 0 ? Math.max(0, timelineRows - 1) : timelineRows;
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
- visibleTimeline[visibleTimeline.length - 1] = dimText(`↓ ${timeline.lines.length - end} newer timeline rows`, inner);
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, token = null,
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
- output.write(renderWorkflowTui(row, {
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
- output.write(renderDashboard({ rows, selected, message }));
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 { output.write(renderDashboard({ rows, selected, message })); } catch { /* keep the loop alive */ }
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
- }, 160);
1368
+ }, Math.max(50, Number(spinnerMs) || 400));
1347
1369
  return new Promise((resolve) => {
1348
1370
  const finish = () => {
1349
1371
  clearInterval(timer);