webmux 0.11.0 → 0.12.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.
package/README.md CHANGED
@@ -89,7 +89,7 @@ workspace:
89
89
 
90
90
  services:
91
91
  - name: BE
92
- portEnv: BACKEND_PORT
92
+ portEnv: PORT
93
93
  portStart: 5111
94
94
  portStep: 10
95
95
  - name: FE
@@ -124,7 +124,7 @@ profiles:
124
124
  writable: true
125
125
  systemPrompt: >
126
126
  You are running inside a sandboxed container.
127
- Backend port: ${BACKEND_PORT}. Frontend port: ${FRONTEND_PORT}.
127
+ Backend port: ${PORT}. Frontend port: ${FRONTEND_PORT}.
128
128
 
129
129
  linkedRepos:
130
130
  - repo: myorg/related-service
@@ -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,10 @@ 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
+ }
8095
+ assertTmuxOk(["set-option", "-t", sessionName, "pane-base-index", "0"], `set pane-base-index on ${sessionName}`);
8094
8096
  }
8095
8097
  hasWindow(sessionName, windowName) {
8096
8098
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -8684,6 +8686,13 @@ class LifecycleService {
8684
8686
  const worktreePath = this.resolveWorktreePath(branch);
8685
8687
  let initialized = null;
8686
8688
  try {
8689
+ await this.reportCreateProgress({
8690
+ branch,
8691
+ path: worktreePath,
8692
+ profile: profileName,
8693
+ agent,
8694
+ phase: "creating_worktree"
8695
+ });
8687
8696
  await mkdir4(dirname3(worktreePath), { recursive: true });
8688
8697
  initialized = await createManagedWorktree({
8689
8698
  repoRoot: this.deps.projectRoot,
@@ -8701,9 +8710,12 @@ class LifecycleService {
8701
8710
  }, {
8702
8711
  git: this.deps.git
8703
8712
  });
8704
- await ensureAgentRuntimeArtifacts({
8705
- gitDir: initialized.paths.gitDir,
8706
- worktreePath
8713
+ await this.reportCreateProgress({
8714
+ branch,
8715
+ path: worktreePath,
8716
+ profile: profileName,
8717
+ agent,
8718
+ phase: "running_post_create_hook"
8707
8719
  });
8708
8720
  await this.runLifecycleHook({
8709
8721
  name: "postCreate",
@@ -8711,6 +8723,29 @@ class LifecycleService {
8711
8723
  meta: initialized.meta,
8712
8724
  worktreePath
8713
8725
  });
8726
+ initialized = await this.refreshManagedArtifactsFromMeta({
8727
+ gitDir: initialized.paths.gitDir,
8728
+ meta: initialized.meta,
8729
+ worktreePath
8730
+ });
8731
+ await this.reportCreateProgress({
8732
+ branch,
8733
+ path: worktreePath,
8734
+ profile: profileName,
8735
+ agent,
8736
+ phase: "preparing_runtime"
8737
+ });
8738
+ await ensureAgentRuntimeArtifacts({
8739
+ gitDir: initialized.paths.gitDir,
8740
+ worktreePath
8741
+ });
8742
+ await this.reportCreateProgress({
8743
+ branch,
8744
+ path: worktreePath,
8745
+ profile: profileName,
8746
+ agent,
8747
+ phase: "starting_session"
8748
+ });
8714
8749
  await this.materializeRuntimeSession({
8715
8750
  branch,
8716
8751
  profile,
@@ -8719,6 +8754,13 @@ class LifecycleService {
8719
8754
  worktreePath,
8720
8755
  prompt: input.prompt
8721
8756
  });
8757
+ await this.reportCreateProgress({
8758
+ branch,
8759
+ path: worktreePath,
8760
+ profile: profileName,
8761
+ agent,
8762
+ phase: "reconciling"
8763
+ });
8722
8764
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
8723
8765
  return {
8724
8766
  branch,
@@ -8732,6 +8774,8 @@ class LifecycleService {
8732
8774
  }
8733
8775
  }
8734
8776
  throw this.wrapOperationError(error);
8777
+ } finally {
8778
+ await this.finishCreateProgress(branch);
8735
8779
  }
8736
8780
  }
8737
8781
  async openWorktree(branch) {
@@ -8891,21 +8935,28 @@ class LifecycleService {
8891
8935
  if (!resolved.meta) {
8892
8936
  throw new Error("Missing managed metadata");
8893
8937
  }
8894
- const dotenvValues = await loadDotenvLocal(resolved.entry.path);
8895
- const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
8896
- WEBMUX_WORKTREE_PATH: resolved.entry.path
8938
+ return await this.refreshManagedArtifactsFromMeta({
8939
+ gitDir: resolved.gitDir,
8940
+ meta: resolved.meta,
8941
+ worktreePath: resolved.entry.path
8942
+ });
8943
+ }
8944
+ async refreshManagedArtifactsFromMeta(input) {
8945
+ const dotenvValues = await loadDotenvLocal(input.worktreePath);
8946
+ const runtimeEnv = buildRuntimeEnvMap(input.meta, {
8947
+ WEBMUX_WORKTREE_PATH: input.worktreePath
8897
8948
  }, dotenvValues);
8898
- await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
8949
+ await writeRuntimeEnv(input.gitDir, runtimeEnv);
8899
8950
  const controlEnv = buildControlEnvMap({
8900
8951
  controlUrl: this.controlUrl(),
8901
8952
  controlToken: await this.deps.getControlToken(),
8902
- worktreeId: resolved.meta.worktreeId,
8903
- branch: resolved.meta.branch
8953
+ worktreeId: input.meta.worktreeId,
8954
+ branch: input.meta.branch
8904
8955
  });
8905
- await writeControlEnv(resolved.gitDir, controlEnv);
8956
+ await writeControlEnv(input.gitDir, controlEnv);
8906
8957
  return {
8907
- meta: resolved.meta,
8908
- paths: getWorktreeStoragePaths(resolved.gitDir),
8958
+ meta: input.meta,
8959
+ paths: getWorktreeStoragePaths(input.gitDir),
8909
8960
  runtimeEnv,
8910
8961
  controlEnv
8911
8962
  };
@@ -9053,6 +9104,12 @@ class LifecycleService {
9053
9104
  });
9054
9105
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
9055
9106
  }
9107
+ async reportCreateProgress(progress) {
9108
+ await this.deps.onCreateProgress?.(progress);
9109
+ }
9110
+ async finishCreateProgress(branch) {
9111
+ await this.deps.onCreateFinished?.(branch);
9112
+ }
9056
9113
  wrapOperationError(error) {
9057
9114
  if (error instanceof LifecycleError) {
9058
9115
  return error;
@@ -9420,7 +9477,12 @@ function clonePrEntry(pr) {
9420
9477
  comments: pr.comments.map((comment) => ({ ...comment }))
9421
9478
  };
9422
9479
  }
9423
- function mapWorktreeSnapshot(state, now, findLinearIssue) {
9480
+ function mapCreationSnapshot(creating) {
9481
+ return creating ? {
9482
+ phase: creating.phase
9483
+ } : null;
9484
+ }
9485
+ function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
9424
9486
  return {
9425
9487
  branch: state.branch,
9426
9488
  path: state.path,
@@ -9430,21 +9492,51 @@ function mapWorktreeSnapshot(state, now, findLinearIssue) {
9430
9492
  mux: state.session.exists,
9431
9493
  dirty: state.git.dirty || state.git.aheadCount > 0,
9432
9494
  paneCount: state.session.paneCount,
9433
- status: state.agent.lifecycle,
9495
+ status: creating ? "creating" : state.agent.lifecycle,
9434
9496
  elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
9435
9497
  services: state.services.map((service) => ({ ...service })),
9436
9498
  prs: state.prs.map((pr) => clonePrEntry(pr)),
9437
- linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null
9499
+ linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
9500
+ creation: mapCreationSnapshot(creating)
9501
+ };
9502
+ }
9503
+ function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
9504
+ return {
9505
+ branch: creating.branch,
9506
+ path: creating.path,
9507
+ dir: creating.path,
9508
+ profile: creating.profile,
9509
+ agentName: creating.agentName,
9510
+ mux: false,
9511
+ dirty: false,
9512
+ paneCount: 0,
9513
+ status: "creating",
9514
+ elapsed: "",
9515
+ services: [],
9516
+ prs: [],
9517
+ linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
9518
+ creation: mapCreationSnapshot(creating)
9438
9519
  };
9439
9520
  }
9440
9521
  function buildProjectSnapshot(input) {
9441
9522
  const now = input.now ?? (() => new Date);
9523
+ const creatingWorktrees = input.creatingWorktrees ?? [];
9524
+ const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
9525
+ const runtimeWorktrees = input.runtime.listWorktrees();
9526
+ const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
9527
+ const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, input.findLinearIssue));
9528
+ for (const creating of creatingWorktrees) {
9529
+ if (!runtimeBranches.has(creating.branch)) {
9530
+ worktrees.push(mapCreatingWorktreeSnapshot(creating, input.findLinearIssue));
9531
+ }
9532
+ }
9533
+ worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
9442
9534
  return {
9443
9535
  project: {
9444
9536
  name: input.projectName,
9445
9537
  mainBranch: input.mainBranch
9446
9538
  },
9447
- worktrees: input.runtime.listWorktrees().map((state) => mapWorktreeSnapshot(state, now, input.findLinearIssue)),
9539
+ worktrees,
9448
9540
  notifications: input.notifications.map((notification) => ({ ...notification }))
9449
9541
  };
9450
9542
  }
@@ -9859,16 +9951,14 @@ class BunPortProbe {
9859
9951
  }
9860
9952
 
9861
9953
  // backend/src/services/auto-name-service.ts
9954
+ var MAX_BRANCH_LENGTH = 40;
9862
9955
  var DEFAULT_SYSTEM_PROMPT = [
9863
9956
  "Generate a concise git branch name from the task description.",
9864
9957
  "Return only the branch name.",
9865
9958
  "Use lowercase kebab-case.",
9959
+ `Maximum ${MAX_BRANCH_LENGTH} characters.`,
9866
9960
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
9867
9961
  ].join(" ");
9868
- function buildPrompt(task) {
9869
- return `Task description:
9870
- ${task.trim()}`;
9871
- }
9872
9962
  function normalizeGeneratedBranchName(raw) {
9873
9963
  let branch = raw.trim();
9874
9964
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -9880,6 +9970,7 @@ function normalizeGeneratedBranchName(raw) {
9880
9970
  branch = branch.replace(/[/.]+/g, "-");
9881
9971
  branch = branch.replace(/-+/g, "-");
9882
9972
  branch = branch.replace(/^-+|-+$/g, "");
9973
+ branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
9883
9974
  if (!branch) {
9884
9975
  throw new Error("Auto-name model returned an empty branch name");
9885
9976
  }
@@ -9922,6 +10013,9 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
9922
10013
  function escapeTomlString(s) {
9923
10014
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
9924
10015
  }
10016
+ function buildPrompt(prompt) {
10017
+ 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.`;
10018
+ }
9925
10019
  function buildCodexArgs(model, systemPrompt, prompt) {
9926
10020
  const args = [
9927
10021
  "codex",
@@ -10348,9 +10442,33 @@ class ReconciliationService {
10348
10442
  }
10349
10443
  }
10350
10444
 
10445
+ // backend/src/services/worktree-creation-service.ts
10446
+ class WorktreeCreationTracker {
10447
+ worktrees = new Map;
10448
+ set(progress) {
10449
+ const next = {
10450
+ branch: progress.branch,
10451
+ path: progress.path,
10452
+ profile: progress.profile,
10453
+ agentName: progress.agent,
10454
+ phase: progress.phase
10455
+ };
10456
+ this.worktrees.set(progress.branch, next);
10457
+ }
10458
+ clear(branch) {
10459
+ return this.worktrees.delete(branch);
10460
+ }
10461
+ has(branch) {
10462
+ return this.worktrees.has(branch);
10463
+ }
10464
+ list() {
10465
+ return [...this.worktrees.values()].sort((left, right) => left.branch.localeCompare(right.branch)).map((state) => ({ ...state }));
10466
+ }
10467
+ }
10468
+
10351
10469
  // backend/src/runtime.ts
10352
10470
  function createWebmuxRuntime(options = {}) {
10353
- const port = options.port ?? parseInt(Bun.env.BACKEND_PORT || "5111", 10);
10471
+ const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
10354
10472
  const projectDir = gitRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
10355
10473
  const config = loadConfig(projectDir);
10356
10474
  const git = new BunGitGateway;
@@ -10360,6 +10478,7 @@ function createWebmuxRuntime(options = {}) {
10360
10478
  const hooks = new BunLifecycleHookRunner;
10361
10479
  const autoName = new AutoNameService;
10362
10480
  const projectRuntime = new ProjectRuntime;
10481
+ const worktreeCreationTracker = new WorktreeCreationTracker;
10363
10482
  const runtimeNotifications = new NotificationService;
10364
10483
  const reconciliationService = new ReconciliationService({
10365
10484
  config,
@@ -10378,7 +10497,13 @@ function createWebmuxRuntime(options = {}) {
10378
10497
  docker,
10379
10498
  reconciliation: reconciliationService,
10380
10499
  hooks,
10381
- autoName
10500
+ autoName,
10501
+ onCreateProgress: (progress) => {
10502
+ worktreeCreationTracker.set(progress);
10503
+ },
10504
+ onCreateFinished: (branch) => {
10505
+ worktreeCreationTracker.clear(branch);
10506
+ }
10382
10507
  });
10383
10508
  return {
10384
10509
  port,
@@ -10391,6 +10516,7 @@ function createWebmuxRuntime(options = {}) {
10391
10516
  hooks,
10392
10517
  autoName,
10393
10518
  projectRuntime,
10519
+ worktreeCreationTracker,
10394
10520
  runtimeNotifications,
10395
10521
  reconciliationService,
10396
10522
  lifecycleService
@@ -10398,7 +10524,7 @@ function createWebmuxRuntime(options = {}) {
10398
10524
  }
10399
10525
 
10400
10526
  // backend/src/server.ts
10401
- var PORT = parseInt(Bun.env.BACKEND_PORT || "5111", 10);
10527
+ var PORT = parseInt(Bun.env.PORT || "5111", 10);
10402
10528
  var STATIC_DIR = Bun.env.WEBMUX_STATIC_DIR || "";
10403
10529
  var runtime = createWebmuxRuntime({
10404
10530
  port: PORT,
@@ -10409,6 +10535,7 @@ var config = runtime.config;
10409
10535
  var git = runtime.git;
10410
10536
  var tmux = runtime.tmux;
10411
10537
  var projectRuntime = runtime.projectRuntime;
10538
+ var worktreeCreationTracker = runtime.worktreeCreationTracker;
10412
10539
  var runtimeNotifications = runtime.runtimeNotifications;
10413
10540
  var reconciliationService = runtime.reconciliationService;
10414
10541
  var removingBranches = new Set;
@@ -10431,7 +10558,11 @@ function getFrontendConfig() {
10431
10558
  })),
10432
10559
  defaultProfileName,
10433
10560
  autoName: config.autoName !== null,
10434
- startupEnvs: config.startupEnvs
10561
+ startupEnvs: config.startupEnvs,
10562
+ linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
10563
+ alias: lr.alias,
10564
+ ...lr.dir ? { dir: resolve5(PROJECT_DIR, lr.dir) } : {}
10565
+ }))
10435
10566
  };
10436
10567
  }
10437
10568
  function parseWsMessage(raw) {
@@ -10489,8 +10620,17 @@ function ensureBranchNotRemoving(branch) {
10489
10620
  throw new LifecycleError(`Worktree is being removed: ${branch}`, 409);
10490
10621
  }
10491
10622
  }
10492
- async function withRemovingBranch(branch, fn) {
10623
+ function ensureBranchNotCreating(branch) {
10624
+ if (worktreeCreationTracker.has(branch)) {
10625
+ throw new LifecycleError(`Worktree is being created: ${branch}`, 409);
10626
+ }
10627
+ }
10628
+ function ensureBranchNotBusy(branch) {
10493
10629
  ensureBranchNotRemoving(branch);
10630
+ ensureBranchNotCreating(branch);
10631
+ }
10632
+ async function withRemovingBranch(branch, fn) {
10633
+ ensureBranchNotBusy(branch);
10494
10634
  removingBranches.add(branch);
10495
10635
  try {
10496
10636
  return await fn();
@@ -10499,7 +10639,7 @@ async function withRemovingBranch(branch, fn) {
10499
10639
  }
10500
10640
  }
10501
10641
  async function resolveTerminalWorktree(branch) {
10502
- ensureBranchNotRemoving(branch);
10642
+ ensureBranchNotBusy(branch);
10503
10643
  await reconciliationService.reconcile(PROJECT_DIR);
10504
10644
  const state = projectRuntime.getWorktreeByBranch(branch);
10505
10645
  if (!state) {
@@ -10562,6 +10702,7 @@ async function apiGetProject() {
10562
10702
  projectName: config.name,
10563
10703
  mainBranch: config.workspace.mainBranch,
10564
10704
  runtime: projectRuntime,
10705
+ creatingWorktrees: worktreeCreationTracker.list(),
10565
10706
  notifications: runtimeNotifications.list(),
10566
10707
  findLinearIssue: (branch) => {
10567
10708
  const match = linearIssues.find((issue) => branchMatchesIssue(branch, issue.branchName));
@@ -10623,6 +10764,9 @@ async function apiCreateWorktree(req) {
10623
10764
  const prompt = typeof body.prompt === "string" ? body.prompt : undefined;
10624
10765
  const profile = typeof body.profile === "string" ? body.profile : undefined;
10625
10766
  const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
10767
+ if (branch) {
10768
+ ensureBranchNotCreating(branch);
10769
+ }
10626
10770
  log.info(`[worktree:add]${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10627
10771
  const result = await lifecycleService.createWorktree({
10628
10772
  branch,
@@ -10643,19 +10787,21 @@ async function apiDeleteWorktree(name) {
10643
10787
  });
10644
10788
  }
10645
10789
  async function apiOpenWorktree(name) {
10646
- ensureBranchNotRemoving(name);
10790
+ ensureBranchNotBusy(name);
10647
10791
  log.info(`[worktree:open] name=${name}`);
10648
10792
  const result = await lifecycleService.openWorktree(name);
10649
10793
  log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
10650
10794
  return jsonResponse({ ok: true });
10651
10795
  }
10652
10796
  async function apiCloseWorktree(name) {
10797
+ ensureBranchNotBusy(name);
10653
10798
  log.info(`[worktree:close] name=${name}`);
10654
10799
  await lifecycleService.closeWorktree(name);
10655
10800
  log.debug(`[worktree:close] done name=${name}`);
10656
10801
  return jsonResponse({ ok: true });
10657
10802
  }
10658
10803
  async function apiSendPrompt(name, req) {
10804
+ ensureBranchNotBusy(name);
10659
10805
  const raw = await req.json();
10660
10806
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
10661
10807
  return errorResponse("Invalid request body", 400);
@@ -10673,6 +10819,7 @@ async function apiSendPrompt(name, req) {
10673
10819
  return jsonResponse({ ok: true });
10674
10820
  }
10675
10821
  async function apiMergeWorktree(name) {
10822
+ ensureBranchNotBusy(name);
10676
10823
  log.info(`[worktree:merge] name=${name}`);
10677
10824
  await lifecycleService.mergeWorktree(name);
10678
10825
  log.debug(`[worktree:merge] done name=${name}`);