bullswarm 0.18.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.18.4",
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": {
@@ -1268,7 +1268,8 @@ function detailRow(bullswarmDir, token) {
1268
1268
  }
1269
1269
 
1270
1270
  export async function runDashboard(bullswarmDir, {
1271
- input = process.stdin, output = process.stdout, refreshMs = 1000, token = null,
1271
+ input = process.stdin, output = process.stdout, refreshMs = 1000,
1272
+ spinnerMs = 400, token = null,
1272
1273
  } = {}) {
1273
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');
1274
1275
  if ((!input.isTTY || !output.isTTY) && token) {
@@ -1282,6 +1283,7 @@ export async function runDashboard(bullswarmDir, {
1282
1283
  let lastGoodRow = null;
1283
1284
  let rows = dashboardRows(bullswarmDir);
1284
1285
  let selectedRunId = token ? detailRow(bullswarmDir, token).runId : (rows[selected]?.runId ?? null);
1286
+ let lastPaintedFrame = null;
1285
1287
  const ui = {
1286
1288
  focus: 0,
1287
1289
  phaseIndex: null,
@@ -1297,6 +1299,24 @@ export async function runDashboard(bullswarmDir, {
1297
1299
  mobileTimeline: true,
1298
1300
  spinnerFrame: 0,
1299
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
+ };
1300
1320
  const paintUnsafe = () => {
1301
1321
  if (selected >= rows.length) selected = Math.max(0, rows.length - 1);
1302
1322
  if (detail && selectedRunId) {
@@ -1311,7 +1331,7 @@ export async function runDashboard(bullswarmDir, {
1311
1331
  });
1312
1332
  ui.phaseIndex = model.phaseIndex;
1313
1333
  ui.agentIndex = model.agentIndex;
1314
- output.write(renderWorkflowTui(row, {
1334
+ writeFrame(renderWorkflowTui(row, {
1315
1335
  width: output.columns,
1316
1336
  height: output.rows,
1317
1337
  ...ui,
@@ -1319,7 +1339,7 @@ export async function runDashboard(bullswarmDir, {
1319
1339
  }));
1320
1340
  return;
1321
1341
  }
1322
- output.write(renderDashboard({ rows, selected, message }));
1342
+ writeFrame(renderDashboard({ rows, selected, message }));
1323
1343
  };
1324
1344
  // A render error must never kill the TUI or strand the terminal in
1325
1345
  // alt-screen raw mode (crash observed 2026-08-29 at detailRow via the
@@ -1327,7 +1347,7 @@ export async function runDashboard(bullswarmDir, {
1327
1347
  const paint = () => {
1328
1348
  try { paintUnsafe(); } catch (err) {
1329
1349
  message = `display error: ${err.message}`;
1330
- try { output.write(renderDashboard({ rows, selected, message })); } catch { /* keep the loop alive */ }
1350
+ try { writeFrame(renderDashboard({ rows, selected, message })); } catch { /* keep the loop alive */ }
1331
1351
  }
1332
1352
  };
1333
1353
  const refresh = () => {
@@ -1339,13 +1359,13 @@ export async function runDashboard(bullswarmDir, {
1339
1359
  };
1340
1360
  input.setRawMode?.(true);
1341
1361
  input.resume();
1342
- output.write(`${ESC}?1049h${ESC}?25l`);
1362
+ output.write(`${ESC}?1049h${ESC}?25l${ESC}2J${ESC}H`);
1343
1363
  paint();
1344
1364
  const timer = setInterval(refresh, refreshMs);
1345
1365
  const spinnerTimer = setInterval(() => {
1346
1366
  ui.spinnerFrame = (ui.spinnerFrame + 1) % SPINNER_FRAMES.length;
1347
1367
  if (detail) paint();
1348
- }, 160);
1368
+ }, Math.max(50, Number(spinnerMs) || 400));
1349
1369
  return new Promise((resolve) => {
1350
1370
  const finish = () => {
1351
1371
  clearInterval(timer);