droid-patch 0.8.0 → 0.8.2
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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli.mjs +1330 -1035
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -690,6 +690,7 @@ const MIN_RENDER_INTERVAL_MS = IS_APPLE_TERMINAL ? 1000 : 500;
|
|
|
690
690
|
const START_MS = Date.now();
|
|
691
691
|
const ARGS = process.argv.slice(2);
|
|
692
692
|
const PGID = Number(process.env.DROID_STATUSLINE_PGID || '');
|
|
693
|
+
const SESSION_ID_RE = /"sessionId":"([0-9a-f-]{36})"/i;
|
|
693
694
|
|
|
694
695
|
function sleep(ms) {
|
|
695
696
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -699,6 +700,40 @@ function isPositiveInt(n) {
|
|
|
699
700
|
return Number.isFinite(n) && n > 0;
|
|
700
701
|
}
|
|
701
702
|
|
|
703
|
+
function extractSessionIdFromLine(line) {
|
|
704
|
+
if (!line) return null;
|
|
705
|
+
const m = String(line).match(SESSION_ID_RE);
|
|
706
|
+
return m ? m[1] : null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function parseLineTimestampMs(line) {
|
|
710
|
+
const s = String(line || '');
|
|
711
|
+
if (!s || s[0] !== '[') return null;
|
|
712
|
+
const end = s.indexOf(']');
|
|
713
|
+
if (end <= 1) return null;
|
|
714
|
+
const raw = s.slice(1, end);
|
|
715
|
+
const ms = Date.parse(raw);
|
|
716
|
+
return Number.isFinite(ms) ? ms : null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function safeStatMtimeMs(p) {
|
|
720
|
+
try {
|
|
721
|
+
const stat = fs.statSync(p);
|
|
722
|
+
const ms = Number(stat?.mtimeMs ?? 0);
|
|
723
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
724
|
+
} catch {
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function nextCompactionState(line, current) {
|
|
730
|
+
if (!line) return current;
|
|
731
|
+
if (line.includes('[Compaction] Start')) return true;
|
|
732
|
+
const endMarkers = ['End', 'Done', 'Finish', 'Finished', 'Complete', 'Completed'];
|
|
733
|
+
if (endMarkers.some(m => line.includes('[Compaction] ' + m))) return false;
|
|
734
|
+
return current;
|
|
735
|
+
}
|
|
736
|
+
|
|
702
737
|
function firstNonNull(promises) {
|
|
703
738
|
const list = Array.isArray(promises) ? promises : [];
|
|
704
739
|
if (list.length === 0) return Promise.resolve(null);
|
|
@@ -1161,7 +1196,7 @@ function buildLine(params) {
|
|
|
1161
1196
|
}
|
|
1162
1197
|
|
|
1163
1198
|
async function main() {
|
|
1164
|
-
|
|
1199
|
+
let factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1165
1200
|
|
|
1166
1201
|
const cwd = process.cwd();
|
|
1167
1202
|
const cwdBase = path.basename(cwd) || cwd;
|
|
@@ -1208,21 +1243,68 @@ async function main() {
|
|
|
1208
1243
|
}
|
|
1209
1244
|
|
|
1210
1245
|
if (!sessionId || !workspaceDir) return;
|
|
1246
|
+
let sessionIdLower = String(sessionId).toLowerCase();
|
|
1247
|
+
|
|
1248
|
+
let settingsPath = '';
|
|
1249
|
+
let sessionSettings = {};
|
|
1250
|
+
({ settingsPath, settings: sessionSettings } = resolveSessionSettings(workspaceDir, sessionId));
|
|
1251
|
+
|
|
1252
|
+
let configMtimeMs = safeStatMtimeMs(CONFIG_PATH);
|
|
1253
|
+
let globalSettingsMtimeMs = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1254
|
+
let globalSettingsModel = resolveGlobalSettingsModel();
|
|
1255
|
+
|
|
1256
|
+
let modelId =
|
|
1257
|
+
(sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null) ||
|
|
1258
|
+
globalSettingsModel ||
|
|
1259
|
+
null;
|
|
1260
|
+
|
|
1261
|
+
let provider =
|
|
1262
|
+
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1263
|
+
? sessionSettings.providerLock
|
|
1264
|
+
: resolveProvider(modelId, factoryConfig);
|
|
1265
|
+
let underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
|
|
1266
|
+
|
|
1267
|
+
function refreshModel() {
|
|
1268
|
+
const nextModelId =
|
|
1269
|
+
(sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null) ||
|
|
1270
|
+
globalSettingsModel ||
|
|
1271
|
+
null;
|
|
1272
|
+
|
|
1273
|
+
// Use providerLock if set, otherwise resolve from model/config (same logic as initialization)
|
|
1274
|
+
const nextProvider =
|
|
1275
|
+
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1276
|
+
? sessionSettings.providerLock
|
|
1277
|
+
: resolveProvider(nextModelId, factoryConfig);
|
|
1278
|
+
const nextUnderlying = resolveUnderlyingModelId(nextModelId, factoryConfig) || nextModelId || 'unknown';
|
|
1279
|
+
|
|
1280
|
+
let changed = false;
|
|
1281
|
+
if (nextModelId !== modelId) {
|
|
1282
|
+
modelId = nextModelId;
|
|
1283
|
+
changed = true;
|
|
1284
|
+
}
|
|
1285
|
+
if (nextProvider !== provider) {
|
|
1286
|
+
provider = nextProvider;
|
|
1287
|
+
changed = true;
|
|
1288
|
+
}
|
|
1289
|
+
if (nextUnderlying !== underlyingModel) {
|
|
1290
|
+
underlyingModel = nextUnderlying;
|
|
1291
|
+
changed = true;
|
|
1292
|
+
}
|
|
1211
1293
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
(settings && typeof settings.model === 'string' ? settings.model : null) || resolveGlobalSettingsModel();
|
|
1215
|
-
|
|
1216
|
-
const provider = resolveProvider(modelId, factoryConfig);
|
|
1217
|
-
const underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
|
|
1294
|
+
if (changed) renderNow();
|
|
1295
|
+
}
|
|
1218
1296
|
|
|
1219
1297
|
let last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1220
|
-
let sessionUsage =
|
|
1298
|
+
let sessionUsage =
|
|
1299
|
+
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1300
|
+
? sessionSettings.tokenUsage
|
|
1301
|
+
: {};
|
|
1221
1302
|
let compacting = false;
|
|
1222
1303
|
let lastRenderAt = 0;
|
|
1223
1304
|
let lastRenderedLine = '';
|
|
1224
1305
|
let gitBranch = '';
|
|
1225
1306
|
let gitDiff = '';
|
|
1307
|
+
let lastContextMs = 0;
|
|
1226
1308
|
|
|
1227
1309
|
function renderNow() {
|
|
1228
1310
|
const usedTokens = (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
|
|
@@ -1257,14 +1339,40 @@ async function main() {
|
|
|
1257
1339
|
} catch {}
|
|
1258
1340
|
}, 0).unref();
|
|
1259
1341
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1342
|
+
let reseedInProgress = false;
|
|
1343
|
+
let reseedQueued = false;
|
|
1344
|
+
|
|
1345
|
+
function updateLastFromContext(ctx, updateOutputTokens, tsMs) {
|
|
1346
|
+
const ts = Number.isFinite(tsMs) ? tsMs : null;
|
|
1347
|
+
if (ts != null && lastContextMs && ts < lastContextMs) return false;
|
|
1348
|
+
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1349
|
+
const contextCount = Number(ctx?.contextCount);
|
|
1350
|
+
const out = Number(ctx?.outputTokens);
|
|
1351
|
+
if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
|
|
1352
|
+
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1353
|
+
if (updateOutputTokens && Number.isFinite(out)) last.outputTokens = out;
|
|
1354
|
+
if (ts != null) lastContextMs = ts;
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function seedLastContextFromLog(options) {
|
|
1359
|
+
const opts = options || {};
|
|
1360
|
+
const maxScanBytes = Number.isFinite(opts.maxScanBytes) ? opts.maxScanBytes : 64 * 1024 * 1024;
|
|
1361
|
+
const preferStreaming = !!opts.preferStreaming;
|
|
1362
|
+
const minTimestampMs = Number.isFinite(lastContextMs) && lastContextMs > 0 ? lastContextMs : 0;
|
|
1363
|
+
const earlyStopAfterBestBytes = Math.min(2 * 1024 * 1024, Math.max(256 * 1024, maxScanBytes));
|
|
1364
|
+
|
|
1365
|
+
if (reseedInProgress) {
|
|
1366
|
+
reseedQueued = true;
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
reseedInProgress = true;
|
|
1370
|
+
|
|
1263
1371
|
setTimeout(() => {
|
|
1264
1372
|
try {
|
|
1265
|
-
// Backward scan to find the most recent
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1373
|
+
// Backward scan to find the most recent context entry for this session.
|
|
1374
|
+
// Prefer streaming context if requested; otherwise accept any context line
|
|
1375
|
+
// that includes cacheReadInputTokens/contextCount fields.
|
|
1268
1376
|
const CHUNK_BYTES = 1024 * 1024; // 1 MiB
|
|
1269
1377
|
|
|
1270
1378
|
const fd = fs.openSync(LOG_PATH, 'r');
|
|
@@ -1274,15 +1382,20 @@ async function main() {
|
|
|
1274
1382
|
let pos = size;
|
|
1275
1383
|
let scanned = 0;
|
|
1276
1384
|
let remainder = '';
|
|
1277
|
-
let
|
|
1385
|
+
let bestCtx = null;
|
|
1386
|
+
let bestIsStreaming = false;
|
|
1387
|
+
let bestTs = null;
|
|
1388
|
+
let bestHasTs = false;
|
|
1389
|
+
let bytesSinceBest = 0;
|
|
1278
1390
|
|
|
1279
|
-
while (pos > 0 && scanned <
|
|
1391
|
+
while (pos > 0 && scanned < maxScanBytes && (!bestHasTs || bytesSinceBest < earlyStopAfterBestBytes)) {
|
|
1280
1392
|
const readSize = Math.min(CHUNK_BYTES, pos);
|
|
1281
1393
|
const start = pos - readSize;
|
|
1282
1394
|
const buf = Buffer.alloc(readSize);
|
|
1283
1395
|
fs.readSync(fd, buf, 0, readSize, start);
|
|
1284
1396
|
pos = start;
|
|
1285
1397
|
scanned += readSize;
|
|
1398
|
+
bytesSinceBest += readSize;
|
|
1286
1399
|
|
|
1287
1400
|
let text = buf.toString('utf8') + remainder;
|
|
1288
1401
|
let lines = String(text).split('\\n');
|
|
@@ -1296,8 +1409,12 @@ async function main() {
|
|
|
1296
1409
|
const line = String(lines[i] || '').trimEnd();
|
|
1297
1410
|
if (!line) continue;
|
|
1298
1411
|
if (!line.includes('Context:')) continue;
|
|
1299
|
-
|
|
1300
|
-
if (!
|
|
1412
|
+
const sid = extractSessionIdFromLine(line);
|
|
1413
|
+
if (!sid || String(sid).toLowerCase() !== sessionIdLower) continue;
|
|
1414
|
+
|
|
1415
|
+
const isStreaming = line.includes('[Agent] Streaming result');
|
|
1416
|
+
if (preferStreaming && !isStreaming) continue;
|
|
1417
|
+
|
|
1301
1418
|
const ctxIndex = line.indexOf('Context: ');
|
|
1302
1419
|
if (ctxIndex === -1) continue;
|
|
1303
1420
|
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
@@ -1307,45 +1424,101 @@ async function main() {
|
|
|
1307
1424
|
} catch {
|
|
1308
1425
|
continue;
|
|
1309
1426
|
}
|
|
1310
|
-
|
|
1311
|
-
const
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
if (
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1427
|
+
|
|
1428
|
+
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1429
|
+
const contextCount = Number(ctx?.contextCount);
|
|
1430
|
+
const hasUsage = Number.isFinite(cacheRead) || Number.isFinite(contextCount);
|
|
1431
|
+
if (!hasUsage) continue;
|
|
1432
|
+
|
|
1433
|
+
const ts = parseLineTimestampMs(line);
|
|
1434
|
+
if (ts != null && minTimestampMs && ts < minTimestampMs) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (ts != null) {
|
|
1439
|
+
if (!bestHasTs || ts > bestTs) {
|
|
1440
|
+
bestCtx = ctx;
|
|
1441
|
+
bestIsStreaming = isStreaming;
|
|
1442
|
+
bestTs = ts;
|
|
1443
|
+
bestHasTs = true;
|
|
1444
|
+
bytesSinceBest = 0;
|
|
1445
|
+
}
|
|
1446
|
+
} else if (!bestHasTs && !bestCtx) {
|
|
1447
|
+
// No timestamps available yet: the first match when scanning backward
|
|
1448
|
+
// is the most recent in file order.
|
|
1449
|
+
bestCtx = ctx;
|
|
1450
|
+
bestIsStreaming = isStreaming;
|
|
1451
|
+
bestTs = null;
|
|
1452
|
+
}
|
|
1318
1453
|
}
|
|
1319
1454
|
|
|
1320
1455
|
if (remainder.length > 8192) remainder = remainder.slice(-8192);
|
|
1321
1456
|
}
|
|
1457
|
+
|
|
1458
|
+
if (bestCtx) {
|
|
1459
|
+
updateLastFromContext(bestCtx, bestIsStreaming, bestTs);
|
|
1460
|
+
}
|
|
1322
1461
|
} finally {
|
|
1323
1462
|
try {
|
|
1324
1463
|
fs.closeSync(fd);
|
|
1325
1464
|
} catch {}
|
|
1326
1465
|
}
|
|
1327
|
-
|
|
1328
|
-
renderNow();
|
|
1329
1466
|
} catch {
|
|
1330
1467
|
// ignore
|
|
1468
|
+
} finally {
|
|
1469
|
+
reseedInProgress = false;
|
|
1470
|
+
if (reseedQueued) {
|
|
1471
|
+
reseedQueued = false;
|
|
1472
|
+
seedLastContextFromLog({ maxScanBytes, preferStreaming });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
renderNow();
|
|
1331
1476
|
}
|
|
1332
1477
|
}, 0).unref();
|
|
1333
1478
|
}
|
|
1334
1479
|
|
|
1480
|
+
// Seed prompt-context usage from existing logs (important for resumed sessions).
|
|
1481
|
+
// Do this asynchronously to avoid delaying the first statusline frame.
|
|
1482
|
+
let initialSeedDone = false;
|
|
1483
|
+
if (resumeFlag || resumeId) {
|
|
1484
|
+
initialSeedDone = true;
|
|
1485
|
+
seedLastContextFromLog({ maxScanBytes: 64 * 1024 * 1024, preferStreaming: true });
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1335
1488
|
// Watch session settings for autonomy/reasoning changes (cheap polling with mtime).
|
|
1336
1489
|
let settingsMtimeMs = 0;
|
|
1490
|
+
let lastCtxPollMs = 0;
|
|
1337
1491
|
setInterval(() => {
|
|
1492
|
+
// Refresh config/global settings if they changed (model display depends on these).
|
|
1493
|
+
const configMtime = safeStatMtimeMs(CONFIG_PATH);
|
|
1494
|
+
if (configMtime && configMtime !== configMtimeMs) {
|
|
1495
|
+
configMtimeMs = configMtime;
|
|
1496
|
+
factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1497
|
+
refreshModel();
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const globalMtime = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1501
|
+
if (globalMtime && globalMtime !== globalSettingsMtimeMs) {
|
|
1502
|
+
globalSettingsMtimeMs = globalMtime;
|
|
1503
|
+
globalSettingsModel = resolveGlobalSettingsModel();
|
|
1504
|
+
refreshModel();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1338
1507
|
try {
|
|
1339
1508
|
const stat = fs.statSync(settingsPath);
|
|
1340
1509
|
if (stat.mtimeMs === settingsMtimeMs) return;
|
|
1341
1510
|
settingsMtimeMs = stat.mtimeMs;
|
|
1342
1511
|
const next = readJsonFile(settingsPath) || {};
|
|
1512
|
+
sessionSettings = next;
|
|
1343
1513
|
|
|
1344
1514
|
// Keep session token usage in sync (used by /status).
|
|
1345
1515
|
if (next && typeof next.tokenUsage === 'object' && next.tokenUsage) {
|
|
1346
1516
|
sessionUsage = next.tokenUsage;
|
|
1347
1517
|
}
|
|
1348
1518
|
|
|
1519
|
+
// Keep model/provider in sync (model can change during a running session).
|
|
1520
|
+
refreshModel();
|
|
1521
|
+
|
|
1349
1522
|
const now = Date.now();
|
|
1350
1523
|
if (now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
1351
1524
|
lastRenderAt = now;
|
|
@@ -1356,6 +1529,48 @@ async function main() {
|
|
|
1356
1529
|
}
|
|
1357
1530
|
}, 750).unref();
|
|
1358
1531
|
|
|
1532
|
+
// Fallback: periodically rescan log if context is still zero after startup.
|
|
1533
|
+
// This handles cases where tail misses early log entries.
|
|
1534
|
+
setInterval(() => {
|
|
1535
|
+
const now = Date.now();
|
|
1536
|
+
if (now - START_MS < 3000) return; // wait 3s after startup
|
|
1537
|
+
if (last.contextCount > 0 || last.cacheReadInputTokens > 0) return; // already have data
|
|
1538
|
+
if (now - lastCtxPollMs < 5000) return; // throttle to every 5s
|
|
1539
|
+
lastCtxPollMs = now;
|
|
1540
|
+
seedLastContextFromLog({ maxScanBytes: 4 * 1024 * 1024, preferStreaming: false });
|
|
1541
|
+
}, 2000).unref();
|
|
1542
|
+
|
|
1543
|
+
function switchToSession(nextSessionId) {
|
|
1544
|
+
if (!nextSessionId || !isUuid(nextSessionId)) return;
|
|
1545
|
+
const nextLower = String(nextSessionId).toLowerCase();
|
|
1546
|
+
if (nextLower === sessionIdLower) return;
|
|
1547
|
+
|
|
1548
|
+
sessionId = nextSessionId;
|
|
1549
|
+
sessionIdLower = nextLower;
|
|
1550
|
+
|
|
1551
|
+
const resolved = resolveSessionSettings(workspaceDir, nextSessionId);
|
|
1552
|
+
settingsPath = resolved.settingsPath;
|
|
1553
|
+
sessionSettings = resolved.settings || {};
|
|
1554
|
+
|
|
1555
|
+
sessionUsage =
|
|
1556
|
+
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1557
|
+
? sessionSettings.tokenUsage
|
|
1558
|
+
: {};
|
|
1559
|
+
|
|
1560
|
+
// Reset cached state for the new session.
|
|
1561
|
+
last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1562
|
+
lastContextMs = 0;
|
|
1563
|
+
compacting = false;
|
|
1564
|
+
settingsMtimeMs = 0;
|
|
1565
|
+
lastCtxPollMs = 0;
|
|
1566
|
+
|
|
1567
|
+
refreshModel();
|
|
1568
|
+
renderNow();
|
|
1569
|
+
|
|
1570
|
+
// Best-effort: if the new session already has Context lines in the log, seed quickly.
|
|
1571
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1359
1574
|
// Follow the Factory log and update based on session-scoped events.
|
|
1360
1575
|
const tail = spawn('tail', ['-n', '0', '-F', LOG_PATH], {
|
|
1361
1576
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -1370,8 +1585,90 @@ async function main() {
|
|
|
1370
1585
|
const line = buffer.slice(0, idx).trimEnd();
|
|
1371
1586
|
buffer = buffer.slice(idx + 1);
|
|
1372
1587
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1588
|
+
const tsMs = parseLineTimestampMs(line);
|
|
1589
|
+
const lineSessionId = extractSessionIdFromLine(line);
|
|
1590
|
+
const isSessionLine =
|
|
1591
|
+
lineSessionId && String(lineSessionId).toLowerCase() === sessionIdLower;
|
|
1592
|
+
|
|
1593
|
+
// /compress (aka /compact) can create a new session ID. Follow it so ctx/model keep updating.
|
|
1594
|
+
if (line.includes('oldSessionId') && line.includes('newSessionId') && line.includes('Context:')) {
|
|
1595
|
+
const ctxIndex = line.indexOf('Context: ');
|
|
1596
|
+
if (ctxIndex !== -1) {
|
|
1597
|
+
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1598
|
+
try {
|
|
1599
|
+
const meta = JSON.parse(jsonStr);
|
|
1600
|
+
const oldId = meta?.oldSessionId;
|
|
1601
|
+
const newId = meta?.newSessionId;
|
|
1602
|
+
if (
|
|
1603
|
+
isUuid(oldId) &&
|
|
1604
|
+
isUuid(newId) &&
|
|
1605
|
+
String(oldId).toLowerCase() === sessionIdLower &&
|
|
1606
|
+
String(newId).toLowerCase() !== sessionIdLower
|
|
1607
|
+
) {
|
|
1608
|
+
switchToSession(String(newId));
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
} catch {
|
|
1612
|
+
// ignore
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
let compactionChanged = false;
|
|
1618
|
+
let compactionEnded = false;
|
|
1619
|
+
if (line.includes('[Compaction]')) {
|
|
1620
|
+
// Accept session-scoped compaction lines; allow end markers to clear even
|
|
1621
|
+
// if the line lacks a session id (some builds omit Context on end lines).
|
|
1622
|
+
if (isSessionLine || (compacting && !lineSessionId)) {
|
|
1623
|
+
const next = nextCompactionState(line, compacting);
|
|
1624
|
+
if (next !== compacting) {
|
|
1625
|
+
compacting = next;
|
|
1626
|
+
compactionChanged = true;
|
|
1627
|
+
if (!compacting) compactionEnded = true;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if (compactionChanged && compacting) {
|
|
1633
|
+
// Compaction can start after a context-limit error. Ensure we display the latest
|
|
1634
|
+
// pre-compaction ctx by reseeding from log (tail can miss bursts).
|
|
1635
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: true });
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (compactionEnded) {
|
|
1639
|
+
// ctx usage changes dramatically after compaction, but the next Context line
|
|
1640
|
+
// can be delayed. Clear displayed ctx immediately to avoid showing stale numbers.
|
|
1641
|
+
last.cacheReadInputTokens = 0;
|
|
1642
|
+
last.contextCount = 0;
|
|
1643
|
+
if (tsMs != null) lastContextMs = tsMs;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (!line.includes('Context:')) {
|
|
1647
|
+
if (compactionChanged) {
|
|
1648
|
+
lastRenderAt = Date.now();
|
|
1649
|
+
renderNow();
|
|
1650
|
+
}
|
|
1651
|
+
if (compactionEnded) {
|
|
1652
|
+
// Compaction often completes between turns. Refresh ctx numbers promptly
|
|
1653
|
+
// by rescanning the most recent Context entry for this session.
|
|
1654
|
+
setTimeout(() => {
|
|
1655
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1656
|
+
}, 250).unref();
|
|
1657
|
+
}
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (!isSessionLine) {
|
|
1661
|
+
if (compactionChanged) {
|
|
1662
|
+
lastRenderAt = Date.now();
|
|
1663
|
+
renderNow();
|
|
1664
|
+
}
|
|
1665
|
+
if (compactionEnded) {
|
|
1666
|
+
setTimeout(() => {
|
|
1667
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1668
|
+
}, 250).unref();
|
|
1669
|
+
}
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1375
1672
|
|
|
1376
1673
|
const ctxIndex = line.indexOf('Context: ');
|
|
1377
1674
|
if (ctxIndex === -1) continue;
|
|
@@ -1380,28 +1677,41 @@ async function main() {
|
|
|
1380
1677
|
try {
|
|
1381
1678
|
ctx = JSON.parse(jsonStr);
|
|
1382
1679
|
} catch {
|
|
1680
|
+
if (compactionChanged) {
|
|
1681
|
+
lastRenderAt = Date.now();
|
|
1682
|
+
renderNow();
|
|
1683
|
+
}
|
|
1383
1684
|
continue;
|
|
1384
1685
|
}
|
|
1385
1686
|
|
|
1386
|
-
//
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1687
|
+
// Context usage can appear on multiple session-scoped log lines; update whenever present.
|
|
1688
|
+
// (Streaming is still the best source for outputTokens / LastOut.)
|
|
1689
|
+
updateLastFromContext(ctx, false, tsMs);
|
|
1690
|
+
|
|
1691
|
+
// For new sessions: if this is the first valid Context line and ctx is still 0,
|
|
1692
|
+
// trigger a reseed to catch any earlier log entries we might have missed.
|
|
1693
|
+
if (!initialSeedDone && last.contextCount === 0) {
|
|
1694
|
+
initialSeedDone = true;
|
|
1695
|
+
setTimeout(() => {
|
|
1696
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1697
|
+
}, 100).unref();
|
|
1394
1698
|
}
|
|
1395
1699
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1700
|
+
if (line.includes('[Agent] Streaming result')) {
|
|
1701
|
+
updateLastFromContext(ctx, true, tsMs);
|
|
1702
|
+
}
|
|
1399
1703
|
|
|
1400
1704
|
const now = Date.now();
|
|
1401
|
-
if (now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
1705
|
+
if (compactionChanged || now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
1402
1706
|
lastRenderAt = now;
|
|
1403
1707
|
renderNow();
|
|
1404
1708
|
}
|
|
1709
|
+
|
|
1710
|
+
if (compactionEnded) {
|
|
1711
|
+
setTimeout(() => {
|
|
1712
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1713
|
+
}, 250).unref();
|
|
1714
|
+
}
|
|
1405
1715
|
}
|
|
1406
1716
|
});
|
|
1407
1717
|
|
|
@@ -1419,986 +1729,944 @@ main().catch(() => {});
|
|
|
1419
1729
|
`;
|
|
1420
1730
|
}
|
|
1421
1731
|
function generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath) {
|
|
1422
|
-
return
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
if
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
os.write(stdout_fd, seq.encode("utf-8", "ignore"))
|
|
2361
|
-
except Exception:
|
|
2362
|
-
pass
|
|
2363
|
-
scroll_region_dirty = False
|
|
2364
|
-
|
|
2365
|
-
# Periodic force repaint to ensure statusline doesn't disappear
|
|
2366
|
-
now_ms = int(time.time() * 1000)
|
|
2367
|
-
if now_ms - last_force_repaint_ms >= FORCE_REPAINT_INTERVAL_MS:
|
|
2368
|
-
renderer.force_repaint(False)
|
|
2369
|
-
last_force_repaint_ms = now_ms
|
|
2370
|
-
|
|
2371
|
-
renderer.render(physical_rows, physical_cols, cr, cc)
|
|
2372
|
-
|
|
2373
|
-
finally:
|
|
2374
|
-
try:
|
|
2375
|
-
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty)
|
|
2376
|
-
except Exception:
|
|
2377
|
-
pass
|
|
2378
|
-
try:
|
|
2379
|
-
renderer.clear()
|
|
2380
|
-
except Exception:
|
|
2381
|
-
pass
|
|
2382
|
-
try:
|
|
2383
|
-
# Restore terminal scroll region and attributes.
|
|
2384
|
-
os.write(stdout_fd, b"\\x1b[r\\x1b[0m\\x1b[?25h")
|
|
2385
|
-
except Exception:
|
|
2386
|
-
pass
|
|
2387
|
-
try:
|
|
2388
|
-
os.close(master_fd)
|
|
2389
|
-
except Exception:
|
|
2390
|
-
pass
|
|
2391
|
-
if monitor is not None:
|
|
2392
|
-
try:
|
|
2393
|
-
monitor.terminate()
|
|
2394
|
-
except Exception:
|
|
2395
|
-
pass
|
|
2396
|
-
|
|
2397
|
-
sys.exit(child.returncode or 0)
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
if __name__ == "__main__":
|
|
2401
|
-
main()
|
|
1732
|
+
return generateStatuslineWrapperScriptBun(execTargetPath, monitorScriptPath, sessionsScriptPath);
|
|
1733
|
+
}
|
|
1734
|
+
function generateStatuslineWrapperScriptBun(execTargetPath, monitorScriptPath, sessionsScriptPath) {
|
|
1735
|
+
return `#!/usr/bin/env bun
|
|
1736
|
+
// Droid with Statusline (Bun PTY proxy)
|
|
1737
|
+
// Auto-generated by droid-patch --statusline
|
|
1738
|
+
|
|
1739
|
+
const EXEC_TARGET = ${JSON.stringify(execTargetPath)};
|
|
1740
|
+
const STATUSLINE_MONITOR = ${JSON.stringify(monitorScriptPath)};
|
|
1741
|
+
const SESSIONS_SCRIPT = ${sessionsScriptPath ? JSON.stringify(sessionsScriptPath) : "null"};
|
|
1742
|
+
|
|
1743
|
+
const IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === "Apple_Terminal";
|
|
1744
|
+
const MIN_RENDER_INTERVAL_MS = IS_APPLE_TERMINAL ? 800 : 400;
|
|
1745
|
+
const QUIET_MS = 50;
|
|
1746
|
+
const FORCE_REPAINT_INTERVAL_MS = 2000;
|
|
1747
|
+
const RESERVED_ROWS = 1;
|
|
1748
|
+
|
|
1749
|
+
const BYPASS_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
|
|
1750
|
+
const BYPASS_COMMANDS = new Set(["help", "version", "completion", "completions", "exec"]);
|
|
1751
|
+
|
|
1752
|
+
function shouldPassthrough(argv) {
|
|
1753
|
+
for (const a of argv) {
|
|
1754
|
+
if (a === "--") break;
|
|
1755
|
+
if (BYPASS_FLAGS.has(a)) return true;
|
|
1756
|
+
}
|
|
1757
|
+
let endOpts = false;
|
|
1758
|
+
let cmd = null;
|
|
1759
|
+
for (const a of argv) {
|
|
1760
|
+
if (a === "--") {
|
|
1761
|
+
endOpts = true;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
if (!endOpts && a.startsWith("-")) continue;
|
|
1765
|
+
cmd = a;
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
return cmd && BYPASS_COMMANDS.has(cmd);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function isSessionsCommand(argv) {
|
|
1772
|
+
for (const a of argv) {
|
|
1773
|
+
if (a === "--") return false;
|
|
1774
|
+
if (a === "--sessions") return true;
|
|
1775
|
+
}
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
async function execPassthrough(argv) {
|
|
1780
|
+
const proc = Bun.spawn([EXEC_TARGET, ...argv], {
|
|
1781
|
+
stdin: "inherit",
|
|
1782
|
+
stdout: "inherit",
|
|
1783
|
+
stderr: "inherit",
|
|
1784
|
+
});
|
|
1785
|
+
const code = await proc.exited;
|
|
1786
|
+
process.exit(code ?? 0);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
async function runSessions() {
|
|
1790
|
+
if (SESSIONS_SCRIPT) {
|
|
1791
|
+
const proc = Bun.spawn(["node", String(SESSIONS_SCRIPT)], {
|
|
1792
|
+
stdin: "inherit",
|
|
1793
|
+
stdout: "inherit",
|
|
1794
|
+
stderr: "inherit",
|
|
1795
|
+
});
|
|
1796
|
+
const code = await proc.exited;
|
|
1797
|
+
process.exit(code ?? 0);
|
|
1798
|
+
}
|
|
1799
|
+
process.stderr.write("[statusline] sessions script not found\\n");
|
|
1800
|
+
process.exit(1);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function writeStdout(s) {
|
|
1804
|
+
try {
|
|
1805
|
+
process.stdout.write(s);
|
|
1806
|
+
} catch {
|
|
1807
|
+
// ignore
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function termSize() {
|
|
1812
|
+
const rows = Number(process.stdout.rows || 24);
|
|
1813
|
+
const cols = Number(process.stdout.columns || 80);
|
|
1814
|
+
return { rows: Number.isFinite(rows) ? rows : 24, cols: Number.isFinite(cols) ? cols : 80 };
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const ANSI_RE = /\\x1b\\[[0-9;]*m/g;
|
|
1818
|
+
const RESET_SGR = "\\x1b[0m";
|
|
1819
|
+
|
|
1820
|
+
function visibleWidth(text) {
|
|
1821
|
+
return String(text || "").replace(ANSI_RE, "").length;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function clampAnsi(text, cols) {
|
|
1825
|
+
if (!cols || cols <= 0) return String(text || "");
|
|
1826
|
+
cols = cols > 1 ? cols - 1 : cols; // avoid last-column wrap
|
|
1827
|
+
if (cols < 10) return String(text || "");
|
|
1828
|
+
const s = String(text || "");
|
|
1829
|
+
let visible = 0;
|
|
1830
|
+
let i = 0;
|
|
1831
|
+
const out = [];
|
|
1832
|
+
while (i < s.length) {
|
|
1833
|
+
const ch = s[i];
|
|
1834
|
+
if (ch === "\\x1b") {
|
|
1835
|
+
const m = s.indexOf("m", i);
|
|
1836
|
+
if (m !== -1) {
|
|
1837
|
+
out.push(s.slice(i, m + 1));
|
|
1838
|
+
i = m + 1;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
out.push(ch);
|
|
1842
|
+
i += 1;
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
if (visible >= cols) break;
|
|
1846
|
+
out.push(ch);
|
|
1847
|
+
i += 1;
|
|
1848
|
+
visible += 1;
|
|
1849
|
+
}
|
|
1850
|
+
if (i < s.length && cols >= 1) {
|
|
1851
|
+
if (visible >= cols) {
|
|
1852
|
+
if (out.length) out[out.length - 1] = "…";
|
|
1853
|
+
else out.push("…");
|
|
1854
|
+
} else {
|
|
1855
|
+
out.push("…");
|
|
1856
|
+
}
|
|
1857
|
+
out.push(RESET_SGR);
|
|
1858
|
+
}
|
|
1859
|
+
return out.join("");
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function splitSegments(text) {
|
|
1863
|
+
if (!text) return [];
|
|
1864
|
+
const s = String(text);
|
|
1865
|
+
const segments = [];
|
|
1866
|
+
let start = 0;
|
|
1867
|
+
while (true) {
|
|
1868
|
+
const idx = s.indexOf(RESET_SGR, start);
|
|
1869
|
+
if (idx === -1) {
|
|
1870
|
+
const tail = s.slice(start);
|
|
1871
|
+
if (tail) segments.push(tail);
|
|
1872
|
+
break;
|
|
1873
|
+
}
|
|
1874
|
+
const seg = s.slice(start, idx + RESET_SGR.length);
|
|
1875
|
+
if (seg) segments.push(seg);
|
|
1876
|
+
start = idx + RESET_SGR.length;
|
|
1877
|
+
}
|
|
1878
|
+
return segments;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function wrapSegments(segments, cols) {
|
|
1882
|
+
if (!segments || segments.length === 0) return [""];
|
|
1883
|
+
if (!cols || cols <= 0) return [segments.join("")];
|
|
1884
|
+
|
|
1885
|
+
const lines = [];
|
|
1886
|
+
let cur = [];
|
|
1887
|
+
let curW = 0;
|
|
1888
|
+
|
|
1889
|
+
for (let seg of segments) {
|
|
1890
|
+
let segW = visibleWidth(seg);
|
|
1891
|
+
if (segW <= 0) continue;
|
|
1892
|
+
|
|
1893
|
+
if (cur.length === 0) {
|
|
1894
|
+
if (segW > cols) {
|
|
1895
|
+
seg = clampAnsi(seg, cols);
|
|
1896
|
+
segW = visibleWidth(seg);
|
|
1897
|
+
}
|
|
1898
|
+
cur = [seg];
|
|
1899
|
+
curW = segW;
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (curW + segW <= cols) {
|
|
1904
|
+
cur.push(seg);
|
|
1905
|
+
curW += segW;
|
|
1906
|
+
} else {
|
|
1907
|
+
lines.push(cur.join(""));
|
|
1908
|
+
if (segW > cols) {
|
|
1909
|
+
seg = clampAnsi(seg, cols);
|
|
1910
|
+
segW = visibleWidth(seg);
|
|
1911
|
+
}
|
|
1912
|
+
cur = [seg];
|
|
1913
|
+
curW = segW;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (cur.length) lines.push(cur.join(""));
|
|
1918
|
+
return lines.length ? lines : [""];
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
class StatusRenderer {
|
|
1922
|
+
constructor() {
|
|
1923
|
+
this.raw = "";
|
|
1924
|
+
this.segments = [];
|
|
1925
|
+
this.lines = [""];
|
|
1926
|
+
this.activeReservedRows = RESERVED_ROWS;
|
|
1927
|
+
this.force = false;
|
|
1928
|
+
this.urgent = false;
|
|
1929
|
+
this.lastRenderMs = 0;
|
|
1930
|
+
this.lastChildOutMs = 0;
|
|
1931
|
+
this.cursorVisible = true;
|
|
1932
|
+
}
|
|
1933
|
+
noteChildOutput() {
|
|
1934
|
+
this.lastChildOutMs = Date.now();
|
|
1935
|
+
}
|
|
1936
|
+
setCursorVisible(v) {
|
|
1937
|
+
this.cursorVisible = !!v;
|
|
1938
|
+
}
|
|
1939
|
+
forceRepaint(urgent = false) {
|
|
1940
|
+
this.force = true;
|
|
1941
|
+
if (urgent) this.urgent = true;
|
|
1942
|
+
}
|
|
1943
|
+
setActiveReservedRows(n) {
|
|
1944
|
+
const v = Number(n || 1);
|
|
1945
|
+
this.activeReservedRows = Number.isFinite(v) ? Math.max(1, Math.trunc(v)) : 1;
|
|
1946
|
+
}
|
|
1947
|
+
setLine(line) {
|
|
1948
|
+
const next = String(line || "");
|
|
1949
|
+
if (next !== this.raw) {
|
|
1950
|
+
this.raw = next;
|
|
1951
|
+
this.segments = splitSegments(next);
|
|
1952
|
+
this.force = true;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
desiredReservedRows(physicalRows, cols, minReserved) {
|
|
1956
|
+
let rows = Number(physicalRows || 24);
|
|
1957
|
+
rows = Number.isFinite(rows) ? rows : 24;
|
|
1958
|
+
cols = Number(cols || 80);
|
|
1959
|
+
cols = Number.isFinite(cols) ? cols : 80;
|
|
1960
|
+
|
|
1961
|
+
const maxReserved = Math.max(1, rows - 4);
|
|
1962
|
+
const segs = this.segments.length ? this.segments : (this.raw ? [this.raw] : []);
|
|
1963
|
+
let lines = segs.length ? wrapSegments(segs, cols) : [""];
|
|
1964
|
+
|
|
1965
|
+
const needed = Math.min(lines.length, maxReserved);
|
|
1966
|
+
let desired = Math.max(Number(minReserved || 1), needed);
|
|
1967
|
+
desired = Math.min(desired, maxReserved);
|
|
1968
|
+
|
|
1969
|
+
if (lines.length < desired) lines = new Array(desired - lines.length).fill("").concat(lines);
|
|
1970
|
+
if (lines.length > desired) lines = lines.slice(-desired);
|
|
1971
|
+
|
|
1972
|
+
this.lines = lines;
|
|
1973
|
+
return desired;
|
|
1974
|
+
}
|
|
1975
|
+
clearReservedArea(physicalRows, cols, reservedRows, restoreRow = 1, restoreCol = 1) {
|
|
1976
|
+
let rows = Number(physicalRows || 24);
|
|
1977
|
+
rows = Number.isFinite(rows) ? rows : 24;
|
|
1978
|
+
cols = Number(cols || 80);
|
|
1979
|
+
cols = Number.isFinite(cols) ? cols : 80;
|
|
1980
|
+
let reserved = Number(reservedRows || 1);
|
|
1981
|
+
reserved = Number.isFinite(reserved) ? Math.max(1, Math.trunc(reserved)) : 1;
|
|
1982
|
+
|
|
1983
|
+
reserved = Math.min(reserved, rows);
|
|
1984
|
+
const startRow = rows - reserved + 1;
|
|
1985
|
+
const parts = ["\\x1b[?2026h", "\\x1b[?25l", RESET_SGR];
|
|
1986
|
+
for (let i = 0; i < reserved; i++) parts.push("\\x1b[" + (startRow + i) + ";1H\\x1b[2K");
|
|
1987
|
+
parts.push("\\x1b[" + restoreRow + ";" + restoreCol + "H");
|
|
1988
|
+
parts.push(this.cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l");
|
|
1989
|
+
parts.push("\\x1b[?2026l");
|
|
1990
|
+
writeStdout(parts.join(""));
|
|
1991
|
+
}
|
|
1992
|
+
render(physicalRows, cols, restoreRow = 1, restoreCol = 1) {
|
|
1993
|
+
if (!this.force) return;
|
|
1994
|
+
if (!this.raw) {
|
|
1995
|
+
this.force = false;
|
|
1996
|
+
this.urgent = false;
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const now = Date.now();
|
|
2000
|
+
if (!this.urgent && now - this.lastRenderMs < MIN_RENDER_INTERVAL_MS) return;
|
|
2001
|
+
if (!this.urgent && QUIET_MS > 0 && now - this.lastChildOutMs < QUIET_MS) return;
|
|
2002
|
+
|
|
2003
|
+
let rows = Number(physicalRows || 24);
|
|
2004
|
+
rows = Number.isFinite(rows) ? rows : 24;
|
|
2005
|
+
cols = Number(cols || 80);
|
|
2006
|
+
cols = Number.isFinite(cols) ? cols : 80;
|
|
2007
|
+
if (cols <= 0) cols = 80;
|
|
2008
|
+
|
|
2009
|
+
const reserved = Math.max(1, Math.min(this.activeReservedRows, Math.max(1, rows - 4)));
|
|
2010
|
+
const startRow = rows - reserved + 1;
|
|
2011
|
+
const childRows = rows - reserved;
|
|
2012
|
+
|
|
2013
|
+
let lines = this.lines.length ? this.lines.slice() : [""];
|
|
2014
|
+
if (lines.length < reserved) lines = new Array(reserved - lines.length).fill("").concat(lines);
|
|
2015
|
+
if (lines.length > reserved) lines = lines.slice(-reserved);
|
|
2016
|
+
|
|
2017
|
+
const parts = ["\\x1b[?2026h", "\\x1b[?25l"];
|
|
2018
|
+
parts.push("\\x1b[1;" + childRows + "r");
|
|
2019
|
+
for (let i = 0; i < reserved; i++) {
|
|
2020
|
+
const row = startRow + i;
|
|
2021
|
+
const text = clampAnsi(lines[i], cols);
|
|
2022
|
+
parts.push("\\x1b[" + row + ";1H" + RESET_SGR + "\\x1b[2K");
|
|
2023
|
+
parts.push("\\x1b[" + row + ";1H" + text + RESET_SGR);
|
|
2024
|
+
}
|
|
2025
|
+
parts.push("\\x1b[" + restoreRow + ";" + restoreCol + "H");
|
|
2026
|
+
parts.push(this.cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l");
|
|
2027
|
+
parts.push("\\x1b[?2026l");
|
|
2028
|
+
writeStdout(parts.join(""));
|
|
2029
|
+
|
|
2030
|
+
this.lastRenderMs = now;
|
|
2031
|
+
this.force = false;
|
|
2032
|
+
this.urgent = false;
|
|
2033
|
+
}
|
|
2034
|
+
clear() {
|
|
2035
|
+
const { rows, cols } = termSize();
|
|
2036
|
+
this.clearReservedArea(rows, cols, Math.max(this.activeReservedRows, RESERVED_ROWS));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
class OutputRewriter {
|
|
2041
|
+
constructor() {
|
|
2042
|
+
this.buf = new Uint8Array(0);
|
|
2043
|
+
}
|
|
2044
|
+
feed(chunk, maxRow) {
|
|
2045
|
+
if (!chunk || chunk.length === 0) return chunk;
|
|
2046
|
+
const merged = new Uint8Array(this.buf.length + chunk.length);
|
|
2047
|
+
merged.set(this.buf, 0);
|
|
2048
|
+
merged.set(chunk, this.buf.length);
|
|
2049
|
+
this.buf = new Uint8Array(0);
|
|
2050
|
+
|
|
2051
|
+
const out = [];
|
|
2052
|
+
let i = 0;
|
|
2053
|
+
|
|
2054
|
+
const isFinal = (v) => v >= 0x40 && v <= 0x7e;
|
|
2055
|
+
|
|
2056
|
+
while (i < merged.length) {
|
|
2057
|
+
const b = merged[i];
|
|
2058
|
+
if (b !== 0x1b) {
|
|
2059
|
+
out.push(b);
|
|
2060
|
+
i += 1;
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (i + 1 >= merged.length) {
|
|
2064
|
+
this.buf = merged.slice(i);
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
const nxt = merged[i + 1];
|
|
2068
|
+
if (nxt !== 0x5b) {
|
|
2069
|
+
out.push(b);
|
|
2070
|
+
i += 1;
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
let j = i + 2;
|
|
2075
|
+
while (j < merged.length && !isFinal(merged[j])) j += 1;
|
|
2076
|
+
if (j >= merged.length) {
|
|
2077
|
+
this.buf = merged.slice(i);
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
const final = merged[j];
|
|
2081
|
+
let seq = merged.slice(i, j + 1);
|
|
2082
|
+
|
|
2083
|
+
if ((final === 0x48 || final === 0x66) && maxRow > 0) {
|
|
2084
|
+
const params = merged.slice(i + 2, j);
|
|
2085
|
+
const s = new TextDecoder().decode(params);
|
|
2086
|
+
if (!s || /^[0-9;]/.test(s)) {
|
|
2087
|
+
const parts = s ? s.split(";") : [];
|
|
2088
|
+
const row = Number(parts[0] || 1);
|
|
2089
|
+
const col = Number(parts[1] || 1);
|
|
2090
|
+
let r = Number.isFinite(row) ? row : 1;
|
|
2091
|
+
let c = Number.isFinite(col) ? col : 1;
|
|
2092
|
+
if (r === 999 || r > maxRow) r = maxRow;
|
|
2093
|
+
if (r < 1) r = 1;
|
|
2094
|
+
if (c < 1) c = 1;
|
|
2095
|
+
const newParams = new TextEncoder().encode(String(r) + ";" + String(c));
|
|
2096
|
+
const ns = new Uint8Array(2 + newParams.length + 1);
|
|
2097
|
+
ns[0] = 0x1b;
|
|
2098
|
+
ns[1] = 0x5b;
|
|
2099
|
+
ns.set(newParams, 2);
|
|
2100
|
+
ns[ns.length - 1] = final;
|
|
2101
|
+
seq = ns;
|
|
2102
|
+
}
|
|
2103
|
+
} else if (final === 0x72 && maxRow > 0) {
|
|
2104
|
+
const params = merged.slice(i + 2, j);
|
|
2105
|
+
const s = new TextDecoder().decode(params);
|
|
2106
|
+
if (!s || /^[0-9;]/.test(s)) {
|
|
2107
|
+
const parts = s ? s.split(";") : [];
|
|
2108
|
+
const top = Number(parts[0] || 1);
|
|
2109
|
+
const bottom = Number(parts[1] || maxRow);
|
|
2110
|
+
let t = Number.isFinite(top) ? top : 1;
|
|
2111
|
+
let btm = Number.isFinite(bottom) ? bottom : maxRow;
|
|
2112
|
+
if (t <= 0) t = 1;
|
|
2113
|
+
if (btm <= 0 || btm === 999 || btm > maxRow) btm = maxRow;
|
|
2114
|
+
if (t > btm) t = 1;
|
|
2115
|
+
const str = "\\x1b[" + String(t) + ";" + String(btm) + "r";
|
|
2116
|
+
seq = new TextEncoder().encode(str);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
for (const bb of seq) out.push(bb);
|
|
2121
|
+
i = j + 1;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
return new Uint8Array(out);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
class CursorTracker {
|
|
2129
|
+
constructor() {
|
|
2130
|
+
this.row = 1;
|
|
2131
|
+
this.col = 1;
|
|
2132
|
+
this.savedRow = 1;
|
|
2133
|
+
this.savedCol = 1;
|
|
2134
|
+
this.buf = new Uint8Array(0);
|
|
2135
|
+
this.inOsc = false;
|
|
2136
|
+
this.utf8Cont = 0;
|
|
2137
|
+
this.wrapPending = false;
|
|
2138
|
+
}
|
|
2139
|
+
position() {
|
|
2140
|
+
return { row: this.row, col: this.col };
|
|
2141
|
+
}
|
|
2142
|
+
feed(chunk, maxRow, maxCol) {
|
|
2143
|
+
if (!chunk || chunk.length === 0) return;
|
|
2144
|
+
maxRow = Math.max(1, Number(maxRow || 1));
|
|
2145
|
+
maxCol = Math.max(1, Number(maxCol || 1));
|
|
2146
|
+
|
|
2147
|
+
const merged = new Uint8Array(this.buf.length + chunk.length);
|
|
2148
|
+
merged.set(this.buf, 0);
|
|
2149
|
+
merged.set(chunk, this.buf.length);
|
|
2150
|
+
this.buf = new Uint8Array(0);
|
|
2151
|
+
|
|
2152
|
+
const clamp = () => {
|
|
2153
|
+
if (this.row < 1) this.row = 1;
|
|
2154
|
+
else if (this.row > maxRow) this.row = maxRow;
|
|
2155
|
+
if (this.col < 1) this.col = 1;
|
|
2156
|
+
else if (this.col > maxCol) this.col = maxCol;
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
const parseIntDefault = (v, d) => {
|
|
2160
|
+
const n = Number(v);
|
|
2161
|
+
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : d;
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
let i = 0;
|
|
2165
|
+
const isFinal = (v) => v >= 0x40 && v <= 0x7e;
|
|
2166
|
+
|
|
2167
|
+
while (i < merged.length) {
|
|
2168
|
+
const b = merged[i];
|
|
2169
|
+
|
|
2170
|
+
if (this.inOsc) {
|
|
2171
|
+
if (b === 0x07) {
|
|
2172
|
+
this.inOsc = false;
|
|
2173
|
+
i += 1;
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
if (b === 0x1b) {
|
|
2177
|
+
if (i + 1 >= merged.length) {
|
|
2178
|
+
this.buf = merged.slice(i);
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
if (merged[i + 1] === 0x5c) {
|
|
2182
|
+
this.inOsc = false;
|
|
2183
|
+
i += 2;
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
i += 1;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
if (this.utf8Cont > 0) {
|
|
2192
|
+
if (b >= 0x80 && b <= 0xbf) {
|
|
2193
|
+
this.utf8Cont -= 1;
|
|
2194
|
+
i += 1;
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
this.utf8Cont = 0;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if (b === 0x1b) {
|
|
2201
|
+
this.wrapPending = false;
|
|
2202
|
+
if (i + 1 >= merged.length) {
|
|
2203
|
+
this.buf = merged.slice(i);
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
const nxt = merged[i + 1];
|
|
2207
|
+
|
|
2208
|
+
if (nxt === 0x5b) {
|
|
2209
|
+
let j = i + 2;
|
|
2210
|
+
while (j < merged.length && !isFinal(merged[j])) j += 1;
|
|
2211
|
+
if (j >= merged.length) {
|
|
2212
|
+
this.buf = merged.slice(i);
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
const final = merged[j];
|
|
2216
|
+
const params = merged.slice(i + 2, j);
|
|
2217
|
+
const s = new TextDecoder().decode(params);
|
|
2218
|
+
if (s && !/^[0-9;]/.test(s)) {
|
|
2219
|
+
i = j + 1;
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const parts = s ? s.split(";") : [];
|
|
2223
|
+
const p0 = parseIntDefault(parts[0] || "", 1);
|
|
2224
|
+
const p1 = parseIntDefault(parts[1] || "", 1);
|
|
2225
|
+
|
|
2226
|
+
if (final === 0x48 || final === 0x66) {
|
|
2227
|
+
this.row = p0;
|
|
2228
|
+
this.col = p1;
|
|
2229
|
+
clamp();
|
|
2230
|
+
} else if (final === 0x41) {
|
|
2231
|
+
this.row = Math.max(1, this.row - p0);
|
|
2232
|
+
} else if (final === 0x42) {
|
|
2233
|
+
this.row = Math.min(maxRow, this.row + p0);
|
|
2234
|
+
} else if (final === 0x43) {
|
|
2235
|
+
this.col = Math.min(maxCol, this.col + p0);
|
|
2236
|
+
} else if (final === 0x44) {
|
|
2237
|
+
this.col = Math.max(1, this.col - p0);
|
|
2238
|
+
} else if (final === 0x45) {
|
|
2239
|
+
this.row = Math.min(maxRow, this.row + p0);
|
|
2240
|
+
this.col = 1;
|
|
2241
|
+
} else if (final === 0x46) {
|
|
2242
|
+
this.row = Math.max(1, this.row - p0);
|
|
2243
|
+
this.col = 1;
|
|
2244
|
+
} else if (final === 0x47) {
|
|
2245
|
+
this.col = p0;
|
|
2246
|
+
clamp();
|
|
2247
|
+
} else if (final === 0x64) {
|
|
2248
|
+
this.row = p0;
|
|
2249
|
+
clamp();
|
|
2250
|
+
} else if (final === 0x72) {
|
|
2251
|
+
this.row = 1;
|
|
2252
|
+
this.col = 1;
|
|
2253
|
+
} else if (final === 0x73) {
|
|
2254
|
+
this.savedRow = this.row;
|
|
2255
|
+
this.savedCol = this.col;
|
|
2256
|
+
} else if (final === 0x75) {
|
|
2257
|
+
this.row = this.savedRow;
|
|
2258
|
+
this.col = this.savedCol;
|
|
2259
|
+
clamp();
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
i = j + 1;
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (nxt === 0x5d || nxt === 0x50 || nxt === 0x5e || nxt === 0x5f || nxt === 0x58) {
|
|
2267
|
+
this.inOsc = true;
|
|
2268
|
+
i += 2;
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
if (nxt === 0x37) {
|
|
2273
|
+
this.savedRow = this.row;
|
|
2274
|
+
this.savedCol = this.col;
|
|
2275
|
+
i += 2;
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
if (nxt === 0x38) {
|
|
2279
|
+
this.row = this.savedRow;
|
|
2280
|
+
this.col = this.savedCol;
|
|
2281
|
+
clamp();
|
|
2282
|
+
i += 2;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
i += 2;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (b === 0x0d) {
|
|
2291
|
+
this.col = 1;
|
|
2292
|
+
this.wrapPending = false;
|
|
2293
|
+
i += 1;
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
if (b === 0x0a || b === 0x0b || b === 0x0c) {
|
|
2297
|
+
this.row = Math.min(maxRow, this.row + 1);
|
|
2298
|
+
this.wrapPending = false;
|
|
2299
|
+
i += 1;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (b === 0x08) {
|
|
2303
|
+
this.col = Math.max(1, this.col - 1);
|
|
2304
|
+
this.wrapPending = false;
|
|
2305
|
+
i += 1;
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
if (b === 0x09) {
|
|
2309
|
+
const nextStop = Math.floor((this.col - 1) / 8 + 1) * 8 + 1;
|
|
2310
|
+
this.col = Math.min(maxCol, nextStop);
|
|
2311
|
+
this.wrapPending = false;
|
|
2312
|
+
i += 1;
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (b < 0x20 || b === 0x7f) {
|
|
2316
|
+
i += 1;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (this.wrapPending) {
|
|
2321
|
+
this.row = Math.min(maxRow, this.row + 1);
|
|
2322
|
+
this.col = 1;
|
|
2323
|
+
this.wrapPending = false;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (b >= 0x80) {
|
|
2327
|
+
if ((b & 0xe0) === 0xc0) this.utf8Cont = 1;
|
|
2328
|
+
else if ((b & 0xf0) === 0xe0) this.utf8Cont = 2;
|
|
2329
|
+
else if ((b & 0xf8) === 0xf0) this.utf8Cont = 3;
|
|
2330
|
+
else this.utf8Cont = 0;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (this.col < maxCol) this.col += 1;
|
|
2334
|
+
else {
|
|
2335
|
+
this.col = maxCol;
|
|
2336
|
+
this.wrapPending = true;
|
|
2337
|
+
}
|
|
2338
|
+
i += 1;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
async function main() {
|
|
2344
|
+
const argv = process.argv.slice(2);
|
|
2345
|
+
|
|
2346
|
+
if (isSessionsCommand(argv)) await runSessions();
|
|
2347
|
+
|
|
2348
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || shouldPassthrough(argv)) {
|
|
2349
|
+
await execPassthrough(argv);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// Clean viewport.
|
|
2354
|
+
writeStdout("\\x1b[?2026h\\x1b[0m\\x1b[r\\x1b[2J\\x1b[H\\x1b[?2026l");
|
|
2355
|
+
|
|
2356
|
+
const renderer = new StatusRenderer();
|
|
2357
|
+
renderer.setLine("\\x1b[48;5;238m\\x1b[38;5;15m Statusline: starting… \\x1b[0m");
|
|
2358
|
+
renderer.forceRepaint(true);
|
|
2359
|
+
|
|
2360
|
+
let { rows: physicalRows, cols: physicalCols } = termSize();
|
|
2361
|
+
let effectiveReservedRows = renderer.desiredReservedRows(physicalRows, physicalCols, RESERVED_ROWS);
|
|
2362
|
+
renderer.setActiveReservedRows(effectiveReservedRows);
|
|
2363
|
+
let childRows = Math.max(4, physicalRows - effectiveReservedRows);
|
|
2364
|
+
let childCols = Math.max(10, physicalCols);
|
|
2365
|
+
|
|
2366
|
+
// Reserve the bottom rows early, before the child starts writing.
|
|
2367
|
+
writeStdout(
|
|
2368
|
+
"\\x1b[?2026h\\x1b[?25l\\x1b[1;" + childRows + "r\\x1b[1;1H\\x1b[?25h\\x1b[?2026l",
|
|
2369
|
+
);
|
|
2370
|
+
renderer.forceRepaint(true);
|
|
2371
|
+
renderer.render(physicalRows, physicalCols, 1, 1);
|
|
2372
|
+
|
|
2373
|
+
// Spawn child with terminal support.
|
|
2374
|
+
let child;
|
|
2375
|
+
try {
|
|
2376
|
+
child = Bun.spawn([EXEC_TARGET, ...argv], {
|
|
2377
|
+
cwd: process.cwd(),
|
|
2378
|
+
env: process.env,
|
|
2379
|
+
detached: true,
|
|
2380
|
+
terminal: {
|
|
2381
|
+
cols: childCols,
|
|
2382
|
+
rows: childRows,
|
|
2383
|
+
data(_terminal, data) {
|
|
2384
|
+
onChildData(data);
|
|
2385
|
+
},
|
|
2386
|
+
},
|
|
2387
|
+
onExit(_proc, exitCode, signal, _error) {
|
|
2388
|
+
onChildExit(exitCode, signal);
|
|
2389
|
+
},
|
|
2390
|
+
});
|
|
2391
|
+
} catch (e) {
|
|
2392
|
+
process.stderr.write("[statusline] failed to spawn child: " + String(e?.message || e) + "\\n");
|
|
2393
|
+
process.exit(1);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
const terminal = child.terminal;
|
|
2397
|
+
|
|
2398
|
+
// Best-effort PGID resolution (matches Python wrapper behavior).
|
|
2399
|
+
// This improves session resolution (ps/lsof scanning) and signal forwarding.
|
|
2400
|
+
let pgid = child.pid;
|
|
2401
|
+
try {
|
|
2402
|
+
const res = Bun.spawnSync(["ps", "-o", "pgid=", "-p", String(child.pid)], {
|
|
2403
|
+
stdin: "ignore",
|
|
2404
|
+
stdout: "pipe",
|
|
2405
|
+
stderr: "ignore",
|
|
2406
|
+
});
|
|
2407
|
+
if (res && res.exitCode === 0 && res.stdout) {
|
|
2408
|
+
const text = new TextDecoder().decode(res.stdout).trim();
|
|
2409
|
+
const n = Number(text);
|
|
2410
|
+
if (Number.isFinite(n) && n > 0) pgid = Math.trunc(n);
|
|
2411
|
+
}
|
|
2412
|
+
} catch {}
|
|
2413
|
+
|
|
2414
|
+
// Spawn monitor (Node).
|
|
2415
|
+
const monitorEnv = { ...process.env, DROID_STATUSLINE_PGID: String(pgid) };
|
|
2416
|
+
const monitor = Bun.spawn(["node", STATUSLINE_MONITOR, ...argv], {
|
|
2417
|
+
stdin: "ignore",
|
|
2418
|
+
stdout: "pipe",
|
|
2419
|
+
stderr: "ignore",
|
|
2420
|
+
env: monitorEnv,
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
let shouldStop = false;
|
|
2424
|
+
const rewriter = new OutputRewriter();
|
|
2425
|
+
const cursor = new CursorTracker();
|
|
2426
|
+
|
|
2427
|
+
let detectBuf = new Uint8Array(0);
|
|
2428
|
+
let detectStr = "";
|
|
2429
|
+
let cursorVisible = true;
|
|
2430
|
+
let scrollRegionDirty = true;
|
|
2431
|
+
let lastForceRepaintMs = Date.now();
|
|
2432
|
+
let lastPhysicalRows = 0;
|
|
2433
|
+
let lastPhysicalCols = 0;
|
|
2434
|
+
|
|
2435
|
+
function appendDetect(chunk) {
|
|
2436
|
+
const max = 128;
|
|
2437
|
+
const merged = new Uint8Array(Math.min(max, detectBuf.length + chunk.length));
|
|
2438
|
+
const takePrev = Math.max(0, merged.length - chunk.length);
|
|
2439
|
+
if (takePrev > 0) merged.set(detectBuf.slice(Math.max(0, detectBuf.length - takePrev)), 0);
|
|
2440
|
+
merged.set(chunk.slice(Math.max(0, chunk.length - (merged.length - takePrev))), takePrev);
|
|
2441
|
+
detectBuf = merged;
|
|
2442
|
+
try {
|
|
2443
|
+
detectStr = Buffer.from(detectBuf).toString("latin1");
|
|
2444
|
+
} catch {
|
|
2445
|
+
detectStr = "";
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function includesBytes(needle) {
|
|
2450
|
+
return detectStr.includes(needle);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function lastIndexOfBytes(needle) {
|
|
2454
|
+
return detectStr.lastIndexOf(needle);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function includesScrollRegionCSI() {
|
|
2458
|
+
return /\\x1b\\[[0-9]*;?[0-9]*r/.test(detectStr);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function updateCursorVisibility() {
|
|
2462
|
+
const show = includesBytes("\\x1b[?25h");
|
|
2463
|
+
const hide = includesBytes("\\x1b[?25l");
|
|
2464
|
+
if (show || hide) {
|
|
2465
|
+
// best-effort: if both present, whichever appears later "wins"
|
|
2466
|
+
const h = lastIndexOfBytes("\\x1b[?25h");
|
|
2467
|
+
const l = lastIndexOfBytes("\\x1b[?25l");
|
|
2468
|
+
cursorVisible = h > l;
|
|
2469
|
+
renderer.setCursorVisible(cursorVisible);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function needsScrollRegionReset() {
|
|
2474
|
+
return (
|
|
2475
|
+
includesBytes("\\x1b[?1049") ||
|
|
2476
|
+
includesBytes("\\x1b[?1047") ||
|
|
2477
|
+
includesBytes("\\x1b[?47") ||
|
|
2478
|
+
includesBytes("\\x1b[J") ||
|
|
2479
|
+
includesBytes("\\x1b[0J") ||
|
|
2480
|
+
includesBytes("\\x1b[1J") ||
|
|
2481
|
+
includesBytes("\\x1b[2J") ||
|
|
2482
|
+
includesBytes("\\x1b[3J") ||
|
|
2483
|
+
includesBytes("\\x1b[r") ||
|
|
2484
|
+
includesScrollRegionCSI()
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function onChildData(data) {
|
|
2489
|
+
if (shouldStop) return;
|
|
2490
|
+
const chunk = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2491
|
+
appendDetect(chunk);
|
|
2492
|
+
if (needsScrollRegionReset()) scrollRegionDirty = true;
|
|
2493
|
+
updateCursorVisibility();
|
|
2494
|
+
|
|
2495
|
+
renderer.noteChildOutput();
|
|
2496
|
+
const rewritten = rewriter.feed(chunk, childRows);
|
|
2497
|
+
cursor.feed(rewritten, childRows, childCols);
|
|
2498
|
+
writeStdout(Buffer.from(rewritten));
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function onChildExit(exitCode, signal) {
|
|
2502
|
+
if (shouldStop) return;
|
|
2503
|
+
shouldStop = true;
|
|
2504
|
+
const code = exitCode ?? (signal != null ? 128 + signal : 0);
|
|
2505
|
+
cleanup().finally(() => process.exit(code));
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
async function readMonitor() {
|
|
2509
|
+
if (!monitor.stdout) return;
|
|
2510
|
+
const reader = monitor.stdout.getReader();
|
|
2511
|
+
let buf = "";
|
|
2512
|
+
while (!shouldStop) {
|
|
2513
|
+
const { value, done } = await reader.read();
|
|
2514
|
+
if (done || !value) break;
|
|
2515
|
+
buf += new TextDecoder().decode(value);
|
|
2516
|
+
while (true) {
|
|
2517
|
+
const idx = buf.indexOf("\\n");
|
|
2518
|
+
if (idx === -1) break;
|
|
2519
|
+
const line = buf.slice(0, idx).replace(/\\r$/, "");
|
|
2520
|
+
buf = buf.slice(idx + 1);
|
|
2521
|
+
if (!line) continue;
|
|
2522
|
+
renderer.setLine(line);
|
|
2523
|
+
renderer.forceRepaint(false);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
readMonitor().catch(() => {});
|
|
2528
|
+
|
|
2529
|
+
function repaintStatusline(forceUrgent = false) {
|
|
2530
|
+
const { row, col } = cursor.position();
|
|
2531
|
+
let r = Math.max(1, Math.min(childRows, row));
|
|
2532
|
+
let c = Math.max(1, Math.min(childCols, col));
|
|
2533
|
+
|
|
2534
|
+
if (scrollRegionDirty) {
|
|
2535
|
+
const seq =
|
|
2536
|
+
"\\x1b[?2026h\\x1b[?25l\\x1b[1;" +
|
|
2537
|
+
childRows +
|
|
2538
|
+
"r\\x1b[" +
|
|
2539
|
+
r +
|
|
2540
|
+
";" +
|
|
2541
|
+
c +
|
|
2542
|
+
"H" +
|
|
2543
|
+
(cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l") +
|
|
2544
|
+
"\\x1b[?2026l";
|
|
2545
|
+
writeStdout(seq);
|
|
2546
|
+
scrollRegionDirty = false;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
renderer.forceRepaint(forceUrgent);
|
|
2550
|
+
renderer.render(physicalRows, physicalCols, r, c);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function handleSizeChange(nextRows, nextCols, forceUrgent = false) {
|
|
2554
|
+
physicalRows = nextRows;
|
|
2555
|
+
physicalCols = nextCols;
|
|
2556
|
+
|
|
2557
|
+
const desired = renderer.desiredReservedRows(physicalRows, physicalCols, RESERVED_ROWS);
|
|
2558
|
+
const { row, col } = cursor.position();
|
|
2559
|
+
if (desired < effectiveReservedRows) {
|
|
2560
|
+
renderer.clearReservedArea(physicalRows, physicalCols, effectiveReservedRows, row, col);
|
|
2561
|
+
}
|
|
2562
|
+
effectiveReservedRows = desired;
|
|
2563
|
+
renderer.setActiveReservedRows(effectiveReservedRows);
|
|
2564
|
+
|
|
2565
|
+
childRows = Math.max(4, physicalRows - effectiveReservedRows);
|
|
2566
|
+
childCols = Math.max(10, physicalCols);
|
|
2567
|
+
try {
|
|
2568
|
+
terminal.resize(childCols, childRows);
|
|
2569
|
+
} catch {}
|
|
2570
|
+
try {
|
|
2571
|
+
process.kill(-child.pid, "SIGWINCH");
|
|
2572
|
+
} catch {
|
|
2573
|
+
try { process.kill(child.pid, "SIGWINCH"); } catch {}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
scrollRegionDirty = true;
|
|
2577
|
+
renderer.forceRepaint(true);
|
|
2578
|
+
repaintStatusline(forceUrgent);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
process.on("SIGWINCH", () => {
|
|
2582
|
+
const next = termSize();
|
|
2583
|
+
handleSizeChange(next.rows, next.cols, true);
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
// Forward signals to child's process group when possible.
|
|
2587
|
+
const forward = (sig) => {
|
|
2588
|
+
try {
|
|
2589
|
+
process.kill(-pgid, sig);
|
|
2590
|
+
} catch {
|
|
2591
|
+
try {
|
|
2592
|
+
process.kill(child.pid, sig);
|
|
2593
|
+
} catch {}
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
for (const s of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
2597
|
+
try {
|
|
2598
|
+
process.on(s, () => forward(s));
|
|
2599
|
+
} catch {}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// Raw stdin -> PTY.
|
|
2603
|
+
try {
|
|
2604
|
+
process.stdin.setRawMode(true);
|
|
2605
|
+
} catch {}
|
|
2606
|
+
process.stdin.resume();
|
|
2607
|
+
process.stdin.on("data", (buf) => {
|
|
2608
|
+
try {
|
|
2609
|
+
if (typeof buf === "string") terminal.write(buf);
|
|
2610
|
+
else {
|
|
2611
|
+
// Prefer bytes when supported; fall back to UTF-8 decoding.
|
|
2612
|
+
try {
|
|
2613
|
+
// Bun.Terminal.write may accept Uint8Array in newer versions.
|
|
2614
|
+
terminal.write(buf);
|
|
2615
|
+
} catch {
|
|
2616
|
+
terminal.write(new TextDecoder().decode(buf));
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
} catch {}
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2622
|
+
const tick = setInterval(() => {
|
|
2623
|
+
if (shouldStop) return;
|
|
2624
|
+
const next = termSize();
|
|
2625
|
+
const sizeChanged = next.rows !== lastPhysicalRows || next.cols !== lastPhysicalCols;
|
|
2626
|
+
const desired = renderer.desiredReservedRows(next.rows, next.cols, RESERVED_ROWS);
|
|
2627
|
+
if (sizeChanged || desired !== effectiveReservedRows) {
|
|
2628
|
+
handleSizeChange(next.rows, next.cols, true);
|
|
2629
|
+
lastPhysicalRows = next.rows;
|
|
2630
|
+
lastPhysicalCols = next.cols;
|
|
2631
|
+
lastForceRepaintMs = Date.now();
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const now = Date.now();
|
|
2635
|
+
if (now - lastForceRepaintMs >= FORCE_REPAINT_INTERVAL_MS) {
|
|
2636
|
+
repaintStatusline(false);
|
|
2637
|
+
lastForceRepaintMs = now;
|
|
2638
|
+
} else {
|
|
2639
|
+
const { row, col } = cursor.position();
|
|
2640
|
+
renderer.render(physicalRows, physicalCols, row, col);
|
|
2641
|
+
}
|
|
2642
|
+
}, 50);
|
|
2643
|
+
|
|
2644
|
+
async function cleanup() {
|
|
2645
|
+
clearInterval(tick);
|
|
2646
|
+
try {
|
|
2647
|
+
process.stdin.setRawMode(false);
|
|
2648
|
+
} catch {}
|
|
2649
|
+
try {
|
|
2650
|
+
const { row, col } = cursor.position();
|
|
2651
|
+
renderer.clearReservedArea(physicalRows, physicalCols, effectiveReservedRows, row, col);
|
|
2652
|
+
} catch {}
|
|
2653
|
+
try {
|
|
2654
|
+
writeStdout("\\x1b[r\\x1b[0m\\x1b[?25h");
|
|
2655
|
+
} catch {}
|
|
2656
|
+
try {
|
|
2657
|
+
monitor.kill();
|
|
2658
|
+
} catch {}
|
|
2659
|
+
try {
|
|
2660
|
+
terminal.close();
|
|
2661
|
+
} catch {}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// Keep process alive until child exits.
|
|
2665
|
+
await child.exited;
|
|
2666
|
+
await cleanup();
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
main().catch(() => process.exit(1));
|
|
2402
2670
|
`;
|
|
2403
2671
|
}
|
|
2404
2672
|
async function createStatuslineFiles(outputDir, execTargetPath, aliasName, sessionsScriptPath) {
|
|
@@ -2613,27 +2881,49 @@ async function main() {
|
|
|
2613
2881
|
let selected = 0;
|
|
2614
2882
|
let offset = 0;
|
|
2615
2883
|
|
|
2884
|
+
function restoreTerminal() {
|
|
2885
|
+
try { process.stdout.write(SHOW_CURSOR); } catch {}
|
|
2886
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
2887
|
+
try { process.stdin.pause(); } catch {}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
function clearScreen() {
|
|
2891
|
+
try { process.stdout.write(CLEAR); } catch {}
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2616
2894
|
process.stdin.setRawMode(true);
|
|
2617
2895
|
process.stdin.resume();
|
|
2618
2896
|
process.stdout.write(HIDE_CURSOR);
|
|
2619
2897
|
|
|
2620
2898
|
render(sessions, selected, offset, rows);
|
|
2621
2899
|
|
|
2622
|
-
|
|
2900
|
+
const onKey = (key) => {
|
|
2623
2901
|
const k = key.toString();
|
|
2624
2902
|
|
|
2625
2903
|
if (k === 'q' || k === '\\x03') { // q or Ctrl+C
|
|
2626
|
-
|
|
2904
|
+
restoreTerminal();
|
|
2905
|
+
clearScreen();
|
|
2627
2906
|
process.exit(0);
|
|
2628
2907
|
}
|
|
2629
2908
|
|
|
2630
2909
|
if (k === '\\r' || k === '\\n') { // Enter
|
|
2631
|
-
|
|
2910
|
+
// Stop reading input / stop reacting to arrow keys before handing off to droid.
|
|
2911
|
+
process.stdin.off('data', onKey);
|
|
2912
|
+
restoreTerminal();
|
|
2913
|
+
clearScreen();
|
|
2632
2914
|
const session = sessions[selected];
|
|
2633
2915
|
console.log(GREEN + 'Resuming session: ' + session.id + RESET);
|
|
2634
2916
|
console.log(DIM + 'Using: ' + ALIAS_NAME + ' --resume ' + session.id + RESET + '\\n');
|
|
2917
|
+
|
|
2918
|
+
// Avoid the sessions browser reacting to signals while droid is running.
|
|
2919
|
+
try { process.removeAllListeners('SIGINT'); } catch {}
|
|
2920
|
+
try { process.removeAllListeners('SIGTERM'); } catch {}
|
|
2921
|
+
try { process.on('SIGINT', () => {}); } catch {}
|
|
2922
|
+
try { process.on('SIGTERM', () => {}); } catch {}
|
|
2923
|
+
|
|
2635
2924
|
const child = spawn(ALIAS_NAME, ['--resume', session.id], { stdio: 'inherit' });
|
|
2636
2925
|
child.on('exit', (code) => process.exit(code || 0));
|
|
2926
|
+
child.on('error', () => process.exit(1));
|
|
2637
2927
|
return;
|
|
2638
2928
|
}
|
|
2639
2929
|
|
|
@@ -2656,10 +2946,13 @@ async function main() {
|
|
|
2656
2946
|
}
|
|
2657
2947
|
|
|
2658
2948
|
render(sessions, selected, offset, rows);
|
|
2659
|
-
}
|
|
2949
|
+
};
|
|
2950
|
+
|
|
2951
|
+
process.stdin.on('data', onKey);
|
|
2660
2952
|
|
|
2661
2953
|
process.on('SIGINT', () => {
|
|
2662
|
-
|
|
2954
|
+
restoreTerminal();
|
|
2955
|
+
clearScreen();
|
|
2663
2956
|
process.exit(0);
|
|
2664
2957
|
});
|
|
2665
2958
|
}
|
|
@@ -2748,7 +3041,9 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2748
3041
|
const backup = options.backup !== false;
|
|
2749
3042
|
const verbose = options.verbose;
|
|
2750
3043
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
2751
|
-
|
|
3044
|
+
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!apiBase && !websearch;
|
|
3045
|
+
const statuslineEnabled = statusline;
|
|
3046
|
+
if (!needsBinaryPatch && (websearch || statuslineEnabled)) {
|
|
2752
3047
|
if (!alias) {
|
|
2753
3048
|
console.log(styleText("red", "Error: Alias name required for --websearch/--statusline"));
|
|
2754
3049
|
console.log(styleText("gray", "Usage: npx droid-patch --websearch <alias>"));
|
|
@@ -2764,14 +3059,14 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2764
3059
|
console.log(styleText("white", `Forward target: ${websearchTarget}`));
|
|
2765
3060
|
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
2766
3061
|
}
|
|
2767
|
-
if (
|
|
3062
|
+
if (statuslineEnabled) console.log(styleText("white", `Statusline: enabled`));
|
|
2768
3063
|
console.log();
|
|
2769
3064
|
let execTargetPath = path;
|
|
2770
3065
|
if (websearch) {
|
|
2771
3066
|
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
2772
3067
|
execTargetPath = wrapperScript;
|
|
2773
3068
|
}
|
|
2774
|
-
if (
|
|
3069
|
+
if (statuslineEnabled) {
|
|
2775
3070
|
const statuslineDir = join(homedir(), ".droid-patch", "statusline");
|
|
2776
3071
|
let sessionsScript;
|
|
2777
3072
|
if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
|
|
@@ -2785,7 +3080,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2785
3080
|
skipLogin: false,
|
|
2786
3081
|
apiBase: apiBase || null,
|
|
2787
3082
|
websearch: !!websearch,
|
|
2788
|
-
statusline: !!
|
|
3083
|
+
statusline: !!statuslineEnabled,
|
|
2789
3084
|
sessions: !!sessions,
|
|
2790
3085
|
reasoningEffort: false,
|
|
2791
3086
|
noTelemetry: false,
|
|
@@ -2822,7 +3117,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2822
3117
|
}
|
|
2823
3118
|
return;
|
|
2824
3119
|
}
|
|
2825
|
-
if (!isCustom && !skipLogin && !apiBase && !websearch && !
|
|
3120
|
+
if (!isCustom && !skipLogin && !apiBase && !websearch && !statuslineEnabled && !reasoningEffort && !noTelemetry) {
|
|
2826
3121
|
console.log(styleText("yellow", "No patch flags specified. Available patches:"));
|
|
2827
3122
|
console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
|
|
2828
3123
|
console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
|
|
@@ -2981,7 +3276,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2981
3276
|
console.log(styleText("white", ` Forward target: ${websearchTarget}`));
|
|
2982
3277
|
if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
|
|
2983
3278
|
}
|
|
2984
|
-
if (
|
|
3279
|
+
if (statuslineEnabled) {
|
|
2985
3280
|
const statuslineDir = join(homedir(), ".droid-patch", "statusline");
|
|
2986
3281
|
let sessionsScript;
|
|
2987
3282
|
if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
|
|
@@ -2991,7 +3286,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2991
3286
|
console.log(styleText("cyan", "Statusline enabled"));
|
|
2992
3287
|
}
|
|
2993
3288
|
let aliasResult;
|
|
2994
|
-
if (websearch ||
|
|
3289
|
+
if (websearch || statuslineEnabled) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
2995
3290
|
else aliasResult = await createAlias(result.outputPath, alias, verbose);
|
|
2996
3291
|
const droidVersion = getDroidVersion(path);
|
|
2997
3292
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -2999,7 +3294,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
2999
3294
|
skipLogin: !!skipLogin,
|
|
3000
3295
|
apiBase: apiBase || null,
|
|
3001
3296
|
websearch: !!websearch,
|
|
3002
|
-
statusline: !!
|
|
3297
|
+
statusline: !!statuslineEnabled,
|
|
3003
3298
|
sessions: !!sessions,
|
|
3004
3299
|
reasoningEffort: !!reasoningEffort,
|
|
3005
3300
|
noTelemetry: !!noTelemetry,
|