webmux 0.12.0 → 0.14.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.
@@ -8092,7 +8092,6 @@ class BunTmuxGateway {
8092
8092
  if (check.exitCode !== 0) {
8093
8093
  assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
8094
8094
  }
8095
- assertTmuxOk(["set-option", "-t", sessionName, "pane-base-index", "0"], `set pane-base-index on ${sessionName}`);
8096
8095
  }
8097
8096
  hasWindow(sessionName, windowName) {
8098
8097
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -8190,15 +8189,22 @@ function buildRuntimeBootstrap(runtimeEnvPath) {
8190
8189
  return `set -a; . ${quoteShell(runtimeEnvPath)}; set +a`;
8191
8190
  }
8192
8191
  function buildAgentInvocation(input) {
8193
- const promptSuffix = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
8194
8192
  if (input.agent === "codex") {
8195
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)}` : "";
8196
8198
  if (input.systemPrompt) {
8197
- return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
8199
+ return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
8198
8200
  }
8199
- return `codex${yoloFlag2}${promptSuffix}`;
8201
+ return `codex${yoloFlag2}${promptSuffix2}`;
8200
8202
  }
8201
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)}` : "";
8202
8208
  if (input.systemPrompt) {
8203
8209
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
8204
8210
  }
@@ -8281,6 +8287,7 @@ function ensureSessionLayout(tmux, plan) {
8281
8287
  cwd: rootPane.cwd,
8282
8288
  command: plan.shellCommand
8283
8289
  });
8290
+ tmux.setWindowOption(plan.sessionName, plan.windowName, "pane-base-index", "0");
8284
8291
  tmux.setWindowOption(plan.sessionName, plan.windowName, "automatic-rename", "off");
8285
8292
  tmux.setWindowOption(plan.sessionName, plan.windowName, "allow-rename", "off");
8286
8293
  for (const pane of plan.panes.slice(1)) {
@@ -8421,6 +8428,11 @@ function listGitWorktrees(cwd) {
8421
8428
  const output = runGit(["worktree", "list", "--porcelain"], cwd);
8422
8429
  return parseGitWorktreePorcelain(output);
8423
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
+ }
8424
8436
  function readGitWorktreeStatus(cwd) {
8425
8437
  const dirtyOutput = runGit(["status", "--porcelain"], cwd);
8426
8438
  const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
@@ -8462,13 +8474,21 @@ class BunGitGateway {
8462
8474
  listWorktrees(cwd) {
8463
8475
  return listGitWorktrees(cwd);
8464
8476
  }
8477
+ listLocalBranches(cwd) {
8478
+ return listLocalGitBranches(cwd);
8479
+ }
8465
8480
  readWorktreeStatus(cwd) {
8466
8481
  return readGitWorktreeStatus(cwd);
8467
8482
  }
8468
8483
  createWorktree(opts) {
8469
- const args = ["worktree", "add", "-b", opts.branch, opts.worktreePath];
8470
- if (opts.baseBranch)
8471
- 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
+ }
8472
8492
  runGit(args, opts.repoRoot);
8473
8493
  }
8474
8494
  removeWorktree(opts) {
@@ -8547,10 +8567,12 @@ function rollbackManagedWorktreeCreation(opts, sessionLayoutPlan, git, deps) {
8547
8567
  } catch (error) {
8548
8568
  cleanupErrors.push(`worktree rollback failed: ${toErrorMessage(error)}`);
8549
8569
  }
8550
- try {
8551
- git.deleteBranch(opts.repoRoot, opts.branch, true);
8552
- } catch (error) {
8553
- 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
+ }
8554
8576
  }
8555
8577
  return cleanupErrors.length > 0 ? joinErrorMessages(cleanupErrors) : null;
8556
8578
  }
@@ -8600,6 +8622,7 @@ async function createManagedWorktree(opts, deps = {}) {
8600
8622
  repoRoot: opts.repoRoot,
8601
8623
  worktreePath: opts.worktreePath,
8602
8624
  branch: opts.branch,
8625
+ mode: opts.mode,
8603
8626
  baseBranch: opts.baseBranch
8604
8627
  });
8605
8628
  worktreeCreated = true;
@@ -8679,11 +8702,13 @@ class LifecycleService {
8679
8702
  this.deps = deps;
8680
8703
  }
8681
8704
  async createWorktree(input) {
8682
- const branch = await this.resolveBranch(input.branch, input.prompt);
8683
- 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);
8684
8708
  const { profileName, profile } = this.resolveProfile(input.profile);
8685
8709
  const agent = this.resolveAgent(input.agent);
8686
8710
  const worktreePath = this.resolveWorktreePath(branch);
8711
+ const deleteBranchOnRollback = mode === "new";
8687
8712
  let initialized = null;
