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 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 < 80; 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
- const byProc = await resolveSessionFromProcessGroup();
1117
- if (byProc?.id) {
1118
- sessionId = byProc.id;
1119
- workspaceDir = byProc.workspaceDir;
1120
- } else if (resumeFlag) {
1121
- const latest = pickLatestSessionAcross(workspaceDirs);
1122
- sessionId = latest?.id || null;
1123
- workspaceDir = latest?.workspaceDir || workspaceDirs[0] || null;
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 fresh = await waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, START_MS);
1126
- if (fresh) {
1127
- sessionId = fresh.id;
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
- const gitBranch = resolveGitBranch(cwd);
1152
- const gitDiff = resolveGitDiffSummary(cwd);
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
- // Seed prompt-context usage from existing logs (important for resumed sessions and early calls).
1176
- // This avoids showing "Ctx: 0" until the next streaming event arrives.
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
- const fd = fs.openSync(LOG_PATH, 'r');
1251
+ // Resolve git info asynchronously so startup isn't blocked on large repos.
1252
+ setTimeout(() => {
1185
1253
  try {
1186
- const stat = fs.fstatSync(fd);
1187
- const size = Number(stat?.size ?? 0);
1188
- let pos = size;
1189
- let scanned = 0;
1190
- let remainder = '';
1191
- let seeded = false;
1192
-
1193
- while (pos > 0 && scanned < MAX_SCAN_BYTES && !seeded) {
1194
- const readSize = Math.min(CHUNK_BYTES, pos);
1195
- const start = pos - readSize;
1196
- const buf = Buffer.alloc(readSize);
1197
- fs.readSync(fd, buf, 0, readSize, start);
1198
- pos = start;
1199
- scanned += readSize;
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
- for (let i = lines.length - 1; i >= 0; i--) {
1210
- const line = String(lines[i] || '').trimEnd();
1211
- if (!line) continue;
1212
- if (!line.includes('Context:')) continue;
1213
- if (!line.includes('"sessionId":"' + sessionId + '"')) continue;
1214
- if (!line.includes('[Agent] Streaming result')) continue;
1215
- const ctxIndex = line.indexOf('Context: ');
1216
- if (ctxIndex === -1) continue;
1217
- const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
1218
- let ctx;
1219
- try {
1220
- ctx = JSON.parse(jsonStr);
1221
- } catch {
1222
- continue;
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
- const cacheRead = Number(ctx?.cacheReadInputTokens ?? 0);
1225
- const contextCount = Number(ctx?.contextCount ?? 0);
1226
- const out = Number(ctx?.outputTokens ?? 0);
1227
- if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
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
- if (remainder.length > 8192) remainder = remainder.slice(-8192);
1328
+ renderNow();
1329
+ } catch {
1330
+ // ignore
1235
1331
  }
1236
- } finally {
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\\[\\d*;?\\d*r", detect_buf):
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)