@buildautomaton/cli 0.1.50 → 0.1.52

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/dist/cli.js CHANGED
@@ -973,8 +973,8 @@ var require_command = __commonJS({
973
973
  "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports) {
974
974
  var EventEmitter2 = __require("node:events").EventEmitter;
975
975
  var childProcess2 = __require("node:child_process");
976
- var path55 = __require("node:path");
977
- var fs46 = __require("node:fs");
976
+ var path63 = __require("node:path");
977
+ var fs47 = __require("node:fs");
978
978
  var process8 = __require("node:process");
979
979
  var { Argument: Argument2, humanReadableArgName } = require_argument();
980
980
  var { CommanderError: CommanderError2 } = require_error();
@@ -1906,11 +1906,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1906
1906
  let launchWithNode = false;
1907
1907
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1908
1908
  function findFile(baseDir, baseName) {
1909
- const localBin = path55.resolve(baseDir, baseName);
1910
- if (fs46.existsSync(localBin)) return localBin;
1911
- if (sourceExt.includes(path55.extname(baseName))) return void 0;
1909
+ const localBin = path63.resolve(baseDir, baseName);
1910
+ if (fs47.existsSync(localBin)) return localBin;
1911
+ if (sourceExt.includes(path63.extname(baseName))) return void 0;
1912
1912
  const foundExt = sourceExt.find(
1913
- (ext) => fs46.existsSync(`${localBin}${ext}`)
1913
+ (ext) => fs47.existsSync(`${localBin}${ext}`)
1914
1914
  );
1915
1915
  if (foundExt) return `${localBin}${foundExt}`;
1916
1916
  return void 0;
@@ -1922,21 +1922,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1922
1922
  if (this._scriptPath) {
1923
1923
  let resolvedScriptPath;
1924
1924
  try {
1925
- resolvedScriptPath = fs46.realpathSync(this._scriptPath);
1925
+ resolvedScriptPath = fs47.realpathSync(this._scriptPath);
1926
1926
  } catch (err) {
1927
1927
  resolvedScriptPath = this._scriptPath;
1928
1928
  }
1929
- executableDir = path55.resolve(
1930
- path55.dirname(resolvedScriptPath),
1929
+ executableDir = path63.resolve(
1930
+ path63.dirname(resolvedScriptPath),
1931
1931
  executableDir
1932
1932
  );
1933
1933
  }
1934
1934
  if (executableDir) {
1935
1935
  let localFile = findFile(executableDir, executableFile);
1936
1936
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1937
- const legacyName = path55.basename(
1937
+ const legacyName = path63.basename(
1938
1938
  this._scriptPath,
1939
- path55.extname(this._scriptPath)
1939
+ path63.extname(this._scriptPath)
1940
1940
  );
1941
1941
  if (legacyName !== this._name) {
1942
1942
  localFile = findFile(
@@ -1947,7 +1947,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1947
1947
  }
1948
1948
  executableFile = localFile || executableFile;
1949
1949
  }
1950
- launchWithNode = sourceExt.includes(path55.extname(executableFile));
1950
+ launchWithNode = sourceExt.includes(path63.extname(executableFile));
1951
1951
  let proc;
1952
1952
  if (process8.platform !== "win32") {
1953
1953
  if (launchWithNode) {
@@ -2787,7 +2787,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2787
2787
  * @return {Command}
2788
2788
  */
2789
2789
  nameFromFilename(filename) {
2790
- this._name = path55.basename(filename, path55.extname(filename));
2790
+ this._name = path63.basename(filename, path63.extname(filename));
2791
2791
  return this;
2792
2792
  }
2793
2793
  /**
@@ -2801,9 +2801,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2801
2801
  * @param {string} [path]
2802
2802
  * @return {(string|null|Command)}
2803
2803
  */
2804
- executableDir(path56) {
2805
- if (path56 === void 0) return this._executableDir;
2806
- this._executableDir = path56;
2804
+ executableDir(path64) {
2805
+ if (path64 === void 0) return this._executableDir;
2806
+ this._executableDir = path64;
2807
2807
  return this;
2808
2808
  }
2809
2809
  /**
@@ -7061,8 +7061,8 @@ var init_parseUtil = __esm({
7061
7061
  init_errors();
7062
7062
  init_en();
7063
7063
  makeIssue = (params) => {
7064
- const { data, path: path55, errorMaps, issueData } = params;
7065
- const fullPath = [...path55, ...issueData.path || []];
7064
+ const { data, path: path63, errorMaps, issueData } = params;
7065
+ const fullPath = [...path63, ...issueData.path || []];
7066
7066
  const fullIssue = {
7067
7067
  ...issueData,
7068
7068
  path: fullPath
@@ -7370,11 +7370,11 @@ var init_types = __esm({
7370
7370
  init_parseUtil();
7371
7371
  init_util();
7372
7372
  ParseInputLazyPath = class {
7373
- constructor(parent, value, path55, key) {
7373
+ constructor(parent, value, path63, key) {
7374
7374
  this._cachedPath = [];
7375
7375
  this.parent = parent;
7376
7376
  this.data = value;
7377
- this._path = path55;
7377
+ this._path = path63;
7378
7378
  this._key = key;
7379
7379
  }
7380
7380
  get path() {
@@ -11099,10 +11099,10 @@ function assignProp(target, prop, value) {
11099
11099
  configurable: true
11100
11100
  });
11101
11101
  }
11102
- function getElementAtPath(obj, path55) {
11103
- if (!path55)
11102
+ function getElementAtPath(obj, path63) {
11103
+ if (!path63)
11104
11104
  return obj;
11105
- return path55.reduce((acc, key) => acc?.[key], obj);
11105
+ return path63.reduce((acc, key) => acc?.[key], obj);
11106
11106
  }
11107
11107
  function promiseAllObject(promisesObj) {
11108
11108
  const keys = Object.keys(promisesObj);
@@ -11351,11 +11351,11 @@ function aborted(x, startIndex = 0) {
11351
11351
  }
11352
11352
  return false;
11353
11353
  }
11354
- function prefixIssues(path55, issues) {
11354
+ function prefixIssues(path63, issues) {
11355
11355
  return issues.map((iss) => {
11356
11356
  var _a2;
11357
11357
  (_a2 = iss).path ?? (_a2.path = []);
11358
- iss.path.unshift(path55);
11358
+ iss.path.unshift(path63);
11359
11359
  return iss;
11360
11360
  });
11361
11361
  }
@@ -11544,7 +11544,7 @@ function treeifyError(error40, _mapper) {
11544
11544
  return issue2.message;
11545
11545
  };
11546
11546
  const result = { errors: [] };
11547
- const processError = (error41, path55 = []) => {
11547
+ const processError = (error41, path63 = []) => {
11548
11548
  var _a2, _b;
11549
11549
  for (const issue2 of error41.issues) {
11550
11550
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -11554,7 +11554,7 @@ function treeifyError(error40, _mapper) {
11554
11554
  } else if (issue2.code === "invalid_element") {
11555
11555
  processError({ issues: issue2.issues }, issue2.path);
11556
11556
  } else {
11557
- const fullpath = [...path55, ...issue2.path];
11557
+ const fullpath = [...path63, ...issue2.path];
11558
11558
  if (fullpath.length === 0) {
11559
11559
  result.errors.push(mapper(issue2));
11560
11560
  continue;
@@ -11584,9 +11584,9 @@ function treeifyError(error40, _mapper) {
11584
11584
  processError(error40);
11585
11585
  return result;
11586
11586
  }
11587
- function toDotPath(path55) {
11587
+ function toDotPath(path63) {
11588
11588
  const segs = [];
11589
- for (const seg of path55) {
11589
+ for (const seg of path63) {
11590
11590
  if (typeof seg === "number")
11591
11591
  segs.push(`[${seg}]`);
11592
11592
  else if (typeof seg === "symbol")
@@ -24049,8 +24049,8 @@ var init_acp = __esm({
24049
24049
  this.#requestHandler = requestHandler;
24050
24050
  this.#notificationHandler = notificationHandler;
24051
24051
  this.#stream = stream;
24052
- this.#closedPromise = new Promise((resolve28) => {
24053
- this.#abortController.signal.addEventListener("abort", () => resolve28());
24052
+ this.#closedPromise = new Promise((resolve35) => {
24053
+ this.#abortController.signal.addEventListener("abort", () => resolve35());
24054
24054
  });
24055
24055
  this.#receive();
24056
24056
  }
@@ -24199,8 +24199,8 @@ var init_acp = __esm({
24199
24199
  }
24200
24200
  async sendRequest(method, params) {
24201
24201
  const id = this.#nextRequestId++;
24202
- const responsePromise = new Promise((resolve28, reject) => {
24203
- this.#pendingResponses.set(id, { resolve: resolve28, reject });
24202
+ const responsePromise = new Promise((resolve35, reject) => {
24203
+ this.#pendingResponses.set(id, { resolve: resolve35, reject });
24204
24204
  });
24205
24205
  await this.#sendMessage({ jsonrpc: "2.0", id, method, params });
24206
24206
  return responsePromise;
@@ -25069,10 +25069,10 @@ var require_src2 = __commonJS({
25069
25069
  var fs_1 = __require("fs");
25070
25070
  var debug_1 = __importDefault(require_src());
25071
25071
  var log2 = debug_1.default("@kwsites/file-exists");
25072
- function check2(path55, isFile, isDirectory) {
25073
- log2(`checking %s`, path55);
25072
+ function check2(path63, isFile, isDirectory) {
25073
+ log2(`checking %s`, path63);
25074
25074
  try {
25075
- const stat3 = fs_1.statSync(path55);
25075
+ const stat3 = fs_1.statSync(path63);
25076
25076
  if (stat3.isFile() && isFile) {
25077
25077
  log2(`[OK] path represents a file`);
25078
25078
  return true;
@@ -25092,8 +25092,8 @@ var require_src2 = __commonJS({
25092
25092
  throw e;
25093
25093
  }
25094
25094
  }
25095
- function exists2(path55, type = exports.READABLE) {
25096
- return check2(path55, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
25095
+ function exists2(path63, type = exports.READABLE) {
25096
+ return check2(path63, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
25097
25097
  }
25098
25098
  exports.exists = exists2;
25099
25099
  exports.FILE = 1;
@@ -25174,15 +25174,15 @@ var {
25174
25174
  } = import_index.default;
25175
25175
 
25176
25176
  // src/cli-version.ts
25177
- var CLI_VERSION = "0.1.50".length > 0 ? "0.1.50" : "0.0.0-dev";
25177
+ var CLI_VERSION = "0.1.52".length > 0 ? "0.1.52" : "0.0.0-dev";
25178
25178
 
25179
25179
  // src/cli/defaults.ts
25180
25180
  var DEFAULT_API_URL = process.env.BUILDAUTOMATON_API_URL ?? "https://api.buildautomaton.com";
25181
25181
  var DEFAULT_FIREHOSE_URL = "https://buildautomaton-firehose.fly.dev";
25182
25182
 
25183
25183
  // src/cli/run-cli-action.ts
25184
- import * as fs45 from "node:fs";
25185
- import * as path54 from "node:path";
25184
+ import * as fs46 from "node:fs";
25185
+ import * as path62 from "node:path";
25186
25186
 
25187
25187
  // src/cli-log-level.ts
25188
25188
  var verbosity = "info";
@@ -26297,14 +26297,14 @@ var baseOpen = async (options) => {
26297
26297
  }
26298
26298
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
26299
26299
  if (options.wait) {
26300
- return new Promise((resolve28, reject) => {
26300
+ return new Promise((resolve35, reject) => {
26301
26301
  subprocess.once("error", reject);
26302
26302
  subprocess.once("close", (exitCode) => {
26303
26303
  if (!options.allowNonzeroExitCode && exitCode > 0) {
26304
26304
  reject(new Error(`Exited with code ${exitCode}`));
26305
26305
  return;
26306
26306
  }
26307
- resolve28(subprocess);
26307
+ resolve35(subprocess);
26308
26308
  });
26309
26309
  });
26310
26310
  }
@@ -26848,8 +26848,8 @@ function runPendingAuth(options) {
26848
26848
  let hasOpenedBrowser = false;
26849
26849
  let resolved = false;
26850
26850
  let resolveAuth;
26851
- const authPromise = new Promise((resolve28) => {
26852
- resolveAuth = resolve28;
26851
+ const authPromise = new Promise((resolve35) => {
26852
+ resolveAuth = resolve35;
26853
26853
  });
26854
26854
  let reconnectAttempt = 0;
26855
26855
  const signInQuiet = createEmptyReconnectQuietSlot();
@@ -27000,7 +27000,7 @@ import sqliteWasm from "node-sqlite3-wasm";
27000
27000
 
27001
27001
  // src/runtime/yield-to-event-loop.ts
27002
27002
  function yieldToEventLoop() {
27003
- return new Promise((resolve28) => setImmediate(resolve28));
27003
+ return new Promise((resolve35) => setImmediate(resolve35));
27004
27004
  }
27005
27005
 
27006
27006
  // src/sqlite/cli-sqlite-paths.ts
@@ -28117,9 +28117,9 @@ function parseChangeSummaryJson(raw, allowedPaths, options) {
28117
28117
  const rawPath = typeof o.path === "string" ? o.path.trim() : "";
28118
28118
  const summary = typeof o.summary === "string" ? o.summary.trim() : "";
28119
28119
  if (!rawPath || !summary) continue;
28120
- const path55 = skip ? normalizeRepoRelativePath(rawPath) || rawPath : resolveChangeSummaryPathAgainstAllowed(rawPath, allowedPaths);
28121
- if (!path55) continue;
28122
- rows.push({ path: path55, summary: clampSummaryToAtMostTwoLines(summary) });
28120
+ const path63 = skip ? normalizeRepoRelativePath(rawPath) || rawPath : resolveChangeSummaryPathAgainstAllowed(rawPath, allowedPaths);
28121
+ if (!path63) continue;
28122
+ rows.push({ path: path63, summary: clampSummaryToAtMostTwoLines(summary) });
28123
28123
  }
28124
28124
  return rows;
28125
28125
  }
@@ -28631,10 +28631,10 @@ async function killChildProcessTreeGracefully(child, graceMs = ACP_PROCESS_TREE_
28631
28631
  }
28632
28632
  await killProcessTree(pid, "SIGTERM");
28633
28633
  if (graceMs <= 0) return;
28634
- const exited = new Promise((resolve28) => {
28635
- child.once("exit", () => resolve28());
28634
+ const exited = new Promise((resolve35) => {
28635
+ child.once("exit", () => resolve35());
28636
28636
  });
28637
- await Promise.race([exited, new Promise((resolve28) => setTimeout(resolve28, graceMs))]);
28637
+ await Promise.race([exited, new Promise((resolve35) => setTimeout(resolve35, graceMs))]);
28638
28638
  if (child.exitCode == null && child.signalCode == null) {
28639
28639
  await killProcessTree(pid, "SIGKILL");
28640
28640
  }
@@ -28651,8 +28651,8 @@ async function gracefulAcpSubprocessDisconnect(params) {
28651
28651
  await transport.closeSession?.(sessionId);
28652
28652
  } catch {
28653
28653
  }
28654
- await new Promise((resolve28) => {
28655
- const timer = setTimeout(resolve28, ACP_GRACEFUL_SESSION_CLOSE_WAIT_MS);
28654
+ await new Promise((resolve35) => {
28655
+ const timer = setTimeout(resolve35, ACP_GRACEFUL_SESSION_CLOSE_WAIT_MS);
28656
28656
  timer.unref?.();
28657
28657
  });
28658
28658
  }
@@ -28927,11 +28927,11 @@ function createSdkStdioExtNotificationHandler(options) {
28927
28927
  // src/agents/acp/clients/sdk/sdk-stdio-permission-request-handshake.ts
28928
28928
  function awaitSdkStdioPermissionRequestHandshake(params) {
28929
28929
  const { requestId, paramsRecord, pending, onRequest } = params;
28930
- return new Promise((resolve28) => {
28931
- pending.set(requestId, { resolve: resolve28, params: paramsRecord });
28930
+ return new Promise((resolve35) => {
28931
+ pending.set(requestId, { resolve: resolve35, params: paramsRecord });
28932
28932
  if (onRequest == null) {
28933
28933
  pending.delete(requestId);
28934
- resolve28({ outcome: { outcome: "denied" } });
28934
+ resolve35({ outcome: { outcome: "denied" } });
28935
28935
  return;
28936
28936
  }
28937
28937
  try {
@@ -29441,10 +29441,10 @@ function createSdkStdioInitSettle(child) {
29441
29441
  forceAcpSubprocessDisconnect(child);
29442
29442
  reject(err);
29443
29443
  },
29444
- settleResolve(resolve28, handle) {
29444
+ settleResolve(resolve35, handle) {
29445
29445
  if (initSettled) return;
29446
29446
  initSettled = true;
29447
- resolve28(handle);
29447
+ resolve35(handle);
29448
29448
  }
29449
29449
  };
29450
29450
  }
@@ -29513,7 +29513,7 @@ async function createSdkStdioAcpClient(options) {
29513
29513
  onFileChange,
29514
29514
  stderrCapture
29515
29515
  });
29516
- return new Promise((resolve28, reject) => {
29516
+ return new Promise((resolve35, reject) => {
29517
29517
  const init = createSdkStdioInitSettle(child);
29518
29518
  child.on("error", (err) => {
29519
29519
  init.settleReject(reject, new Error(formatSpawnError(err, command[0])));
@@ -29538,7 +29538,7 @@ async function createSdkStdioAcpClient(options) {
29538
29538
  protocolVersion: PROTOCOL_VERSION2
29539
29539
  });
29540
29540
  init.settleResolve(
29541
- resolve28,
29541
+ resolve35,
29542
29542
  createSdkStdioHandle({
29543
29543
  child,
29544
29544
  sessionId: established.sessionId,
@@ -30071,7 +30071,7 @@ async function createCursorAcpClient(options) {
30071
30071
  onFileChange,
30072
30072
  stderrCapture
30073
30073
  });
30074
- return new Promise((resolve28, reject) => {
30074
+ return new Promise((resolve35, reject) => {
30075
30075
  child.on("error", (err) => {
30076
30076
  killChildProcessTree(child, "SIGKILL");
30077
30077
  reject(new Error(formatSpawnError(err, command[0])));
@@ -30083,7 +30083,7 @@ async function createCursorAcpClient(options) {
30083
30083
  sessionCtx,
30084
30084
  incomingDeps: { dbgFs, sessionCtx, onSessionUpdate, onRequest }
30085
30085
  });
30086
- resolve28(
30086
+ resolve35(
30087
30087
  createCursorAcpHandle({
30088
30088
  child,
30089
30089
  sessionId: established.sessionId,
@@ -31289,8 +31289,8 @@ function pathspec(...paths) {
31289
31289
  cache.set(key, paths);
31290
31290
  return key;
31291
31291
  }
31292
- function isPathSpec(path55) {
31293
- return path55 instanceof String && cache.has(path55);
31292
+ function isPathSpec(path63) {
31293
+ return path63 instanceof String && cache.has(path63);
31294
31294
  }
31295
31295
  function toPaths(pathSpec) {
31296
31296
  return cache.get(pathSpec) || [];
@@ -31379,8 +31379,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
31379
31379
  function forEachLineWithContent(input, callback) {
31380
31380
  return toLinesWithContent(input, true).map((line) => callback(line));
31381
31381
  }
31382
- function folderExists(path55) {
31383
- return (0, import_file_exists.exists)(path55, import_file_exists.FOLDER);
31382
+ function folderExists(path63) {
31383
+ return (0, import_file_exists.exists)(path63, import_file_exists.FOLDER);
31384
31384
  }
31385
31385
  function append(target, item) {
31386
31386
  if (Array.isArray(target)) {
@@ -31784,8 +31784,8 @@ function checkIsRepoRootTask() {
31784
31784
  commands,
31785
31785
  format: "utf-8",
31786
31786
  onError,
31787
- parser(path55) {
31788
- return /^\.(git)?$/.test(path55.trim());
31787
+ parser(path63) {
31788
+ return /^\.(git)?$/.test(path63.trim());
31789
31789
  }
31790
31790
  };
31791
31791
  }
@@ -32219,11 +32219,11 @@ function parseGrep(grep) {
32219
32219
  const paths = /* @__PURE__ */ new Set();
32220
32220
  const results = {};
32221
32221
  forEachLineWithContent(grep, (input) => {
32222
- const [path55, line, preview] = input.split(NULL);
32223
- paths.add(path55);
32224
- (results[path55] = results[path55] || []).push({
32222
+ const [path63, line, preview] = input.split(NULL);
32223
+ paths.add(path63);
32224
+ (results[path63] = results[path63] || []).push({
32225
32225
  line: asNumber(line),
32226
- path: path55,
32226
+ path: path63,
32227
32227
  preview
32228
32228
  });
32229
32229
  });
@@ -32988,14 +32988,14 @@ var init_hash_object = __esm2({
32988
32988
  init_task();
32989
32989
  }
32990
32990
  });
32991
- function parseInit(bare, path55, text) {
32991
+ function parseInit(bare, path63, text) {
32992
32992
  const response = String(text).trim();
32993
32993
  let result;
32994
32994
  if (result = initResponseRegex.exec(response)) {
32995
- return new InitSummary(bare, path55, false, result[1]);
32995
+ return new InitSummary(bare, path63, false, result[1]);
32996
32996
  }
32997
32997
  if (result = reInitResponseRegex.exec(response)) {
32998
- return new InitSummary(bare, path55, true, result[1]);
32998
+ return new InitSummary(bare, path63, true, result[1]);
32999
32999
  }
33000
33000
  let gitDir = "";
33001
33001
  const tokens = response.split(" ");
@@ -33006,7 +33006,7 @@ function parseInit(bare, path55, text) {
33006
33006
  break;
33007
33007
  }
33008
33008
  }
33009
- return new InitSummary(bare, path55, /^re/i.test(response), gitDir);
33009
+ return new InitSummary(bare, path63, /^re/i.test(response), gitDir);
33010
33010
  }
33011
33011
  var InitSummary;
33012
33012
  var initResponseRegex;
@@ -33015,9 +33015,9 @@ var init_InitSummary = __esm2({
33015
33015
  "src/lib/responses/InitSummary.ts"() {
33016
33016
  "use strict";
33017
33017
  InitSummary = class {
33018
- constructor(bare, path55, existing, gitDir) {
33018
+ constructor(bare, path63, existing, gitDir) {
33019
33019
  this.bare = bare;
33020
- this.path = path55;
33020
+ this.path = path63;
33021
33021
  this.existing = existing;
33022
33022
  this.gitDir = gitDir;
33023
33023
  }
@@ -33029,7 +33029,7 @@ var init_InitSummary = __esm2({
33029
33029
  function hasBareCommand(command) {
33030
33030
  return command.includes(bareCommand);
33031
33031
  }
33032
- function initTask(bare = false, path55, customArgs) {
33032
+ function initTask(bare = false, path63, customArgs) {
33033
33033
  const commands = ["init", ...customArgs];
33034
33034
  if (bare && !hasBareCommand(commands)) {
33035
33035
  commands.splice(1, 0, bareCommand);
@@ -33038,7 +33038,7 @@ function initTask(bare = false, path55, customArgs) {
33038
33038
  commands,
33039
33039
  format: "utf-8",
33040
33040
  parser(text) {
33041
- return parseInit(commands.includes("--bare"), path55, text);
33041
+ return parseInit(commands.includes("--bare"), path63, text);
33042
33042
  }
33043
33043
  };
33044
33044
  }
@@ -33854,12 +33854,12 @@ var init_FileStatusSummary = __esm2({
33854
33854
  "use strict";
33855
33855
  fromPathRegex = /^(.+)\0(.+)$/;
33856
33856
  FileStatusSummary = class {
33857
- constructor(path55, index, working_dir) {
33858
- this.path = path55;
33857
+ constructor(path63, index, working_dir) {
33858
+ this.path = path63;
33859
33859
  this.index = index;
33860
33860
  this.working_dir = working_dir;
33861
33861
  if (index === "R" || working_dir === "R") {
33862
- const detail = fromPathRegex.exec(path55) || [null, path55, path55];
33862
+ const detail = fromPathRegex.exec(path63) || [null, path63, path63];
33863
33863
  this.from = detail[2] || "";
33864
33864
  this.path = detail[1] || "";
33865
33865
  }
@@ -33890,14 +33890,14 @@ function splitLine(result, lineStr) {
33890
33890
  default:
33891
33891
  return;
33892
33892
  }
33893
- function data(index, workingDir, path55) {
33893
+ function data(index, workingDir, path63) {
33894
33894
  const raw = `${index}${workingDir}`;
33895
33895
  const handler = parsers6.get(raw);
33896
33896
  if (handler) {
33897
- handler(result, path55);
33897
+ handler(result, path63);
33898
33898
  }
33899
33899
  if (raw !== "##" && raw !== "!!") {
33900
- result.files.push(new FileStatusSummary(path55, index, workingDir));
33900
+ result.files.push(new FileStatusSummary(path63, index, workingDir));
33901
33901
  }
33902
33902
  }
33903
33903
  }
@@ -34206,9 +34206,9 @@ var init_simple_git_api = __esm2({
34206
34206
  next
34207
34207
  );
34208
34208
  }
34209
- hashObject(path55, write) {
34209
+ hashObject(path63, write) {
34210
34210
  return this._runTask(
34211
- hashObjectTask(path55, write === true),
34211
+ hashObjectTask(path63, write === true),
34212
34212
  trailingFunctionArgument(arguments)
34213
34213
  );
34214
34214
  }
@@ -34561,8 +34561,8 @@ var init_branch = __esm2({
34561
34561
  }
34562
34562
  });
34563
34563
  function toPath(input) {
34564
- const path55 = input.trim().replace(/^["']|["']$/g, "");
34565
- return path55 && normalize3(path55);
34564
+ const path63 = input.trim().replace(/^["']|["']$/g, "");
34565
+ return path63 && normalize3(path63);
34566
34566
  }
34567
34567
  var parseCheckIgnore;
34568
34568
  var init_CheckIgnore = __esm2({
@@ -34876,8 +34876,8 @@ __export2(sub_module_exports, {
34876
34876
  subModuleTask: () => subModuleTask,
34877
34877
  updateSubModuleTask: () => updateSubModuleTask
34878
34878
  });
34879
- function addSubModuleTask(repo, path55) {
34880
- return subModuleTask(["add", repo, path55]);
34879
+ function addSubModuleTask(repo, path63) {
34880
+ return subModuleTask(["add", repo, path63]);
34881
34881
  }
34882
34882
  function initSubModuleTask(customArgs) {
34883
34883
  return subModuleTask(["init", ...customArgs]);
@@ -35210,8 +35210,8 @@ var require_git = __commonJS2({
35210
35210
  }
35211
35211
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
35212
35212
  };
35213
- Git2.prototype.submoduleAdd = function(repo, path55, then) {
35214
- return this._runTask(addSubModuleTask2(repo, path55), trailingFunctionArgument2(arguments));
35213
+ Git2.prototype.submoduleAdd = function(repo, path63, then) {
35214
+ return this._runTask(addSubModuleTask2(repo, path63), trailingFunctionArgument2(arguments));
35215
35215
  };
35216
35216
  Git2.prototype.submoduleUpdate = function(args, then) {
35217
35217
  return this._runTask(
@@ -35848,8 +35848,8 @@ async function runGitTask(fn) {
35848
35848
  }
35849
35849
  async function yieldToEventLoop2() {
35850
35850
  throwIfGitShutdownRequested();
35851
- await new Promise((resolve28) => {
35852
- setImmediate(resolve28);
35851
+ await new Promise((resolve35) => {
35852
+ setImmediate(resolve35);
35853
35853
  });
35854
35854
  throwIfGitShutdownRequested();
35855
35855
  }
@@ -35889,11 +35889,8 @@ function cliSimpleGit(baseDir) {
35889
35889
  async function getRemoteOriginUrl(gitDir) {
35890
35890
  try {
35891
35891
  const git = cliSimpleGit(gitDir);
35892
- const remotes = await git.getRemotes(true);
35893
- const list = Array.isArray(remotes) ? remotes : [];
35894
- const origin = list.find((r) => r.name === "origin");
35895
- if (!origin?.refs?.fetch && !origin?.refs?.push) return "";
35896
- return (origin.refs.fetch ?? origin.refs.push ?? "").trim();
35892
+ const out = await git.raw(["config", "--get", "remote.origin.url"]);
35893
+ return String(out).trim();
35897
35894
  } catch {
35898
35895
  return "";
35899
35896
  }
@@ -35909,6 +35906,12 @@ async function isGitRepoDirectory(dirPath) {
35909
35906
  }
35910
35907
 
35911
35908
  // src/git/discover-repos.ts
35909
+ var GIT_DISCOVER_YIELD_EVERY = 32;
35910
+ async function yieldGitDiscoverWork(entryCount) {
35911
+ if (entryCount > 0 && entryCount % GIT_DISCOVER_YIELD_EVERY === 0) {
35912
+ await yieldToEventLoop();
35913
+ }
35914
+ }
35912
35915
  async function discoverGitRepos(cwd = getBridgeRoot()) {
35913
35916
  const result = [];
35914
35917
  const cwdResolved = path17.resolve(cwd);
@@ -35922,12 +35925,14 @@ async function discoverGitRepos(cwd = getBridgeRoot()) {
35922
35925
  } catch {
35923
35926
  return result;
35924
35927
  }
35925
- for (const ent of entries) {
35928
+ for (let i = 0; i < entries.length; i++) {
35929
+ await yieldGitDiscoverWork(i + 1);
35930
+ const ent = entries[i];
35926
35931
  if (!ent.isDirectory()) continue;
35927
- const childPath = path17.join(cwdResolved, ent.name);
35928
- if (await isGitRepoDirectory(childPath)) {
35929
- const remoteUrl = await getRemoteOriginUrl(childPath);
35930
- result.push({ absolutePath: childPath, remoteUrl });
35932
+ const childPath2 = path17.join(cwdResolved, ent.name);
35933
+ if (await isGitRepoDirectory(childPath2)) {
35934
+ const remoteUrl = await getRemoteOriginUrl(childPath2);
35935
+ result.push({ absolutePath: childPath2, remoteUrl });
35931
35936
  }
35932
35937
  }
35933
35938
  return result;
@@ -35935,6 +35940,7 @@ async function discoverGitRepos(cwd = getBridgeRoot()) {
35935
35940
  async function discoverGitReposUnderRoot(rootPath) {
35936
35941
  const root = path17.resolve(rootPath);
35937
35942
  const roots = [];
35943
+ let walkEntries = 0;
35938
35944
  async function walk(dir) {
35939
35945
  if (await isGitRepoDirectory(dir)) {
35940
35946
  roots.push(path17.resolve(dir));
@@ -35946,7 +35952,9 @@ async function discoverGitReposUnderRoot(rootPath) {
35946
35952
  } catch {
35947
35953
  return;
35948
35954
  }
35949
- for (const ent of entries) {
35955
+ for (let i = 0; i < entries.length; i++) {
35956
+ await yieldGitDiscoverWork(++walkEntries);
35957
+ const ent = entries[i];
35950
35958
  if (!ent.isDirectory() || ent.name === ".git") continue;
35951
35959
  await walk(path17.join(dir, ent.name));
35952
35960
  }
@@ -35954,7 +35962,9 @@ async function discoverGitReposUnderRoot(rootPath) {
35954
35962
  await walk(root);
35955
35963
  const uniq = [...new Set(roots)];
35956
35964
  const out = [];
35957
- for (const p of uniq) {
35965
+ for (let i = 0; i < uniq.length; i++) {
35966
+ await yieldGitDiscoverWork(i + 1);
35967
+ const p = uniq[i];
35958
35968
  const remoteUrl = await getRemoteOriginUrl(p);
35959
35969
  out.push({ absolutePath: p, remoteUrl });
35960
35970
  }
@@ -36174,9 +36184,9 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
36174
36184
  // src/agents/acp/put-summarize-change-summaries.ts
36175
36185
  async function putEncryptedChangeSummaryRows(params) {
36176
36186
  const base = params.apiBaseUrl.replace(/\/+$/, "");
36177
- const entries = params.rows.map(({ path: path55, summary }) => {
36187
+ const entries = params.rows.map(({ path: path63, summary }) => {
36178
36188
  const enc = params.e2ee.encryptFields({ summary }, ["summary"]);
36179
- return { path: path55, summary: JSON.stringify(enc) };
36189
+ return { path: path63, summary: JSON.stringify(enc) };
36180
36190
  });
36181
36191
  const res = await fetch(
36182
36192
  `${base}/internal/sessions/${encodeURIComponent(params.sessionId)}/follow-ups/summarize-changes`,
@@ -36920,503 +36930,436 @@ function allocateDirNameForLauncherCwd(layout, bridgeRootPath2) {
36920
36930
  return name;
36921
36931
  }
36922
36932
 
36923
- // src/worktrees/discovery/discover-session-worktree-on-disk.ts
36924
- import * as fs21 from "node:fs";
36925
- import * as path25 from "node:path";
36926
-
36927
- // src/worktrees/discovery/collect-worktree-paths.ts
36928
- import * as fs19 from "node:fs";
36929
- import * as path22 from "node:path";
36933
+ // src/worktrees/manager/session-worktree-cache.ts
36934
+ import * as path21 from "node:path";
36935
+ var SessionWorktreeCache = class {
36936
+ sessionRepoCheckoutPaths = /* @__PURE__ */ new Map();
36937
+ sessionParentPathBySession = /* @__PURE__ */ new Map();
36938
+ sessionWorkingTreeRelRootBySession = /* @__PURE__ */ new Map();
36939
+ remember(sessionId, binding) {
36940
+ const paths = binding.repoCheckoutPaths.map((p) => path21.resolve(p));
36941
+ this.sessionRepoCheckoutPaths.set(sessionId, paths);
36942
+ this.sessionParentPathBySession.set(sessionId, path21.resolve(binding.sessionParentPath));
36943
+ this.sessionWorkingTreeRelRootBySession.set(sessionId, path21.resolve(binding.workingTreeRelRoot));
36944
+ }
36945
+ clearSession(sessionId) {
36946
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
36947
+ this.sessionRepoCheckoutPaths.delete(sessionId);
36948
+ this.sessionParentPathBySession.delete(sessionId);
36949
+ this.sessionWorkingTreeRelRootBySession.delete(sessionId);
36950
+ return paths;
36951
+ }
36952
+ getSessionParentPath(sessionId) {
36953
+ return this.sessionParentPathBySession.get(sessionId);
36954
+ }
36955
+ getWorkingTreeRelRoot(sessionId) {
36956
+ return this.sessionWorkingTreeRelRootBySession.get(sessionId) ?? null;
36957
+ }
36958
+ hasSession(sessionId) {
36959
+ return this.sessionParentPathBySession.has(sessionId);
36960
+ }
36961
+ getRepoCheckoutPaths(sessionId) {
36962
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
36963
+ return paths?.length ? [...paths] : void 0;
36964
+ }
36965
+ getRepoCheckoutPathsRef(sessionId) {
36966
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
36967
+ return paths?.length ? paths : void 0;
36968
+ }
36969
+ isLegacyNestedLayout(sessionId) {
36970
+ const parent = this.sessionParentPathBySession.get(sessionId);
36971
+ const relRoot = this.sessionWorkingTreeRelRootBySession.get(sessionId);
36972
+ if (!parent || !relRoot) return false;
36973
+ return path21.resolve(parent) !== path21.resolve(relRoot);
36974
+ }
36975
+ };
36930
36976
 
36931
- // src/worktrees/discovery/disk-walk-constants.ts
36932
- var DISK_WALK_YIELD_EVERY = 64;
36933
- var LEGACY_WALK_MAX_DEPTH = 12;
36934
- var SKIP_DISK_WALK_DIR_NAMES = /* @__PURE__ */ new Set([
36935
- "node_modules",
36936
- "bower_components",
36937
- "vendor",
36938
- "Pods",
36939
- "Carthage",
36940
- "DerivedData",
36941
- ".yarn",
36942
- ".pnpm-store",
36943
- "dist",
36944
- "build",
36945
- ".next",
36946
- "coverage",
36947
- ".turbo",
36948
- ".cache",
36949
- "target",
36950
- "__pycache__"
36951
- ]);
36952
- function shouldSkipDiskWalkEntry(name) {
36953
- return name.startsWith(".") || SKIP_DISK_WALK_DIR_NAMES.has(name);
36977
+ // src/worktrees/manager/session-worktree-manager-context.ts
36978
+ function createSessionWorktreeManagerContext(options) {
36979
+ return {
36980
+ worktreesRootPath: options.worktreesRootPath,
36981
+ log: options.log,
36982
+ cache: new SessionWorktreeCache(),
36983
+ layout: loadWorktreeLayout()
36984
+ };
36954
36985
  }
36955
36986
 
36956
- // src/worktrees/discovery/disk-walk-utils.ts
36957
- import * as fs18 from "node:fs";
36958
- import * as path21 from "node:path";
36959
- async function yieldDuringDiskWalk(state) {
36960
- state.entries++;
36961
- if (state.entries % DISK_WALK_YIELD_EVERY !== 0) return true;
36962
- if (isCliImmediateShutdownRequested()) return false;
36963
- await yieldToEventLoop();
36964
- return !isCliImmediateShutdownRequested();
36965
- }
36966
- async function isGitDir(dirPath) {
36967
- try {
36968
- await fs18.promises.access(path21.join(dirPath, ".git"));
36969
- return true;
36970
- } catch {
36971
- return false;
36987
+ // src/git/branches/commit-and-push.ts
36988
+ async function gitCommitAllIfDirty(repoDir, message, options) {
36989
+ const g = cliSimpleGit(repoDir);
36990
+ const st = await g.status();
36991
+ if (!st.files?.length) return;
36992
+ const branch = options.branch.trim();
36993
+ if (!branch) {
36994
+ throw new Error("Branch name is required");
36995
+ }
36996
+ const branches = await g.branchLocal();
36997
+ const localNames = new Set(branches.all.map((b) => b.replace(/^\*\s*/, "").trim()));
36998
+ if (!localNames.has(branch)) {
36999
+ await g.checkoutLocalBranch(branch);
37000
+ } else {
37001
+ await g.checkout(branch);
37002
+ }
37003
+ await g.add(".");
37004
+ await g.commit(message);
37005
+ if (options.push) {
37006
+ await g.push(["-u", "origin", branch]);
36972
37007
  }
36973
37008
  }
36974
37009
 
36975
- // src/worktrees/discovery/collect-worktree-paths.ts
36976
- async function collectGitRepoRootsUnderDirectory(rootPath) {
36977
- const out = [];
36978
- const state = { entries: 0 };
36979
- const walk = async (dir) => {
36980
- if (!await yieldDuringDiskWalk(state)) return;
36981
- if (await isGitDir(dir)) {
36982
- out.push(path22.resolve(dir));
36983
- return;
36984
- }
36985
- let entries;
36986
- try {
36987
- entries = await fs19.promises.readdir(dir, { withFileTypes: true });
36988
- } catch {
36989
- return;
36990
- }
36991
- for (const e of entries) {
36992
- if (shouldSkipDiskWalkEntry(e.name)) continue;
36993
- const full = path22.join(dir, e.name);
36994
- if (!e.isDirectory()) continue;
36995
- await walk(full);
37010
+ // src/worktrees/commit-session-worktrees.ts
37011
+ async function commitSessionWorktrees(options) {
37012
+ const { paths, branch, message, push } = options;
37013
+ try {
37014
+ for (const wt of paths) {
37015
+ await gitCommitAllIfDirty(wt, message, { push, branch });
36996
37016
  }
36997
- };
36998
- await walk(path22.resolve(rootPath));
36999
- return { paths: [...new Set(out)], entriesVisited: state.entries };
37017
+ return { ok: true };
37018
+ } catch (e) {
37019
+ const err = e instanceof Error ? e.message : String(e);
37020
+ return { ok: false, error: err };
37021
+ }
37000
37022
  }
37001
- async function collectWorktreeRootsNamed(root, sessionId, maxDepth = LEGACY_WALK_MAX_DEPTH) {
37002
- const out = [];
37003
- const state = { entries: 0 };
37004
- const walk = async (dir, depth) => {
37005
- if (depth > maxDepth) return;
37006
- if (!await yieldDuringDiskWalk(state)) return;
37007
- let entries;
37008
- try {
37009
- entries = await fs19.promises.readdir(dir, { withFileTypes: true });
37010
- } catch {
37011
- return;
37012
- }
37013
- for (const e of entries) {
37014
- if (shouldSkipDiskWalkEntry(e.name)) continue;
37015
- const full = path22.join(dir, e.name);
37016
- if (!e.isDirectory()) continue;
37017
- if (e.name === sessionId) {
37018
- if (await isGitDir(full)) out.push(path22.resolve(full));
37019
- continue;
37020
- }
37021
- if (await isGitDir(full)) continue;
37022
- await walk(full, depth + 1);
37023
- }
37024
- };
37025
- await walk(root, 0);
37026
- return { paths: out, entriesVisited: state.entries };
37023
+
37024
+ // src/worktrees/manager/git/commit-session-worktree.ts
37025
+ async function commitSessionWorktree(cache2, params) {
37026
+ const paths = cache2.getRepoCheckoutPathsRef(params.sessionId);
37027
+ const targets = paths?.length ? paths : [getBridgeRoot()];
37028
+ return commitSessionWorktrees({
37029
+ paths: targets,
37030
+ branch: params.branch,
37031
+ message: params.message,
37032
+ push: params.push
37033
+ });
37027
37034
  }
37028
37035
 
37029
- // src/worktrees/discovery/layout-keys-for-bridge-root.ts
37030
- import * as path23 from "node:path";
37031
- function layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk) {
37032
- const bridge = path23.resolve(bridgeRoot);
37033
- const preferred = getLauncherDirNameIfPresent(layout, bridgeRoot);
37034
- const relevant = layout.launcherCwds.filter((e) => {
37035
- const entry = path23.resolve(e.absolutePath);
37036
- return bridge === entry || bridge.startsWith(entry + path23.sep) || entry.startsWith(bridge + path23.sep);
37037
- }).sort((a, b) => path23.resolve(b.absolutePath).length - path23.resolve(a.absolutePath).length).map((e) => e.dirName).filter((name) => keysOnDisk.includes(name));
37038
- const ordered = [];
37039
- const seen = /* @__PURE__ */ new Set();
37040
- const add = (k) => {
37041
- if (seen.has(k) || !keysOnDisk.includes(k)) return;
37042
- seen.add(k);
37043
- ordered.push(k);
37044
- };
37045
- if (preferred) add(preferred);
37046
- for (const k of relevant) add(k);
37047
- if (relevant.length > 0) {
37048
- return ordered;
37049
- }
37050
- for (const k of keysOnDisk) add(k);
37051
- return ordered;
37036
+ // src/git/changes/parse/normalize-git-diff-path.ts
37037
+ function normalizeGitDiffPath(path63) {
37038
+ return path63.replace(/\\/g, "/").replace(/\/ +/g, "/").trim();
37052
37039
  }
37053
37040
 
37054
- // src/worktrees/discovery/try-binding-from-session-directory.ts
37055
- import * as fs20 from "node:fs";
37056
- import * as path24 from "node:path";
37057
- async function tryBindingFromSessionDirectory(sessionDir) {
37058
- let st;
37059
- try {
37060
- st = await fs20.promises.stat(sessionDir);
37061
- } catch {
37041
+ // src/git/changes/parse/expand-git-rename-numstat-path.ts
37042
+ function expandGitRenameNumstatPath(rawPath) {
37043
+ const open2 = rawPath.indexOf("{");
37044
+ const arrow = rawPath.indexOf("=>");
37045
+ const close = rawPath.lastIndexOf("}");
37046
+ if (open2 === -1 || arrow === -1 || close === -1 || open2 > arrow || arrow > close) {
37062
37047
  return null;
37063
37048
  }
37064
- if (!st.isDirectory()) return null;
37065
- const { paths: worktreePaths } = await collectGitRepoRootsUnderDirectory(sessionDir);
37066
- if (worktreePaths.length === 0) return null;
37067
- const abs = path24.resolve(sessionDir);
37049
+ const prefix = rawPath.slice(0, open2);
37050
+ const suffix = rawPath.slice(close + 1);
37051
+ const oldPart = rawPath.slice(open2 + 1, arrow).trim();
37052
+ const newPart = rawPath.slice(arrow + 2, close).trim();
37068
37053
  return {
37069
- sessionParentPath: abs,
37070
- workingTreeRelRoot: abs,
37071
- repoCheckoutPaths: worktreePaths
37054
+ oldPath: normalizeGitDiffPath(`${prefix}${oldPart}${suffix}`),
37055
+ newPath: normalizeGitDiffPath(`${prefix}${newPart}${suffix}`)
37072
37056
  };
37073
37057
  }
37058
+ function isGitRenameNumstatPath(rawPath) {
37059
+ return expandGitRenameNumstatPath(rawPath) != null;
37060
+ }
37074
37061
 
37075
- // src/worktrees/discovery/discover-session-worktree-on-disk.ts
37076
- async function discoverSessionWorktreeOnDisk(options) {
37077
- const { sessionId, worktreesRootPath, layout, bridgeRoot } = options;
37078
- const sid = sessionId.trim();
37079
- if (!sid) return null;
37080
- try {
37081
- await fs21.promises.access(worktreesRootPath);
37082
- } catch {
37083
- return null;
37084
- }
37085
- const preferredKey = getLauncherDirNameIfPresent(layout, bridgeRoot);
37086
- const keysOnDisk = [];
37087
- if (preferredKey) keysOnDisk.push(preferredKey);
37088
- try {
37089
- for (const name of await fs21.promises.readdir(worktreesRootPath)) {
37090
- if (name.startsWith(".")) continue;
37091
- const p = path25.join(worktreesRootPath, name);
37092
- let st;
37093
- try {
37094
- st = await fs21.promises.stat(p);
37095
- } catch {
37096
- continue;
37062
+ // src/git/changes/parse/parse-name-status-entries.ts
37063
+ function parseNameStatusEntries(lines) {
37064
+ const entries = [];
37065
+ for (const line of lines) {
37066
+ if (!line.trim()) continue;
37067
+ const tabParts = line.split(" ");
37068
+ if (tabParts.length < 2) continue;
37069
+ const status = tabParts[0].trim();
37070
+ const code = status[0];
37071
+ if (code === "A") {
37072
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "added" });
37073
+ } else if (code === "D") {
37074
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "removed" });
37075
+ } else if (code === "R" || code === "C") {
37076
+ if (tabParts.length >= 3) {
37077
+ entries.push({
37078
+ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]),
37079
+ oldPathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 2]),
37080
+ change: "moved"
37081
+ });
37097
37082
  }
37098
- if (!st.isDirectory()) continue;
37099
- if (!keysOnDisk.includes(name)) keysOnDisk.push(name);
37100
- }
37101
- } catch {
37102
- return null;
37103
- }
37104
- const keys = layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk);
37105
- for (const key of keys) {
37106
- if (isCliImmediateShutdownRequested()) return null;
37107
- await yieldToEventLoop();
37108
- const layoutRoot = path25.join(worktreesRootPath, key);
37109
- let layoutSt;
37110
- try {
37111
- layoutSt = await fs21.promises.stat(layoutRoot);
37112
- } catch {
37113
- continue;
37114
- }
37115
- if (!layoutSt.isDirectory()) continue;
37116
- const sessionDir = path25.join(layoutRoot, sid);
37117
- const nested = await tryBindingFromSessionDirectory(sessionDir);
37118
- if (nested) return nested;
37119
- const legacy = await collectWorktreeRootsNamed(layoutRoot, sid);
37120
- if (legacy.paths.length > 0) {
37121
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacy.paths) ?? path25.resolve(legacy.paths[0]);
37122
- return {
37123
- sessionParentPath: path25.resolve(isolated),
37124
- workingTreeRelRoot: path25.resolve(layoutRoot),
37125
- repoCheckoutPaths: legacy.paths.map((p) => path25.resolve(p))
37126
- };
37083
+ } else if (code === "M" || code === "U" || code === "T") {
37084
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "modified" });
37127
37085
  }
37128
37086
  }
37129
- return null;
37087
+ return entries;
37130
37088
  }
37131
-
37132
- // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
37133
- import * as fs22 from "node:fs";
37134
- import * as path27 from "node:path";
37135
-
37136
- // src/worktrees/discovery/discover-legacy-binding-ascending-from-checkout.ts
37137
- import * as path26 from "node:path";
37138
- async function discoverLegacyBindingAscendingFromCheckout(sessionId, checkoutPath) {
37139
- const sid = sessionId.trim();
37140
- if (!sid) return null;
37141
- const hintR = path26.resolve(checkoutPath);
37142
- let best = null;
37143
- let cur = path26.dirname(hintR);
37144
- for (let i = 0; i < 40; i++) {
37145
- if (isCliImmediateShutdownRequested()) return best;
37146
- await yieldToEventLoop();
37147
- const paths = await collectWorktreeRootsNamed(cur, sid);
37148
- if (paths.paths.some((p) => path26.resolve(p) === hintR)) {
37149
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(paths.paths) ?? path26.resolve(paths.paths[0]);
37150
- best = {
37151
- sessionParentPath: path26.resolve(isolated),
37152
- workingTreeRelRoot: path26.resolve(cur),
37153
- repoCheckoutPaths: paths.paths.map((p) => path26.resolve(p))
37154
- };
37155
- }
37156
- const next = path26.dirname(cur);
37157
- if (next === cur) break;
37158
- cur = next;
37089
+ function parseNameStatusLines(lines) {
37090
+ const m = /* @__PURE__ */ new Map();
37091
+ for (const entry of parseNameStatusEntries(lines)) {
37092
+ m.set(entry.pathInRepo, entry.change);
37159
37093
  }
37160
- return best;
37094
+ return m;
37161
37095
  }
37162
37096
 
37163
- // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
37164
- async function discoverSessionWorktreesUnderSessionWorktreeRoot(sessionWorktreeRootPathOrHint, sessionId) {
37165
- const sid = sessionId.trim();
37166
- if (!sid) return null;
37167
- const hint = path27.resolve(sessionWorktreeRootPathOrHint);
37168
- const underHint = await tryBindingFromSessionDirectory(path27.join(hint, sid));
37169
- if (underHint) return underHint;
37170
- const direct = await tryBindingFromSessionDirectory(hint);
37171
- if (direct) {
37172
- if (path27.basename(hint) === sid && await isGitDir(hint)) {
37173
- const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
37174
- if (legacyFromCheckout && legacyFromCheckout.repoCheckoutPaths.length > direct.repoCheckoutPaths.length) {
37175
- return legacyFromCheckout;
37176
- }
37097
+ // src/git/changes/parse/parse-numstat-entries.ts
37098
+ function parseNumstatCounts(parts) {
37099
+ const [a, d] = parts;
37100
+ return {
37101
+ additions: a === "-" ? 0 : parseInt(String(a), 10) || 0,
37102
+ deletions: d === "-" ? 0 : parseInt(String(d), 10) || 0
37103
+ };
37104
+ }
37105
+ function parseNumstatLine(line) {
37106
+ const parts = line.split(" ");
37107
+ if (parts.length < 3) return null;
37108
+ const rawPath = parts[parts.length - 1];
37109
+ const { additions, deletions } = parseNumstatCounts(parts);
37110
+ const expanded = expandGitRenameNumstatPath(rawPath);
37111
+ const pathInRepo = normalizeGitDiffPath(expanded?.newPath ?? rawPath);
37112
+ return {
37113
+ pathInRepo,
37114
+ additions,
37115
+ deletions,
37116
+ ...expanded ? { oldPathInRepo: expanded.oldPath } : {}
37117
+ };
37118
+ }
37119
+ function parseNumstatEntries(lines) {
37120
+ const m = /* @__PURE__ */ new Map();
37121
+ for (const line of lines) {
37122
+ if (!line.trim()) continue;
37123
+ const parts = line.split(" ");
37124
+ if (parts.length < 3) continue;
37125
+ const rawPath = parts[parts.length - 1];
37126
+ const expanded = expandGitRenameNumstatPath(rawPath);
37127
+ const entry = parseNumstatLine(line);
37128
+ if (!entry) continue;
37129
+ const existing = m.get(entry.pathInRepo);
37130
+ if (!existing || expanded) {
37131
+ m.set(entry.pathInRepo, entry);
37177
37132
  }
37178
- return direct;
37179
37133
  }
37180
- if (path27.basename(hint) === sid && await isGitDir(hint)) {
37181
- const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
37182
- if (legacyFromCheckout) return legacyFromCheckout;
37134
+ return m;
37135
+ }
37136
+ function parseNumstat(lines) {
37137
+ const m = /* @__PURE__ */ new Map();
37138
+ for (const [path63, entry] of parseNumstatEntries(lines)) {
37139
+ m.set(path63, { additions: entry.additions, deletions: entry.deletions });
37183
37140
  }
37184
- let st;
37141
+ return m;
37142
+ }
37143
+
37144
+ // src/git/changes/parse/numstat-from-git-no-index.ts
37145
+ async function numstatFromGitNoIndex(g, pathInRepo) {
37146
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
37185
37147
  try {
37186
- st = await fs22.promises.stat(hint);
37148
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
37149
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
37150
+ const parsed = parseNumstatLine(first2);
37151
+ if (!parsed) return null;
37152
+ return { additions: parsed.additions, deletions: parsed.deletions };
37187
37153
  } catch {
37188
37154
  return null;
37189
37155
  }
37190
- if (!st.isDirectory()) return null;
37191
- const legacyPaths = await collectWorktreeRootsNamed(hint, sid);
37192
- if (legacyPaths.paths.length === 0) return null;
37193
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths.paths) ?? path27.resolve(legacyPaths.paths[0]);
37194
- return {
37195
- sessionParentPath: path27.resolve(isolated),
37196
- workingTreeRelRoot: hint,
37197
- repoCheckoutPaths: legacyPaths.paths.map((p) => path27.resolve(p))
37198
- };
37199
37156
  }
37200
37157
 
37201
- // src/worktrees/manager/discover-session-binding.ts
37202
- var discoverInflightBySessionId = /* @__PURE__ */ new Map();
37203
- function discoverSessionBindingAsync(params) {
37204
- const sid = params.sessionId.trim();
37205
- if (!sid) return Promise.resolve(null);
37206
- const existing = discoverInflightBySessionId.get(sid);
37207
- if (existing) return existing;
37208
- const promise2 = discoverSessionWorktreeOnDisk({
37209
- sessionId: sid,
37210
- worktreesRootPath: params.worktreesRootPath,
37211
- layout: params.layout,
37212
- bridgeRoot: getBridgeRoot()
37213
- }).finally(() => {
37214
- if (discoverInflightBySessionId.get(sid) === promise2) {
37215
- discoverInflightBySessionId.delete(sid);
37216
- }
37217
- });
37218
- discoverInflightBySessionId.set(sid, promise2);
37219
- return promise2;
37158
+ // src/git/changes/status/working-tree-changed-path-count.ts
37159
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
37160
+ const kindByPath = parseNameStatusLines(nameStatusLines);
37161
+ const numByPath = parseNumstat(numstatLines);
37162
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
37163
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
37164
+ paths.add(p);
37165
+ }
37166
+ return paths.size;
37220
37167
  }
37221
37168
 
37222
- // src/worktrees/manager/resolve-isolated-session-parent-path.ts
37223
- function resolveIsolatedSessionParentPath(sessionId, cache2, ensureRepoCheckoutPaths) {
37224
- if (!sessionId) return null;
37225
- const sid = sessionId.trim();
37226
- const cached2 = cache2.getSessionParentPath(sid);
37227
- if (cached2) return cached2;
37228
- const paths = ensureRepoCheckoutPaths(sid);
37229
- if (!paths?.length) return null;
37230
- return resolveIsolatedSessionParentPathFromCheckouts(paths);
37231
- }
37232
- function ensureRepoCheckoutPathsForSession(sessionId, cache2) {
37233
- if (!sessionId?.trim()) return void 0;
37234
- const cached2 = cache2.getRepoCheckoutPaths(sessionId.trim());
37235
- return cached2?.length ? [...cached2] : void 0;
37169
+ // src/git/changes/count-working-tree-changed-files.ts
37170
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
37171
+ return runGitTask(async () => {
37172
+ const g = cliSimpleGit(repoGitCwd);
37173
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
37174
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
37175
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
37176
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
37177
+ ]);
37178
+ return workingTreeChangedPathCount(
37179
+ String(nameStatusRaw).split("\n"),
37180
+ String(numstatRaw).split("\n"),
37181
+ String(untrackedRaw).split("\n")
37182
+ );
37183
+ });
37236
37184
  }
37237
- async function ensureRepoCheckoutPathsForSessionAsync(sessionId, cache2, discover) {
37238
- if (!sessionId?.trim()) return void 0;
37239
- const sid = sessionId.trim();
37240
- const cached2 = cache2.getRepoCheckoutPaths(sid);
37241
- if (cached2?.length) return cached2;
37242
- const disc = await discover(sid);
37243
- if (disc?.repoCheckoutPaths.length) {
37244
- cache2.remember(sid, disc);
37245
- return [...disc.repoCheckoutPaths];
37185
+
37186
+ // src/git/commits/remote-tracking/rev-parse-safe.ts
37187
+ async function revParseSafe(g, ref) {
37188
+ try {
37189
+ const v = (await g.raw(["rev-parse", "--verify", ref])).trim();
37190
+ return v || null;
37191
+ } catch {
37192
+ return null;
37246
37193
  }
37247
- return void 0;
37248
37194
  }
37249
37195
 
37250
- // src/worktrees/manager/parse-session-parent.ts
37251
- function parseSessionParent(v) {
37252
- if (v === "bridge_root" || v === "worktrees_root") return v;
37253
- if (v === "session_worktrees_root") return "worktrees_root";
37254
- return null;
37196
+ // src/git/commits/remote-tracking/git-config.ts
37197
+ async function tryConfigGet(g, key) {
37198
+ try {
37199
+ const out = await g.raw(["config", "--get", key]);
37200
+ const v = String(out).trim();
37201
+ return v || null;
37202
+ } catch {
37203
+ return null;
37204
+ }
37255
37205
  }
37256
37206
 
37257
- // src/worktrees/prepare-new-session-worktrees.ts
37258
- import * as fs23 from "node:fs";
37259
- import * as path28 from "node:path";
37260
-
37261
- // src/git/worktrees/worktree-add.ts
37262
- async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch, baseRef = "HEAD") {
37263
- const mainGit = cliSimpleGit(mainRepoPath);
37264
- const base = baseRef.trim() || "HEAD";
37265
- await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, base]);
37207
+ // src/git/commits/remote-tracking/branch-remote-config.ts
37208
+ async function readBranchRemoteConfig(g) {
37209
+ const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
37210
+ if (!branch || branch === "HEAD") {
37211
+ return { branch: null, remote: "origin", merge: null };
37212
+ }
37213
+ const [remote, merge2] = await Promise.all([
37214
+ tryConfigGet(g, `branch.${branch}.remote`),
37215
+ tryConfigGet(g, `branch.${branch}.merge`)
37216
+ ]);
37217
+ return { branch, remote: remote ?? "origin", merge: merge2 };
37218
+ }
37219
+ function trackingRefCandidates(config2) {
37220
+ const candidates = ["HEAD@{upstream}", "@{u}"];
37221
+ if (!config2.branch) return candidates;
37222
+ if (config2.merge) {
37223
+ candidates.push(
37224
+ `refs/remotes/${config2.remote}/${config2.merge.replace(/^refs\/heads\//, "")}`
37225
+ );
37226
+ }
37227
+ candidates.push(`refs/remotes/${config2.remote}/${config2.branch}`);
37228
+ return candidates;
37266
37229
  }
37267
37230
 
37268
- // src/worktrees/prepare-new-session-worktrees.ts
37269
- function normalizeRepoRelPath(rel) {
37270
- return rel === "" ? "." : rel.replace(/\\/g, "/");
37231
+ // src/git/commits/remote-tracking/first-resolved-ref.ts
37232
+ async function firstResolvedSha(g, candidates) {
37233
+ const resolved = await Promise.all(
37234
+ candidates.map(async (ref) => ({ ref, sha: await revParseSafe(g, ref) }))
37235
+ );
37236
+ return resolved.find((entry) => entry.sha)?.sha ?? null;
37271
37237
  }
37272
- function resolveBaseRefForRepo(relNorm, baseBranches) {
37273
- if (!baseBranches) return "HEAD";
37274
- const direct = baseBranches[relNorm]?.trim();
37275
- if (direct) return direct;
37276
- if (relNorm !== "." && baseBranches["."]?.trim()) return baseBranches["."].trim();
37277
- return "HEAD";
37238
+
37239
+ // src/git/commits/remote-tracking/resolve-default-remote-branch-ref.ts
37240
+ var DEFAULT_REMOTE_BRANCH_NAMES = ["main", "master", "trunk", "develop"];
37241
+ function defaultRemoteBranchNameRefs(remote) {
37242
+ return DEFAULT_REMOTE_BRANCH_NAMES.map((name) => `refs/remotes/${remote}/${name}`);
37278
37243
  }
37279
- async function prepareNewSessionWorktrees(options) {
37280
- const { worktreesRootPath, bridgeRoot, sessionId, layout, log: log2, worktreeBaseBranches } = options;
37281
- const bridgeResolved = path28.resolve(bridgeRoot);
37282
- const cwdKey = allocateDirNameForLauncherCwd(layout, bridgeResolved);
37283
- const bridgeKeyDir = path28.join(worktreesRootPath, cwdKey);
37284
- const sessionDir = path28.join(bridgeKeyDir, sessionId);
37285
- const repos = await discoverGitReposUnderRoot(bridgeResolved);
37286
- if (repos.length === 0) {
37287
- log2("[worktrees] No Git repositories under bridge root; skipping worktree creation.");
37244
+ async function resolveRemoteHeadSymbolicRef(g, remote) {
37245
+ const headSym = `refs/remotes/${remote}/HEAD`;
37246
+ try {
37247
+ const out = await g.raw(["symbolic-ref", "-q", "--verify", headSym]);
37248
+ const resolved = String(out).trim();
37249
+ return resolved.startsWith("refs/remotes/") ? resolved : null;
37250
+ } catch {
37288
37251
  return null;
37289
37252
  }
37290
- const branch = `session-${sessionId}`;
37291
- const worktreePaths = [];
37292
- fs23.mkdirSync(sessionDir, { recursive: true });
37293
- for (const repo of repos) {
37294
- let rel = path28.relative(bridgeResolved, repo.absolutePath);
37295
- if (rel.startsWith("..") || path28.isAbsolute(rel)) continue;
37296
- const relNorm = normalizeRepoRelPath(rel === "" ? "." : rel);
37297
- const wtPath = relNorm === "." ? sessionDir : path28.join(sessionDir, relNorm);
37298
- if (relNorm !== ".") {
37299
- fs23.mkdirSync(path28.dirname(wtPath), { recursive: true });
37300
- }
37301
- const baseRef = resolveBaseRefForRepo(relNorm, worktreeBaseBranches);
37302
- try {
37303
- await gitWorktreeAddBranch(repo.absolutePath, wtPath, branch, baseRef);
37304
- log2(`[worktrees] Added worktree ${wtPath} (branch ${branch}, base ${baseRef}).`);
37305
- worktreePaths.push(wtPath);
37306
- } catch (e) {
37307
- log2(
37308
- `[worktrees] Worktree add failed for ${repo.absolutePath}: ${e instanceof Error ? e.message : String(e)}`
37309
- );
37310
- }
37253
+ }
37254
+ async function resolveDefaultRemoteBranchSha(g, remote) {
37255
+ const nameRefs = defaultRemoteBranchNameRefs(remote);
37256
+ const [symRef, ...nameShas] = await Promise.all([
37257
+ resolveRemoteHeadSymbolicRef(g, remote),
37258
+ ...nameRefs.map((ref) => revParseSafe(g, ref))
37259
+ ]);
37260
+ if (symRef) return revParseSafe(g, symRef);
37261
+ for (let i = 0; i < nameRefs.length; i++) {
37262
+ if (nameShas[i]) return nameShas[i];
37311
37263
  }
37312
- if (worktreePaths.length === 0) return null;
37313
- return {
37314
- worktreePaths,
37315
- sessionParentPath: sessionDir,
37316
- workingTreeRelRoot: sessionDir
37317
- };
37264
+ return null;
37318
37265
  }
37319
37266
 
37320
- // src/worktrees/manager/prepare-and-remember-session-worktrees.ts
37321
- async function prepareAndRememberSessionWorktrees(params) {
37322
- const prep = await prepareNewSessionWorktrees({
37323
- worktreesRootPath: params.worktreesRootPath,
37324
- bridgeRoot: getBridgeRoot(),
37325
- sessionId: params.sessionId,
37326
- layout: params.layout,
37327
- log: params.log,
37328
- ...params.worktreeBaseBranches && Object.keys(params.worktreeBaseBranches).length > 0 ? { worktreeBaseBranches: params.worktreeBaseBranches } : {}
37267
+ // src/git/commits/remote-tracking/resolve-base-sha-for-unpushed-commits.ts
37268
+ import * as path22 from "node:path";
37269
+ var BASE_SHA_CACHE_TTL_MS = 3e4;
37270
+ var baseShaCache = /* @__PURE__ */ new Map();
37271
+ async function resolveBaseShaForUnpushedCommitsUncached(g) {
37272
+ const config2 = await readBranchRemoteConfig(g);
37273
+ const trackingSha = await firstResolvedSha(g, trackingRefCandidates(config2));
37274
+ if (trackingSha) return trackingSha;
37275
+ return resolveDefaultRemoteBranchSha(g, config2.remote);
37276
+ }
37277
+ async function resolveBaseShaForUnpushedCommits(g, repoDir) {
37278
+ if (repoDir) {
37279
+ const key = path22.resolve(repoDir);
37280
+ const now = Date.now();
37281
+ const cached2 = baseShaCache.get(key);
37282
+ if (cached2 && cached2.expiresAt > now) return cached2.baseSha;
37283
+ const baseSha = await resolveBaseShaForUnpushedCommitsUncached(g);
37284
+ baseShaCache.set(key, { baseSha, expiresAt: now + BASE_SHA_CACHE_TTL_MS });
37285
+ return baseSha;
37286
+ }
37287
+ return resolveBaseShaForUnpushedCommitsUncached(g);
37288
+ }
37289
+
37290
+ // src/git/commits/remote-tracking/commits-ahead-of-remote-tracking.ts
37291
+ async function commitsAheadOfRemoteTracking(repoDir) {
37292
+ const g = cliSimpleGit(repoDir);
37293
+ const headSha = await revParseSafe(g, "HEAD");
37294
+ if (!headSha) return 0;
37295
+ const baseSha = await resolveBaseShaForUnpushedCommits(g, repoDir);
37296
+ if (!baseSha || baseSha === headSha) return 0;
37297
+ try {
37298
+ const out = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
37299
+ const n = parseInt(String(out).trim(), 10);
37300
+ return Number.isNaN(n) ? 0 : n;
37301
+ } catch {
37302
+ return 0;
37303
+ }
37304
+ }
37305
+
37306
+ // src/git/status/working-tree-status.ts
37307
+ async function getRepoWorkingTreeStatus(repoDir) {
37308
+ return runGitTask(async () => {
37309
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
37310
+ const hasUncommittedChanges = uncommittedFileCount > 0;
37311
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
37312
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
37329
37313
  });
37330
- if (!prep) return void 0;
37331
- params.cache.remember(params.sessionId, {
37332
- sessionParentPath: prep.sessionParentPath,
37333
- workingTreeRelRoot: prep.workingTreeRelRoot,
37334
- repoCheckoutPaths: prep.worktreePaths
37314
+ }
37315
+ async function aggregateSessionPathsWorkingTreeStatus(paths) {
37316
+ let hasUncommittedChanges = false;
37317
+ let hasUnpushedCommits = false;
37318
+ let uncommittedFileCount = 0;
37319
+ await forEachWithGitYield(paths, async (p) => {
37320
+ const s = await getRepoWorkingTreeStatus(p);
37321
+ uncommittedFileCount += s.uncommittedFileCount;
37322
+ if (s.hasUncommittedChanges) hasUncommittedChanges = true;
37323
+ if (s.hasUnpushedCommits) hasUnpushedCommits = true;
37335
37324
  });
37336
- return params.cache.getSessionParentPath(params.sessionId);
37325
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
37337
37326
  }
37338
-
37339
- // src/worktrees/manager/resolve-existing-session-parent-path.ts
37340
- async function resolveExistingSessionParentPath(sessionId, cache2, discover) {
37341
- const cached2 = cache2.getSessionParentPath(sessionId);
37342
- if (cached2) return cached2;
37343
- const disc = await discover();
37344
- if (disc) {
37345
- cache2.remember(sessionId, disc);
37346
- return cache2.getSessionParentPath(sessionId);
37347
- }
37348
- return void 0;
37327
+ async function pushAheadOfUpstreamForPaths(paths) {
37328
+ await forEachWithGitYield(paths, async (p) => {
37329
+ const g = cliSimpleGit(p);
37330
+ const ahead = await commitsAheadOfRemoteTracking(p);
37331
+ if (ahead <= 0) return;
37332
+ await g.push();
37333
+ });
37349
37334
  }
37350
37335
 
37351
- // src/worktrees/manager/resolve-explicit-session-parent-path.ts
37352
- import * as path29 from "node:path";
37353
- async function resolveExplicitSessionParentPath(params) {
37354
- const resolved = path29.resolve(params.parentPathRaw);
37355
- if (parseSessionParent(params.sessionParent) !== "worktrees_root") {
37356
- return resolved;
37357
- }
37358
- const rememberAndReturn = (binding) => {
37359
- params.cache.remember(params.sessionId, binding);
37360
- return params.cache.getSessionParentPath(params.sessionId) ?? resolved;
37336
+ // src/worktrees/manager/resolve-commit-targets.ts
37337
+ function bridgeRootBinding(bridgeRoot) {
37338
+ return {
37339
+ sessionParentPath: bridgeRoot,
37340
+ workingTreeRelRoot: bridgeRoot,
37341
+ repoCheckoutPaths: [bridgeRoot]
37361
37342
  };
37362
- const diskFirst = await params.discover();
37363
- if (diskFirst) return rememberAndReturn(diskFirst);
37364
- const fromRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(resolved, params.sessionId);
37365
- if (fromRoot) return rememberAndReturn(fromRoot);
37366
- let cur = resolved;
37367
- for (let i = 0; i < 16; i++) {
37368
- await yieldToEventLoop();
37369
- const tryRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(cur, params.sessionId);
37370
- if (tryRoot) return rememberAndReturn(tryRoot);
37371
- const next = path29.dirname(cur);
37372
- if (next === cur) break;
37373
- cur = next;
37374
- }
37375
- return resolved;
37376
37343
  }
37377
-
37378
- // src/worktrees/manager/resolve-session-parent-path-for-prompt.ts
37379
- async function resolveSessionParentPathForPrompt(params) {
37380
- const { sessionId, cache: cache2, worktreesRootPath, layout, log: log2, discover, opts } = params;
37381
- if (!sessionId) return void 0;
37344
+ async function resolveCommitTargetsAsync(sessionId, cache2, discover) {
37382
37345
  const sid = sessionId.trim();
37383
- const parentPathRaw = opts.sessionParentPath?.trim();
37384
- if (parentPathRaw) {
37385
- return resolveExplicitSessionParentPath({
37386
- sessionId: sid,
37387
- sessionParent: opts.sessionParent,
37388
- parentPathRaw,
37389
- cache: cache2,
37390
- discover: () => discover(sid)
37391
- });
37392
- }
37393
- const parentKind = parseSessionParent(opts.sessionParent);
37394
- if (parentKind === "bridge_root") {
37395
- return void 0;
37396
- }
37397
- if (parentKind === "worktrees_root") {
37398
- if (!opts.isNewSession) {
37399
- return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
37400
- }
37401
- return prepareAndRememberSessionWorktrees({
37402
- cache: cache2,
37403
- sessionId: sid,
37404
- worktreesRootPath,
37405
- layout,
37406
- log: log2,
37407
- ...opts.worktreeBaseBranches ? { worktreeBaseBranches: opts.worktreeBaseBranches } : {}
37408
- });
37409
- }
37410
- if (!opts.isNewSession) {
37411
- return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
37346
+ const paths = cache2.getRepoCheckoutPathsRef(sid);
37347
+ if (paths?.length) return paths;
37348
+ const disc = await discover(sid);
37349
+ if (disc?.repoCheckoutPaths.length) {
37350
+ cache2.remember(sid, disc);
37351
+ return disc.repoCheckoutPaths;
37412
37352
  }
37413
- return prepareAndRememberSessionWorktrees({
37414
- cache: cache2,
37415
- sessionId: sid,
37416
- worktreesRootPath,
37417
- layout,
37418
- log: log2
37419
- });
37353
+ const bridgeRoot = getBridgeRoot();
37354
+ cache2.remember(sid, bridgeRootBinding(bridgeRoot));
37355
+ return [bridgeRoot];
37356
+ }
37357
+
37358
+ // src/worktrees/manager/git/get-session-working-tree-status.ts
37359
+ async function getSessionWorkingTreeStatus(cache2, sessionId, discover) {
37360
+ return aggregateSessionPathsWorkingTreeStatus(
37361
+ await resolveCommitTargetsAsync(sessionId, cache2, discover)
37362
+ );
37420
37363
  }
37421
37364
 
37422
37365
  // src/git/changes/types.ts
@@ -37477,108 +37420,13 @@ function formatRemoteDisplayLabel(remoteUrl) {
37477
37420
  }
37478
37421
 
37479
37422
  // src/git/changes/repo/get-working-tree-change-repo-details.ts
37480
- import * as path34 from "node:path";
37423
+ import * as path29 from "node:path";
37481
37424
 
37482
37425
  // src/git/changes/repo/build-working-tree-change-repo-detail.ts
37483
- import * as path33 from "node:path";
37426
+ import * as path28 from "node:path";
37484
37427
 
37485
- // src/git/commits/resolve-remote-tracking.ts
37486
- async function tryConfigGet(g, key) {
37487
- try {
37488
- const out = await g.raw(["config", "--get", key]);
37489
- const v = String(out).trim();
37490
- return v || null;
37491
- } catch {
37492
- return null;
37493
- }
37494
- }
37495
- async function revParseSafe(g, ref) {
37496
- try {
37497
- const v = (await g.raw(["rev-parse", ref])).trim();
37498
- return v || null;
37499
- } catch {
37500
- return null;
37501
- }
37502
- }
37503
- async function resolveRemoteTrackingRefForAhead(g) {
37504
- try {
37505
- await g.raw(["rev-parse", "--verify", "HEAD@{upstream}"]);
37506
- return "HEAD@{upstream}";
37507
- } catch {
37508
- try {
37509
- await g.raw(["rev-parse", "--verify", "@{u}"]);
37510
- return "@{u}";
37511
- } catch {
37512
- }
37513
- }
37514
- const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
37515
- if (!branch || branch === "HEAD") return null;
37516
- const remote = await tryConfigGet(g, `branch.${branch}.remote`) ?? "origin";
37517
- const merge2 = await tryConfigGet(g, `branch.${branch}.merge`);
37518
- if (merge2) {
37519
- const upstreamBranch = merge2.replace(/^refs\/heads\//, "");
37520
- const ref = `refs/remotes/${remote}/${upstreamBranch}`;
37521
- try {
37522
- await g.raw(["rev-parse", "--verify", ref]);
37523
- return ref;
37524
- } catch {
37525
- }
37526
- }
37527
- const fallbackRef = `refs/remotes/${remote}/${branch}`;
37528
- try {
37529
- await g.raw(["rev-parse", "--verify", fallbackRef]);
37530
- return fallbackRef;
37531
- } catch {
37532
- return null;
37533
- }
37534
- }
37535
- async function remoteForCurrentBranch(g) {
37536
- const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
37537
- if (!branch || branch === "HEAD") return "origin";
37538
- return await tryConfigGet(g, `branch.${branch}.remote`) ?? "origin";
37539
- }
37540
- async function resolveDefaultRemoteBranchRef(g, remote) {
37541
- const headSym = `refs/remotes/${remote}/HEAD`;
37542
- try {
37543
- const resolved = (await g.raw(["symbolic-ref", "-q", "--verify", headSym])).trim();
37544
- if (resolved.startsWith("refs/remotes/")) return resolved;
37545
- } catch {
37546
- }
37547
- for (const name of ["main", "master", "trunk", "develop"]) {
37548
- const r = `refs/remotes/${remote}/${name}`;
37549
- try {
37550
- await g.raw(["rev-parse", "--verify", r]);
37551
- return r;
37552
- } catch {
37553
- }
37554
- }
37555
- return null;
37556
- }
37557
- async function resolveBaseShaForUnpushedCommits(g) {
37558
- const trackingRef = await resolveRemoteTrackingRefForAhead(g);
37559
- if (trackingRef) {
37560
- const sha = await revParseSafe(g, trackingRef);
37561
- if (sha) return sha;
37562
- }
37563
- const remote = await remoteForCurrentBranch(g);
37564
- const defaultRef = await resolveDefaultRemoteBranchRef(g, remote);
37565
- if (!defaultRef) return null;
37566
- return revParseSafe(g, defaultRef);
37567
- }
37568
- async function commitsAheadOfRemoteTracking(repoDir) {
37569
- const g = cliSimpleGit(repoDir);
37570
- const headSha = await revParseSafe(g, "HEAD");
37571
- if (!headSha) return 0;
37572
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
37573
- if (!baseSha || baseSha === headSha) return 0;
37574
- try {
37575
- const out = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
37576
- const n = parseInt(String(out).trim(), 10);
37577
- return Number.isNaN(n) ? 0 : n;
37578
- } catch {
37579
- return 0;
37580
- }
37581
- }
37428
+ // src/git/commits/list-unpushed-commits.ts
37429
+ import * as path23 from "node:path";
37582
37430
 
37583
37431
  // src/git/commits/lib/parse-log-lines.ts
37584
37432
  function parseLogShaDateSubjectLines(raw) {
@@ -37598,9 +37446,14 @@ function parseLogShaDateSubjectLines(raw) {
37598
37446
  }
37599
37447
 
37600
37448
  // src/git/commits/list-unpushed-commits.ts
37449
+ var UNPUSHED_CACHE_TTL_MS = 3e4;
37450
+ var unpushedCache = /* @__PURE__ */ new Map();
37601
37451
  async function gitLogNotReachableFromBase(g, baseSha, headSha) {
37602
37452
  if (baseSha === headSha) return [];
37603
37453
  try {
37454
+ const countOut = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
37455
+ const count = parseInt(String(countOut).trim(), 10);
37456
+ if (!Number.isFinite(count) || count <= 0) return [];
37604
37457
  const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
37605
37458
  return parseLogShaDateSubjectLines(logOut);
37606
37459
  } catch {
@@ -37608,12 +37461,22 @@ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
37608
37461
  }
37609
37462
  }
37610
37463
  async function listUnpushedCommits(repoDir) {
37464
+ const key = path23.resolve(repoDir);
37465
+ const now = Date.now();
37466
+ const cached2 = unpushedCache.get(key);
37467
+ if (cached2 && cached2.expiresAt > now) return cached2.commits;
37611
37468
  const g = cliSimpleGit(repoDir);
37612
- const headSha = await revParseSafe(g, "HEAD");
37613
- if (!headSha) return [];
37614
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
37615
- if (!baseSha) return [];
37616
- return gitLogNotReachableFromBase(g, baseSha, headSha);
37469
+ const [headSha, baseSha] = await Promise.all([
37470
+ revParseSafe(g, "HEAD"),
37471
+ resolveBaseShaForUnpushedCommits(g, repoDir)
37472
+ ]);
37473
+ if (!headSha || !baseSha) {
37474
+ unpushedCache.set(key, { commits: [], expiresAt: now + UNPUSHED_CACHE_TTL_MS });
37475
+ return [];
37476
+ }
37477
+ const commits = await gitLogNotReachableFromBase(g, baseSha, headSha);
37478
+ unpushedCache.set(key, { commits, expiresAt: now + UNPUSHED_CACHE_TTL_MS });
37479
+ return commits;
37617
37480
  }
37618
37481
 
37619
37482
  // src/git/commits/lib/sanitize-recent-commits-limit.ts
@@ -37627,72 +37490,40 @@ function sanitizeRecentCommitsLimit(value) {
37627
37490
  }
37628
37491
 
37629
37492
  // src/git/commits/list-recent-commits.ts
37630
- async function listRecentCommits(repoDir, limitInput) {
37493
+ async function listRecentCommitRefs(repoDir, limitInput) {
37631
37494
  const limit = sanitizeRecentCommitsLimit(limitInput);
37632
37495
  const g = cliSimpleGit(repoDir);
37633
- const headSha = await revParseSafe(g, "HEAD");
37634
- if (!headSha) return { commits: [], hasMore: false };
37635
- const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
37636
37496
  try {
37637
- const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
37638
- const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
37639
- ...c,
37640
- needsPush: unpushedSet.has(c.sha)
37641
- }));
37642
- let hasMore = false;
37643
- if (commits.length === limit) {
37644
- const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
37645
- hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
37646
- }
37497
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit + 1), "HEAD"]);
37498
+ const parsed = parseLogShaDateSubjectLines(logOut);
37499
+ const hasMore = parsed.length > limit;
37500
+ const commits = parsed.slice(0, limit);
37647
37501
  return { commits, hasMore };
37648
37502
  } catch {
37649
37503
  return { commits: [], hasMore: false };
37650
37504
  }
37651
37505
  }
37652
37506
 
37653
- // src/git/changes/parse/normalize-git-diff-path.ts
37654
- function normalizeGitDiffPath(path55) {
37655
- return path55.replace(/\\/g, "/").replace(/\/ +/g, "/").trim();
37656
- }
37657
-
37658
- // src/git/changes/parse/expand-git-rename-numstat-path.ts
37659
- function expandGitRenameNumstatPath(rawPath) {
37660
- const open2 = rawPath.indexOf("{");
37661
- const arrow = rawPath.indexOf("=>");
37662
- const close = rawPath.lastIndexOf("}");
37663
- if (open2 === -1 || arrow === -1 || close === -1 || open2 > arrow || arrow > close) {
37664
- return null;
37665
- }
37666
- const prefix = rawPath.slice(0, open2);
37667
- const suffix = rawPath.slice(close + 1);
37668
- const oldPart = rawPath.slice(open2 + 1, arrow).trim();
37669
- const newPart = rawPath.slice(arrow + 2, close).trim();
37670
- return {
37671
- oldPath: normalizeGitDiffPath(`${prefix}${oldPart}${suffix}`),
37672
- newPath: normalizeGitDiffPath(`${prefix}${newPart}${suffix}`)
37673
- };
37674
- }
37675
- function isGitRenameNumstatPath(rawPath) {
37676
- return expandGitRenameNumstatPath(rawPath) != null;
37677
- }
37507
+ // src/git/changes/listing/list-changed-files-for-commit.ts
37508
+ import * as path25 from "node:path";
37678
37509
 
37679
37510
  // src/git/changes/rows/build-changed-file-row.ts
37680
37511
  function buildChangedFileRow(options) {
37681
37512
  const pathInRepo = normalizeGitDiffPath(options.pathInRepo);
37682
- const relLauncher = posixJoinDirFile(options.repoRelPath, pathInRepo);
37513
+ const workspaceRelPath = posixJoinDirFile(options.repoRelPath, pathInRepo);
37683
37514
  const additions = options.numEntry?.additions ?? 0;
37684
37515
  const deletions = options.numEntry?.deletions ?? 0;
37685
37516
  let change = options.nameEntry?.change ?? "modified";
37686
- let movedFromPathRelLauncher;
37517
+ let movedFromWorkspaceRelPath;
37687
37518
  let movedFromPathInRepo;
37688
37519
  const oldPathInRepo = options.nameEntry?.oldPathInRepo ?? options.numEntry?.oldPathInRepo;
37689
37520
  if (change === "moved" && oldPathInRepo) {
37690
37521
  movedFromPathInRepo = normalizeGitDiffPath(oldPathInRepo);
37691
- movedFromPathRelLauncher = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
37522
+ movedFromWorkspaceRelPath = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
37692
37523
  } else if (oldPathInRepo && change === "modified") {
37693
37524
  change = "moved";
37694
37525
  movedFromPathInRepo = normalizeGitDiffPath(oldPathInRepo);
37695
- movedFromPathRelLauncher = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
37526
+ movedFromWorkspaceRelPath = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
37696
37527
  }
37697
37528
  if (options.untracked && !options.nameEntry) {
37698
37529
  change = "added";
@@ -37702,11 +37533,11 @@ function buildChangedFileRow(options) {
37702
37533
  else change = "modified";
37703
37534
  }
37704
37535
  return {
37705
- pathRelLauncher: relLauncher,
37536
+ workspaceRelPath,
37706
37537
  additions,
37707
37538
  deletions,
37708
37539
  change,
37709
- ...movedFromPathRelLauncher ? { movedFromPathRelLauncher } : {},
37540
+ ...movedFromWorkspaceRelPath ? { movedFromWorkspaceRelPath } : {},
37710
37541
  ...movedFromPathInRepo ? { movedFromPathInRepo } : {}
37711
37542
  };
37712
37543
  }
@@ -37765,8 +37596,8 @@ async function unifiedDiffForFileInRange(repoCwd, range, pathInRepo, change, mov
37765
37596
  }
37766
37597
 
37767
37598
  // src/git/changes/total-lines/resolve-changed-file-total-lines.ts
37768
- import * as fs24 from "node:fs";
37769
- import * as path30 from "node:path";
37599
+ import * as fs18 from "node:fs";
37600
+ import * as path24 from "node:path";
37770
37601
 
37771
37602
  // src/git/changes/lines/count-lines.ts
37772
37603
  import { createReadStream } from "node:fs";
@@ -37812,13 +37643,13 @@ async function countLinesInGitRef(repoGitCwd, ref, pathInRepo) {
37812
37643
  }
37813
37644
  async function resolveWorkingTreeFileTotalLines(options) {
37814
37645
  if (options.isBinary) return null;
37815
- const filePath = path30.join(options.repoGitCwd, options.pathInRepo);
37646
+ const filePath = path24.join(options.repoGitCwd, options.pathInRepo);
37816
37647
  if (options.change === "removed") {
37817
37648
  return countLinesInGitRef(options.repoGitCwd, "HEAD", options.pathInRepo);
37818
37649
  }
37819
37650
  if (options.change === "moved" || options.change === "modified") {
37820
37651
  try {
37821
- const st = await fs24.promises.stat(filePath);
37652
+ const st = await fs18.promises.stat(filePath);
37822
37653
  if (!st.isFile()) return null;
37823
37654
  return await countTextFileLines(filePath);
37824
37655
  } catch {
@@ -37838,23 +37669,23 @@ async function resolveCommitFileTotalLines(options) {
37838
37669
  return null;
37839
37670
  }
37840
37671
 
37841
- // src/git/changes/listing/path-in-repo-from-launcher.ts
37842
- function pathInRepoFromLauncher(pathRelLauncher, repoRelPath) {
37672
+ // src/git/changes/listing/path-in-repo-from-workspace.ts
37673
+ function pathInRepoFromWorkspace(workspaceRelPath, repoRelPath) {
37843
37674
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
37844
- if (normRel === ".") return pathRelLauncher;
37845
- if (pathRelLauncher.startsWith(`${normRel}/`)) {
37846
- return pathRelLauncher.slice(normRel.length + 1);
37675
+ if (normRel === ".") return workspaceRelPath;
37676
+ if (workspaceRelPath.startsWith(`${normRel}/`)) {
37677
+ return workspaceRelPath.slice(normRel.length + 1);
37847
37678
  }
37848
- return pathRelLauncher;
37679
+ return workspaceRelPath;
37849
37680
  }
37850
- function normalizeRepoRelPath2(repoRelPath) {
37681
+ function normalizeRepoRelPath(repoRelPath) {
37851
37682
  return repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
37852
37683
  }
37853
37684
 
37854
37685
  // src/git/changes/listing/enrich-commit-file-rows.ts
37855
37686
  async function enrichCommitFileRows(options) {
37856
37687
  await forEachWithGitYield(options.rows, async (row) => {
37857
- const pathInRepo = pathInRepoFromLauncher(row.pathRelLauncher, options.repoRelPath);
37688
+ const pathInRepo = pathInRepoFromWorkspace(row.workspaceRelPath, options.repoRelPath);
37858
37689
  row.patchContent = await unifiedDiffForFileInRange(
37859
37690
  options.repoGitCwd,
37860
37691
  options.range,
@@ -37873,102 +37704,6 @@ async function enrichCommitFileRows(options) {
37873
37704
  });
37874
37705
  }
37875
37706
 
37876
- // src/git/changes/parse/parse-name-status-entries.ts
37877
- function parseNameStatusEntries(lines) {
37878
- const entries = [];
37879
- for (const line of lines) {
37880
- if (!line.trim()) continue;
37881
- const tabParts = line.split(" ");
37882
- if (tabParts.length < 2) continue;
37883
- const status = tabParts[0].trim();
37884
- const code = status[0];
37885
- if (code === "A") {
37886
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "added" });
37887
- } else if (code === "D") {
37888
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "removed" });
37889
- } else if (code === "R" || code === "C") {
37890
- if (tabParts.length >= 3) {
37891
- entries.push({
37892
- pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]),
37893
- oldPathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 2]),
37894
- change: "moved"
37895
- });
37896
- }
37897
- } else if (code === "M" || code === "U" || code === "T") {
37898
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "modified" });
37899
- }
37900
- }
37901
- return entries;
37902
- }
37903
- function parseNameStatusLines(lines) {
37904
- const m = /* @__PURE__ */ new Map();
37905
- for (const entry of parseNameStatusEntries(lines)) {
37906
- m.set(entry.pathInRepo, entry.change);
37907
- }
37908
- return m;
37909
- }
37910
-
37911
- // src/git/changes/parse/parse-numstat-entries.ts
37912
- function parseNumstatCounts(parts) {
37913
- const [a, d] = parts;
37914
- return {
37915
- additions: a === "-" ? 0 : parseInt(String(a), 10) || 0,
37916
- deletions: d === "-" ? 0 : parseInt(String(d), 10) || 0
37917
- };
37918
- }
37919
- function parseNumstatLine(line) {
37920
- const parts = line.split(" ");
37921
- if (parts.length < 3) return null;
37922
- const rawPath = parts[parts.length - 1];
37923
- const { additions, deletions } = parseNumstatCounts(parts);
37924
- const expanded = expandGitRenameNumstatPath(rawPath);
37925
- const pathInRepo = normalizeGitDiffPath(expanded?.newPath ?? rawPath);
37926
- return {
37927
- pathInRepo,
37928
- additions,
37929
- deletions,
37930
- ...expanded ? { oldPathInRepo: expanded.oldPath } : {}
37931
- };
37932
- }
37933
- function parseNumstatEntries(lines) {
37934
- const m = /* @__PURE__ */ new Map();
37935
- for (const line of lines) {
37936
- if (!line.trim()) continue;
37937
- const parts = line.split(" ");
37938
- if (parts.length < 3) continue;
37939
- const rawPath = parts[parts.length - 1];
37940
- const expanded = expandGitRenameNumstatPath(rawPath);
37941
- const entry = parseNumstatLine(line);
37942
- if (!entry) continue;
37943
- const existing = m.get(entry.pathInRepo);
37944
- if (!existing || expanded) {
37945
- m.set(entry.pathInRepo, entry);
37946
- }
37947
- }
37948
- return m;
37949
- }
37950
- function parseNumstat(lines) {
37951
- const m = /* @__PURE__ */ new Map();
37952
- for (const [path55, entry] of parseNumstatEntries(lines)) {
37953
- m.set(path55, { additions: entry.additions, deletions: entry.deletions });
37954
- }
37955
- return m;
37956
- }
37957
-
37958
- // src/git/changes/parse/numstat-from-git-no-index.ts
37959
- async function numstatFromGitNoIndex(g, pathInRepo) {
37960
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
37961
- try {
37962
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
37963
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
37964
- const parsed = parseNumstatLine(first2);
37965
- if (!parsed) return null;
37966
- return { additions: parsed.additions, deletions: parsed.deletions };
37967
- } catch {
37968
- return null;
37969
- }
37970
- }
37971
-
37972
37707
  // src/git/changes/rows/collect-moved-source-paths.ts
37973
37708
  function collectMovedSourcePaths(nameEntries, numByPath) {
37974
37709
  const sources = /* @__PURE__ */ new Set();
@@ -38048,8 +37783,8 @@ function pickPreferredChangedFileRow(a, b) {
38048
37783
  const aHasPatch = Boolean(a.patchContent?.trim());
38049
37784
  const bHasPatch = Boolean(b.patchContent?.trim());
38050
37785
  if (aHasPatch !== bHasPatch) return aHasPatch ? a : b;
38051
- const aHasMoveMeta = Boolean(a.movedFromPathInRepo || a.movedFromPathRelLauncher);
38052
- const bHasMoveMeta = Boolean(b.movedFromPathInRepo || b.movedFromPathRelLauncher);
37786
+ const aHasMoveMeta = Boolean(a.movedFromPathInRepo || a.movedFromWorkspaceRelPath);
37787
+ const bHasMoveMeta = Boolean(b.movedFromPathInRepo || b.movedFromWorkspaceRelPath);
38053
37788
  if (aHasMoveMeta !== bHasMoveMeta) return aHasMoveMeta ? a : b;
38054
37789
  return a;
38055
37790
  }
@@ -38058,30 +37793,39 @@ function pickPreferredChangedFileRow(a, b) {
38058
37793
  function dedupeChangedFileRows(rows) {
38059
37794
  const byPath = /* @__PURE__ */ new Map();
38060
37795
  for (const row of rows) {
38061
- const key = normalizeGitDiffPath(row.pathRelLauncher);
38062
- const normalizedRow = key === row.pathRelLauncher ? row : { ...row, pathRelLauncher: key };
37796
+ const key = normalizeGitDiffPath(row.workspaceRelPath);
37797
+ const normalizedRow = key === row.workspaceRelPath ? row : { ...row, workspaceRelPath: key };
38063
37798
  const existing = byPath.get(key);
38064
37799
  if (!existing) {
38065
37800
  byPath.set(key, normalizedRow);
38066
37801
  continue;
38067
37802
  }
38068
37803
  const preferRow = pickPreferredChangedFileRow(existing, normalizedRow);
38069
- byPath.set(key, { ...preferRow, pathRelLauncher: key });
37804
+ byPath.set(key, { ...preferRow, workspaceRelPath: key });
38070
37805
  }
38071
37806
  return [...byPath.values()];
38072
37807
  }
38073
37808
 
38074
37809
  // src/git/changes/listing/list-changed-files-for-commit.ts
38075
- async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
37810
+ var COMMIT_FILES_CACHE_TTL_MS = 5 * 6e4;
37811
+ var commitFilesCache = /* @__PURE__ */ new Map();
37812
+ function commitFilesCacheKey(repoGitCwd, commitSha) {
37813
+ return `${path25.resolve(repoGitCwd)}:${commitSha}`;
37814
+ }
37815
+ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha, options = {}) {
37816
+ const cacheKey = commitFilesCacheKey(repoGitCwd, commitSha);
37817
+ if (!options.includeFilePatches) {
37818
+ const now = Date.now();
37819
+ const cached2 = commitFilesCache.get(cacheKey);
37820
+ if (cached2 && cached2.expiresAt > now) return cached2.rows;
37821
+ }
38076
37822
  const g = cliSimpleGit(repoGitCwd);
38077
- const parent = await parentForCommitDiff(g, commitSha);
38078
- const range = `${parent}..${commitSha}`;
37823
+ const normRel = normalizeRepoRelPath(repoRelPath);
38079
37824
  const [nameStatusRaw, numstatRaw] = await Promise.all([
38080
- g.raw(["diff", ...RENAME_DIFF_ARGS, "--name-status", range]).catch(() => ""),
38081
- g.raw(["diff", ...RENAME_DIFF_ARGS, "--numstat", range]).catch(() => "")
37825
+ g.raw(["diff-tree", "--no-commit-id", "--name-status", "-r", ...RENAME_DIFF_ARGS, commitSha]).catch(() => ""),
37826
+ g.raw(["show", "--numstat", "--format=format:", commitSha]).catch(() => "")
38082
37827
  ]);
38083
37828
  const parsed = parseDiffOutput(String(nameStatusRaw), String(numstatRaw));
38084
- const normRel = normalizeRepoRelPath2(repoRelPath);
38085
37829
  const paths = collectChangedPaths({
38086
37830
  nameEntries: parsed.nameEntries,
38087
37831
  numByPath: parsed.numByPath
@@ -38094,29 +37838,39 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
38094
37838
  movedSourcePaths: parsed.movedSourcePaths
38095
37839
  });
38096
37840
  rows = dedupeChangedFileRows(rows);
38097
- await enrichCommitFileRows({
38098
- rows,
38099
- repoGitCwd,
38100
- repoRelPath: normRel,
38101
- range,
38102
- commitSha,
38103
- parentSha: parent,
38104
- binaryByPath: parsed.binaryByPath
38105
- });
37841
+ if (options.includeFilePatches) {
37842
+ const parent = await parentForCommitDiff(g, commitSha);
37843
+ const range = `${parent}..${commitSha}`;
37844
+ await enrichCommitFileRows({
37845
+ rows,
37846
+ repoGitCwd,
37847
+ repoRelPath: normRel,
37848
+ range,
37849
+ commitSha,
37850
+ parentSha: parent,
37851
+ binaryByPath: parsed.binaryByPath
37852
+ });
37853
+ }
38106
37854
  const finalRows = dedupeChangedFileRows(rows);
38107
- finalRows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
37855
+ finalRows.sort((a, b) => a.workspaceRelPath.localeCompare(b.workspaceRelPath));
37856
+ if (!options.includeFilePatches) {
37857
+ commitFilesCache.set(cacheKey, {
37858
+ rows: finalRows,
37859
+ expiresAt: Date.now() + COMMIT_FILES_CACHE_TTL_MS
37860
+ });
37861
+ }
38108
37862
  return finalRows;
38109
37863
  }
38110
37864
 
38111
37865
  // src/git/changes/listing/apply-untracked-file-stats.ts
38112
- import * as fs25 from "node:fs";
38113
- import * as path31 from "node:path";
37866
+ import * as fs19 from "node:fs";
37867
+ import * as path26 from "node:path";
38114
37868
  function createUntrackedStatsApplier(options) {
38115
37869
  return async (row, pathInRepo) => {
38116
37870
  if (!options.untrackedSet.has(pathInRepo)) return;
38117
37871
  if (options.nameByPath.has(pathInRepo)) return;
38118
37872
  if (row.change === "moved") return;
38119
- const repoFilePath = path31.join(options.repoGitCwd, pathInRepo);
37873
+ const repoFilePath = path26.join(options.repoGitCwd, pathInRepo);
38120
37874
  const fromGit = await numstatFromGitNoIndex(options.g, pathInRepo);
38121
37875
  if (fromGit) {
38122
37876
  row.additions = fromGit.additions;
@@ -38124,7 +37878,7 @@ function createUntrackedStatsApplier(options) {
38124
37878
  return;
38125
37879
  }
38126
37880
  try {
38127
- const st = await fs25.promises.stat(repoFilePath);
37881
+ const st = await fs19.promises.stat(repoFilePath);
38128
37882
  row.additions = st.isFile() ? await countTextFileLines(repoFilePath) : 0;
38129
37883
  } catch {
38130
37884
  row.additions = 0;
@@ -38134,7 +37888,7 @@ function createUntrackedStatsApplier(options) {
38134
37888
  }
38135
37889
 
38136
37890
  // src/git/changes/listing/enrich-working-tree-file-rows.ts
38137
- import * as path32 from "node:path";
37891
+ import * as path27 from "node:path";
38138
37892
 
38139
37893
  // src/git/changes/patch/hydrate/append-injected-context-lines.ts
38140
37894
  async function appendInjectedContextLines(out, inject, maybeYield, injectedTotal) {
@@ -38214,10 +37968,10 @@ async function readGitBlobLines(repoCwd, pathInRepo) {
38214
37968
  }
38215
37969
 
38216
37970
  // src/git/changes/patch/hydrate/read-worktree-file-lines.ts
38217
- import * as fs26 from "node:fs";
37971
+ import * as fs20 from "node:fs";
38218
37972
  async function readWorktreeFileLines(filePath) {
38219
37973
  try {
38220
- const raw = await fs26.promises.readFile(filePath, "utf8");
37974
+ const raw = await fs20.promises.readFile(filePath, "utf8");
38221
37975
  return raw.split(/\r?\n/);
38222
37976
  } catch {
38223
37977
  return null;
@@ -38313,8 +38067,8 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
38313
38067
  // src/git/changes/listing/enrich-working-tree-file-rows.ts
38314
38068
  async function enrichWorkingTreeFileRows(options) {
38315
38069
  await forEachWithGitYield(options.rows, async (row) => {
38316
- const pathInRepo = pathInRepoFromLauncher(row.pathRelLauncher, options.repoRelPath);
38317
- const filePath = path32.join(options.repoGitCwd, pathInRepo);
38070
+ const pathInRepo = pathInRepoFromWorkspace(row.workspaceRelPath, options.repoRelPath);
38071
+ const filePath = path27.join(options.repoGitCwd, pathInRepo);
38318
38072
  const hydrateKind = row.change === "moved" ? "modified" : row.change;
38319
38073
  let patch = await unifiedDiffForFile(options.repoGitCwd, pathInRepo, row.change, row.movedFromPathInRepo);
38320
38074
  if (patch) {
@@ -38337,7 +38091,7 @@ async function enrichWorkingTreeFileRows(options) {
38337
38091
  }
38338
38092
 
38339
38093
  // src/git/changes/listing/list-changed-files-for-repo.ts
38340
- async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
38094
+ async function listChangedFilesForRepo(repoGitCwd, repoRelPath, options = {}) {
38341
38095
  const g = cliSimpleGit(repoGitCwd);
38342
38096
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
38343
38097
  g.raw(["diff", ...RENAME_DIFF_ARGS, "--name-status", "HEAD"]).catch(() => ""),
@@ -38367,12 +38121,14 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
38367
38121
  })
38368
38122
  });
38369
38123
  rows = dedupeChangedFileRows(rows);
38370
- await enrichWorkingTreeFileRows({
38371
- rows,
38372
- repoGitCwd,
38373
- repoRelPath,
38374
- binaryByPath: parsed.binaryByPath
38375
- });
38124
+ if (options.includeFilePatches) {
38125
+ await enrichWorkingTreeFileRows({
38126
+ rows,
38127
+ repoGitCwd,
38128
+ repoRelPath,
38129
+ binaryByPath: parsed.binaryByPath
38130
+ });
38131
+ }
38376
38132
  return dedupeChangedFileRows(rows);
38377
38133
  }
38378
38134
 
@@ -38383,48 +38139,62 @@ function normRepoRel(p) {
38383
38139
  }
38384
38140
 
38385
38141
  // src/git/changes/repo/build-working-tree-change-repo-detail.ts
38142
+ async function loadRecentAndUnpushedCommits(repoDir, recentCommitsLimit) {
38143
+ const [unpushedCommits, recentCommitList] = await Promise.all([
38144
+ listUnpushedCommits(repoDir),
38145
+ listRecentCommitRefs(repoDir, recentCommitsLimit)
38146
+ ]);
38147
+ const unpushedSet = new Set(unpushedCommits.map((c) => c.sha));
38148
+ return {
38149
+ unpushedCommits,
38150
+ recentCommits: recentCommitList.commits.map((c) => ({
38151
+ ...c,
38152
+ needsPush: unpushedSet.has(c.sha)
38153
+ })),
38154
+ recentCommitsHasMore: recentCommitList.hasMore
38155
+ };
38156
+ }
38386
38157
  async function buildWorkingTreeChangeRepoDetail(options) {
38387
- const t = path33.resolve(options.targetPath);
38158
+ const t = path28.resolve(options.targetPath);
38388
38159
  if (!await isGitRepoDirectory(t)) return null;
38389
38160
  const g = cliSimpleGit(t);
38390
- let branch = "HEAD";
38391
- try {
38392
- await yieldToEventLoop2();
38393
- branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim() || "HEAD";
38394
- } catch {
38395
- branch = "HEAD";
38396
- }
38161
+ const isCommitView = options.basis.kind === "commit";
38162
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path28.resolve(options.sessionWorktreeRootPath) : null;
38163
+ const legacyNested = options.legacyRepoNestedSessionLayout;
38397
38164
  await yieldToEventLoop2();
38398
- const remoteUrl = await getRemoteOriginUrl(t);
38165
+ const branchPromise = g.raw(["rev-parse", "--abbrev-ref", "HEAD"]).then((out) => out.trim() || "HEAD").catch(() => "HEAD");
38166
+ const remoteUrlPromise = getRemoteOriginUrl(t);
38167
+ const topLevelPromise = sessionWtRoot ? Promise.resolve(t) : g.raw(["rev-parse", "--show-toplevel"]).then((out) => out.trim()).catch(() => t);
38168
+ const [branch, remoteUrl, topOrT] = await Promise.all([
38169
+ branchPromise,
38170
+ remoteUrlPromise,
38171
+ topLevelPromise
38172
+ ]);
38399
38173
  const remoteDisplay = formatRemoteDisplayLabel(remoteUrl);
38400
- const sessionWtRoot = options.sessionWorktreeRootPath ? path33.resolve(options.sessionWorktreeRootPath) : null;
38401
- const legacyNested = options.legacyRepoNestedSessionLayout;
38402
38174
  let repoRelPath;
38403
38175
  if (sessionWtRoot) {
38404
- const anchor = legacyNested ? path33.dirname(t) : t;
38405
- const relNorm = path33.relative(sessionWtRoot, anchor);
38176
+ const anchor = legacyNested ? path28.dirname(t) : t;
38177
+ const relNorm = path28.relative(sessionWtRoot, anchor);
38406
38178
  repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
38407
38179
  } else {
38408
- let top = t;
38409
- try {
38410
- await yieldToEventLoop2();
38411
- top = (await g.raw(["rev-parse", "--show-toplevel"])).trim();
38412
- } catch {
38413
- top = t;
38414
- }
38415
- const rel = path33.relative(options.bridgeRoot, path33.resolve(top)).replace(/\\/g, "/") || ".";
38416
- repoRelPath = rel.startsWith("..") ? path33.basename(path33.resolve(top)) : rel;
38180
+ const top = topOrT;
38181
+ const rel = path28.relative(options.bridgeRoot, path28.resolve(top)).replace(/\\/g, "/") || ".";
38182
+ repoRelPath = rel.startsWith("..") ? path28.basename(path28.resolve(top)) : rel;
38417
38183
  }
38418
38184
  const norm = normRepoRel(repoRelPath === "" ? "." : repoRelPath);
38419
38185
  if (options.repoFilterRelPath && norm !== options.repoFilterRelPath) return null;
38420
38186
  const repoDisplayName = formatRepoShortTitle(remoteUrl, norm === "." ? "." : norm);
38421
38187
  const relForList = norm === "." ? "." : norm;
38422
- const files = options.basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, options.basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
38423
- const st = await g.status();
38424
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
38425
- const [unpushedCommits, recentCommitList] = await Promise.all([
38426
- listUnpushedCommits(t),
38427
- listRecentCommits(t, options.recentCommitsLimit)
38188
+ const commitSha = isCommitView ? options.basis.sha.trim() : null;
38189
+ const filesPromise = isCommitView && commitSha ? listChangedFilesForCommit(t, relForList, commitSha, {
38190
+ includeFilePatches: options.includeFilePatches
38191
+ }) : listChangedFilesForRepo(t, relForList, { includeFilePatches: options.includeFilePatches });
38192
+ const statusPromise = isCommitView ? Promise.resolve(false) : g.status().then((st) => (st.files?.length ?? 0) > 0);
38193
+ const commitsPromise = isCommitView ? Promise.resolve(null) : loadRecentAndUnpushedCommits(t, options.recentCommitsLimit);
38194
+ const [files, hasUncommittedChanges, commitData] = await Promise.all([
38195
+ filesPromise,
38196
+ statusPromise,
38197
+ commitsPromise
38428
38198
  ]);
38429
38199
  return {
38430
38200
  repoRelPath: norm,
@@ -38434,18 +38204,18 @@ async function buildWorkingTreeChangeRepoDetail(options) {
38434
38204
  remoteDisplay,
38435
38205
  files,
38436
38206
  hasUncommittedChanges,
38437
- unpushedCommits,
38438
- recentCommits: recentCommitList.commits,
38439
- recentCommitsHasMore: recentCommitList.hasMore,
38440
- changesView: options.basis.kind === "commit" ? "commit" : "working",
38441
- changesCommitSha: options.basis.kind === "commit" ? options.basis.sha.trim() : null
38207
+ unpushedCommits: commitData?.unpushedCommits ?? [],
38208
+ recentCommits: commitData?.recentCommits ?? [],
38209
+ recentCommitsHasMore: commitData?.recentCommitsHasMore ?? false,
38210
+ changesView: isCommitView ? "commit" : "working",
38211
+ changesCommitSha: commitSha
38442
38212
  };
38443
38213
  }
38444
38214
 
38445
38215
  // src/git/changes/repo/get-working-tree-change-repo-details.ts
38446
38216
  async function getWorkingTreeChangeRepoDetails(options) {
38447
- const bridgeRoot = path34.resolve(getBridgeRoot());
38448
- const sessionWtRoot = options.sessionWorktreeRootPath ? path34.resolve(options.sessionWorktreeRootPath) : null;
38217
+ const bridgeRoot = path29.resolve(getBridgeRoot());
38218
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path29.resolve(options.sessionWorktreeRootPath) : null;
38449
38219
  const legacyNested = options.legacyRepoNestedSessionLayout === true;
38450
38220
  const out = [];
38451
38221
  const filter = options.repoFilterRelPath != null ? normRepoRel(options.repoFilterRelPath) : null;
@@ -38459,14 +38229,16 @@ async function getWorkingTreeChangeRepoDetails(options) {
38459
38229
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
38460
38230
  for (let i = 0; i < options.commitTargetPaths.length; i++) {
38461
38231
  await yieldToEventLoop2();
38232
+ const targetPath = options.commitTargetPaths[i];
38462
38233
  const detail = await buildWorkingTreeChangeRepoDetail({
38463
- targetPath: options.commitTargetPaths[i],
38234
+ targetPath,
38464
38235
  bridgeRoot,
38465
38236
  sessionWorktreeRootPath: sessionWtRoot,
38466
38237
  legacyRepoNestedSessionLayout: legacyNested,
38467
38238
  repoFilterRelPath: filter,
38468
38239
  basis,
38469
- recentCommitsLimit: options.recentCommitsLimit
38240
+ recentCommitsLimit: options.recentCommitsLimit,
38241
+ includeFilePatches: options.includeFilePatches
38470
38242
  });
38471
38243
  if (!detail) continue;
38472
38244
  out.push(detail);
@@ -38475,94 +38247,148 @@ async function getWorkingTreeChangeRepoDetails(options) {
38475
38247
  return out;
38476
38248
  }
38477
38249
 
38478
- // src/git/changes/status/working-tree-changed-path-count.ts
38479
- function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
38480
- const kindByPath = parseNameStatusLines(nameStatusLines);
38481
- const numByPath = parseNumstat(numstatLines);
38482
- const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
38483
- for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
38484
- paths.add(p);
38485
- }
38486
- return paths.size;
38487
- }
38488
-
38489
- // src/git/changes/count-working-tree-changed-files.ts
38490
- async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
38491
- return runGitTask(async () => {
38250
+ // src/git/changes/repo/get-working-tree-change-file-patch.ts
38251
+ import * as path30 from "node:path";
38252
+ async function getWorkingTreeChangeFilePatch(options) {
38253
+ const repoGitCwd = path30.resolve(options.repoGitCwd);
38254
+ if (!await isGitRepoDirectory(repoGitCwd)) {
38255
+ throw new Error("Not a git repository");
38256
+ }
38257
+ const normRel = options.repoRelPath === "." || options.repoRelPath === "" ? "." : options.repoRelPath;
38258
+ const pathInRepo = pathInRepoFromWorkspace(options.workspaceRelPath, normRel);
38259
+ const movedFromPathInRepo = options.change === "moved" && options.movedFromWorkspaceRelPath ? pathInRepoFromWorkspace(options.movedFromWorkspaceRelPath, normRel) : void 0;
38260
+ if (options.basis.kind === "commit") {
38261
+ const commitSha = options.basis.sha.trim();
38262
+ if (!commitSha) throw new Error("commit sha is required for commit file patch");
38492
38263
  const g = cliSimpleGit(repoGitCwd);
38493
- const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
38494
- g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
38495
- g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
38496
- g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
38497
- ]);
38498
- return workingTreeChangedPathCount(
38499
- String(nameStatusRaw).split("\n"),
38500
- String(numstatRaw).split("\n"),
38501
- String(untrackedRaw).split("\n")
38264
+ const parentSha = await parentForCommitDiff(g, commitSha);
38265
+ const range = `${parentSha}..${commitSha}`;
38266
+ const patchContent2 = await unifiedDiffForFileInRange(
38267
+ repoGitCwd,
38268
+ range,
38269
+ pathInRepo,
38270
+ options.change,
38271
+ movedFromPathInRepo
38502
38272
  );
38273
+ const isBinary = Boolean(patchContent2?.includes("Binary files"));
38274
+ const totalLines2 = await resolveCommitFileTotalLines({
38275
+ repoGitCwd,
38276
+ pathInRepo,
38277
+ change: options.change,
38278
+ commitSha,
38279
+ parentSha,
38280
+ isBinary
38281
+ });
38282
+ return { patchContent: patchContent2, totalLines: totalLines2 };
38283
+ }
38284
+ const filePath = path30.join(repoGitCwd, pathInRepo);
38285
+ const hydrateKind = options.change === "moved" ? "modified" : options.change;
38286
+ let patchContent = await unifiedDiffForFile(
38287
+ repoGitCwd,
38288
+ pathInRepo,
38289
+ options.change,
38290
+ movedFromPathInRepo
38291
+ );
38292
+ if (patchContent) {
38293
+ patchContent = await hydrateUnifiedPatchWithFileContext(
38294
+ patchContent,
38295
+ filePath,
38296
+ repoGitCwd,
38297
+ pathInRepo,
38298
+ hydrateKind
38299
+ );
38300
+ }
38301
+ const totalLines = await resolveWorkingTreeFileTotalLines({
38302
+ repoGitCwd,
38303
+ pathInRepo,
38304
+ change: options.change,
38305
+ isBinary: Boolean(patchContent?.includes("Binary files"))
38503
38306
  });
38307
+ return { patchContent, totalLines };
38504
38308
  }
38505
38309
 
38506
- // src/git/status/working-tree-status.ts
38507
- async function getRepoWorkingTreeStatus(repoDir) {
38508
- return runGitTask(async () => {
38509
- const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
38510
- const hasUncommittedChanges = uncommittedFileCount > 0;
38511
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
38512
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
38513
- });
38514
- }
38515
- async function aggregateSessionPathsWorkingTreeStatus(paths) {
38516
- let hasUncommittedChanges = false;
38517
- let hasUnpushedCommits = false;
38518
- let uncommittedFileCount = 0;
38519
- await forEachWithGitYield(paths, async (p) => {
38520
- const s = await getRepoWorkingTreeStatus(p);
38521
- uncommittedFileCount += s.uncommittedFileCount;
38522
- if (s.hasUncommittedChanges) hasUncommittedChanges = true;
38523
- if (s.hasUnpushedCommits) hasUnpushedCommits = true;
38310
+ // src/worktrees/manager/git/get-session-working-tree-change-details.ts
38311
+ async function getSessionWorkingTreeChangeDetails(cache2, sessionId, discover, opts) {
38312
+ const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
38313
+ return getWorkingTreeChangeRepoDetails({
38314
+ commitTargetPaths: targets,
38315
+ sessionWorktreeRootPath: cache2.getWorkingTreeRelRoot(sessionId),
38316
+ legacyRepoNestedSessionLayout: cache2.isLegacyNestedLayout(sessionId),
38317
+ repoFilterRelPath: opts?.repoRelPath?.trim() ? normRepoRel(opts.repoRelPath.trim()) : null,
38318
+ basis: opts?.basis,
38319
+ recentCommitsLimit: opts?.recentCommitsLimit,
38320
+ includeFilePatches: false
38524
38321
  });
38525
- return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
38526
38322
  }
38527
- async function pushAheadOfUpstreamForPaths(paths) {
38528
- await forEachWithGitYield(paths, async (p) => {
38529
- const g = cliSimpleGit(p);
38530
- const ahead = await commitsAheadOfRemoteTracking(p);
38531
- if (ahead <= 0) return;
38532
- await g.push();
38533
- });
38323
+
38324
+ // src/worktrees/manager/git/parse-working-tree-change-kind.ts
38325
+ function parseWorkingTreeChangeKind(value) {
38326
+ if (value === "added" || value === "removed" || value === "modified" || value === "moved") return value;
38327
+ return null;
38534
38328
  }
38535
38329
 
38536
- // src/git/branches/commit-and-push.ts
38537
- async function gitCommitAllIfDirty(repoDir, message, options) {
38538
- const g = cliSimpleGit(repoDir);
38539
- const st = await g.status();
38540
- if (!st.files?.length) return;
38541
- const branch = options.branch.trim();
38542
- if (!branch) {
38543
- throw new Error("Branch name is required");
38544
- }
38545
- const branches = await g.branchLocal();
38546
- const localNames = new Set(branches.all.map((b) => b.replace(/^\*\s*/, "").trim()));
38547
- if (!localNames.has(branch)) {
38548
- await g.checkoutLocalBranch(branch);
38330
+ // src/worktrees/manager/git/get-session-working-tree-change-file-patch.ts
38331
+ import * as path32 from "node:path";
38332
+
38333
+ // src/git/changes/repo/resolve-checkout-repo-rel-path.ts
38334
+ import * as path31 from "node:path";
38335
+ async function resolveCheckoutRepoRelPath(options) {
38336
+ const t = path31.resolve(options.checkoutPath);
38337
+ const g = cliSimpleGit(t);
38338
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path31.resolve(options.sessionWorktreeRootPath) : null;
38339
+ const legacyNested = options.legacyRepoNestedSessionLayout;
38340
+ let repoRelPath;
38341
+ if (sessionWtRoot) {
38342
+ const anchor = legacyNested ? path31.dirname(t) : t;
38343
+ const relNorm = path31.relative(sessionWtRoot, anchor);
38344
+ repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
38549
38345
  } else {
38550
- await g.checkout(branch);
38346
+ let top = t;
38347
+ try {
38348
+ await yieldToEventLoop2();
38349
+ top = (await g.raw(["rev-parse", "--show-toplevel"])).trim();
38350
+ } catch {
38351
+ top = t;
38352
+ }
38353
+ const rel = path31.relative(options.bridgeRoot, path31.resolve(top)).replace(/\\/g, "/") || ".";
38354
+ repoRelPath = rel.startsWith("..") ? path31.basename(path31.resolve(top)) : rel;
38551
38355
  }
38552
- await g.add(".");
38553
- await g.commit(message);
38554
- if (options.push) {
38555
- await g.push(["-u", "origin", branch]);
38356
+ return normRepoRel(repoRelPath === "" ? "." : repoRelPath);
38357
+ }
38358
+
38359
+ // src/worktrees/manager/git/get-session-working-tree-change-file-patch.ts
38360
+ async function getSessionWorkingTreeChangeFilePatch(cache2, sessionId, discover, opts) {
38361
+ const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
38362
+ const bridgeRoot = path32.resolve(getBridgeRoot());
38363
+ const sessionWtRoot = cache2.getWorkingTreeRelRoot(sessionId) ? path32.resolve(cache2.getWorkingTreeRelRoot(sessionId)) : null;
38364
+ const legacyNested = cache2.isLegacyNestedLayout(sessionId);
38365
+ const filter = normRepoRel(opts.repoRelPath.trim());
38366
+ for (const targetPath of targets) {
38367
+ const checkoutPath = path32.resolve(targetPath);
38368
+ if (!await isGitRepoDirectory(checkoutPath)) continue;
38369
+ const repoRelPath = await resolveCheckoutRepoRelPath({
38370
+ checkoutPath,
38371
+ bridgeRoot,
38372
+ sessionWorktreeRootPath: sessionWtRoot,
38373
+ legacyRepoNestedSessionLayout: legacyNested
38374
+ });
38375
+ if (repoRelPath !== filter) continue;
38376
+ return getWorkingTreeChangeFilePatch({
38377
+ repoGitCwd: checkoutPath,
38378
+ repoRelPath: filter,
38379
+ workspaceRelPath: opts.workspaceRelPath.trim(),
38380
+ change: opts.change,
38381
+ movedFromWorkspaceRelPath: opts.movedFromWorkspaceRelPath?.trim() || null,
38382
+ basis: opts.basis
38383
+ });
38556
38384
  }
38385
+ throw new Error(`Repository not found: ${filter}`);
38557
38386
  }
38558
38387
 
38559
- // src/worktrees/commit-session-worktrees.ts
38560
- async function commitSessionWorktrees(options) {
38561
- const { paths, branch, message, push } = options;
38388
+ // src/worktrees/manager/git/push-session-upstream.ts
38389
+ async function pushSessionUpstream(cache2, sessionId, discover) {
38562
38390
  try {
38563
- for (const wt of paths) {
38564
- await gitCommitAllIfDirty(wt, message, { push, branch });
38565
- }
38391
+ await pushAheadOfUpstreamForPaths(await resolveCommitTargetsAsync(sessionId, cache2, discover));
38566
38392
  return { ok: true };
38567
38393
  } catch (e) {
38568
38394
  const err = e instanceof Error ? e.message : String(e);
@@ -38571,23 +38397,23 @@ async function commitSessionWorktrees(options) {
38571
38397
  }
38572
38398
 
38573
38399
  // src/worktrees/remove-session-worktrees.ts
38574
- import * as fs29 from "node:fs";
38400
+ import * as fs23 from "node:fs";
38575
38401
 
38576
38402
  // src/git/worktrees/worktree-remove.ts
38577
- import * as fs28 from "node:fs";
38403
+ import * as fs22 from "node:fs";
38578
38404
 
38579
38405
  // src/git/worktrees/resolve-main-repo-from-git-file.ts
38580
- import * as fs27 from "node:fs";
38581
- import * as path35 from "node:path";
38406
+ import * as fs21 from "node:fs";
38407
+ import * as path33 from "node:path";
38582
38408
  function resolveMainRepoFromWorktreeGitFile(wt) {
38583
- const gitDirFile = path35.join(wt, ".git");
38584
- if (!fs27.existsSync(gitDirFile) || !fs27.statSync(gitDirFile).isFile()) return "";
38585
- const first2 = fs27.readFileSync(gitDirFile, "utf8").trim();
38409
+ const gitDirFile = path33.join(wt, ".git");
38410
+ if (!fs21.existsSync(gitDirFile) || !fs21.statSync(gitDirFile).isFile()) return "";
38411
+ const first2 = fs21.readFileSync(gitDirFile, "utf8").trim();
38586
38412
  const m = first2.match(/^gitdir:\s*(.+)$/im);
38587
38413
  if (!m) return "";
38588
- const gitWorktreePath = path35.resolve(wt, m[1].trim());
38589
- const gitDir = path35.dirname(path35.dirname(gitWorktreePath));
38590
- return path35.dirname(gitDir);
38414
+ const gitWorktreePath = path33.resolve(wt, m[1].trim());
38415
+ const gitDir = path33.dirname(path33.dirname(gitWorktreePath));
38416
+ return path33.dirname(gitDir);
38591
38417
  }
38592
38418
 
38593
38419
  // src/git/worktrees/worktree-remove.ts
@@ -38596,7 +38422,7 @@ async function gitWorktreeRemoveForce(worktreePath) {
38596
38422
  if (mainRepo) {
38597
38423
  await cliSimpleGit(mainRepo).raw(["worktree", "remove", "--force", worktreePath]);
38598
38424
  } else {
38599
- fs28.rmSync(worktreePath, { recursive: true, force: true });
38425
+ fs22.rmSync(worktreePath, { recursive: true, force: true });
38600
38426
  }
38601
38427
  }
38602
38428
 
@@ -38609,13 +38435,20 @@ async function removeSessionWorktrees(paths, log2) {
38609
38435
  } catch (e) {
38610
38436
  log2(`[worktrees] Remove failed for ${wt}: ${e instanceof Error ? e.message : String(e)}`);
38611
38437
  try {
38612
- fs29.rmSync(wt, { recursive: true, force: true });
38438
+ fs23.rmSync(wt, { recursive: true, force: true });
38613
38439
  } catch {
38614
38440
  }
38615
38441
  }
38616
38442
  }
38617
38443
  }
38618
38444
 
38445
+ // src/worktrees/manager/git/remove-session-worktree-checkouts.ts
38446
+ async function removeSessionWorktreeCheckouts(cache2, sessionId, log2) {
38447
+ const paths = cache2.clearSession(sessionId);
38448
+ if (!paths?.length) return;
38449
+ await removeSessionWorktrees(paths, log2);
38450
+ }
38451
+
38619
38452
  // src/git/branches/rename-branch.ts
38620
38453
  async function gitRenameCurrentBranch(repoDir, newName) {
38621
38454
  const g = cliSimpleGit(repoDir);
@@ -38637,216 +38470,623 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
38637
38470
  }
38638
38471
  }
38639
38472
 
38640
- // src/worktrees/manager/resolve-commit-targets.ts
38641
- function bridgeRootBinding(bridgeRoot) {
38642
- return {
38643
- sessionParentPath: bridgeRoot,
38644
- workingTreeRelRoot: bridgeRoot,
38645
- repoCheckoutPaths: [bridgeRoot]
38646
- };
38647
- }
38648
- async function resolveCommitTargetsAsync(sessionId, cache2, discover) {
38649
- const sid = sessionId.trim();
38650
- const paths = cache2.getRepoCheckoutPathsRef(sid);
38651
- if (paths?.length) return paths;
38652
- const disc = await discover(sid);
38653
- if (disc?.repoCheckoutPaths.length) {
38654
- cache2.remember(sid, disc);
38655
- return disc.repoCheckoutPaths;
38656
- }
38657
- const bridgeRoot = getBridgeRoot();
38658
- cache2.remember(sid, bridgeRootBinding(bridgeRoot));
38659
- return [bridgeRoot];
38473
+ // src/worktrees/manager/git/rename-session-worktree-branch.ts
38474
+ async function renameSessionWorktreeBranch(cache2, sessionId, newBranch, log2) {
38475
+ const paths = cache2.getRepoCheckoutPathsRef(sessionId);
38476
+ if (!paths?.length) return;
38477
+ await renameSessionWorktreeBranches(paths, newBranch, log2);
38660
38478
  }
38661
38479
 
38662
- // src/worktrees/manager/session-git-ops.ts
38663
- async function commitSessionWorktree(cache2, params) {
38664
- const paths = cache2.getRepoCheckoutPathsRef(params.sessionId);
38665
- const targets = paths?.length ? paths : [getBridgeRoot()];
38666
- return commitSessionWorktrees({
38667
- paths: targets,
38668
- branch: params.branch,
38669
- message: params.message,
38670
- push: params.push
38671
- });
38672
- }
38673
- async function getSessionWorkingTreeStatus(cache2, sessionId, discover) {
38674
- return aggregateSessionPathsWorkingTreeStatus(
38675
- await resolveCommitTargetsAsync(sessionId, cache2, discover)
38676
- );
38480
+ // src/worktrees/discovery/discover-session-worktree-on-disk.ts
38481
+ import * as fs27 from "node:fs";
38482
+ import * as path38 from "node:path";
38483
+
38484
+ // src/worktrees/discovery/collect-worktree-paths.ts
38485
+ import * as fs25 from "node:fs";
38486
+ import * as path35 from "node:path";
38487
+
38488
+ // src/worktrees/discovery/disk-walk-constants.ts
38489
+ var DISK_WALK_YIELD_EVERY = 64;
38490
+ var LEGACY_WALK_MAX_DEPTH = 12;
38491
+ var SKIP_DISK_WALK_DIR_NAMES = /* @__PURE__ */ new Set([
38492
+ "node_modules",
38493
+ "bower_components",
38494
+ "vendor",
38495
+ "Pods",
38496
+ "Carthage",
38497
+ "DerivedData",
38498
+ ".yarn",
38499
+ ".pnpm-store",
38500
+ "dist",
38501
+ "build",
38502
+ ".next",
38503
+ "coverage",
38504
+ ".turbo",
38505
+ ".cache",
38506
+ "target",
38507
+ "__pycache__"
38508
+ ]);
38509
+ function shouldSkipDiskWalkEntry(name) {
38510
+ return name.startsWith(".") || SKIP_DISK_WALK_DIR_NAMES.has(name);
38677
38511
  }
38678
- async function getSessionWorkingTreeChangeDetails(cache2, sessionId, discover, opts) {
38679
- const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
38680
- return getWorkingTreeChangeRepoDetails({
38681
- commitTargetPaths: targets,
38682
- sessionWorktreeRootPath: cache2.getWorkingTreeRelRoot(sessionId),
38683
- legacyRepoNestedSessionLayout: cache2.isLegacyNestedLayout(sessionId),
38684
- repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
38685
- basis: opts?.basis,
38686
- recentCommitsLimit: opts?.recentCommitsLimit
38687
- });
38512
+
38513
+ // src/worktrees/discovery/disk-walk-utils.ts
38514
+ import * as fs24 from "node:fs";
38515
+ import * as path34 from "node:path";
38516
+ async function yieldDuringDiskWalk(state) {
38517
+ state.entries++;
38518
+ if (state.entries % DISK_WALK_YIELD_EVERY !== 0) return true;
38519
+ if (isCliImmediateShutdownRequested()) return false;
38520
+ await yieldToEventLoop();
38521
+ return !isCliImmediateShutdownRequested();
38688
38522
  }
38689
- async function pushSessionUpstream(cache2, sessionId, discover) {
38523
+ async function isGitDir(dirPath) {
38690
38524
  try {
38691
- await pushAheadOfUpstreamForPaths(await resolveCommitTargetsAsync(sessionId, cache2, discover));
38692
- return { ok: true };
38693
- } catch (e) {
38694
- const err = e instanceof Error ? e.message : String(e);
38695
- return { ok: false, error: err };
38525
+ await fs24.promises.access(path34.join(dirPath, ".git"));
38526
+ return true;
38527
+ } catch {
38528
+ return false;
38696
38529
  }
38697
38530
  }
38698
- async function removeSessionWorktreeCheckouts(cache2, sessionId, log2) {
38699
- const paths = cache2.clearSession(sessionId);
38700
- if (!paths?.length) return;
38701
- await removeSessionWorktrees(paths, log2);
38531
+
38532
+ // src/worktrees/discovery/collect-worktree-paths.ts
38533
+ async function collectGitRepoRootsUnderDirectory(rootPath) {
38534
+ const out = [];
38535
+ const state = { entries: 0 };
38536
+ const walk = async (dir) => {
38537
+ if (!await yieldDuringDiskWalk(state)) return;
38538
+ if (await isGitDir(dir)) {
38539
+ out.push(path35.resolve(dir));
38540
+ return;
38541
+ }
38542
+ let entries;
38543
+ try {
38544
+ entries = await fs25.promises.readdir(dir, { withFileTypes: true });
38545
+ } catch {
38546
+ return;
38547
+ }
38548
+ for (const e of entries) {
38549
+ if (shouldSkipDiskWalkEntry(e.name)) continue;
38550
+ const full = path35.join(dir, e.name);
38551
+ if (!e.isDirectory()) continue;
38552
+ await walk(full);
38553
+ }
38554
+ };
38555
+ await walk(path35.resolve(rootPath));
38556
+ return { paths: [...new Set(out)], entriesVisited: state.entries };
38702
38557
  }
38703
- async function renameSessionWorktreeBranch(cache2, sessionId, newBranch, log2) {
38704
- const paths = cache2.getRepoCheckoutPathsRef(sessionId);
38705
- if (!paths?.length) return;
38706
- await renameSessionWorktreeBranches(paths, newBranch, log2);
38558
+ async function collectWorktreeRootsNamed(root, sessionId, maxDepth = LEGACY_WALK_MAX_DEPTH) {
38559
+ const out = [];
38560
+ const state = { entries: 0 };
38561
+ const walk = async (dir, depth) => {
38562
+ if (depth > maxDepth) return;
38563
+ if (!await yieldDuringDiskWalk(state)) return;
38564
+ let entries;
38565
+ try {
38566
+ entries = await fs25.promises.readdir(dir, { withFileTypes: true });
38567
+ } catch {
38568
+ return;
38569
+ }
38570
+ for (const e of entries) {
38571
+ if (shouldSkipDiskWalkEntry(e.name)) continue;
38572
+ const full = path35.join(dir, e.name);
38573
+ if (!e.isDirectory()) continue;
38574
+ if (e.name === sessionId) {
38575
+ if (await isGitDir(full)) out.push(path35.resolve(full));
38576
+ continue;
38577
+ }
38578
+ if (await isGitDir(full)) continue;
38579
+ await walk(full, depth + 1);
38580
+ }
38581
+ };
38582
+ await walk(root, 0);
38583
+ return { paths: out, entriesVisited: state.entries };
38707
38584
  }
38708
38585
 
38709
- // src/worktrees/manager/session-worktree-cache.ts
38586
+ // src/worktrees/discovery/layout-keys-for-bridge-root.ts
38710
38587
  import * as path36 from "node:path";
38711
- var SessionWorktreeCache = class {
38712
- sessionRepoCheckoutPaths = /* @__PURE__ */ new Map();
38713
- sessionParentPathBySession = /* @__PURE__ */ new Map();
38714
- sessionWorkingTreeRelRootBySession = /* @__PURE__ */ new Map();
38715
- remember(sessionId, binding) {
38716
- const paths = binding.repoCheckoutPaths.map((p) => path36.resolve(p));
38717
- this.sessionRepoCheckoutPaths.set(sessionId, paths);
38718
- this.sessionParentPathBySession.set(sessionId, path36.resolve(binding.sessionParentPath));
38719
- this.sessionWorkingTreeRelRootBySession.set(sessionId, path36.resolve(binding.workingTreeRelRoot));
38720
- }
38721
- clearSession(sessionId) {
38722
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
38723
- this.sessionRepoCheckoutPaths.delete(sessionId);
38724
- this.sessionParentPathBySession.delete(sessionId);
38725
- this.sessionWorkingTreeRelRootBySession.delete(sessionId);
38726
- return paths;
38727
- }
38728
- getSessionParentPath(sessionId) {
38729
- return this.sessionParentPathBySession.get(sessionId);
38730
- }
38731
- getWorkingTreeRelRoot(sessionId) {
38732
- return this.sessionWorkingTreeRelRootBySession.get(sessionId) ?? null;
38588
+ function layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk) {
38589
+ const bridge = path36.resolve(bridgeRoot);
38590
+ const preferred = getLauncherDirNameIfPresent(layout, bridgeRoot);
38591
+ const relevant = layout.launcherCwds.filter((e) => {
38592
+ const entry = path36.resolve(e.absolutePath);
38593
+ return bridge === entry || bridge.startsWith(entry + path36.sep) || entry.startsWith(bridge + path36.sep);
38594
+ }).sort((a, b) => path36.resolve(b.absolutePath).length - path36.resolve(a.absolutePath).length).map((e) => e.dirName).filter((name) => keysOnDisk.includes(name));
38595
+ const ordered = [];
38596
+ const seen = /* @__PURE__ */ new Set();
38597
+ const add = (k) => {
38598
+ if (seen.has(k) || !keysOnDisk.includes(k)) return;
38599
+ seen.add(k);
38600
+ ordered.push(k);
38601
+ };
38602
+ if (preferred) add(preferred);
38603
+ for (const k of relevant) add(k);
38604
+ if (relevant.length > 0) {
38605
+ return ordered;
38733
38606
  }
38734
- hasSession(sessionId) {
38735
- return this.sessionParentPathBySession.has(sessionId);
38607
+ for (const k of keysOnDisk) add(k);
38608
+ return ordered;
38609
+ }
38610
+
38611
+ // src/worktrees/discovery/try-binding-from-session-directory.ts
38612
+ import * as fs26 from "node:fs";
38613
+ import * as path37 from "node:path";
38614
+ async function tryBindingFromSessionDirectory(sessionDir) {
38615
+ let st;
38616
+ try {
38617
+ st = await fs26.promises.stat(sessionDir);
38618
+ } catch {
38619
+ return null;
38736
38620
  }
38737
- getRepoCheckoutPaths(sessionId) {
38738
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
38739
- return paths?.length ? [...paths] : void 0;
38621
+ if (!st.isDirectory()) return null;
38622
+ const { paths: worktreePaths } = await collectGitRepoRootsUnderDirectory(sessionDir);
38623
+ if (worktreePaths.length === 0) return null;
38624
+ const abs = path37.resolve(sessionDir);
38625
+ return {
38626
+ sessionParentPath: abs,
38627
+ workingTreeRelRoot: abs,
38628
+ repoCheckoutPaths: worktreePaths
38629
+ };
38630
+ }
38631
+
38632
+ // src/worktrees/discovery/discover-session-worktree-on-disk.ts
38633
+ async function discoverSessionWorktreeOnDisk(options) {
38634
+ const { sessionId, worktreesRootPath, layout, bridgeRoot } = options;
38635
+ const sid = sessionId.trim();
38636
+ if (!sid) return null;
38637
+ try {
38638
+ await fs27.promises.access(worktreesRootPath);
38639
+ } catch {
38640
+ return null;
38740
38641
  }
38741
- getRepoCheckoutPathsRef(sessionId) {
38742
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
38743
- return paths?.length ? paths : void 0;
38642
+ const preferredKey = getLauncherDirNameIfPresent(layout, bridgeRoot);
38643
+ const keysOnDisk = [];
38644
+ if (preferredKey) keysOnDisk.push(preferredKey);
38645
+ try {
38646
+ for (const name of await fs27.promises.readdir(worktreesRootPath)) {
38647
+ if (name.startsWith(".")) continue;
38648
+ const p = path38.join(worktreesRootPath, name);
38649
+ let st;
38650
+ try {
38651
+ st = await fs27.promises.stat(p);
38652
+ } catch {
38653
+ continue;
38654
+ }
38655
+ if (!st.isDirectory()) continue;
38656
+ if (!keysOnDisk.includes(name)) keysOnDisk.push(name);
38657
+ }
38658
+ } catch {
38659
+ return null;
38744
38660
  }
38745
- isLegacyNestedLayout(sessionId) {
38746
- const parent = this.sessionParentPathBySession.get(sessionId);
38747
- const relRoot = this.sessionWorkingTreeRelRootBySession.get(sessionId);
38748
- if (!parent || !relRoot) return false;
38749
- return path36.resolve(parent) !== path36.resolve(relRoot);
38661
+ const keys = layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk);
38662
+ for (const key of keys) {
38663
+ if (isCliImmediateShutdownRequested()) return null;
38664
+ await yieldToEventLoop();
38665
+ const layoutRoot = path38.join(worktreesRootPath, key);
38666
+ let layoutSt;
38667
+ try {
38668
+ layoutSt = await fs27.promises.stat(layoutRoot);
38669
+ } catch {
38670
+ continue;
38671
+ }
38672
+ if (!layoutSt.isDirectory()) continue;
38673
+ const sessionDir = path38.join(layoutRoot, sid);
38674
+ const nested = await tryBindingFromSessionDirectory(sessionDir);
38675
+ if (nested) return nested;
38676
+ const legacy = await collectWorktreeRootsNamed(layoutRoot, sid);
38677
+ if (legacy.paths.length > 0) {
38678
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacy.paths) ?? path38.resolve(legacy.paths[0]);
38679
+ return {
38680
+ sessionParentPath: path38.resolve(isolated),
38681
+ workingTreeRelRoot: path38.resolve(layoutRoot),
38682
+ repoCheckoutPaths: legacy.paths.map((p) => path38.resolve(p))
38683
+ };
38684
+ }
38750
38685
  }
38751
- };
38686
+ return null;
38687
+ }
38752
38688
 
38753
- // src/worktrees/manager/manager.ts
38754
- var SessionWorktreeManager = class {
38755
- worktreesRootPath;
38756
- log;
38757
- cache = new SessionWorktreeCache();
38758
- layout;
38759
- constructor(options) {
38760
- this.worktreesRootPath = options.worktreesRootPath;
38761
- this.log = options.log;
38762
- this.layout = loadWorktreeLayout();
38689
+ // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
38690
+ import * as fs28 from "node:fs";
38691
+ import * as path40 from "node:path";
38692
+
38693
+ // src/worktrees/discovery/discover-legacy-binding-ascending-from-checkout.ts
38694
+ import * as path39 from "node:path";
38695
+ async function discoverLegacyBindingAscendingFromCheckout(sessionId, checkoutPath) {
38696
+ const sid = sessionId.trim();
38697
+ if (!sid) return null;
38698
+ const hintR = path39.resolve(checkoutPath);
38699
+ let best = null;
38700
+ let cur = path39.dirname(hintR);
38701
+ for (let i = 0; i < 40; i++) {
38702
+ if (isCliImmediateShutdownRequested()) return best;
38703
+ await yieldToEventLoop();
38704
+ const paths = await collectWorktreeRootsNamed(cur, sid);
38705
+ if (paths.paths.some((p) => path39.resolve(p) === hintR)) {
38706
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(paths.paths) ?? path39.resolve(paths.paths[0]);
38707
+ best = {
38708
+ sessionParentPath: path39.resolve(isolated),
38709
+ workingTreeRelRoot: path39.resolve(cur),
38710
+ repoCheckoutPaths: paths.paths.map((p) => path39.resolve(p))
38711
+ };
38712
+ }
38713
+ const next = path39.dirname(cur);
38714
+ if (next === cur) break;
38715
+ cur = next;
38763
38716
  }
38764
- discoverAsync(sessionId) {
38765
- return discoverSessionBindingAsync({
38766
- sessionId,
38767
- worktreesRootPath: this.worktreesRootPath,
38768
- layout: this.layout
38769
- });
38717
+ return best;
38718
+ }
38719
+
38720
+ // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
38721
+ async function discoverSessionWorktreesUnderSessionWorktreeRoot(sessionWorktreeRootPathOrHint, sessionId) {
38722
+ const sid = sessionId.trim();
38723
+ if (!sid) return null;
38724
+ const hint = path40.resolve(sessionWorktreeRootPathOrHint);
38725
+ const underHint = await tryBindingFromSessionDirectory(path40.join(hint, sid));
38726
+ if (underHint) return underHint;
38727
+ const direct = await tryBindingFromSessionDirectory(hint);
38728
+ if (direct) {
38729
+ if (path40.basename(hint) === sid && await isGitDir(hint)) {
38730
+ const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
38731
+ if (legacyFromCheckout && legacyFromCheckout.repoCheckoutPaths.length > direct.repoCheckoutPaths.length) {
38732
+ return legacyFromCheckout;
38733
+ }
38734
+ }
38735
+ return direct;
38770
38736
  }
38771
- getIsolatedSessionParentPathForSession(sessionId) {
38772
- return resolveIsolatedSessionParentPath(
38773
- sessionId,
38774
- this.cache,
38775
- (sid) => this.ensureRepoCheckoutPathsForSession(sid)
38776
- );
38737
+ if (path40.basename(hint) === sid && await isGitDir(hint)) {
38738
+ const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
38739
+ if (legacyFromCheckout) return legacyFromCheckout;
38777
38740
  }
38778
- resolveSessionParentPathForPrompt(sessionId, opts) {
38779
- return resolveSessionParentPathForPrompt({
38780
- sessionId,
38781
- cache: this.cache,
38782
- worktreesRootPath: this.worktreesRootPath,
38783
- layout: this.layout,
38784
- log: this.log,
38785
- discover: (sid) => this.discoverAsync(sid),
38786
- opts
38787
- });
38741
+ let st;
38742
+ try {
38743
+ st = await fs28.promises.stat(hint);
38744
+ } catch {
38745
+ return null;
38788
38746
  }
38789
- async renameSessionBranch(sessionId, newBranch) {
38790
- await renameSessionWorktreeBranch(this.cache, sessionId, newBranch, this.log);
38747
+ if (!st.isDirectory()) return null;
38748
+ const legacyPaths = await collectWorktreeRootsNamed(hint, sid);
38749
+ if (legacyPaths.paths.length === 0) return null;
38750
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths.paths) ?? path40.resolve(legacyPaths.paths[0]);
38751
+ return {
38752
+ sessionParentPath: path40.resolve(isolated),
38753
+ workingTreeRelRoot: hint,
38754
+ repoCheckoutPaths: legacyPaths.paths.map((p) => path40.resolve(p))
38755
+ };
38756
+ }
38757
+
38758
+ // src/worktrees/manager/discover-session-binding.ts
38759
+ var discoverInflightBySessionId = /* @__PURE__ */ new Map();
38760
+ function discoverSessionBindingAsync(params) {
38761
+ const sid = params.sessionId.trim();
38762
+ if (!sid) return Promise.resolve(null);
38763
+ const existing = discoverInflightBySessionId.get(sid);
38764
+ if (existing) return existing;
38765
+ const promise2 = discoverSessionWorktreeOnDisk({
38766
+ sessionId: sid,
38767
+ worktreesRootPath: params.worktreesRootPath,
38768
+ layout: params.layout,
38769
+ bridgeRoot: getBridgeRoot()
38770
+ }).finally(() => {
38771
+ if (discoverInflightBySessionId.get(sid) === promise2) {
38772
+ discoverInflightBySessionId.delete(sid);
38773
+ }
38774
+ });
38775
+ discoverInflightBySessionId.set(sid, promise2);
38776
+ return promise2;
38777
+ }
38778
+
38779
+ // src/worktrees/manager/resolve-isolated-session-parent-path.ts
38780
+ function resolveIsolatedSessionParentPath(sessionId, cache2, ensureRepoCheckoutPaths) {
38781
+ if (!sessionId) return null;
38782
+ const sid = sessionId.trim();
38783
+ const cached2 = cache2.getSessionParentPath(sid);
38784
+ if (cached2) return cached2;
38785
+ const paths = ensureRepoCheckoutPaths(sid);
38786
+ if (!paths?.length) return null;
38787
+ return resolveIsolatedSessionParentPathFromCheckouts(paths);
38788
+ }
38789
+ function ensureRepoCheckoutPathsForSession(sessionId, cache2) {
38790
+ if (!sessionId?.trim()) return void 0;
38791
+ const cached2 = cache2.getRepoCheckoutPaths(sessionId.trim());
38792
+ return cached2?.length ? [...cached2] : void 0;
38793
+ }
38794
+ async function ensureRepoCheckoutPathsForSessionAsync(sessionId, cache2, discover) {
38795
+ if (!sessionId?.trim()) return void 0;
38796
+ const sid = sessionId.trim();
38797
+ const cached2 = cache2.getRepoCheckoutPaths(sid);
38798
+ if (cached2?.length) return cached2;
38799
+ const disc = await discover(sid);
38800
+ if (disc?.repoCheckoutPaths.length) {
38801
+ cache2.remember(sid, disc);
38802
+ return [...disc.repoCheckoutPaths];
38791
38803
  }
38792
- usesWorktreeSession(sessionId) {
38793
- if (!sessionId) return false;
38794
- return this.cache.hasSession(sessionId);
38804
+ return void 0;
38805
+ }
38806
+
38807
+ // src/worktrees/manager/parse-session-parent.ts
38808
+ function parseSessionParent(v) {
38809
+ if (v === "bridge_root" || v === "worktrees_root") return v;
38810
+ if (v === "session_worktrees_root") return "worktrees_root";
38811
+ return null;
38812
+ }
38813
+
38814
+ // src/worktrees/prepare-new-session-worktrees.ts
38815
+ import * as fs29 from "node:fs";
38816
+ import * as path41 from "node:path";
38817
+
38818
+ // src/git/worktrees/worktree-add.ts
38819
+ async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch, baseRef = "HEAD") {
38820
+ const mainGit = cliSimpleGit(mainRepoPath);
38821
+ const base = baseRef.trim() || "HEAD";
38822
+ await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, base]);
38823
+ }
38824
+
38825
+ // src/worktrees/prepare-new-session-worktrees.ts
38826
+ function normalizeRepoRelPath2(rel) {
38827
+ return rel === "" ? "." : rel.replace(/\\/g, "/");
38828
+ }
38829
+ function resolveBaseRefForRepo(relNorm, baseBranches) {
38830
+ if (!baseBranches) return "HEAD";
38831
+ const direct = baseBranches[relNorm]?.trim();
38832
+ if (direct) return direct;
38833
+ if (relNorm !== "." && baseBranches["."]?.trim()) return baseBranches["."].trim();
38834
+ return "HEAD";
38835
+ }
38836
+ async function prepareNewSessionWorktrees(options) {
38837
+ const { worktreesRootPath, bridgeRoot, sessionId, layout, log: log2, worktreeBaseBranches } = options;
38838
+ const bridgeResolved = path41.resolve(bridgeRoot);
38839
+ const cwdKey = allocateDirNameForLauncherCwd(layout, bridgeResolved);
38840
+ const bridgeKeyDir = path41.join(worktreesRootPath, cwdKey);
38841
+ const sessionDir = path41.join(bridgeKeyDir, sessionId);
38842
+ const repos = await discoverGitReposUnderRoot(bridgeResolved);
38843
+ if (repos.length === 0) {
38844
+ log2("[worktrees] No Git repositories under bridge root; skipping worktree creation.");
38845
+ return null;
38795
38846
  }
38796
- getRepoCheckoutPathsForSession(sessionId) {
38797
- if (!sessionId) return void 0;
38798
- return this.cache.getRepoCheckoutPaths(sessionId);
38847
+ const branch = `session-${sessionId}`;
38848
+ const worktreePaths = [];
38849
+ fs29.mkdirSync(sessionDir, { recursive: true });
38850
+ for (const repo of repos) {
38851
+ let rel = path41.relative(bridgeResolved, repo.absolutePath);
38852
+ if (rel.startsWith("..") || path41.isAbsolute(rel)) continue;
38853
+ const relNorm = normalizeRepoRelPath2(rel === "" ? "." : rel);
38854
+ const wtPath = relNorm === "." ? sessionDir : path41.join(sessionDir, relNorm);
38855
+ if (relNorm !== ".") {
38856
+ fs29.mkdirSync(path41.dirname(wtPath), { recursive: true });
38857
+ }
38858
+ const baseRef = resolveBaseRefForRepo(relNorm, worktreeBaseBranches);
38859
+ try {
38860
+ await gitWorktreeAddBranch(repo.absolutePath, wtPath, branch, baseRef);
38861
+ log2(`[worktrees] Added worktree ${wtPath} (branch ${branch}, base ${baseRef}).`);
38862
+ worktreePaths.push(wtPath);
38863
+ } catch (e) {
38864
+ log2(
38865
+ `[worktrees] Worktree add failed for ${repo.absolutePath}: ${e instanceof Error ? e.message : String(e)}`
38866
+ );
38867
+ }
38799
38868
  }
38800
- ensureRepoCheckoutPathsForSession(sessionId) {
38801
- return ensureRepoCheckoutPathsForSession(sessionId, this.cache);
38869
+ if (worktreePaths.length === 0) return null;
38870
+ return {
38871
+ worktreePaths,
38872
+ sessionParentPath: sessionDir,
38873
+ workingTreeRelRoot: sessionDir
38874
+ };
38875
+ }
38876
+
38877
+ // src/worktrees/manager/prepare-and-remember-session-worktrees.ts
38878
+ async function prepareAndRememberSessionWorktrees(params) {
38879
+ const prep = await prepareNewSessionWorktrees({
38880
+ worktreesRootPath: params.worktreesRootPath,
38881
+ bridgeRoot: getBridgeRoot(),
38882
+ sessionId: params.sessionId,
38883
+ layout: params.layout,
38884
+ log: params.log,
38885
+ ...params.worktreeBaseBranches && Object.keys(params.worktreeBaseBranches).length > 0 ? { worktreeBaseBranches: params.worktreeBaseBranches } : {}
38886
+ });
38887
+ if (!prep) return void 0;
38888
+ params.cache.remember(params.sessionId, {
38889
+ sessionParentPath: prep.sessionParentPath,
38890
+ workingTreeRelRoot: prep.workingTreeRelRoot,
38891
+ repoCheckoutPaths: prep.worktreePaths
38892
+ });
38893
+ return params.cache.getSessionParentPath(params.sessionId);
38894
+ }
38895
+
38896
+ // src/worktrees/manager/resolve-existing-session-parent-path.ts
38897
+ async function resolveExistingSessionParentPath(sessionId, cache2, discover) {
38898
+ const cached2 = cache2.getSessionParentPath(sessionId);
38899
+ if (cached2) return cached2;
38900
+ const disc = await discover();
38901
+ if (disc) {
38902
+ cache2.remember(sessionId, disc);
38903
+ return cache2.getSessionParentPath(sessionId);
38802
38904
  }
38803
- async ensureRepoCheckoutPathsForSessionAsync(sessionId) {
38804
- return ensureRepoCheckoutPathsForSessionAsync(sessionId, this.cache, (sid) => this.discoverAsync(sid));
38905
+ return void 0;
38906
+ }
38907
+
38908
+ // src/worktrees/manager/resolve-explicit-session-parent-path.ts
38909
+ import * as path42 from "node:path";
38910
+ async function resolveExplicitSessionParentPath(params) {
38911
+ const resolved = path42.resolve(params.parentPathRaw);
38912
+ if (parseSessionParent(params.sessionParent) !== "worktrees_root") {
38913
+ return resolved;
38805
38914
  }
38806
- getSessionWorktreeRootForSession(sessionId) {
38807
- return this.getIsolatedSessionParentPathForSession(sessionId);
38915
+ const rememberAndReturn = (binding) => {
38916
+ params.cache.remember(params.sessionId, binding);
38917
+ return params.cache.getSessionParentPath(params.sessionId) ?? resolved;
38918
+ };
38919
+ const diskFirst = await params.discover();
38920
+ if (diskFirst) return rememberAndReturn(diskFirst);
38921
+ const fromRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(resolved, params.sessionId);
38922
+ if (fromRoot) return rememberAndReturn(fromRoot);
38923
+ let cur = resolved;
38924
+ for (let i = 0; i < 16; i++) {
38925
+ await yieldToEventLoop();
38926
+ const tryRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(cur, params.sessionId);
38927
+ if (tryRoot) return rememberAndReturn(tryRoot);
38928
+ const next = path42.dirname(cur);
38929
+ if (next === cur) break;
38930
+ cur = next;
38808
38931
  }
38809
- async removeSessionWorktrees(sessionId) {
38810
- await removeSessionWorktreeCheckouts(this.cache, sessionId, this.log);
38932
+ return resolved;
38933
+ }
38934
+
38935
+ // src/worktrees/manager/resolve-session-parent-path-for-prompt.ts
38936
+ async function resolveSessionParentPathForPrompt(params) {
38937
+ const { sessionId, cache: cache2, worktreesRootPath, layout, log: log2, discover, opts } = params;
38938
+ if (!sessionId) return void 0;
38939
+ const sid = sessionId.trim();
38940
+ const parentPathRaw = opts.sessionParentPath?.trim();
38941
+ if (parentPathRaw) {
38942
+ return resolveExplicitSessionParentPath({
38943
+ sessionId: sid,
38944
+ sessionParent: opts.sessionParent,
38945
+ parentPathRaw,
38946
+ cache: cache2,
38947
+ discover: () => discover(sid)
38948
+ });
38811
38949
  }
38812
- async commitSession(params) {
38813
- return commitSessionWorktree(this.cache, params);
38950
+ const parentKind = parseSessionParent(opts.sessionParent);
38951
+ if (parentKind === "bridge_root") {
38952
+ return void 0;
38814
38953
  }
38815
- async getSessionWorkingTreeStatus(sessionId) {
38816
- return getSessionWorkingTreeStatus(this.cache, sessionId, (sid) => this.discoverAsync(sid));
38954
+ if (parentKind === "worktrees_root") {
38955
+ if (!opts.isNewSession) {
38956
+ return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
38957
+ }
38958
+ return prepareAndRememberSessionWorktrees({
38959
+ cache: cache2,
38960
+ sessionId: sid,
38961
+ worktreesRootPath,
38962
+ layout,
38963
+ log: log2,
38964
+ ...opts.worktreeBaseBranches ? { worktreeBaseBranches: opts.worktreeBaseBranches } : {}
38965
+ });
38817
38966
  }
38818
- async getSessionWorkingTreeChangeDetails(sessionId, opts) {
38819
- return getSessionWorkingTreeChangeDetails(this.cache, sessionId, (sid) => this.discoverAsync(sid), opts);
38967
+ if (!opts.isNewSession) {
38968
+ return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
38820
38969
  }
38821
- async pushSessionUpstream(sessionId) {
38822
- return pushSessionUpstream(this.cache, sessionId, (sid) => this.discoverAsync(sid));
38970
+ return prepareAndRememberSessionWorktrees({
38971
+ cache: cache2,
38972
+ sessionId: sid,
38973
+ worktreesRootPath,
38974
+ layout,
38975
+ log: log2
38976
+ });
38977
+ }
38978
+
38979
+ // src/worktrees/manager/session-worktree-paths.ts
38980
+ function createSessionWorktreePathsApi(ctx) {
38981
+ const discoverAsync = (sessionId) => discoverSessionBindingAsync({
38982
+ sessionId,
38983
+ worktreesRootPath: ctx.worktreesRootPath,
38984
+ layout: ctx.layout
38985
+ });
38986
+ return {
38987
+ discoverAsync,
38988
+ getIsolatedSessionParentPathForSession(sessionId) {
38989
+ return resolveIsolatedSessionParentPath(
38990
+ sessionId,
38991
+ ctx.cache,
38992
+ (sid) => ensureRepoCheckoutPathsForSession(sid, ctx.cache)
38993
+ );
38994
+ },
38995
+ resolveSessionParentPathForPrompt(sessionId, opts) {
38996
+ return resolveSessionParentPathForPrompt({
38997
+ sessionId,
38998
+ cache: ctx.cache,
38999
+ worktreesRootPath: ctx.worktreesRootPath,
39000
+ layout: ctx.layout,
39001
+ log: ctx.log,
39002
+ discover: (sid) => discoverAsync(sid),
39003
+ opts
39004
+ });
39005
+ },
39006
+ usesWorktreeSession(sessionId) {
39007
+ if (!sessionId) return false;
39008
+ return ctx.cache.hasSession(sessionId);
39009
+ },
39010
+ getRepoCheckoutPathsForSession(sessionId) {
39011
+ if (!sessionId) return void 0;
39012
+ return ctx.cache.getRepoCheckoutPaths(sessionId);
39013
+ },
39014
+ ensureRepoCheckoutPathsForSession(sessionId) {
39015
+ return ensureRepoCheckoutPathsForSession(sessionId, ctx.cache);
39016
+ },
39017
+ async ensureRepoCheckoutPathsForSessionAsync(sessionId) {
39018
+ return ensureRepoCheckoutPathsForSessionAsync(sessionId, ctx.cache, (sid) => discoverAsync(sid));
39019
+ },
39020
+ getSessionWorktreeRootForSession(sessionId) {
39021
+ return resolveIsolatedSessionParentPath(
39022
+ sessionId,
39023
+ ctx.cache,
39024
+ (sid) => ensureRepoCheckoutPathsForSession(sid, ctx.cache)
39025
+ );
39026
+ }
39027
+ };
39028
+ }
39029
+
39030
+ // src/worktrees/manager/session-worktree-git.ts
39031
+ function createSessionWorktreeGitApi(ctx) {
39032
+ const paths = createSessionWorktreePathsApi(ctx);
39033
+ return {
39034
+ async renameSessionBranch(sessionId, newBranch) {
39035
+ await renameSessionWorktreeBranch(ctx.cache, sessionId, newBranch, ctx.log);
39036
+ },
39037
+ async removeSessionWorktrees(sessionId) {
39038
+ await removeSessionWorktreeCheckouts(ctx.cache, sessionId, ctx.log);
39039
+ },
39040
+ async commitSession(params) {
39041
+ return commitSessionWorktree(ctx.cache, params);
39042
+ },
39043
+ async getSessionWorkingTreeStatus(sessionId) {
39044
+ return getSessionWorkingTreeStatus(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid));
39045
+ },
39046
+ async getSessionWorkingTreeChangeDetails(sessionId, opts) {
39047
+ return getSessionWorkingTreeChangeDetails(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid), opts);
39048
+ },
39049
+ async getSessionWorkingTreeChangeFilePatch(sessionId, opts) {
39050
+ return getSessionWorkingTreeChangeFilePatch(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid), opts);
39051
+ },
39052
+ async pushSessionUpstream(sessionId) {
39053
+ return pushSessionUpstream(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid));
39054
+ }
39055
+ };
39056
+ }
39057
+
39058
+ // src/worktrees/manager/session-worktree-manager.ts
39059
+ var SessionWorktreeManager = class {
39060
+ constructor(options) {
39061
+ const ctx = createSessionWorktreeManagerContext(options);
39062
+ Object.assign(this, createSessionWorktreePathsApi(ctx), createSessionWorktreeGitApi(ctx));
38823
39063
  }
38824
39064
  };
38825
39065
 
38826
39066
  // src/worktrees/manager/default-worktrees-root-path.ts
38827
- import * as path37 from "node:path";
39067
+ import * as path43 from "node:path";
38828
39068
  import os8 from "node:os";
38829
39069
  function defaultWorktreesRootPath() {
38830
- return path37.join(os8.homedir(), ".buildautomaton", "worktrees");
39070
+ return path43.join(os8.homedir(), ".buildautomaton", "worktrees");
38831
39071
  }
38832
39072
 
38833
39073
  // src/files/watch-file-index.ts
38834
39074
  import { watch } from "node:fs";
38835
- import path42 from "node:path";
39075
+ import path48 from "node:path";
38836
39076
 
38837
39077
  // src/files/index/paths.ts
38838
- import path38 from "node:path";
39078
+ import path44 from "node:path";
38839
39079
  import crypto2 from "node:crypto";
38840
39080
  function getCwdHashForFileIndex(resolvedCwd) {
38841
- return crypto2.createHash("sha256").update(path38.resolve(resolvedCwd)).digest("hex").slice(0, INDEX_HASH_LEN);
39081
+ return crypto2.createHash("sha256").update(path44.resolve(resolvedCwd)).digest("hex").slice(0, INDEX_HASH_LEN);
38842
39082
  }
38843
39083
 
38844
39084
  // src/files/index/build-file-index.ts
38845
- import path40 from "node:path";
39085
+ import path46 from "node:path";
38846
39086
 
38847
39087
  // src/files/index/walk-workspace-tree.ts
38848
39088
  import fs30 from "node:fs";
38849
- import path39 from "node:path";
39089
+ import path45 from "node:path";
38850
39090
  var DEPENDENCY_INSTALL_DIR_NAMES = /* @__PURE__ */ new Set([
38851
39091
  "node_modules",
38852
39092
  "bower_components",
@@ -38875,18 +39115,18 @@ async function walkWorkspaceTreeAsync(dir, baseDir, onFile, state) {
38875
39115
  if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
38876
39116
  }
38877
39117
  state.n++;
38878
- const full = path39.join(dir, name);
39118
+ const full = path45.join(dir, name);
38879
39119
  let stat3;
38880
39120
  try {
38881
39121
  stat3 = await fs30.promises.stat(full);
38882
39122
  } catch {
38883
39123
  continue;
38884
39124
  }
38885
- const relative6 = path39.relative(baseDir, full).replace(/\\/g, "/");
39125
+ const relative7 = path45.relative(baseDir, full).replace(/\\/g, "/");
38886
39126
  if (stat3.isDirectory()) {
38887
39127
  await walkWorkspaceTreeAsync(full, baseDir, onFile, state);
38888
39128
  } else if (stat3.isFile()) {
38889
- onFile(relative6);
39129
+ onFile(relative7);
38890
39130
  }
38891
39131
  }
38892
39132
  }
@@ -38984,7 +39224,7 @@ async function collectWorkspacePathsAsync(resolved) {
38984
39224
  }
38985
39225
  async function buildFileIndexAsync(cwd) {
38986
39226
  return withFileIndexSqliteLock(async () => {
38987
- const resolved = path40.resolve(cwd);
39227
+ const resolved = path46.resolve(cwd);
38988
39228
  await yieldToEventLoop();
38989
39229
  assertNotShutdown();
38990
39230
  const paths = await collectWorkspacePathsAsync(resolved);
@@ -38996,7 +39236,7 @@ async function buildFileIndexAsync(cwd) {
38996
39236
  }
38997
39237
 
38998
39238
  // src/files/index/ensure-file-index.ts
38999
- import path41 from "node:path";
39239
+ import path47 from "node:path";
39000
39240
 
39001
39241
  // src/files/index/search-file-index.ts
39002
39242
  function escapeLikePattern(fragment) {
@@ -39048,7 +39288,7 @@ async function searchBridgeFilePathsAsync(resolvedCwd, query, limit = 100) {
39048
39288
 
39049
39289
  // src/files/index/ensure-file-index.ts
39050
39290
  async function ensureFileIndexAsync(cwd) {
39051
- const resolved = path41.resolve(cwd);
39291
+ const resolved = path47.resolve(cwd);
39052
39292
  if (await bridgeFileIndexIsPopulated(resolved)) {
39053
39293
  return { fromCache: true, pathCount: await bridgeFileIndexPathCount(resolved) };
39054
39294
  }
@@ -39092,7 +39332,7 @@ function createFsWatcher(resolved, schedule) {
39092
39332
  }
39093
39333
  }
39094
39334
  function startFileIndexWatcher(cwd = getBridgeRoot()) {
39095
- const resolved = path42.resolve(cwd);
39335
+ const resolved = path48.resolve(cwd);
39096
39336
  void buildFileIndexAsync(resolved).catch((e) => {
39097
39337
  if (e instanceof CliSqliteInterrupted) return;
39098
39338
  console.error("[file-index] Initial index build failed:", e);
@@ -39122,7 +39362,7 @@ function startFileIndexWatcher(cwd = getBridgeRoot()) {
39122
39362
  }
39123
39363
 
39124
39364
  // src/connection/create-bridge-connection.ts
39125
- import * as path53 from "node:path";
39365
+ import * as path61 from "node:path";
39126
39366
 
39127
39367
  // src/dev-servers/manager/dev-server-manager.ts
39128
39368
  import { rm as rm2 } from "node:fs/promises";
@@ -39144,15 +39384,15 @@ function sendDevServerStatus(getWs, serverId, status, options) {
39144
39384
 
39145
39385
  // src/dev-servers/process/terminate-child-process.ts
39146
39386
  async function sigtermAndWaitForExit(proc, graceMs, log2, shortId) {
39147
- const exited = new Promise((resolve28) => {
39148
- proc.once("exit", () => resolve28());
39387
+ const exited = new Promise((resolve35) => {
39388
+ proc.once("exit", () => resolve35());
39149
39389
  });
39150
39390
  log2(`[dev-server] Sending SIGTERM to ${shortId} (pid=${proc.pid ?? "?"}).`);
39151
39391
  try {
39152
39392
  proc.kill("SIGTERM");
39153
39393
  } catch {
39154
39394
  }
39155
- await Promise.race([exited, new Promise((resolve28) => setTimeout(resolve28, graceMs))]);
39395
+ await Promise.race([exited, new Promise((resolve35) => setTimeout(resolve35, graceMs))]);
39156
39396
  }
39157
39397
  function forceKillChild(proc, log2, shortId, graceMs) {
39158
39398
  log2(
@@ -39432,10 +39672,10 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
39432
39672
  import { spawn as spawn7 } from "node:child_process";
39433
39673
  import fs34 from "node:fs";
39434
39674
  import { tmpdir } from "node:os";
39435
- import path43 from "node:path";
39675
+ import path49 from "node:path";
39436
39676
  function trySpawnMergedLogFile(command, env, cwd, signal) {
39437
- const tmpRoot = fs34.mkdtempSync(path43.join(tmpdir(), "ba-devsrv-log-"));
39438
- const logPath = path43.join(tmpRoot, "combined.log");
39677
+ const tmpRoot = fs34.mkdtempSync(path49.join(tmpdir(), "ba-devsrv-log-"));
39678
+ const logPath = path49.join(tmpRoot, "combined.log");
39439
39679
  let logFd;
39440
39680
  try {
39441
39681
  logFd = fs34.openSync(logPath, "a");
@@ -39479,15 +39719,15 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
39479
39719
  import { spawn as spawn8 } from "node:child_process";
39480
39720
  import fs35 from "node:fs";
39481
39721
  import { tmpdir as tmpdir2 } from "node:os";
39482
- import path44 from "node:path";
39722
+ import path50 from "node:path";
39483
39723
  function shSingleQuote(s) {
39484
39724
  return `'${s.replace(/'/g, `'\\''`)}'`;
39485
39725
  }
39486
39726
  function trySpawnShellScriptLogRedirectUnix(command, env, cwd, signal) {
39487
- const tmpRoot = fs35.mkdtempSync(path44.join(tmpdir2(), "ba-devsrv-sh-"));
39488
- const logPath = path44.join(tmpRoot, "combined.log");
39489
- const innerPath = path44.join(tmpRoot, "_cmd.sh");
39490
- const runnerPath = path44.join(tmpRoot, "_run.sh");
39727
+ const tmpRoot = fs35.mkdtempSync(path50.join(tmpdir2(), "ba-devsrv-sh-"));
39728
+ const logPath = path50.join(tmpRoot, "combined.log");
39729
+ const innerPath = path50.join(tmpRoot, "_cmd.sh");
39730
+ const runnerPath = path50.join(tmpRoot, "_run.sh");
39491
39731
  try {
39492
39732
  fs35.writeFileSync(innerPath, `#!/bin/sh
39493
39733
  ${command}
@@ -39518,9 +39758,9 @@ cd ${shSingleQuote(cwd)}
39518
39758
  }
39519
39759
  }
39520
39760
  function trySpawnShellScriptLogRedirectWin(command, env, cwd, signal) {
39521
- const tmpRoot = fs35.mkdtempSync(path44.join(tmpdir2(), "ba-devsrv-sh-"));
39522
- const logPath = path44.join(tmpRoot, "combined.log");
39523
- const runnerPath = path44.join(tmpRoot, "_run.bat");
39761
+ const tmpRoot = fs35.mkdtempSync(path50.join(tmpdir2(), "ba-devsrv-sh-"));
39762
+ const logPath = path50.join(tmpRoot, "combined.log");
39763
+ const runnerPath = path50.join(tmpRoot, "_run.bat");
39524
39764
  const q = (p) => `"${p.replace(/"/g, '""')}"`;
39525
39765
  const com = process.env.ComSpec || "cmd.exe";
39526
39766
  try {
@@ -40033,7 +40273,7 @@ async function proxyToLocal(request) {
40033
40273
  };
40034
40274
  const maxAttempts = isIdempotentProxyMethod(request.method) ? LOCAL_PREVIEW_FETCH_RETRY_DELAYS_MS.length + 1 : 1;
40035
40275
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
40036
- const once = await new Promise((resolve28) => {
40276
+ const once = await new Promise((resolve35) => {
40037
40277
  const req = mod.request(opts, (res) => {
40038
40278
  const chunks = [];
40039
40279
  res.on("data", (c) => chunks.push(c));
@@ -40044,7 +40284,7 @@ async function proxyToLocal(request) {
40044
40284
  if (typeof v === "string") headers[k] = v;
40045
40285
  else if (Array.isArray(v) && v[0]) headers[k] = v[0];
40046
40286
  }
40047
- resolve28({
40287
+ resolve35({
40048
40288
  id: request.id,
40049
40289
  statusCode: res.statusCode ?? 0,
40050
40290
  headers,
@@ -40053,7 +40293,7 @@ async function proxyToLocal(request) {
40053
40293
  });
40054
40294
  });
40055
40295
  req.on("error", (err) => {
40056
- resolve28({
40296
+ resolve35({
40057
40297
  id: request.id,
40058
40298
  statusCode: 0,
40059
40299
  headers: {},
@@ -40460,15 +40700,38 @@ function createOnBridgeIdentified(opts) {
40460
40700
  };
40461
40701
  }
40462
40702
 
40463
- // src/skills/discover-local-agent-skills.ts
40464
- import fs36 from "node:fs";
40465
- import path45 from "node:path";
40703
+ // src/skills/discovery/skill-discovery-roots.ts
40466
40704
  var SKILL_DISCOVERY_ROOTS = [".agents/skills", ".claude/skills", ".cursor/skills", "skills"];
40467
- function discoverLocalSkills(cwd) {
40705
+ async function yieldSkillDiscoveryWork(entryCount) {
40706
+ if (entryCount > 0 && entryCount % INDEX_WORK_YIELD_EVERY === 0) {
40707
+ await yieldToEventLoop();
40708
+ }
40709
+ }
40710
+
40711
+ // src/skills/discovery/discover-local-skills.ts
40712
+ import fs36 from "node:fs";
40713
+ import path51 from "node:path";
40714
+ function collectSkillFromDir(rel, base, name, seenKeys, out) {
40715
+ const dir = path51.join(base, name);
40716
+ try {
40717
+ if (!fs36.statSync(dir).isDirectory()) return;
40718
+ } catch {
40719
+ return;
40720
+ }
40721
+ const skillMd = path51.join(dir, "SKILL.md");
40722
+ if (!fs36.existsSync(skillMd)) return;
40723
+ const key = `${rel}/${name}`;
40724
+ if (seenKeys.has(key)) return;
40725
+ seenKeys.add(key);
40726
+ out.push({ skillKey: name, path: `${rel}/${name}`.replace(/\\/g, "/") });
40727
+ }
40728
+ async function discoverLocalSkillsAsync(cwd) {
40468
40729
  const out = [];
40469
40730
  const seenKeys = /* @__PURE__ */ new Set();
40731
+ let work = 0;
40470
40732
  for (const rel of SKILL_DISCOVERY_ROOTS) {
40471
- const base = path45.join(cwd, rel);
40733
+ await yieldSkillDiscoveryWork(++work);
40734
+ const base = path51.join(cwd, rel);
40472
40735
  if (!fs36.existsSync(base) || !fs36.statSync(base).isDirectory()) continue;
40473
40736
  let entries = [];
40474
40737
  try {
@@ -40476,45 +40739,45 @@ function discoverLocalSkills(cwd) {
40476
40739
  } catch {
40477
40740
  continue;
40478
40741
  }
40479
- for (const name of entries) {
40480
- const dir = path45.join(base, name);
40481
- try {
40482
- if (!fs36.statSync(dir).isDirectory()) continue;
40483
- } catch {
40484
- continue;
40485
- }
40486
- const skillMd = path45.join(dir, "SKILL.md");
40487
- if (!fs36.existsSync(skillMd)) continue;
40488
- const key = `${rel}/${name}`;
40489
- if (seenKeys.has(key)) continue;
40490
- seenKeys.add(key);
40491
- out.push({ skillKey: name, path: `${rel}/${name}`.replace(/\\/g, "/") });
40742
+ for (let i = 0; i < entries.length; i++) {
40743
+ await yieldSkillDiscoveryWork(++work);
40744
+ collectSkillFromDir(rel, base, entries[i], seenKeys, out);
40492
40745
  }
40493
40746
  }
40494
40747
  return out;
40495
40748
  }
40496
- function discoverSkillLayoutRoots(cwd) {
40749
+
40750
+ // src/skills/discovery/discover-skill-layout-roots.ts
40751
+ import fs37 from "node:fs";
40752
+ import path52 from "node:path";
40753
+ function collectLayoutSkill(rel, base, name, skills2) {
40754
+ const dir = path52.join(base, name);
40755
+ try {
40756
+ if (!fs37.statSync(dir).isDirectory()) return;
40757
+ } catch {
40758
+ return;
40759
+ }
40760
+ if (!fs37.existsSync(path52.join(dir, "SKILL.md"))) return;
40761
+ const relPath = `${rel}/${name}`.replace(/\\/g, "/");
40762
+ skills2.push({ name, relPath });
40763
+ }
40764
+ async function discoverSkillLayoutRootsAsync(cwd) {
40497
40765
  const roots = [];
40766
+ let work = 0;
40498
40767
  for (const rel of SKILL_DISCOVERY_ROOTS) {
40499
- const base = path45.join(cwd, rel);
40500
- if (!fs36.existsSync(base) || !fs36.statSync(base).isDirectory()) continue;
40768
+ await yieldSkillDiscoveryWork(++work);
40769
+ const base = path52.join(cwd, rel);
40770
+ if (!fs37.existsSync(base) || !fs37.statSync(base).isDirectory()) continue;
40501
40771
  let entries = [];
40502
40772
  try {
40503
- entries = fs36.readdirSync(base);
40773
+ entries = fs37.readdirSync(base);
40504
40774
  } catch {
40505
40775
  continue;
40506
40776
  }
40507
40777
  const skills2 = [];
40508
- for (const name of entries) {
40509
- const dir = path45.join(base, name);
40510
- try {
40511
- if (!fs36.statSync(dir).isDirectory()) continue;
40512
- } catch {
40513
- continue;
40514
- }
40515
- if (!fs36.existsSync(path45.join(dir, "SKILL.md"))) continue;
40516
- const relPath = `${rel}/${name}`.replace(/\\/g, "/");
40517
- skills2.push({ name, relPath });
40778
+ for (let i = 0; i < entries.length; i++) {
40779
+ await yieldSkillDiscoveryWork(++work);
40780
+ collectLayoutSkill(rel, base, entries[i], skills2);
40518
40781
  }
40519
40782
  if (skills2.length > 0) {
40520
40783
  roots.push({ path: rel.replace(/\\/g, "/"), skills: skills2 });
@@ -40556,16 +40819,18 @@ async function detectLocalAgentTypes() {
40556
40819
  function createSendLocalSkillsReport(getWs, logFn) {
40557
40820
  return () => {
40558
40821
  setImmediate(() => {
40559
- try {
40560
- const socket = getWs();
40561
- if (!socket || socket.readyState !== wrapper_default.OPEN) return;
40562
- const skills2 = discoverLocalSkills(getBridgeRoot());
40563
- sendWsMessage(socket, { type: "local_skills", skills: skills2 });
40564
- } catch (e) {
40565
- logFn(
40566
- `[Bridge service] Local skills report failed: ${e instanceof Error ? e.message : String(e)}`
40567
- );
40568
- }
40822
+ void (async () => {
40823
+ try {
40824
+ const socket = getWs();
40825
+ if (!socket || socket.readyState !== wrapper_default.OPEN) return;
40826
+ const skills2 = await discoverLocalSkillsAsync(getBridgeRoot());
40827
+ sendWsMessage(socket, { type: "local_skills", skills: skills2 });
40828
+ } catch (e) {
40829
+ logFn(
40830
+ `[Bridge service] Local skills report failed: ${e instanceof Error ? e.message : String(e)}`
40831
+ );
40832
+ }
40833
+ })();
40569
40834
  });
40570
40835
  };
40571
40836
  }
@@ -40868,7 +41133,7 @@ function syncRunningTurnQueueKeys(turns, queueKey) {
40868
41133
  }
40869
41134
 
40870
41135
  // src/prompt-turn-queue/runner/run-local-revert-before-queued-prompt.ts
40871
- import fs37 from "node:fs";
41136
+ import fs38 from "node:fs";
40872
41137
  async function runLocalRevertBeforeQueuedPrompt(next, deps) {
40873
41138
  if (next.serverState !== "requeued_with_revert") return true;
40874
41139
  const sid = next.sessionId;
@@ -40877,7 +41142,7 @@ async function runLocalRevertBeforeQueuedPrompt(next, deps) {
40877
41142
  const agentBase = deps.sessionWorktreeManager.getSessionWorktreeRootForSession(sid) ?? getBridgeRoot();
40878
41143
  const file2 = snapshotFilePath(agentBase, tid);
40879
41144
  try {
40880
- await fs37.promises.access(file2, fs37.constants.F_OK);
41145
+ await fs38.promises.access(file2, fs38.constants.F_OK);
40881
41146
  } catch {
40882
41147
  deps.log(
40883
41148
  `[Queue] requeued_with_revert: no pre-turn snapshot for ${tid.slice(0, 8)}\u2026; continuing without revert.`
@@ -41165,9 +41430,9 @@ function parseChangeSummarySnapshots(raw) {
41165
41430
  for (const item of raw) {
41166
41431
  if (!item || typeof item !== "object") continue;
41167
41432
  const o = item;
41168
- const path55 = typeof o.path === "string" && o.path.trim() !== "" ? o.path.trim() : "";
41169
- if (!path55) continue;
41170
- const row = { path: path55 };
41433
+ const path63 = typeof o.path === "string" && o.path.trim() !== "" ? o.path.trim() : "";
41434
+ if (!path63) continue;
41435
+ const row = { path: path63 };
41171
41436
  if (typeof o.patchContent === "string") row.patchContent = o.patchContent;
41172
41437
  if (typeof o.oldText === "string") row.oldText = o.oldText;
41173
41438
  if (typeof o.newText === "string") row.newText = o.newText;
@@ -41377,8 +41642,8 @@ function randomSecret() {
41377
41642
  }
41378
41643
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
41379
41644
  }
41380
- async function requestPreviewApi(port, secret, method, path55, body) {
41381
- const url2 = `http://127.0.0.1:${port}${path55}`;
41645
+ async function requestPreviewApi(port, secret, method, path63, body) {
41646
+ const url2 = `http://127.0.0.1:${port}${path63}`;
41382
41647
  const headers = {
41383
41648
  [PREVIEW_SECRET_HEADER]: secret,
41384
41649
  "Content-Type": "application/json"
@@ -41390,7 +41655,7 @@ async function requestPreviewApi(port, secret, method, path55, body) {
41390
41655
  });
41391
41656
  const data = await res.json().catch(() => ({}));
41392
41657
  if (!res.ok) {
41393
- throw new Error(data?.error ?? `Preview API ${method} ${path55}: ${res.status}`);
41658
+ throw new Error(data?.error ?? `Preview API ${method} ${path63}: ${res.status}`);
41394
41659
  }
41395
41660
  return data;
41396
41661
  }
@@ -41554,15 +41819,26 @@ var handleSkillCallMessage = (msg, { getWs, log: log2 }) => {
41554
41819
  );
41555
41820
  };
41556
41821
 
41822
+ // src/files/resolve-file-browser-session-parent.ts
41823
+ async function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
41824
+ const sid = sessionId?.trim();
41825
+ if (sid) {
41826
+ await sessionWorktreeManager.ensureRepoCheckoutPathsForSessionAsync(sid);
41827
+ const worktreeRoot = sessionWorktreeManager.getSessionWorktreeRootForSession(sid);
41828
+ if (worktreeRoot) return worktreeRoot;
41829
+ }
41830
+ return getBridgeRoot();
41831
+ }
41832
+
41557
41833
  // src/files/list-dir/index.ts
41558
- import fs39 from "node:fs";
41834
+ import fs40 from "node:fs";
41559
41835
 
41560
41836
  // src/files/ensure-under-cwd.ts
41561
- import path46 from "node:path";
41837
+ import path53 from "node:path";
41562
41838
  function ensureUnderCwd(relativePath, cwd = getBridgeRoot()) {
41563
- const normalized = path46.normalize(relativePath).replace(/^(\.\/)+/, "");
41564
- const resolved = path46.resolve(cwd, normalized);
41565
- if (!resolved.startsWith(cwd + path46.sep) && resolved !== cwd) {
41839
+ const normalized = path53.normalize(relativePath).replace(/^(\.\/)+/, "");
41840
+ const resolved = path53.resolve(cwd, normalized);
41841
+ if (!resolved.startsWith(cwd + path53.sep) && resolved !== cwd) {
41566
41842
  return null;
41567
41843
  }
41568
41844
  return resolved;
@@ -41572,15 +41848,15 @@ function ensureUnderCwd(relativePath, cwd = getBridgeRoot()) {
41572
41848
  var LIST_DIR_YIELD_EVERY = 256;
41573
41849
 
41574
41850
  // src/files/list-dir/map-dir-entry.ts
41575
- import path47 from "node:path";
41576
- import fs38 from "node:fs";
41851
+ import path54 from "node:path";
41852
+ import fs39 from "node:fs";
41577
41853
  async function mapDirEntry(d, relativePath, resolved) {
41578
- const entryPath = path47.join(relativePath || ".", d.name).replace(/\\/g, "/");
41579
- const fullPath = path47.join(resolved, d.name);
41854
+ const entryPath = path54.join(relativePath || ".", d.name).replace(/\\/g, "/");
41855
+ const fullPath = path54.join(resolved, d.name);
41580
41856
  let isDir = d.isDirectory();
41581
41857
  if (d.isSymbolicLink()) {
41582
41858
  try {
41583
- const targetStat = await fs38.promises.stat(fullPath);
41859
+ const targetStat = await fs39.promises.stat(fullPath);
41584
41860
  isDir = targetStat.isDirectory();
41585
41861
  } catch {
41586
41862
  isDir = false;
@@ -41610,7 +41886,7 @@ async function listDirAsync(relativePath, sessionParentPath = getBridgeRoot()) {
41610
41886
  return { error: "Path is outside working directory" };
41611
41887
  }
41612
41888
  try {
41613
- const names = await fs39.promises.readdir(resolved, { withFileTypes: true });
41889
+ const names = await fs40.promises.readdir(resolved, { withFileTypes: true });
41614
41890
  const entries = [];
41615
41891
  for (let i = 0; i < names.length; i++) {
41616
41892
  if (i > 0 && i % LIST_DIR_YIELD_EVERY === 0) {
@@ -41625,23 +41901,460 @@ async function listDirAsync(relativePath, sessionParentPath = getBridgeRoot()) {
41625
41901
  }
41626
41902
  }
41627
41903
 
41904
+ // src/files/handle-file-browser-search.ts
41905
+ import path55 from "node:path";
41906
+ var SEARCH_LIMIT = 100;
41907
+ function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
41908
+ void (async () => {
41909
+ await yieldToEventLoop();
41910
+ const q = typeof msg.q === "string" ? msg.q : "";
41911
+ const sessionParentPath = path55.resolve(
41912
+ sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
41913
+ );
41914
+ if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
41915
+ const payload2 = {
41916
+ type: "file_browser_search_response",
41917
+ id: msg.id,
41918
+ paths: [],
41919
+ indexReady: false
41920
+ };
41921
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload2, ["paths"]) : payload2);
41922
+ return;
41923
+ }
41924
+ const results = await searchBridgeFilePathsAsync(sessionParentPath, q, SEARCH_LIMIT);
41925
+ const payload = {
41926
+ type: "file_browser_search_response",
41927
+ id: msg.id,
41928
+ paths: results,
41929
+ indexReady: true
41930
+ };
41931
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["paths"]) : payload);
41932
+ })();
41933
+ }
41934
+ function triggerFileIndexBuild(sessionParentPath = getBridgeRoot()) {
41935
+ setImmediate(() => {
41936
+ void ensureFileIndexAsync(sessionParentPath).catch((e) => {
41937
+ console.error("[file-index] Background build failed:", e);
41938
+ });
41939
+ });
41940
+ }
41941
+
41942
+ // src/git/tree/resolve-repo-abs-path.ts
41943
+ import * as path56 from "node:path";
41944
+ function resolveRepoAbsPathFromBridge(repoRelPath, bridgeRoot = getBridgeRoot()) {
41945
+ const bridgeResolved = path56.resolve(bridgeRoot);
41946
+ const rel = repoRelPath.trim() === "." ? "" : repoRelPath.trim().replace(/\\/g, "/");
41947
+ const repoPath = rel === "" ? bridgeResolved : path56.join(bridgeResolved, rel);
41948
+ const resolved = path56.resolve(repoPath);
41949
+ if (!resolved.startsWith(bridgeResolved + path56.sep) && resolved !== bridgeResolved) {
41950
+ return null;
41951
+ }
41952
+ return resolved;
41953
+ }
41954
+
41955
+ // src/git/tree/resolve-branch-commit.ts
41956
+ async function resolveBranchCommit(repoAbsPath, branch) {
41957
+ const branchRef = branch.trim();
41958
+ if (!branchRef) return null;
41959
+ try {
41960
+ return (await cliSimpleGit(repoAbsPath).revparse([`${branchRef}^{commit}`])).trim();
41961
+ } catch {
41962
+ return null;
41963
+ }
41964
+ }
41965
+
41966
+ // src/git/tree/list-git-tree-dir.ts
41967
+ function parseLsTreeLine(line) {
41968
+ const tab = line.indexOf(" ");
41969
+ if (tab < 0) return null;
41970
+ const meta = line.slice(0, tab).trim().split(/\s+/);
41971
+ if (meta.length < 3) return null;
41972
+ const mode = meta[0];
41973
+ const objectType2 = meta[1];
41974
+ let name = line.slice(tab + 1);
41975
+ if (name.startsWith('"') && name.endsWith('"')) {
41976
+ name = name.slice(1, -1).replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
41977
+ }
41978
+ return { mode, objectType: objectType2, name };
41979
+ }
41980
+ function childPath(relativePath, name) {
41981
+ const base = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
41982
+ if (!base || base === ".") return name;
41983
+ return `${base}/${name}`.replace(/\\/g, "/");
41984
+ }
41985
+ function lsTreeArgs(commit, relativePath) {
41986
+ const rel = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
41987
+ if (!rel || rel === ".") return ["ls-tree", commit];
41988
+ return ["ls-tree", `${commit}:${rel}`];
41989
+ }
41990
+ async function listGitTreeDirAsync(repoAbsPath, branch, relativePath) {
41991
+ await yieldToEventLoop();
41992
+ const commit = await resolveBranchCommit(repoAbsPath, branch);
41993
+ if (!commit) return { error: "Branch not found" };
41994
+ try {
41995
+ const raw = await cliSimpleGit(repoAbsPath).raw(lsTreeArgs(commit, relativePath));
41996
+ await yieldToEventLoop();
41997
+ const lines = String(raw ?? "").split(/\r?\n/).filter((line) => line.trim().length > 0);
41998
+ const entries = [];
41999
+ for (let i = 0; i < lines.length; i++) {
42000
+ if (i > 0 && i % LIST_DIR_YIELD_EVERY === 0) {
42001
+ await yieldToEventLoop();
42002
+ }
42003
+ const parsed = parseLsTreeLine(lines[i]);
42004
+ if (!parsed) continue;
42005
+ const { mode, objectType: objectType2, name } = parsed;
42006
+ if (!name || name === "." || name === "..") continue;
42007
+ const isSymlink = mode === "120000";
42008
+ const isDir = objectType2 === "tree";
42009
+ entries.push({
42010
+ name,
42011
+ path: childPath(relativePath, name),
42012
+ isDir,
42013
+ ...isSymlink ? { isSymlink: true } : {}
42014
+ });
42015
+ }
42016
+ if (entries.length >= LIST_DIR_YIELD_EVERY) {
42017
+ await yieldToEventLoop();
42018
+ }
42019
+ return { entries: sortListEntries(entries) };
42020
+ } catch (err) {
42021
+ const message = err instanceof Error ? err.message : String(err);
42022
+ if (/not a tree object|does not exist|bad revision/i.test(message)) {
42023
+ return { error: "Path not found in branch" };
42024
+ }
42025
+ return { error: message };
42026
+ }
42027
+ }
42028
+
41628
42029
  // src/files/read-file/types.ts
41629
42030
  var LINE_CHUNK_SIZE = 64 * 1024;
41630
42031
  var READ_RANGE_YIELD_EVERY_BYTES = 256 * 1024;
41631
42032
 
42033
+ // src/files/read-file/guess-mime-type.ts
42034
+ var MIME_BY_EXT = {
42035
+ png: "image/png",
42036
+ jpg: "image/jpeg",
42037
+ jpeg: "image/jpeg",
42038
+ gif: "image/gif",
42039
+ bmp: "image/bmp",
42040
+ ico: "image/x-icon",
42041
+ webp: "image/webp",
42042
+ avif: "image/avif",
42043
+ svg: "image/svg+xml",
42044
+ pdf: "application/pdf",
42045
+ json: "application/json",
42046
+ html: "text/html",
42047
+ htm: "text/html",
42048
+ css: "text/css",
42049
+ js: "text/javascript",
42050
+ mjs: "text/javascript",
42051
+ ts: "text/typescript",
42052
+ txt: "text/plain",
42053
+ md: "text/markdown",
42054
+ xml: "application/xml",
42055
+ zip: "application/zip",
42056
+ gz: "application/gzip",
42057
+ wasm: "application/wasm"
42058
+ };
42059
+ function guessMimeType(filePath) {
42060
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
42061
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
42062
+ }
42063
+
42064
+ // src/git/tree/file/git-blob-ref.ts
42065
+ async function gitBlobRef(repoAbsPath, branch, relativePath) {
42066
+ const commit = await resolveBranchCommit(repoAbsPath, branch);
42067
+ await yieldToEventLoop();
42068
+ if (!commit) return null;
42069
+ const rel = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
42070
+ if (!rel || rel === ".") return null;
42071
+ return `${commit}:${rel}`;
42072
+ }
42073
+
42074
+ // src/git/tree/file/is-binary-buffer.ts
42075
+ function isBinaryBuffer(buf) {
42076
+ const sample = buf.subarray(0, Math.min(buf.length, 8192));
42077
+ for (let i = 0; i < sample.length; i++) {
42078
+ if (sample[i] === 0) return true;
42079
+ }
42080
+ return false;
42081
+ }
42082
+
42083
+ // src/git/tree/file/read-git-blob-text-async.ts
42084
+ import { StringDecoder } from "node:string_decoder";
42085
+ async function countGitBlobLinesAsync(buf) {
42086
+ if (buf.length === 0) return 1;
42087
+ let lines = 1;
42088
+ let bytesSinceYield = 0;
42089
+ for (let i = 0; i < buf.length; i++) {
42090
+ const b = buf[i];
42091
+ if (b === 10) {
42092
+ lines++;
42093
+ } else if (b === 13) {
42094
+ if (i + 1 < buf.length && buf[i + 1] === 10) {
42095
+ lines++;
42096
+ i++;
42097
+ } else {
42098
+ lines++;
42099
+ }
42100
+ }
42101
+ bytesSinceYield++;
42102
+ if (bytesSinceYield >= READ_RANGE_YIELD_EVERY_BYTES) {
42103
+ await yieldToEventLoop();
42104
+ bytesSinceYield = 0;
42105
+ }
42106
+ }
42107
+ return lines;
42108
+ }
42109
+ async function readGitBlobLineRangeAsync(buf, startLine, endLine, lineOffsetIn, lineChunkSize = LINE_CHUNK_SIZE) {
42110
+ const bufSize = 64 * 1024;
42111
+ const decoder = new StringDecoder("utf8");
42112
+ let currentLine = 0;
42113
+ const resultLines = [];
42114
+ let partial2 = "";
42115
+ let done = false;
42116
+ let skipLine0Chars = typeof lineOffsetIn === "number" ? lineOffsetIn : 0;
42117
+ let line0CharsReturned = 0;
42118
+ let line0Accum = "";
42119
+ let bytesSinceYield = 0;
42120
+ let position = 0;
42121
+ const finish = async () => {
42122
+ const totalLines = await countGitBlobLinesAsync(buf);
42123
+ return { content: resultLines.join("\n"), totalLines };
42124
+ };
42125
+ while (!done && position < buf.length) {
42126
+ const chunkEnd = Math.min(position + bufSize, buf.length);
42127
+ const bytesRead = chunkEnd - position;
42128
+ position = chunkEnd;
42129
+ bytesSinceYield += bytesRead;
42130
+ if (bytesSinceYield >= READ_RANGE_YIELD_EVERY_BYTES) {
42131
+ await yieldToEventLoop();
42132
+ bytesSinceYield = 0;
42133
+ }
42134
+ const text = partial2 + decoder.write(buf.subarray(position - bytesRead, position));
42135
+ partial2 = "";
42136
+ let lineStart = 0;
42137
+ for (let i = 0; i < text.length; i++) {
42138
+ if (text[i] === "\n") {
42139
+ const lineContent = (() => {
42140
+ let lineEnd = i;
42141
+ if (lineEnd > lineStart && text[lineEnd - 1] === "\r") lineEnd--;
42142
+ return text.slice(lineStart, lineEnd);
42143
+ })();
42144
+ if (currentLine === 0 && (startLine === 0 || lineOffsetIn !== void 0)) {
42145
+ line0Accum += lineContent;
42146
+ const totalLine0 = line0Accum.length;
42147
+ if (skipLine0Chars > 0) {
42148
+ if (totalLine0 <= skipLine0Chars) {
42149
+ skipLine0Chars -= totalLine0;
42150
+ line0Accum = "";
42151
+ currentLine++;
42152
+ lineStart = i + 1;
42153
+ if (currentLine > endLine) {
42154
+ done = true;
42155
+ break;
42156
+ }
42157
+ continue;
42158
+ }
42159
+ const from = skipLine0Chars;
42160
+ const take = Math.min(lineChunkSize, totalLine0 - from);
42161
+ resultLines.push(line0Accum.slice(from, from + take));
42162
+ line0CharsReturned += take;
42163
+ if (from + take < totalLine0) {
42164
+ const totalLines = await countGitBlobLinesAsync(buf);
42165
+ return {
42166
+ content: resultLines.join("\n"),
42167
+ totalLines,
42168
+ lineOffset: lineOffsetIn + line0CharsReturned
42169
+ };
42170
+ }
42171
+ line0Accum = "";
42172
+ skipLine0Chars = 0;
42173
+ line0CharsReturned = 0;
42174
+ } else if (totalLine0 > lineChunkSize) {
42175
+ resultLines.push(line0Accum.slice(0, lineChunkSize));
42176
+ const totalLines = await countGitBlobLinesAsync(buf);
42177
+ return {
42178
+ content: resultLines.join("\n"),
42179
+ totalLines,
42180
+ lineOffset: lineChunkSize
42181
+ };
42182
+ } else {
42183
+ resultLines.push(line0Accum);
42184
+ line0Accum = "";
42185
+ }
42186
+ } else if (currentLine >= startLine && currentLine <= endLine) {
42187
+ resultLines.push(lineContent);
42188
+ }
42189
+ currentLine++;
42190
+ lineStart = i + 1;
42191
+ if (currentLine > endLine) {
42192
+ done = true;
42193
+ break;
42194
+ }
42195
+ }
42196
+ }
42197
+ if (!done) {
42198
+ const lineContent = text.slice(lineStart);
42199
+ if (currentLine === 0 && (startLine === 0 || lineOffsetIn !== void 0)) {
42200
+ line0Accum += lineContent;
42201
+ const totalLine0 = line0Accum.length;
42202
+ if (skipLine0Chars > 0) {
42203
+ if (totalLine0 <= skipLine0Chars) {
42204
+ skipLine0Chars -= totalLine0;
42205
+ line0Accum = "";
42206
+ } else {
42207
+ const from = skipLine0Chars;
42208
+ const take = Math.min(lineChunkSize, totalLine0 - from);
42209
+ resultLines.push(line0Accum.slice(from, from + take));
42210
+ const totalLines = await countGitBlobLinesAsync(buf);
42211
+ return {
42212
+ content: resultLines.join("\n"),
42213
+ totalLines,
42214
+ lineOffset: (lineOffsetIn ?? 0) + take
42215
+ };
42216
+ }
42217
+ } else if (totalLine0 > lineChunkSize) {
42218
+ resultLines.push(line0Accum.slice(0, lineChunkSize));
42219
+ const totalLines = await countGitBlobLinesAsync(buf);
42220
+ return {
42221
+ content: resultLines.join("\n"),
42222
+ totalLines,
42223
+ lineOffset: lineChunkSize
42224
+ };
42225
+ }
42226
+ }
42227
+ partial2 = text.slice(lineStart);
42228
+ }
42229
+ }
42230
+ if (!done) {
42231
+ const tail = partial2 + decoder.end();
42232
+ if (currentLine === 0 && (startLine === 0 || lineOffsetIn !== void 0) && tail.length > 0) {
42233
+ line0Accum += tail.endsWith("\r") ? tail.slice(0, -1) : tail;
42234
+ const totalLine0 = line0Accum.length;
42235
+ if (skipLine0Chars > 0) {
42236
+ if (totalLine0 <= skipLine0Chars) {
42237
+ return finish();
42238
+ }
42239
+ const from = skipLine0Chars;
42240
+ const take = Math.min(lineChunkSize, totalLine0 - from);
42241
+ resultLines.push(line0Accum.slice(from, from + take));
42242
+ line0CharsReturned += take;
42243
+ if (from + take < totalLine0) {
42244
+ const totalLines = await countGitBlobLinesAsync(buf);
42245
+ return {
42246
+ content: resultLines.join("\n"),
42247
+ totalLines,
42248
+ lineOffset: (lineOffsetIn ?? 0) + line0CharsReturned
42249
+ };
42250
+ }
42251
+ } else if (totalLine0 > lineChunkSize) {
42252
+ resultLines.push(line0Accum.slice(0, lineChunkSize));
42253
+ const totalLines = await countGitBlobLinesAsync(buf);
42254
+ return {
42255
+ content: resultLines.join("\n"),
42256
+ totalLines,
42257
+ lineOffset: lineChunkSize
42258
+ };
42259
+ } else {
42260
+ resultLines.push(line0Accum);
42261
+ }
42262
+ } else if (tail.length > 0 && currentLine >= startLine && currentLine <= endLine) {
42263
+ resultLines.push(tail.endsWith("\r") ? tail.slice(0, -1) : tail);
42264
+ }
42265
+ }
42266
+ return finish();
42267
+ }
42268
+ async function decodeGitBlobUtf8Async(buf) {
42269
+ await yieldToEventLoop();
42270
+ const text = buf.toString("utf8");
42271
+ await yieldToEventLoop();
42272
+ return text;
42273
+ }
42274
+ async function readGitBlobUtf8FullAsync(buf) {
42275
+ const text = await decodeGitBlobUtf8Async(buf);
42276
+ const totalLines = await countGitBlobLinesAsync(buf);
42277
+ return { content: text, totalLines };
42278
+ }
42279
+
42280
+ // src/git/tree/file/index.ts
42281
+ async function readGitTreeFileAsync(repoAbsPath, branch, relativePath, startLine, endLine, lineOffset, lineChunkSize = LINE_CHUNK_SIZE, encoding = "utf8") {
42282
+ await yieldToEventLoop();
42283
+ const branchRef = branch.trim();
42284
+ if (!branchRef) return { error: "branch required" };
42285
+ const rel = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
42286
+ if (!rel || rel === ".") return { error: "file path required" };
42287
+ const blobRef = await gitBlobRef(repoAbsPath, branchRef, rel);
42288
+ if (!blobRef) return { error: "Branch not found" };
42289
+ const hasRange = typeof startLine === "number" && typeof endLine === "number";
42290
+ try {
42291
+ if (encoding === "base64") {
42292
+ if (hasRange) return { error: "base64 encoding requires a full file read (no line range)" };
42293
+ const buf2 = await cliSimpleGit(repoAbsPath).showBuffer([blobRef]);
42294
+ await yieldToEventLoop();
42295
+ return {
42296
+ content: buf2.toString("base64"),
42297
+ size: buf2.length,
42298
+ mimeType: guessMimeType(rel)
42299
+ };
42300
+ }
42301
+ const buf = await cliSimpleGit(repoAbsPath).showBuffer([blobRef]);
42302
+ await yieldToEventLoop();
42303
+ const size = buf.length;
42304
+ if (isBinaryBuffer(buf)) {
42305
+ return { error: "Binary file cannot be displayed as text" };
42306
+ }
42307
+ if (hasRange) {
42308
+ const ranged = await readGitBlobLineRangeAsync(buf, startLine, endLine, lineOffset, lineChunkSize);
42309
+ return {
42310
+ content: ranged.content,
42311
+ totalLines: ranged.totalLines,
42312
+ size,
42313
+ ...ranged.lineOffset != null ? { lineOffset: ranged.lineOffset } : {}
42314
+ };
42315
+ }
42316
+ const full = await readGitBlobUtf8FullAsync(buf);
42317
+ return { content: full.content, totalLines: full.totalLines, size, mimeType: guessMimeType(rel) };
42318
+ } catch (err) {
42319
+ const message = err instanceof Error ? err.message : String(err);
42320
+ if (/does not exist|bad revision|exists on disk, but not in/i.test(message)) {
42321
+ return { error: "File not found in branch" };
42322
+ }
42323
+ return { error: message };
42324
+ }
42325
+ }
42326
+
42327
+ // src/files/browser/send-file-browser-message.ts
42328
+ function sendFileBrowserMessage(socket, e2ee, payload) {
42329
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["entries", "content", "totalLines", "size", "lineOffset"]) : payload);
42330
+ }
42331
+
42332
+ // src/files/browser/handle-file-browser-list.ts
42333
+ async function handleFileBrowserList(socket, e2ee, id, reqPath, sessionParentPath, gitScope) {
42334
+ const result = gitScope ? await listGitTreeDirAsync(gitScope.repoAbsPath, gitScope.branch, reqPath) : await listDirAsync(reqPath, sessionParentPath);
42335
+ if ("error" in result) {
42336
+ sendWsMessage(socket, { type: "file_browser_response", id, error: result.error });
42337
+ return;
42338
+ }
42339
+ sendFileBrowserMessage(socket, e2ee, { type: "file_browser_response", id, entries: result.entries });
42340
+ if (!gitScope && (reqPath === "." || reqPath === "")) {
42341
+ triggerFileIndexBuild(sessionParentPath);
42342
+ }
42343
+ }
42344
+
41632
42345
  // src/files/read-file/resolve-file-path.ts
41633
- import fs40 from "node:fs";
42346
+ import fs41 from "node:fs";
41634
42347
  async function resolveFilePathAsync(relativePath, sessionParentPath = getBridgeRoot()) {
41635
42348
  const resolved = ensureUnderCwd(relativePath, sessionParentPath);
41636
42349
  if (!resolved) return { error: "Path is outside working directory" };
41637
42350
  let real;
41638
42351
  try {
41639
- real = await fs40.promises.realpath(resolved);
42352
+ real = await fs41.promises.realpath(resolved);
41640
42353
  } catch {
41641
42354
  real = resolved;
41642
42355
  }
41643
42356
  try {
41644
- const stat3 = await fs40.promises.stat(real);
42357
+ const stat3 = await fs41.promises.stat(real);
41645
42358
  if (!stat3.isFile()) return { error: "Not a file" };
41646
42359
  return real;
41647
42360
  } catch (err) {
@@ -41650,14 +42363,14 @@ async function resolveFilePathAsync(relativePath, sessionParentPath = getBridgeR
41650
42363
  }
41651
42364
 
41652
42365
  // src/files/read-file/read-file-range-async.ts
41653
- import fs41 from "node:fs";
41654
- import { StringDecoder } from "node:string_decoder";
42366
+ import fs42 from "node:fs";
42367
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
41655
42368
  async function readFileRangeAsync(filePath, startLine, endLine, lineOffsetIn, lineChunkSize = LINE_CHUNK_SIZE) {
41656
- const fileSize = (await fs41.promises.stat(filePath)).size;
41657
- const fd = await fs41.promises.open(filePath, "r");
42369
+ const fileSize = (await fs42.promises.stat(filePath)).size;
42370
+ const fd = await fs42.promises.open(filePath, "r");
41658
42371
  const bufSize = 64 * 1024;
41659
42372
  const buf = Buffer.alloc(bufSize);
41660
- const decoder = new StringDecoder("utf8");
42373
+ const decoder = new StringDecoder2("utf8");
41661
42374
  let currentLine = 0;
41662
42375
  const resultLines = [];
41663
42376
  let partial2 = "";
@@ -41816,11 +42529,11 @@ async function readFileRangeAsync(filePath, startLine, endLine, lineOffsetIn, li
41816
42529
  }
41817
42530
 
41818
42531
  // src/files/read-file/read-file-buffer-full-async.ts
41819
- import fs42 from "node:fs";
42532
+ import fs43 from "node:fs";
41820
42533
  var READ_CHUNK_BYTES = 256 * 1024;
41821
42534
  async function readFileBufferFullAsync(filePath) {
41822
- const stat3 = await fs42.promises.stat(filePath);
41823
- const fd = await fs42.promises.open(filePath, "r");
42535
+ const stat3 = await fs43.promises.stat(filePath);
42536
+ const fd = await fs43.promises.open(filePath, "r");
41824
42537
  const chunks = [];
41825
42538
  let position = 0;
41826
42539
  let bytesSinceYield = 0;
@@ -41851,37 +42564,6 @@ async function readFileFullAsync(filePath) {
41851
42564
  return { content: raw, totalLines: lines.length, size };
41852
42565
  }
41853
42566
 
41854
- // src/files/read-file/guess-mime-type.ts
41855
- var MIME_BY_EXT = {
41856
- png: "image/png",
41857
- jpg: "image/jpeg",
41858
- jpeg: "image/jpeg",
41859
- gif: "image/gif",
41860
- bmp: "image/bmp",
41861
- ico: "image/x-icon",
41862
- webp: "image/webp",
41863
- avif: "image/avif",
41864
- svg: "image/svg+xml",
41865
- pdf: "application/pdf",
41866
- json: "application/json",
41867
- html: "text/html",
41868
- htm: "text/html",
41869
- css: "text/css",
41870
- js: "text/javascript",
41871
- mjs: "text/javascript",
41872
- ts: "text/typescript",
41873
- txt: "text/plain",
41874
- md: "text/markdown",
41875
- xml: "application/xml",
41876
- zip: "application/zip",
41877
- gz: "application/gzip",
41878
- wasm: "application/wasm"
41879
- };
41880
- function guessMimeType(filePath) {
41881
- const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
41882
- return MIME_BY_EXT[ext] ?? "application/octet-stream";
41883
- }
41884
-
41885
42567
  // src/files/read-file/read-file-binary-full-async.ts
41886
42568
  async function readFileBinaryFullAsync(filePath) {
41887
42569
  const { buffer, size } = await readFileBufferFullAsync(filePath);
@@ -41915,104 +42597,72 @@ async function readFileAsync(relativePath, startLine, endLine, lineOffset, lineC
41915
42597
  }
41916
42598
  }
41917
42599
 
41918
- // src/files/resolve-file-browser-session-parent.ts
41919
- async function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
41920
- const sid = sessionId?.trim();
41921
- if (sid) {
41922
- await sessionWorktreeManager.ensureRepoCheckoutPathsForSessionAsync(sid);
41923
- const worktreeRoot = sessionWorktreeManager.getSessionWorktreeRootForSession(sid);
41924
- if (worktreeRoot) return worktreeRoot;
42600
+ // src/files/browser/handle-file-browser-read.ts
42601
+ async function handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope) {
42602
+ const startLine = typeof msg.startLine === "number" ? msg.startLine : void 0;
42603
+ const endLine = typeof msg.endLine === "number" ? msg.endLine : void 0;
42604
+ const lineOffset = typeof msg.lineOffset === "number" ? msg.lineOffset : void 0;
42605
+ const lineChunkSize = typeof msg.lineChunkSize === "number" ? msg.lineChunkSize : void 0;
42606
+ const encoding = msg.encoding === "base64" ? "base64" : "utf8";
42607
+ const result = gitScope ? await readGitTreeFileAsync(
42608
+ gitScope.repoAbsPath,
42609
+ gitScope.branch,
42610
+ reqPath,
42611
+ startLine,
42612
+ endLine,
42613
+ lineOffset,
42614
+ lineChunkSize,
42615
+ encoding
42616
+ ) : await readFileAsync(
42617
+ reqPath,
42618
+ startLine,
42619
+ endLine,
42620
+ lineOffset,
42621
+ lineChunkSize,
42622
+ sessionParentPath,
42623
+ encoding
42624
+ );
42625
+ if ("error" in result) {
42626
+ sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
42627
+ return;
41925
42628
  }
41926
- return getBridgeRoot();
42629
+ const payload = {
42630
+ type: "file_browser_response",
42631
+ id: msg.id,
42632
+ content: result.content,
42633
+ totalLines: result.totalLines,
42634
+ size: result.size
42635
+ };
42636
+ if (result.lineOffset != null) payload.lineOffset = result.lineOffset;
42637
+ if (result.mimeType != null) payload.mimeType = result.mimeType;
42638
+ if (result.resolvedPath != null) payload.resolvedPath = result.resolvedPath;
42639
+ sendFileBrowserMessage(socket, e2ee, payload);
41927
42640
  }
41928
42641
 
41929
- // src/files/handle-file-browser-search.ts
41930
- import path48 from "node:path";
41931
- var SEARCH_LIMIT = 100;
41932
- function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
41933
- void (async () => {
41934
- await yieldToEventLoop();
41935
- const q = typeof msg.q === "string" ? msg.q : "";
41936
- const sessionParentPath = path48.resolve(
41937
- sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
41938
- );
41939
- if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
41940
- const payload2 = {
41941
- type: "file_browser_search_response",
41942
- id: msg.id,
41943
- paths: [],
41944
- indexReady: false
41945
- };
41946
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload2, ["paths"]) : payload2);
41947
- return;
41948
- }
41949
- const results = await searchBridgeFilePathsAsync(sessionParentPath, q, SEARCH_LIMIT);
41950
- const payload = {
41951
- type: "file_browser_search_response",
41952
- id: msg.id,
41953
- paths: results,
41954
- indexReady: true
41955
- };
41956
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["paths"]) : payload);
41957
- })();
41958
- }
41959
- function triggerFileIndexBuild(sessionParentPath = getBridgeRoot()) {
41960
- setImmediate(() => {
41961
- void ensureFileIndexAsync(sessionParentPath).catch((e) => {
41962
- console.error("[file-index] Background build failed:", e);
41963
- });
41964
- });
42642
+ // src/files/browser/resolve-git-branch-scope.ts
42643
+ function resolveGitBranchScope(msg) {
42644
+ const gitBranchScope = msg.source === "git_branch" && typeof msg.repoRelPath === "string" && typeof msg.branch === "string" && msg.branch.trim().length > 0;
42645
+ if (!gitBranchScope) return null;
42646
+ const repoAbsPath = resolveRepoAbsPathFromBridge(msg.repoRelPath);
42647
+ if (!repoAbsPath) return null;
42648
+ return { repoAbsPath, branch: msg.branch.trim() };
41965
42649
  }
41966
42650
 
41967
- // src/files/handle-file-browser-request.ts
41968
- function sendFileBrowserMessage(socket, e2ee, payload) {
41969
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["entries", "content", "totalLines", "size", "lineOffset"]) : payload);
41970
- }
42651
+ // src/files/browser/index.ts
41971
42652
  function handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager) {
41972
42653
  void (async () => {
41973
42654
  const reqPath = msg.path.replace(/^\/+/, "") || ".";
41974
42655
  const op = msg.op === "read" ? "read" : "list";
41975
42656
  const sessionParentPath = sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : void 0;
42657
+ const gitScope = resolveGitBranchScope(msg);
42658
+ if (msg.source === "git_branch" && typeof msg.repoRelPath === "string" && typeof msg.branch === "string" && msg.branch.trim().length > 0 && !gitScope) {
42659
+ sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: "Invalid repository path" });
42660
+ return;
42661
+ }
41976
42662
  if (op === "list") {
41977
- const result = await listDirAsync(reqPath, sessionParentPath);
41978
- if ("error" in result) {
41979
- sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
41980
- } else {
41981
- sendFileBrowserMessage(socket, e2ee, { type: "file_browser_response", id: msg.id, entries: result.entries });
41982
- if (reqPath === "." || reqPath === "") {
41983
- triggerFileIndexBuild(sessionParentPath);
41984
- }
41985
- }
42663
+ await handleFileBrowserList(socket, e2ee, msg.id, reqPath, sessionParentPath, gitScope);
41986
42664
  } else {
41987
- const startLine = typeof msg.startLine === "number" ? msg.startLine : void 0;
41988
- const endLine = typeof msg.endLine === "number" ? msg.endLine : void 0;
41989
- const lineOffset = typeof msg.lineOffset === "number" ? msg.lineOffset : void 0;
41990
- const lineChunkSize = typeof msg.lineChunkSize === "number" ? msg.lineChunkSize : void 0;
41991
- const encoding = msg.encoding === "base64" ? "base64" : "utf8";
41992
- const result = await readFileAsync(
41993
- reqPath,
41994
- startLine,
41995
- endLine,
41996
- lineOffset,
41997
- lineChunkSize,
41998
- sessionParentPath,
41999
- encoding
42000
- );
42001
- if ("error" in result) {
42002
- sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
42003
- } else {
42004
- const payload = {
42005
- type: "file_browser_response",
42006
- id: msg.id,
42007
- content: result.content,
42008
- totalLines: result.totalLines,
42009
- size: result.size
42010
- };
42011
- if (result.lineOffset != null) payload.lineOffset = result.lineOffset;
42012
- if (result.mimeType != null) payload.mimeType = result.mimeType;
42013
- if (result.resolvedPath != null) payload.resolvedPath = result.resolvedPath;
42014
- sendFileBrowserMessage(socket, e2ee, payload);
42015
- }
42665
+ await handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope);
42016
42666
  }
42017
42667
  })();
42018
42668
  }
@@ -42022,12 +42672,7 @@ function handleFileBrowserRequestMessage(msg, { getWs, e2ee, sessionWorktreeMana
42022
42672
  if (typeof msg.id !== "string" || typeof msg.path !== "string") return;
42023
42673
  const socket = getWs();
42024
42674
  if (!socket) return;
42025
- handleFileBrowserRequest(
42026
- msg,
42027
- socket,
42028
- e2ee,
42029
- sessionWorktreeManager
42030
- );
42675
+ handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager);
42031
42676
  }
42032
42677
  function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
42033
42678
  if (typeof msg.id !== "string") return;
@@ -42043,37 +42688,56 @@ function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManag
42043
42688
 
42044
42689
  // src/routing/handlers/skill-layout-request.ts
42045
42690
  function handleSkillLayoutRequest(msg, deps) {
42046
- const socket = deps.getWs();
42047
42691
  const id = typeof msg.id === "string" ? msg.id : "";
42048
- const roots = discoverSkillLayoutRoots(getBridgeRoot());
42049
- if (socket) {
42050
- sendWsMessage(socket, { type: "skill_layout_response", id, roots });
42051
- }
42692
+ void (async () => {
42693
+ const socket = deps.getWs();
42694
+ const roots = await discoverSkillLayoutRootsAsync(getBridgeRoot());
42695
+ if (socket) {
42696
+ sendWsMessage(socket, { type: "skill_layout_response", id, roots });
42697
+ }
42698
+ })();
42052
42699
  }
42053
42700
 
42054
- // src/skills/install-remote-skills.ts
42055
- import fs43 from "node:fs";
42056
- import path49 from "node:path";
42057
- function installRemoteSkills(cwd, targetDir, items) {
42701
+ // src/skills/install/is-valid-install-item.ts
42702
+ function isValidRemoteSkillInstallItem(item) {
42703
+ if (item === null || typeof item !== "object") return false;
42704
+ const o = item;
42705
+ return typeof o.sourceId === "string" && typeof o.skillName === "string" && typeof o.versionHash === "string" && Array.isArray(o.files);
42706
+ }
42707
+
42708
+ // src/skills/install/install-remote-skills-async.ts
42709
+ import fs44 from "node:fs";
42710
+ import path57 from "node:path";
42711
+
42712
+ // src/skills/install/constants.ts
42713
+ var INSTALL_SKILLS_YIELD_EVERY = 16;
42714
+
42715
+ // src/skills/install/install-remote-skills-async.ts
42716
+ async function writeInstallFileAsync(skillDir, f, filesWritten) {
42717
+ if (typeof f.path !== "string" || !f.text && !f.base64) return;
42718
+ if (++filesWritten.count % INSTALL_SKILLS_YIELD_EVERY === 0) {
42719
+ await yieldToEventLoop();
42720
+ }
42721
+ const dest = path57.join(skillDir, f.path);
42722
+ await fs44.promises.mkdir(path57.dirname(dest), { recursive: true });
42723
+ if (f.text !== void 0) {
42724
+ await fs44.promises.writeFile(dest, f.text, "utf8");
42725
+ } else if (f.base64) {
42726
+ await fs44.promises.writeFile(dest, Buffer.from(f.base64, "base64"));
42727
+ }
42728
+ }
42729
+ async function installRemoteSkillsAsync(cwd, targetDir, items) {
42058
42730
  const installed2 = [];
42059
42731
  if (!Array.isArray(items)) {
42060
42732
  return { success: false, error: "Invalid items" };
42061
42733
  }
42734
+ const filesWritten = { count: 0 };
42062
42735
  try {
42063
42736
  for (const item of items) {
42064
- if (typeof item.sourceId !== "string" || typeof item.skillName !== "string" || typeof item.versionHash !== "string" || !Array.isArray(item.files)) {
42065
- continue;
42066
- }
42067
- const skillDir = path49.join(cwd, targetDir, item.skillName);
42737
+ if (!isValidRemoteSkillInstallItem(item)) continue;
42738
+ const skillDir = path57.join(cwd, targetDir, item.skillName);
42068
42739
  for (const f of item.files) {
42069
- if (typeof f.path !== "string" || !f.text && !f.base64) continue;
42070
- const dest = path49.join(skillDir, f.path);
42071
- fs43.mkdirSync(path49.dirname(dest), { recursive: true });
42072
- if (f.text !== void 0) {
42073
- fs43.writeFileSync(dest, f.text, "utf8");
42074
- } else if (f.base64) {
42075
- fs43.writeFileSync(dest, Buffer.from(f.base64, "base64"));
42076
- }
42740
+ await writeInstallFileAsync(skillDir, f, filesWritten);
42077
42741
  }
42078
42742
  installed2.push({
42079
42743
  sourceId: item.sourceId,
@@ -42089,28 +42753,32 @@ function installRemoteSkills(cwd, targetDir, items) {
42089
42753
 
42090
42754
  // src/routing/handlers/install-skills.ts
42091
42755
  var handleInstallSkillsMessage = (msg, deps) => {
42092
- const socket = deps.getWs();
42093
42756
  const id = typeof msg.id === "string" ? msg.id : "";
42094
42757
  const targetDir = typeof msg.targetDir === "string" && msg.targetDir.trim() ? msg.targetDir.trim() : ".agents/skills";
42095
42758
  const rawItems = msg.items;
42096
42759
  const cwd = getBridgeRoot();
42097
- const result = installRemoteSkills(cwd, targetDir, rawItems);
42098
- if (!result.success) {
42099
- const err = result.error ?? "Invalid items";
42100
- deps.log(`[Bridge service] Install skills failed: ${err}`);
42760
+ void (async () => {
42761
+ const socket = deps.getWs();
42762
+ const result = await installRemoteSkillsAsync(cwd, targetDir, rawItems);
42763
+ if (!result.success) {
42764
+ const err = result.error ?? "Invalid items";
42765
+ deps.log(`[Bridge service] Install skills failed: ${err}`);
42766
+ if (socket) {
42767
+ sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
42768
+ }
42769
+ return;
42770
+ }
42101
42771
  if (socket) {
42102
- sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
42772
+ sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
42103
42773
  }
42104
- return;
42105
- }
42106
- if (socket) {
42107
- sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
42108
- }
42774
+ })();
42109
42775
  };
42110
42776
 
42111
42777
  // src/routing/handlers/refresh-local-skills.ts
42112
42778
  var handleRefreshLocalSkills = (_msg, deps) => {
42113
- deps.sendLocalSkillsReport?.();
42779
+ setImmediate(() => {
42780
+ deps.sendLocalSkillsReport?.();
42781
+ });
42114
42782
  };
42115
42783
 
42116
42784
  // src/routing/handlers/git/handle-session-git-commit.ts
@@ -42141,13 +42809,78 @@ async function handleSessionGitCommitAction(deps, sessionId, msg, reply) {
42141
42809
  });
42142
42810
  }
42143
42811
 
42812
+ // src/routing/handlers/git/session-git-changes-params.ts
42813
+ function readString(value) {
42814
+ return typeof value === "string" ? value.trim() : "";
42815
+ }
42816
+ function readChangesObject(msg) {
42817
+ const changes = msg.changes;
42818
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return null;
42819
+ return changes;
42820
+ }
42821
+ function parseSessionGitChangesParams(msg) {
42822
+ const nested = readChangesObject(msg);
42823
+ const file2 = nested?.file && typeof nested.file === "object" && !Array.isArray(nested.file) ? nested.file : null;
42824
+ const repoRelPath = readString(nested?.repoRelPath) || readString(msg.changesRepoRelPath);
42825
+ const viewRaw = nested?.view ?? msg.changesView;
42826
+ const view = viewRaw === "commit" ? "commit" : "working";
42827
+ const commitSha = readString(nested?.commitSha) || readString(msg.changesCommitSha);
42828
+ const recentCommitsLimit = nested?.recentCommitsLimit ?? msg.changesRecentCommitsLimit;
42829
+ return {
42830
+ repoRelPath,
42831
+ view,
42832
+ commitSha,
42833
+ recentCommitsLimit: typeof recentCommitsLimit === "number" || typeof recentCommitsLimit === "string" ? recentCommitsLimit : void 0,
42834
+ fileWorkspaceRelPath: readString(file2?.workspaceRelPath),
42835
+ fileChange: readString(file2?.change),
42836
+ fileMovedFromWorkspaceRelPath: readString(file2?.movedFromWorkspaceRelPath)
42837
+ };
42838
+ }
42839
+
42840
+ // src/routing/handlers/git/handle-session-git-get-change-file-patch.ts
42841
+ async function handleSessionGitGetChangeFilePatchAction(deps, msg, reply) {
42842
+ const changes = parseSessionGitChangesParams(msg);
42843
+ const { repoRelPath: repoRel, view, commitSha, fileWorkspaceRelPath, fileChange, fileMovedFromWorkspaceRelPath } = changes;
42844
+ const change = parseWorkingTreeChangeKind(fileChange);
42845
+ if (!repoRel || !fileWorkspaceRelPath || !change) {
42846
+ reply({
42847
+ ok: false,
42848
+ error: "changes.repoRelPath, changes.file.workspaceRelPath, and changes.file.change are required"
42849
+ });
42850
+ return;
42851
+ }
42852
+ if (view === "commit" && !commitSha) {
42853
+ reply({ ok: false, error: "changes.commitSha is required for commit file patch" });
42854
+ return;
42855
+ }
42856
+ const patch = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeFilePatch(msg.sessionId, {
42857
+ repoRelPath: repoRel,
42858
+ workspaceRelPath: fileWorkspaceRelPath,
42859
+ change,
42860
+ movedFromWorkspaceRelPath: fileMovedFromWorkspaceRelPath || null,
42861
+ basis: view === "commit" ? { kind: "commit", sha: commitSha } : { kind: "working" }
42862
+ });
42863
+ reply(
42864
+ {
42865
+ ok: true,
42866
+ patchContent: patch.patchContent ?? null,
42867
+ totalLines: patch.totalLines
42868
+ },
42869
+ ["patchContent"]
42870
+ );
42871
+ }
42872
+
42144
42873
  // src/routing/handlers/git/coalesce-list-changes.ts
42145
42874
  var listChangesInflight = /* @__PURE__ */ new Map();
42875
+ function normalizeListChangesRepoRel(repoRelPath) {
42876
+ if (!repoRelPath?.trim()) return "";
42877
+ return normRepoRel(repoRelPath.trim());
42878
+ }
42146
42879
  function listChangesInflightKey(sessionId, opts) {
42147
42880
  const basis = opts?.basis;
42148
42881
  return [
42149
42882
  sessionId,
42150
- opts?.repoRelPath ?? "",
42883
+ normalizeListChangesRepoRel(opts?.repoRelPath),
42151
42884
  basis?.kind === "commit" ? `commit:${basis.sha}` : "working",
42152
42885
  String(opts?.recentCommitsLimit ?? "")
42153
42886
  ].join("|");
@@ -42165,16 +42898,17 @@ function getSessionWorkingTreeChangeDetailsCoalesced(sessionWorktreeManager, ses
42165
42898
 
42166
42899
  // src/routing/handlers/git/handle-session-git-list-changes.ts
42167
42900
  async function handleSessionGitListChangesAction(deps, msg, reply) {
42168
- const repoRel = typeof msg.changesRepoRelPath === "string" ? msg.changesRepoRelPath.trim() : "";
42169
- const view = msg.changesView === "commit" ? "commit" : "working";
42170
- const commitSha = typeof msg.changesCommitSha === "string" ? msg.changesCommitSha.trim() : "";
42901
+ const changes = parseSessionGitChangesParams(msg);
42902
+ const { repoRelPath: repoRel, view, commitSha, recentCommitsLimit } = changes;
42171
42903
  if (view === "commit") {
42172
42904
  if (!repoRel || !commitSha) {
42173
- reply({ ok: false, error: "changesRepoRelPath and changesCommitSha are required for commit view" });
42905
+ reply({
42906
+ ok: false,
42907
+ error: "changes.repoRelPath and changes.commitSha are required for commit view"
42908
+ });
42174
42909
  return;
42175
42910
  }
42176
42911
  }
42177
- const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
42178
42912
  const opts = repoRel && view === "commit" && commitSha ? {
42179
42913
  repoRelPath: repoRel,
42180
42914
  basis: { kind: "commit", sha: commitSha },
@@ -42219,7 +42953,11 @@ async function handleSessionGitStatusAction(deps, sessionId, reply) {
42219
42953
  function sendSessionGitResult(ws, id, payload, e2ee, encryptedFields = []) {
42220
42954
  if (!ws) return;
42221
42955
  const message = { type: "session_git_result", id, ...payload };
42222
- sendWsMessage(ws, e2ee && encryptedFields.length > 0 ? e2ee.encryptFields(message, encryptedFields) : message);
42956
+ let wire = message;
42957
+ if (e2ee && encryptedFields.length > 0) {
42958
+ wire = e2ee.encryptFields(message, encryptedFields);
42959
+ }
42960
+ sendWsMessage(ws, wire);
42223
42961
  }
42224
42962
 
42225
42963
  // src/routing/handlers/git/session-git-request.ts
@@ -42227,7 +42965,7 @@ var handleSessionGitRequestMessage = (msg, deps) => {
42227
42965
  if (typeof msg.id !== "string") return;
42228
42966
  const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
42229
42967
  const action = msg.action;
42230
- if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes")
42968
+ if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes" && action !== "file_diff")
42231
42969
  return;
42232
42970
  void (async () => {
42233
42971
  const ws = deps.getWs();
@@ -42241,6 +42979,10 @@ var handleSessionGitRequestMessage = (msg, deps) => {
42241
42979
  await handleSessionGitListChangesAction(deps, { ...msg, sessionId }, reply);
42242
42980
  return;
42243
42981
  }
42982
+ if (action === "file_diff") {
42983
+ await handleSessionGitGetChangeFilePatchAction(deps, { ...msg, sessionId }, reply);
42984
+ return;
42985
+ }
42244
42986
  if (action === "push") {
42245
42987
  await handleSessionGitPushAction(deps, sessionId, reply);
42246
42988
  return;
@@ -42275,7 +43017,7 @@ var handleSessionDiscardedMessage = (msg, deps) => {
42275
43017
  };
42276
43018
 
42277
43019
  // src/routing/handlers/revert-turn-snapshot.ts
42278
- import * as fs44 from "node:fs";
43020
+ import * as fs45 from "node:fs";
42279
43021
  var handleRevertTurnSnapshotMessage = (msg, deps) => {
42280
43022
  const id = typeof msg.id === "string" ? msg.id : "";
42281
43023
  const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
@@ -42288,7 +43030,7 @@ var handleRevertTurnSnapshotMessage = (msg, deps) => {
42288
43030
  const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
42289
43031
  const file2 = snapshotFilePath(agentBase, turnId);
42290
43032
  try {
42291
- await fs44.promises.access(file2, fs44.constants.F_OK);
43033
+ await fs45.promises.access(file2, fs45.constants.F_OK);
42292
43034
  } catch {
42293
43035
  sendWsMessage(s, {
42294
43036
  type: "revert_turn_snapshot_result",
@@ -42326,11 +43068,13 @@ var handleDevServerControl = (msg, deps) => {
42326
43068
  // src/routing/handlers/dev-servers-config.ts
42327
43069
  var handleDevServersConfig = (msg, deps) => {
42328
43070
  const devServers = msg.devServers;
42329
- deps.devServerManager?.applyConfig(devServers ?? []);
43071
+ setImmediate(() => {
43072
+ deps.devServerManager?.applyConfig(devServers ?? []);
43073
+ });
42330
43074
  };
42331
43075
 
42332
43076
  // src/git/bridge-git-context.ts
42333
- import * as path50 from "node:path";
43077
+ import * as path58 from "node:path";
42334
43078
 
42335
43079
  // src/git/branches/get-current-branch.ts
42336
43080
  async function getCurrentBranch(repoPath) {
@@ -42380,12 +43124,12 @@ async function listRepoBranchRefs(repoPath) {
42380
43124
  // src/git/bridge-git-context.ts
42381
43125
  function folderNameForRelPath(relPath, bridgeRoot) {
42382
43126
  if (relPath === "." || relPath === "") {
42383
- return path50.basename(path50.resolve(bridgeRoot)) || "repo";
43127
+ return path58.basename(path58.resolve(bridgeRoot)) || "repo";
42384
43128
  }
42385
- return path50.basename(relPath) || relPath;
43129
+ return path58.basename(relPath) || relPath;
42386
43130
  }
42387
43131
  async function discoverGitReposForBridgeContext(bridgeRoot) {
42388
- const root = path50.resolve(bridgeRoot);
43132
+ const root = path58.resolve(bridgeRoot);
42389
43133
  if (await isGitRepoDirectory(root)) {
42390
43134
  const remoteUrl = await getRemoteOriginUrl(root);
42391
43135
  return [{ absolutePath: root, remoteUrl }];
@@ -42393,17 +43137,19 @@ async function discoverGitReposForBridgeContext(bridgeRoot) {
42393
43137
  const [deep, shallow] = await Promise.all([discoverGitReposUnderRoot(root), discoverGitRepos(root)]);
42394
43138
  const byPath = /* @__PURE__ */ new Map();
42395
43139
  for (const repo of [...deep, ...shallow]) {
42396
- byPath.set(path50.resolve(repo.absolutePath), repo);
43140
+ byPath.set(path58.resolve(repo.absolutePath), repo);
42397
43141
  }
42398
43142
  return [...byPath.values()];
42399
43143
  }
42400
43144
  async function getBridgeGitContext(bridgeRoot = getBridgeRoot()) {
42401
- const bridgeResolved = path50.resolve(bridgeRoot);
43145
+ const bridgeResolved = path58.resolve(bridgeRoot);
42402
43146
  const repos = await discoverGitReposForBridgeContext(bridgeResolved);
42403
43147
  const rows = [];
42404
- for (const repo of repos) {
42405
- let rel = path50.relative(bridgeResolved, repo.absolutePath);
42406
- if (rel.startsWith("..") || path50.isAbsolute(rel)) continue;
43148
+ for (let i = 0; i < repos.length; i++) {
43149
+ if (i > 0) await yieldToEventLoop();
43150
+ const repo = repos[i];
43151
+ let rel = path58.relative(bridgeResolved, repo.absolutePath);
43152
+ if (rel.startsWith("..") || path58.isAbsolute(rel)) continue;
42407
43153
  const relPath = rel === "" ? "." : rel.replace(/\\/g, "/");
42408
43154
  const currentBranch = await getCurrentBranch(repo.absolutePath);
42409
43155
  const remoteUrl = repo.remoteUrl.trim() || null;
@@ -42418,11 +43164,11 @@ async function getBridgeGitContext(bridgeRoot = getBridgeRoot()) {
42418
43164
  return rows;
42419
43165
  }
42420
43166
  async function listRepoBranchesForBridge(repoRelPath, bridgeRoot = getBridgeRoot()) {
42421
- const bridgeResolved = path50.resolve(bridgeRoot);
43167
+ const bridgeResolved = path58.resolve(bridgeRoot);
42422
43168
  const rel = repoRelPath.trim() === "." ? "" : repoRelPath.trim().replace(/\\/g, "/");
42423
- const repoPath = rel === "" ? bridgeResolved : path50.join(bridgeResolved, rel);
42424
- const resolved = path50.resolve(repoPath);
42425
- if (!resolved.startsWith(bridgeResolved + path50.sep) && resolved !== bridgeResolved) {
43169
+ const repoPath = rel === "" ? bridgeResolved : path58.join(bridgeResolved, rel);
43170
+ const resolved = path58.resolve(repoPath);
43171
+ if (!resolved.startsWith(bridgeResolved + path58.sep) && resolved !== bridgeResolved) {
42426
43172
  return [];
42427
43173
  }
42428
43174
  return listRepoBranchRefs(resolved);
@@ -42927,10 +43673,10 @@ function listCliAgentCapabilityCacheForWorkspace(db, workspaceId) {
42927
43673
  }
42928
43674
 
42929
43675
  // src/agents/capabilities/warmup-agent-capabilities-on-connect.ts
42930
- import * as path52 from "node:path";
43676
+ import * as path60 from "node:path";
42931
43677
 
42932
43678
  // src/agents/capabilities/probe-one-agent-type-for-capabilities.ts
42933
- import * as path51 from "node:path";
43679
+ import * as path59 from "node:path";
42934
43680
  async function probeOneAgentTypeForCapabilities(params) {
42935
43681
  const { agentType, cwd, workspaceId, log: log2, reportAgentCapabilities, bridgeReport = true, shouldContinue } = params;
42936
43682
  const canContinue = () => shouldContinue?.() !== false;
@@ -42968,7 +43714,7 @@ async function probeOneAgentTypeForCapabilities(params) {
42968
43714
  if (!canContinue()) return false;
42969
43715
  handle = await resolved.createClient({
42970
43716
  command: resolved.command,
42971
- cwd: path51.resolve(cwd),
43717
+ cwd: path59.resolve(cwd),
42972
43718
  backendAgentType: agentType,
42973
43719
  sessionMode: "agent",
42974
43720
  persistedAcpSessionId: null,
@@ -43046,7 +43792,7 @@ async function warmupAgentCapabilitiesOnConnect(params) {
43046
43792
  const { workspaceId, log: log2, getWs } = params;
43047
43793
  const isCurrent = beginAgentCapabilityWarmupRun();
43048
43794
  if (!isCurrent()) return;
43049
- const cwd = path52.resolve(getBridgeRoot());
43795
+ const cwd = path60.resolve(getBridgeRoot());
43050
43796
  async function sendBatchFromCache() {
43051
43797
  if (!isCurrent()) return;
43052
43798
  const socket = getWs();
@@ -43392,18 +44138,18 @@ import * as http from "node:http";
43392
44138
 
43393
44139
  // src/mcp/bridge-access/read-json-body.ts
43394
44140
  function readJsonBody(req) {
43395
- return new Promise((resolve28, reject) => {
44141
+ return new Promise((resolve35, reject) => {
43396
44142
  const chunks = [];
43397
44143
  req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
43398
44144
  req.on("end", () => {
43399
44145
  try {
43400
44146
  const raw = Buffer.concat(chunks).toString("utf8").trim();
43401
44147
  if (!raw) {
43402
- resolve28({});
44148
+ resolve35({});
43403
44149
  return;
43404
44150
  }
43405
44151
  const parsed = JSON.parse(raw);
43406
- resolve28(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
44152
+ resolve35(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
43407
44153
  } catch (e) {
43408
44154
  reject(e);
43409
44155
  }
@@ -43461,7 +44207,7 @@ function createBridgeAccessRequestHandler(registry2) {
43461
44207
  // src/mcp/bridge-access/start-server.ts
43462
44208
  function startBridgeAccessServer(registry2) {
43463
44209
  const server = http.createServer(createBridgeAccessRequestHandler(registry2));
43464
- return new Promise((resolve28, reject) => {
44210
+ return new Promise((resolve35, reject) => {
43465
44211
  server.once("error", reject);
43466
44212
  server.listen(0, "127.0.0.1", () => {
43467
44213
  const addr = server.address();
@@ -43469,7 +44215,7 @@ function startBridgeAccessServer(registry2) {
43469
44215
  reject(new Error("Bridge access server did not bind"));
43470
44216
  return;
43471
44217
  }
43472
- resolve28({
44218
+ resolve35({
43473
44219
  port: addr.port,
43474
44220
  close: () => new Promise((closeResolve, closeReject) => {
43475
44221
  server.close((err) => err ? closeReject(err) : closeResolve());
@@ -43609,8 +44355,8 @@ async function createBridgeConnection(options) {
43609
44355
  getCloudAccessToken: () => tokens.accessToken
43610
44356
  };
43611
44357
  const identifyReportedPaths = {
43612
- bridgeRootPath: path53.resolve(getBridgeRoot()),
43613
- worktreesRootPath: path53.resolve(worktreesRootPath)
44358
+ bridgeRootPath: path61.resolve(getBridgeRoot()),
44359
+ worktreesRootPath: path61.resolve(worktreesRootPath)
43614
44360
  };
43615
44361
  const { connect } = createMainBridgeWebSocketLifecycle({
43616
44362
  state,
@@ -43865,9 +44611,9 @@ async function runCliAction(program2, opts) {
43865
44611
  const firehoseServerUrl = opts.firehoseUrl ?? opts.proxyUrl ?? process.env.BUILDAUTOMATON_FIREHOSE_URL ?? process.env.BUILDAUTOMATON_PROXY_URL ?? DEFAULT_FIREHOSE_URL;
43866
44612
  const bridgeRootOpt = (opts.bridgeRoot && typeof opts.bridgeRoot === "string" && opts.bridgeRoot.trim() ? opts.bridgeRoot.trim() : null) ?? (opts.cwd && typeof opts.cwd === "string" && opts.cwd.trim() ? opts.cwd.trim() : null);
43867
44613
  if (bridgeRootOpt) {
43868
- const resolvedBridgeRoot = path54.resolve(process.cwd(), bridgeRootOpt);
44614
+ const resolvedBridgeRoot = path62.resolve(process.cwd(), bridgeRootOpt);
43869
44615
  try {
43870
- const st = fs45.statSync(resolvedBridgeRoot);
44616
+ const st = fs46.statSync(resolvedBridgeRoot);
43871
44617
  if (!st.isDirectory()) {
43872
44618
  console.error(`Bridge root is not a directory: ${resolvedBridgeRoot}`);
43873
44619
  process.exit(1);
@@ -43887,7 +44633,7 @@ async function runCliAction(program2, opts) {
43887
44633
  );
43888
44634
  let worktreesRootPath;
43889
44635
  if (opts.worktreesRoot && opts.worktreesRoot.trim()) {
43890
- worktreesRootPath = path54.resolve(opts.worktreesRoot.trim());
44636
+ worktreesRootPath = path62.resolve(opts.worktreesRoot.trim());
43891
44637
  }
43892
44638
  const e2eCertificates = opts.e2eeCertificatesDir?.trim() ? await loadOrCreateE2eCertificates(opts.e2eeCertificatesDir.trim()) : void 0;
43893
44639
  if (e2eCertificates) {