droid-patch 0.7.0 → 0.7.1
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/cli.mjs +210 -90
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -214,6 +214,7 @@ Enables WebSearch functionality through a local proxy server that intercepts `/a
|
|
|
214
214
|
- **Per-instance proxy**: Each droid instance runs its own proxy on an auto-assigned port
|
|
215
215
|
- **Auto-cleanup**: Proxy automatically stops when droid exits
|
|
216
216
|
- **Forward target**: Use `--api-base` with `--websearch` to forward non-search requests to a custom backend
|
|
217
|
+
- **Non-interactive passthrough**: `--version`/`version`, `--help`/`help`, `completion(s)`, `exec` do not start the proxy
|
|
217
218
|
|
|
218
219
|
**Usage**:
|
|
219
220
|
|
|
@@ -339,6 +340,7 @@ Enables a Claude-style statusline at the bottom of the terminal, displaying real
|
|
|
339
340
|
- **Git integration**: Shows current branch and diff summary (+insertions, -deletions)
|
|
340
341
|
- **Compaction indicator**: Shows when context compaction is in progress
|
|
341
342
|
- **PTY proxy architecture**: Reserves bottom row(s) for statusline without flickering
|
|
343
|
+
- **Non-interactive passthrough**: `--version`/`version`, `--help`/`help`, `completion(s)`, `exec` (or non-TTY) bypass the statusline wrapper
|
|
342
344
|
|
|
343
345
|
**How it works**:
|
|
344
346
|
|
package/README.zh-CN.md
CHANGED
|
@@ -214,6 +214,7 @@ npx droid-patch --websearch --api-base "http://my-proxy.example.com:3000" droid-
|
|
|
214
214
|
- **每实例独立代理**:每个 droid 实例运行自己的代理,自动分配端口
|
|
215
215
|
- **自动清理**:droid 退出时代理自动停止
|
|
216
216
|
- **转发目标**:使用 `--api-base` 配合 `--websearch` 可将非搜索请求转发到自定义后端
|
|
217
|
+
- **非交互直通**:运行 `--version`/`version`、`--help`/`help`、`completion(s)`、`exec` 等命令时不会启动代理
|
|
217
218
|
|
|
218
219
|
**使用方法**:
|
|
219
220
|
|
|
@@ -339,6 +340,7 @@ npx droid-patch --is-custom --skip-login --disable-telemetry droid-private
|
|
|
339
340
|
- **Git 集成**:显示当前分支和差异摘要(+插入行数,-删除行数)
|
|
340
341
|
- **压缩指示器**:显示上下文压缩正在进行中
|
|
341
342
|
- **PTY 代理架构**:为状态栏预留底部行,避免闪烁
|
|
343
|
+
- **非交互直通**:运行 `--version`/`version`、`--help`/`help`、`completion(s)`、`exec` 或在非 TTY(管道/重定向)时自动绕过状态栏
|
|
342
344
|
|
|
343
345
|
**工作原理**:
|
|
344
346
|
|
package/dist/cli.mjs
CHANGED
|
@@ -531,6 +531,45 @@ PROXY_PID=""
|
|
|
531
531
|
PORT_FILE="/tmp/droid-websearch-\$\$.port"
|
|
532
532
|
STANDALONE="${standalone ? "1" : "0"}"
|
|
533
533
|
|
|
534
|
+
# Passthrough for non-interactive/meta commands (avoid starting a proxy for help/version/etc)
|
|
535
|
+
should_passthrough() {
|
|
536
|
+
# Any help/version flags before "--"
|
|
537
|
+
for arg in "\$@"; do
|
|
538
|
+
if [ "\$arg" = "--" ]; then
|
|
539
|
+
break
|
|
540
|
+
fi
|
|
541
|
+
case "\$arg" in
|
|
542
|
+
--help|-h|--version|-V)
|
|
543
|
+
return 0
|
|
544
|
+
;;
|
|
545
|
+
esac
|
|
546
|
+
done
|
|
547
|
+
|
|
548
|
+
# Top-level command token
|
|
549
|
+
local end_opts=0
|
|
550
|
+
for arg in "\$@"; do
|
|
551
|
+
if [ "\$arg" = "--" ]; then
|
|
552
|
+
end_opts=1
|
|
553
|
+
continue
|
|
554
|
+
fi
|
|
555
|
+
if [ "\$end_opts" -eq 0 ] && [[ "\$arg" == -* ]]; then
|
|
556
|
+
continue
|
|
557
|
+
fi
|
|
558
|
+
case "\$arg" in
|
|
559
|
+
help|version|completion|completions|exec)
|
|
560
|
+
return 0
|
|
561
|
+
;;
|
|
562
|
+
esac
|
|
563
|
+
break
|
|
564
|
+
done
|
|
565
|
+
|
|
566
|
+
return 1
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if should_passthrough "\$@"; then
|
|
570
|
+
exec "\$DROID_BIN" "\$@"
|
|
571
|
+
fi
|
|
572
|
+
|
|
534
573
|
# Cleanup function - kill proxy when droid exits
|
|
535
574
|
cleanup() {
|
|
536
575
|
if [ -n "\$PROXY_PID" ] && kill -0 "\$PROXY_PID" 2>/dev/null; then
|
|
@@ -660,6 +699,33 @@ function isPositiveInt(n) {
|
|
|
660
699
|
return Number.isFinite(n) && n > 0;
|
|
661
700
|
}
|
|
662
701
|
|
|
702
|
+
function firstNonNull(promises) {
|
|
703
|
+
const list = Array.isArray(promises) ? promises : [];
|
|
704
|
+
if (list.length === 0) return Promise.resolve(null);
|
|
705
|
+
return new Promise((resolve) => {
|
|
706
|
+
let pending = list.length;
|
|
707
|
+
let done = false;
|
|
708
|
+
for (const p of list) {
|
|
709
|
+
Promise.resolve(p)
|
|
710
|
+
.then((value) => {
|
|
711
|
+
if (done) return;
|
|
712
|
+
if (value) {
|
|
713
|
+
done = true;
|
|
714
|
+
resolve(value);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
pending -= 1;
|
|
718
|
+
if (pending <= 0) resolve(null);
|
|
719
|
+
})
|
|
720
|
+
.catch(() => {
|
|
721
|
+
if (done) return;
|
|
722
|
+
pending -= 1;
|
|
723
|
+
if (pending <= 0) resolve(null);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
663
729
|
function listPidsInProcessGroup(pgid) {
|
|
664
730
|
if (!isPositiveInt(pgid)) return [];
|
|
665
731
|
try {
|
|
@@ -712,10 +778,11 @@ function resolveOpenSessionFromPids(pids) {
|
|
|
712
778
|
return null;
|
|
713
779
|
}
|
|
714
780
|
|
|
715
|
-
async function resolveSessionFromProcessGroup() {
|
|
781
|
+
async function resolveSessionFromProcessGroup(shouldAbort, maxTries = 20) {
|
|
716
782
|
if (!isPositiveInt(PGID)) return null;
|
|
717
783
|
// Wait a little for droid to create/open the session files.
|
|
718
|
-
for (let i = 0; i <
|
|
784
|
+
for (let i = 0; i < maxTries; i++) {
|
|
785
|
+
if (shouldAbort && shouldAbort()) return null;
|
|
719
786
|
const pids = listPidsInProcessGroup(PGID);
|
|
720
787
|
const found = resolveOpenSessionFromPids(pids);
|
|
721
788
|
if (found) return found;
|
|
@@ -872,8 +939,9 @@ function pickLatestSessionAcross(workspaceDirs) {
|
|
|
872
939
|
return best ? { workspaceDir: best.workspaceDir, id: best.id } : null;
|
|
873
940
|
}
|
|
874
941
|
|
|
875
|
-
async function waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, startMs) {
|
|
942
|
+
async function waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, startMs, shouldAbort) {
|
|
876
943
|
for (let i = 0; i < 80; i++) {
|
|
944
|
+
if (shouldAbort && shouldAbort()) return null;
|
|
877
945
|
let best = null;
|
|
878
946
|
for (const dir of workspaceDirs) {
|
|
879
947
|
const known = knownIdsByWorkspace.get(dir) || new Set();
|
|
@@ -1113,25 +1181,30 @@ async function main() {
|
|
|
1113
1181
|
sessionId = resumeId;
|
|
1114
1182
|
workspaceDir = findWorkspaceDirForSessionId(workspaceDirs, sessionId) || workspaceDirs[0] || null;
|
|
1115
1183
|
} else {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1184
|
+
let abortResolve = false;
|
|
1185
|
+
const shouldAbort = () => abortResolve;
|
|
1186
|
+
|
|
1187
|
+
const byProcPromise = resolveSessionFromProcessGroup(shouldAbort, 20);
|
|
1188
|
+
|
|
1189
|
+
let picked = null;
|
|
1190
|
+
if (resumeFlag) {
|
|
1191
|
+
// For --resume without an explicit id, don't block startup too long on ps/lsof.
|
|
1192
|
+
// Prefer process-group resolution when it is fast; otherwise fall back to latest.
|
|
1193
|
+
picked = await Promise.race([
|
|
1194
|
+
byProcPromise,
|
|
1195
|
+
sleep(400).then(() => null),
|
|
1196
|
+
]);
|
|
1197
|
+
if (!picked) picked = pickLatestSessionAcross(workspaceDirs);
|
|
1124
1198
|
} else {
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
workspaceDir = fresh.workspaceDir;
|
|
1129
|
-
} else {
|
|
1130
|
-
const latest = pickLatestSessionAcross(workspaceDirs);
|
|
1131
|
-
sessionId = latest?.id || null;
|
|
1132
|
-
workspaceDir = latest?.workspaceDir || workspaceDirs[0] || null;
|
|
1133
|
-
}
|
|
1199
|
+
const freshPromise = waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, START_MS, shouldAbort);
|
|
1200
|
+
picked = await firstNonNull([byProcPromise, freshPromise]);
|
|
1201
|
+
if (!picked) picked = pickLatestSessionAcross(workspaceDirs);
|
|
1134
1202
|
}
|
|
1203
|
+
|
|
1204
|
+
abortResolve = true;
|
|
1205
|
+
|
|
1206
|
+
sessionId = picked?.id || null;
|
|
1207
|
+
workspaceDir = picked?.workspaceDir || workspaceDirs[0] || null;
|
|
1135
1208
|
}
|
|
1136
1209
|
|
|
1137
1210
|
if (!sessionId || !workspaceDir) return;
|
|
@@ -1148,8 +1221,8 @@ async function main() {
|
|
|
1148
1221
|
let compacting = false;
|
|
1149
1222
|
let lastRenderAt = 0;
|
|
1150
1223
|
let lastRenderedLine = '';
|
|
1151
|
-
|
|
1152
|
-
|
|
1224
|
+
let gitBranch = '';
|
|
1225
|
+
let gitDiff = '';
|
|
1153
1226
|
|
|
1154
1227
|
function renderNow() {
|
|
1155
1228
|
const usedTokens = (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
|
|
@@ -1172,79 +1245,93 @@ async function main() {
|
|
|
1172
1245
|
}
|
|
1173
1246
|
}
|
|
1174
1247
|
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
try {
|
|
1178
|
-
// Backward scan to find the most recent streaming-context entry for this session.
|
|
1179
|
-
// The log can be large and shared across multiple sessions, so a small tail slice
|
|
1180
|
-
// may miss older resumed sessions.
|
|
1181
|
-
const MAX_SCAN_BYTES = 64 * 1024 * 1024; // 64 MiB
|
|
1182
|
-
const CHUNK_BYTES = 1024 * 1024; // 1 MiB
|
|
1248
|
+
// Initial render.
|
|
1249
|
+
renderNow();
|
|
1183
1250
|
|
|
1184
|
-
|
|
1251
|
+
// Resolve git info asynchronously so startup isn't blocked on large repos.
|
|
1252
|
+
setTimeout(() => {
|
|
1185
1253
|
try {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
let text = buf.toString('utf8') + remainder;
|
|
1202
|
-
let lines = String(text).split('\\n');
|
|
1203
|
-
remainder = lines.shift() || '';
|
|
1204
|
-
if (pos === 0 && remainder) {
|
|
1205
|
-
lines.unshift(remainder);
|
|
1206
|
-
remainder = '';
|
|
1207
|
-
}
|
|
1254
|
+
gitBranch = resolveGitBranch(cwd);
|
|
1255
|
+
gitDiff = resolveGitDiffSummary(cwd);
|
|
1256
|
+
renderNow();
|
|
1257
|
+
} catch {}
|
|
1258
|
+
}, 0).unref();
|
|
1259
|
+
|
|
1260
|
+
// Seed prompt-context usage from existing logs (important for resumed sessions).
|
|
1261
|
+
// Do this asynchronously to avoid delaying the first statusline frame.
|
|
1262
|
+
if (resumeFlag || resumeId) {
|
|
1263
|
+
setTimeout(() => {
|
|
1264
|
+
try {
|
|
1265
|
+
// Backward scan to find the most recent streaming-context entry for this session.
|
|
1266
|
+
// The log can be large and shared across multiple sessions.
|
|
1267
|
+
const MAX_SCAN_BYTES = 64 * 1024 * 1024; // 64 MiB
|
|
1268
|
+
const CHUNK_BYTES = 1024 * 1024; // 1 MiB
|
|
1208
1269
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1270
|
+
const fd = fs.openSync(LOG_PATH, 'r');
|
|
1271
|
+
try {
|
|
1272
|
+
const stat = fs.fstatSync(fd);
|
|
1273
|
+
const size = Number(stat?.size ?? 0);
|
|
1274
|
+
let pos = size;
|
|
1275
|
+
let scanned = 0;
|
|
1276
|
+
let remainder = '';
|
|
1277
|
+
let seeded = false;
|
|
1278
|
+
|
|
1279
|
+
while (pos > 0 && scanned < MAX_SCAN_BYTES && !seeded) {
|
|
1280
|
+
const readSize = Math.min(CHUNK_BYTES, pos);
|
|
1281
|
+
const start = pos - readSize;
|
|
1282
|
+
const buf = Buffer.alloc(readSize);
|
|
1283
|
+
fs.readSync(fd, buf, 0, readSize, start);
|
|
1284
|
+
pos = start;
|
|
1285
|
+
scanned += readSize;
|
|
1286
|
+
|
|
1287
|
+
let text = buf.toString('utf8') + remainder;
|
|
1288
|
+
let lines = String(text).split('\\n');
|
|
1289
|
+
remainder = lines.shift() || '';
|
|
1290
|
+
if (pos === 0 && remainder) {
|
|
1291
|
+
lines.unshift(remainder);
|
|
1292
|
+
remainder = '';
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1296
|
+
const line = String(lines[i] || '').trimEnd();
|
|
1297
|
+
if (!line) continue;
|
|
1298
|
+
if (!line.includes('Context:')) continue;
|
|
1299
|
+
if (!line.includes('"sessionId":"' + sessionId + '"')) continue;
|
|
1300
|
+
if (!line.includes('[Agent] Streaming result')) continue;
|
|
1301
|
+
const ctxIndex = line.indexOf('Context: ');
|
|
1302
|
+
if (ctxIndex === -1) continue;
|
|
1303
|
+
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1304
|
+
let ctx;
|
|
1305
|
+
try {
|
|
1306
|
+
ctx = JSON.parse(jsonStr);
|
|
1307
|
+
} catch {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const cacheRead = Number(ctx?.cacheReadInputTokens ?? 0);
|
|
1311
|
+
const contextCount = Number(ctx?.contextCount ?? 0);
|
|
1312
|
+
const out = Number(ctx?.outputTokens ?? 0);
|
|
1313
|
+
if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
|
|
1314
|
+
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1315
|
+
if (Number.isFinite(out)) last.outputTokens = out;
|
|
1316
|
+
seeded = true;
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (remainder.length > 8192) remainder = remainder.slice(-8192);
|
|
1223
1321
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1229
|
-
if (Number.isFinite(out)) last.outputTokens = out;
|
|
1230
|
-
seeded = true;
|
|
1231
|
-
break;
|
|
1322
|
+
} finally {
|
|
1323
|
+
try {
|
|
1324
|
+
fs.closeSync(fd);
|
|
1325
|
+
} catch {}
|
|
1232
1326
|
}
|
|
1233
1327
|
|
|
1234
|
-
|
|
1328
|
+
renderNow();
|
|
1329
|
+
} catch {
|
|
1330
|
+
// ignore
|
|
1235
1331
|
}
|
|
1236
|
-
}
|
|
1237
|
-
try {
|
|
1238
|
-
fs.closeSync(fd);
|
|
1239
|
-
} catch {}
|
|
1240
|
-
}
|
|
1241
|
-
} catch {
|
|
1242
|
-
// ignore
|
|
1332
|
+
}, 0).unref();
|
|
1243
1333
|
}
|
|
1244
1334
|
|
|
1245
|
-
// Initial render.
|
|
1246
|
-
renderNow();
|
|
1247
|
-
|
|
1248
1335
|
// Watch session settings for autonomy/reasoning changes (cheap polling with mtime).
|
|
1249
1336
|
let settingsMtimeMs = 0;
|
|
1250
1337
|
setInterval(() => {
|
|
@@ -1364,6 +1451,42 @@ QUIET_MS = 50 # Reduced to prevent statusline disappearing
|
|
|
1364
1451
|
FORCE_REPAINT_INTERVAL_MS = 2000 # Force repaint every 2 seconds
|
|
1365
1452
|
RESERVED_ROWS = 1
|
|
1366
1453
|
|
|
1454
|
+
BYPASS_FLAGS = {"--help", "-h", "--version", "-V"}
|
|
1455
|
+
BYPASS_COMMANDS = {"help", "version", "completion", "completions", "exec"}
|
|
1456
|
+
|
|
1457
|
+
def _should_passthrough(argv):
|
|
1458
|
+
# Any help/version flags before "--"
|
|
1459
|
+
for a in argv:
|
|
1460
|
+
if a == "--":
|
|
1461
|
+
break
|
|
1462
|
+
if a in BYPASS_FLAGS:
|
|
1463
|
+
return True
|
|
1464
|
+
|
|
1465
|
+
# Top-level command token
|
|
1466
|
+
end_opts = False
|
|
1467
|
+
cmd = None
|
|
1468
|
+
for a in argv:
|
|
1469
|
+
if a == "--":
|
|
1470
|
+
end_opts = True
|
|
1471
|
+
continue
|
|
1472
|
+
if (not end_opts) and a.startswith("-"):
|
|
1473
|
+
continue
|
|
1474
|
+
cmd = a
|
|
1475
|
+
break
|
|
1476
|
+
|
|
1477
|
+
return cmd in BYPASS_COMMANDS
|
|
1478
|
+
|
|
1479
|
+
def _exec_passthrough():
|
|
1480
|
+
try:
|
|
1481
|
+
os.execvp(EXEC_TARGET, [EXEC_TARGET] + sys.argv[1:])
|
|
1482
|
+
except Exception as e:
|
|
1483
|
+
sys.stderr.write(f"[statusline] passthrough failed: {e}\\n")
|
|
1484
|
+
sys.exit(1)
|
|
1485
|
+
|
|
1486
|
+
# Passthrough for non-interactive/meta commands (avoid clearing screen / PTY proxy)
|
|
1487
|
+
if (not sys.stdin.isatty()) or (not sys.stdout.isatty()) or _should_passthrough(sys.argv[1:]):
|
|
1488
|
+
_exec_passthrough()
|
|
1489
|
+
|
|
1367
1490
|
ANSI_RE = re.compile(r"\\x1b\\[[0-9;]*m")
|
|
1368
1491
|
RESET_SGR = "\\x1b[0m"
|
|
1369
1492
|
|
|
@@ -1975,9 +2098,6 @@ class CursorTracker:
|
|
|
1975
2098
|
|
|
1976
2099
|
|
|
1977
2100
|
def main():
|
|
1978
|
-
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
1979
|
-
os.execv(EXEC_TARGET, [EXEC_TARGET] + sys.argv[1:])
|
|
1980
|
-
|
|
1981
2101
|
# Start from a clean viewport. Droid's TUI assumes a fresh screen; without this,
|
|
1982
2102
|
# it can visually mix with prior shell output (especially when scrollback exists).
|
|
1983
2103
|
try:
|
|
@@ -2125,7 +2245,7 @@ def main():
|
|
|
2125
2245
|
)
|
|
2126
2246
|
# Also detect scroll region changes with parameters (DECSTBM pattern ESC[n;mr)
|
|
2127
2247
|
if b"\\x1b[" in detect_buf and b"r" in detect_buf:
|
|
2128
|
-
if re.search(b"\\x1b
|
|
2248
|
+
if re.search(b"\\x1b\\\\[[0-9]*;?[0-9]*r", detect_buf):
|
|
2129
2249
|
needs_scroll_region_reset = True
|
|
2130
2250
|
if needs_scroll_region_reset:
|
|
2131
2251
|
renderer.force_repaint(True)
|