webmux 0.11.0 → 0.13.0

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.
@@ -6932,7 +6932,7 @@ var log = {
6932
6932
  // backend/src/adapters/terminal.ts
6933
6933
  var textDecoder = new TextDecoder;
6934
6934
  var textEncoder = new TextEncoder;
6935
- var DASH_PORT = Bun.env.BACKEND_PORT || "5111";
6935
+ var DASH_PORT = Bun.env.PORT || "5111";
6936
6936
  var SESSION_PREFIX = `wm-dash-${DASH_PORT}-`;
6937
6937
  var MAX_SCROLLBACK_BYTES = 1 * 1024 * 1024;
6938
6938
  var TMUX_TIMEOUT_MS = 5000;
@@ -7413,7 +7413,8 @@ function parseLinkedRepos(raw) {
7413
7413
  return [];
7414
7414
  return raw.filter(isRecord).filter((entry) => typeof entry.repo === "string").map((entry) => ({
7415
7415
  repo: entry.repo,
7416
- alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo"
7416
+ alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo",
7417
+ ...typeof entry.dir === "string" && entry.dir.trim() ? { dir: entry.dir.trim() } : {}
7417
7418
  }));
7418
7419
  }
7419
7420
  function isDockerProfile(profile) {
@@ -7453,7 +7454,7 @@ function loadConfig(dir) {
7453
7454
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
7454
7455
  integrations: {
7455
7456
  github: {
7456
- linkedRepos: isRecord(parsed.integrations) && isRecord(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : []
7457
+ linkedRepos: isRecord(parsed.integrations) && isRecord(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : []
7457
7458
  },
7458
7459
  linear: {
7459
7460
  enabled: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
@@ -8088,9 +8089,9 @@ class BunTmuxGateway {
8088
8089
  }
8089
8090
  ensureSession(sessionName, cwd) {
8090
8091
  const check = runTmux(["has-session", "-t", sessionName]);
8091
- if (check.exitCode === 0)
8092
- return;
8093
- assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
8092
+ if (check.exitCode !== 0) {
8093
+ assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
8094
+ }
8094
8095
  }
8095
8096
  hasWindow(sessionName, windowName) {
8096
8097
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -8188,15 +8189,22 @@ function buildRuntimeBootstrap(runtimeEnvPath) {
8188
8189
  return `set -a; . ${quoteShell(runtimeEnvPath)}; set +a`;
8189
8190
  }
8190
8191
  function buildAgentInvocation(input) {
8191
- const promptSuffix = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
8192
8192
  if (input.agent === "codex") {
8193
8193
  const yoloFlag2 = input.yolo ? " --yolo" : "";
8194
+ if (input.launchMode === "resume") {
8195
+ return `codex${yoloFlag2} resume --last`;
8196
+ }
8197
+ const promptSuffix2 = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
8194
8198
  if (input.systemPrompt) {
8195
- return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
8199
+ return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
8196
8200
  }
8197
- return `codex${yoloFlag2}${promptSuffix}`;
8201
+ return `codex${yoloFlag2}${promptSuffix2}`;
8198
8202
  }
8199
8203
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
8204
+ if (input.launchMode === "resume") {
8205
+ return `claude${yoloFlag} --continue`;
8206
+ }
8207
+ const promptSuffix = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
8200
8208
  if (input.systemPrompt) {
8201
8209
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
8202
8210
  }
@@ -8279,6 +8287,7 @@ function ensureSessionLayout(tmux, plan) {
8279
8287
  cwd: rootPane.cwd,
8280
8288
  command: plan.shellCommand
8281
8289
  });
8290
+ tmux.setWindowOption(plan.sessionName, plan.windowName, "pane-base-index", "0");
8282
8291
  tmux.setWindowOption(plan.sessionName, plan.windowName, "automatic-rename", "off");
8283
8292
  tmux.setWindowOption(plan.sessionName, plan.windowName, "allow-rename", "off");
8284
8293
  for (const pane of plan.panes.slice(1)) {
@@ -8419,6 +8428,11 @@ function listGitWorktrees(cwd) {
8419
8428
  const output = runGit(["worktree", "list", "--porcelain"], cwd);
8420
8429
  return parseGitWorktreePorcelain(output);
8421
8430
  }
8431
+ function listLocalGitBranches(cwd) {
8432
+ const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd);
8433
+ return output.split(`
8434
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
8435
+ }
8422
8436
  function readGitWorktreeStatus(cwd) {
8423
8437
  const dirtyOutput = runGit(["status", "--porcelain"], cwd);
8424
8438
  const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
@@ -8460,13 +8474,21 @@ class BunGitGateway {
8460
8474
  listWorktrees(cwd) {
8461
8475
  return listGitWorktrees(cwd);
8462
8476
  }
8477
+ listLocalBranches(cwd) {
8478
+ return listLocalGitBranches(cwd);
8479
+ }
8463
8480
  readWorktreeStatus(cwd) {
8464
8481
  return readGitWorktreeStatus(cwd);
8465
8482
  }
8466
8483
  createWorktree(opts) {
8467
- const args = ["worktree", "add", "-b", opts.branch, opts.worktreePath];
8468
- if (opts.baseBranch)
8469
- args.push(opts.baseBranch);
8484
+ const args = ["worktree", "add"];
8485
+ if (opts.mode === "new") {
8486
+ args.push("-b", opts.branch, opts.worktreePath);
8487
+ if (opts.baseBranch)
8488
+ args.push(opts.baseBranch);
8489
+ } else {
8490
+ args.push(opts.worktreePath, opts.branch);
8491
+ }
8470
8492
  runGit(args, opts.repoRoot);
8471
8493
  }
8472
8494
  removeWorktree(opts) {
@@ -8545,10 +8567,12 @@ function rollbackManagedWorktreeCreation(opts, sessionLayoutPlan, git, deps) {
8545
8567
  } catch (error) {
8546
8568
  cleanupErrors.push(`worktree rollback failed: ${toErrorMessage(error)}`);
8547
8569
  }
8548
- try {
8549
- git.deleteBranch(opts.repoRoot, opts.branch, true);
8550
- } catch (error) {
8551
- cleanupErrors.push(`branch rollback failed: ${toErrorMessage(error)}`);
8570
+ if (opts.deleteBranchOnRollback ?? true) {
8571
+ try {
8572
+ git.deleteBranch(opts.repoRoot, opts.branch, true);
8573
+ } catch (error) {
8574
+ cleanupErrors.push(`branch rollback failed: ${toErrorMessage(error)}`);
8575
+ }
8552
8576
  }
8553
8577
  return cleanupErrors.length > 0 ? joinErrorMessages(cleanupErrors) : null;
8554
8578
  }
@@ -8598,6 +8622,7 @@ async function createManagedWorktree(opts, deps = {}) {
8598
8622
  repoRoot: opts.repoRoot,
8599
8623
  worktreePath: opts.worktreePath,
8600
8624
  branch: opts.branch,
8625
+ mode: opts.mode,
8601
8626
  baseBranch: opts.baseBranch
8602
8627
  });
8603
8628
  worktreeCreated = true;
@@ -8677,19 +8702,29 @@ class LifecycleService {
8677
8702
  this.deps = deps;
8678
8703
  }
8679
8704
  async createWorktree(input) {
8680
- const branch = await this.resolveBranch(input.branch, input.prompt);
8681
- this.ensureBranchAvailable(branch);
8705
+ const mode = input.mode ?? "new";
8706
+ const branch = await this.resolveBranch(input.branch, input.prompt, mode);
8707
+ this.ensureBranchAvailable(branch, mode);
8682
8708
  const { profileName, profile } = this.resolveProfile(input.profile);
8683
8709
  const agent = this.resolveAgent(input.agent);
8684
8710
  const worktreePath = this.resolveWorktreePath(branch);
8711
+ const deleteBranchOnRollback = mode === "new";
8685
8712
  let initialized = null;
8686
8713
  try {
8714
+ await this.reportCreateProgress({
8715
+ branch,
8716
+ path: worktreePath,
8717
+ profile: profileName,
8718
+ agent,
8719
+ phase: "creating_worktree"
8720
+ });
8687
8721
  await mkdir4(dirname3(worktreePath), { recursive: true });
8688
8722
  initialized = await createManagedWorktree({
8689
8723
  repoRoot: this.deps.projectRoot,
8690
8724
  worktreePath,
8691
8725
  branch,
8692
- baseBranch: this.deps.config.workspace.mainBranch,
8726
+ mode,
8727
+ ...mode === "new" ? { baseBranch: this.deps.config.workspace.mainBranch } : {},
8693
8728
  profile: profileName,
8694
8729
  agent,
8695
8730
  runtime: profile.runtime,
@@ -8697,13 +8732,17 @@ class LifecycleService {
8697
8732
  allocatedPorts: await this.allocatePorts(),
8698
8733
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
8699
8734
  controlUrl: this.controlUrl(),
8700
- controlToken: await this.deps.getControlToken()
8735
+ controlToken: await this.deps.getControlToken(),
8736
+ deleteBranchOnRollback
8701
8737
  }, {
8702
8738
  git: this.deps.git
8703
8739
  });
8704
- await ensureAgentRuntimeArtifacts({
8705
- gitDir: initialized.paths.gitDir,
8706
- worktreePath
8740
+ await this.reportCreateProgress({
8741
+ branch,
8742
+ path: worktreePath,
8743
+ profile: profileName,
8744
+ agent,
8745
+ phase: "running_post_create_hook"
8707
8746
  });
8708
8747
  await this.runLifecycleHook({
8709
8748
  name: "postCreate",
@@ -8711,13 +8750,44 @@ class LifecycleService {
8711
8750
  meta: initialized.meta,
8712
8751
  worktreePath
8713
8752
  });
8753
+ initialized = await this.refreshManagedArtifactsFromMeta({
8754
+ gitDir: initialized.paths.gitDir,
8755
+ meta: initialized.meta,
8756
+ worktreePath
8757
+ });
8758
+ await this.reportCreateProgress({
8759
+ branch,
8760
+ path: worktreePath,
8761
+ profile: profileName,
8762
+ agent,
8763
+ phase: "preparing_runtime"
8764
+ });
8765
+ await ensureAgentRuntimeArtifacts({
8766
+ gitDir: initialized.paths.gitDir,
8767
+ worktreePath
8768
+ });
8769
+ await this.reportCreateProgress({
8770
+ branch,
8771
+ path: worktreePath,
8772
+ profile: profileName,
8773
+ agent,
8774
+ phase: "starting_session"
8775
+ });
8714
8776
  await this.materializeRuntimeSession({
8715
8777
  branch,
8716
8778
  profile,
8717
8779
  agent,
8718
8780
  initialized,
8719
8781
  worktreePath,
8720
- prompt: input.prompt
8782
+ prompt: input.prompt,
8783
+ launchMode: "fresh"
8784
+ });
8785
+ await this.reportCreateProgress({
8786
+ branch,
8787
+ path: worktreePath,
8788
+ profile: profileName,
8789
+ agent,
8790
+ phase: "reconciling"
8721
8791
  });
8722
8792
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
8723
8793
  return {
@@ -8726,17 +8796,20 @@ class LifecycleService {
8726
8796
  };
8727
8797
  } catch (error) {
8728
8798
  if (initialized) {
8729
- const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime);
8799
+ const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime, deleteBranchOnRollback);
8730
8800
  if (cleanupError) {
8731
8801
  throw this.wrapOperationError(new Error(`${toErrorMessage2(error)}; ${cleanupError}`));
8732
8802
  }
8733
8803
  }
8734
8804
  throw this.wrapOperationError(error);
8805
+ } finally {
8806
+ await this.finishCreateProgress(branch);
8735
8807
  }
8736
8808
  }
8737
8809
  async openWorktree(branch) {
8738
8810
  try {
8739
8811
  const resolved = await this.resolveExistingWorktree(branch);
8812
+ const launchMode = resolved.meta ? "resume" : "fresh";
8740
8813
  const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
8741
8814
  const { profile } = this.resolveProfile(initialized.meta.profile);
8742
8815
  await ensureAgentRuntimeArtifacts({
@@ -8748,7 +8821,8 @@ class LifecycleService {
8748
8821
  profile,
8749
8822
  agent: initialized.meta.agent,
8750
8823
  initialized,
8751
- worktreePath: resolved.entry.path
8824
+ worktreePath: resolved.entry.path,
8825
+ launchMode
8752
8826
  });
8753
8827
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
8754
8828
  return {
@@ -8776,6 +8850,20 @@ class LifecycleService {
8776
8850
  throw this.wrapOperationError(error);
8777
8851
  }
8778
8852
  }
8853
+ async pruneWorktrees() {
8854
+ try {
8855
+ const resolvedWorktrees = await this.resolveAllWorktrees();
8856
+ const removedBranches = [];
8857
+ for (const resolved of resolvedWorktrees) {
8858
+ const branch = resolved.entry.branch ?? resolved.entry.path;
8859
+ await this.removeResolvedWorktree(resolved);
8860
+ removedBranches.push(branch);
8861
+ }
8862
+ return { removedBranches };
8863
+ } catch (error) {
8864
+ throw this.wrapOperationError(error);
8865
+ }
8866
+ }
8779
8867
  async mergeWorktree(branch) {
8780
8868
  try {
8781
8869
  const resolved = await this.resolveExistingWorktree(branch);
@@ -8794,9 +8882,17 @@ class LifecycleService {
8794
8882
  throw this.wrapOperationError(error);
8795
8883
  }
8796
8884
  }
8797
- async resolveBranch(rawBranch, prompt) {
8885
+ listAvailableBranches() {
8886
+ const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
8887
+ const checkedOutBranches = this.listCheckedOutBranches();
8888
+ return localBranches.filter((branch) => !checkedOutBranches.has(branch)).sort((left, right) => left.localeCompare(right)).map((name) => ({ name }));
8889
+ }
8890
+ async resolveBranch(rawBranch, prompt, mode) {
8798
8891
  const explicitBranch = rawBranch?.trim();
8799
- const branch = explicitBranch || await this.generateAutoName(prompt) || generateBranchName();
8892
+ const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) || generateBranchName();
8893
+ if (!branch) {
8894
+ throw new LifecycleError("Existing branch is required", 400);
8895
+ }
8800
8896
  if (!isValidBranchName(branch)) {
8801
8897
  throw new LifecycleError(`Invalid branch name: ${branch}`, 400);
8802
8898
  }
@@ -8808,10 +8904,19 @@ class LifecycleService {
8808
8904
  }
8809
8905
  return await this.deps.autoName.generateBranchName(this.deps.config.autoName, prompt);
8810
8906
  }
8811
- ensureBranchAvailable(branch) {
8812
- const exists = this.listProjectWorktrees().some((entry) => entry.branch === branch);
8813
- if (exists) {
8814
- throw new LifecycleError(`Worktree already exists: ${branch}`, 409);
8907
+ ensureBranchAvailable(branch, mode) {
8908
+ const localBranches = new Set(this.listLocalBranches());
8909
+ if (mode === "new") {
8910
+ if (localBranches.has(branch)) {
8911
+ throw new LifecycleError(`Branch already exists: ${branch}`, 409);
8912
+ }
8913
+ return;
8914
+ }
8915
+ if (!localBranches.has(branch)) {
8916
+ throw new LifecycleError(`Branch not found: ${branch}`, 404);
8917
+ }
8918
+ if (this.listCheckedOutBranches().has(branch)) {
8919
+ throw new LifecycleError(`Branch already has a worktree: ${branch}`, 409);
8815
8920
  }
8816
8921
  }
8817
8922
  resolveProfile(profileName) {
@@ -8850,6 +8955,12 @@ class LifecycleService {
8850
8955
  resolveWorktreePath(branch) {
8851
8956
  return resolve3(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
8852
8957
  }
8958
+ listLocalBranches() {
8959
+ return this.deps.git.listLocalBranches(resolve3(this.deps.projectRoot));
8960
+ }
8961
+ listCheckedOutBranches() {
8962
+ return new Set(this.deps.git.listWorktrees(resolve3(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
8963
+ }
8853
8964
  listProjectWorktrees() {
8854
8965
  const projectRoot = resolve3(this.deps.projectRoot);
8855
8966
  return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve3(entry.path) !== projectRoot);
@@ -8870,6 +8981,14 @@ class LifecycleService {
8870
8981
  const meta = await readWorktreeMeta(gitDir);
8871
8982
  return { entry, gitDir, meta };
8872
8983
  }
8984
+ async resolveAllWorktrees() {
8985
+ const entries = this.listProjectWorktrees().sort((left, right) => (left.branch ?? left.path).localeCompare(right.branch ?? right.path));
8986
+ return await Promise.all(entries.map(async (entry) => {
8987
+ const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
8988
+ const meta = await readWorktreeMeta(gitDir);
8989
+ return { entry, gitDir, meta };
8990
+ }));
8991
+ }
8873
8992
  async initializeUnmanagedWorktree(resolved) {
8874
8993
  const { profileName, profile } = this.resolveProfile(undefined);
8875
8994
  const dotenvValues = await loadDotenvLocal(resolved.entry.path);
@@ -8891,21 +9010,28 @@ class LifecycleService {
8891
9010
  if (!resolved.meta) {
8892
9011
  throw new Error("Missing managed metadata");
8893
9012
  }
8894
- const dotenvValues = await loadDotenvLocal(resolved.entry.path);
8895
- const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
8896
- WEBMUX_WORKTREE_PATH: resolved.entry.path
9013
+ return await this.refreshManagedArtifactsFromMeta({
9014
+ gitDir: resolved.gitDir,
9015
+ meta: resolved.meta,
9016
+ worktreePath: resolved.entry.path
9017
+ });
9018
+ }
9019
+ async refreshManagedArtifactsFromMeta(input) {
9020
+ const dotenvValues = await loadDotenvLocal(input.worktreePath);
9021
+ const runtimeEnv = buildRuntimeEnvMap(input.meta, {
9022
+ WEBMUX_WORKTREE_PATH: input.worktreePath
8897
9023
  }, dotenvValues);
8898
- await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
9024
+ await writeRuntimeEnv(input.gitDir, runtimeEnv);
8899
9025
  const controlEnv = buildControlEnvMap({
8900
9026
  controlUrl: this.controlUrl(),
8901
9027
  controlToken: await this.deps.getControlToken(),
8902
- worktreeId: resolved.meta.worktreeId,
8903
- branch: resolved.meta.branch
9028
+ worktreeId: input.meta.worktreeId,
9029
+ branch: input.meta.branch
8904
9030
  });
8905
- await writeControlEnv(resolved.gitDir, controlEnv);
9031
+ await writeControlEnv(input.gitDir, controlEnv);
8906
9032
  return {
8907
- meta: resolved.meta,
8908
- paths: getWorktreeStoragePaths(resolved.gitDir),
9033
+ meta: input.meta,
9034
+ paths: getWorktreeStoragePaths(input.gitDir),
8909
9035
  runtimeEnv,
8910
9036
  controlEnv
8911
9037
  };
@@ -8928,6 +9054,7 @@ class LifecycleService {
8928
9054
  initialized: input.initialized,
8929
9055
  worktreePath: input.worktreePath,
8930
9056
  prompt: input.prompt,
9057
+ launchMode: input.launchMode,
8931
9058
  containerName
8932
9059
  }));
8933
9060
  return;
@@ -8938,11 +9065,12 @@ class LifecycleService {
8938
9065
  agent: input.agent,
8939
9066
  initialized: input.initialized,
8940
9067
  worktreePath: input.worktreePath,
8941
- prompt: input.prompt
9068
+ prompt: input.prompt,
9069
+ launchMode: input.launchMode
8942
9070
  }));
8943
9071
  }
8944
9072
  buildSessionLayout(input) {
8945
- const systemPrompt = input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
9073
+ const systemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
8946
9074
  const containerName = input.containerName;
8947
9075
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
8948
9076
  repoRoot: this.deps.projectRoot,
@@ -8955,7 +9083,8 @@ class LifecycleService {
8955
9083
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
8956
9084
  yolo: input.profile.yolo === true,
8957
9085
  systemPrompt,
8958
- prompt: input.prompt
9086
+ prompt: input.launchMode === "fresh" ? input.prompt : undefined,
9087
+ launchMode: input.launchMode
8959
9088
  }),
8960
9089
  shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
8961
9090
  } : {
@@ -8964,7 +9093,8 @@ class LifecycleService {
8964
9093
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
8965
9094
  yolo: input.profile.yolo === true,
8966
9095
  systemPrompt,
8967
- prompt: input.prompt
9096
+ prompt: input.launchMode === "fresh" ? input.prompt : undefined,
9097
+ launchMode: input.launchMode
8968
9098
  }),
8969
9099
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
8970
9100
  }
@@ -8976,7 +9106,7 @@ class LifecycleService {
8976
9106
  }
8977
9107
  return profile;
8978
9108
  }
8979
- async cleanupFailedCreate(branch, worktreePath, runtime) {
9109
+ async cleanupFailedCreate(branch, worktreePath, runtime, deleteBranch) {
8980
9110
  const cleanupErrors = [];
8981
9111
  if (runtime === "docker") {
8982
9112
  try {
@@ -8996,8 +9126,8 @@ class LifecycleService {
8996
9126
  worktreePath,
8997
9127
  branch,
8998
9128
  force: true,
8999
- deleteBranch: true,
9000
- deleteBranchForce: true
9129
+ deleteBranch,
9130
+ deleteBranchForce: deleteBranch
9001
9131
  }, this.deps.git);
9002
9132
  } catch (error) {
9003
9133
  cleanupErrors.push(`worktree cleanup failed: ${toErrorMessage2(error)}`);
@@ -9053,6 +9183,12 @@ class LifecycleService {
9053
9183
  });
9054
9184
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
9055
9185
  }
9186
+ async reportCreateProgress(progress) {
9187
+ await this.deps.onCreateProgress?.(progress);
9188
+ }
9189
+ async finishCreateProgress(branch) {
9190
+ await this.deps.onCreateFinished?.(branch);
9191
+ }
9056
9192
  wrapOperationError(error) {
9057
9193
  if (error instanceof LifecycleError) {
9058
9194
  return error;
@@ -9420,7 +9556,12 @@ function clonePrEntry(pr) {
9420
9556
  comments: pr.comments.map((comment) => ({ ...comment }))
9421
9557
  };
9422
9558
  }
9423
- function mapWorktreeSnapshot(state, now, findLinearIssue) {
9559
+ function mapCreationSnapshot(creating) {
9560
+ return creating ? {
9561
+ phase: creating.phase
9562
+ } : null;
9563
+ }
9564
+ function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
9424
9565
  return {
9425
9566
  branch: state.branch,
9426
9567
  path: state.path,
@@ -9430,21 +9571,51 @@ function mapWorktreeSnapshot(state, now, findLinearIssue) {
9430
9571
  mux: state.session.exists,
9431
9572
  dirty: state.git.dirty || state.git.aheadCount > 0,
9432
9573
  paneCount: state.session.paneCount,
9433
- status: state.agent.lifecycle,
9574
+ status: creating ? "creating" : state.agent.lifecycle,
9434
9575
  elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
9435
9576
  services: state.services.map((service) => ({ ...service })),
9436
9577
  prs: state.prs.map((pr) => clonePrEntry(pr)),
9437
- linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null
9578
+ linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
9579
+ creation: mapCreationSnapshot(creating)
9580
+ };
9581
+ }
9582
+ function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
9583
+ return {
9584
+ branch: creating.branch,
9585
+ path: creating.path,
9586
+ dir: creating.path,
9587
+ profile: creating.profile,
9588
+ agentName: creating.agentName,
9589
+ mux: false,
9590
+ dirty: false,
9591
+ paneCount: 0,
9592
+ status: "creating",
9593
+ elapsed: "",
9594
+ services: [],
9595
+ prs: [],
9596
+ linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
9597
+ creation: mapCreationSnapshot(creating)
9438
9598
  };
9439
9599
  }
9440
9600
  function buildProjectSnapshot(input) {
9441
9601
  const now = input.now ?? (() => new Date);
9602
+ const creatingWorktrees = input.creatingWorktrees ?? [];
9603
+ const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
9604
+ const runtimeWorktrees = input.runtime.listWorktrees();
9605
+ const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
9606
+ const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, input.findLinearIssue));
9607
+ for (const creating of creatingWorktrees) {
9608
+ if (!runtimeBranches.has(creating.branch)) {
9609
+ worktrees.push(mapCreatingWorktreeSnapshot(creating, input.findLinearIssue));
9610
+ }
9611
+ }
9612
+ worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
9442
9613
  return {
9443
9614
  project: {
9444
9615
  name: input.projectName,
9445
9616
  mainBranch: input.mainBranch
9446
9617
  },
9447
- worktrees: input.runtime.listWorktrees().map((state) => mapWorktreeSnapshot(state, now, input.findLinearIssue)),
9618
+ worktrees,
9448
9619
  notifications: input.notifications.map((notification) => ({ ...notification }))
9449
9620
  };
9450
9621
  }
@@ -9859,16 +10030,14 @@ class BunPortProbe {
9859
10030
  }
9860
10031
 
9861
10032
  // backend/src/services/auto-name-service.ts
10033
+ var MAX_BRANCH_LENGTH = 40;
9862
10034
  var DEFAULT_SYSTEM_PROMPT = [
9863
10035
  "Generate a concise git branch name from the task description.",
9864
10036
  "Return only the branch name.",
9865
10037
  "Use lowercase kebab-case.",
10038
+ `Maximum ${MAX_BRANCH_LENGTH} characters.`,
9866
10039
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
9867
10040
  ].join(" ");
9868
- function buildPrompt(task) {
9869
- return `Task description:
9870
- ${task.trim()}`;
9871
- }
9872
10041
  function normalizeGeneratedBranchName(raw) {
9873
10042
  let branch = raw.trim();
9874
10043
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -9880,6 +10049,7 @@ function normalizeGeneratedBranchName(raw) {
9880
10049
  branch = branch.replace(/[/.]+/g, "-");
9881
10050
  branch = branch.replace(/-+/g, "-");
9882
10051
  branch = branch.replace(/^-+|-+$/g, "");
10052
+ branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
9883
10053
  if (!branch) {
9884
10054
  throw new Error("Auto-name model returned an empty branch name");
9885
10055
  }
@@ -9922,6 +10092,9 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
9922
10092
  function escapeTomlString(s) {
9923
10093
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
9924
10094
  }
10095
+ function buildPrompt(prompt) {
10096
+ return `Here is the task description: ${prompt}. You MUST return the branch name only, no other text or comments. Be fast, make it simple, and concise.`;
10097
+ }
9925
10098
  function buildCodexArgs(model, systemPrompt, prompt) {
9926
10099
  const args = [
9927
10100
  "codex",
@@ -10348,9 +10521,33 @@ class ReconciliationService {
10348
10521
  }
10349
10522
  }
10350
10523
 
10524
+ // backend/src/services/worktree-creation-service.ts
10525
+ class WorktreeCreationTracker {
10526
+ worktrees = new Map;
10527
+ set(progress) {
10528
+ const next = {
10529
+ branch: progress.branch,
10530
+ path: progress.path,
10531
+ profile: progress.profile,
10532
+ agentName: progress.agent,
10533
+ phase: progress.phase
10534
+ };
10535
+ this.worktrees.set(progress.branch, next);
10536
+ }
10537
+ clear(branch) {
10538
+ return this.worktrees.delete(branch);
10539
+ }
10540
+ has(branch) {
10541
+ return this.worktrees.has(branch);
10542
+ }
10543
+ list() {
10544
+ return [...this.worktrees.values()].sort((left, right) => left.branch.localeCompare(right.branch)).map((state) => ({ ...state }));
10545
+ }
10546
+ }
10547
+
10351
10548
  // backend/src/runtime.ts
10352
10549
  function createWebmuxRuntime(options = {}) {
10353
- const port = options.port ?? parseInt(Bun.env.BACKEND_PORT || "5111", 10);
10550
+ const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
10354
10551
  const projectDir = gitRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
10355
10552
  const config = loadConfig(projectDir);
10356
10553
  const git = new BunGitGateway;
@@ -10360,6 +10557,7 @@ function createWebmuxRuntime(options = {}) {
10360
10557
  const hooks = new BunLifecycleHookRunner;
10361
10558
  const autoName = new AutoNameService;
10362
10559
  const projectRuntime = new ProjectRuntime;
10560
+ const worktreeCreationTracker = new WorktreeCreationTracker;
10363
10561
  const runtimeNotifications = new NotificationService;
10364
10562
  const reconciliationService = new ReconciliationService({
10365
10563
  config,
@@ -10378,7 +10576,13 @@ function createWebmuxRuntime(options = {}) {
10378
10576
  docker,
10379
10577
  reconciliation: reconciliationService,
10380
10578
  hooks,
10381
- autoName
10579
+ autoName,
10580
+ onCreateProgress: (progress) => {
10581
+ worktreeCreationTracker.set(progress);
10582
+ },
10583
+ onCreateFinished: (branch) => {
10584
+ worktreeCreationTracker.clear(branch);
10585
+ }
10382
10586
  });
10383
10587
  return {
10384
10588
  port,
@@ -10391,6 +10595,7 @@ function createWebmuxRuntime(options = {}) {
10391
10595
  hooks,
10392
10596
  autoName,
10393
10597
  projectRuntime,
10598
+ worktreeCreationTracker,
10394
10599
  runtimeNotifications,
10395
10600
  reconciliationService,
10396
10601
  lifecycleService
@@ -10398,7 +10603,7 @@ function createWebmuxRuntime(options = {}) {
10398
10603
  }
10399
10604
 
10400
10605
  // backend/src/server.ts
10401
- var PORT = parseInt(Bun.env.BACKEND_PORT || "5111", 10);
10606
+ var PORT = parseInt(Bun.env.PORT || "5111", 10);
10402
10607
  var STATIC_DIR = Bun.env.WEBMUX_STATIC_DIR || "";
10403
10608
  var runtime = createWebmuxRuntime({
10404
10609
  port: PORT,
@@ -10409,6 +10614,7 @@ var config = runtime.config;
10409
10614
  var git = runtime.git;
10410
10615
  var tmux = runtime.tmux;
10411
10616
  var projectRuntime = runtime.projectRuntime;
10617
+ var worktreeCreationTracker = runtime.worktreeCreationTracker;
10412
10618
  var runtimeNotifications = runtime.runtimeNotifications;
10413
10619
  var reconciliationService = runtime.reconciliationService;
10414
10620
  var removingBranches = new Set;
@@ -10431,7 +10637,11 @@ function getFrontendConfig() {
10431
10637
  })),
10432
10638
  defaultProfileName,
10433
10639
  autoName: config.autoName !== null,
10434
- startupEnvs: config.startupEnvs
10640
+ startupEnvs: config.startupEnvs,
10641
+ linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
10642
+ alias: lr.alias,
10643
+ ...lr.dir ? { dir: resolve5(PROJECT_DIR, lr.dir) } : {}
10644
+ }))
10435
10645
  };
10436
10646
  }
10437
10647
  function parseWsMessage(raw) {
@@ -10489,8 +10699,17 @@ function ensureBranchNotRemoving(branch) {
10489
10699
  throw new LifecycleError(`Worktree is being removed: ${branch}`, 409);
10490
10700
  }
10491
10701
  }
10492
- async function withRemovingBranch(branch, fn) {
10702
+ function ensureBranchNotCreating(branch) {
10703
+ if (worktreeCreationTracker.has(branch)) {
10704
+ throw new LifecycleError(`Worktree is being created: ${branch}`, 409);
10705
+ }
10706
+ }
10707
+ function ensureBranchNotBusy(branch) {
10493
10708
  ensureBranchNotRemoving(branch);
10709
+ ensureBranchNotCreating(branch);
10710
+ }
10711
+ async function withRemovingBranch(branch, fn) {
10712
+ ensureBranchNotBusy(branch);
10494
10713
  removingBranches.add(branch);
10495
10714
  try {
10496
10715
  return await fn();
@@ -10499,7 +10718,7 @@ async function withRemovingBranch(branch, fn) {
10499
10718
  }
10500
10719
  }
10501
10720
  async function resolveTerminalWorktree(branch) {
10502
- ensureBranchNotRemoving(branch);
10721
+ ensureBranchNotBusy(branch);
10503
10722
  await reconciliationService.reconcile(PROJECT_DIR);
10504
10723
  const state = projectRuntime.getWorktreeByBranch(branch);
10505
10724
  if (!state) {
@@ -10562,6 +10781,7 @@ async function apiGetProject() {
10562
10781
  projectName: config.name,
10563
10782
  mainBranch: config.workspace.mainBranch,
10564
10783
  runtime: projectRuntime,
10784
+ creatingWorktrees: worktreeCreationTracker.list(),
10565
10785
  notifications: runtimeNotifications.list(),
10566
10786
  findLinearIssue: (branch) => {
10567
10787
  const match = linearIssues.find((issue) => branchMatchesIssue(branch, issue.branchName));
@@ -10602,6 +10822,11 @@ async function apiRuntimeEvent(req) {
10602
10822
  ...notification ? { notification } : {}
10603
10823
  });
10604
10824
  }
10825
+ async function apiListBranches() {
10826
+ return jsonResponse({
10827
+ branches: lifecycleService.listAvailableBranches()
10828
+ });
10829
+ }
10605
10830
  async function apiCreateWorktree(req) {
10606
10831
  const raw = await req.json();
10607
10832
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -10623,8 +10848,16 @@ async function apiCreateWorktree(req) {
10623
10848
  const prompt = typeof body.prompt === "string" ? body.prompt : undefined;
10624
10849
  const profile = typeof body.profile === "string" ? body.profile : undefined;
10625
10850
  const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
10626
- log.info(`[worktree:add]${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10851
+ const mode = body.mode;
10852
+ if (mode !== undefined && mode !== "new" && mode !== "existing") {
10853
+ return errorResponse("Invalid worktree create mode", 400);
10854
+ }
10855
+ if (branch) {
10856
+ ensureBranchNotCreating(branch);
10857
+ }
10858
+ log.info(`[worktree:add] mode=${mode ?? "new"}${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10627
10859
  const result = await lifecycleService.createWorktree({
10860
+ mode,
10628
10861
  branch,
10629
10862
  prompt,
10630
10863
  profile,
@@ -10643,19 +10876,21 @@ async function apiDeleteWorktree(name) {
10643
10876
  });
10644
10877
  }
10645
10878
  async function apiOpenWorktree(name) {
10646
- ensureBranchNotRemoving(name);
10879
+ ensureBranchNotBusy(name);
10647
10880
  log.info(`[worktree:open] name=${name}`);
10648
10881
  const result = await lifecycleService.openWorktree(name);
10649
10882
  log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
10650
10883
  return jsonResponse({ ok: true });
10651
10884
  }
10652
10885
  async function apiCloseWorktree(name) {
10886
+ ensureBranchNotBusy(name);
10653
10887
  log.info(`[worktree:close] name=${name}`);
10654
10888
  await lifecycleService.closeWorktree(name);
10655
10889
  log.debug(`[worktree:close] done name=${name}`);
10656
10890
  return jsonResponse({ ok: true });
10657
10891
  }
10658
10892
  async function apiSendPrompt(name, req) {
10893
+ ensureBranchNotBusy(name);
10659
10894
  const raw = await req.json();
10660
10895
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
10661
10896
  return errorResponse("Invalid request body", 400);
@@ -10673,6 +10908,7 @@ async function apiSendPrompt(name, req) {
10673
10908
  return jsonResponse({ ok: true });
10674
10909
  }
10675
10910
  async function apiMergeWorktree(name) {
10911
+ ensureBranchNotBusy(name);
10676
10912
  log.info(`[worktree:merge] name=${name}`);
10677
10913
  await lifecycleService.mergeWorktree(name);
10678
10914
  log.debug(`[worktree:merge] done name=${name}`);
@@ -10710,6 +10946,9 @@ Bun.serve({
10710
10946
  "/api/config": {
10711
10947
  GET: () => jsonResponse(getFrontendConfig())
10712
10948
  },
10949
+ "/api/branches": {
10950
+ GET: () => catching("GET /api/branches", () => apiListBranches())
10951
+ },
10713
10952
  "/api/project": {
10714
10953
  GET: () => catching("GET /api/project", () => apiGetProject())
10715
10954
  },