@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +86 -43
  2. package/dist/cli.js +3540 -3521
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  14. package/dist/types/launch/spawn-options.d.ts +10 -0
  15. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  16. package/dist/types/modes/components/status-line/component.d.ts +2 -3
  17. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  18. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  19. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  20. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  21. package/dist/types/modes/interactive-mode.d.ts +2 -0
  22. package/dist/types/modes/print-mode.d.ts +4 -0
  23. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  24. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  25. package/dist/types/modes/types.d.ts +2 -0
  26. package/dist/types/sdk.d.ts +2 -0
  27. package/dist/types/session/agent-session.d.ts +19 -1
  28. package/dist/types/session/messages.d.ts +6 -0
  29. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  30. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  31. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  32. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  33. package/dist/types/tools/gh.d.ts +5 -3
  34. package/dist/types/tools/hub/index.d.ts +1 -1
  35. package/dist/types/tools/hub/jobs.d.ts +1 -0
  36. package/dist/types/tools/hub/types.d.ts +2 -0
  37. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  38. package/dist/types/utils/git.d.ts +33 -0
  39. package/dist/types/web/search/providers/codex.d.ts +1 -1
  40. package/package.json +12 -12
  41. package/src/advisor/__tests__/advisor.test.ts +150 -0
  42. package/src/advisor/advise-tool.ts +4 -0
  43. package/src/advisor/runtime.ts +38 -14
  44. package/src/async/job-manager.ts +3 -0
  45. package/src/cli/bench-cli.ts +8 -2
  46. package/src/cli/dry-balance-cli.ts +1 -0
  47. package/src/config/model-registry.ts +89 -8
  48. package/src/config/model-resolver.ts +78 -14
  49. package/src/config/models-config-schema.ts +1 -0
  50. package/src/config/settings.ts +3 -1
  51. package/src/dap/client.ts +168 -1
  52. package/src/dap/config.ts +51 -1
  53. package/src/dap/session.ts +575 -234
  54. package/src/dap/types.ts +6 -5
  55. package/src/discovery/agents.ts +2 -2
  56. package/src/extensibility/extensions/runner.ts +6 -4
  57. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  58. package/src/launch/broker.ts +34 -31
  59. package/src/launch/client.ts +7 -1
  60. package/src/launch/spawn-options.test.ts +31 -0
  61. package/src/launch/spawn-options.ts +17 -0
  62. package/src/lsp/types.ts +5 -1
  63. package/src/main.ts +17 -4
  64. package/src/mcp/transports/stdio.test.ts +9 -1
  65. package/src/modes/components/ask-dialog.ts +137 -73
  66. package/src/modes/components/status-line/component.ts +2 -2
  67. package/src/modes/components/status-line/segments.ts +20 -3
  68. package/src/modes/components/status-line/types.ts +3 -1
  69. package/src/modes/components/tool-execution.ts +5 -0
  70. package/src/modes/components/transcript-container.ts +23 -122
  71. package/src/modes/components/tree-selector.ts +10 -4
  72. package/src/modes/controllers/event-controller.ts +1 -2
  73. package/src/modes/controllers/input-controller.ts +1 -1
  74. package/src/modes/controllers/selector-controller.ts +29 -8
  75. package/src/modes/interactive-mode.ts +40 -8
  76. package/src/modes/noninteractive-dispose.test.ts +12 -1
  77. package/src/modes/print-mode.ts +21 -9
  78. package/src/modes/rpc/rpc-input.ts +38 -0
  79. package/src/modes/rpc/rpc-mode.ts +7 -2
  80. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  81. package/src/modes/types.ts +2 -0
  82. package/src/modes/utils/ui-helpers.ts +3 -2
  83. package/src/prompts/tools/browser.md +3 -2
  84. package/src/prompts/tools/debug.md +2 -7
  85. package/src/prompts/tools/github.md +6 -1
  86. package/src/prompts/tools/hub.md +4 -2
  87. package/src/prompts/tools/task-async-contract.md +1 -0
  88. package/src/prompts/tools/task.md +9 -2
  89. package/src/sdk.ts +38 -6
  90. package/src/session/agent-session.ts +395 -162
  91. package/src/session/messages.test.ts +91 -0
  92. package/src/session/messages.ts +248 -110
  93. package/src/slash-commands/builtin-registry.ts +1 -0
  94. package/src/task/executor.ts +59 -33
  95. package/src/task/index.ts +46 -9
  96. package/src/task/worktree.ts +10 -0
  97. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  98. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  99. package/src/tools/browser/cmux/socket-client.ts +139 -3
  100. package/src/tools/browser/run-cancellation.ts +4 -0
  101. package/src/tools/browser/tab-protocol.ts +2 -0
  102. package/src/tools/browser/tab-supervisor.ts +21 -11
  103. package/src/tools/browser/tab-worker.ts +199 -33
  104. package/src/tools/debug.ts +3 -0
  105. package/src/tools/gh.ts +40 -2
  106. package/src/tools/hub/index.ts +4 -1
  107. package/src/tools/hub/jobs.ts +42 -1
  108. package/src/tools/hub/types.ts +2 -0
  109. package/src/tools/tool-timeouts.ts +1 -1
  110. package/src/utils/git.ts +237 -0
  111. package/src/web/search/index.ts +9 -5
  112. package/src/web/search/providers/codex.ts +195 -99