8688
8713
  try {
8689
8714
  await this.reportCreateProgress({
@@ -8698,7 +8723,8 @@ class LifecycleService {
8698
8723
  repoRoot: this.deps.projectRoot,
8699
8724
  worktreePath,
8700
8725
  branch,
8701
- baseBranch: this.deps.config.workspace.mainBranch,
8726
+ mode,
8727
+ ...mode === "new" ? { baseBranch: this.deps.config.workspace.mainBranch } : {},
8702
8728
  profile: profileName,
8703
8729
  agent,
8704
8730
  runtime: profile.runtime,
@@ -8706,7 +8732,8 @@ class LifecycleService {
8706
8732
  allocatedPorts: await this.allocatePorts(),
8707
8733
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
8708
8734
  controlUrl: this.controlUrl(),
8709
- controlToken: await this.deps.getControlToken()
8735
+ controlToken: await this.deps.getControlToken(),
8736
+ deleteBranchOnRollback
8710
8737
  }, {
8711
8738
  git: this.deps.git
8712
8739
  });
@@ -8752,7 +8779,8 @@ class LifecycleService {
8752
8779
  agent,
8753
8780
  initialized,
8754
8781
  worktreePath,
8755
- prompt: input.prompt
8782
+ prompt: input.prompt,
8783
+ launchMode: "fresh"
8756
8784
  });
8757
8785
  await this.reportCreateProgress({
8758
8786
  branch,
@@ -8768,7 +8796,7 @@ class LifecycleService {
8768
8796
  };
8769
8797
  } catch (error) {
8770
8798
  if (initialized) {
8771
- const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime);
8799
+ const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime, deleteBranchOnRollback);
8772
8800
  if (cleanupError) {
8773
8801
  throw this.wrapOperationError(new Error(`${toErrorMessage2(error)}; ${cleanupError}`));
8774
8802
  }
@@ -8781,6 +8809,7 @@ class LifecycleService {
8781
8809
  async openWorktree(branch) {
8782
8810
  try {
8783
8811
  const resolved = await this.resolveExistingWorktree(branch);
8812
+ const launchMode = resolved.meta ? "resume" : "fresh";
8784
8813
  const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
8785
8814
  const { profile } = this.resolveProfile(initialized.meta.profile);
8786
8815
  await ensureAgentRuntimeArtifacts({
@@ -8792,7 +8821,8 @@ class LifecycleService {
8792
8821
  profile,
8793
8822
  agent: initialized.meta.agent,
8794
8823
  initialized,
8795
- worktreePath: resolved.entry.path
8824
+ worktreePath: resolved.entry.path,
8825
+ launchMode
8796
8826
  });
8797
8827
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
8798
8828
  return {
@@ -8820,6 +8850,20 @@ class LifecycleService {
8820
8850
  throw this.wrapOperationError(error);
8821
8851
  }
8822
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
+ }
8823
8867
  async mergeWorktree(branch) {
8824
8868
  try {
8825
8869
  const resolved = await this.resolveExistingWorktree(branch);
@@ -8838,9 +8882,17 @@ class LifecycleService {
8838
8882
  throw this.wrapOperationError(error);
8839
8883
  }
8840
8884
  }
8841
- 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) {
8842
8891
  const explicitBranch = rawBranch?.trim();
8843
- 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
+ }
8844
8896
  if (!isValidBranchName(branch)) {
8845
8897
  throw new LifecycleError(`Invalid branch name: ${branch}`, 400);
8846
8898
  }
@@ -8852,10 +8904,19 @@ class LifecycleService {
8852
8904
  }
8853
8905
  return await this.deps.autoName.generateBranchName(this.deps.config.autoName, prompt);
8854
8906
  }
8855
- ensureBranchAvailable(branch) {
8856
- const exists = this.listProjectWorktrees().some((entry) => entry.branch === branch);
8857
- if (exists) {
8858
- 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);
8859
8920
  }
8860
8921
  }
8861
8922
  resolveProfile(profileName) {
@@ -8894,6 +8955,12 @@ class LifecycleService {
8894
8955
  resolveWorktreePath(branch) {
8895
8956
  return resolve3(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
8896
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
+ }
8897
8964
  listProjectWorktrees() {
8898
8965
  const projectRoot = resolve3(this.deps.projectRoot);
8899
8966
  return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve3(entry.path) !== projectRoot);
@@ -8914,6 +8981,14 @@ class LifecycleService {
8914
8981
  const meta = await readWorktreeMeta(gitDir);
8915
8982
  return { entry, gitDir, meta };
8916
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
+ }
8917
8992
  async initializeUnmanagedWorktree(resolved) {
8918
8993
  const { profileName, profile } = this.resolveProfile(undefined);
8919
8994
  const dotenvValues = await loadDotenvLocal(resolved.entry.path);
@@ -8979,6 +9054,7 @@ class LifecycleService {
8979
9054
  initialized: input.initialized,
8980
9055
  worktreePath: input.worktreePath,
8981
9056
  prompt: input.prompt,
9057
+ launchMode: input.launchMode,
8982
9058
  containerName
8983
9059
  }));
8984
9060
  return;
@@ -8989,11 +9065,12 @@ class LifecycleService {
8989
9065
  agent: input.agent,
8990
9066
  initialized: input.initialized,
8991
9067
  worktreePath: input.worktreePath,
8992
- prompt: input.prompt
9068
+ prompt: input.prompt,
9069
+ launchMode: input.launchMode
8993
9070
  }));
8994
9071
  }
8995
9072
  buildSessionLayout(input) {
8996
- 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;
8997
9074
  const containerName = input.containerName;
8998
9075
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
8999
9076
  repoRoot: this.deps.projectRoot,
@@ -9006,7 +9083,8 @@ class LifecycleService {
9006
9083
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
9007
9084
  yolo: input.profile.yolo === true,
9008
9085
  systemPrompt,
9009
- prompt: input.prompt
9086
+ prompt: input.launchMode === "fresh" ? input.prompt : undefined,
9087
+ launchMode: input.launchMode
9010
9088
  }),
9011
9089
  shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
9012
9090
  } : {
@@ -9015,7 +9093,8 @@ class LifecycleService {
9015
9093
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
9016
9094
  yolo: input.profile.yolo === true,
9017
9095
  systemPrompt,
9018
- prompt: input.prompt
9096
+ prompt: input.launchMode === "fresh" ? input.prompt : undefined,
9097
+ launchMode: input.launchMode
9019
9098
  }),
9020
9099
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
9021
9100
  }
@@ -9027,7 +9106,7 @@ class LifecycleService {
9027
9106
  }
9028
9107
  return profile;
9029
9108
  }
9030
- async cleanupFailedCreate(branch, worktreePath, runtime) {
9109
+ async cleanupFailedCreate(branch, worktreePath, runtime, deleteBranch) {
9031
9110
  const cleanupErrors = [];
9032
9111
  if (runtime === "docker") {
9033
9112
  try {
@@ -9047,8 +9126,8 @@ class LifecycleService {
9047
9126
  worktreePath,
9048
9127
  branch,
9049
9128
  force: true,
9050
- deleteBranch: true,
9051
- deleteBranchForce: true
9129
+ deleteBranch,
9130
+ deleteBranchForce: deleteBranch
9052
9131
  }, this.deps.git);
9053
9132
  } catch (error) {
9054
9133
  cleanupErrors.push(`worktree cleanup failed: ${toErrorMessage2(error)}`);
@@ -10743,6 +10822,11 @@ async function apiRuntimeEvent(req) {
10743
10822
  ...notification ? { notification } : {}
10744
10823
  });
10745
10824
  }
10825
+ async function apiListBranches() {
10826
+ return jsonResponse({
10827
+ branches: lifecycleService.listAvailableBranches()
10828
+ });
10829
+ }
10746
10830
  async function apiCreateWorktree(req) {
10747
10831
  const raw = await req.json();
10748
10832
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -10764,11 +10848,16 @@ async function apiCreateWorktree(req) {
10764
10848
  const prompt = typeof body.prompt === "string" ? body.prompt : undefined;
10765
10849
  const profile = typeof body.profile === "string" ? body.profile : undefined;
10766
10850
  const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
10851
+ const mode = body.mode;
10852
+ if (mode !== undefined && mode !== "new" && mode !== "existing") {
10853
+ return errorResponse("Invalid worktree create mode", 400);
10854
+ }
10767
10855
  if (branch) {
10768
10856
  ensureBranchNotCreating(branch);
10769
10857
  }
10770
- log.info(`[worktree:add]${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10858
+ log.info(`[worktree:add] mode=${mode ?? "new"}${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10771
10859
  const result = await lifecycleService.createWorktree({
10860
+ mode,
10772
10861
  branch,
10773
10862
  prompt,
10774
10863
  profile,
@@ -10857,6 +10946,9 @@ Bun.serve({
10857
10946
  "/api/config": {
10858
10947
  GET: () => jsonResponse(getFrontendConfig())
10859
10948
  },
10949
+ "/api/branches": {
10950
+ GET: () => catching("GET /api/branches", () => apiListBranches())
10951
+ },
10860
10952
  "/api/project": {
10861
10953
  GET: () => catching("GET /api/project", () => apiGetProject())
10862
10954
  },