@sideboard-ai/core 0.1.67 → 0.1.68

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 (35) hide show
  1. package/dist/{agents-TS3EI2YI.js → agents-JBD7IAGK.js} +6 -6
  2. package/dist/{agents-OTBZVTZO.js → agents-PE3RHQS5.js} +6 -6
  3. package/dist/{chunk-K7LM4CLD.js → chunk-47RBLBRK.js} +4 -4
  4. package/dist/{chunk-B4PRRKMA.js → chunk-6Q7H6SMX.js} +4 -4
  5. package/dist/{chunk-FGDJOJ2L.js → chunk-AE5VBOFE.js} +1 -1
  6. package/dist/{chunk-DB3I75FI.js → chunk-BTL7EMGT.js} +4 -4
  7. package/dist/{chunk-4UIYN56C.js → chunk-D4DPQ552.js} +1 -1
  8. package/dist/{chunk-WAFIF22N.js → chunk-DPDGMMGI.js} +2 -2
  9. package/dist/{chunk-4BUCGNZS.js → chunk-EEBDVLDK.js} +22 -20
  10. package/dist/{chunk-57WWNMFP.js → chunk-N5LMEP65.js} +1 -1
  11. package/dist/{chunk-JWZNK3CQ.js → chunk-OICHV4DH.js} +2 -2
  12. package/dist/{chunk-5HTTCYWA.js → chunk-QQC2D4MY.js} +2 -2
  13. package/dist/{chunk-LW5CMQGZ.js → chunk-REKGFVUX.js} +22 -20
  14. package/dist/{chunk-7ZOQY4A7.js → chunk-ROXEENBT.js} +2 -2
  15. package/dist/{chunk-M5V4QIWF.js → chunk-VROPG6QF.js} +1 -1
  16. package/dist/{chunk-ZRFAZVGL.js → chunk-VX6ACNXS.js} +1 -1
  17. package/dist/{connected-teams-RUNMOCG5.js → connected-teams-35EIRJCP.js} +2 -2
  18. package/dist/{connected-teams-OMDHTL6A.js → connected-teams-ZRS53OAT.js} +2 -2
  19. package/dist/{coordinator-prompt-3BSMKZL4.js → coordinator-prompt-HD6IMCKS.js} +3 -3
  20. package/dist/{coordinator-prompt-6HMTOX4G.js → coordinator-prompt-YZYZ5P6P.js} +3 -3
  21. package/dist/{global-workspace-QS7A23CB.js → global-workspace-7P5NG3JZ.js} +4 -4
  22. package/dist/{global-workspace-YZ2NJVLL.js → global-workspace-Z6GF7D2K.js} +4 -4
  23. package/dist/index.cjs +367 -158
  24. package/dist/index.d.cts +27 -1
  25. package/dist/index.d.ts +27 -1
  26. package/dist/index.js +364 -157
  27. package/dist/mcp/run-stdio.cjs +367 -158
  28. package/dist/mcp/run-stdio.js +363 -156
  29. package/dist/{run-Y2MI3EVV.js → run-A4RHY7HH.js} +1 -1
  30. package/dist/{run-JLD2Y52J.js → run-V2WZUMJA.js} +1 -1
  31. package/dist/{workspaces-EBFW7I7S.js → workspaces-BWJNGEE6.js} +5 -5
  32. package/dist/{workspaces-AIUYUXB7.js → workspaces-O6EZGKQK.js} +5 -5
  33. package/dist/{worktree-WMDJW2BL.js → worktree-VDXLUOWR.js} +2 -2
  34. package/dist/{worktree-7NBPIVFZ.js → worktree-ZPSKEZFE.js} +2 -2
  35. package/package.json +1 -1
@@ -1649,7 +1649,7 @@ async function git(args, cwd, opts) {
1649
1649
  prefix.push("-c", `${key}=${value}`);
1650
1650
  }
1651
1651
  }