package/src/utils/git.ts CHANGED
@@ -637,6 +637,10 @@ async function readOptionalText(filePath: string): Promise<string | null> {
637
637
  return retryOnEintr(async () => await Bun.file(filePath).text());
638
638
  }
639
639
 
640
+ async function readOptionalBytes(filePath: string): Promise<Uint8Array | null> {
641
+ return retryOnEintr(async () => await Bun.file(filePath).bytes());
642
+ }
643
+
640
644
  function parseGitDirPointer(content: string): string | null {
641
645
  const match = /^gitdir:\s*(.+)\s*$/iu.exec(content.trim());
642
646
  return match?.[1] ?? null;
@@ -1379,6 +1383,239 @@ export async function writeTree(cwd: string, options: Pick<CommandOptions, "env"
1379
1383
  return (await runText(cwd, ["write-tree"], options)).trim();
1380
1384
  }
1381
1385
 
1386
+ // ════════════════════════════════════════════════════════════════════════════
1387
+ // API: worktree isolation
1388
+ // ════════════════════════════════════════════════════════════════════════════
1389
+
1390
+ /** Outcome of {@link detachGitDir}. */
1391
+ export type DetachGitDirResult =
1392
+ /** `worktreeRoot` had no `.git`; nothing to detach. */
1393
+ | "no-git"
1394
+ /** `.git` already resolves to an independent object DB — left untouched. */
1395
+ | "independent"
1396
+ /** Detached into a standalone repo borrowing `sourceCommonDir`'s objects. */
1397
+ | "detached";
1398
+
1399
+ /**
1400
+ * Sever a copied/mounted working tree from the git metadata it shares with a
1401
+ * source checkout, turning it into a standalone repository that borrows the
1402
+ * source object database through `objects/info/alternates`.
1403
+ *
1404
+ * Isolation backends (reflink/apfs/btrfs/rcopy…) materialise `merged` by
1405
+ * copying `worktreeRoot` byte-for-byte. When `worktreeRoot` is a **linked git
1406
+ * worktree** its `.git` is a pointer file (`gitdir: …/worktrees/<name>`), so
1407
+ * the copy still resolves HEAD/index/refs through the source repo — a task's
1408
+ * `git checkout`/`commit` inside the isolation then mutates the *parent*
1409
+ * checkout. The rcopy `git worktree add` path leaks the other way: task
1410
+ * branches land in the shared ref namespace and stack on each other.
1411
+ *
1412
+ * After detaching, the working tree keeps its files verbatim while:
1413
+ * - HEAD, refs, and the index are frozen to the snapshot at call time;
1414
+ * - all commits/branches the task creates stay private to the isolation;
1415
+ * - objects resolve against `sourceCommonDir` via alternates, so history reads
1416
+ * and later `git fetch <merged>` object transfer keep working;
1417
+ * - the source checkout's HEAD, branch, index, and working tree are untouched.
1418
+ *
1419
+ * A full-copy `.git` (non-worktree source) already owns its object DB and is
1420
+ * returned as `"independent"` without modification. `worktreeRoot` without a
1421
+ * `.git` yields `"no-git"`.
1422
+ */
1423
+ export async function detachGitDir(worktreeRoot: string, sourceCommonDir: string): Promise<DetachGitDirResult> {
1424
+ ensureAvailable();
1425
+ const gitEntry = path.join(worktreeRoot, ".git");
1426
+ let entryStat: fs.Stats;
1427
+ try {
1428
+ entryStat = await fs.promises.lstat(gitEntry);
1429
+ } catch (err) {
1430
+ if (isEnoent(err)) return "no-git";
1431
+ throw err;
1432
+ }
1433
+ // Canonicalize both sides before comparing: `rev-parse` resolves symlinks
1434
+ // (macOS `/tmp` → `/private/tmp`) while callers derive `sourceCommonDir`
1435
+ // lexically from the session cwd. A lexical mismatch here would silently
1436
+ // classify a shared linked-worktree copy as "independent" and skip the
1437
+ // detach entirely — leaving the parent-mutation leak in place.
1438
+ const parentCommon = await fs.promises.realpath(sourceCommonDir).catch(() => path.resolve(sourceCommonDir));
1439
+ const isoCommonRaw = (
1440
+ await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
1441
+ readOnly: true,
1442
+ })
1443
+ ).trim();
1444
+ const isoCommon = await fs.promises.realpath(isoCommonRaw).catch(() => path.resolve(isoCommonRaw));
1445
+ // A full-copy `.git` already resolves to its own object DB — leave it alone.
1446
+ if (isoCommon !== parentCommon) return "independent";
1447
+
1448
+ // Snapshot the state the standalone repo must preserve. HEAD may be a branch
1449
+ // ref (normal checkout), detached, or unborn (a fresh/orphan branch with no
1450
+ // commits — a linked worktree still shares the parent ref namespace, so it
1451
+ // must be severed too). Refs are frozen so `baseSha..branch` ranges and
1452
+ // history reads keep resolving after the source moves on.
1453
+ const headSha = (await tryText(worktreeRoot, ["rev-parse", "HEAD"], { readOnly: true }))?.trim() ?? "";
1454
+ const headRef = (await tryText(worktreeRoot, ["symbolic-ref", "-q", "HEAD"], { readOnly: true }))?.trim() ?? "";
1455
+ const refDump = headSha
1456
+ ? (
1457
+ await runText(worktreeRoot, ["for-each-ref", "--format=%(objectname) %(refname)"], {
1458
+ readOnly: true,
1459
+ })
1460
+ ).trim()
1461
+ : "";
1462
+ const objectFormat =
1463
+ (await tryText(worktreeRoot, ["rev-parse", "--show-object-format"], { readOnly: true }))?.trim() || "sha1";
1464
+ const userName = await config.get(worktreeRoot, "user.name");
1465
+ const userEmail = await config.get(worktreeRoot, "user.email");
1466
+
1467
+ // Preserve the index verbatim rather than round-tripping through
1468
+ // write-tree/read-tree: the raw index carries skip-worktree bits (sparse
1469
+ // checkout), assume-unchanged flags, and exact stage entries. A rebuilt
1470
+ // index drops skip-worktree, so files intentionally absent from a sparse
1471
+ // working tree would read as deletions and delta capture would apply those
1472
+ // deletions back to the parent. Sparse config + patterns are carried too so
1473
+ // later git operations in the isolation keep honouring the sparse view.
1474
+ const indexPath = (
1475
+ await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-path", "index"], {
1476
+ readOnly: true,
1477
+ })
1478
+ ).trim();
1479
+ const indexBytes = await readOptionalBytes(indexPath);
1480
+ const sparseCheckout = await config.get(worktreeRoot, "core.sparseCheckout");
1481
+ const sparseCone = await config.get(worktreeRoot, "core.sparseCheckoutCone");
1482
+ const sparsePatternPath = (
1483
+ await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-path", "info/sparse-checkout"], {
1484
+ readOnly: true,
1485
+ })
1486
+ ).trim();
1487
+ const sparsePatterns = await readOptionalText(sparsePatternPath);
1488
+ // Status parity with the source: an explicit core.filemode (e.g. false on
1489
+ // mounts ignoring the executable bit) must carry over, or the re-inited
1490
+ // repo's platform default makes clean files read as mode-changed and delta
1491
+ // capture would apply bogus chmod diffs back to the parent.
1492
+ const fileMode = await config.get(worktreeRoot, "core.fileMode");
1493
+ // A split index references sharedindex.* files beside the source index;
1494
+ // restoring the raw index without them makes every git read fail. Carry the
1495
+ // shared files (and the config) alongside the verbatim index bytes.
1496
+ const splitIndex = await config.get(worktreeRoot, "core.splitIndex");
1497
+ const sharedIndexFiles: Array<{ name: string; bytes: Uint8Array }> = [];
1498
+ if (indexBytes) {
1499
+ const indexDir = path.dirname(indexPath);
1500
+ let entries: string[] = [];
1501
+ try {
1502
+ entries = await fs.promises.readdir(indexDir);
1503
+ } catch {}
1504
+ for (const name of entries) {
1505
+ if (!name.startsWith("sharedindex.")) continue;
1506
+ const bytes = await readOptionalBytes(path.join(indexDir, name));
1507
+ if (bytes) sharedIndexFiles.push({ name, bytes });
1508
+ }
1509
+ }
1510
+ // A shallow source deliberately lacks parents beyond its `shallow` boundary
1511
+ // file; without it, history traversal over the borrowed objects treats the
1512
+ // boundary commit's missing parent as corruption.
1513
+ const shallowBoundary = await readOptionalText(path.join(parentCommon, "shallow"));
1514
+
1515
+ // A pointer `.git` file whose worktree-admin dir back-references this exact
1516
+ // tree is the rcopy `git worktree add` registration. Remove that admin entry
1517
+ // so the source repo's worktree list stops tracking the isolation. A pointer
1518
+ // referencing the *source's* admin (a copied linked-worktree `.git`) is not
1519
+ // ours to delete — only the local pointer file is discarded. Compare via
1520
+ // realpath: git canonicalizes the back-reference (e.g. macOS `/var` →
1521
+ // `/private/var`), so a lexical path comparison would miss the match and
1522
+ // leave a stale registration in the source repo's worktree list.
1523
+ let ownWorktreeAdmin: string | undefined;
1524
+ if (entryStat.isFile()) {
1525
+ const pointer = parseGitDirPointer((await readOptionalText(gitEntry)) ?? "");
1526
+ if (pointer) {
1527
+ const adminDir = path.resolve(path.dirname(gitEntry), pointer);
1528
+ const backRef = (await readOptionalText(path.join(adminDir, "gitdir")))?.trim();
1529
+ if (backRef) {
1530
+ const [realBackRef, realGitEntry] = await Promise.all([
1531
+ fs.promises.realpath(backRef).catch(() => path.resolve(backRef)),
1532
+ fs.promises.realpath(gitEntry).catch(() => path.resolve(gitEntry)),
1533
+ ]);
1534
+ if (realBackRef === realGitEntry) ownWorktreeAdmin = adminDir;
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ await fs.promises.rm(gitEntry, { recursive: true, force: true });
1540
+ if (ownWorktreeAdmin) await fs.promises.rm(ownWorktreeAdmin, { recursive: true, force: true });
1541
+
1542
+ // Preserve the checked-out branch name so an unborn HEAD (fresh/orphan
1543
+ // branch with no commits) keeps its symbolic ref after `init` rather than
1544
+ // snapping to the init default; born HEADs get the ref rewritten below anyway.
1545
+ const initArgs = ["init", "--object-format", objectFormat, "-q"];
1546
+ const initialBranch = headRef.startsWith(LOCAL_BRANCH_PREFIX) ? headRef.slice(LOCAL_BRANCH_PREFIX.length) : "";
1547
+ if (initialBranch) initArgs.push("-b", initialBranch);
1548
+ await runEffect(worktreeRoot, initArgs);
1549
+ const objectsInfo = path.join(gitEntry, "objects", "info");
1550
+ await fs.promises.mkdir(objectsInfo, { recursive: true });
1551
+ const alternates = [path.join(parentCommon, "objects")];
1552
+ const chained = await readOptionalText(path.join(parentCommon, "objects", "info", "alternates"));
1553
+ if (chained) {
1554
+ for (const line of chained.split("\n")) {
1555
+ const entry = line.trim();
1556
+ if (!entry) continue;
1557
+ alternates.push(path.isAbsolute(entry) ? entry : path.resolve(parentCommon, "objects", entry));
1558
+ }
1559
+ }
1560
+ await Bun.write(path.join(objectsInfo, "alternates"), `${alternates.join("\n")}\n`);
1561
+
1562
+ // Freeze refs when HEAD is born. Point HEAD at the raw SHA first so
1563
+ // `update-ref` writes land even for the branch HEAD currently names, then
1564
+ // restore the symbolic HEAD. An unborn HEAD has no refs to freeze; `init -b`
1565
+ // above already set the symbolic HEAD to the unborn branch.
1566
+ if (headSha) {
1567
+ await Bun.write(path.join(gitEntry, "HEAD"), `${headSha}\n`);
1568
+ if (refDump) {
1569
+ const commands = refDump
1570
+ .split("\n")
1571
+ .filter(Boolean)
1572
+ .map(line => {
1573
+ const sep = line.indexOf(" ");
1574
+ return `create ${line.slice(sep + 1)} ${line.slice(0, sep)}`;
1575
+ })
1576
+ .join("\n");
1577
+ await runEffect(worktreeRoot, ["update-ref", "--stdin"], { stdin: `${commands}\n` });
1578
+ }
1579
+ if (headRef) await Bun.write(path.join(gitEntry, "HEAD"), `ref: ${headRef}\n`);
1580
+ } else if (headRef && !initialBranch) {
1581
+ // Unborn detached HEAD (no branch, no commit) — restore the raw ref target.
1582
+ await Bun.write(path.join(gitEntry, "HEAD"), `ref: ${headRef}\n`);
1583
+ }
1584
+
1585
+ // Carry the source identity so isolated commits have an author.
1586
+ if (userName) await config.set(worktreeRoot, "user.name", userName);
1587
+ if (userEmail) await config.set(worktreeRoot, "user.email", userEmail);
1588
+ if (fileMode !== undefined) await config.set(worktreeRoot, "core.fileMode", fileMode);
1589
+ if (splitIndex !== undefined) await config.set(worktreeRoot, "core.splitIndex", splitIndex);
1590
+ // Preserve the shallow boundary so history traversal over the borrowed
1591
+ // object DB stops at the boundary instead of failing on missing parents.
1592
+ if (shallowBoundary !== null) await Bun.write(path.join(gitEntry, "shallow"), shallowBoundary);
1593
+
1594
+ // Restore sparse-checkout state before the index so skip-worktree entries
1595
+ // keep resolving against the carried patterns.
1596
+ if (sparseCheckout) await config.set(worktreeRoot, "core.sparseCheckout", sparseCheckout);
1597
+ if (sparseCone) await config.set(worktreeRoot, "core.sparseCheckoutCone", sparseCone);
1598
+ if (sparsePatterns !== null) {
1599
+ const infoDir = path.join(gitEntry, "info");
1600
+ await fs.promises.mkdir(infoDir, { recursive: true });
1601
+ await Bun.write(path.join(infoDir, "sparse-checkout"), sparsePatterns);
1602
+ }
1603
+
1604
+ // Restore the index verbatim (skip-worktree, assume-unchanged, exact stage
1605
+ // entries) so the working tree's dirty set — including sparse-excluded files
1606
+ // — matches the source. Fall back to rebuilding from HEAD only when the
1607
+ // source had no index (a bare-ish/never-staged checkout).
1608
+ if (indexBytes) {
1609
+ for (const shared of sharedIndexFiles) {
1610
+ await Bun.write(path.join(gitEntry, shared.name), shared.bytes);
1611
+ }
1612
+ await Bun.write(path.join(gitEntry, "index"), indexBytes);
1613
+ } else if (headSha) {
1614
+ await readTree(worktreeRoot, headSha);
1615
+ }
1616
+ return "detached";
1617
+ }
1618
+
1382
1619
  // ════════════════════════════════════════════════════════════════════════════
1383
1620
  // API: show
1384
1621
  // ════════════════════════════════════════════════════════════════════════════
@@ -134,10 +134,7 @@ async function executeSearch(
134
134
  const explicitProvider = params.provider;
135
135
  let candidates: SearchProviderCandidate[];
136
136
  if (explicitProvider && explicitProvider !== "auto") {
137
- const provider = await getSearchProvider(explicitProvider);
138
- candidates = (await provider.isExplicitlyAvailable(authStorage))
139
- ? [{ id: explicitProvider, explicit: true }]
140
- : resolveProviderCandidates("auto");
137
+ candidates = [{ id: explicitProvider, explicit: true }];
141
138
  } else if (explicitProvider === "auto") {
142
139
  // Explicit `--provider auto` bypasses the configured preferred provider
143
140
  // for this invocation; exclusions still apply.
@@ -175,7 +172,13 @@ async function executeSearch(
175
172
  const available = candidate.explicit
176
173
  ? await provider.isExplicitlyAvailable(authStorage)
177
174
  : await provider.isAvailable(authStorage);
178
- if (!available) continue;
175
+ if (!available && !candidate.explicit) continue;
176
+ if (!available && candidate.explicit) {
177
+ throw new SearchProviderError(
178
+ provider.id,
179
+ `${provider.label} web search is unavailable. Configure its credentials or select the automatic provider chain.`,
180
+ );
181
+ }
179
182
  availableProviderCount++;
180
183
  lastProvider = provider;
181
184
 
@@ -213,6 +216,7 @@ async function executeSearch(
213
216
  // summary error), masking the cancellation.
214
217
  throwIfAborted(signal);
215
218
  failures.push({ provider: provider ?? providerMeta, error });
219
+ if (candidate.explicit) break;
216
220
  }
217
221
  }
218
222