@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/index.js CHANGED
@@ -4175,8 +4175,8 @@ var init_parseUtil = __esm({
4175
4175
  init_errors();
4176
4176
  init_en();
4177
4177
  makeIssue = (params) => {
4178
- const { data, path: path53, errorMaps, issueData } = params;
4179
- const fullPath = [...path53, ...issueData.path || []];
4178
+ const { data, path: path61, errorMaps, issueData } = params;
4179
+ const fullPath = [...path61, ...issueData.path || []];
4180
4180
  const fullIssue = {
4181
4181
  ...issueData,
4182
4182
  path: fullPath
@@ -4484,11 +4484,11 @@ var init_types = __esm({
4484
4484
  init_parseUtil();
4485
4485
  init_util();
4486
4486
  ParseInputLazyPath = class {
4487
- constructor(parent, value, path53, key) {
4487
+ constructor(parent, value, path61, key) {
4488
4488
  this._cachedPath = [];
4489
4489
  this.parent = parent;
4490
4490
  this.data = value;
4491
- this._path = path53;
4491
+ this._path = path61;
4492
4492
  this._key = key;
4493
4493
  }
4494
4494
  get path() {
@@ -8103,10 +8103,10 @@ function assignProp(target, prop, value) {
8103
8103
  configurable: true
8104
8104
  });
8105
8105
  }
8106
- function getElementAtPath(obj, path53) {
8107
- if (!path53)
8106
+ function getElementAtPath(obj, path61) {
8107
+ if (!path61)
8108
8108
  return obj;
8109
- return path53.reduce((acc, key) => acc?.[key], obj);
8109
+ return path61.reduce((acc, key) => acc?.[key], obj);
8110
8110
  }
8111
8111
  function promiseAllObject(promisesObj) {
8112
8112
  const keys = Object.keys(promisesObj);
@@ -8355,11 +8355,11 @@ function aborted(x, startIndex = 0) {
8355
8355
  }
8356
8356
  return false;
8357
8357
  }
8358
- function prefixIssues(path53, issues) {
8358
+ function prefixIssues(path61, issues) {
8359
8359
  return issues.map((iss) => {
8360
8360
  var _a2;
8361
8361
  (_a2 = iss).path ?? (_a2.path = []);
8362
- iss.path.unshift(path53);
8362
+ iss.path.unshift(path61);
8363
8363
  return iss;
8364
8364
  });
8365
8365
  }
@@ -8548,7 +8548,7 @@ function treeifyError(error40, _mapper) {
8548
8548
  return issue2.message;
8549
8549
  };
8550
8550
  const result = { errors: [] };
8551
- const processError = (error41, path53 = []) => {
8551
+ const processError = (error41, path61 = []) => {
8552
8552
  var _a2, _b;
8553
8553
  for (const issue2 of error41.issues) {
8554
8554
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -8558,7 +8558,7 @@ function treeifyError(error40, _mapper) {
8558
8558
  } else if (issue2.code === "invalid_element") {
8559
8559
  processError({ issues: issue2.issues }, issue2.path);
8560
8560
  } else {
8561
- const fullpath = [...path53, ...issue2.path];
8561
+ const fullpath = [...path61, ...issue2.path];
8562
8562
  if (fullpath.length === 0) {
8563
8563
  result.errors.push(mapper(issue2));
8564
8564
  continue;
@@ -8588,9 +8588,9 @@ function treeifyError(error40, _mapper) {
8588
8588
  processError(error40);
8589
8589
  return result;
8590
8590
  }
8591
- function toDotPath(path53) {
8591
+ function toDotPath(path61) {
8592
8592
  const segs = [];
8593
- for (const seg of path53) {
8593
+ for (const seg of path61) {
8594
8594
  if (typeof seg === "number")
8595
8595
  segs.push(`[${seg}]`);
8596
8596
  else if (typeof seg === "symbol")
@@ -21053,8 +21053,8 @@ var init_acp = __esm({
21053
21053
  this.#requestHandler = requestHandler;
21054
21054
  this.#notificationHandler = notificationHandler;
21055
21055
  this.#stream = stream;
21056
- this.#closedPromise = new Promise((resolve26) => {
21057
- this.#abortController.signal.addEventListener("abort", () => resolve26());
21056
+ this.#closedPromise = new Promise((resolve33) => {
21057
+ this.#abortController.signal.addEventListener("abort", () => resolve33());
21058
21058
  });
21059
21059
  this.#receive();
21060
21060
  }
@@ -21203,8 +21203,8 @@ var init_acp = __esm({
21203
21203
  }
21204
21204
  async sendRequest(method, params) {
21205
21205
  const id = this.#nextRequestId++;
21206
- const responsePromise = new Promise((resolve26, reject) => {
21207
- this.#pendingResponses.set(id, { resolve: resolve26, reject });
21206
+ const responsePromise = new Promise((resolve33, reject) => {
21207
+ this.#pendingResponses.set(id, { resolve: resolve33, reject });
21208
21208
  });
21209
21209
  await this.#sendMessage({ jsonrpc: "2.0", id, method, params });
21210
21210
  return responsePromise;
@@ -22073,10 +22073,10 @@ var require_src2 = __commonJS({
22073
22073
  var fs_1 = __require("fs");
22074
22074
  var debug_1 = __importDefault(require_src());
22075
22075
  var log2 = debug_1.default("@kwsites/file-exists");
22076
- function check2(path53, isFile, isDirectory) {
22077
- log2(`checking %s`, path53);
22076
+ function check2(path61, isFile, isDirectory) {
22077
+ log2(`checking %s`, path61);
22078
22078
  try {
22079
- const stat2 = fs_1.statSync(path53);
22079
+ const stat2 = fs_1.statSync(path61);
22080
22080
  if (stat2.isFile() && isFile) {
22081
22081
  log2(`[OK] path represents a file`);
22082
22082
  return true;
@@ -22096,8 +22096,8 @@ var require_src2 = __commonJS({
22096
22096
  throw e;
22097
22097
  }
22098
22098
  }
22099
- function exists2(path53, type = exports.READABLE) {
22100
- return check2(path53, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
22099
+ function exists2(path61, type = exports.READABLE) {
22100
+ return check2(path61, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
22101
22101
  }
22102
22102
  exports.exists = exists2;
22103
22103
  exports.FILE = 1;
@@ -22446,10 +22446,10 @@ async function killChildProcessTreeGracefully(child, graceMs = ACP_PROCESS_TREE_
22446
22446
  }
22447
22447
  await killProcessTree(pid, "SIGTERM");
22448
22448
  if (graceMs <= 0) return;
22449
- const exited = new Promise((resolve26) => {
22450
- child.once("exit", () => resolve26());
22449
+ const exited = new Promise((resolve33) => {
22450
+ child.once("exit", () => resolve33());
22451
22451
  });
22452
- await Promise.race([exited, new Promise((resolve26) => setTimeout(resolve26, graceMs))]);
22452
+ await Promise.race([exited, new Promise((resolve33) => setTimeout(resolve33, graceMs))]);
22453
22453
  if (child.exitCode == null && child.signalCode == null) {
22454
22454
  await killProcessTree(pid, "SIGKILL");
22455
22455
  }
@@ -22466,8 +22466,8 @@ async function gracefulAcpSubprocessDisconnect(params) {
22466
22466
  await transport.closeSession?.(sessionId);
22467
22467
  } catch {
22468
22468
  }
22469
- await new Promise((resolve26) => {
22470
- const timer = setTimeout(resolve26, ACP_GRACEFUL_SESSION_CLOSE_WAIT_MS);
22469
+ await new Promise((resolve33) => {
22470
+ const timer = setTimeout(resolve33, ACP_GRACEFUL_SESSION_CLOSE_WAIT_MS);
22471
22471
  timer.unref?.();
22472
22472
  });
22473
22473
  }
@@ -23172,9 +23172,9 @@ function parseChangeSummaryJson(raw, allowedPaths, options) {
23172
23172
  const rawPath = typeof o.path === "string" ? o.path.trim() : "";
23173
23173
  const summary = typeof o.summary === "string" ? o.summary.trim() : "";
23174
23174
  if (!rawPath || !summary) continue;
23175
- const path53 = skip ? normalizeRepoRelativePath(rawPath) || rawPath : resolveChangeSummaryPathAgainstAllowed(rawPath, allowedPaths);
23176
- if (!path53) continue;
23177
- rows.push({ path: path53, summary: clampSummaryToAtMostTwoLines(summary) });
23175
+ const path61 = skip ? normalizeRepoRelativePath(rawPath) || rawPath : resolveChangeSummaryPathAgainstAllowed(rawPath, allowedPaths);
23176
+ if (!path61) continue;
23177
+ rows.push({ path: path61, summary: clampSummaryToAtMostTwoLines(summary) });
23178
23178
  }
23179
23179
  return rows;
23180
23180
  }
@@ -23628,11 +23628,11 @@ function createSdkStdioExtNotificationHandler(options) {
23628
23628
  // src/agents/acp/clients/sdk/sdk-stdio-permission-request-handshake.ts
23629
23629
  function awaitSdkStdioPermissionRequestHandshake(params) {
23630
23630
  const { requestId, paramsRecord, pending, onRequest } = params;
23631
- return new Promise((resolve26) => {
23632
- pending.set(requestId, { resolve: resolve26, params: paramsRecord });
23631
+ return new Promise((resolve33) => {
23632
+ pending.set(requestId, { resolve: resolve33, params: paramsRecord });
23633
23633
  if (onRequest == null) {
23634
23634
  pending.delete(requestId);
23635
- resolve26({ outcome: { outcome: "denied" } });
23635
+ resolve33({ outcome: { outcome: "denied" } });
23636
23636
  return;
23637
23637
  }
23638
23638
  try {
@@ -24176,10 +24176,10 @@ function createSdkStdioInitSettle(child) {
24176
24176
  forceAcpSubprocessDisconnect(child);
24177
24177
  reject(err);
24178
24178
  },
24179
- settleResolve(resolve26, handle) {
24179
+ settleResolve(resolve33, handle) {
24180
24180
  if (initSettled) return;
24181
24181
  initSettled = true;
24182
- resolve26(handle);
24182
+ resolve33(handle);
24183
24183
  }
24184
24184
  };
24185
24185
  }
@@ -24248,7 +24248,7 @@ async function createSdkStdioAcpClient(options) {
24248
24248
  onFileChange,
24249
24249
  stderrCapture
24250
24250
  });
24251
- return new Promise((resolve26, reject) => {
24251
+ return new Promise((resolve33, reject) => {
24252
24252
  const init = createSdkStdioInitSettle(child);
24253
24253
  child.on("error", (err) => {
24254
24254
  init.settleReject(reject, new Error(formatSpawnError(err, command[0])));
@@ -24273,7 +24273,7 @@ async function createSdkStdioAcpClient(options) {
24273
24273
  protocolVersion: PROTOCOL_VERSION2
24274
24274
  });
24275
24275
  init.settleResolve(
24276
- resolve26,
24276
+ resolve33,
24277
24277
  createSdkStdioHandle({
24278
24278
  child,
24279
24279
  sessionId: established.sessionId,
@@ -24381,7 +24381,7 @@ async function proxyToLocal(request) {
24381
24381
  };
24382
24382
  const maxAttempts = isIdempotentProxyMethod(request.method) ? LOCAL_PREVIEW_FETCH_RETRY_DELAYS_MS.length + 1 : 1;
24383
24383
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
24384
- const once = await new Promise((resolve26) => {
24384
+ const once = await new Promise((resolve33) => {
24385
24385
  const req = mod.request(opts, (res) => {
24386
24386
  const chunks = [];
24387
24387
  res.on("data", (c) => chunks.push(c));
@@ -24392,7 +24392,7 @@ async function proxyToLocal(request) {
24392
24392
  if (typeof v === "string") headers[k] = v;
24393
24393
  else if (Array.isArray(v) && v[0]) headers[k] = v[0];
24394
24394
  }
24395
- resolve26({
24395
+ resolve33({
24396
24396
  id: request.id,
24397
24397
  statusCode: res.statusCode ?? 0,
24398
24398
  headers,
@@ -24401,7 +24401,7 @@ async function proxyToLocal(request) {
24401
24401
  });
24402
24402
  });
24403
24403
  req.on("error", (err) => {
24404
- resolve26({
24404
+ resolve33({
24405
24405
  id: request.id,
24406
24406
  statusCode: 0,
24407
24407
  headers: {},
@@ -24483,8 +24483,8 @@ function randomSecret() {
24483
24483
  }
24484
24484
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
24485
24485
  }
24486
- async function requestPreviewApi(port, secret, method, path53, body) {
24487
- const url2 = `http://127.0.0.1:${port}${path53}`;
24486
+ async function requestPreviewApi(port, secret, method, path61, body) {
24487
+ const url2 = `http://127.0.0.1:${port}${path61}`;
24488
24488
  const headers = {
24489
24489
  [PREVIEW_SECRET_HEADER]: secret,
24490
24490
  "Content-Type": "application/json"
@@ -24496,7 +24496,7 @@ async function requestPreviewApi(port, secret, method, path53, body) {
24496
24496
  });
24497
24497
  const data = await res.json().catch(() => ({}));
24498
24498
  if (!res.ok) {
24499
- throw new Error(data?.error ?? `Preview API ${method} ${path53}: ${res.status}`);
24499
+ throw new Error(data?.error ?? `Preview API ${method} ${path61}: ${res.status}`);
24500
24500
  }
24501
24501
  return data;
24502
24502
  }
@@ -24711,7 +24711,7 @@ function installBridgeProcessResilience() {
24711
24711
  }
24712
24712
 
24713
24713
  // src/cli-version.ts
24714
- var CLI_VERSION = "0.1.50".length > 0 ? "0.1.50" : "0.0.0-dev";
24714
+ var CLI_VERSION = "0.1.52".length > 0 ? "0.1.52" : "0.0.0-dev";
24715
24715
 
24716
24716
  // src/connection/heartbeat/constants.ts
24717
24717
  var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
@@ -25152,14 +25152,14 @@ var baseOpen = async (options) => {
25152
25152
  }
25153
25153
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
25154
25154
  if (options.wait) {
25155
- return new Promise((resolve26, reject) => {
25155
+ return new Promise((resolve33, reject) => {
25156
25156
  subprocess.once("error", reject);
25157
25157
  subprocess.once("close", (exitCode) => {
25158
25158
  if (!options.allowNonzeroExitCode && exitCode > 0) {
25159
25159
  reject(new Error(`Exited with code ${exitCode}`));
25160
25160
  return;
25161
25161
  }
25162
- resolve26(subprocess);
25162
+ resolve33(subprocess);
25163
25163
  });
25164
25164
  });
25165
25165
  }
@@ -25703,8 +25703,8 @@ function runPendingAuth(options) {
25703
25703
  let hasOpenedBrowser = false;
25704
25704
  let resolved = false;
25705
25705
  let resolveAuth;
25706
- const authPromise = new Promise((resolve26) => {
25707
- resolveAuth = resolve26;
25706
+ const authPromise = new Promise((resolve33) => {
25707
+ resolveAuth = resolve33;
25708
25708
  });
25709
25709
  let reconnectAttempt = 0;
25710
25710
  const signInQuiet = createEmptyReconnectQuietSlot();
@@ -25855,7 +25855,7 @@ import sqliteWasm from "node-sqlite3-wasm";
25855
25855
 
25856
25856
  // src/runtime/yield-to-event-loop.ts
25857
25857
  function yieldToEventLoop() {
25858
- return new Promise((resolve26) => setImmediate(resolve26));
25858
+ return new Promise((resolve33) => setImmediate(resolve33));
25859
25859
  }
25860
25860
 
25861
25861
  // src/sqlite/cli-sqlite-paths.ts
@@ -27019,7 +27019,7 @@ async function createCursorAcpClient(options) {
27019
27019
  onFileChange,
27020
27020
  stderrCapture
27021
27021
  });
27022
- return new Promise((resolve26, reject) => {
27022
+ return new Promise((resolve33, reject) => {
27023
27023
  child.on("error", (err) => {
27024
27024
  killChildProcessTree(child, "SIGKILL");
27025
27025
  reject(new Error(formatSpawnError(err, command[0])));
@@ -27031,7 +27031,7 @@ async function createCursorAcpClient(options) {
27031
27031
  sessionCtx,
27032
27032
  incomingDeps: { dbgFs, sessionCtx, onSessionUpdate, onRequest }
27033
27033
  });
27034
- resolve26(
27034
+ resolve33(
27035
27035
  createCursorAcpHandle({
27036
27036
  child,
27037
27037
  sessionId: established.sessionId,
@@ -28237,8 +28237,8 @@ function pathspec(...paths) {
28237
28237
  cache.set(key, paths);
28238
28238
  return key;
28239
28239
  }
28240
- function isPathSpec(path53) {
28241
- return path53 instanceof String && cache.has(path53);
28240
+ function isPathSpec(path61) {
28241
+ return path61 instanceof String && cache.has(path61);
28242
28242
  }
28243
28243
  function toPaths(pathSpec) {
28244
28244
  return cache.get(pathSpec) || [];
@@ -28327,8 +28327,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
28327
28327
  function forEachLineWithContent(input, callback) {
28328
28328
  return toLinesWithContent(input, true).map((line) => callback(line));
28329
28329
  }
28330
- function folderExists(path53) {
28331
- return (0, import_file_exists.exists)(path53, import_file_exists.FOLDER);
28330
+ function folderExists(path61) {
28331
+ return (0, import_file_exists.exists)(path61, import_file_exists.FOLDER);
28332
28332
  }
28333
28333
  function append(target, item) {
28334
28334
  if (Array.isArray(target)) {
@@ -28732,8 +28732,8 @@ function checkIsRepoRootTask() {
28732
28732
  commands,
28733
28733
  format: "utf-8",
28734
28734
  onError,
28735
- parser(path53) {
28736
- return /^\.(git)?$/.test(path53.trim());
28735
+ parser(path61) {
28736
+ return /^\.(git)?$/.test(path61.trim());
28737
28737
  }
28738
28738
  };
28739
28739
  }
@@ -29167,11 +29167,11 @@ function parseGrep(grep) {
29167
29167
  const paths = /* @__PURE__ */ new Set();
29168
29168
  const results = {};
29169
29169
  forEachLineWithContent(grep, (input) => {
29170
- const [path53, line, preview] = input.split(NULL);
29171
- paths.add(path53);
29172
- (results[path53] = results[path53] || []).push({
29170
+ const [path61, line, preview] = input.split(NULL);
29171
+ paths.add(path61);
29172
+ (results[path61] = results[path61] || []).push({
29173
29173
  line: asNumber(line),
29174
- path: path53,
29174
+ path: path61,
29175
29175
  preview
29176
29176
  });
29177
29177
  });
@@ -29936,14 +29936,14 @@ var init_hash_object = __esm2({
29936
29936
  init_task();
29937
29937
  }
29938
29938
  });
29939
- function parseInit(bare, path53, text) {
29939
+ function parseInit(bare, path61, text) {
29940
29940
  const response = String(text).trim();
29941
29941
  let result;
29942
29942
  if (result = initResponseRegex.exec(response)) {
29943
- return new InitSummary(bare, path53, false, result[1]);
29943
+ return new InitSummary(bare, path61, false, result[1]);
29944
29944
  }
29945
29945
  if (result = reInitResponseRegex.exec(response)) {
29946
- return new InitSummary(bare, path53, true, result[1]);
29946
+ return new InitSummary(bare, path61, true, result[1]);
29947
29947
  }
29948
29948
  let gitDir = "";
29949
29949
  const tokens = response.split(" ");
@@ -29954,7 +29954,7 @@ function parseInit(bare, path53, text) {
29954
29954
  break;
29955
29955
  }
29956
29956
  }
29957
- return new InitSummary(bare, path53, /^re/i.test(response), gitDir);
29957
+ return new InitSummary(bare, path61, /^re/i.test(response), gitDir);
29958
29958
  }
29959
29959
  var InitSummary;
29960
29960
  var initResponseRegex;
@@ -29963,9 +29963,9 @@ var init_InitSummary = __esm2({
29963
29963
  "src/lib/responses/InitSummary.ts"() {
29964
29964
  "use strict";
29965
29965
  InitSummary = class {
29966
- constructor(bare, path53, existing, gitDir) {
29966
+ constructor(bare, path61, existing, gitDir) {
29967
29967
  this.bare = bare;
29968
- this.path = path53;
29968
+ this.path = path61;
29969
29969
  this.existing = existing;
29970
29970
  this.gitDir = gitDir;
29971
29971
  }
@@ -29977,7 +29977,7 @@ var init_InitSummary = __esm2({
29977
29977
  function hasBareCommand(command) {
29978
29978
  return command.includes(bareCommand);
29979
29979
  }
29980
- function initTask(bare = false, path53, customArgs) {
29980
+ function initTask(bare = false, path61, customArgs) {
29981
29981
  const commands = ["init", ...customArgs];
29982
29982
  if (bare && !hasBareCommand(commands)) {
29983
29983
  commands.splice(1, 0, bareCommand);
@@ -29986,7 +29986,7 @@ function initTask(bare = false, path53, customArgs) {
29986
29986
  commands,
29987
29987
  format: "utf-8",
29988
29988
  parser(text) {
29989
- return parseInit(commands.includes("--bare"), path53, text);
29989
+ return parseInit(commands.includes("--bare"), path61, text);
29990
29990
  }
29991
29991
  };
29992
29992
  }
@@ -30802,12 +30802,12 @@ var init_FileStatusSummary = __esm2({
30802
30802
  "use strict";
30803
30803
  fromPathRegex = /^(.+)\0(.+)$/;
30804
30804
  FileStatusSummary = class {
30805
- constructor(path53, index, working_dir) {
30806
- this.path = path53;
30805
+ constructor(path61, index, working_dir) {
30806
+ this.path = path61;
30807
30807
  this.index = index;
30808
30808
  this.working_dir = working_dir;
30809
30809
  if (index === "R" || working_dir === "R") {
30810
- const detail = fromPathRegex.exec(path53) || [null, path53, path53];
30810
+ const detail = fromPathRegex.exec(path61) || [null, path61, path61];
30811
30811
  this.from = detail[2] || "";
30812
30812
  this.path = detail[1] || "";
30813
30813
  }
@@ -30838,14 +30838,14 @@ function splitLine(result, lineStr) {
30838
30838
  default:
30839
30839
  return;
30840
30840
  }
30841
- function data(index, workingDir, path53) {
30841
+ function data(index, workingDir, path61) {
30842
30842
  const raw = `${index}${workingDir}`;
30843
30843
  const handler = parsers6.get(raw);
30844
30844
  if (handler) {
30845
- handler(result, path53);
30845
+ handler(result, path61);
30846
30846
  }
30847
30847
  if (raw !== "##" && raw !== "!!") {
30848
- result.files.push(new FileStatusSummary(path53, index, workingDir));
30848
+ result.files.push(new FileStatusSummary(path61, index, workingDir));
30849
30849
  }
30850
30850
  }
30851
30851
  }
@@ -31154,9 +31154,9 @@ var init_simple_git_api = __esm2({
31154
31154
  next
31155
31155
  );
31156
31156
  }
31157
- hashObject(path53, write) {
31157
+ hashObject(path61, write) {
31158
31158
  return this._runTask(
31159
- hashObjectTask(path53, write === true),
31159
+ hashObjectTask(path61, write === true),
31160
31160
  trailingFunctionArgument(arguments)
31161
31161
  );
31162
31162
  }
@@ -31509,8 +31509,8 @@ var init_branch = __esm2({
31509
31509
  }
31510
31510
  });
31511
31511
  function toPath(input) {
31512
- const path53 = input.trim().replace(/^["']|["']$/g, "");
31513
- return path53 && normalize3(path53);
31512
+ const path61 = input.trim().replace(/^["']|["']$/g, "");
31513
+ return path61 && normalize3(path61);
31514
31514
  }
31515
31515
  var parseCheckIgnore;
31516
31516
  var init_CheckIgnore = __esm2({
@@ -31824,8 +31824,8 @@ __export2(sub_module_exports, {
31824
31824
  subModuleTask: () => subModuleTask,
31825
31825
  updateSubModuleTask: () => updateSubModuleTask
31826
31826
  });
31827
- function addSubModuleTask(repo, path53) {
31828
- return subModuleTask(["add", repo, path53]);
31827
+ function addSubModuleTask(repo, path61) {
31828
+ return subModuleTask(["add", repo, path61]);
31829
31829
  }
31830
31830
  function initSubModuleTask(customArgs) {
31831
31831
  return subModuleTask(["init", ...customArgs]);
@@ -32158,8 +32158,8 @@ var require_git = __commonJS2({
32158
32158
  }
32159
32159
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
32160
32160
  };
32161
- Git2.prototype.submoduleAdd = function(repo, path53, then) {
32162
- return this._runTask(addSubModuleTask2(repo, path53), trailingFunctionArgument2(arguments));
32161
+ Git2.prototype.submoduleAdd = function(repo, path61, then) {
32162
+ return this._runTask(addSubModuleTask2(repo, path61), trailingFunctionArgument2(arguments));
32163
32163
  };
32164
32164
  Git2.prototype.submoduleUpdate = function(args, then) {
32165
32165
  return this._runTask(
@@ -32796,8 +32796,8 @@ async function runGitTask(fn) {
32796
32796
  }
32797
32797
  async function yieldToEventLoop2() {
32798
32798
  throwIfGitShutdownRequested();
32799
- await new Promise((resolve26) => {
32800
- setImmediate(resolve26);
32799
+ await new Promise((resolve33) => {
32800
+ setImmediate(resolve33);
32801
32801
  });
32802
32802
  throwIfGitShutdownRequested();
32803
32803
  }
@@ -32837,11 +32837,8 @@ function cliSimpleGit(baseDir) {
32837
32837
  async function getRemoteOriginUrl(gitDir) {
32838
32838
  try {
32839
32839
  const git = cliSimpleGit(gitDir);
32840
- const remotes = await git.getRemotes(true);
32841
- const list = Array.isArray(remotes) ? remotes : [];
32842
- const origin = list.find((r) => r.name === "origin");
32843
- if (!origin?.refs?.fetch && !origin?.refs?.push) return "";
32844
- return (origin.refs.fetch ?? origin.refs.push ?? "").trim();
32840
+ const out = await git.raw(["config", "--get", "remote.origin.url"]);
32841
+ return String(out).trim();
32845
32842
  } catch {
32846
32843
  return "";
32847
32844
  }
@@ -32857,6 +32854,12 @@ async function isGitRepoDirectory(dirPath) {
32857
32854
  }
32858
32855
 
32859
32856
  // src/git/discover-repos.ts
32857
+ var GIT_DISCOVER_YIELD_EVERY = 32;
32858
+ async function yieldGitDiscoverWork(entryCount) {
32859
+ if (entryCount > 0 && entryCount % GIT_DISCOVER_YIELD_EVERY === 0) {
32860
+ await yieldToEventLoop();
32861
+ }
32862
+ }
32860
32863
  async function discoverGitRepos(cwd = getBridgeRoot()) {
32861
32864
  const result = [];
32862
32865
  const cwdResolved = path16.resolve(cwd);
@@ -32870,12 +32873,14 @@ async function discoverGitRepos(cwd = getBridgeRoot()) {
32870
32873
  } catch {
32871
32874
  return result;
32872
32875
  }
32873
- for (const ent of entries) {
32876
+ for (let i = 0; i < entries.length; i++) {
32877
+ await yieldGitDiscoverWork(i + 1);
32878
+ const ent = entries[i];
32874
32879
  if (!ent.isDirectory()) continue;
32875
- const childPath = path16.join(cwdResolved, ent.name);
32876
- if (await isGitRepoDirectory(childPath)) {
32877
- const remoteUrl = await getRemoteOriginUrl(childPath);
32878
- result.push({ absolutePath: childPath, remoteUrl });
32880
+ const childPath2 = path16.join(cwdResolved, ent.name);
32881
+ if (await isGitRepoDirectory(childPath2)) {
32882
+ const remoteUrl = await getRemoteOriginUrl(childPath2);
32883
+ result.push({ absolutePath: childPath2, remoteUrl });
32879
32884
  }
32880
32885
  }
32881
32886
  return result;
@@ -32883,6 +32888,7 @@ async function discoverGitRepos(cwd = getBridgeRoot()) {
32883
32888
  async function discoverGitReposUnderRoot(rootPath) {
32884
32889
  const root = path16.resolve(rootPath);
32885
32890
  const roots = [];
32891
+ let walkEntries = 0;
32886
32892
  async function walk(dir) {
32887
32893
  if (await isGitRepoDirectory(dir)) {
32888
32894
  roots.push(path16.resolve(dir));
@@ -32894,7 +32900,9 @@ async function discoverGitReposUnderRoot(rootPath) {
32894
32900
  } catch {
32895
32901
  return;
32896
32902
  }
32897
- for (const ent of entries) {
32903
+ for (let i = 0; i < entries.length; i++) {
32904
+ await yieldGitDiscoverWork(++walkEntries);
32905
+ const ent = entries[i];
32898
32906
  if (!ent.isDirectory() || ent.name === ".git") continue;
32899
32907
  await walk(path16.join(dir, ent.name));
32900
32908
  }
@@ -32902,7 +32910,9 @@ async function discoverGitReposUnderRoot(rootPath) {
32902
32910
  await walk(root);
32903
32911
  const uniq = [...new Set(roots)];
32904
32912
  const out = [];
32905
- for (const p of uniq) {
32913
+ for (let i = 0; i < uniq.length; i++) {
32914
+ await yieldGitDiscoverWork(i + 1);
32915
+ const p = uniq[i];
32906
32916
  const remoteUrl = await getRemoteOriginUrl(p);
32907
32917
  out.push({ absolutePath: p, remoteUrl });
32908
32918
  }
@@ -33122,9 +33132,9 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
33122
33132
  // src/agents/acp/put-summarize-change-summaries.ts
33123
33133
  async function putEncryptedChangeSummaryRows(params) {
33124
33134
  const base = params.apiBaseUrl.replace(/\/+$/, "");
33125
- const entries = params.rows.map(({ path: path53, summary }) => {
33135
+ const entries = params.rows.map(({ path: path61, summary }) => {
33126
33136
  const enc = params.e2ee.encryptFields({ summary }, ["summary"]);
33127
- return { path: path53, summary: JSON.stringify(enc) };
33137
+ return { path: path61, summary: JSON.stringify(enc) };
33128
33138
  });
33129
33139
  const res = await fetch(
33130
33140
  `${base}/internal/sessions/${encodeURIComponent(params.sessionId)}/follow-ups/summarize-changes`,
@@ -33960,503 +33970,436 @@ function allocateDirNameForLauncherCwd(layout, bridgeRootPath2) {
33960
33970
  return name;
33961
33971
  }
33962
33972
 
33963
- // src/worktrees/discovery/discover-session-worktree-on-disk.ts
33964
- import * as fs20 from "node:fs";
33965
- import * as path24 from "node:path";
33966
-
33967
- // src/worktrees/discovery/collect-worktree-paths.ts
33968
- import * as fs18 from "node:fs";
33969
- import * as path21 from "node:path";
33973
+ // src/worktrees/manager/session-worktree-cache.ts
33974
+ import * as path20 from "node:path";
33975
+ var SessionWorktreeCache = class {
33976
+ sessionRepoCheckoutPaths = /* @__PURE__ */ new Map();
33977
+ sessionParentPathBySession = /* @__PURE__ */ new Map();
33978
+ sessionWorkingTreeRelRootBySession = /* @__PURE__ */ new Map();
33979
+ remember(sessionId, binding) {
33980
+ const paths = binding.repoCheckoutPaths.map((p) => path20.resolve(p));
33981
+ this.sessionRepoCheckoutPaths.set(sessionId, paths);
33982
+ this.sessionParentPathBySession.set(sessionId, path20.resolve(binding.sessionParentPath));
33983
+ this.sessionWorkingTreeRelRootBySession.set(sessionId, path20.resolve(binding.workingTreeRelRoot));
33984
+ }
33985
+ clearSession(sessionId) {
33986
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
33987
+ this.sessionRepoCheckoutPaths.delete(sessionId);
33988
+ this.sessionParentPathBySession.delete(sessionId);
33989
+ this.sessionWorkingTreeRelRootBySession.delete(sessionId);
33990
+ return paths;
33991
+ }
33992
+ getSessionParentPath(sessionId) {
33993
+ return this.sessionParentPathBySession.get(sessionId);
33994
+ }
33995
+ getWorkingTreeRelRoot(sessionId) {
33996
+ return this.sessionWorkingTreeRelRootBySession.get(sessionId) ?? null;
33997
+ }
33998
+ hasSession(sessionId) {
33999
+ return this.sessionParentPathBySession.has(sessionId);
34000
+ }
34001
+ getRepoCheckoutPaths(sessionId) {
34002
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
34003
+ return paths?.length ? [...paths] : void 0;
34004
+ }
34005
+ getRepoCheckoutPathsRef(sessionId) {
34006
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
34007
+ return paths?.length ? paths : void 0;
34008
+ }
34009
+ isLegacyNestedLayout(sessionId) {
34010
+ const parent = this.sessionParentPathBySession.get(sessionId);
34011
+ const relRoot = this.sessionWorkingTreeRelRootBySession.get(sessionId);
34012
+ if (!parent || !relRoot) return false;
34013
+ return path20.resolve(parent) !== path20.resolve(relRoot);
34014
+ }
34015
+ };
33970
34016
 
33971
- // src/worktrees/discovery/disk-walk-constants.ts
33972
- var DISK_WALK_YIELD_EVERY = 64;
33973
- var LEGACY_WALK_MAX_DEPTH = 12;
33974
- var SKIP_DISK_WALK_DIR_NAMES = /* @__PURE__ */ new Set([
33975
- "node_modules",
33976
- "bower_components",
33977
- "vendor",
33978
- "Pods",
33979
- "Carthage",
33980
- "DerivedData",
33981
- ".yarn",
33982
- ".pnpm-store",
33983
- "dist",
33984
- "build",
33985
- ".next",
33986
- "coverage",
33987
- ".turbo",
33988
- ".cache",
33989
- "target",
33990
- "__pycache__"
33991
- ]);
33992
- function shouldSkipDiskWalkEntry(name) {
33993
- return name.startsWith(".") || SKIP_DISK_WALK_DIR_NAMES.has(name);
34017
+ // src/worktrees/manager/session-worktree-manager-context.ts
34018
+ function createSessionWorktreeManagerContext(options) {
34019
+ return {
34020
+ worktreesRootPath: options.worktreesRootPath,
34021
+ log: options.log,
34022
+ cache: new SessionWorktreeCache(),
34023
+ layout: loadWorktreeLayout()
34024
+ };
33994
34025
  }
33995
34026
 
33996
- // src/worktrees/discovery/disk-walk-utils.ts
33997
- import * as fs17 from "node:fs";
33998
- import * as path20 from "node:path";
33999
- async function yieldDuringDiskWalk(state) {
34000
- state.entries++;
34001
- if (state.entries % DISK_WALK_YIELD_EVERY !== 0) return true;
34002
- if (isCliImmediateShutdownRequested()) return false;
34003
- await yieldToEventLoop();
34004
- return !isCliImmediateShutdownRequested();
34005
- }
34006
- async function isGitDir(dirPath) {
34007
- try {
34008
- await fs17.promises.access(path20.join(dirPath, ".git"));
34009
- return true;
34010
- } catch {
34011
- return false;
34027
+ // src/git/branches/commit-and-push.ts
34028
+ async function gitCommitAllIfDirty(repoDir, message, options) {
34029
+ const g = cliSimpleGit(repoDir);
34030
+ const st = await g.status();
34031
+ if (!st.files?.length) return;
34032
+ const branch = options.branch.trim();
34033
+ if (!branch) {
34034
+ throw new Error("Branch name is required");
34035
+ }
34036
+ const branches = await g.branchLocal();
34037
+ const localNames = new Set(branches.all.map((b) => b.replace(/^\*\s*/, "").trim()));
34038
+ if (!localNames.has(branch)) {
34039
+ await g.checkoutLocalBranch(branch);
34040
+ } else {
34041
+ await g.checkout(branch);
34042
+ }
34043
+ await g.add(".");
34044
+ await g.commit(message);
34045
+ if (options.push) {
34046
+ await g.push(["-u", "origin", branch]);
34012
34047
  }
34013
34048
  }
34014
34049
 
34015
- // src/worktrees/discovery/collect-worktree-paths.ts
34016
- async function collectGitRepoRootsUnderDirectory(rootPath) {
34017
- const out = [];
34018
- const state = { entries: 0 };
34019
- const walk = async (dir) => {
34020
- if (!await yieldDuringDiskWalk(state)) return;
34021
- if (await isGitDir(dir)) {
34022
- out.push(path21.resolve(dir));
34023
- return;
34024
- }
34025
- let entries;
34026
- try {
34027
- entries = await fs18.promises.readdir(dir, { withFileTypes: true });
34028
- } catch {
34029
- return;
34030
- }
34031
- for (const e of entries) {
34032
- if (shouldSkipDiskWalkEntry(e.name)) continue;
34033
- const full = path21.join(dir, e.name);
34034
- if (!e.isDirectory()) continue;
34035
- await walk(full);
34050
+ // src/worktrees/commit-session-worktrees.ts
34051
+ async function commitSessionWorktrees(options) {
34052
+ const { paths, branch, message, push } = options;
34053
+ try {
34054
+ for (const wt of paths) {
34055
+ await gitCommitAllIfDirty(wt, message, { push, branch });
34036
34056
  }
34037
- };
34038
- await walk(path21.resolve(rootPath));
34039
- return { paths: [...new Set(out)], entriesVisited: state.entries };
34057
+ return { ok: true };
34058
+ } catch (e) {
34059
+ const err = e instanceof Error ? e.message : String(e);
34060
+ return { ok: false, error: err };
34061
+ }
34040
34062
  }
34041
- async function collectWorktreeRootsNamed(root, sessionId, maxDepth = LEGACY_WALK_MAX_DEPTH) {
34042
- const out = [];
34043
- const state = { entries: 0 };
34044
- const walk = async (dir, depth) => {
34045
- if (depth > maxDepth) return;
34046
- if (!await yieldDuringDiskWalk(state)) return;
34047
- let entries;
34048
- try {
34049
- entries = await fs18.promises.readdir(dir, { withFileTypes: true });
34050
- } catch {
34051
- return;
34052
- }
34053
- for (const e of entries) {
34054
- if (shouldSkipDiskWalkEntry(e.name)) continue;
34055
- const full = path21.join(dir, e.name);
34056
- if (!e.isDirectory()) continue;
34057
- if (e.name === sessionId) {
34058
- if (await isGitDir(full)) out.push(path21.resolve(full));
34059
- continue;
34060
- }
34061
- if (await isGitDir(full)) continue;
34062
- await walk(full, depth + 1);
34063
- }
34064
- };
34065
- await walk(root, 0);
34066
- return { paths: out, entriesVisited: state.entries };
34063
+
34064
+ // src/worktrees/manager/git/commit-session-worktree.ts
34065
+ async function commitSessionWorktree(cache2, params) {
34066
+ const paths = cache2.getRepoCheckoutPathsRef(params.sessionId);
34067
+ const targets = paths?.length ? paths : [getBridgeRoot()];
34068
+ return commitSessionWorktrees({
34069
+ paths: targets,
34070
+ branch: params.branch,
34071
+ message: params.message,
34072
+ push: params.push
34073
+ });
34067
34074
  }
34068
34075
 
34069
- // src/worktrees/discovery/layout-keys-for-bridge-root.ts
34070
- import * as path22 from "node:path";
34071
- function layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk) {
34072
- const bridge = path22.resolve(bridgeRoot);
34073
- const preferred = getLauncherDirNameIfPresent(layout, bridgeRoot);
34074
- const relevant = layout.launcherCwds.filter((e) => {
34075
- const entry = path22.resolve(e.absolutePath);
34076
- return bridge === entry || bridge.startsWith(entry + path22.sep) || entry.startsWith(bridge + path22.sep);
34077
- }).sort((a, b) => path22.resolve(b.absolutePath).length - path22.resolve(a.absolutePath).length).map((e) => e.dirName).filter((name) => keysOnDisk.includes(name));
34078
- const ordered = [];
34079
- const seen = /* @__PURE__ */ new Set();
34080
- const add = (k) => {
34081
- if (seen.has(k) || !keysOnDisk.includes(k)) return;
34082
- seen.add(k);
34083
- ordered.push(k);
34084
- };
34085
- if (preferred) add(preferred);
34086
- for (const k of relevant) add(k);
34087
- if (relevant.length > 0) {
34088
- return ordered;
34089
- }
34090
- for (const k of keysOnDisk) add(k);
34091
- return ordered;
34076
+ // src/git/changes/parse/normalize-git-diff-path.ts
34077
+ function normalizeGitDiffPath(path61) {
34078
+ return path61.replace(/\\/g, "/").replace(/\/ +/g, "/").trim();
34092
34079
  }
34093
34080
 
34094
- // src/worktrees/discovery/try-binding-from-session-directory.ts
34095
- import * as fs19 from "node:fs";
34096
- import * as path23 from "node:path";
34097
- async function tryBindingFromSessionDirectory(sessionDir) {
34098
- let st;
34099
- try {
34100
- st = await fs19.promises.stat(sessionDir);
34101
- } catch {
34081
+ // src/git/changes/parse/expand-git-rename-numstat-path.ts
34082
+ function expandGitRenameNumstatPath(rawPath) {
34083
+ const open2 = rawPath.indexOf("{");
34084
+ const arrow = rawPath.indexOf("=>");
34085
+ const close = rawPath.lastIndexOf("}");
34086
+ if (open2 === -1 || arrow === -1 || close === -1 || open2 > arrow || arrow > close) {
34102
34087
  return null;
34103
34088
  }
34104
- if (!st.isDirectory()) return null;
34105
- const { paths: worktreePaths } = await collectGitRepoRootsUnderDirectory(sessionDir);
34106
- if (worktreePaths.length === 0) return null;
34107
- const abs = path23.resolve(sessionDir);
34089
+ const prefix = rawPath.slice(0, open2);
34090
+ const suffix = rawPath.slice(close + 1);
34091
+ const oldPart = rawPath.slice(open2 + 1, arrow).trim();
34092
+ const newPart = rawPath.slice(arrow + 2, close).trim();
34108
34093
  return {
34109
- sessionParentPath: abs,
34110
- workingTreeRelRoot: abs,
34111
- repoCheckoutPaths: worktreePaths
34094
+ oldPath: normalizeGitDiffPath(`${prefix}${oldPart}${suffix}`),
34095
+ newPath: normalizeGitDiffPath(`${prefix}${newPart}${suffix}`)
34112
34096
  };
34113
34097
  }
34098
+ function isGitRenameNumstatPath(rawPath) {
34099
+ return expandGitRenameNumstatPath(rawPath) != null;
34100
+ }
34114
34101
 
34115
- // src/worktrees/discovery/discover-session-worktree-on-disk.ts
34116
- async function discoverSessionWorktreeOnDisk(options) {
34117
- const { sessionId, worktreesRootPath, layout, bridgeRoot } = options;
34118
- const sid = sessionId.trim();
34119
- if (!sid) return null;
34120
- try {
34121
- await fs20.promises.access(worktreesRootPath);
34122
- } catch {
34123
- return null;
34124
- }
34125
- const preferredKey = getLauncherDirNameIfPresent(layout, bridgeRoot);
34126
- const keysOnDisk = [];
34127
- if (preferredKey) keysOnDisk.push(preferredKey);
34128
- try {
34129
- for (const name of await fs20.promises.readdir(worktreesRootPath)) {
34130
- if (name.startsWith(".")) continue;
34131
- const p = path24.join(worktreesRootPath, name);
34132
- let st;
34133
- try {
34134
- st = await fs20.promises.stat(p);
34135
- } catch {
34136
- continue;
34102
+ // src/git/changes/parse/parse-name-status-entries.ts
34103
+ function parseNameStatusEntries(lines) {
34104
+ const entries = [];
34105
+ for (const line of lines) {
34106
+ if (!line.trim()) continue;
34107
+ const tabParts = line.split(" ");
34108
+ if (tabParts.length < 2) continue;
34109
+ const status = tabParts[0].trim();
34110
+ const code = status[0];
34111
+ if (code === "A") {
34112
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "added" });
34113
+ } else if (code === "D") {
34114
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "removed" });
34115
+ } else if (code === "R" || code === "C") {
34116
+ if (tabParts.length >= 3) {
34117
+ entries.push({
34118
+ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]),
34119
+ oldPathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 2]),
34120
+ change: "moved"
34121
+ });
34137
34122
  }
34138
- if (!st.isDirectory()) continue;
34139
- if (!keysOnDisk.includes(name)) keysOnDisk.push(name);
34140
- }
34141
- } catch {
34142
- return null;
34143
- }
34144
- const keys = layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk);
34145
- for (const key of keys) {
34146
- if (isCliImmediateShutdownRequested()) return null;
34147
- await yieldToEventLoop();
34148
- const layoutRoot = path24.join(worktreesRootPath, key);
34149
- let layoutSt;
34150
- try {
34151
- layoutSt = await fs20.promises.stat(layoutRoot);
34152
- } catch {
34153
- continue;
34154
- }
34155
- if (!layoutSt.isDirectory()) continue;
34156
- const sessionDir = path24.join(layoutRoot, sid);
34157
- const nested = await tryBindingFromSessionDirectory(sessionDir);
34158
- if (nested) return nested;
34159
- const legacy = await collectWorktreeRootsNamed(layoutRoot, sid);
34160
- if (legacy.paths.length > 0) {
34161
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacy.paths) ?? path24.resolve(legacy.paths[0]);
34162
- return {
34163
- sessionParentPath: path24.resolve(isolated),
34164
- workingTreeRelRoot: path24.resolve(layoutRoot),
34165
- repoCheckoutPaths: legacy.paths.map((p) => path24.resolve(p))
34166
- };
34123
+ } else if (code === "M" || code === "U" || code === "T") {
34124
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "modified" });
34167
34125
  }
34168
34126
  }
34169
- return null;
34127
+ return entries;
34170
34128
  }
34171
-
34172
- // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
34173
- import * as fs21 from "node:fs";
34174
- import * as path26 from "node:path";
34175
-
34176
- // src/worktrees/discovery/discover-legacy-binding-ascending-from-checkout.ts
34177
- import * as path25 from "node:path";
34178
- async function discoverLegacyBindingAscendingFromCheckout(sessionId, checkoutPath) {
34179
- const sid = sessionId.trim();
34180
- if (!sid) return null;
34181
- const hintR = path25.resolve(checkoutPath);
34182
- let best = null;
34183
- let cur = path25.dirname(hintR);
34184
- for (let i = 0; i < 40; i++) {
34185
- if (isCliImmediateShutdownRequested()) return best;
34186
- await yieldToEventLoop();
34187
- const paths = await collectWorktreeRootsNamed(cur, sid);
34188
- if (paths.paths.some((p) => path25.resolve(p) === hintR)) {
34189
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(paths.paths) ?? path25.resolve(paths.paths[0]);
34190
- best = {
34191
- sessionParentPath: path25.resolve(isolated),
34192
- workingTreeRelRoot: path25.resolve(cur),
34193
- repoCheckoutPaths: paths.paths.map((p) => path25.resolve(p))
34194
- };
34195
- }
34196
- const next = path25.dirname(cur);
34197
- if (next === cur) break;
34198
- cur = next;
34129
+ function parseNameStatusLines(lines) {
34130
+ const m = /* @__PURE__ */ new Map();
34131
+ for (const entry of parseNameStatusEntries(lines)) {
34132
+ m.set(entry.pathInRepo, entry.change);
34199
34133
  }
34200
- return best;
34134
+ return m;
34201
34135
  }
34202
34136
 
34203
- // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
34204
- async function discoverSessionWorktreesUnderSessionWorktreeRoot(sessionWorktreeRootPathOrHint, sessionId) {
34205
- const sid = sessionId.trim();
34206
- if (!sid) return null;
34207
- const hint = path26.resolve(sessionWorktreeRootPathOrHint);
34208
- const underHint = await tryBindingFromSessionDirectory(path26.join(hint, sid));
34209
- if (underHint) return underHint;
34210
- const direct = await tryBindingFromSessionDirectory(hint);
34211
- if (direct) {
34212
- if (path26.basename(hint) === sid && await isGitDir(hint)) {
34213
- const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
34214
- if (legacyFromCheckout && legacyFromCheckout.repoCheckoutPaths.length > direct.repoCheckoutPaths.length) {
34215
- return legacyFromCheckout;
34216
- }
34137
+ // src/git/changes/parse/parse-numstat-entries.ts
34138
+ function parseNumstatCounts(parts) {
34139
+ const [a, d] = parts;
34140
+ return {
34141
+ additions: a === "-" ? 0 : parseInt(String(a), 10) || 0,
34142
+ deletions: d === "-" ? 0 : parseInt(String(d), 10) || 0
34143
+ };
34144
+ }
34145
+ function parseNumstatLine(line) {
34146
+ const parts = line.split(" ");
34147
+ if (parts.length < 3) return null;
34148
+ const rawPath = parts[parts.length - 1];
34149
+ const { additions, deletions } = parseNumstatCounts(parts);
34150
+ const expanded = expandGitRenameNumstatPath(rawPath);
34151
+ const pathInRepo = normalizeGitDiffPath(expanded?.newPath ?? rawPath);
34152
+ return {
34153
+ pathInRepo,
34154
+ additions,
34155
+ deletions,
34156
+ ...expanded ? { oldPathInRepo: expanded.oldPath } : {}
34157
+ };
34158
+ }
34159
+ function parseNumstatEntries(lines) {
34160
+ const m = /* @__PURE__ */ new Map();
34161
+ for (const line of lines) {
34162
+ if (!line.trim()) continue;
34163
+ const parts = line.split(" ");
34164
+ if (parts.length < 3) continue;
34165
+ const rawPath = parts[parts.length - 1];
34166
+ const expanded = expandGitRenameNumstatPath(rawPath);
34167
+ const entry = parseNumstatLine(line);
34168
+ if (!entry) continue;
34169
+ const existing = m.get(entry.pathInRepo);
34170
+ if (!existing || expanded) {
34171
+ m.set(entry.pathInRepo, entry);
34217
34172
  }
34218
- return direct;
34219
34173
  }
34220
- if (path26.basename(hint) === sid && await isGitDir(hint)) {
34221
- const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
34222
- if (legacyFromCheckout) return legacyFromCheckout;
34174
+ return m;
34175
+ }
34176
+ function parseNumstat(lines) {
34177
+ const m = /* @__PURE__ */ new Map();
34178
+ for (const [path61, entry] of parseNumstatEntries(lines)) {
34179
+ m.set(path61, { additions: entry.additions, deletions: entry.deletions });
34223
34180
  }
34224
- let st;
34181
+ return m;
34182
+ }
34183
+
34184
+ // src/git/changes/parse/numstat-from-git-no-index.ts
34185
+ async function numstatFromGitNoIndex(g, pathInRepo) {
34186
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
34225
34187
  try {
34226
- st = await fs21.promises.stat(hint);
34188
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
34189
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
34190
+ const parsed = parseNumstatLine(first2);
34191
+ if (!parsed) return null;
34192
+ return { additions: parsed.additions, deletions: parsed.deletions };
34227
34193
  } catch {
34228
34194
  return null;
34229
34195
  }
34230
- if (!st.isDirectory()) return null;
34231
- const legacyPaths = await collectWorktreeRootsNamed(hint, sid);
34232
- if (legacyPaths.paths.length === 0) return null;
34233
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths.paths) ?? path26.resolve(legacyPaths.paths[0]);
34234
- return {
34235
- sessionParentPath: path26.resolve(isolated),
34236
- workingTreeRelRoot: hint,
34237
- repoCheckoutPaths: legacyPaths.paths.map((p) => path26.resolve(p))
34238
- };
34239
34196
  }
34240
34197
 
34241
- // src/worktrees/manager/discover-session-binding.ts
34242
- var discoverInflightBySessionId = /* @__PURE__ */ new Map();
34243
- function discoverSessionBindingAsync(params) {
34244
- const sid = params.sessionId.trim();
34245
- if (!sid) return Promise.resolve(null);
34246
- const existing = discoverInflightBySessionId.get(sid);
34247
- if (existing) return existing;
34248
- const promise2 = discoverSessionWorktreeOnDisk({
34249
- sessionId: sid,
34250
- worktreesRootPath: params.worktreesRootPath,
34251
- layout: params.layout,
34252
- bridgeRoot: getBridgeRoot()
34253
- }).finally(() => {
34254
- if (discoverInflightBySessionId.get(sid) === promise2) {
34255
- discoverInflightBySessionId.delete(sid);
34256
- }
34257
- });
34258
- discoverInflightBySessionId.set(sid, promise2);
34259
- return promise2;
34198
+ // src/git/changes/status/working-tree-changed-path-count.ts
34199
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
34200
+ const kindByPath = parseNameStatusLines(nameStatusLines);
34201
+ const numByPath = parseNumstat(numstatLines);
34202
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
34203
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
34204
+ paths.add(p);
34205
+ }
34206
+ return paths.size;
34260
34207
  }
34261
34208
 
34262
- // src/worktrees/manager/resolve-isolated-session-parent-path.ts
34263
- function resolveIsolatedSessionParentPath(sessionId, cache2, ensureRepoCheckoutPaths) {
34264
- if (!sessionId) return null;
34265
- const sid = sessionId.trim();
34266
- const cached2 = cache2.getSessionParentPath(sid);
34267
- if (cached2) return cached2;
34268
- const paths = ensureRepoCheckoutPaths(sid);
34269
- if (!paths?.length) return null;
34270
- return resolveIsolatedSessionParentPathFromCheckouts(paths);
34271
- }
34272
- function ensureRepoCheckoutPathsForSession(sessionId, cache2) {
34273
- if (!sessionId?.trim()) return void 0;
34274
- const cached2 = cache2.getRepoCheckoutPaths(sessionId.trim());
34275
- return cached2?.length ? [...cached2] : void 0;
34209
+ // src/git/changes/count-working-tree-changed-files.ts
34210
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
34211
+ return runGitTask(async () => {
34212
+ const g = cliSimpleGit(repoGitCwd);
34213
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
34214
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
34215
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
34216
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
34217
+ ]);
34218
+ return workingTreeChangedPathCount(
34219
+ String(nameStatusRaw).split("\n"),
34220
+ String(numstatRaw).split("\n"),
34221
+ String(untrackedRaw).split("\n")
34222
+ );
34223
+ });
34276
34224
  }
34277
- async function ensureRepoCheckoutPathsForSessionAsync(sessionId, cache2, discover) {
34278
- if (!sessionId?.trim()) return void 0;
34279
- const sid = sessionId.trim();
34280
- const cached2 = cache2.getRepoCheckoutPaths(sid);
34281
- if (cached2?.length) return cached2;
34282
- const disc = await discover(sid);
34283
- if (disc?.repoCheckoutPaths.length) {
34284
- cache2.remember(sid, disc);
34285
- return [...disc.repoCheckoutPaths];
34225
+
34226
+ // src/git/commits/remote-tracking/rev-parse-safe.ts
34227
+ async function revParseSafe(g, ref) {
34228
+ try {
34229
+ const v = (await g.raw(["rev-parse", "--verify", ref])).trim();
34230
+ return v || null;
34231
+ } catch {
34232
+ return null;
34286
34233
  }
34287
- return void 0;
34288
34234
  }
34289
34235
 
34290
- // src/worktrees/manager/parse-session-parent.ts
34291
- function parseSessionParent(v) {
34292
- if (v === "bridge_root" || v === "worktrees_root") return v;
34293
- if (v === "session_worktrees_root") return "worktrees_root";
34294
- return null;
34236
+ // src/git/commits/remote-tracking/git-config.ts
34237
+ async function tryConfigGet(g, key) {
34238
+ try {
34239
+ const out = await g.raw(["config", "--get", key]);
34240
+ const v = String(out).trim();
34241
+ return v || null;
34242
+ } catch {
34243
+ return null;
34244
+ }
34295
34245
  }
34296
34246
 
34297
- // src/worktrees/prepare-new-session-worktrees.ts
34298
- import * as fs22 from "node:fs";
34299
- import * as path27 from "node:path";
34300
-
34301
- // src/git/worktrees/worktree-add.ts
34302
- async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch, baseRef = "HEAD") {
34303
- const mainGit = cliSimpleGit(mainRepoPath);
34304
- const base = baseRef.trim() || "HEAD";
34305
- await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, base]);
34247
+ // src/git/commits/remote-tracking/branch-remote-config.ts
34248
+ async function readBranchRemoteConfig(g) {
34249
+ const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
34250
+ if (!branch || branch === "HEAD") {
34251
+ return { branch: null, remote: "origin", merge: null };
34252
+ }
34253
+ const [remote, merge2] = await Promise.all([
34254
+ tryConfigGet(g, `branch.${branch}.remote`),
34255
+ tryConfigGet(g, `branch.${branch}.merge`)
34256
+ ]);
34257
+ return { branch, remote: remote ?? "origin", merge: merge2 };
34258
+ }
34259
+ function trackingRefCandidates(config2) {
34260
+ const candidates = ["HEAD@{upstream}", "@{u}"];
34261
+ if (!config2.branch) return candidates;
34262
+ if (config2.merge) {
34263
+ candidates.push(
34264
+ `refs/remotes/${config2.remote}/${config2.merge.replace(/^refs\/heads\//, "")}`
34265
+ );
34266
+ }
34267
+ candidates.push(`refs/remotes/${config2.remote}/${config2.branch}`);
34268
+ return candidates;
34306
34269
  }
34307
34270
 
34308
- // src/worktrees/prepare-new-session-worktrees.ts
34309
- function normalizeRepoRelPath(rel) {
34310
- return rel === "" ? "." : rel.replace(/\\/g, "/");
34271
+ // src/git/commits/remote-tracking/first-resolved-ref.ts
34272
+ async function firstResolvedSha(g, candidates) {
34273
+ const resolved = await Promise.all(
34274
+ candidates.map(async (ref) => ({ ref, sha: await revParseSafe(g, ref) }))
34275
+ );
34276
+ return resolved.find((entry) => entry.sha)?.sha ?? null;
34311
34277
  }
34312
- function resolveBaseRefForRepo(relNorm, baseBranches) {
34313
- if (!baseBranches) return "HEAD";
34314
- const direct = baseBranches[relNorm]?.trim();
34315
- if (direct) return direct;
34316
- if (relNorm !== "." && baseBranches["."]?.trim()) return baseBranches["."].trim();
34317
- return "HEAD";
34278
+
34279
+ // src/git/commits/remote-tracking/resolve-default-remote-branch-ref.ts
34280
+ var DEFAULT_REMOTE_BRANCH_NAMES = ["main", "master", "trunk", "develop"];
34281
+ function defaultRemoteBranchNameRefs(remote) {
34282
+ return DEFAULT_REMOTE_BRANCH_NAMES.map((name) => `refs/remotes/${remote}/${name}`);
34318
34283
  }
34319
- async function prepareNewSessionWorktrees(options) {
34320
- const { worktreesRootPath, bridgeRoot, sessionId, layout, log: log2, worktreeBaseBranches } = options;
34321
- const bridgeResolved = path27.resolve(bridgeRoot);
34322
- const cwdKey = allocateDirNameForLauncherCwd(layout, bridgeResolved);
34323
- const bridgeKeyDir = path27.join(worktreesRootPath, cwdKey);
34324
- const sessionDir = path27.join(bridgeKeyDir, sessionId);
34325
- const repos = await discoverGitReposUnderRoot(bridgeResolved);
34326
- if (repos.length === 0) {
34327
- log2("[worktrees] No Git repositories under bridge root; skipping worktree creation.");
34284
+ async function resolveRemoteHeadSymbolicRef(g, remote) {
34285
+ const headSym = `refs/remotes/${remote}/HEAD`;
34286
+ try {
34287
+ const out = await g.raw(["symbolic-ref", "-q", "--verify", headSym]);
34288
+ const resolved = String(out).trim();
34289
+ return resolved.startsWith("refs/remotes/") ? resolved : null;
34290
+ } catch {
34328
34291
  return null;
34329
34292
  }
34330
- const branch = `session-${sessionId}`;
34331
- const worktreePaths = [];
34332
- fs22.mkdirSync(sessionDir, { recursive: true });
34333
- for (const repo of repos) {
34334
- let rel = path27.relative(bridgeResolved, repo.absolutePath);
34335
- if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
34336
- const relNorm = normalizeRepoRelPath(rel === "" ? "." : rel);
34337
- const wtPath = relNorm === "." ? sessionDir : path27.join(sessionDir, relNorm);
34338
- if (relNorm !== ".") {
34339
- fs22.mkdirSync(path27.dirname(wtPath), { recursive: true });
34340
- }
34341
- const baseRef = resolveBaseRefForRepo(relNorm, worktreeBaseBranches);
34342
- try {
34343
- await gitWorktreeAddBranch(repo.absolutePath, wtPath, branch, baseRef);
34344
- log2(`[worktrees] Added worktree ${wtPath} (branch ${branch}, base ${baseRef}).`);
34345
- worktreePaths.push(wtPath);
34346
- } catch (e) {
34347
- log2(
34348
- `[worktrees] Worktree add failed for ${repo.absolutePath}: ${e instanceof Error ? e.message : String(e)}`
34349
- );
34350
- }
34293
+ }
34294
+ async function resolveDefaultRemoteBranchSha(g, remote) {
34295
+ const nameRefs = defaultRemoteBranchNameRefs(remote);
34296
+ const [symRef, ...nameShas] = await Promise.all([
34297
+ resolveRemoteHeadSymbolicRef(g, remote),
34298
+ ...nameRefs.map((ref) => revParseSafe(g, ref))
34299
+ ]);
34300
+ if (symRef) return revParseSafe(g, symRef);
34301
+ for (let i = 0; i < nameRefs.length; i++) {
34302
+ if (nameShas[i]) return nameShas[i];
34351
34303
  }
34352
- if (worktreePaths.length === 0) return null;
34353
- return {
34354
- worktreePaths,
34355
- sessionParentPath: sessionDir,
34356
- workingTreeRelRoot: sessionDir
34357
- };
34304
+ return null;
34358
34305
  }
34359
34306
 
34360
- // src/worktrees/manager/prepare-and-remember-session-worktrees.ts
34361
- async function prepareAndRememberSessionWorktrees(params) {
34362
- const prep = await prepareNewSessionWorktrees({
34363
- worktreesRootPath: params.worktreesRootPath,
34364
- bridgeRoot: getBridgeRoot(),
34365
- sessionId: params.sessionId,
34366
- layout: params.layout,
34367
- log: params.log,
34368
- ...params.worktreeBaseBranches && Object.keys(params.worktreeBaseBranches).length > 0 ? { worktreeBaseBranches: params.worktreeBaseBranches } : {}
34307
+ // src/git/commits/remote-tracking/resolve-base-sha-for-unpushed-commits.ts
34308
+ import * as path21 from "node:path";
34309
+ var BASE_SHA_CACHE_TTL_MS = 3e4;
34310
+ var baseShaCache = /* @__PURE__ */ new Map();
34311
+ async function resolveBaseShaForUnpushedCommitsUncached(g) {
34312
+ const config2 = await readBranchRemoteConfig(g);
34313
+ const trackingSha = await firstResolvedSha(g, trackingRefCandidates(config2));
34314
+ if (trackingSha) return trackingSha;
34315
+ return resolveDefaultRemoteBranchSha(g, config2.remote);
34316
+ }
34317
+ async function resolveBaseShaForUnpushedCommits(g, repoDir) {
34318
+ if (repoDir) {
34319
+ const key = path21.resolve(repoDir);
34320
+ const now = Date.now();
34321
+ const cached2 = baseShaCache.get(key);
34322
+ if (cached2 && cached2.expiresAt > now) return cached2.baseSha;
34323
+ const baseSha = await resolveBaseShaForUnpushedCommitsUncached(g);
34324
+ baseShaCache.set(key, { baseSha, expiresAt: now + BASE_SHA_CACHE_TTL_MS });
34325
+ return baseSha;
34326
+ }
34327
+ return resolveBaseShaForUnpushedCommitsUncached(g);
34328
+ }
34329
+
34330
+ // src/git/commits/remote-tracking/commits-ahead-of-remote-tracking.ts
34331
+ async function commitsAheadOfRemoteTracking(repoDir) {
34332
+ const g = cliSimpleGit(repoDir);
34333
+ const headSha = await revParseSafe(g, "HEAD");
34334
+ if (!headSha) return 0;
34335
+ const baseSha = await resolveBaseShaForUnpushedCommits(g, repoDir);
34336
+ if (!baseSha || baseSha === headSha) return 0;
34337
+ try {
34338
+ const out = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
34339
+ const n = parseInt(String(out).trim(), 10);
34340
+ return Number.isNaN(n) ? 0 : n;
34341
+ } catch {
34342
+ return 0;
34343
+ }
34344
+ }
34345
+
34346
+ // src/git/status/working-tree-status.ts
34347
+ async function getRepoWorkingTreeStatus(repoDir) {
34348
+ return runGitTask(async () => {
34349
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
34350
+ const hasUncommittedChanges = uncommittedFileCount > 0;
34351
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
34352
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
34369
34353
  });
34370
- if (!prep) return void 0;
34371
- params.cache.remember(params.sessionId, {
34372
- sessionParentPath: prep.sessionParentPath,
34373
- workingTreeRelRoot: prep.workingTreeRelRoot,
34374
- repoCheckoutPaths: prep.worktreePaths
34354
+ }
34355
+ async function aggregateSessionPathsWorkingTreeStatus(paths) {
34356
+ let hasUncommittedChanges = false;
34357
+ let hasUnpushedCommits = false;
34358
+ let uncommittedFileCount = 0;
34359
+ await forEachWithGitYield(paths, async (p) => {
34360
+ const s = await getRepoWorkingTreeStatus(p);
34361
+ uncommittedFileCount += s.uncommittedFileCount;
34362
+ if (s.hasUncommittedChanges) hasUncommittedChanges = true;
34363
+ if (s.hasUnpushedCommits) hasUnpushedCommits = true;
34375
34364
  });
34376
- return params.cache.getSessionParentPath(params.sessionId);
34365
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
34377
34366
  }
34378
-
34379
- // src/worktrees/manager/resolve-existing-session-parent-path.ts
34380
- async function resolveExistingSessionParentPath(sessionId, cache2, discover) {
34381
- const cached2 = cache2.getSessionParentPath(sessionId);
34382
- if (cached2) return cached2;
34383
- const disc = await discover();
34384
- if (disc) {
34385
- cache2.remember(sessionId, disc);
34386
- return cache2.getSessionParentPath(sessionId);
34387
- }
34388
- return void 0;
34367
+ async function pushAheadOfUpstreamForPaths(paths) {
34368
+ await forEachWithGitYield(paths, async (p) => {
34369
+ const g = cliSimpleGit(p);
34370
+ const ahead = await commitsAheadOfRemoteTracking(p);
34371
+ if (ahead <= 0) return;
34372
+ await g.push();
34373
+ });
34389
34374
  }
34390
34375
 
34391
- // src/worktrees/manager/resolve-explicit-session-parent-path.ts
34392
- import * as path28 from "node:path";
34393
- async function resolveExplicitSessionParentPath(params) {
34394
- const resolved = path28.resolve(params.parentPathRaw);
34395
- if (parseSessionParent(params.sessionParent) !== "worktrees_root") {
34396
- return resolved;
34397
- }
34398
- const rememberAndReturn = (binding) => {
34399
- params.cache.remember(params.sessionId, binding);
34400
- return params.cache.getSessionParentPath(params.sessionId) ?? resolved;
34376
+ // src/worktrees/manager/resolve-commit-targets.ts
34377
+ function bridgeRootBinding(bridgeRoot) {
34378
+ return {
34379
+ sessionParentPath: bridgeRoot,
34380
+ workingTreeRelRoot: bridgeRoot,
34381
+ repoCheckoutPaths: [bridgeRoot]
34401
34382
  };
34402
- const diskFirst = await params.discover();
34403
- if (diskFirst) return rememberAndReturn(diskFirst);
34404
- const fromRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(resolved, params.sessionId);
34405
- if (fromRoot) return rememberAndReturn(fromRoot);
34406
- let cur = resolved;
34407
- for (let i = 0; i < 16; i++) {
34408
- await yieldToEventLoop();
34409
- const tryRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(cur, params.sessionId);
34410
- if (tryRoot) return rememberAndReturn(tryRoot);
34411
- const next = path28.dirname(cur);
34412
- if (next === cur) break;
34413
- cur = next;
34414
- }
34415
- return resolved;
34416
34383
  }
34417
-
34418
- // src/worktrees/manager/resolve-session-parent-path-for-prompt.ts
34419
- async function resolveSessionParentPathForPrompt(params) {
34420
- const { sessionId, cache: cache2, worktreesRootPath, layout, log: log2, discover, opts } = params;
34421
- if (!sessionId) return void 0;
34384
+ async function resolveCommitTargetsAsync(sessionId, cache2, discover) {
34422
34385
  const sid = sessionId.trim();
34423
- const parentPathRaw = opts.sessionParentPath?.trim();
34424
- if (parentPathRaw) {
34425
- return resolveExplicitSessionParentPath({
34426
- sessionId: sid,
34427
- sessionParent: opts.sessionParent,
34428
- parentPathRaw,
34429
- cache: cache2,
34430
- discover: () => discover(sid)
34431
- });
34432
- }
34433
- const parentKind = parseSessionParent(opts.sessionParent);
34434
- if (parentKind === "bridge_root") {
34435
- return void 0;
34436
- }
34437
- if (parentKind === "worktrees_root") {
34438
- if (!opts.isNewSession) {
34439
- return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
34440
- }
34441
- return prepareAndRememberSessionWorktrees({
34442
- cache: cache2,
34443
- sessionId: sid,
34444
- worktreesRootPath,
34445
- layout,
34446
- log: log2,
34447
- ...opts.worktreeBaseBranches ? { worktreeBaseBranches: opts.worktreeBaseBranches } : {}
34448
- });
34449
- }
34450
- if (!opts.isNewSession) {
34451
- return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
34386
+ const paths = cache2.getRepoCheckoutPathsRef(sid);
34387
+ if (paths?.length) return paths;
34388
+ const disc = await discover(sid);
34389
+ if (disc?.repoCheckoutPaths.length) {
34390
+ cache2.remember(sid, disc);
34391
+ return disc.repoCheckoutPaths;
34452
34392
  }
34453
- return prepareAndRememberSessionWorktrees({
34454
- cache: cache2,
34455
- sessionId: sid,
34456
- worktreesRootPath,
34457
- layout,
34458
- log: log2
34459
- });
34393
+ const bridgeRoot = getBridgeRoot();
34394
+ cache2.remember(sid, bridgeRootBinding(bridgeRoot));
34395
+ return [bridgeRoot];
34396
+ }
34397
+
34398
+ // src/worktrees/manager/git/get-session-working-tree-status.ts
34399
+ async function getSessionWorkingTreeStatus(cache2, sessionId, discover) {
34400
+ return aggregateSessionPathsWorkingTreeStatus(
34401
+ await resolveCommitTargetsAsync(sessionId, cache2, discover)
34402
+ );
34460
34403
  }
34461
34404
 
34462
34405
  // src/git/changes/types.ts
@@ -34517,108 +34460,13 @@ function formatRemoteDisplayLabel(remoteUrl) {
34517
34460
  }
34518
34461
 
34519
34462
  // src/git/changes/repo/get-working-tree-change-repo-details.ts
34520
- import * as path33 from "node:path";
34463
+ import * as path28 from "node:path";
34521
34464
 
34522
34465
  // src/git/changes/repo/build-working-tree-change-repo-detail.ts
34523
- import * as path32 from "node:path";
34466
+ import * as path27 from "node:path";
34524
34467
 
34525
- // src/git/commits/resolve-remote-tracking.ts
34526
- async function tryConfigGet(g, key) {
34527
- try {
34528
- const out = await g.raw(["config", "--get", key]);
34529
- const v = String(out).trim();
34530
- return v || null;
34531
- } catch {
34532
- return null;
34533
- }
34534
- }
34535
- async function revParseSafe(g, ref) {
34536
- try {
34537
- const v = (await g.raw(["rev-parse", ref])).trim();
34538
- return v || null;
34539
- } catch {
34540
- return null;
34541
- }
34542
- }
34543
- async function resolveRemoteTrackingRefForAhead(g) {
34544
- try {
34545
- await g.raw(["rev-parse", "--verify", "HEAD@{upstream}"]);
34546
- return "HEAD@{upstream}";
34547
- } catch {
34548
- try {
34549
- await g.raw(["rev-parse", "--verify", "@{u}"]);
34550
- return "@{u}";
34551
- } catch {
34552
- }
34553
- }
34554
- const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
34555
- if (!branch || branch === "HEAD") return null;
34556
- const remote = await tryConfigGet(g, `branch.${branch}.remote`) ?? "origin";
34557
- const merge2 = await tryConfigGet(g, `branch.${branch}.merge`);
34558
- if (merge2) {
34559
- const upstreamBranch = merge2.replace(/^refs\/heads\//, "");
34560
- const ref = `refs/remotes/${remote}/${upstreamBranch}`;
34561
- try {
34562
- await g.raw(["rev-parse", "--verify", ref]);
34563
- return ref;
34564
- } catch {
34565
- }
34566
- }
34567
- const fallbackRef = `refs/remotes/${remote}/${branch}`;
34568
- try {
34569
- await g.raw(["rev-parse", "--verify", fallbackRef]);
34570
- return fallbackRef;
34571
- } catch {
34572
- return null;
34573
- }
34574
- }
34575
- async function remoteForCurrentBranch(g) {
34576
- const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
34577
- if (!branch || branch === "HEAD") return "origin";
34578
- return await tryConfigGet(g, `branch.${branch}.remote`) ?? "origin";
34579
- }
34580
- async function resolveDefaultRemoteBranchRef(g, remote) {
34581
- const headSym = `refs/remotes/${remote}/HEAD`;
34582
- try {
34583
- const resolved = (await g.raw(["symbolic-ref", "-q", "--verify", headSym])).trim();
34584
- if (resolved.startsWith("refs/remotes/")) return resolved;
34585
- } catch {
34586
- }
34587
- for (const name of ["main", "master", "trunk", "develop"]) {
34588
- const r = `refs/remotes/${remote}/${name}`;
34589
- try {
34590
- await g.raw(["rev-parse", "--verify", r]);
34591
- return r;
34592
- } catch {
34593
- }
34594
- }
34595
- return null;
34596
- }
34597
- async function resolveBaseShaForUnpushedCommits(g) {
34598
- const trackingRef = await resolveRemoteTrackingRefForAhead(g);
34599
- if (trackingRef) {
34600
- const sha = await revParseSafe(g, trackingRef);
34601
- if (sha) return sha;
34602
- }
34603
- const remote = await remoteForCurrentBranch(g);
34604
- const defaultRef = await resolveDefaultRemoteBranchRef(g, remote);
34605
- if (!defaultRef) return null;
34606
- return revParseSafe(g, defaultRef);
34607
- }
34608
- async function commitsAheadOfRemoteTracking(repoDir) {
34609
- const g = cliSimpleGit(repoDir);
34610
- const headSha = await revParseSafe(g, "HEAD");
34611
- if (!headSha) return 0;
34612
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
34613
- if (!baseSha || baseSha === headSha) return 0;
34614
- try {
34615
- const out = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
34616
- const n = parseInt(String(out).trim(), 10);
34617
- return Number.isNaN(n) ? 0 : n;
34618
- } catch {
34619
- return 0;
34620
- }
34621
- }
34468
+ // src/git/commits/list-unpushed-commits.ts
34469
+ import * as path22 from "node:path";
34622
34470
 
34623
34471
  // src/git/commits/lib/parse-log-lines.ts
34624
34472
  function parseLogShaDateSubjectLines(raw) {
@@ -34638,9 +34486,14 @@ function parseLogShaDateSubjectLines(raw) {
34638
34486
  }
34639
34487
 
34640
34488
  // src/git/commits/list-unpushed-commits.ts
34489
+ var UNPUSHED_CACHE_TTL_MS = 3e4;
34490
+ var unpushedCache = /* @__PURE__ */ new Map();
34641
34491
  async function gitLogNotReachableFromBase(g, baseSha, headSha) {
34642
34492
  if (baseSha === headSha) return [];
34643
34493
  try {
34494
+ const countOut = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
34495
+ const count = parseInt(String(countOut).trim(), 10);
34496
+ if (!Number.isFinite(count) || count <= 0) return [];
34644
34497
  const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
34645
34498
  return parseLogShaDateSubjectLines(logOut);
34646
34499
  } catch {
@@ -34648,12 +34501,22 @@ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
34648
34501
  }
34649
34502
  }
34650
34503
  async function listUnpushedCommits(repoDir) {
34504
+ const key = path22.resolve(repoDir);
34505
+ const now = Date.now();
34506
+ const cached2 = unpushedCache.get(key);
34507
+ if (cached2 && cached2.expiresAt > now) return cached2.commits;
34651
34508
  const g = cliSimpleGit(repoDir);
34652
- const headSha = await revParseSafe(g, "HEAD");
34653
- if (!headSha) return [];
34654
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
34655
- if (!baseSha) return [];
34656
- return gitLogNotReachableFromBase(g, baseSha, headSha);
34509
+ const [headSha, baseSha] = await Promise.all([
34510
+ revParseSafe(g, "HEAD"),
34511
+ resolveBaseShaForUnpushedCommits(g, repoDir)
34512
+ ]);
34513
+ if (!headSha || !baseSha) {
34514
+ unpushedCache.set(key, { commits: [], expiresAt: now + UNPUSHED_CACHE_TTL_MS });
34515
+ return [];
34516
+ }
34517
+ const commits = await gitLogNotReachableFromBase(g, baseSha, headSha);
34518
+ unpushedCache.set(key, { commits, expiresAt: now + UNPUSHED_CACHE_TTL_MS });
34519
+ return commits;
34657
34520
  }
34658
34521
 
34659
34522
  // src/git/commits/lib/sanitize-recent-commits-limit.ts
@@ -34667,72 +34530,40 @@ function sanitizeRecentCommitsLimit(value) {
34667
34530
  }
34668
34531
 
34669
34532
  // src/git/commits/list-recent-commits.ts
34670
- async function listRecentCommits(repoDir, limitInput) {
34533
+ async function listRecentCommitRefs(repoDir, limitInput) {
34671
34534
  const limit = sanitizeRecentCommitsLimit(limitInput);
34672
34535
  const g = cliSimpleGit(repoDir);
34673
- const headSha = await revParseSafe(g, "HEAD");
34674
- if (!headSha) return { commits: [], hasMore: false };
34675
- const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
34676
34536
  try {
34677
- const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
34678
- const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
34679
- ...c,
34680
- needsPush: unpushedSet.has(c.sha)
34681
- }));
34682
- let hasMore = false;
34683
- if (commits.length === limit) {
34684
- const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
34685
- hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
34686
- }
34537
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit + 1), "HEAD"]);
34538
+ const parsed = parseLogShaDateSubjectLines(logOut);
34539
+ const hasMore = parsed.length > limit;
34540
+ const commits = parsed.slice(0, limit);
34687
34541
  return { commits, hasMore };
34688
34542
  } catch {
34689
34543
  return { commits: [], hasMore: false };
34690
34544
  }
34691
34545
  }
34692
34546
 
34693
- // src/git/changes/parse/normalize-git-diff-path.ts
34694
- function normalizeGitDiffPath(path53) {
34695
- return path53.replace(/\\/g, "/").replace(/\/ +/g, "/").trim();
34696
- }
34697
-
34698
- // src/git/changes/parse/expand-git-rename-numstat-path.ts
34699
- function expandGitRenameNumstatPath(rawPath) {
34700
- const open2 = rawPath.indexOf("{");
34701
- const arrow = rawPath.indexOf("=>");
34702
- const close = rawPath.lastIndexOf("}");
34703
- if (open2 === -1 || arrow === -1 || close === -1 || open2 > arrow || arrow > close) {
34704
- return null;
34705
- }
34706
- const prefix = rawPath.slice(0, open2);
34707
- const suffix = rawPath.slice(close + 1);
34708
- const oldPart = rawPath.slice(open2 + 1, arrow).trim();
34709
- const newPart = rawPath.slice(arrow + 2, close).trim();
34710
- return {
34711
- oldPath: normalizeGitDiffPath(`${prefix}${oldPart}${suffix}`),
34712
- newPath: normalizeGitDiffPath(`${prefix}${newPart}${suffix}`)
34713
- };
34714
- }
34715
- function isGitRenameNumstatPath(rawPath) {
34716
- return expandGitRenameNumstatPath(rawPath) != null;
34717
- }
34547
+ // src/git/changes/listing/list-changed-files-for-commit.ts
34548
+ import * as path24 from "node:path";
34718
34549
 
34719
34550
  // src/git/changes/rows/build-changed-file-row.ts
34720
34551
  function buildChangedFileRow(options) {
34721
34552
  const pathInRepo = normalizeGitDiffPath(options.pathInRepo);
34722
- const relLauncher = posixJoinDirFile(options.repoRelPath, pathInRepo);
34553
+ const workspaceRelPath = posixJoinDirFile(options.repoRelPath, pathInRepo);
34723
34554
  const additions = options.numEntry?.additions ?? 0;
34724
34555
  const deletions = options.numEntry?.deletions ?? 0;
34725
34556
  let change = options.nameEntry?.change ?? "modified";
34726
- let movedFromPathRelLauncher;
34557
+ let movedFromWorkspaceRelPath;
34727
34558
  let movedFromPathInRepo;
34728
34559
  const oldPathInRepo = options.nameEntry?.oldPathInRepo ?? options.numEntry?.oldPathInRepo;
34729
34560
  if (change === "moved" && oldPathInRepo) {
34730
34561
  movedFromPathInRepo = normalizeGitDiffPath(oldPathInRepo);
34731
- movedFromPathRelLauncher = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34562
+ movedFromWorkspaceRelPath = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34732
34563
  } else if (oldPathInRepo && change === "modified") {
34733
34564
  change = "moved";
34734
34565
  movedFromPathInRepo = normalizeGitDiffPath(oldPathInRepo);
34735
- movedFromPathRelLauncher = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34566
+ movedFromWorkspaceRelPath = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34736
34567
  }
34737
34568
  if (options.untracked && !options.nameEntry) {
34738
34569
  change = "added";
@@ -34742,11 +34573,11 @@ function buildChangedFileRow(options) {
34742
34573
  else change = "modified";
34743
34574
  }
34744
34575
  return {
34745
- pathRelLauncher: relLauncher,
34576
+ workspaceRelPath,
34746
34577
  additions,
34747
34578
  deletions,
34748
34579
  change,
34749
- ...movedFromPathRelLauncher ? { movedFromPathRelLauncher } : {},
34580
+ ...movedFromWorkspaceRelPath ? { movedFromWorkspaceRelPath } : {},
34750
34581
  ...movedFromPathInRepo ? { movedFromPathInRepo } : {}
34751
34582
  };
34752
34583
  }
@@ -34805,8 +34636,8 @@ async function unifiedDiffForFileInRange(repoCwd, range, pathInRepo, change, mov
34805
34636
  }
34806
34637
 
34807
34638
  // src/git/changes/total-lines/resolve-changed-file-total-lines.ts
34808
- import * as fs23 from "node:fs";
34809
- import * as path29 from "node:path";
34639
+ import * as fs17 from "node:fs";
34640
+ import * as path23 from "node:path";
34810
34641
 
34811
34642
  // src/git/changes/lines/count-lines.ts
34812
34643
  import { createReadStream } from "node:fs";
@@ -34852,13 +34683,13 @@ async function countLinesInGitRef(repoGitCwd, ref, pathInRepo) {
34852
34683
  }
34853
34684
  async function resolveWorkingTreeFileTotalLines(options) {
34854
34685
  if (options.isBinary) return null;
34855
- const filePath = path29.join(options.repoGitCwd, options.pathInRepo);
34686
+ const filePath = path23.join(options.repoGitCwd, options.pathInRepo);
34856
34687
  if (options.change === "removed") {
34857
34688
  return countLinesInGitRef(options.repoGitCwd, "HEAD", options.pathInRepo);
34858
34689
  }
34859
34690
  if (options.change === "moved" || options.change === "modified") {
34860
34691
  try {
34861
- const st = await fs23.promises.stat(filePath);
34692
+ const st = await fs17.promises.stat(filePath);
34862
34693
  if (!st.isFile()) return null;
34863
34694
  return await countTextFileLines(filePath);
34864
34695
  } catch {
@@ -34878,23 +34709,23 @@ async function resolveCommitFileTotalLines(options) {
34878
34709
  return null;
34879
34710
  }
34880
34711
 
34881
- // src/git/changes/listing/path-in-repo-from-launcher.ts
34882
- function pathInRepoFromLauncher(pathRelLauncher, repoRelPath) {
34712
+ // src/git/changes/listing/path-in-repo-from-workspace.ts
34713
+ function pathInRepoFromWorkspace(workspaceRelPath, repoRelPath) {
34883
34714
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
34884
- if (normRel === ".") return pathRelLauncher;
34885
- if (pathRelLauncher.startsWith(`${normRel}/`)) {
34886
- return pathRelLauncher.slice(normRel.length + 1);
34715
+ if (normRel === ".") return workspaceRelPath;
34716
+ if (workspaceRelPath.startsWith(`${normRel}/`)) {
34717
+ return workspaceRelPath.slice(normRel.length + 1);
34887
34718
  }
34888
- return pathRelLauncher;
34719
+ return workspaceRelPath;
34889
34720
  }
34890
- function normalizeRepoRelPath2(repoRelPath) {
34721
+ function normalizeRepoRelPath(repoRelPath) {
34891
34722
  return repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
34892
34723
  }
34893
34724
 
34894
34725
  // src/git/changes/listing/enrich-commit-file-rows.ts
34895
34726
  async function enrichCommitFileRows(options) {
34896
34727
  await forEachWithGitYield(options.rows, async (row) => {
34897
- const pathInRepo = pathInRepoFromLauncher(row.pathRelLauncher, options.repoRelPath);
34728
+ const pathInRepo = pathInRepoFromWorkspace(row.workspaceRelPath, options.repoRelPath);
34898
34729
  row.patchContent = await unifiedDiffForFileInRange(
34899
34730
  options.repoGitCwd,
34900
34731
  options.range,
@@ -34913,102 +34744,6 @@ async function enrichCommitFileRows(options) {
34913
34744
  });
34914
34745
  }
34915
34746
 
34916
- // src/git/changes/parse/parse-name-status-entries.ts
34917
- function parseNameStatusEntries(lines) {
34918
- const entries = [];
34919
- for (const line of lines) {
34920
- if (!line.trim()) continue;
34921
- const tabParts = line.split(" ");
34922
- if (tabParts.length < 2) continue;
34923
- const status = tabParts[0].trim();
34924
- const code = status[0];
34925
- if (code === "A") {
34926
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "added" });
34927
- } else if (code === "D") {
34928
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "removed" });
34929
- } else if (code === "R" || code === "C") {
34930
- if (tabParts.length >= 3) {
34931
- entries.push({
34932
- pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]),
34933
- oldPathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 2]),
34934
- change: "moved"
34935
- });
34936
- }
34937
- } else if (code === "M" || code === "U" || code === "T") {
34938
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "modified" });
34939
- }
34940
- }
34941
- return entries;
34942
- }
34943
- function parseNameStatusLines(lines) {
34944
- const m = /* @__PURE__ */ new Map();
34945
- for (const entry of parseNameStatusEntries(lines)) {
34946
- m.set(entry.pathInRepo, entry.change);
34947
- }
34948
- return m;
34949
- }
34950
-
34951
- // src/git/changes/parse/parse-numstat-entries.ts
34952
- function parseNumstatCounts(parts) {
34953
- const [a, d] = parts;
34954
- return {
34955
- additions: a === "-" ? 0 : parseInt(String(a), 10) || 0,
34956
- deletions: d === "-" ? 0 : parseInt(String(d), 10) || 0
34957
- };
34958
- }
34959
- function parseNumstatLine(line) {
34960
- const parts = line.split(" ");
34961
- if (parts.length < 3) return null;
34962
- const rawPath = parts[parts.length - 1];
34963
- const { additions, deletions } = parseNumstatCounts(parts);
34964
- const expanded = expandGitRenameNumstatPath(rawPath);
34965
- const pathInRepo = normalizeGitDiffPath(expanded?.newPath ?? rawPath);
34966
- return {
34967
- pathInRepo,
34968
- additions,
34969
- deletions,
34970
- ...expanded ? { oldPathInRepo: expanded.oldPath } : {}
34971
- };
34972
- }
34973
- function parseNumstatEntries(lines) {
34974
- const m = /* @__PURE__ */ new Map();
34975
- for (const line of lines) {
34976
- if (!line.trim()) continue;
34977
- const parts = line.split(" ");
34978
- if (parts.length < 3) continue;
34979
- const rawPath = parts[parts.length - 1];
34980
- const expanded = expandGitRenameNumstatPath(rawPath);
34981
- const entry = parseNumstatLine(line);
34982
- if (!entry) continue;
34983
- const existing = m.get(entry.pathInRepo);
34984
- if (!existing || expanded) {
34985
- m.set(entry.pathInRepo, entry);
34986
- }
34987
- }
34988
- return m;
34989
- }
34990
- function parseNumstat(lines) {
34991
- const m = /* @__PURE__ */ new Map();
34992
- for (const [path53, entry] of parseNumstatEntries(lines)) {
34993
- m.set(path53, { additions: entry.additions, deletions: entry.deletions });
34994
- }
34995
- return m;
34996
- }
34997
-
34998
- // src/git/changes/parse/numstat-from-git-no-index.ts
34999
- async function numstatFromGitNoIndex(g, pathInRepo) {
35000
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35001
- try {
35002
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35003
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35004
- const parsed = parseNumstatLine(first2);
35005
- if (!parsed) return null;
35006
- return { additions: parsed.additions, deletions: parsed.deletions };
35007
- } catch {
35008
- return null;
35009
- }
35010
- }
35011
-
35012
34747
  // src/git/changes/rows/collect-moved-source-paths.ts
35013
34748
  function collectMovedSourcePaths(nameEntries, numByPath) {
35014
34749
  const sources = /* @__PURE__ */ new Set();
@@ -35088,8 +34823,8 @@ function pickPreferredChangedFileRow(a, b) {
35088
34823
  const aHasPatch = Boolean(a.patchContent?.trim());
35089
34824
  const bHasPatch = Boolean(b.patchContent?.trim());
35090
34825
  if (aHasPatch !== bHasPatch) return aHasPatch ? a : b;
35091
- const aHasMoveMeta = Boolean(a.movedFromPathInRepo || a.movedFromPathRelLauncher);
35092
- const bHasMoveMeta = Boolean(b.movedFromPathInRepo || b.movedFromPathRelLauncher);
34826
+ const aHasMoveMeta = Boolean(a.movedFromPathInRepo || a.movedFromWorkspaceRelPath);
34827
+ const bHasMoveMeta = Boolean(b.movedFromPathInRepo || b.movedFromWorkspaceRelPath);
35093
34828
  if (aHasMoveMeta !== bHasMoveMeta) return aHasMoveMeta ? a : b;
35094
34829
  return a;
35095
34830
  }
@@ -35098,30 +34833,39 @@ function pickPreferredChangedFileRow(a, b) {
35098
34833
  function dedupeChangedFileRows(rows) {
35099
34834
  const byPath = /* @__PURE__ */ new Map();
35100
34835
  for (const row of rows) {
35101
- const key = normalizeGitDiffPath(row.pathRelLauncher);
35102
- const normalizedRow = key === row.pathRelLauncher ? row : { ...row, pathRelLauncher: key };
34836
+ const key = normalizeGitDiffPath(row.workspaceRelPath);
34837
+ const normalizedRow = key === row.workspaceRelPath ? row : { ...row, workspaceRelPath: key };
35103
34838
  const existing = byPath.get(key);
35104
34839
  if (!existing) {
35105
34840
  byPath.set(key, normalizedRow);
35106
34841
  continue;
35107
34842
  }
35108
34843
  const preferRow = pickPreferredChangedFileRow(existing, normalizedRow);
35109
- byPath.set(key, { ...preferRow, pathRelLauncher: key });
34844
+ byPath.set(key, { ...preferRow, workspaceRelPath: key });
35110
34845
  }
35111
34846
  return [...byPath.values()];
35112
34847
  }
35113
34848
 
35114
34849
  // src/git/changes/listing/list-changed-files-for-commit.ts
35115
- async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
34850
+ var COMMIT_FILES_CACHE_TTL_MS = 5 * 6e4;
34851
+ var commitFilesCache = /* @__PURE__ */ new Map();
34852
+ function commitFilesCacheKey(repoGitCwd, commitSha) {
34853
+ return `${path24.resolve(repoGitCwd)}:${commitSha}`;
34854
+ }
34855
+ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha, options = {}) {
34856
+ const cacheKey = commitFilesCacheKey(repoGitCwd, commitSha);
34857
+ if (!options.includeFilePatches) {
34858
+ const now = Date.now();
34859
+ const cached2 = commitFilesCache.get(cacheKey);
34860
+ if (cached2 && cached2.expiresAt > now) return cached2.rows;
34861
+ }
35116
34862
  const g = cliSimpleGit(repoGitCwd);
35117
- const parent = await parentForCommitDiff(g, commitSha);
35118
- const range = `${parent}..${commitSha}`;
34863
+ const normRel = normalizeRepoRelPath(repoRelPath);
35119
34864
  const [nameStatusRaw, numstatRaw] = await Promise.all([
35120
- g.raw(["diff", ...RENAME_DIFF_ARGS, "--name-status", range]).catch(() => ""),
35121
- g.raw(["diff", ...RENAME_DIFF_ARGS, "--numstat", range]).catch(() => "")
34865
+ g.raw(["diff-tree", "--no-commit-id", "--name-status", "-r", ...RENAME_DIFF_ARGS, commitSha]).catch(() => ""),
34866
+ g.raw(["show", "--numstat", "--format=format:", commitSha]).catch(() => "")
35122
34867
  ]);
35123
34868
  const parsed = parseDiffOutput(String(nameStatusRaw), String(numstatRaw));
35124
- const normRel = normalizeRepoRelPath2(repoRelPath);
35125
34869
  const paths = collectChangedPaths({
35126
34870
  nameEntries: parsed.nameEntries,
35127
34871
  numByPath: parsed.numByPath
@@ -35134,29 +34878,39 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35134
34878
  movedSourcePaths: parsed.movedSourcePaths
35135
34879
  });
35136
34880
  rows = dedupeChangedFileRows(rows);
35137
- await enrichCommitFileRows({
35138
- rows,
35139
- repoGitCwd,
35140
- repoRelPath: normRel,
35141
- range,
35142
- commitSha,
35143
- parentSha: parent,
35144
- binaryByPath: parsed.binaryByPath
35145
- });
34881
+ if (options.includeFilePatches) {
34882
+ const parent = await parentForCommitDiff(g, commitSha);
34883
+ const range = `${parent}..${commitSha}`;
34884
+ await enrichCommitFileRows({
34885
+ rows,
34886
+ repoGitCwd,
34887
+ repoRelPath: normRel,
34888
+ range,
34889
+ commitSha,
34890
+ parentSha: parent,
34891
+ binaryByPath: parsed.binaryByPath
34892
+ });
34893
+ }
35146
34894
  const finalRows = dedupeChangedFileRows(rows);
35147
- finalRows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
34895
+ finalRows.sort((a, b) => a.workspaceRelPath.localeCompare(b.workspaceRelPath));
34896
+ if (!options.includeFilePatches) {
34897
+ commitFilesCache.set(cacheKey, {
34898
+ rows: finalRows,
34899
+ expiresAt: Date.now() + COMMIT_FILES_CACHE_TTL_MS
34900
+ });
34901
+ }
35148
34902
  return finalRows;
35149
34903
  }
35150
34904
 
35151
34905
  // src/git/changes/listing/apply-untracked-file-stats.ts
35152
- import * as fs24 from "node:fs";
35153
- import * as path30 from "node:path";
34906
+ import * as fs18 from "node:fs";
34907
+ import * as path25 from "node:path";
35154
34908
  function createUntrackedStatsApplier(options) {
35155
34909
  return async (row, pathInRepo) => {
35156
34910
  if (!options.untrackedSet.has(pathInRepo)) return;
35157
34911
  if (options.nameByPath.has(pathInRepo)) return;
35158
34912
  if (row.change === "moved") return;
35159
- const repoFilePath = path30.join(options.repoGitCwd, pathInRepo);
34913
+ const repoFilePath = path25.join(options.repoGitCwd, pathInRepo);
35160
34914
  const fromGit = await numstatFromGitNoIndex(options.g, pathInRepo);
35161
34915
  if (fromGit) {
35162
34916
  row.additions = fromGit.additions;
@@ -35164,7 +34918,7 @@ function createUntrackedStatsApplier(options) {
35164
34918
  return;
35165
34919
  }
35166
34920
  try {
35167
- const st = await fs24.promises.stat(repoFilePath);
34921
+ const st = await fs18.promises.stat(repoFilePath);
35168
34922
  row.additions = st.isFile() ? await countTextFileLines(repoFilePath) : 0;
35169
34923
  } catch {
35170
34924
  row.additions = 0;
@@ -35174,7 +34928,7 @@ function createUntrackedStatsApplier(options) {
35174
34928
  }
35175
34929
 
35176
34930
  // src/git/changes/listing/enrich-working-tree-file-rows.ts
35177
- import * as path31 from "node:path";
34931
+ import * as path26 from "node:path";
35178
34932
 
35179
34933
  // src/git/changes/patch/hydrate/append-injected-context-lines.ts
35180
34934
  async function appendInjectedContextLines(out, inject, maybeYield, injectedTotal) {
@@ -35254,10 +35008,10 @@ async function readGitBlobLines(repoCwd, pathInRepo) {
35254
35008
  }
35255
35009
 
35256
35010
  // src/git/changes/patch/hydrate/read-worktree-file-lines.ts
35257
- import * as fs25 from "node:fs";
35011
+ import * as fs19 from "node:fs";
35258
35012
  async function readWorktreeFileLines(filePath) {
35259
35013
  try {
35260
- const raw = await fs25.promises.readFile(filePath, "utf8");
35014
+ const raw = await fs19.promises.readFile(filePath, "utf8");
35261
35015
  return raw.split(/\r?\n/);
35262
35016
  } catch {
35263
35017
  return null;
@@ -35353,8 +35107,8 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
35353
35107
  // src/git/changes/listing/enrich-working-tree-file-rows.ts
35354
35108
  async function enrichWorkingTreeFileRows(options) {
35355
35109
  await forEachWithGitYield(options.rows, async (row) => {
35356
- const pathInRepo = pathInRepoFromLauncher(row.pathRelLauncher, options.repoRelPath);
35357
- const filePath = path31.join(options.repoGitCwd, pathInRepo);
35110
+ const pathInRepo = pathInRepoFromWorkspace(row.workspaceRelPath, options.repoRelPath);
35111
+ const filePath = path26.join(options.repoGitCwd, pathInRepo);
35358
35112
  const hydrateKind = row.change === "moved" ? "modified" : row.change;
35359
35113
  let patch = await unifiedDiffForFile(options.repoGitCwd, pathInRepo, row.change, row.movedFromPathInRepo);
35360
35114
  if (patch) {
@@ -35377,7 +35131,7 @@ async function enrichWorkingTreeFileRows(options) {
35377
35131
  }
35378
35132
 
35379
35133
  // src/git/changes/listing/list-changed-files-for-repo.ts
35380
- async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
35134
+ async function listChangedFilesForRepo(repoGitCwd, repoRelPath, options = {}) {
35381
35135
  const g = cliSimpleGit(repoGitCwd);
35382
35136
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
35383
35137
  g.raw(["diff", ...RENAME_DIFF_ARGS, "--name-status", "HEAD"]).catch(() => ""),
@@ -35407,12 +35161,14 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
35407
35161
  })
35408
35162
  });
35409
35163
  rows = dedupeChangedFileRows(rows);
35410
- await enrichWorkingTreeFileRows({
35411
- rows,
35412
- repoGitCwd,
35413
- repoRelPath,
35414
- binaryByPath: parsed.binaryByPath
35415
- });
35164
+ if (options.includeFilePatches) {
35165
+ await enrichWorkingTreeFileRows({
35166
+ rows,
35167
+ repoGitCwd,
35168
+ repoRelPath,
35169
+ binaryByPath: parsed.binaryByPath
35170
+ });
35171
+ }
35416
35172
  return dedupeChangedFileRows(rows);
35417
35173
  }
35418
35174
 
@@ -35423,48 +35179,62 @@ function normRepoRel(p) {
35423
35179
  }
35424
35180
 
35425
35181
  // src/git/changes/repo/build-working-tree-change-repo-detail.ts
35182
+ async function loadRecentAndUnpushedCommits(repoDir, recentCommitsLimit) {
35183
+ const [unpushedCommits, recentCommitList] = await Promise.all([
35184
+ listUnpushedCommits(repoDir),
35185
+ listRecentCommitRefs(repoDir, recentCommitsLimit)
35186
+ ]);
35187
+ const unpushedSet = new Set(unpushedCommits.map((c) => c.sha));
35188
+ return {
35189
+ unpushedCommits,
35190
+ recentCommits: recentCommitList.commits.map((c) => ({
35191
+ ...c,
35192
+ needsPush: unpushedSet.has(c.sha)
35193
+ })),
35194
+ recentCommitsHasMore: recentCommitList.hasMore
35195
+ };
35196
+ }
35426
35197
  async function buildWorkingTreeChangeRepoDetail(options) {
35427
- const t = path32.resolve(options.targetPath);
35198
+ const t = path27.resolve(options.targetPath);
35428
35199
  if (!await isGitRepoDirectory(t)) return null;
35429
35200
  const g = cliSimpleGit(t);
35430
- let branch = "HEAD";
35431
- try {
35432
- await yieldToEventLoop2();
35433
- branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim() || "HEAD";
35434
- } catch {
35435
- branch = "HEAD";
35436
- }
35201
+ const isCommitView = options.basis.kind === "commit";
35202
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path27.resolve(options.sessionWorktreeRootPath) : null;
35203
+ const legacyNested = options.legacyRepoNestedSessionLayout;
35437
35204
  await yieldToEventLoop2();
35438
- const remoteUrl = await getRemoteOriginUrl(t);
35205
+ const branchPromise = g.raw(["rev-parse", "--abbrev-ref", "HEAD"]).then((out) => out.trim() || "HEAD").catch(() => "HEAD");
35206
+ const remoteUrlPromise = getRemoteOriginUrl(t);
35207
+ const topLevelPromise = sessionWtRoot ? Promise.resolve(t) : g.raw(["rev-parse", "--show-toplevel"]).then((out) => out.trim()).catch(() => t);
35208
+ const [branch, remoteUrl, topOrT] = await Promise.all([
35209
+ branchPromise,
35210
+ remoteUrlPromise,
35211
+ topLevelPromise
35212
+ ]);
35439
35213
  const remoteDisplay = formatRemoteDisplayLabel(remoteUrl);
35440
- const sessionWtRoot = options.sessionWorktreeRootPath ? path32.resolve(options.sessionWorktreeRootPath) : null;
35441
- const legacyNested = options.legacyRepoNestedSessionLayout;
35442
35214
  let repoRelPath;
35443
35215
  if (sessionWtRoot) {
35444
- const anchor = legacyNested ? path32.dirname(t) : t;
35445
- const relNorm = path32.relative(sessionWtRoot, anchor);
35216
+ const anchor = legacyNested ? path27.dirname(t) : t;
35217
+ const relNorm = path27.relative(sessionWtRoot, anchor);
35446
35218
  repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
35447
35219
  } else {
35448
- let top = t;
35449
- try {
35450
- await yieldToEventLoop2();
35451
- top = (await g.raw(["rev-parse", "--show-toplevel"])).trim();
35452
- } catch {
35453
- top = t;
35454
- }
35455
- const rel = path32.relative(options.bridgeRoot, path32.resolve(top)).replace(/\\/g, "/") || ".";
35456
- repoRelPath = rel.startsWith("..") ? path32.basename(path32.resolve(top)) : rel;
35220
+ const top = topOrT;
35221
+ const rel = path27.relative(options.bridgeRoot, path27.resolve(top)).replace(/\\/g, "/") || ".";
35222
+ repoRelPath = rel.startsWith("..") ? path27.basename(path27.resolve(top)) : rel;
35457
35223
  }
35458
35224
  const norm = normRepoRel(repoRelPath === "" ? "." : repoRelPath);
35459
35225
  if (options.repoFilterRelPath && norm !== options.repoFilterRelPath) return null;
35460
35226
  const repoDisplayName = formatRepoShortTitle(remoteUrl, norm === "." ? "." : norm);
35461
35227
  const relForList = norm === "." ? "." : norm;
35462
- const files = options.basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, options.basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
35463
- const st = await g.status();
35464
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
35465
- const [unpushedCommits, recentCommitList] = await Promise.all([
35466
- listUnpushedCommits(t),
35467
- listRecentCommits(t, options.recentCommitsLimit)
35228
+ const commitSha = isCommitView ? options.basis.sha.trim() : null;
35229
+ const filesPromise = isCommitView && commitSha ? listChangedFilesForCommit(t, relForList, commitSha, {
35230
+ includeFilePatches: options.includeFilePatches
35231
+ }) : listChangedFilesForRepo(t, relForList, { includeFilePatches: options.includeFilePatches });
35232
+ const statusPromise = isCommitView ? Promise.resolve(false) : g.status().then((st) => (st.files?.length ?? 0) > 0);
35233
+ const commitsPromise = isCommitView ? Promise.resolve(null) : loadRecentAndUnpushedCommits(t, options.recentCommitsLimit);
35234
+ const [files, hasUncommittedChanges, commitData] = await Promise.all([
35235
+ filesPromise,
35236
+ statusPromise,
35237
+ commitsPromise
35468
35238
  ]);
35469
35239
  return {
35470
35240
  repoRelPath: norm,
@@ -35474,18 +35244,18 @@ async function buildWorkingTreeChangeRepoDetail(options) {
35474
35244
  remoteDisplay,
35475
35245
  files,
35476
35246
  hasUncommittedChanges,
35477
- unpushedCommits,
35478
- recentCommits: recentCommitList.commits,
35479
- recentCommitsHasMore: recentCommitList.hasMore,
35480
- changesView: options.basis.kind === "commit" ? "commit" : "working",
35481
- changesCommitSha: options.basis.kind === "commit" ? options.basis.sha.trim() : null
35247
+ unpushedCommits: commitData?.unpushedCommits ?? [],
35248
+ recentCommits: commitData?.recentCommits ?? [],
35249
+ recentCommitsHasMore: commitData?.recentCommitsHasMore ?? false,
35250
+ changesView: isCommitView ? "commit" : "working",
35251
+ changesCommitSha: commitSha
35482
35252
  };
35483
35253
  }
35484
35254
 
35485
35255
  // src/git/changes/repo/get-working-tree-change-repo-details.ts
35486
35256
  async function getWorkingTreeChangeRepoDetails(options) {
35487
- const bridgeRoot = path33.resolve(getBridgeRoot());
35488
- const sessionWtRoot = options.sessionWorktreeRootPath ? path33.resolve(options.sessionWorktreeRootPath) : null;
35257
+ const bridgeRoot = path28.resolve(getBridgeRoot());
35258
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path28.resolve(options.sessionWorktreeRootPath) : null;
35489
35259
  const legacyNested = options.legacyRepoNestedSessionLayout === true;
35490
35260
  const out = [];
35491
35261
  const filter = options.repoFilterRelPath != null ? normRepoRel(options.repoFilterRelPath) : null;
@@ -35499,14 +35269,16 @@ async function getWorkingTreeChangeRepoDetails(options) {
35499
35269
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
35500
35270
  for (let i = 0; i < options.commitTargetPaths.length; i++) {
35501
35271
  await yieldToEventLoop2();
35272
+ const targetPath = options.commitTargetPaths[i];
35502
35273
  const detail = await buildWorkingTreeChangeRepoDetail({
35503
- targetPath: options.commitTargetPaths[i],
35274
+ targetPath,
35504
35275
  bridgeRoot,
35505
35276
  sessionWorktreeRootPath: sessionWtRoot,
35506
35277
  legacyRepoNestedSessionLayout: legacyNested,
35507
35278
  repoFilterRelPath: filter,
35508
35279
  basis,
35509
- recentCommitsLimit: options.recentCommitsLimit
35280
+ recentCommitsLimit: options.recentCommitsLimit,
35281
+ includeFilePatches: options.includeFilePatches
35510
35282
  });
35511
35283
  if (!detail) continue;
35512
35284
  out.push(detail);
@@ -35515,378 +35287,846 @@ async function getWorkingTreeChangeRepoDetails(options) {
35515
35287
  return out;
35516
35288
  }
35517
35289
 
35518
- // src/git/changes/status/working-tree-changed-path-count.ts
35519
- function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
35520
- const kindByPath = parseNameStatusLines(nameStatusLines);
35521
- const numByPath = parseNumstat(numstatLines);
35522
- const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
35523
- for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
35524
- paths.add(p);
35525
- }
35526
- return paths.size;
35527
- }
35528
-
35529
- // src/git/changes/count-working-tree-changed-files.ts
35530
- async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
35531
- return runGitTask(async () => {
35290
+ // src/git/changes/repo/get-working-tree-change-file-patch.ts
35291
+ import * as path29 from "node:path";
35292
+ async function getWorkingTreeChangeFilePatch(options) {
35293
+ const repoGitCwd = path29.resolve(options.repoGitCwd);
35294
+ if (!await isGitRepoDirectory(repoGitCwd)) {
35295
+ throw new Error("Not a git repository");
35296
+ }
35297
+ const normRel = options.repoRelPath === "." || options.repoRelPath === "" ? "." : options.repoRelPath;
35298
+ const pathInRepo = pathInRepoFromWorkspace(options.workspaceRelPath, normRel);
35299
+ const movedFromPathInRepo = options.change === "moved" && options.movedFromWorkspaceRelPath ? pathInRepoFromWorkspace(options.movedFromWorkspaceRelPath, normRel) : void 0;
35300
+ if (options.basis.kind === "commit") {
35301
+ const commitSha = options.basis.sha.trim();
35302
+ if (!commitSha) throw new Error("commit sha is required for commit file patch");
35532
35303
  const g = cliSimpleGit(repoGitCwd);
35533
- const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
35534
- g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
35535
- g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
35536
- g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
35537
- ]);
35538
- return workingTreeChangedPathCount(
35539
- String(nameStatusRaw).split("\n"),
35540
- String(numstatRaw).split("\n"),
35541
- String(untrackedRaw).split("\n")
35304
+ const parentSha = await parentForCommitDiff(g, commitSha);
35305
+ const range = `${parentSha}..${commitSha}`;
35306
+ const patchContent2 = await unifiedDiffForFileInRange(
35307
+ repoGitCwd,
35308
+ range,
35309
+ pathInRepo,
35310
+ options.change,
35311
+ movedFromPathInRepo
35542
35312
  );
35313
+ const isBinary = Boolean(patchContent2?.includes("Binary files"));
35314
+ const totalLines2 = await resolveCommitFileTotalLines({
35315
+ repoGitCwd,
35316
+ pathInRepo,
35317
+ change: options.change,
35318
+ commitSha,
35319
+ parentSha,
35320
+ isBinary
35321
+ });
35322
+ return { patchContent: patchContent2, totalLines: totalLines2 };
35323
+ }
35324
+ const filePath = path29.join(repoGitCwd, pathInRepo);
35325
+ const hydrateKind = options.change === "moved" ? "modified" : options.change;
35326
+ let patchContent = await unifiedDiffForFile(
35327
+ repoGitCwd,
35328
+ pathInRepo,
35329
+ options.change,
35330
+ movedFromPathInRepo
35331
+ );
35332
+ if (patchContent) {
35333
+ patchContent = await hydrateUnifiedPatchWithFileContext(
35334
+ patchContent,
35335
+ filePath,
35336
+ repoGitCwd,
35337
+ pathInRepo,
35338
+ hydrateKind
35339
+ );
35340
+ }
35341
+ const totalLines = await resolveWorkingTreeFileTotalLines({
35342
+ repoGitCwd,
35343
+ pathInRepo,
35344
+ change: options.change,
35345
+ isBinary: Boolean(patchContent?.includes("Binary files"))
35543
35346
  });
35347
+ return { patchContent, totalLines };
35544
35348
  }
35545
35349
 
35546
- // src/git/status/working-tree-status.ts
35547
- async function getRepoWorkingTreeStatus(repoDir) {
35548
- return runGitTask(async () => {
35549
- const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
35550
- const hasUncommittedChanges = uncommittedFileCount > 0;
35551
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
35552
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
35350
+ // src/worktrees/manager/git/get-session-working-tree-change-details.ts
35351
+ async function getSessionWorkingTreeChangeDetails(cache2, sessionId, discover, opts) {
35352
+ const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
35353
+ return getWorkingTreeChangeRepoDetails({
35354
+ commitTargetPaths: targets,
35355
+ sessionWorktreeRootPath: cache2.getWorkingTreeRelRoot(sessionId),
35356
+ legacyRepoNestedSessionLayout: cache2.isLegacyNestedLayout(sessionId),
35357
+ repoFilterRelPath: opts?.repoRelPath?.trim() ? normRepoRel(opts.repoRelPath.trim()) : null,
35358
+ basis: opts?.basis,
35359
+ recentCommitsLimit: opts?.recentCommitsLimit,
35360
+ includeFilePatches: false
35553
35361
  });
35554
35362
  }
35555
- async function aggregateSessionPathsWorkingTreeStatus(paths) {
35556
- let hasUncommittedChanges = false;
35557
- let hasUnpushedCommits = false;
35558
- let uncommittedFileCount = 0;
35559
- await forEachWithGitYield(paths, async (p) => {
35560
- const s = await getRepoWorkingTreeStatus(p);
35561
- uncommittedFileCount += s.uncommittedFileCount;
35562
- if (s.hasUncommittedChanges) hasUncommittedChanges = true;
35563
- if (s.hasUnpushedCommits) hasUnpushedCommits = true;
35564
- });
35565
- return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
35363
+
35364
+ // src/worktrees/manager/git/parse-working-tree-change-kind.ts
35365
+ function parseWorkingTreeChangeKind(value) {
35366
+ if (value === "added" || value === "removed" || value === "modified" || value === "moved") return value;
35367
+ return null;
35566
35368
  }
35567
- async function pushAheadOfUpstreamForPaths(paths) {
35568
- await forEachWithGitYield(paths, async (p) => {
35569
- const g = cliSimpleGit(p);
35570
- const ahead = await commitsAheadOfRemoteTracking(p);
35571
- if (ahead <= 0) return;
35572
- await g.push();
35573
- });
35369
+
35370
+ // src/worktrees/manager/git/get-session-working-tree-change-file-patch.ts
35371
+ import * as path31 from "node:path";
35372
+
35373
+ // src/git/changes/repo/resolve-checkout-repo-rel-path.ts
35374
+ import * as path30 from "node:path";
35375
+ async function resolveCheckoutRepoRelPath(options) {
35376
+ const t = path30.resolve(options.checkoutPath);
35377
+ const g = cliSimpleGit(t);
35378
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path30.resolve(options.sessionWorktreeRootPath) : null;
35379
+ const legacyNested = options.legacyRepoNestedSessionLayout;
35380
+ let repoRelPath;
35381
+ if (sessionWtRoot) {
35382
+ const anchor = legacyNested ? path30.dirname(t) : t;
35383
+ const relNorm = path30.relative(sessionWtRoot, anchor);
35384
+ repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
35385
+ } else {
35386
+ let top = t;
35387
+ try {
35388
+ await yieldToEventLoop2();
35389
+ top = (await g.raw(["rev-parse", "--show-toplevel"])).trim();
35390
+ } catch {
35391
+ top = t;
35392
+ }
35393
+ const rel = path30.relative(options.bridgeRoot, path30.resolve(top)).replace(/\\/g, "/") || ".";
35394
+ repoRelPath = rel.startsWith("..") ? path30.basename(path30.resolve(top)) : rel;
35395
+ }
35396
+ return normRepoRel(repoRelPath === "" ? "." : repoRelPath);
35574
35397
  }
35575
35398
 
35576
- // src/git/branches/commit-and-push.ts
35577
- async function gitCommitAllIfDirty(repoDir, message, options) {
35578
- const g = cliSimpleGit(repoDir);
35579
- const st = await g.status();
35580
- if (!st.files?.length) return;
35581
- const branch = options.branch.trim();
35582
- if (!branch) {
35583
- throw new Error("Branch name is required");
35399
+ // src/worktrees/manager/git/get-session-working-tree-change-file-patch.ts
35400
+ async function getSessionWorkingTreeChangeFilePatch(cache2, sessionId, discover, opts) {
35401
+ const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
35402
+ const bridgeRoot = path31.resolve(getBridgeRoot());
35403
+ const sessionWtRoot = cache2.getWorkingTreeRelRoot(sessionId) ? path31.resolve(cache2.getWorkingTreeRelRoot(sessionId)) : null;
35404
+ const legacyNested = cache2.isLegacyNestedLayout(sessionId);
35405
+ const filter = normRepoRel(opts.repoRelPath.trim());
35406
+ for (const targetPath of targets) {
35407
+ const checkoutPath = path31.resolve(targetPath);
35408
+ if (!await isGitRepoDirectory(checkoutPath)) continue;
35409
+ const repoRelPath = await resolveCheckoutRepoRelPath({
35410
+ checkoutPath,
35411
+ bridgeRoot,
35412
+ sessionWorktreeRootPath: sessionWtRoot,
35413
+ legacyRepoNestedSessionLayout: legacyNested
35414
+ });
35415
+ if (repoRelPath !== filter) continue;
35416
+ return getWorkingTreeChangeFilePatch({
35417
+ repoGitCwd: checkoutPath,
35418
+ repoRelPath: filter,
35419
+ workspaceRelPath: opts.workspaceRelPath.trim(),
35420
+ change: opts.change,
35421
+ movedFromWorkspaceRelPath: opts.movedFromWorkspaceRelPath?.trim() || null,
35422
+ basis: opts.basis
35423
+ });
35584
35424
  }
35585
- const branches = await g.branchLocal();
35586
- const localNames = new Set(branches.all.map((b) => b.replace(/^\*\s*/, "").trim()));
35587
- if (!localNames.has(branch)) {
35588
- await g.checkoutLocalBranch(branch);
35425
+ throw new Error(`Repository not found: ${filter}`);
35426
+ }
35427
+
35428
+ // src/worktrees/manager/git/push-session-upstream.ts
35429
+ async function pushSessionUpstream(cache2, sessionId, discover) {
35430
+ try {
35431
+ await pushAheadOfUpstreamForPaths(await resolveCommitTargetsAsync(sessionId, cache2, discover));
35432
+ return { ok: true };
35433
+ } catch (e) {
35434
+ const err = e instanceof Error ? e.message : String(e);
35435
+ return { ok: false, error: err };
35436
+ }
35437
+ }
35438
+
35439
+ // src/worktrees/remove-session-worktrees.ts
35440
+ import * as fs22 from "node:fs";
35441
+
35442
+ // src/git/worktrees/worktree-remove.ts
35443
+ import * as fs21 from "node:fs";
35444
+
35445
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
35446
+ import * as fs20 from "node:fs";
35447
+ import * as path32 from "node:path";
35448
+ function resolveMainRepoFromWorktreeGitFile(wt) {
35449
+ const gitDirFile = path32.join(wt, ".git");
35450
+ if (!fs20.existsSync(gitDirFile) || !fs20.statSync(gitDirFile).isFile()) return "";
35451
+ const first2 = fs20.readFileSync(gitDirFile, "utf8").trim();
35452
+ const m = first2.match(/^gitdir:\s*(.+)$/im);
35453
+ if (!m) return "";
35454
+ const gitWorktreePath = path32.resolve(wt, m[1].trim());
35455
+ const gitDir = path32.dirname(path32.dirname(gitWorktreePath));
35456
+ return path32.dirname(gitDir);
35457
+ }
35458
+
35459
+ // src/git/worktrees/worktree-remove.ts
35460
+ async function gitWorktreeRemoveForce(worktreePath) {
35461
+ const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
35462
+ if (mainRepo) {
35463
+ await cliSimpleGit(mainRepo).raw(["worktree", "remove", "--force", worktreePath]);
35589
35464
  } else {
35590
- await g.checkout(branch);
35465
+ fs21.rmSync(worktreePath, { recursive: true, force: true });
35591
35466
  }
35592
- await g.add(".");
35593
- await g.commit(message);
35594
- if (options.push) {
35595
- await g.push(["-u", "origin", branch]);
35467
+ }
35468
+
35469
+ // src/worktrees/remove-session-worktrees.ts
35470
+ async function removeSessionWorktrees(paths, log2) {
35471
+ for (const wt of paths) {
35472
+ try {
35473
+ await gitWorktreeRemoveForce(wt);
35474
+ log2(`[worktrees] Removed worktree ${wt}`);
35475
+ } catch (e) {
35476
+ log2(`[worktrees] Remove failed for ${wt}: ${e instanceof Error ? e.message : String(e)}`);
35477
+ try {
35478
+ fs22.rmSync(wt, { recursive: true, force: true });
35479
+ } catch {
35480
+ }
35481
+ }
35482
+ }
35483
+ }
35484
+
35485
+ // src/worktrees/manager/git/remove-session-worktree-checkouts.ts
35486
+ async function removeSessionWorktreeCheckouts(cache2, sessionId, log2) {
35487
+ const paths = cache2.clearSession(sessionId);
35488
+ if (!paths?.length) return;
35489
+ await removeSessionWorktrees(paths, log2);
35490
+ }
35491
+
35492
+ // src/git/branches/rename-branch.ts
35493
+ async function gitRenameCurrentBranch(repoDir, newName) {
35494
+ const g = cliSimpleGit(repoDir);
35495
+ await g.raw(["branch", "-m", newName]);
35496
+ }
35497
+
35498
+ // src/worktrees/rename-session-worktree-branches.ts
35499
+ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
35500
+ const safe = newBranch.replace(/[^a-zA-Z0-9/_-]+/g, "-").slice(0, 80) || "session-branch";
35501
+ for (const wt of paths) {
35502
+ try {
35503
+ await gitRenameCurrentBranch(wt, safe);
35504
+ log2(`[worktrees] Renamed branch in ${wt} \u2192 ${safe}`);
35505
+ } catch (e) {
35506
+ log2(
35507
+ `[worktrees] Branch rename failed in ${wt}: ${e instanceof Error ? e.message : String(e)}`
35508
+ );
35509
+ }
35510
+ }
35511
+ }
35512
+
35513
+ // src/worktrees/manager/git/rename-session-worktree-branch.ts
35514
+ async function renameSessionWorktreeBranch(cache2, sessionId, newBranch, log2) {
35515
+ const paths = cache2.getRepoCheckoutPathsRef(sessionId);
35516
+ if (!paths?.length) return;
35517
+ await renameSessionWorktreeBranches(paths, newBranch, log2);
35518
+ }
35519
+
35520
+ // src/worktrees/discovery/discover-session-worktree-on-disk.ts
35521
+ import * as fs26 from "node:fs";
35522
+ import * as path37 from "node:path";
35523
+
35524
+ // src/worktrees/discovery/collect-worktree-paths.ts
35525
+ import * as fs24 from "node:fs";
35526
+ import * as path34 from "node:path";
35527
+
35528
+ // src/worktrees/discovery/disk-walk-constants.ts
35529
+ var DISK_WALK_YIELD_EVERY = 64;
35530
+ var LEGACY_WALK_MAX_DEPTH = 12;
35531
+ var SKIP_DISK_WALK_DIR_NAMES = /* @__PURE__ */ new Set([
35532
+ "node_modules",
35533
+ "bower_components",
35534
+ "vendor",
35535
+ "Pods",
35536
+ "Carthage",
35537
+ "DerivedData",
35538
+ ".yarn",
35539
+ ".pnpm-store",
35540
+ "dist",
35541
+ "build",
35542
+ ".next",
35543
+ "coverage",
35544
+ ".turbo",
35545
+ ".cache",
35546
+ "target",
35547
+ "__pycache__"
35548
+ ]);
35549
+ function shouldSkipDiskWalkEntry(name) {
35550
+ return name.startsWith(".") || SKIP_DISK_WALK_DIR_NAMES.has(name);
35551
+ }
35552
+
35553
+ // src/worktrees/discovery/disk-walk-utils.ts
35554
+ import * as fs23 from "node:fs";
35555
+ import * as path33 from "node:path";
35556
+ async function yieldDuringDiskWalk(state) {
35557
+ state.entries++;
35558
+ if (state.entries % DISK_WALK_YIELD_EVERY !== 0) return true;
35559
+ if (isCliImmediateShutdownRequested()) return false;
35560
+ await yieldToEventLoop();
35561
+ return !isCliImmediateShutdownRequested();
35562
+ }
35563
+ async function isGitDir(dirPath) {
35564
+ try {
35565
+ await fs23.promises.access(path33.join(dirPath, ".git"));
35566
+ return true;
35567
+ } catch {
35568
+ return false;
35569
+ }
35570
+ }
35571
+
35572
+ // src/worktrees/discovery/collect-worktree-paths.ts
35573
+ async function collectGitRepoRootsUnderDirectory(rootPath) {
35574
+ const out = [];
35575
+ const state = { entries: 0 };
35576
+ const walk = async (dir) => {
35577
+ if (!await yieldDuringDiskWalk(state)) return;
35578
+ if (await isGitDir(dir)) {
35579
+ out.push(path34.resolve(dir));
35580
+ return;
35581
+ }
35582
+ let entries;
35583
+ try {
35584
+ entries = await fs24.promises.readdir(dir, { withFileTypes: true });
35585
+ } catch {
35586
+ return;
35587
+ }
35588
+ for (const e of entries) {
35589
+ if (shouldSkipDiskWalkEntry(e.name)) continue;
35590
+ const full = path34.join(dir, e.name);
35591
+ if (!e.isDirectory()) continue;
35592
+ await walk(full);
35593
+ }
35594
+ };
35595
+ await walk(path34.resolve(rootPath));
35596
+ return { paths: [...new Set(out)], entriesVisited: state.entries };
35597
+ }
35598
+ async function collectWorktreeRootsNamed(root, sessionId, maxDepth = LEGACY_WALK_MAX_DEPTH) {
35599
+ const out = [];
35600
+ const state = { entries: 0 };
35601
+ const walk = async (dir, depth) => {
35602
+ if (depth > maxDepth) return;
35603
+ if (!await yieldDuringDiskWalk(state)) return;
35604
+ let entries;
35605
+ try {
35606
+ entries = await fs24.promises.readdir(dir, { withFileTypes: true });
35607
+ } catch {
35608
+ return;
35609
+ }
35610
+ for (const e of entries) {
35611
+ if (shouldSkipDiskWalkEntry(e.name)) continue;
35612
+ const full = path34.join(dir, e.name);
35613
+ if (!e.isDirectory()) continue;
35614
+ if (e.name === sessionId) {
35615
+ if (await isGitDir(full)) out.push(path34.resolve(full));
35616
+ continue;
35617
+ }
35618
+ if (await isGitDir(full)) continue;
35619
+ await walk(full, depth + 1);
35620
+ }
35621
+ };
35622
+ await walk(root, 0);
35623
+ return { paths: out, entriesVisited: state.entries };
35624
+ }
35625
+
35626
+ // src/worktrees/discovery/layout-keys-for-bridge-root.ts
35627
+ import * as path35 from "node:path";
35628
+ function layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk) {
35629
+ const bridge = path35.resolve(bridgeRoot);
35630
+ const preferred = getLauncherDirNameIfPresent(layout, bridgeRoot);
35631
+ const relevant = layout.launcherCwds.filter((e) => {
35632
+ const entry = path35.resolve(e.absolutePath);
35633
+ return bridge === entry || bridge.startsWith(entry + path35.sep) || entry.startsWith(bridge + path35.sep);
35634
+ }).sort((a, b) => path35.resolve(b.absolutePath).length - path35.resolve(a.absolutePath).length).map((e) => e.dirName).filter((name) => keysOnDisk.includes(name));
35635
+ const ordered = [];
35636
+ const seen = /* @__PURE__ */ new Set();
35637
+ const add = (k) => {
35638
+ if (seen.has(k) || !keysOnDisk.includes(k)) return;
35639
+ seen.add(k);
35640
+ ordered.push(k);
35641
+ };
35642
+ if (preferred) add(preferred);
35643
+ for (const k of relevant) add(k);
35644
+ if (relevant.length > 0) {
35645
+ return ordered;
35646
+ }
35647
+ for (const k of keysOnDisk) add(k);
35648
+ return ordered;
35649
+ }
35650
+
35651
+ // src/worktrees/discovery/try-binding-from-session-directory.ts
35652
+ import * as fs25 from "node:fs";
35653
+ import * as path36 from "node:path";
35654
+ async function tryBindingFromSessionDirectory(sessionDir) {
35655
+ let st;
35656
+ try {
35657
+ st = await fs25.promises.stat(sessionDir);
35658
+ } catch {
35659
+ return null;
35596
35660
  }
35661
+ if (!st.isDirectory()) return null;
35662
+ const { paths: worktreePaths } = await collectGitRepoRootsUnderDirectory(sessionDir);
35663
+ if (worktreePaths.length === 0) return null;
35664
+ const abs = path36.resolve(sessionDir);
35665
+ return {
35666
+ sessionParentPath: abs,
35667
+ workingTreeRelRoot: abs,
35668
+ repoCheckoutPaths: worktreePaths
35669
+ };
35597
35670
  }
35598
35671
 
35599
- // src/worktrees/commit-session-worktrees.ts
35600
- async function commitSessionWorktrees(options) {
35601
- const { paths, branch, message, push } = options;
35672
+ // src/worktrees/discovery/discover-session-worktree-on-disk.ts
35673
+ async function discoverSessionWorktreeOnDisk(options) {
35674
+ const { sessionId, worktreesRootPath, layout, bridgeRoot } = options;
35675
+ const sid = sessionId.trim();
35676
+ if (!sid) return null;
35602
35677
  try {
35603
- for (const wt of paths) {
35604
- await gitCommitAllIfDirty(wt, message, { push, branch });
35678
+ await fs26.promises.access(worktreesRootPath);
35679
+ } catch {
35680
+ return null;
35681
+ }
35682
+ const preferredKey = getLauncherDirNameIfPresent(layout, bridgeRoot);
35683
+ const keysOnDisk = [];
35684
+ if (preferredKey) keysOnDisk.push(preferredKey);
35685
+ try {
35686
+ for (const name of await fs26.promises.readdir(worktreesRootPath)) {
35687
+ if (name.startsWith(".")) continue;
35688
+ const p = path37.join(worktreesRootPath, name);
35689
+ let st;
35690
+ try {
35691
+ st = await fs26.promises.stat(p);
35692
+ } catch {
35693
+ continue;
35694
+ }
35695
+ if (!st.isDirectory()) continue;
35696
+ if (!keysOnDisk.includes(name)) keysOnDisk.push(name);
35697
+ }
35698
+ } catch {
35699
+ return null;
35700
+ }
35701
+ const keys = layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk);
35702
+ for (const key of keys) {
35703
+ if (isCliImmediateShutdownRequested()) return null;
35704
+ await yieldToEventLoop();
35705
+ const layoutRoot = path37.join(worktreesRootPath, key);
35706
+ let layoutSt;
35707
+ try {
35708
+ layoutSt = await fs26.promises.stat(layoutRoot);
35709
+ } catch {
35710
+ continue;
35711
+ }
35712
+ if (!layoutSt.isDirectory()) continue;
35713
+ const sessionDir = path37.join(layoutRoot, sid);
35714
+ const nested = await tryBindingFromSessionDirectory(sessionDir);
35715
+ if (nested) return nested;
35716
+ const legacy = await collectWorktreeRootsNamed(layoutRoot, sid);
35717
+ if (legacy.paths.length > 0) {
35718
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacy.paths) ?? path37.resolve(legacy.paths[0]);
35719
+ return {
35720
+ sessionParentPath: path37.resolve(isolated),
35721
+ workingTreeRelRoot: path37.resolve(layoutRoot),
35722
+ repoCheckoutPaths: legacy.paths.map((p) => path37.resolve(p))
35723
+ };
35605
35724
  }
35606
- return { ok: true };
35607
- } catch (e) {
35608
- const err = e instanceof Error ? e.message : String(e);
35609
- return { ok: false, error: err };
35610
35725
  }
35726
+ return null;
35611
35727
  }
35612
35728
 
35613
- // src/worktrees/remove-session-worktrees.ts
35614
- import * as fs28 from "node:fs";
35615
-
35616
- // src/git/worktrees/worktree-remove.ts
35729
+ // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
35617
35730
  import * as fs27 from "node:fs";
35731
+ import * as path39 from "node:path";
35618
35732
 
35619
- // src/git/worktrees/resolve-main-repo-from-git-file.ts
35620
- import * as fs26 from "node:fs";
35621
- import * as path34 from "node:path";
35622
- function resolveMainRepoFromWorktreeGitFile(wt) {
35623
- const gitDirFile = path34.join(wt, ".git");
35624
- if (!fs26.existsSync(gitDirFile) || !fs26.statSync(gitDirFile).isFile()) return "";
35625
- const first2 = fs26.readFileSync(gitDirFile, "utf8").trim();
35626
- const m = first2.match(/^gitdir:\s*(.+)$/im);
35627
- if (!m) return "";
35628
- const gitWorktreePath = path34.resolve(wt, m[1].trim());
35629
- const gitDir = path34.dirname(path34.dirname(gitWorktreePath));
35630
- return path34.dirname(gitDir);
35631
- }
35632
-
35633
- // src/git/worktrees/worktree-remove.ts
35634
- async function gitWorktreeRemoveForce(worktreePath) {
35635
- const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
35636
- if (mainRepo) {
35637
- await cliSimpleGit(mainRepo).raw(["worktree", "remove", "--force", worktreePath]);
35638
- } else {
35639
- fs27.rmSync(worktreePath, { recursive: true, force: true });
35733
+ // src/worktrees/discovery/discover-legacy-binding-ascending-from-checkout.ts
35734
+ import * as path38 from "node:path";
35735
+ async function discoverLegacyBindingAscendingFromCheckout(sessionId, checkoutPath) {
35736
+ const sid = sessionId.trim();
35737
+ if (!sid) return null;
35738
+ const hintR = path38.resolve(checkoutPath);
35739
+ let best = null;
35740
+ let cur = path38.dirname(hintR);
35741
+ for (let i = 0; i < 40; i++) {
35742
+ if (isCliImmediateShutdownRequested()) return best;
35743
+ await yieldToEventLoop();
35744
+ const paths = await collectWorktreeRootsNamed(cur, sid);
35745
+ if (paths.paths.some((p) => path38.resolve(p) === hintR)) {
35746
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(paths.paths) ?? path38.resolve(paths.paths[0]);
35747
+ best = {
35748
+ sessionParentPath: path38.resolve(isolated),
35749
+ workingTreeRelRoot: path38.resolve(cur),
35750
+ repoCheckoutPaths: paths.paths.map((p) => path38.resolve(p))
35751
+ };
35752
+ }
35753
+ const next = path38.dirname(cur);
35754
+ if (next === cur) break;
35755
+ cur = next;
35640
35756
  }
35757
+ return best;
35641
35758
  }
35642
35759
 
35643
- // src/worktrees/remove-session-worktrees.ts
35644
- async function removeSessionWorktrees(paths, log2) {
35645
- for (const wt of paths) {
35646
- try {
35647
- await gitWorktreeRemoveForce(wt);
35648
- log2(`[worktrees] Removed worktree ${wt}`);
35649
- } catch (e) {
35650
- log2(`[worktrees] Remove failed for ${wt}: ${e instanceof Error ? e.message : String(e)}`);
35651
- try {
35652
- fs28.rmSync(wt, { recursive: true, force: true });
35653
- } catch {
35760
+ // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
35761
+ async function discoverSessionWorktreesUnderSessionWorktreeRoot(sessionWorktreeRootPathOrHint, sessionId) {
35762
+ const sid = sessionId.trim();
35763
+ if (!sid) return null;
35764
+ const hint = path39.resolve(sessionWorktreeRootPathOrHint);
35765
+ const underHint = await tryBindingFromSessionDirectory(path39.join(hint, sid));
35766
+ if (underHint) return underHint;
35767
+ const direct = await tryBindingFromSessionDirectory(hint);
35768
+ if (direct) {
35769
+ if (path39.basename(hint) === sid && await isGitDir(hint)) {
35770
+ const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
35771
+ if (legacyFromCheckout && legacyFromCheckout.repoCheckoutPaths.length > direct.repoCheckoutPaths.length) {
35772
+ return legacyFromCheckout;
35654
35773
  }
35655
35774
  }
35775
+ return direct;
35656
35776
  }
35777
+ if (path39.basename(hint) === sid && await isGitDir(hint)) {
35778
+ const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
35779
+ if (legacyFromCheckout) return legacyFromCheckout;
35780
+ }
35781
+ let st;
35782
+ try {
35783
+ st = await fs27.promises.stat(hint);
35784
+ } catch {
35785
+ return null;
35786
+ }
35787
+ if (!st.isDirectory()) return null;
35788
+ const legacyPaths = await collectWorktreeRootsNamed(hint, sid);
35789
+ if (legacyPaths.paths.length === 0) return null;
35790
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths.paths) ?? path39.resolve(legacyPaths.paths[0]);
35791
+ return {
35792
+ sessionParentPath: path39.resolve(isolated),
35793
+ workingTreeRelRoot: hint,
35794
+ repoCheckoutPaths: legacyPaths.paths.map((p) => path39.resolve(p))
35795
+ };
35657
35796
  }
35658
35797
 
35659
- // src/git/branches/rename-branch.ts
35660
- async function gitRenameCurrentBranch(repoDir, newName) {
35661
- const g = cliSimpleGit(repoDir);
35662
- await g.raw(["branch", "-m", newName]);
35663
- }
35664
-
35665
- // src/worktrees/rename-session-worktree-branches.ts
35666
- async function renameSessionWorktreeBranches(paths, newBranch, log2) {
35667
- const safe = newBranch.replace(/[^a-zA-Z0-9/_-]+/g, "-").slice(0, 80) || "session-branch";
35668
- for (const wt of paths) {
35669
- try {
35670
- await gitRenameCurrentBranch(wt, safe);
35671
- log2(`[worktrees] Renamed branch in ${wt} \u2192 ${safe}`);
35672
- } catch (e) {
35673
- log2(
35674
- `[worktrees] Branch rename failed in ${wt}: ${e instanceof Error ? e.message : String(e)}`
35675
- );
35798
+ // src/worktrees/manager/discover-session-binding.ts
35799
+ var discoverInflightBySessionId = /* @__PURE__ */ new Map();
35800
+ function discoverSessionBindingAsync(params) {
35801
+ const sid = params.sessionId.trim();
35802
+ if (!sid) return Promise.resolve(null);
35803
+ const existing = discoverInflightBySessionId.get(sid);
35804
+ if (existing) return existing;
35805
+ const promise2 = discoverSessionWorktreeOnDisk({
35806
+ sessionId: sid,
35807
+ worktreesRootPath: params.worktreesRootPath,
35808
+ layout: params.layout,
35809
+ bridgeRoot: getBridgeRoot()
35810
+ }).finally(() => {
35811
+ if (discoverInflightBySessionId.get(sid) === promise2) {
35812
+ discoverInflightBySessionId.delete(sid);
35676
35813
  }
35677
- }
35814
+ });
35815
+ discoverInflightBySessionId.set(sid, promise2);
35816
+ return promise2;
35678
35817
  }
35679
35818
 
35680
- // src/worktrees/manager/resolve-commit-targets.ts
35681
- function bridgeRootBinding(bridgeRoot) {
35682
- return {
35683
- sessionParentPath: bridgeRoot,
35684
- workingTreeRelRoot: bridgeRoot,
35685
- repoCheckoutPaths: [bridgeRoot]
35686
- };
35819
+ // src/worktrees/manager/resolve-isolated-session-parent-path.ts
35820
+ function resolveIsolatedSessionParentPath(sessionId, cache2, ensureRepoCheckoutPaths) {
35821
+ if (!sessionId) return null;
35822
+ const sid = sessionId.trim();
35823
+ const cached2 = cache2.getSessionParentPath(sid);
35824
+ if (cached2) return cached2;
35825
+ const paths = ensureRepoCheckoutPaths(sid);
35826
+ if (!paths?.length) return null;
35827
+ return resolveIsolatedSessionParentPathFromCheckouts(paths);
35687
35828
  }
35688
- async function resolveCommitTargetsAsync(sessionId, cache2, discover) {
35829
+ function ensureRepoCheckoutPathsForSession(sessionId, cache2) {
35830
+ if (!sessionId?.trim()) return void 0;
35831
+ const cached2 = cache2.getRepoCheckoutPaths(sessionId.trim());
35832
+ return cached2?.length ? [...cached2] : void 0;
35833
+ }
35834
+ async function ensureRepoCheckoutPathsForSessionAsync(sessionId, cache2, discover) {
35835
+ if (!sessionId?.trim()) return void 0;
35689
35836
  const sid = sessionId.trim();
35690
- const paths = cache2.getRepoCheckoutPathsRef(sid);
35691
- if (paths?.length) return paths;
35837
+ const cached2 = cache2.getRepoCheckoutPaths(sid);
35838
+ if (cached2?.length) return cached2;
35692
35839
  const disc = await discover(sid);
35693
35840
  if (disc?.repoCheckoutPaths.length) {
35694
35841
  cache2.remember(sid, disc);
35695
- return disc.repoCheckoutPaths;
35842
+ return [...disc.repoCheckoutPaths];
35696
35843
  }
35697
- const bridgeRoot = getBridgeRoot();
35698
- cache2.remember(sid, bridgeRootBinding(bridgeRoot));
35699
- return [bridgeRoot];
35844
+ return void 0;
35700
35845
  }
35701
35846
 
35702
- // src/worktrees/manager/session-git-ops.ts
35703
- async function commitSessionWorktree(cache2, params) {
35704
- const paths = cache2.getRepoCheckoutPathsRef(params.sessionId);
35705
- const targets = paths?.length ? paths : [getBridgeRoot()];
35706
- return commitSessionWorktrees({
35707
- paths: targets,
35708
- branch: params.branch,
35709
- message: params.message,
35710
- push: params.push
35711
- });
35847
+ // src/worktrees/manager/parse-session-parent.ts
35848
+ function parseSessionParent(v) {
35849
+ if (v === "bridge_root" || v === "worktrees_root") return v;
35850
+ if (v === "session_worktrees_root") return "worktrees_root";
35851
+ return null;
35712
35852
  }
35713
- async function getSessionWorkingTreeStatus(cache2, sessionId, discover) {
35714
- return aggregateSessionPathsWorkingTreeStatus(
35715
- await resolveCommitTargetsAsync(sessionId, cache2, discover)
35716
- );
35853
+
35854
+ // src/worktrees/prepare-new-session-worktrees.ts
35855
+ import * as fs28 from "node:fs";
35856
+ import * as path40 from "node:path";
35857
+
35858
+ // src/git/worktrees/worktree-add.ts
35859
+ async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch, baseRef = "HEAD") {
35860
+ const mainGit = cliSimpleGit(mainRepoPath);
35861
+ const base = baseRef.trim() || "HEAD";
35862
+ await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, base]);
35717
35863
  }
35718
- async function getSessionWorkingTreeChangeDetails(cache2, sessionId, discover, opts) {
35719
- const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
35720
- return getWorkingTreeChangeRepoDetails({
35721
- commitTargetPaths: targets,
35722
- sessionWorktreeRootPath: cache2.getWorkingTreeRelRoot(sessionId),
35723
- legacyRepoNestedSessionLayout: cache2.isLegacyNestedLayout(sessionId),
35724
- repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
35725
- basis: opts?.basis,
35726
- recentCommitsLimit: opts?.recentCommitsLimit
35727
- });
35864
+
35865
+ // src/worktrees/prepare-new-session-worktrees.ts
35866
+ function normalizeRepoRelPath2(rel) {
35867
+ return rel === "" ? "." : rel.replace(/\\/g, "/");
35728
35868
  }
35729
- async function pushSessionUpstream(cache2, sessionId, discover) {
35730
- try {
35731
- await pushAheadOfUpstreamForPaths(await resolveCommitTargetsAsync(sessionId, cache2, discover));
35732
- return { ok: true };
35733
- } catch (e) {
35734
- const err = e instanceof Error ? e.message : String(e);
35735
- return { ok: false, error: err };
35736
- }
35869
+ function resolveBaseRefForRepo(relNorm, baseBranches) {
35870
+ if (!baseBranches) return "HEAD";
35871
+ const direct = baseBranches[relNorm]?.trim();
35872
+ if (direct) return direct;
35873
+ if (relNorm !== "." && baseBranches["."]?.trim()) return baseBranches["."].trim();
35874
+ return "HEAD";
35737
35875
  }
35738
- async function removeSessionWorktreeCheckouts(cache2, sessionId, log2) {
35739
- const paths = cache2.clearSession(sessionId);
35740
- if (!paths?.length) return;
35741
- await removeSessionWorktrees(paths, log2);
35876
+ async function prepareNewSessionWorktrees(options) {
35877
+ const { worktreesRootPath, bridgeRoot, sessionId, layout, log: log2, worktreeBaseBranches } = options;
35878
+ const bridgeResolved = path40.resolve(bridgeRoot);
35879
+ const cwdKey = allocateDirNameForLauncherCwd(layout, bridgeResolved);
35880
+ const bridgeKeyDir = path40.join(worktreesRootPath, cwdKey);
35881
+ const sessionDir = path40.join(bridgeKeyDir, sessionId);
35882
+ const repos = await discoverGitReposUnderRoot(bridgeResolved);
35883
+ if (repos.length === 0) {
35884
+ log2("[worktrees] No Git repositories under bridge root; skipping worktree creation.");
35885
+ return null;
35886
+ }
35887
+ const branch = `session-${sessionId}`;
35888
+ const worktreePaths = [];
35889
+ fs28.mkdirSync(sessionDir, { recursive: true });
35890
+ for (const repo of repos) {
35891
+ let rel = path40.relative(bridgeResolved, repo.absolutePath);
35892
+ if (rel.startsWith("..") || path40.isAbsolute(rel)) continue;
35893
+ const relNorm = normalizeRepoRelPath2(rel === "" ? "." : rel);
35894
+ const wtPath = relNorm === "." ? sessionDir : path40.join(sessionDir, relNorm);
35895
+ if (relNorm !== ".") {
35896
+ fs28.mkdirSync(path40.dirname(wtPath), { recursive: true });
35897
+ }
35898
+ const baseRef = resolveBaseRefForRepo(relNorm, worktreeBaseBranches);
35899
+ try {
35900
+ await gitWorktreeAddBranch(repo.absolutePath, wtPath, branch, baseRef);
35901
+ log2(`[worktrees] Added worktree ${wtPath} (branch ${branch}, base ${baseRef}).`);
35902
+ worktreePaths.push(wtPath);
35903
+ } catch (e) {
35904
+ log2(
35905
+ `[worktrees] Worktree add failed for ${repo.absolutePath}: ${e instanceof Error ? e.message : String(e)}`
35906
+ );
35907
+ }
35908
+ }
35909
+ if (worktreePaths.length === 0) return null;
35910
+ return {
35911
+ worktreePaths,
35912
+ sessionParentPath: sessionDir,
35913
+ workingTreeRelRoot: sessionDir
35914
+ };
35742
35915
  }
35743
- async function renameSessionWorktreeBranch(cache2, sessionId, newBranch, log2) {
35744
- const paths = cache2.getRepoCheckoutPathsRef(sessionId);
35745
- if (!paths?.length) return;
35746
- await renameSessionWorktreeBranches(paths, newBranch, log2);
35916
+
35917
+ // src/worktrees/manager/prepare-and-remember-session-worktrees.ts
35918
+ async function prepareAndRememberSessionWorktrees(params) {
35919
+ const prep = await prepareNewSessionWorktrees({
35920
+ worktreesRootPath: params.worktreesRootPath,
35921
+ bridgeRoot: getBridgeRoot(),
35922
+ sessionId: params.sessionId,
35923
+ layout: params.layout,
35924
+ log: params.log,
35925
+ ...params.worktreeBaseBranches && Object.keys(params.worktreeBaseBranches).length > 0 ? { worktreeBaseBranches: params.worktreeBaseBranches } : {}
35926
+ });
35927
+ if (!prep) return void 0;
35928
+ params.cache.remember(params.sessionId, {
35929
+ sessionParentPath: prep.sessionParentPath,
35930
+ workingTreeRelRoot: prep.workingTreeRelRoot,
35931
+ repoCheckoutPaths: prep.worktreePaths
35932
+ });
35933
+ return params.cache.getSessionParentPath(params.sessionId);
35747
35934
  }
35748
35935
 
35749
- // src/worktrees/manager/session-worktree-cache.ts
35750
- import * as path35 from "node:path";
35751
- var SessionWorktreeCache = class {
35752
- sessionRepoCheckoutPaths = /* @__PURE__ */ new Map();
35753
- sessionParentPathBySession = /* @__PURE__ */ new Map();
35754
- sessionWorkingTreeRelRootBySession = /* @__PURE__ */ new Map();
35755
- remember(sessionId, binding) {
35756
- const paths = binding.repoCheckoutPaths.map((p) => path35.resolve(p));
35757
- this.sessionRepoCheckoutPaths.set(sessionId, paths);
35758
- this.sessionParentPathBySession.set(sessionId, path35.resolve(binding.sessionParentPath));
35759
- this.sessionWorkingTreeRelRootBySession.set(sessionId, path35.resolve(binding.workingTreeRelRoot));
35760
- }
35761
- clearSession(sessionId) {
35762
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
35763
- this.sessionRepoCheckoutPaths.delete(sessionId);
35764
- this.sessionParentPathBySession.delete(sessionId);
35765
- this.sessionWorkingTreeRelRootBySession.delete(sessionId);
35766
- return paths;
35767
- }
35768
- getSessionParentPath(sessionId) {
35769
- return this.sessionParentPathBySession.get(sessionId);
35770
- }
35771
- getWorkingTreeRelRoot(sessionId) {
35772
- return this.sessionWorkingTreeRelRootBySession.get(sessionId) ?? null;
35773
- }
35774
- hasSession(sessionId) {
35775
- return this.sessionParentPathBySession.has(sessionId);
35776
- }
35777
- getRepoCheckoutPaths(sessionId) {
35778
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
35779
- return paths?.length ? [...paths] : void 0;
35936
+ // src/worktrees/manager/resolve-existing-session-parent-path.ts
35937
+ async function resolveExistingSessionParentPath(sessionId, cache2, discover) {
35938
+ const cached2 = cache2.getSessionParentPath(sessionId);
35939
+ if (cached2) return cached2;
35940
+ const disc = await discover();
35941
+ if (disc) {
35942
+ cache2.remember(sessionId, disc);
35943
+ return cache2.getSessionParentPath(sessionId);
35780
35944
  }
35781
- getRepoCheckoutPathsRef(sessionId) {
35782
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
35783
- return paths?.length ? paths : void 0;
35945
+ return void 0;
35946
+ }
35947
+
35948
+ // src/worktrees/manager/resolve-explicit-session-parent-path.ts
35949
+ import * as path41 from "node:path";
35950
+ async function resolveExplicitSessionParentPath(params) {
35951
+ const resolved = path41.resolve(params.parentPathRaw);
35952
+ if (parseSessionParent(params.sessionParent) !== "worktrees_root") {
35953
+ return resolved;
35784
35954
  }
35785
- isLegacyNestedLayout(sessionId) {
35786
- const parent = this.sessionParentPathBySession.get(sessionId);
35787
- const relRoot = this.sessionWorkingTreeRelRootBySession.get(sessionId);
35788
- if (!parent || !relRoot) return false;
35789
- return path35.resolve(parent) !== path35.resolve(relRoot);
35955
+ const rememberAndReturn = (binding) => {
35956
+ params.cache.remember(params.sessionId, binding);
35957
+ return params.cache.getSessionParentPath(params.sessionId) ?? resolved;
35958
+ };
35959
+ const diskFirst = await params.discover();
35960
+ if (diskFirst) return rememberAndReturn(diskFirst);
35961
+ const fromRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(resolved, params.sessionId);
35962
+ if (fromRoot) return rememberAndReturn(fromRoot);
35963
+ let cur = resolved;
35964
+ for (let i = 0; i < 16; i++) {
35965
+ await yieldToEventLoop();
35966
+ const tryRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(cur, params.sessionId);
35967
+ if (tryRoot) return rememberAndReturn(tryRoot);
35968
+ const next = path41.dirname(cur);
35969
+ if (next === cur) break;
35970
+ cur = next;
35790
35971
  }
35791
- };
35972
+ return resolved;
35973
+ }
35792
35974
 
35793
- // src/worktrees/manager/manager.ts
35794
- var SessionWorktreeManager = class {
35795
- worktreesRootPath;
35796
- log;
35797
- cache = new SessionWorktreeCache();
35798
- layout;
35799
- constructor(options) {
35800
- this.worktreesRootPath = options.worktreesRootPath;
35801
- this.log = options.log;
35802
- this.layout = loadWorktreeLayout();
35803
- }
35804
- discoverAsync(sessionId) {
35805
- return discoverSessionBindingAsync({
35806
- sessionId,
35807
- worktreesRootPath: this.worktreesRootPath,
35808
- layout: this.layout
35975
+ // src/worktrees/manager/resolve-session-parent-path-for-prompt.ts
35976
+ async function resolveSessionParentPathForPrompt(params) {
35977
+ const { sessionId, cache: cache2, worktreesRootPath, layout, log: log2, discover, opts } = params;
35978
+ if (!sessionId) return void 0;
35979
+ const sid = sessionId.trim();
35980
+ const parentPathRaw = opts.sessionParentPath?.trim();
35981
+ if (parentPathRaw) {
35982
+ return resolveExplicitSessionParentPath({
35983
+ sessionId: sid,
35984
+ sessionParent: opts.sessionParent,
35985
+ parentPathRaw,
35986
+ cache: cache2,
35987
+ discover: () => discover(sid)
35809
35988
  });
35810
35989
  }
35811
- getIsolatedSessionParentPathForSession(sessionId) {
35812
- return resolveIsolatedSessionParentPath(
35813
- sessionId,
35814
- this.cache,
35815
- (sid) => this.ensureRepoCheckoutPathsForSession(sid)
35816
- );
35990
+ const parentKind = parseSessionParent(opts.sessionParent);
35991
+ if (parentKind === "bridge_root") {
35992
+ return void 0;
35817
35993
  }
35818
- resolveSessionParentPathForPrompt(sessionId, opts) {
35819
- return resolveSessionParentPathForPrompt({
35820
- sessionId,
35821
- cache: this.cache,
35822
- worktreesRootPath: this.worktreesRootPath,
35823
- layout: this.layout,
35824
- log: this.log,
35825
- discover: (sid) => this.discoverAsync(sid),
35826
- opts
35994
+ if (parentKind === "worktrees_root") {
35995
+ if (!opts.isNewSession) {
35996
+ return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
35997
+ }
35998
+ return prepareAndRememberSessionWorktrees({
35999
+ cache: cache2,
36000
+ sessionId: sid,
36001
+ worktreesRootPath,
36002
+ layout,
36003
+ log: log2,
36004
+ ...opts.worktreeBaseBranches ? { worktreeBaseBranches: opts.worktreeBaseBranches } : {}
35827
36005
  });
35828
36006
  }
35829
- async renameSessionBranch(sessionId, newBranch) {
35830
- await renameSessionWorktreeBranch(this.cache, sessionId, newBranch, this.log);
35831
- }
35832
- usesWorktreeSession(sessionId) {
35833
- if (!sessionId) return false;
35834
- return this.cache.hasSession(sessionId);
35835
- }
35836
- getRepoCheckoutPathsForSession(sessionId) {
35837
- if (!sessionId) return void 0;
35838
- return this.cache.getRepoCheckoutPaths(sessionId);
35839
- }
35840
- ensureRepoCheckoutPathsForSession(sessionId) {
35841
- return ensureRepoCheckoutPathsForSession(sessionId, this.cache);
35842
- }
35843
- async ensureRepoCheckoutPathsForSessionAsync(sessionId) {
35844
- return ensureRepoCheckoutPathsForSessionAsync(sessionId, this.cache, (sid) => this.discoverAsync(sid));
35845
- }
35846
- getSessionWorktreeRootForSession(sessionId) {
35847
- return this.getIsolatedSessionParentPathForSession(sessionId);
35848
- }
35849
- async removeSessionWorktrees(sessionId) {
35850
- await removeSessionWorktreeCheckouts(this.cache, sessionId, this.log);
35851
- }
35852
- async commitSession(params) {
35853
- return commitSessionWorktree(this.cache, params);
35854
- }
35855
- async getSessionWorkingTreeStatus(sessionId) {
35856
- return getSessionWorkingTreeStatus(this.cache, sessionId, (sid) => this.discoverAsync(sid));
35857
- }
35858
- async getSessionWorkingTreeChangeDetails(sessionId, opts) {
35859
- return getSessionWorkingTreeChangeDetails(this.cache, sessionId, (sid) => this.discoverAsync(sid), opts);
36007
+ if (!opts.isNewSession) {
36008
+ return resolveExistingSessionParentPath(sid, cache2, () => discover(sid));
35860
36009
  }
35861
- async pushSessionUpstream(sessionId) {
35862
- return pushSessionUpstream(this.cache, sessionId, (sid) => this.discoverAsync(sid));
36010
+ return prepareAndRememberSessionWorktrees({
36011
+ cache: cache2,
36012
+ sessionId: sid,
36013
+ worktreesRootPath,
36014
+ layout,
36015
+ log: log2
36016
+ });
36017
+ }
36018
+
36019
+ // src/worktrees/manager/session-worktree-paths.ts
36020
+ function createSessionWorktreePathsApi(ctx) {
36021
+ const discoverAsync = (sessionId) => discoverSessionBindingAsync({
36022
+ sessionId,
36023
+ worktreesRootPath: ctx.worktreesRootPath,
36024
+ layout: ctx.layout
36025
+ });
36026
+ return {
36027
+ discoverAsync,
36028
+ getIsolatedSessionParentPathForSession(sessionId) {
36029
+ return resolveIsolatedSessionParentPath(
36030
+ sessionId,
36031
+ ctx.cache,
36032
+ (sid) => ensureRepoCheckoutPathsForSession(sid, ctx.cache)
36033
+ );
36034
+ },
36035
+ resolveSessionParentPathForPrompt(sessionId, opts) {
36036
+ return resolveSessionParentPathForPrompt({
36037
+ sessionId,
36038
+ cache: ctx.cache,
36039
+ worktreesRootPath: ctx.worktreesRootPath,
36040
+ layout: ctx.layout,
36041
+ log: ctx.log,
36042
+ discover: (sid) => discoverAsync(sid),
36043
+ opts
36044
+ });
36045
+ },
36046
+ usesWorktreeSession(sessionId) {
36047
+ if (!sessionId) return false;
36048
+ return ctx.cache.hasSession(sessionId);
36049
+ },
36050
+ getRepoCheckoutPathsForSession(sessionId) {
36051
+ if (!sessionId) return void 0;
36052
+ return ctx.cache.getRepoCheckoutPaths(sessionId);
36053
+ },
36054
+ ensureRepoCheckoutPathsForSession(sessionId) {
36055
+ return ensureRepoCheckoutPathsForSession(sessionId, ctx.cache);
36056
+ },
36057
+ async ensureRepoCheckoutPathsForSessionAsync(sessionId) {
36058
+ return ensureRepoCheckoutPathsForSessionAsync(sessionId, ctx.cache, (sid) => discoverAsync(sid));
36059
+ },
36060
+ getSessionWorktreeRootForSession(sessionId) {
36061
+ return resolveIsolatedSessionParentPath(
36062
+ sessionId,
36063
+ ctx.cache,
36064
+ (sid) => ensureRepoCheckoutPathsForSession(sid, ctx.cache)
36065
+ );
36066
+ }
36067
+ };
36068
+ }
36069
+
36070
+ // src/worktrees/manager/session-worktree-git.ts
36071
+ function createSessionWorktreeGitApi(ctx) {
36072
+ const paths = createSessionWorktreePathsApi(ctx);
36073
+ return {
36074
+ async renameSessionBranch(sessionId, newBranch) {
36075
+ await renameSessionWorktreeBranch(ctx.cache, sessionId, newBranch, ctx.log);
36076
+ },
36077
+ async removeSessionWorktrees(sessionId) {
36078
+ await removeSessionWorktreeCheckouts(ctx.cache, sessionId, ctx.log);
36079
+ },
36080
+ async commitSession(params) {
36081
+ return commitSessionWorktree(ctx.cache, params);
36082
+ },
36083
+ async getSessionWorkingTreeStatus(sessionId) {
36084
+ return getSessionWorkingTreeStatus(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid));
36085
+ },
36086
+ async getSessionWorkingTreeChangeDetails(sessionId, opts) {
36087
+ return getSessionWorkingTreeChangeDetails(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid), opts);
36088
+ },
36089
+ async getSessionWorkingTreeChangeFilePatch(sessionId, opts) {
36090
+ return getSessionWorkingTreeChangeFilePatch(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid), opts);
36091
+ },
36092
+ async pushSessionUpstream(sessionId) {
36093
+ return pushSessionUpstream(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid));
36094
+ }
36095
+ };
36096
+ }
36097
+
36098
+ // src/worktrees/manager/session-worktree-manager.ts
36099
+ var SessionWorktreeManager = class {
36100
+ constructor(options) {
36101
+ const ctx = createSessionWorktreeManagerContext(options);
36102
+ Object.assign(this, createSessionWorktreePathsApi(ctx), createSessionWorktreeGitApi(ctx));
35863
36103
  }
35864
36104
  };
35865
36105
 
35866
36106
  // src/worktrees/manager/default-worktrees-root-path.ts
35867
- import * as path36 from "node:path";
36107
+ import * as path42 from "node:path";
35868
36108
  import os8 from "node:os";
35869
36109
  function defaultWorktreesRootPath() {
35870
- return path36.join(os8.homedir(), ".buildautomaton", "worktrees");
36110
+ return path42.join(os8.homedir(), ".buildautomaton", "worktrees");
35871
36111
  }
35872
36112
 
35873
36113
  // src/files/watch-file-index.ts
35874
36114
  import { watch } from "node:fs";
35875
- import path41 from "node:path";
36115
+ import path47 from "node:path";
35876
36116
 
35877
36117
  // src/files/index/paths.ts
35878
- import path37 from "node:path";
36118
+ import path43 from "node:path";
35879
36119
  import crypto2 from "node:crypto";
35880
36120
  function getCwdHashForFileIndex(resolvedCwd) {
35881
- return crypto2.createHash("sha256").update(path37.resolve(resolvedCwd)).digest("hex").slice(0, INDEX_HASH_LEN);
36121
+ return crypto2.createHash("sha256").update(path43.resolve(resolvedCwd)).digest("hex").slice(0, INDEX_HASH_LEN);
35882
36122
  }
35883
36123
 
35884
36124
  // src/files/index/build-file-index.ts
35885
- import path39 from "node:path";
36125
+ import path45 from "node:path";
35886
36126
 
35887
36127
  // src/files/index/walk-workspace-tree.ts
35888
36128
  import fs29 from "node:fs";
35889
- import path38 from "node:path";
36129
+ import path44 from "node:path";
35890
36130
  var DEPENDENCY_INSTALL_DIR_NAMES = /* @__PURE__ */ new Set([
35891
36131
  "node_modules",
35892
36132
  "bower_components",
@@ -35915,18 +36155,18 @@ async function walkWorkspaceTreeAsync(dir, baseDir, onFile, state) {
35915
36155
  if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
35916
36156
  }
35917
36157
  state.n++;
35918
- const full = path38.join(dir, name);
36158
+ const full = path44.join(dir, name);
35919
36159
  let stat2;
35920
36160
  try {
35921
36161
  stat2 = await fs29.promises.stat(full);
35922
36162
  } catch {
35923
36163
  continue;
35924
36164
  }
35925
- const relative6 = path38.relative(baseDir, full).replace(/\\/g, "/");
36165
+ const relative7 = path44.relative(baseDir, full).replace(/\\/g, "/");
35926
36166
  if (stat2.isDirectory()) {
35927
36167
  await walkWorkspaceTreeAsync(full, baseDir, onFile, state);
35928
36168
  } else if (stat2.isFile()) {
35929
- onFile(relative6);
36169
+ onFile(relative7);
35930
36170
  }
35931
36171
  }
35932
36172
  }
@@ -36024,7 +36264,7 @@ async function collectWorkspacePathsAsync(resolved) {
36024
36264
  }
36025
36265
  async function buildFileIndexAsync(cwd) {
36026
36266
  return withFileIndexSqliteLock(async () => {
36027
- const resolved = path39.resolve(cwd);
36267
+ const resolved = path45.resolve(cwd);
36028
36268
  await yieldToEventLoop();
36029
36269
  assertNotShutdown();
36030
36270
  const paths = await collectWorkspacePathsAsync(resolved);
@@ -36036,7 +36276,7 @@ async function buildFileIndexAsync(cwd) {
36036
36276
  }
36037
36277
 
36038
36278
  // src/files/index/ensure-file-index.ts
36039
- import path40 from "node:path";
36279
+ import path46 from "node:path";
36040
36280
 
36041
36281
  // src/files/index/search-file-index.ts
36042
36282
  function escapeLikePattern(fragment) {
@@ -36088,7 +36328,7 @@ async function searchBridgeFilePathsAsync(resolvedCwd, query, limit = 100) {
36088
36328
 
36089
36329
  // src/files/index/ensure-file-index.ts
36090
36330
  async function ensureFileIndexAsync(cwd) {
36091
- const resolved = path40.resolve(cwd);
36331
+ const resolved = path46.resolve(cwd);
36092
36332
  if (await bridgeFileIndexIsPopulated(resolved)) {
36093
36333
  return { fromCache: true, pathCount: await bridgeFileIndexPathCount(resolved) };
36094
36334
  }
@@ -36132,7 +36372,7 @@ function createFsWatcher(resolved, schedule) {
36132
36372
  }
36133
36373
  }
36134
36374
  function startFileIndexWatcher(cwd = getBridgeRoot()) {
36135
- const resolved = path41.resolve(cwd);
36375
+ const resolved = path47.resolve(cwd);
36136
36376
  void buildFileIndexAsync(resolved).catch((e) => {
36137
36377
  if (e instanceof CliSqliteInterrupted) return;
36138
36378
  console.error("[file-index] Initial index build failed:", e);
@@ -36162,7 +36402,7 @@ function startFileIndexWatcher(cwd = getBridgeRoot()) {
36162
36402
  }
36163
36403
 
36164
36404
  // src/connection/create-bridge-connection.ts
36165
- import * as path52 from "node:path";
36405
+ import * as path60 from "node:path";
36166
36406
 
36167
36407
  // src/dev-servers/manager/dev-server-manager.ts
36168
36408
  import { rm as rm2 } from "node:fs/promises";
@@ -36184,15 +36424,15 @@ function sendDevServerStatus(getWs, serverId, status, options) {
36184
36424
 
36185
36425
  // src/dev-servers/process/terminate-child-process.ts
36186
36426
  async function sigtermAndWaitForExit(proc, graceMs, log2, shortId) {
36187
- const exited = new Promise((resolve26) => {
36188
- proc.once("exit", () => resolve26());
36427
+ const exited = new Promise((resolve33) => {
36428
+ proc.once("exit", () => resolve33());
36189
36429
  });
36190
36430
  log2(`[dev-server] Sending SIGTERM to ${shortId} (pid=${proc.pid ?? "?"}).`);
36191
36431
  try {
36192
36432
  proc.kill("SIGTERM");
36193
36433
  } catch {
36194
36434
  }
36195
- await Promise.race([exited, new Promise((resolve26) => setTimeout(resolve26, graceMs))]);
36435
+ await Promise.race([exited, new Promise((resolve33) => setTimeout(resolve33, graceMs))]);
36196
36436
  }
36197
36437
  function forceKillChild(proc, log2, shortId, graceMs) {
36198
36438
  log2(
@@ -36472,10 +36712,10 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
36472
36712
  import { spawn as spawn8 } from "node:child_process";
36473
36713
  import fs33 from "node:fs";
36474
36714
  import { tmpdir } from "node:os";
36475
- import path42 from "node:path";
36715
+ import path48 from "node:path";
36476
36716
  function trySpawnMergedLogFile(command, env, cwd, signal) {
36477
- const tmpRoot = fs33.mkdtempSync(path42.join(tmpdir(), "ba-devsrv-log-"));
36478
- const logPath = path42.join(tmpRoot, "combined.log");
36717
+ const tmpRoot = fs33.mkdtempSync(path48.join(tmpdir(), "ba-devsrv-log-"));
36718
+ const logPath = path48.join(tmpRoot, "combined.log");
36479
36719
  let logFd;
36480
36720
  try {
36481
36721
  logFd = fs33.openSync(logPath, "a");
@@ -36519,15 +36759,15 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
36519
36759
  import { spawn as spawn9 } from "node:child_process";
36520
36760
  import fs34 from "node:fs";
36521
36761
  import { tmpdir as tmpdir2 } from "node:os";
36522
- import path43 from "node:path";
36762
+ import path49 from "node:path";
36523
36763
  function shSingleQuote(s) {
36524
36764
  return `'${s.replace(/'/g, `'\\''`)}'`;
36525
36765
  }
36526
36766
  function trySpawnShellScriptLogRedirectUnix(command, env, cwd, signal) {
36527
- const tmpRoot = fs34.mkdtempSync(path43.join(tmpdir2(), "ba-devsrv-sh-"));
36528
- const logPath = path43.join(tmpRoot, "combined.log");
36529
- const innerPath = path43.join(tmpRoot, "_cmd.sh");
36530
- const runnerPath = path43.join(tmpRoot, "_run.sh");
36767
+ const tmpRoot = fs34.mkdtempSync(path49.join(tmpdir2(), "ba-devsrv-sh-"));
36768
+ const logPath = path49.join(tmpRoot, "combined.log");
36769
+ const innerPath = path49.join(tmpRoot, "_cmd.sh");
36770
+ const runnerPath = path49.join(tmpRoot, "_run.sh");
36531
36771
  try {
36532
36772
  fs34.writeFileSync(innerPath, `#!/bin/sh
36533
36773
  ${command}
@@ -36558,9 +36798,9 @@ cd ${shSingleQuote(cwd)}
36558
36798
  }
36559
36799
  }
36560
36800
  function trySpawnShellScriptLogRedirectWin(command, env, cwd, signal) {
36561
- const tmpRoot = fs34.mkdtempSync(path43.join(tmpdir2(), "ba-devsrv-sh-"));
36562
- const logPath = path43.join(tmpRoot, "combined.log");
36563
- const runnerPath = path43.join(tmpRoot, "_run.bat");
36801
+ const tmpRoot = fs34.mkdtempSync(path49.join(tmpdir2(), "ba-devsrv-sh-"));
36802
+ const logPath = path49.join(tmpRoot, "combined.log");
36803
+ const runnerPath = path49.join(tmpRoot, "_run.bat");
36564
36804
  const q = (p) => `"${p.replace(/"/g, '""')}"`;
36565
36805
  const com = process.env.ComSpec || "cmd.exe";
36566
36806
  try {
@@ -37332,15 +37572,38 @@ function createOnBridgeIdentified(opts) {
37332
37572
  };
37333
37573
  }
37334
37574
 
37335
- // src/skills/discover-local-agent-skills.ts
37336
- import fs35 from "node:fs";
37337
- import path44 from "node:path";
37575
+ // src/skills/discovery/skill-discovery-roots.ts
37338
37576
  var SKILL_DISCOVERY_ROOTS = [".agents/skills", ".claude/skills", ".cursor/skills", "skills"];
37339
- function discoverLocalSkills(cwd) {
37577
+ async function yieldSkillDiscoveryWork(entryCount) {
37578
+ if (entryCount > 0 && entryCount % INDEX_WORK_YIELD_EVERY === 0) {
37579
+ await yieldToEventLoop();
37580
+ }
37581
+ }
37582
+
37583
+ // src/skills/discovery/discover-local-skills.ts
37584
+ import fs35 from "node:fs";
37585
+ import path50 from "node:path";
37586
+ function collectSkillFromDir(rel, base, name, seenKeys, out) {
37587
+ const dir = path50.join(base, name);
37588
+ try {
37589
+ if (!fs35.statSync(dir).isDirectory()) return;
37590
+ } catch {
37591
+ return;
37592
+ }
37593
+ const skillMd = path50.join(dir, "SKILL.md");
37594
+ if (!fs35.existsSync(skillMd)) return;
37595
+ const key = `${rel}/${name}`;
37596
+ if (seenKeys.has(key)) return;
37597
+ seenKeys.add(key);
37598
+ out.push({ skillKey: name, path: `${rel}/${name}`.replace(/\\/g, "/") });
37599
+ }
37600
+ async function discoverLocalSkillsAsync(cwd) {
37340
37601
  const out = [];
37341
37602
  const seenKeys = /* @__PURE__ */ new Set();
37603
+ let work = 0;
37342
37604
  for (const rel of SKILL_DISCOVERY_ROOTS) {
37343
- const base = path44.join(cwd, rel);
37605
+ await yieldSkillDiscoveryWork(++work);
37606
+ const base = path50.join(cwd, rel);
37344
37607
  if (!fs35.existsSync(base) || !fs35.statSync(base).isDirectory()) continue;
37345
37608
  let entries = [];
37346
37609
  try {
@@ -37348,45 +37611,45 @@ function discoverLocalSkills(cwd) {
37348
37611
  } catch {
37349
37612
  continue;
37350
37613
  }
37351
- for (const name of entries) {
37352
- const dir = path44.join(base, name);
37353
- try {
37354
- if (!fs35.statSync(dir).isDirectory()) continue;
37355
- } catch {
37356
- continue;
37357
- }
37358
- const skillMd = path44.join(dir, "SKILL.md");
37359
- if (!fs35.existsSync(skillMd)) continue;
37360
- const key = `${rel}/${name}`;
37361
- if (seenKeys.has(key)) continue;
37362
- seenKeys.add(key);
37363
- out.push({ skillKey: name, path: `${rel}/${name}`.replace(/\\/g, "/") });
37614
+ for (let i = 0; i < entries.length; i++) {
37615
+ await yieldSkillDiscoveryWork(++work);
37616
+ collectSkillFromDir(rel, base, entries[i], seenKeys, out);
37364
37617
  }
37365
37618
  }
37366
37619
  return out;
37367
37620
  }
37368
- function discoverSkillLayoutRoots(cwd) {
37621
+
37622
+ // src/skills/discovery/discover-skill-layout-roots.ts
37623
+ import fs36 from "node:fs";
37624
+ import path51 from "node:path";
37625
+ function collectLayoutSkill(rel, base, name, skills2) {
37626
+ const dir = path51.join(base, name);
37627
+ try {
37628
+ if (!fs36.statSync(dir).isDirectory()) return;
37629
+ } catch {
37630
+ return;
37631
+ }
37632
+ if (!fs36.existsSync(path51.join(dir, "SKILL.md"))) return;
37633
+ const relPath = `${rel}/${name}`.replace(/\\/g, "/");
37634
+ skills2.push({ name, relPath });
37635
+ }
37636
+ async function discoverSkillLayoutRootsAsync(cwd) {
37369
37637
  const roots = [];
37638
+ let work = 0;
37370
37639
  for (const rel of SKILL_DISCOVERY_ROOTS) {
37371
- const base = path44.join(cwd, rel);
37372
- if (!fs35.existsSync(base) || !fs35.statSync(base).isDirectory()) continue;
37640
+ await yieldSkillDiscoveryWork(++work);
37641
+ const base = path51.join(cwd, rel);
37642
+ if (!fs36.existsSync(base) || !fs36.statSync(base).isDirectory()) continue;
37373
37643
  let entries = [];
37374
37644
  try {
37375
- entries = fs35.readdirSync(base);
37645
+ entries = fs36.readdirSync(base);
37376
37646
  } catch {
37377
37647
  continue;
37378
37648
  }
37379
37649
  const skills2 = [];
37380
- for (const name of entries) {
37381
- const dir = path44.join(base, name);
37382
- try {
37383
- if (!fs35.statSync(dir).isDirectory()) continue;
37384
- } catch {
37385
- continue;
37386
- }
37387
- if (!fs35.existsSync(path44.join(dir, "SKILL.md"))) continue;
37388
- const relPath = `${rel}/${name}`.replace(/\\/g, "/");
37389
- skills2.push({ name, relPath });
37650
+ for (let i = 0; i < entries.length; i++) {
37651
+ await yieldSkillDiscoveryWork(++work);
37652
+ collectLayoutSkill(rel, base, entries[i], skills2);
37390
37653
  }
37391
37654
  if (skills2.length > 0) {
37392
37655
  roots.push({ path: rel.replace(/\\/g, "/"), skills: skills2 });
@@ -37428,16 +37691,18 @@ async function detectLocalAgentTypes() {
37428
37691
  function createSendLocalSkillsReport(getWs, logFn) {
37429
37692
  return () => {
37430
37693
  setImmediate(() => {
37431
- try {
37432
- const socket = getWs();
37433
- if (!socket || socket.readyState !== wrapper_default.OPEN) return;
37434
- const skills2 = discoverLocalSkills(getBridgeRoot());
37435
- sendWsMessage(socket, { type: "local_skills", skills: skills2 });
37436
- } catch (e) {
37437
- logFn(
37438
- `[Bridge service] Local skills report failed: ${e instanceof Error ? e.message : String(e)}`
37439
- );
37440
- }
37694
+ void (async () => {
37695
+ try {
37696
+ const socket = getWs();
37697
+ if (!socket || socket.readyState !== wrapper_default.OPEN) return;
37698
+ const skills2 = await discoverLocalSkillsAsync(getBridgeRoot());
37699
+ sendWsMessage(socket, { type: "local_skills", skills: skills2 });
37700
+ } catch (e) {
37701
+ logFn(
37702
+ `[Bridge service] Local skills report failed: ${e instanceof Error ? e.message : String(e)}`
37703
+ );
37704
+ }
37705
+ })();
37441
37706
  });
37442
37707
  };
37443
37708
  }
@@ -37740,7 +38005,7 @@ function syncRunningTurnQueueKeys(turns, queueKey) {
37740
38005
  }
37741
38006
 
37742
38007
  // src/prompt-turn-queue/runner/run-local-revert-before-queued-prompt.ts
37743
- import fs36 from "node:fs";
38008
+ import fs37 from "node:fs";
37744
38009
  async function runLocalRevertBeforeQueuedPrompt(next, deps) {
37745
38010
  if (next.serverState !== "requeued_with_revert") return true;
37746
38011
  const sid = next.sessionId;
@@ -37749,7 +38014,7 @@ async function runLocalRevertBeforeQueuedPrompt(next, deps) {
37749
38014
  const agentBase = deps.sessionWorktreeManager.getSessionWorktreeRootForSession(sid) ?? getBridgeRoot();
37750
38015
  const file2 = snapshotFilePath(agentBase, tid);
37751
38016
  try {
37752
- await fs36.promises.access(file2, fs36.constants.F_OK);
38017
+ await fs37.promises.access(file2, fs37.constants.F_OK);
37753
38018
  } catch {
37754
38019
  deps.log(
37755
38020
  `[Queue] requeued_with_revert: no pre-turn snapshot for ${tid.slice(0, 8)}\u2026; continuing without revert.`
@@ -38037,9 +38302,9 @@ function parseChangeSummarySnapshots(raw) {
38037
38302
  for (const item of raw) {
38038
38303
  if (!item || typeof item !== "object") continue;
38039
38304
  const o = item;
38040
- const path53 = typeof o.path === "string" && o.path.trim() !== "" ? o.path.trim() : "";
38041
- if (!path53) continue;
38042
- const row = { path: path53 };
38305
+ const path61 = typeof o.path === "string" && o.path.trim() !== "" ? o.path.trim() : "";
38306
+ if (!path61) continue;
38307
+ const row = { path: path61 };
38043
38308
  if (typeof o.patchContent === "string") row.patchContent = o.patchContent;
38044
38309
  if (typeof o.oldText === "string") row.oldText = o.oldText;
38045
38310
  if (typeof o.newText === "string") row.newText = o.newText;
@@ -38252,15 +38517,26 @@ var handleSkillCallMessage = (msg, { getWs, log: log2 }) => {
38252
38517
  );
38253
38518
  };
38254
38519
 
38520
+ // src/files/resolve-file-browser-session-parent.ts
38521
+ async function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
38522
+ const sid = sessionId?.trim();
38523
+ if (sid) {
38524
+ await sessionWorktreeManager.ensureRepoCheckoutPathsForSessionAsync(sid);
38525
+ const worktreeRoot = sessionWorktreeManager.getSessionWorktreeRootForSession(sid);
38526
+ if (worktreeRoot) return worktreeRoot;
38527
+ }
38528
+ return getBridgeRoot();
38529
+ }
38530
+
38255
38531
  // src/files/list-dir/index.ts
38256
- import fs38 from "node:fs";
38532
+ import fs39 from "node:fs";
38257
38533
 
38258
38534
  // src/files/ensure-under-cwd.ts
38259
- import path45 from "node:path";
38535
+ import path52 from "node:path";
38260
38536
  function ensureUnderCwd(relativePath, cwd = getBridgeRoot()) {
38261
- const normalized = path45.normalize(relativePath).replace(/^(\.\/)+/, "");
38262
- const resolved = path45.resolve(cwd, normalized);
38263
- if (!resolved.startsWith(cwd + path45.sep) && resolved !== cwd) {
38537
+ const normalized = path52.normalize(relativePath).replace(/^(\.\/)+/, "");
38538
+ const resolved = path52.resolve(cwd, normalized);
38539
+ if (!resolved.startsWith(cwd + path52.sep) && resolved !== cwd) {
38264
38540
  return null;
38265
38541
  }
38266
38542
  return resolved;
@@ -38270,15 +38546,15 @@ function ensureUnderCwd(relativePath, cwd = getBridgeRoot()) {
38270
38546
  var LIST_DIR_YIELD_EVERY = 256;
38271
38547
 
38272
38548
  // src/files/list-dir/map-dir-entry.ts
38273
- import path46 from "node:path";
38274
- import fs37 from "node:fs";
38549
+ import path53 from "node:path";
38550
+ import fs38 from "node:fs";
38275
38551
  async function mapDirEntry(d, relativePath, resolved) {
38276
- const entryPath = path46.join(relativePath || ".", d.name).replace(/\\/g, "/");
38277
- const fullPath = path46.join(resolved, d.name);
38552
+ const entryPath = path53.join(relativePath || ".", d.name).replace(/\\/g, "/");
38553
+ const fullPath = path53.join(resolved, d.name);
38278
38554
  let isDir = d.isDirectory();
38279
38555
  if (d.isSymbolicLink()) {
38280
38556
  try {
38281
- const targetStat = await fs37.promises.stat(fullPath);
38557
+ const targetStat = await fs38.promises.stat(fullPath);
38282
38558
  isDir = targetStat.isDirectory();
38283
38559
  } catch {
38284
38560
  isDir = false;
@@ -38308,7 +38584,7 @@ async function listDirAsync(relativePath, sessionParentPath = getBridgeRoot()) {
38308
38584
  return { error: "Path is outside working directory" };
38309
38585
  }
38310
38586
  try {
38311
- const names = await fs38.promises.readdir(resolved, { withFileTypes: true });
38587
+ const names = await fs39.promises.readdir(resolved, { withFileTypes: true });
38312
38588
  const entries = [];
38313
38589
  for (let i = 0; i < names.length; i++) {
38314
38590
  if (i > 0 && i % LIST_DIR_YIELD_EVERY === 0) {
@@ -38323,23 +38599,460 @@ async function listDirAsync(relativePath, sessionParentPath = getBridgeRoot()) {
38323
38599
  }
38324
38600
  }
38325
38601
 
38602
+ // src/files/handle-file-browser-search.ts
38603
+ import path54 from "node:path";
38604
+ var SEARCH_LIMIT = 100;
38605
+ function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
38606
+ void (async () => {
38607
+ await yieldToEventLoop();
38608
+ const q = typeof msg.q === "string" ? msg.q : "";
38609
+ const sessionParentPath = path54.resolve(
38610
+ sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
38611
+ );
38612
+ if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
38613
+ const payload2 = {
38614
+ type: "file_browser_search_response",
38615
+ id: msg.id,
38616
+ paths: [],
38617
+ indexReady: false
38618
+ };
38619
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload2, ["paths"]) : payload2);
38620
+ return;
38621
+ }
38622
+ const results = await searchBridgeFilePathsAsync(sessionParentPath, q, SEARCH_LIMIT);
38623
+ const payload = {
38624
+ type: "file_browser_search_response",
38625
+ id: msg.id,
38626
+ paths: results,
38627
+ indexReady: true
38628
+ };
38629
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["paths"]) : payload);
38630
+ })();
38631
+ }
38632
+ function triggerFileIndexBuild(sessionParentPath = getBridgeRoot()) {
38633
+ setImmediate(() => {
38634
+ void ensureFileIndexAsync(sessionParentPath).catch((e) => {
38635
+ console.error("[file-index] Background build failed:", e);
38636
+ });
38637
+ });
38638
+ }
38639
+
38640
+ // src/git/tree/resolve-repo-abs-path.ts
38641
+ import * as path55 from "node:path";
38642
+ function resolveRepoAbsPathFromBridge(repoRelPath, bridgeRoot = getBridgeRoot()) {
38643
+ const bridgeResolved = path55.resolve(bridgeRoot);
38644
+ const rel = repoRelPath.trim() === "." ? "" : repoRelPath.trim().replace(/\\/g, "/");
38645
+ const repoPath = rel === "" ? bridgeResolved : path55.join(bridgeResolved, rel);
38646
+ const resolved = path55.resolve(repoPath);
38647
+ if (!resolved.startsWith(bridgeResolved + path55.sep) && resolved !== bridgeResolved) {
38648
+ return null;
38649
+ }
38650
+ return resolved;
38651
+ }
38652
+
38653
+ // src/git/tree/resolve-branch-commit.ts
38654
+ async function resolveBranchCommit(repoAbsPath, branch) {
38655
+ const branchRef = branch.trim();
38656
+ if (!branchRef) return null;
38657
+ try {
38658
+ return (await cliSimpleGit(repoAbsPath).revparse([`${branchRef}^{commit}`])).trim();
38659
+ } catch {
38660
+ return null;
38661
+ }
38662
+ }
38663
+
38664
+ // src/git/tree/list-git-tree-dir.ts
38665
+ function parseLsTreeLine(line) {
38666
+ const tab = line.indexOf(" ");
38667
+ if (tab < 0) return null;
38668
+ const meta = line.slice(0, tab).trim().split(/\s+/);
38669
+ if (meta.length < 3) return null;
38670
+ const mode = meta[0];
38671
+ const objectType2 = meta[1];
38672
+ let name = line.slice(tab + 1);
38673
+ if (name.startsWith('"') && name.endsWith('"')) {
38674
+ name = name.slice(1, -1).replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
38675
+ }
38676
+ return { mode, objectType: objectType2, name };
38677
+ }
38678
+ function childPath(relativePath, name) {
38679
+ const base = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
38680
+ if (!base || base === ".") return name;
38681
+ return `${base}/${name}`.replace(/\\/g, "/");
38682
+ }
38683
+ function lsTreeArgs(commit, relativePath) {
38684
+ const rel = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
38685
+ if (!rel || rel === ".") return ["ls-tree", commit];
38686
+ return ["ls-tree", `${commit}:${rel}`];
38687
+ }
38688
+ async function listGitTreeDirAsync(repoAbsPath, branch, relativePath) {
38689
+ await yieldToEventLoop();
38690
+ const commit = await resolveBranchCommit(repoAbsPath, branch);
38691
+ if (!commit) return { error: "Branch not found" };
38692
+ try {
38693
+ const raw = await cliSimpleGit(repoAbsPath).raw(lsTreeArgs(commit, relativePath));
38694
+ await yieldToEventLoop();
38695
+ const lines = String(raw ?? "").split(/\r?\n/).filter((line) => line.trim().length > 0);
38696
+ const entries = [];
38697
+ for (let i = 0; i < lines.length; i++) {
38698
+ if (i > 0 && i % LIST_DIR_YIELD_EVERY === 0) {
38699
+ await yieldToEventLoop();
38700
+ }
38701
+ const parsed = parseLsTreeLine(lines[i]);
38702
+ if (!parsed) continue;
38703
+ const { mode, objectType: objectType2, name } = parsed;
38704
+ if (!name || name === "." || name === "..") continue;
38705
+ const isSymlink = mode === "120000";
38706
+ const isDir = objectType2 === "tree";
38707
+ entries.push({
38708
+ name,
38709
+ path: childPath(relativePath, name),
38710
+ isDir,
38711
+ ...isSymlink ? { isSymlink: true } : {}
38712
+ });
38713
+ }
38714
+ if (entries.length >= LIST_DIR_YIELD_EVERY) {
38715
+ await yieldToEventLoop();
38716
+ }
38717
+ return { entries: sortListEntries(entries) };
38718
+ } catch (err) {
38719
+ const message = err instanceof Error ? err.message : String(err);
38720
+ if (/not a tree object|does not exist|bad revision/i.test(message)) {
38721
+ return { error: "Path not found in branch" };
38722
+ }
38723
+ return { error: message };
38724
+ }
38725
+ }
38726
+
38326
38727
  // src/files/read-file/types.ts
38327
38728
  var LINE_CHUNK_SIZE = 64 * 1024;
38328
38729
  var READ_RANGE_YIELD_EVERY_BYTES = 256 * 1024;
38329
38730
 
38731
+ // src/files/read-file/guess-mime-type.ts
38732
+ var MIME_BY_EXT = {
38733
+ png: "image/png",
38734
+ jpg: "image/jpeg",
38735
+ jpeg: "image/jpeg",
38736
+ gif: "image/gif",
38737
+ bmp: "image/bmp",
38738
+ ico: "image/x-icon",
38739
+ webp: "image/webp",
38740
+ avif: "image/avif",
38741
+ svg: "image/svg+xml",
38742
+ pdf: "application/pdf",
38743
+ json: "application/json",
38744
+ html: "text/html",
38745
+ htm: "text/html",
38746
+ css: "text/css",
38747
+ js: "text/javascript",
38748
+ mjs: "text/javascript",
38749
+ ts: "text/typescript",
38750
+ txt: "text/plain",
38751
+ md: "text/markdown",
38752
+ xml: "application/xml",
38753
+ zip: "application/zip",
38754
+ gz: "application/gzip",
38755
+ wasm: "application/wasm"
38756
+ };
38757
+ function guessMimeType(filePath) {
38758
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
38759
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
38760
+ }
38761
+
38762
+ // src/git/tree/file/git-blob-ref.ts
38763
+ async function gitBlobRef(repoAbsPath, branch, relativePath) {
38764
+ const commit = await resolveBranchCommit(repoAbsPath, branch);
38765
+ await yieldToEventLoop();
38766
+ if (!commit) return null;
38767
+ const rel = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
38768
+ if (!rel || rel === ".") return null;
38769
+ return `${commit}:${rel}`;
38770
+ }
38771
+
38772
+ // src/git/tree/file/is-binary-buffer.ts
38773
+ function isBinaryBuffer(buf) {
38774
+ const sample = buf.subarray(0, Math.min(buf.length, 8192));
38775
+ for (let i = 0; i < sample.length; i++) {
38776
+ if (sample[i] === 0) return true;
38777
+ }
38778
+ return false;
38779
+ }
38780
+
38781
+ // src/git/tree/file/read-git-blob-text-async.ts
38782
+ import { StringDecoder } from "node:string_decoder";
38783
+ async function countGitBlobLinesAsync(buf) {
38784
+ if (buf.length === 0) return 1;
38785
+ let lines = 1;
38786
+ let bytesSinceYield = 0;
38787
+ for (let i = 0; i < buf.length; i++) {
38788
+ const b = buf[i];
38789
+ if (b === 10) {
38790
+ lines++;
38791
+ } else if (b === 13) {
38792
+ if (i + 1 < buf.length && buf[i + 1] === 10) {
38793
+ lines++;
38794
+ i++;
38795
+ } else {
38796
+ lines++;
38797
+ }
38798
+ }
38799
+ bytesSinceYield++;
38800
+ if (bytesSinceYield >= READ_RANGE_YIELD_EVERY_BYTES) {
38801
+ await yieldToEventLoop();
38802
+ bytesSinceYield = 0;
38803
+ }
38804
+ }
38805
+ return lines;
38806
+ }
38807
+ async function readGitBlobLineRangeAsync(buf, startLine, endLine, lineOffsetIn, lineChunkSize = LINE_CHUNK_SIZE) {
38808
+ const bufSize = 64 * 1024;
38809
+ const decoder = new StringDecoder("utf8");
38810
+ let currentLine = 0;
38811
+ const resultLines = [];
38812
+ let partial2 = "";
38813
+ let done = false;
38814
+ let skipLine0Chars = typeof lineOffsetIn === "number" ? lineOffsetIn : 0;
38815
+ let line0CharsReturned = 0;
38816
+ let line0Accum = "";
38817
+ let bytesSinceYield = 0;
38818
+ let position = 0;
38819
+ const finish = async () => {
38820
+ const totalLines = await countGitBlobLinesAsync(buf);
38821
+ return { content: resultLines.join("\n"), totalLines };
38822
+ };
38823
+ while (!done && position < buf.length) {
38824
+ const chunkEnd = Math.min(position + bufSize, buf.length);
38825
+ const bytesRead = chunkEnd - position;
38826
+ position = chunkEnd;
38827
+ bytesSinceYield += bytesRead;
38828
+ if (bytesSinceYield >= READ_RANGE_YIELD_EVERY_BYTES) {
38829
+ await yieldToEventLoop();
38830
+ bytesSinceYield = 0;
38831
+ }
38832
+ const text = partial2 + decoder.write(buf.subarray(position - bytesRead, position));
38833
+ partial2 = "";
38834
+ let lineStart = 0;
38835
+ for (let i = 0; i < text.length; i++) {
38836
+ if (text[i] === "\n") {
38837
+ const lineContent = (() => {
38838
+ let lineEnd = i;
38839
+ if (lineEnd > lineStart && text[lineEnd - 1] === "\r") lineEnd--;
38840
+ return text.slice(lineStart, lineEnd);
38841
+ })();
38842
+ if (currentLine === 0 && (startLine === 0 || lineOffsetIn !== void 0)) {
38843
+ line0Accum += lineContent;
38844
+ const totalLine0 = line0Accum.length;
38845
+ if (skipLine0Chars > 0) {
38846
+ if (totalLine0 <= skipLine0Chars) {
38847
+ skipLine0Chars -= totalLine0;
38848
+ line0Accum = "";
38849
+ currentLine++;
38850
+ lineStart = i + 1;
38851
+ if (currentLine > endLine) {
38852
+ done = true;
38853
+ break;
38854
+ }
38855
+ continue;
38856
+ }
38857
+ const from = skipLine0Chars;
38858
+ const take = Math.min(lineChunkSize, totalLine0 - from);
38859
+ resultLines.push(line0Accum.slice(from, from + take));
38860
+ line0CharsReturned += take;
38861
+ if (from + take < totalLine0) {
38862
+ const totalLines = await countGitBlobLinesAsync(buf);
38863
+ return {
38864
+ content: resultLines.join("\n"),
38865
+ totalLines,
38866
+ lineOffset: lineOffsetIn + line0CharsReturned
38867
+ };
38868
+ }
38869
+ line0Accum = "";
38870
+ skipLine0Chars = 0;
38871
+ line0CharsReturned = 0;
38872
+ } else if (totalLine0 > lineChunkSize) {
38873
+ resultLines.push(line0Accum.slice(0, lineChunkSize));
38874
+ const totalLines = await countGitBlobLinesAsync(buf);
38875
+ return {
38876
+ content: resultLines.join("\n"),
38877
+ totalLines,
38878
+ lineOffset: lineChunkSize
38879
+ };
38880
+ } else {
38881
+ resultLines.push(line0Accum);
38882
+ line0Accum = "";
38883
+ }
38884
+ } else if (currentLine >= startLine && currentLine <= endLine) {
38885
+ resultLines.push(lineContent);
38886
+ }
38887
+ currentLine++;
38888
+ lineStart = i + 1;
38889
+ if (currentLine > endLine) {
38890
+ done = true;
38891
+ break;
38892
+ }
38893
+ }
38894
+ }
38895
+ if (!done) {
38896
+ const lineContent = text.slice(lineStart);
38897
+ if (currentLine === 0 && (startLine === 0 || lineOffsetIn !== void 0)) {
38898
+ line0Accum += lineContent;
38899
+ const totalLine0 = line0Accum.length;
38900
+ if (skipLine0Chars > 0) {
38901
+ if (totalLine0 <= skipLine0Chars) {
38902
+ skipLine0Chars -= totalLine0;
38903
+ line0Accum = "";
38904
+ } else {
38905
+ const from = skipLine0Chars;
38906
+ const take = Math.min(lineChunkSize, totalLine0 - from);
38907
+ resultLines.push(line0Accum.slice(from, from + take));
38908
+ const totalLines = await countGitBlobLinesAsync(buf);
38909
+ return {
38910
+ content: resultLines.join("\n"),
38911
+ totalLines,
38912
+ lineOffset: (lineOffsetIn ?? 0) + take
38913
+ };
38914
+ }
38915
+ } else if (totalLine0 > lineChunkSize) {
38916
+ resultLines.push(line0Accum.slice(0, lineChunkSize));
38917
+ const totalLines = await countGitBlobLinesAsync(buf);
38918
+ return {
38919
+ content: resultLines.join("\n"),
38920
+ totalLines,
38921
+ lineOffset: lineChunkSize
38922
+ };
38923
+ }
38924
+ }
38925
+ partial2 = text.slice(lineStart);
38926
+ }
38927
+ }
38928
+ if (!done) {
38929
+ const tail = partial2 + decoder.end();
38930
+ if (currentLine === 0 && (startLine === 0 || lineOffsetIn !== void 0) && tail.length > 0) {
38931
+ line0Accum += tail.endsWith("\r") ? tail.slice(0, -1) : tail;
38932
+ const totalLine0 = line0Accum.length;
38933
+ if (skipLine0Chars > 0) {
38934
+ if (totalLine0 <= skipLine0Chars) {
38935
+ return finish();
38936
+ }
38937
+ const from = skipLine0Chars;
38938
+ const take = Math.min(lineChunkSize, totalLine0 - from);
38939
+ resultLines.push(line0Accum.slice(from, from + take));
38940
+ line0CharsReturned += take;
38941
+ if (from + take < totalLine0) {
38942
+ const totalLines = await countGitBlobLinesAsync(buf);
38943
+ return {
38944
+ content: resultLines.join("\n"),
38945
+ totalLines,
38946
+ lineOffset: (lineOffsetIn ?? 0) + line0CharsReturned
38947
+ };
38948
+ }
38949
+ } else if (totalLine0 > lineChunkSize) {
38950
+ resultLines.push(line0Accum.slice(0, lineChunkSize));
38951
+ const totalLines = await countGitBlobLinesAsync(buf);
38952
+ return {
38953
+ content: resultLines.join("\n"),
38954
+ totalLines,
38955
+ lineOffset: lineChunkSize
38956
+ };
38957
+ } else {
38958
+ resultLines.push(line0Accum);
38959
+ }
38960
+ } else if (tail.length > 0 && currentLine >= startLine && currentLine <= endLine) {
38961
+ resultLines.push(tail.endsWith("\r") ? tail.slice(0, -1) : tail);
38962
+ }
38963
+ }
38964
+ return finish();
38965
+ }
38966
+ async function decodeGitBlobUtf8Async(buf) {
38967
+ await yieldToEventLoop();
38968
+ const text = buf.toString("utf8");
38969
+ await yieldToEventLoop();
38970
+ return text;
38971
+ }
38972
+ async function readGitBlobUtf8FullAsync(buf) {
38973
+ const text = await decodeGitBlobUtf8Async(buf);
38974
+ const totalLines = await countGitBlobLinesAsync(buf);
38975
+ return { content: text, totalLines };
38976
+ }
38977
+
38978
+ // src/git/tree/file/index.ts
38979
+ async function readGitTreeFileAsync(repoAbsPath, branch, relativePath, startLine, endLine, lineOffset, lineChunkSize = LINE_CHUNK_SIZE, encoding = "utf8") {
38980
+ await yieldToEventLoop();
38981
+ const branchRef = branch.trim();
38982
+ if (!branchRef) return { error: "branch required" };
38983
+ const rel = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
38984
+ if (!rel || rel === ".") return { error: "file path required" };
38985
+ const blobRef = await gitBlobRef(repoAbsPath, branchRef, rel);
38986
+ if (!blobRef) return { error: "Branch not found" };
38987
+ const hasRange = typeof startLine === "number" && typeof endLine === "number";
38988
+ try {
38989
+ if (encoding === "base64") {
38990
+ if (hasRange) return { error: "base64 encoding requires a full file read (no line range)" };
38991
+ const buf2 = await cliSimpleGit(repoAbsPath).showBuffer([blobRef]);
38992
+ await yieldToEventLoop();
38993
+ return {
38994
+ content: buf2.toString("base64"),
38995
+ size: buf2.length,
38996
+ mimeType: guessMimeType(rel)
38997
+ };
38998
+ }
38999
+ const buf = await cliSimpleGit(repoAbsPath).showBuffer([blobRef]);
39000
+ await yieldToEventLoop();
39001
+ const size = buf.length;
39002
+ if (isBinaryBuffer(buf)) {
39003
+ return { error: "Binary file cannot be displayed as text" };
39004
+ }
39005
+ if (hasRange) {
39006
+ const ranged = await readGitBlobLineRangeAsync(buf, startLine, endLine, lineOffset, lineChunkSize);
39007
+ return {
39008
+ content: ranged.content,
39009
+ totalLines: ranged.totalLines,
39010
+ size,
39011
+ ...ranged.lineOffset != null ? { lineOffset: ranged.lineOffset } : {}
39012
+ };
39013
+ }
39014
+ const full = await readGitBlobUtf8FullAsync(buf);
39015
+ return { content: full.content, totalLines: full.totalLines, size, mimeType: guessMimeType(rel) };
39016
+ } catch (err) {
39017
+ const message = err instanceof Error ? err.message : String(err);
39018
+ if (/does not exist|bad revision|exists on disk, but not in/i.test(message)) {
39019
+ return { error: "File not found in branch" };
39020
+ }
39021
+ return { error: message };
39022
+ }
39023
+ }
39024
+
39025
+ // src/files/browser/send-file-browser-message.ts
39026
+ function sendFileBrowserMessage(socket, e2ee, payload) {
39027
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["entries", "content", "totalLines", "size", "lineOffset"]) : payload);
39028
+ }
39029
+
39030
+ // src/files/browser/handle-file-browser-list.ts
39031
+ async function handleFileBrowserList(socket, e2ee, id, reqPath, sessionParentPath, gitScope) {
39032
+ const result = gitScope ? await listGitTreeDirAsync(gitScope.repoAbsPath, gitScope.branch, reqPath) : await listDirAsync(reqPath, sessionParentPath);
39033
+ if ("error" in result) {
39034
+ sendWsMessage(socket, { type: "file_browser_response", id, error: result.error });
39035
+ return;
39036
+ }
39037
+ sendFileBrowserMessage(socket, e2ee, { type: "file_browser_response", id, entries: result.entries });
39038
+ if (!gitScope && (reqPath === "." || reqPath === "")) {
39039
+ triggerFileIndexBuild(sessionParentPath);
39040
+ }
39041
+ }
39042
+
38330
39043
  // src/files/read-file/resolve-file-path.ts
38331
- import fs39 from "node:fs";
39044
+ import fs40 from "node:fs";
38332
39045
  async function resolveFilePathAsync(relativePath, sessionParentPath = getBridgeRoot()) {
38333
39046
  const resolved = ensureUnderCwd(relativePath, sessionParentPath);
38334
39047
  if (!resolved) return { error: "Path is outside working directory" };
38335
39048
  let real;
38336
39049
  try {
38337
- real = await fs39.promises.realpath(resolved);
39050
+ real = await fs40.promises.realpath(resolved);
38338
39051
  } catch {
38339
39052
  real = resolved;
38340
39053
  }
38341
39054
  try {
38342
- const stat2 = await fs39.promises.stat(real);
39055
+ const stat2 = await fs40.promises.stat(real);
38343
39056
  if (!stat2.isFile()) return { error: "Not a file" };
38344
39057
  return real;
38345
39058
  } catch (err) {
@@ -38348,14 +39061,14 @@ async function resolveFilePathAsync(relativePath, sessionParentPath = getBridgeR
38348
39061
  }
38349
39062
 
38350
39063
  // src/files/read-file/read-file-range-async.ts
38351
- import fs40 from "node:fs";
38352
- import { StringDecoder } from "node:string_decoder";
39064
+ import fs41 from "node:fs";
39065
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
38353
39066
  async function readFileRangeAsync(filePath, startLine, endLine, lineOffsetIn, lineChunkSize = LINE_CHUNK_SIZE) {
38354
- const fileSize = (await fs40.promises.stat(filePath)).size;
38355
- const fd = await fs40.promises.open(filePath, "r");
39067
+ const fileSize = (await fs41.promises.stat(filePath)).size;
39068
+ const fd = await fs41.promises.open(filePath, "r");
38356
39069
  const bufSize = 64 * 1024;
38357
39070
  const buf = Buffer.alloc(bufSize);
38358
- const decoder = new StringDecoder("utf8");
39071
+ const decoder = new StringDecoder2("utf8");
38359
39072
  let currentLine = 0;
38360
39073
  const resultLines = [];
38361
39074
  let partial2 = "";
@@ -38514,11 +39227,11 @@ async function readFileRangeAsync(filePath, startLine, endLine, lineOffsetIn, li
38514
39227
  }
38515
39228
 
38516
39229
  // src/files/read-file/read-file-buffer-full-async.ts
38517
- import fs41 from "node:fs";
39230
+ import fs42 from "node:fs";
38518
39231
  var READ_CHUNK_BYTES = 256 * 1024;
38519
39232
  async function readFileBufferFullAsync(filePath) {
38520
- const stat2 = await fs41.promises.stat(filePath);
38521
- const fd = await fs41.promises.open(filePath, "r");
39233
+ const stat2 = await fs42.promises.stat(filePath);
39234
+ const fd = await fs42.promises.open(filePath, "r");
38522
39235
  const chunks = [];
38523
39236
  let position = 0;
38524
39237
  let bytesSinceYield = 0;
@@ -38549,37 +39262,6 @@ async function readFileFullAsync(filePath) {
38549
39262
  return { content: raw, totalLines: lines.length, size };
38550
39263
  }
38551
39264
 
38552
- // src/files/read-file/guess-mime-type.ts
38553
- var MIME_BY_EXT = {
38554
- png: "image/png",
38555
- jpg: "image/jpeg",
38556
- jpeg: "image/jpeg",
38557
- gif: "image/gif",
38558
- bmp: "image/bmp",
38559
- ico: "image/x-icon",
38560
- webp: "image/webp",
38561
- avif: "image/avif",
38562
- svg: "image/svg+xml",
38563
- pdf: "application/pdf",
38564
- json: "application/json",
38565
- html: "text/html",
38566
- htm: "text/html",
38567
- css: "text/css",
38568
- js: "text/javascript",
38569
- mjs: "text/javascript",
38570
- ts: "text/typescript",
38571
- txt: "text/plain",
38572
- md: "text/markdown",
38573
- xml: "application/xml",
38574
- zip: "application/zip",
38575
- gz: "application/gzip",
38576
- wasm: "application/wasm"
38577
- };
38578
- function guessMimeType(filePath) {
38579
- const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
38580
- return MIME_BY_EXT[ext] ?? "application/octet-stream";
38581
- }
38582
-
38583
39265
  // src/files/read-file/read-file-binary-full-async.ts
38584
39266
  async function readFileBinaryFullAsync(filePath) {
38585
39267
  const { buffer, size } = await readFileBufferFullAsync(filePath);
@@ -38613,104 +39295,72 @@ async function readFileAsync(relativePath, startLine, endLine, lineOffset, lineC
38613
39295
  }
38614
39296
  }
38615
39297
 
38616
- // src/files/resolve-file-browser-session-parent.ts
38617
- async function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
38618
- const sid = sessionId?.trim();
38619
- if (sid) {
38620
- await sessionWorktreeManager.ensureRepoCheckoutPathsForSessionAsync(sid);
38621
- const worktreeRoot = sessionWorktreeManager.getSessionWorktreeRootForSession(sid);
38622
- if (worktreeRoot) return worktreeRoot;
39298
+ // src/files/browser/handle-file-browser-read.ts
39299
+ async function handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope) {
39300
+ const startLine = typeof msg.startLine === "number" ? msg.startLine : void 0;
39301
+ const endLine = typeof msg.endLine === "number" ? msg.endLine : void 0;
39302
+ const lineOffset = typeof msg.lineOffset === "number" ? msg.lineOffset : void 0;
39303
+ const lineChunkSize = typeof msg.lineChunkSize === "number" ? msg.lineChunkSize : void 0;
39304
+ const encoding = msg.encoding === "base64" ? "base64" : "utf8";
39305
+ const result = gitScope ? await readGitTreeFileAsync(
39306
+ gitScope.repoAbsPath,
39307
+ gitScope.branch,
39308
+ reqPath,
39309
+ startLine,
39310
+ endLine,
39311
+ lineOffset,
39312
+ lineChunkSize,
39313
+ encoding
39314
+ ) : await readFileAsync(
39315
+ reqPath,
39316
+ startLine,
39317
+ endLine,
39318
+ lineOffset,
39319
+ lineChunkSize,
39320
+ sessionParentPath,
39321
+ encoding
39322
+ );
39323
+ if ("error" in result) {
39324
+ sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
39325
+ return;
38623
39326
  }
38624
- return getBridgeRoot();
39327
+ const payload = {
39328
+ type: "file_browser_response",
39329
+ id: msg.id,
39330
+ content: result.content,
39331
+ totalLines: result.totalLines,
39332
+ size: result.size
39333
+ };
39334
+ if (result.lineOffset != null) payload.lineOffset = result.lineOffset;
39335
+ if (result.mimeType != null) payload.mimeType = result.mimeType;
39336
+ if (result.resolvedPath != null) payload.resolvedPath = result.resolvedPath;
39337
+ sendFileBrowserMessage(socket, e2ee, payload);
38625
39338
  }
38626
39339
 
38627
- // src/files/handle-file-browser-search.ts
38628
- import path47 from "node:path";
38629
- var SEARCH_LIMIT = 100;
38630
- function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
38631
- void (async () => {
38632
- await yieldToEventLoop();
38633
- const q = typeof msg.q === "string" ? msg.q : "";
38634
- const sessionParentPath = path47.resolve(
38635
- sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
38636
- );
38637
- if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
38638
- const payload2 = {
38639
- type: "file_browser_search_response",
38640
- id: msg.id,
38641
- paths: [],
38642
- indexReady: false
38643
- };
38644
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload2, ["paths"]) : payload2);
38645
- return;
38646
- }
38647
- const results = await searchBridgeFilePathsAsync(sessionParentPath, q, SEARCH_LIMIT);
38648
- const payload = {
38649
- type: "file_browser_search_response",
38650
- id: msg.id,
38651
- paths: results,
38652
- indexReady: true
38653
- };
38654
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["paths"]) : payload);
38655
- })();
38656
- }
38657
- function triggerFileIndexBuild(sessionParentPath = getBridgeRoot()) {
38658
- setImmediate(() => {
38659
- void ensureFileIndexAsync(sessionParentPath).catch((e) => {
38660
- console.error("[file-index] Background build failed:", e);
38661
- });
38662
- });
39340
+ // src/files/browser/resolve-git-branch-scope.ts
39341
+ function resolveGitBranchScope(msg) {
39342
+ const gitBranchScope = msg.source === "git_branch" && typeof msg.repoRelPath === "string" && typeof msg.branch === "string" && msg.branch.trim().length > 0;
39343
+ if (!gitBranchScope) return null;
39344
+ const repoAbsPath = resolveRepoAbsPathFromBridge(msg.repoRelPath);
39345
+ if (!repoAbsPath) return null;
39346
+ return { repoAbsPath, branch: msg.branch.trim() };
38663
39347
  }
38664
39348
 
38665
- // src/files/handle-file-browser-request.ts
38666
- function sendFileBrowserMessage(socket, e2ee, payload) {
38667
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["entries", "content", "totalLines", "size", "lineOffset"]) : payload);
38668
- }
39349
+ // src/files/browser/index.ts
38669
39350
  function handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager) {
38670
39351
  void (async () => {
38671
39352
  const reqPath = msg.path.replace(/^\/+/, "") || ".";
38672
39353
  const op = msg.op === "read" ? "read" : "list";
38673
39354
  const sessionParentPath = sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : void 0;
39355
+ const gitScope = resolveGitBranchScope(msg);
39356
+ if (msg.source === "git_branch" && typeof msg.repoRelPath === "string" && typeof msg.branch === "string" && msg.branch.trim().length > 0 && !gitScope) {
39357
+ sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: "Invalid repository path" });
39358
+ return;
39359
+ }
38674
39360
  if (op === "list") {
38675
- const result = await listDirAsync(reqPath, sessionParentPath);
38676
- if ("error" in result) {
38677
- sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
38678
- } else {
38679
- sendFileBrowserMessage(socket, e2ee, { type: "file_browser_response", id: msg.id, entries: result.entries });
38680
- if (reqPath === "." || reqPath === "") {
38681
- triggerFileIndexBuild(sessionParentPath);
38682
- }
38683
- }
39361
+ await handleFileBrowserList(socket, e2ee, msg.id, reqPath, sessionParentPath, gitScope);
38684
39362
  } else {
38685
- const startLine = typeof msg.startLine === "number" ? msg.startLine : void 0;
38686
- const endLine = typeof msg.endLine === "number" ? msg.endLine : void 0;
38687
- const lineOffset = typeof msg.lineOffset === "number" ? msg.lineOffset : void 0;
38688
- const lineChunkSize = typeof msg.lineChunkSize === "number" ? msg.lineChunkSize : void 0;
38689
- const encoding = msg.encoding === "base64" ? "base64" : "utf8";
38690
- const result = await readFileAsync(
38691
- reqPath,
38692
- startLine,
38693
- endLine,
38694
- lineOffset,
38695
- lineChunkSize,
38696
- sessionParentPath,
38697
- encoding
38698
- );
38699
- if ("error" in result) {
38700
- sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
38701
- } else {
38702
- const payload = {
38703
- type: "file_browser_response",
38704
- id: msg.id,
38705
- content: result.content,
38706
- totalLines: result.totalLines,
38707
- size: result.size
38708
- };
38709
- if (result.lineOffset != null) payload.lineOffset = result.lineOffset;
38710
- if (result.mimeType != null) payload.mimeType = result.mimeType;
38711
- if (result.resolvedPath != null) payload.resolvedPath = result.resolvedPath;
38712
- sendFileBrowserMessage(socket, e2ee, payload);
38713
- }
39363
+ await handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope);
38714
39364
  }
38715
39365
  })();
38716
39366
  }
@@ -38720,12 +39370,7 @@ function handleFileBrowserRequestMessage(msg, { getWs, e2ee, sessionWorktreeMana
38720
39370
  if (typeof msg.id !== "string" || typeof msg.path !== "string") return;
38721
39371
  const socket = getWs();
38722
39372
  if (!socket) return;
38723
- handleFileBrowserRequest(
38724
- msg,
38725
- socket,
38726
- e2ee,
38727
- sessionWorktreeManager
38728
- );
39373
+ handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager);
38729
39374
  }
38730
39375
  function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
38731
39376
  if (typeof msg.id !== "string") return;
@@ -38741,37 +39386,56 @@ function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManag
38741
39386
 
38742
39387
  // src/routing/handlers/skill-layout-request.ts
38743
39388
  function handleSkillLayoutRequest(msg, deps) {
38744
- const socket = deps.getWs();
38745
39389
  const id = typeof msg.id === "string" ? msg.id : "";
38746
- const roots = discoverSkillLayoutRoots(getBridgeRoot());
38747
- if (socket) {
38748
- sendWsMessage(socket, { type: "skill_layout_response", id, roots });
38749
- }
39390
+ void (async () => {
39391
+ const socket = deps.getWs();
39392
+ const roots = await discoverSkillLayoutRootsAsync(getBridgeRoot());
39393
+ if (socket) {
39394
+ sendWsMessage(socket, { type: "skill_layout_response", id, roots });
39395
+ }
39396
+ })();
38750
39397
  }
38751
39398
 
38752
- // src/skills/install-remote-skills.ts
38753
- import fs42 from "node:fs";
38754
- import path48 from "node:path";
38755
- function installRemoteSkills(cwd, targetDir, items) {
39399
+ // src/skills/install/is-valid-install-item.ts
39400
+ function isValidRemoteSkillInstallItem(item) {
39401
+ if (item === null || typeof item !== "object") return false;
39402
+ const o = item;
39403
+ return typeof o.sourceId === "string" && typeof o.skillName === "string" && typeof o.versionHash === "string" && Array.isArray(o.files);
39404
+ }
39405
+
39406
+ // src/skills/install/install-remote-skills-async.ts
39407
+ import fs43 from "node:fs";
39408
+ import path56 from "node:path";
39409
+
39410
+ // src/skills/install/constants.ts
39411
+ var INSTALL_SKILLS_YIELD_EVERY = 16;
39412
+
39413
+ // src/skills/install/install-remote-skills-async.ts
39414
+ async function writeInstallFileAsync(skillDir, f, filesWritten) {
39415
+ if (typeof f.path !== "string" || !f.text && !f.base64) return;
39416
+ if (++filesWritten.count % INSTALL_SKILLS_YIELD_EVERY === 0) {
39417
+ await yieldToEventLoop();
39418
+ }
39419
+ const dest = path56.join(skillDir, f.path);
39420
+ await fs43.promises.mkdir(path56.dirname(dest), { recursive: true });
39421
+ if (f.text !== void 0) {
39422
+ await fs43.promises.writeFile(dest, f.text, "utf8");
39423
+ } else if (f.base64) {
39424
+ await fs43.promises.writeFile(dest, Buffer.from(f.base64, "base64"));
39425
+ }
39426
+ }
39427
+ async function installRemoteSkillsAsync(cwd, targetDir, items) {
38756
39428
  const installed2 = [];
38757
39429
  if (!Array.isArray(items)) {
38758
39430
  return { success: false, error: "Invalid items" };
38759
39431
  }
39432
+ const filesWritten = { count: 0 };
38760
39433
  try {
38761
39434
  for (const item of items) {
38762
- if (typeof item.sourceId !== "string" || typeof item.skillName !== "string" || typeof item.versionHash !== "string" || !Array.isArray(item.files)) {
38763
- continue;
38764
- }
38765
- const skillDir = path48.join(cwd, targetDir, item.skillName);
39435
+ if (!isValidRemoteSkillInstallItem(item)) continue;
39436
+ const skillDir = path56.join(cwd, targetDir, item.skillName);
38766
39437
  for (const f of item.files) {
38767
- if (typeof f.path !== "string" || !f.text && !f.base64) continue;
38768
- const dest = path48.join(skillDir, f.path);
38769
- fs42.mkdirSync(path48.dirname(dest), { recursive: true });
38770
- if (f.text !== void 0) {
38771
- fs42.writeFileSync(dest, f.text, "utf8");
38772
- } else if (f.base64) {
38773
- fs42.writeFileSync(dest, Buffer.from(f.base64, "base64"));
38774
- }
39438
+ await writeInstallFileAsync(skillDir, f, filesWritten);
38775
39439
  }
38776
39440
  installed2.push({
38777
39441
  sourceId: item.sourceId,
@@ -38787,28 +39451,32 @@ function installRemoteSkills(cwd, targetDir, items) {
38787
39451
 
38788
39452
  // src/routing/handlers/install-skills.ts
38789
39453
  var handleInstallSkillsMessage = (msg, deps) => {
38790
- const socket = deps.getWs();
38791
39454
  const id = typeof msg.id === "string" ? msg.id : "";
38792
39455
  const targetDir = typeof msg.targetDir === "string" && msg.targetDir.trim() ? msg.targetDir.trim() : ".agents/skills";
38793
39456
  const rawItems = msg.items;
38794
39457
  const cwd = getBridgeRoot();
38795
- const result = installRemoteSkills(cwd, targetDir, rawItems);
38796
- if (!result.success) {
38797
- const err = result.error ?? "Invalid items";
38798
- deps.log(`[Bridge service] Install skills failed: ${err}`);
39458
+ void (async () => {
39459
+ const socket = deps.getWs();
39460
+ const result = await installRemoteSkillsAsync(cwd, targetDir, rawItems);
39461
+ if (!result.success) {
39462
+ const err = result.error ?? "Invalid items";
39463
+ deps.log(`[Bridge service] Install skills failed: ${err}`);
39464
+ if (socket) {
39465
+ sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
39466
+ }
39467
+ return;
39468
+ }
38799
39469
  if (socket) {
38800
- sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
39470
+ sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
38801
39471
  }
38802
- return;
38803
- }
38804
- if (socket) {
38805
- sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
38806
- }
39472
+ })();
38807
39473
  };
38808
39474
 
38809
39475
  // src/routing/handlers/refresh-local-skills.ts
38810
39476
  var handleRefreshLocalSkills = (_msg, deps) => {
38811
- deps.sendLocalSkillsReport?.();
39477
+ setImmediate(() => {
39478
+ deps.sendLocalSkillsReport?.();
39479
+ });
38812
39480
  };
38813
39481
 
38814
39482
  // src/routing/handlers/git/handle-session-git-commit.ts
@@ -38839,13 +39507,78 @@ async function handleSessionGitCommitAction(deps, sessionId, msg, reply) {
38839
39507
  });
38840
39508
  }
38841
39509
 
39510
+ // src/routing/handlers/git/session-git-changes-params.ts
39511
+ function readString(value) {
39512
+ return typeof value === "string" ? value.trim() : "";
39513
+ }
39514
+ function readChangesObject(msg) {
39515
+ const changes = msg.changes;
39516
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return null;
39517
+ return changes;
39518
+ }
39519
+ function parseSessionGitChangesParams(msg) {
39520
+ const nested = readChangesObject(msg);
39521
+ const file2 = nested?.file && typeof nested.file === "object" && !Array.isArray(nested.file) ? nested.file : null;
39522
+ const repoRelPath = readString(nested?.repoRelPath) || readString(msg.changesRepoRelPath);
39523
+ const viewRaw = nested?.view ?? msg.changesView;
39524
+ const view = viewRaw === "commit" ? "commit" : "working";
39525
+ const commitSha = readString(nested?.commitSha) || readString(msg.changesCommitSha);
39526
+ const recentCommitsLimit = nested?.recentCommitsLimit ?? msg.changesRecentCommitsLimit;
39527
+ return {
39528
+ repoRelPath,
39529
+ view,
39530
+ commitSha,
39531
+ recentCommitsLimit: typeof recentCommitsLimit === "number" || typeof recentCommitsLimit === "string" ? recentCommitsLimit : void 0,
39532
+ fileWorkspaceRelPath: readString(file2?.workspaceRelPath),
39533
+ fileChange: readString(file2?.change),
39534
+ fileMovedFromWorkspaceRelPath: readString(file2?.movedFromWorkspaceRelPath)
39535
+ };
39536
+ }
39537
+
39538
+ // src/routing/handlers/git/handle-session-git-get-change-file-patch.ts
39539
+ async function handleSessionGitGetChangeFilePatchAction(deps, msg, reply) {
39540
+ const changes = parseSessionGitChangesParams(msg);
39541
+ const { repoRelPath: repoRel, view, commitSha, fileWorkspaceRelPath, fileChange, fileMovedFromWorkspaceRelPath } = changes;
39542
+ const change = parseWorkingTreeChangeKind(fileChange);
39543
+ if (!repoRel || !fileWorkspaceRelPath || !change) {
39544
+ reply({
39545
+ ok: false,
39546
+ error: "changes.repoRelPath, changes.file.workspaceRelPath, and changes.file.change are required"
39547
+ });
39548
+ return;
39549
+ }
39550
+ if (view === "commit" && !commitSha) {
39551
+ reply({ ok: false, error: "changes.commitSha is required for commit file patch" });
39552
+ return;
39553
+ }
39554
+ const patch = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeFilePatch(msg.sessionId, {
39555
+ repoRelPath: repoRel,
39556
+ workspaceRelPath: fileWorkspaceRelPath,
39557
+ change,
39558
+ movedFromWorkspaceRelPath: fileMovedFromWorkspaceRelPath || null,
39559
+ basis: view === "commit" ? { kind: "commit", sha: commitSha } : { kind: "working" }
39560
+ });
39561
+ reply(
39562
+ {
39563
+ ok: true,
39564
+ patchContent: patch.patchContent ?? null,
39565
+ totalLines: patch.totalLines
39566
+ },
39567
+ ["patchContent"]
39568
+ );
39569
+ }
39570
+
38842
39571
  // src/routing/handlers/git/coalesce-list-changes.ts
38843
39572
  var listChangesInflight = /* @__PURE__ */ new Map();
39573
+ function normalizeListChangesRepoRel(repoRelPath) {
39574
+ if (!repoRelPath?.trim()) return "";
39575
+ return normRepoRel(repoRelPath.trim());
39576
+ }
38844
39577
  function listChangesInflightKey(sessionId, opts) {
38845
39578
  const basis = opts?.basis;
38846
39579
  return [
38847
39580
  sessionId,
38848
- opts?.repoRelPath ?? "",
39581
+ normalizeListChangesRepoRel(opts?.repoRelPath),
38849
39582
  basis?.kind === "commit" ? `commit:${basis.sha}` : "working",
38850
39583
  String(opts?.recentCommitsLimit ?? "")
38851
39584
  ].join("|");
@@ -38863,16 +39596,17 @@ function getSessionWorkingTreeChangeDetailsCoalesced(sessionWorktreeManager, ses
38863
39596
 
38864
39597
  // src/routing/handlers/git/handle-session-git-list-changes.ts
38865
39598
  async function handleSessionGitListChangesAction(deps, msg, reply) {
38866
- const repoRel = typeof msg.changesRepoRelPath === "string" ? msg.changesRepoRelPath.trim() : "";
38867
- const view = msg.changesView === "commit" ? "commit" : "working";
38868
- const commitSha = typeof msg.changesCommitSha === "string" ? msg.changesCommitSha.trim() : "";
39599
+ const changes = parseSessionGitChangesParams(msg);
39600
+ const { repoRelPath: repoRel, view, commitSha, recentCommitsLimit } = changes;
38869
39601
  if (view === "commit") {
38870
39602
  if (!repoRel || !commitSha) {
38871
- reply({ ok: false, error: "changesRepoRelPath and changesCommitSha are required for commit view" });
39603
+ reply({
39604
+ ok: false,
39605
+ error: "changes.repoRelPath and changes.commitSha are required for commit view"
39606
+ });
38872
39607
  return;
38873
39608
  }
38874
39609
  }
38875
- const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
38876
39610
  const opts = repoRel && view === "commit" && commitSha ? {
38877
39611
  repoRelPath: repoRel,
38878
39612
  basis: { kind: "commit", sha: commitSha },
@@ -38917,7 +39651,11 @@ async function handleSessionGitStatusAction(deps, sessionId, reply) {
38917
39651
  function sendSessionGitResult(ws, id, payload, e2ee, encryptedFields = []) {
38918
39652
  if (!ws) return;
38919
39653
  const message = { type: "session_git_result", id, ...payload };
38920
- sendWsMessage(ws, e2ee && encryptedFields.length > 0 ? e2ee.encryptFields(message, encryptedFields) : message);
39654
+ let wire = message;
39655
+ if (e2ee && encryptedFields.length > 0) {
39656
+ wire = e2ee.encryptFields(message, encryptedFields);
39657
+ }
39658
+ sendWsMessage(ws, wire);
38921
39659
  }
38922
39660
 
38923
39661
  // src/routing/handlers/git/session-git-request.ts
@@ -38925,7 +39663,7 @@ var handleSessionGitRequestMessage = (msg, deps) => {
38925
39663
  if (typeof msg.id !== "string") return;
38926
39664
  const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
38927
39665
  const action = msg.action;
38928
- if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes")
39666
+ if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes" && action !== "file_diff")
38929
39667
  return;
38930
39668
  void (async () => {
38931
39669
  const ws = deps.getWs();
@@ -38939,6 +39677,10 @@ var handleSessionGitRequestMessage = (msg, deps) => {
38939
39677
  await handleSessionGitListChangesAction(deps, { ...msg, sessionId }, reply);
38940
39678
  return;
38941
39679
  }
39680
+ if (action === "file_diff") {
39681
+ await handleSessionGitGetChangeFilePatchAction(deps, { ...msg, sessionId }, reply);
39682
+ return;
39683
+ }
38942
39684
  if (action === "push") {
38943
39685
  await handleSessionGitPushAction(deps, sessionId, reply);
38944
39686
  return;
@@ -38973,7 +39715,7 @@ var handleSessionDiscardedMessage = (msg, deps) => {
38973
39715
  };
38974
39716
 
38975
39717
  // src/routing/handlers/revert-turn-snapshot.ts
38976
- import * as fs43 from "node:fs";
39718
+ import * as fs44 from "node:fs";
38977
39719
  var handleRevertTurnSnapshotMessage = (msg, deps) => {
38978
39720
  const id = typeof msg.id === "string" ? msg.id : "";
38979
39721
  const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
@@ -38986,7 +39728,7 @@ var handleRevertTurnSnapshotMessage = (msg, deps) => {
38986
39728
  const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
38987
39729
  const file2 = snapshotFilePath(agentBase, turnId);
38988
39730
  try {
38989
- await fs43.promises.access(file2, fs43.constants.F_OK);
39731
+ await fs44.promises.access(file2, fs44.constants.F_OK);
38990
39732
  } catch {
38991
39733
  sendWsMessage(s, {
38992
39734
  type: "revert_turn_snapshot_result",
@@ -39024,11 +39766,13 @@ var handleDevServerControl = (msg, deps) => {
39024
39766
  // src/routing/handlers/dev-servers-config.ts
39025
39767
  var handleDevServersConfig = (msg, deps) => {
39026
39768
  const devServers = msg.devServers;
39027
- deps.devServerManager?.applyConfig(devServers ?? []);
39769
+ setImmediate(() => {
39770
+ deps.devServerManager?.applyConfig(devServers ?? []);
39771
+ });
39028
39772
  };
39029
39773
 
39030
39774
  // src/git/bridge-git-context.ts
39031
- import * as path49 from "node:path";
39775
+ import * as path57 from "node:path";
39032
39776
 
39033
39777
  // src/git/branches/get-current-branch.ts
39034
39778
  async function getCurrentBranch(repoPath) {
@@ -39078,12 +39822,12 @@ async function listRepoBranchRefs(repoPath) {
39078
39822
  // src/git/bridge-git-context.ts
39079
39823
  function folderNameForRelPath(relPath, bridgeRoot) {
39080
39824
  if (relPath === "." || relPath === "") {
39081
- return path49.basename(path49.resolve(bridgeRoot)) || "repo";
39825
+ return path57.basename(path57.resolve(bridgeRoot)) || "repo";
39082
39826
  }
39083
- return path49.basename(relPath) || relPath;
39827
+ return path57.basename(relPath) || relPath;
39084
39828
  }
39085
39829
  async function discoverGitReposForBridgeContext(bridgeRoot) {
39086
- const root = path49.resolve(bridgeRoot);
39830
+ const root = path57.resolve(bridgeRoot);
39087
39831
  if (await isGitRepoDirectory(root)) {
39088
39832
  const remoteUrl = await getRemoteOriginUrl(root);
39089
39833
  return [{ absolutePath: root, remoteUrl }];
@@ -39091,17 +39835,19 @@ async function discoverGitReposForBridgeContext(bridgeRoot) {
39091
39835
  const [deep, shallow] = await Promise.all([discoverGitReposUnderRoot(root), discoverGitRepos(root)]);
39092
39836
  const byPath = /* @__PURE__ */ new Map();
39093
39837
  for (const repo of [...deep, ...shallow]) {
39094
- byPath.set(path49.resolve(repo.absolutePath), repo);
39838
+ byPath.set(path57.resolve(repo.absolutePath), repo);
39095
39839
  }
39096
39840
  return [...byPath.values()];
39097
39841
  }
39098
39842
  async function getBridgeGitContext(bridgeRoot = getBridgeRoot()) {
39099
- const bridgeResolved = path49.resolve(bridgeRoot);
39843
+ const bridgeResolved = path57.resolve(bridgeRoot);
39100
39844
  const repos = await discoverGitReposForBridgeContext(bridgeResolved);
39101
39845
  const rows = [];
39102
- for (const repo of repos) {
39103
- let rel = path49.relative(bridgeResolved, repo.absolutePath);
39104
- if (rel.startsWith("..") || path49.isAbsolute(rel)) continue;
39846
+ for (let i = 0; i < repos.length; i++) {
39847
+ if (i > 0) await yieldToEventLoop();
39848
+ const repo = repos[i];
39849
+ let rel = path57.relative(bridgeResolved, repo.absolutePath);
39850
+ if (rel.startsWith("..") || path57.isAbsolute(rel)) continue;
39105
39851
  const relPath = rel === "" ? "." : rel.replace(/\\/g, "/");
39106
39852
  const currentBranch = await getCurrentBranch(repo.absolutePath);
39107
39853
  const remoteUrl = repo.remoteUrl.trim() || null;
@@ -39116,11 +39862,11 @@ async function getBridgeGitContext(bridgeRoot = getBridgeRoot()) {
39116
39862
  return rows;
39117
39863
  }
39118
39864
  async function listRepoBranchesForBridge(repoRelPath, bridgeRoot = getBridgeRoot()) {
39119
- const bridgeResolved = path49.resolve(bridgeRoot);
39865
+ const bridgeResolved = path57.resolve(bridgeRoot);
39120
39866
  const rel = repoRelPath.trim() === "." ? "" : repoRelPath.trim().replace(/\\/g, "/");
39121
- const repoPath = rel === "" ? bridgeResolved : path49.join(bridgeResolved, rel);
39122
- const resolved = path49.resolve(repoPath);
39123
- if (!resolved.startsWith(bridgeResolved + path49.sep) && resolved !== bridgeResolved) {
39867
+ const repoPath = rel === "" ? bridgeResolved : path57.join(bridgeResolved, rel);
39868
+ const resolved = path57.resolve(repoPath);
39869
+ if (!resolved.startsWith(bridgeResolved + path57.sep) && resolved !== bridgeResolved) {
39124
39870
  return [];
39125
39871
  }
39126
39872
  return listRepoBranchRefs(resolved);
@@ -39704,10 +40450,10 @@ function listCliAgentCapabilityCacheForWorkspace(db, workspaceId) {
39704
40450
  }
39705
40451
 
39706
40452
  // src/agents/capabilities/warmup-agent-capabilities-on-connect.ts
39707
- import * as path51 from "node:path";
40453
+ import * as path59 from "node:path";
39708
40454
 
39709
40455
  // src/agents/capabilities/probe-one-agent-type-for-capabilities.ts
39710
- import * as path50 from "node:path";
40456
+ import * as path58 from "node:path";
39711
40457
  async function probeOneAgentTypeForCapabilities(params) {
39712
40458
  const { agentType, cwd, workspaceId, log: log2, reportAgentCapabilities, bridgeReport = true, shouldContinue } = params;
39713
40459
  const canContinue = () => shouldContinue?.() !== false;
@@ -39745,7 +40491,7 @@ async function probeOneAgentTypeForCapabilities(params) {
39745
40491
  if (!canContinue()) return false;
39746
40492
  handle = await resolved.createClient({
39747
40493
  command: resolved.command,
39748
- cwd: path50.resolve(cwd),
40494
+ cwd: path58.resolve(cwd),
39749
40495
  backendAgentType: agentType,
39750
40496
  sessionMode: "agent",
39751
40497
  persistedAcpSessionId: null,
@@ -39823,7 +40569,7 @@ async function warmupAgentCapabilitiesOnConnect(params) {
39823
40569
  const { workspaceId, log: log2, getWs } = params;
39824
40570
  const isCurrent = beginAgentCapabilityWarmupRun();
39825
40571
  if (!isCurrent()) return;
39826
- const cwd = path51.resolve(getBridgeRoot());
40572
+ const cwd = path59.resolve(getBridgeRoot());
39827
40573
  async function sendBatchFromCache() {
39828
40574
  if (!isCurrent()) return;
39829
40575
  const socket = getWs();
@@ -40169,18 +40915,18 @@ import * as http from "node:http";
40169
40915
 
40170
40916
  // src/mcp/bridge-access/read-json-body.ts
40171
40917
  function readJsonBody(req) {
40172
- return new Promise((resolve26, reject) => {
40918
+ return new Promise((resolve33, reject) => {
40173
40919
  const chunks = [];
40174
40920
  req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
40175
40921
  req.on("end", () => {
40176
40922
  try {
40177
40923
  const raw = Buffer.concat(chunks).toString("utf8").trim();
40178
40924
  if (!raw) {
40179
- resolve26({});
40925
+ resolve33({});
40180
40926
  return;
40181
40927
  }
40182
40928
  const parsed = JSON.parse(raw);
40183
- resolve26(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
40929
+ resolve33(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
40184
40930
  } catch (e) {
40185
40931
  reject(e);
40186
40932
  }
@@ -40238,7 +40984,7 @@ function createBridgeAccessRequestHandler(registry2) {
40238
40984
  // src/mcp/bridge-access/start-server.ts
40239
40985
  function startBridgeAccessServer(registry2) {
40240
40986
  const server = http.createServer(createBridgeAccessRequestHandler(registry2));
40241
- return new Promise((resolve26, reject) => {
40987
+ return new Promise((resolve33, reject) => {
40242
40988
  server.once("error", reject);
40243
40989
  server.listen(0, "127.0.0.1", () => {
40244
40990
  const addr = server.address();
@@ -40246,7 +40992,7 @@ function startBridgeAccessServer(registry2) {
40246
40992
  reject(new Error("Bridge access server did not bind"));
40247
40993
  return;
40248
40994
  }
40249
- resolve26({
40995
+ resolve33({
40250
40996
  port: addr.port,
40251
40997
  close: () => new Promise((closeResolve, closeReject) => {
40252
40998
  server.close((err) => err ? closeReject(err) : closeResolve());
@@ -40386,8 +41132,8 @@ async function createBridgeConnection(options) {
40386
41132
  getCloudAccessToken: () => tokens.accessToken
40387
41133
  };
40388
41134
  const identifyReportedPaths = {
40389
- bridgeRootPath: path52.resolve(getBridgeRoot()),
40390
- worktreesRootPath: path52.resolve(worktreesRootPath)
41135
+ bridgeRootPath: path60.resolve(getBridgeRoot()),
41136
+ worktreesRootPath: path60.resolve(worktreesRootPath)
40391
41137
  };
40392
41138
  const { connect } = createMainBridgeWebSocketLifecycle({
40393
41139
  state,