1652
- return run("git", [...prefix, ...args], {
1652
+ return run("git", ["--no-pager", ...prefix, ...args], {
1653
1653
  cwd,
1654
1654
  reject: opts?.reject,
1655
1655
  timeoutMs: opts?.timeoutMs,
@@ -2124,7 +2124,7 @@ async function originGhRepoEnv(cwd) {
2124
2124
  const slug = await resolveGithubRepoSlug(cwd);
2125
2125
  return slug ? { GH_REPO: slug } : {};
2126
2126
  }
2127
- async function resolveDefaultBranch(repoPath) {
2127
+ async function resolveDefaultBranch(repoPath, opts) {
2128
2128
  const viaOrigin = await git(
2129
2129
  ["symbolic-ref", "refs/remotes/origin/HEAD"],
2130
2130
  repoPath,
@@ -2133,22 +2133,24 @@ async function resolveDefaultBranch(repoPath) {
2133
2133
  if (viaOrigin.exitCode === 0 && viaOrigin.stdout.trim()) {
2134
2134
  return viaOrigin.stdout.trim().replace(/^refs\/remotes\/origin\//, "");
2135
2135
  }
2136
- const slug = await resolveGithubRepoSlug(repoPath);
2137
- const viaGh = await gh(
2138
- [
2139
- "repo",
2140
- "view",
2141
- ...slug ? ["--repo", slug] : [],
2142
- "--json",
2143
- "defaultBranchRef",
2144
- "--jq",
2145
- ".defaultBranchRef.name"
2146
- ],
2147
- repoPath,
2148
- { reject: false }
2149
- );
2150
- if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2151
- return viaGh.stdout.trim();
2136
+ if (opts?.network !== false) {
2137
+ const slug = await resolveGithubRepoSlug(repoPath);
2138
+ const viaGh = await gh(
2139
+ [
2140
+ "repo",
2141
+ "view",
2142
+ ...slug ? ["--repo", slug] : [],
2143
+ "--json",
2144
+ "defaultBranchRef",
2145
+ "--jq",
2146
+ ".defaultBranchRef.name"
2147
+ ],
2148
+ repoPath,
2149
+ { reject: false, timeoutMs: 8e3 }
2150
+ );
2151
+ if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2152
+ return viaGh.stdout.trim();
2153
+ }
2152
2154
  }
2153
2155
  for (const candidate of ["main", "master"]) {
2154
2156
  const check = await git(
@@ -2800,11 +2802,11 @@ async function listWorktrees(repoPath) {
2800
2802
  return entries;
2801
2803
  }
2802
2804
  async function isDirty(worktreePath) {
2803
- const { stdout } = await git(["status", "--porcelain", "-uall"], worktreePath);
2805
+ const { stdout } = await git(["status", "--porcelain"], worktreePath);
2804
2806
  for (const line of stdout.split("\n")) {
2805
2807
  if (!line.trim()) continue;
2806
2808
  const rel = porcelainStatusPath(line);
2807
- if (!rel || isSideboardScratchPath(rel)) continue;
2809
+ if (!rel || isSideboardScratchPath(rel) || rel === ".sideboard") continue;
2808
2810
  return true;
2809
2811
  }
2810
2812
  return false;
@@ -9744,24 +9746,25 @@ function capPatch(patch, maxHunk) {
9744
9746
 
9745
9747
  \u2026 truncated (${patch.length - maxHunk} more chars)`;
9746
9748
  }
9749
+ var mergeBaseCache = /* @__PURE__ */ new Map();
9750
+ var MERGE_BASE_TTL_MS = 45e3;
9747
9751
  async function resolveMergeBase(worktreePath, base, repoPath) {
9748
9752
  const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
9753
+ const cacheKey = `${worktreePath}\0${baseRef}`;
9754
+ const hit = mergeBaseCache.get(cacheKey);
9755
+ if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
9749
9756
  const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
9750
9757
  reject: false
9751
9758
  });
9752
- const mb = stdout.trim();
9753
- return mb || baseRef;
9759
+ const mb = stdout.trim() || baseRef;
9760
+ mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
9761
+ return mb;
9754
9762
  }
9755
9763
  async function countUnpushedCommits(worktreePath) {
9756
9764
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
9757
9765
  reject: false
9758
9766
  });
9759
9767
  const branch = head.stdout.trim();
9760
- if (branch && branch !== "HEAD") {
9761
- await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
9762
- reject: false
9763
- });
9764
- }
9765
9768
  const upstream = await git(
9766
9769
  ["rev-list", "--count", "@{upstream}..HEAD"],
9767
9770
  worktreePath,
@@ -9794,11 +9797,37 @@ function splitCombinedDiff(stdout) {
9794
9797
  }
9795
9798
  return map;
9796
9799
  }
9800
+ function syntheticAddPatch(path, content, maxHunk) {
9801
+ const lines = content.split("\n");
9802
+ const body = lines.map((l) => `+${l}`).join("\n");
9803
+ const header = `diff --git a/${path} b/${path}
9804
+ new file mode 100644
9805
+ --- /dev/null
9806
+ +++ b/${path}
9807
+ @@ -0,0 +1,${lines.length} @@
9808
+ `;
9809
+ return {
9810
+ path,
9811
+ status: "A",
9812
+ patch: capPatch(`${header}${body}`, maxHunk),
9813
+ additions: lines.length,
9814
+ deletions: 0
9815
+ };
9816
+ }
9797
9817
  async function untrackedPatch(worktreePath, path, maxHunk) {
9818
+ const abs = (0, import_node_path20.join)(worktreePath, path);
9819
+ try {
9820
+ const st = (0, import_node_fs25.statSync)(abs);
9821
+ if (st.isFile() && st.size > maxHunk) {
9822
+ const buf = (0, import_node_fs25.readFileSync)(abs).subarray(0, maxHunk);
9823
+ return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
9824
+ }
9825
+ } catch {
9826
+ }
9798
9827
  const { stdout: patch } = await git(
9799
- ["diff", "--no-index", "--", "/dev/null", path],
9828
+ ["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
9800
9829
  worktreePath,
9801
- { reject: false }
9830
+ { reject: false, timeoutMs: 4e3 }
9802
9831
  );
9803
9832
  const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
9804
9833
  const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
@@ -9810,22 +9839,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
9810
9839
  deletions: 0
9811
9840
  };
9812
9841
  }
9813
- async function listUntrackedPaths(worktreePath) {
9814
- const { stdout } = await git(
9815
- ["ls-files", "-z", "--others", "--exclude-standard"],
9816
- worktreePath,
9817
- { reject: false }
9818
- );
9842
+ async function listUntrackedPaths(worktreePath, path) {
9843
+ const args = ["ls-files", "-z", "--others", "--exclude-standard"];
9844
+ if (path) args.push("--", path);
9845
+ const { stdout } = await git(args, worktreePath, { reject: false });
9819
9846
  return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
9820
9847
  }
9821
- async function listUntrackedFiles(worktreePath, maxHunk) {
9822
- const paths = await listUntrackedPaths(worktreePath);
9823
- if (paths.length === 0) return [];
9824
- return Promise.all(paths.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
9848
+ async function listUntrackedFiles(worktreePath, maxHunk, paths) {
9849
+ const list = paths ?? await listUntrackedPaths(worktreePath);
9850
+ if (list.length === 0) return [];
9851
+ return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
9852
+ }
9853
+ function untrackedStubs(paths) {
9854
+ return paths.map((path) => ({
9855
+ path,
9856
+ status: "A",
9857
+ patch: "",
9858
+ additions: 0,
9859
+ deletions: 0
9860
+ }));
9861
+ }
9862
+ function pathspec(path) {
9863
+ return path ? ["--", path] : [];
9864
+ }
9865
+ function gitDiff(args, cwd, opts) {
9866
+ return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
9867
+ reject: false,
9868
+ timeoutMs: opts?.timeoutMs
9869
+ });
9870
+ }
9871
+ function fileFromPatch(path, patch, maxHunk) {
9872
+ let status = "M";
9873
+ if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
9874
+ status = "A";
9875
+ } else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
9876
+ status = "D";
9877
+ } else if (/^rename from /m.test(patch)) {
9878
+ status = "R";
9879
+ }
9880
+ const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
9881
+ const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
9882
+ return {
9883
+ path,
9884
+ status,
9885
+ patch: capPatch(patch, maxHunk),
9886
+ additions,
9887
+ deletions
9888
+ };
9889
+ }
9890
+ async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
9891
+ const extra = pathspec(path);
9892
+ if (includePatches) {
9893
+ const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
9894
+ timeoutMs: 4e3
9895
+ });
9896
+ return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
9897
+ }
9898
+ const [ns, num2] = await Promise.all([
9899
+ gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
9900
+ gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
9901
+ ]);
9902
+ return {
9903
+ nameStatus: ns.stdout,
9904
+ numstat: num2.stdout,
9905
+ combinedDiff: ""
9906
+ };
9907
+ }
9908
+ function formatStat(files) {
9909
+ if (files.length === 0) return "(no changes)";
9910
+ const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
9911
+ const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
9912
+ const n = files.length;
9913
+ return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
9914
+ }
9915
+ function emptyScopeStats() {
9916
+ const z2 = emptyStat();
9917
+ return {
9918
+ commits: z2,
9919
+ uncommitted: z2,
9920
+ staged: z2,
9921
+ unstaged: z2,
9922
+ last_turn: z2
9923
+ };
9825
9924
  }
9826
9925
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
9827
- const counts = parseNumstat(numstat);
9828
9926
  const patches = splitCombinedDiff(combinedDiff);
9927
+ if (!nameStatus.trim() && patches.size > 0) {
9928
+ return [...patches.entries()].map(
9929
+ ([path, patch]) => fileFromPatch(path, patch, maxHunk)
9930
+ );
9931
+ }
9932
+ const counts = parseNumstat(numstat);
9829
9933
  const files = [];
9830
9934
  for (const { status, path } of parseNameStatus(nameStatus)) {
9831
9935
  const n = counts.get(path);
@@ -9847,11 +9951,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
9847
9951
  { stdout: unstagedNum },
9848
9952
  lastTurnNum
9849
9953
  ] = await Promise.all([
9850
- mergeBase ? git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" }),
9851
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
9852
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9853
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9854
- lastTurnBase ? git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" })
9954
+ mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
9955
+ gitDiff(["--numstat", "HEAD"], worktreePath),
9956
+ gitDiff(["--numstat", "--cached"], worktreePath),
9957
+ gitDiff(["--numstat"], worktreePath),
9958
+ lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
9855
9959
  ]);
9856
9960
  return {
9857
9961
  commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
@@ -9870,7 +9974,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9870
9974
  if (head.exitCode !== 0) return [];
9871
9975
  let base = opts?.base;
9872
9976
  try {
9873
- base = base ?? await resolveDefaultBranch(repoPath);
9977
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
9874
9978
  } catch {
9875
9979
  base = "HEAD";
9876
9980
  }
@@ -9900,7 +10004,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9900
10004
  }
9901
10005
  return commits;
9902
10006
  }
10007
+ var SHA_RE = /^[0-9a-f]{7,40}$/i;
10008
+ function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
10009
+ return {
10010
+ scope,
10011
+ commitSha: scope === "commits" ? commitSha : null,
10012
+ base,
10013
+ files: [],
10014
+ stat: formatStat([]),
10015
+ dirty: false,
10016
+ unpushed: 0,
10017
+ hasLastTurnBase,
10018
+ commits: [],
10019
+ scopeStats: emptyScopeStats()
10020
+ };
10021
+ }
10022
+ async function getPathDiff(worktreePath, repoPath, opts) {
10023
+ const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
10024
+ if (scope === "last_turn" && !lastTurnBase) {
10025
+ return emptyPathDiff(scope, "last turn", false);
10026
+ }
10027
+ let diffArgs;
10028
+ let labelBase;
10029
+ if (scope === "staged") {
10030
+ diffArgs = ["--cached"];
10031
+ labelBase = "staged";
10032
+ } else if (scope === "unstaged") {
10033
+ diffArgs = [];
10034
+ labelBase = "unstaged";
10035
+ } else if (scope === "uncommitted") {
10036
+ diffArgs = ["HEAD"];
10037
+ labelBase = "HEAD";
10038
+ } else if (scope === "last_turn") {
10039
+ diffArgs = [lastTurnBase];
10040
+ labelBase = "last turn";
10041
+ } else if (scope === "commits" && commitSha) {
10042
+ diffArgs = [`${commitSha}^!`];
10043
+ labelBase = commitSha.slice(0, 7);
10044
+ } else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
10045
+ diffArgs = [opts.base];
10046
+ labelBase = opts.base.slice(0, 7);
10047
+ } else if (scope === "commits") {
10048
+ let base = opts.base;
10049
+ try {
10050
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
10051
+ } catch {
10052
+ base = "HEAD";
10053
+ }
10054
+ const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
10055
+ diffArgs = [mergeBase];
10056
+ labelBase = mergeBase;
10057
+ } else {
10058
+ diffArgs = ["HEAD"];
10059
+ labelBase = "HEAD";
10060
+ }
10061
+ const diff = await gitDiff(
10062
+ includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
10063
+ worktreePath,
10064
+ { timeoutMs: 4e3 }
10065
+ );
10066
+ const filesMap = /* @__PURE__ */ new Map();
10067
+ if (includePatches) {
10068
+ const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
10069
+ for (const file of parsed) filesMap.set(file.path, file);
10070
+ if (diff.stdout.trim() && !filesMap.has(path)) {
10071
+ filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
10072
+ }
10073
+ } else {
10074
+ for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
10075
+ filesMap.set(file.path, file);
10076
+ }
10077
+ }
10078
+ if (filesMap.size === 0) {
10079
+ const maybe = await listUntrackedPaths(worktreePath, path);
10080
+ if (maybe.length > 0) {
10081
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
10082
+ for (const file of extra) filesMap.set(file.path, file);
10083
+ }
10084
+ }
10085
+ const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
10086
+ return {
10087
+ scope,
10088
+ commitSha: scope === "commits" ? commitSha : null,
10089
+ base: labelBase,
10090
+ files,
10091
+ stat: formatStat(files),
10092
+ dirty: files.length > 0,
10093
+ unpushed: 0,
10094
+ hasLastTurnBase,
10095
+ commits: [],
10096
+ scopeStats: emptyScopeStats(),
10097
+ mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
10098
+ };
10099
+ }
9903
10100
  async function getDiff(worktreePath, repoPath, opts) {
10101
+ const maxHunk = opts?.maxHunkChars ?? 8e3;
10102
+ const scope = opts?.scope ?? "commits";
10103
+ const lastTurnBase = opts?.lastTurnBase?.trim() || null;
10104
+ const hasLastTurnBase = Boolean(lastTurnBase);
10105
+ const commitSha = opts?.commitSha?.trim() || null;
10106
+ const includePatches = opts?.includePatches ?? true;
10107
+ const pathFilter = opts?.path?.trim() || null;
10108
+ const fileOnly = Boolean(pathFilter);
10109
+ const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
10110
+ if (fileOnly && pathFilter && !includeMeta) {
10111
+ return getPathDiff(worktreePath, repoPath, {
10112
+ path: pathFilter,
10113
+ scope,
10114
+ commitSha,
10115
+ lastTurnBase,
10116
+ hasLastTurnBase,
10117
+ base: opts?.base,
10118
+ includePatches,
10119
+ maxHunk
10120
+ });
10121
+ }
9904
10122
  const status = await inspectGitWorktree(worktreePath);
9905
10123
  if (status === "missing_worktree") {
9906
10124
  throw new Error("Worktree not found");
@@ -9908,18 +10126,19 @@ async function getDiff(worktreePath, repoPath, opts) {
9908
10126
  if (status === "not_git") {
9909
10127
  throw new Error("Not a Git repository");
9910
10128
  }
9911
- const maxHunk = opts?.maxHunkChars ?? 8e3;
9912
- const scope = opts?.scope ?? "commits";
9913
- const lastTurnBase = opts?.lastTurnBase?.trim() || null;
9914
- const hasLastTurnBase = Boolean(lastTurnBase);
9915
- const commitSha = opts?.commitSha?.trim() || null;
9916
- const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
10129
+ const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
10130
+ const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
10131
+ const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
9917
10132
  reject: false
9918
10133
  });
10134
+ const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
10135
+ const head = await headP;
9919
10136
  const hasHead = head.exitCode === 0;
9920
- const untrackedPaths = await listUntrackedPaths(worktreePath);
9921
10137
  if (!hasHead) {
9922
- const files2 = scope === "staged" ? [] : (await listUntrackedFiles(worktreePath, maxHunk)).sort(
10138
+ const untrackedPaths2 = await untrackedP;
10139
+ const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
10140
+ (a, b) => a.path.localeCompare(b.path)
10141
+ ) : untrackedStubs(untrackedPaths2).sort(
9923
10142
  (a, b) => a.path.localeCompare(b.path)
9924
10143
  );
9925
10144
  const untrackedStat = {
@@ -9933,7 +10152,7 @@ async function getDiff(worktreePath, repoPath, opts) {
9933
10152
  commitSha: null,
9934
10153
  base: "(no commits)",
9935
10154
  files: files2,
9936
- stat: files2.length ? files2.map((f) => ` ${f.path} | untracked`).join("\n") : "(no changes)",
10155
+ stat: formatStat(files2),
9937
10156
  dirty: files2.length > 0,
9938
10157
  unpushed: 0,
9939
10158
  hasLastTurnBase,
@@ -9947,137 +10166,122 @@ async function getDiff(worktreePath, repoPath, opts) {
9947
10166
  }
9948
10167
  };
9949
10168
  }
9950
- let base = opts?.base;
9951
- try {
9952
- base = base ?? await resolveDefaultBranch(repoPath);
9953
- } catch {
9954
- base = "HEAD";
10169
+ let mergeBase = "HEAD";
10170
+ let baseLabel = "HEAD";
10171
+ if (needsMergeBase) {
10172
+ let base = opts?.base;
10173
+ try {
10174
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
10175
+ } catch {
10176
+ base = "HEAD";
10177
+ }
10178
+ mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
10179
+ baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
9955
10180
  }
9956
- base = await resolveDiffBaseRef(worktreePath, base, repoPath);
9957
- if (base.startsWith("origin/")) {
9958
- const short = base.slice("origin/".length);
9959
- await git(["fetch", "origin", short, "--quiet"], worktreePath, {
9960
- reject: false
9961
- });
9962
- base = await resolveDiffBaseRef(worktreePath, short, repoPath);
10181
+ const includeUntracked = wantUntracked;
10182
+ const metaPromise = includeMeta ? Promise.all([
10183
+ computeScopeStats(
10184
+ worktreePath,
10185
+ mergeBase,
10186
+ lastTurnBase,
10187
+ // Cheap: don't wait on the untracked walk just to pad the badge.
10188
+ 0
10189
+ ),
10190
+ listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
10191
+ isDirty(worktreePath),
10192
+ countUnpushedCommits(worktreePath)
10193
+ ]).then(([scopeStats, commits, dirty, unpushed]) => ({
10194
+ scopeStats,
10195
+ commits,
10196
+ dirty,
10197
+ unpushed
10198
+ })) : Promise.resolve({
10199
+ scopeStats: emptyScopeStats(),
10200
+ commits: [],
10201
+ dirty: false,
10202
+ unpushed: 0
10203
+ });
10204
+ if (scope === "last_turn" && !lastTurnBase) {
10205
+ const meta2 = await metaPromise;
10206
+ return {
10207
+ scope,
10208
+ commitSha: null,
10209
+ base: "last turn",
10210
+ files: [],
10211
+ stat: "(no last agent turn)",
10212
+ dirty: meta2.dirty,
10213
+ unpushed: meta2.unpushed,
10214
+ hasLastTurnBase: false,
10215
+ commits: meta2.commits,
10216
+ scopeStats: meta2.scopeStats
10217
+ };
9963
10218
  }
9964
- const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
9965
- const scopeStats = await computeScopeStats(
9966
- worktreePath,
9967
- mergeBase,
9968
- lastTurnBase,
9969
- untrackedPaths.length
9970
- );
9971
- const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
9972
- let nameStatus = "";
9973
- let numstat = "";
9974
- let combinedDiff = "";
9975
- let labelBase = base;
10219
+ let diffArgs;
10220
+ let labelBase = baseLabel;
9976
10221
  if (scope === "staged") {
9977
- const [ns, num2, diff] = await Promise.all([
9978
- git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
9979
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9980
- git(["diff", "--cached"], worktreePath, { reject: false })
9981
- ]);
9982
- nameStatus = ns.stdout;
9983
- numstat = num2.stdout;
9984
- combinedDiff = diff.stdout;
10222
+ diffArgs = ["--cached"];
9985
10223
  labelBase = "staged";
9986
10224
  } else if (scope === "unstaged") {
9987
- const [ns, num2, diff] = await Promise.all([
9988
- git(["diff", "--name-status"], worktreePath, { reject: false }),
9989
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9990
- git(["diff"], worktreePath, { reject: false })
9991
- ]);
9992
- nameStatus = ns.stdout;
9993
- numstat = num2.stdout;
9994
- combinedDiff = diff.stdout;
10225
+ diffArgs = [];
9995
10226
  labelBase = "unstaged";
9996
10227
  } else if (scope === "uncommitted") {
9997
- const [ns, num2, diff] = await Promise.all([
9998
- git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
9999
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
10000
- git(["diff", "HEAD"], worktreePath, { reject: false })
10001
- ]);
10002
- nameStatus = ns.stdout;
10003
- numstat = num2.stdout;
10004
- combinedDiff = diff.stdout;
10228
+ diffArgs = ["HEAD"];
10005
10229
  labelBase = "HEAD";
10006
10230
  } else if (scope === "last_turn") {
10007
- if (!lastTurnBase) {
10008
- const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
10009
- return {
10010
- scope,
10011
- commitSha: null,
10012
- base: "last turn",
10013
- files: [],
10014
- stat: "(no last agent turn)",
10015
- dirty: await isDirty(worktreePath),
10016
- unpushed: await countUnpushedCommits(worktreePath),
10017
- hasLastTurnBase: false,
10018
- commits: commits2,
10019
- scopeStats
10020
- };
10021
- }
10022
- const [ns, num2, diff] = await Promise.all([
10023
- git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
10024
- git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
10025
- git(["diff", lastTurnBase], worktreePath, { reject: false })
10026
- ]);
10027
- nameStatus = ns.stdout;
10028
- numstat = num2.stdout;
10029
- combinedDiff = diff.stdout;
10231
+ diffArgs = [lastTurnBase];
10030
10232
  labelBase = "last turn";
10031
10233
  } else if (scope === "commits" && commitSha) {
10032
- const range = `${commitSha}^!`;
10033
- const [ns, num2, diff] = await Promise.all([
10034
- git(["diff", "--name-status", range], worktreePath, { reject: false }),
10035
- git(["diff", "--numstat", range], worktreePath, { reject: false }),
10036
- git(["diff", range], worktreePath, { reject: false })
10037
- ]);
10038
- nameStatus = ns.stdout;
10039
- numstat = num2.stdout;
10040
- combinedDiff = diff.stdout;
10234
+ diffArgs = [`${commitSha}^!`];
10041
10235
  labelBase = commitSha.slice(0, 7);
10042
10236
  } else {
10043
- const [ns, num2, diff] = await Promise.all([
10044
- git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
10045
- git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
10046
- git(["diff", mergeBase], worktreePath, { reject: false })
10047
- ]);
10048
- nameStatus = ns.stdout;
10049
- numstat = num2.stdout;
10050
- combinedDiff = diff.stdout;
10051
- labelBase = base;
10237
+ diffArgs = [mergeBase];
10238
+ labelBase = baseLabel;
10052
10239
  }
10053
- const commits = await listBranchCommits(worktreePath, repoPath, { base });
10240
+ const partsPromise = gitDiffParts(
10241
+ worktreePath,
10242
+ diffArgs,
10243
+ includePatches,
10244
+ pathFilter
10245
+ );
10246
+ const [parts, meta, untrackedPaths] = await Promise.all([
10247
+ partsPromise,
10248
+ metaPromise,
10249
+ untrackedP
10250
+ ]);
10054
10251
  const filesMap = /* @__PURE__ */ new Map();
10055
- for (const file of filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk)) {
10252
+ for (const file of filesFromDiff(
10253
+ parts.nameStatus,
10254
+ parts.numstat,
10255
+ parts.combinedDiff,
10256
+ maxHunk
10257
+ )) {
10056
10258
  filesMap.set(file.path, file);
10057
10259
  }
10058
10260
  if (includeUntracked) {
10059
- const untrackedFiles = await listUntrackedFiles(worktreePath, maxHunk);
10060
- for (const file of untrackedFiles) {
10261
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
10262
+ for (const file of extra) {
10061
10263
  if (!filesMap.has(file.path)) filesMap.set(file.path, file);
10062
10264
  }
10265
+ } else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
10266
+ const maybe = await listUntrackedPaths(worktreePath, pathFilter);
10267
+ if (maybe.length > 0) {
10268
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
10269
+ for (const file of extra) filesMap.set(file.path, file);
10270
+ }
10063
10271
  }
10064
10272
  const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
10065
- const { stdout: statOut } = await git(
10066
- scope === "staged" ? ["diff", "--stat", "--cached"] : scope === "unstaged" ? ["diff", "--stat"] : scope === "uncommitted" ? ["diff", "--stat", "HEAD"] : scope === "last_turn" && lastTurnBase ? ["diff", "--stat", lastTurnBase] : scope === "commits" && commitSha ? ["diff", "--stat", `${commitSha}^!`] : ["diff", "--stat", mergeBase],
10067
- worktreePath,
10068
- { reject: false }
10069
- );
10070
10273
  return {
10071
10274
  scope,
10072
10275
  commitSha: scope === "commits" ? commitSha : null,
10073
10276
  base: labelBase,
10074
10277
  files,
10075
- stat: statOut.trim() || (files.length ? `${files.length} file(s)` : "(no changes)"),
10076
- dirty: await isDirty(worktreePath),
10077
- unpushed: await countUnpushedCommits(worktreePath),
10278
+ stat: formatStat(files),
10279
+ dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
10280
+ unpushed: meta.unpushed,
10078
10281
  hasLastTurnBase,
10079
- commits,
10080
- scopeStats
10282
+ commits: meta.commits,
10283
+ scopeStats: meta.scopeStats,
10284
+ mergeBase: needsMergeBase ? mergeBase : null
10081
10285
  };
10082
10286
  }
10083
10287
  async function listWorktreeFiles(worktreePath, opts) {
@@ -12031,6 +12235,11 @@ var Orchestrator = class {
12031
12235
  return getDiff(thread.worktreePath, thread.repoPath, {
12032
12236
  scope: opts?.scope,
12033
12237
  commitSha: opts?.commitSha,
12238
+ base: opts?.base,
12239
+ includePatches: opts?.includePatches,
12240
+ includeMeta: opts?.includeMeta,
12241
+ includeUntracked: opts?.includeUntracked,
12242
+ path: opts?.path,
12034
12243
  lastTurnBase: this.turnBaselines.get(thread.id) ?? null
12035
12244
  });
12036
12245
  }