webmux 0.10.1 → 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.
@@ -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;
@@ -7399,10 +7399,12 @@ function parseLifecycleHooks(raw) {
7399
7399
  function parseAutoName(raw) {
7400
7400
  if (!isRecord(raw))
7401
7401
  return null;
7402
- if (typeof raw.model !== "string" || !raw.model.trim())
7402
+ const provider = raw.provider;
7403
+ if (provider !== "claude" && provider !== "codex")
7403
7404
  return null;
7404
7405
  return {
7405
- model: raw.model.trim(),
7406
+ provider,
7407
+ ...typeof raw.model === "string" && raw.model.trim() ? { model: raw.model.trim() } : {},
7406
7408
  ...typeof raw.system_prompt === "string" && raw.system_prompt.trim() ? { systemPrompt: raw.system_prompt.trim() } : {}
7407
7409
  };
7408
7410
  }
@@ -7411,7 +7413,8 @@ function parseLinkedRepos(raw) {
7411
7413
  return [];
7412
7414
  return raw.filter(isRecord).filter((entry) => typeof entry.repo === "string").map((entry) => ({
7413
7415
  repo: entry.repo,
7414
- 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() } : {}
7415
7418
  }));
7416
7419
  }
7417
7420
  function isDockerProfile(profile) {
@@ -7451,7 +7454,7 @@ function loadConfig(dir) {
7451
7454
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
7452
7455
  integrations: {
7453
7456
  github: {
7454
- 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) : []
7455
7458
  },
7456
7459
  linear: {
7457
7460
  enabled: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
@@ -8086,9 +8089,10 @@ class BunTmuxGateway {
8086
8089
  }
8087
8090
  ensureSession(sessionName, cwd) {
8088
8091
  const check = runTmux(["has-session", "-t", sessionName]);
8089
- if (check.exitCode === 0)
8090
- return;
8091
- 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}`);
8092
8096
  }
8093
8097
  hasWindow(sessionName, windowName) {
8094
8098
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -8682,6 +8686,13 @@ class LifecycleService {
8682
8686
  const worktreePath = this.resolveWorktreePath(branch);
8683
8687
  let initialized = null;
8684
8688
  try {
8689
+ await this.reportCreateProgress({
8690
+ branch,
8691
+ path: worktreePath,
8692
+ profile: profileName,
8693
+ agent,
8694
+ phase: "creating_worktree"
8695
+ });
8685
8696
  await mkdir4(dirname3(worktreePath), { recursive: true });
8686
8697
  initialized = await createManagedWorktree({
8687
8698
  repoRoot: this.deps.projectRoot,
@@ -8699,9 +8710,12 @@ class LifecycleService {
8699
8710
  }, {
8700
8711
  git: this.deps.git
8701
8712
  });
8702
- await ensureAgentRuntimeArtifacts({
8703
- gitDir: initialized.paths.gitDir,
8704
- worktreePath
8713
+ await this.reportCreateProgress({
8714
+ branch,
8715
+ path: worktreePath,
8716
+ profile: profileName,
8717
+ agent,
8718
+ phase: "running_post_create_hook"
8705
8719
  });
8706
8720
  await this.runLifecycleHook({
8707
8721
  name: "postCreate",
@@ -8709,6 +8723,29 @@ class LifecycleService {
8709
8723
  meta: initialized.meta,
8710
8724
  worktreePath
8711
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
+ });
8712
8749
  await this.materializeRuntimeSession({
8713
8750
  branch,
8714
8751
  profile,
@@ -8717,6 +8754,13 @@ class LifecycleService {
8717
8754
  worktreePath,
8718
8755
  prompt: input.prompt
8719
8756
  });
8757
+ await this.reportCreateProgress({
8758
+ branch,
8759
+ path: worktreePath,
8760
+ profile: profileName,
8761
+ agent,
8762
+ phase: "reconciling"
8763
+ });
8720
8764
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
8721
8765
  return {
8722
8766
  branch,
@@ -8730,6 +8774,8 @@ class LifecycleService {
8730
8774
  }
8731
8775
  }
8732
8776
  throw this.wrapOperationError(error);
8777
+ } finally {
8778
+ await this.finishCreateProgress(branch);
8733
8779
  }
8734
8780
  }
8735
8781
  async openWorktree(branch) {
@@ -8889,21 +8935,28 @@ class LifecycleService {
8889
8935
  if (!resolved.meta) {
8890
8936
  throw new Error("Missing managed metadata");
8891
8937
  }
8892
- const dotenvValues = await loadDotenvLocal(resolved.entry.path);
8893
- const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
8894
- 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
8895
8948
  }, dotenvValues);
8896
- await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
8949
+ await writeRuntimeEnv(input.gitDir, runtimeEnv);
8897
8950
  const controlEnv = buildControlEnvMap({
8898
8951
  controlUrl: this.controlUrl(),
8899
8952
  controlToken: await this.deps.getControlToken(),
8900
- worktreeId: resolved.meta.worktreeId,
8901
- branch: resolved.meta.branch
8953
+ worktreeId: input.meta.worktreeId,
8954
+ branch: input.meta.branch
8902
8955
  });
8903
- await writeControlEnv(resolved.gitDir, controlEnv);
8956
+ await writeControlEnv(input.gitDir, controlEnv);
8904
8957
  return {
8905
- meta: resolved.meta,
8906
- paths: getWorktreeStoragePaths(resolved.gitDir),
8958
+ meta: input.meta,
8959
+ paths: getWorktreeStoragePaths(input.gitDir),
8907
8960
  runtimeEnv,
8908
8961
  controlEnv
8909
8962
  };
@@ -9051,6 +9104,12 @@ class LifecycleService {
9051
9104
  });
9052
9105
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
9053
9106
  }
9107
+ async reportCreateProgress(progress) {
9108
+ await this.deps.onCreateProgress?.(progress);
9109
+ }
9110
+ async finishCreateProgress(branch) {
9111
+ await this.deps.onCreateFinished?.(branch);
9112
+ }
9054
9113
  wrapOperationError(error) {
9055
9114
  if (error instanceof LifecycleError) {
9056
9115
  return error;
@@ -9418,7 +9477,12 @@ function clonePrEntry(pr) {
9418
9477
  comments: pr.comments.map((comment) => ({ ...comment }))
9419
9478
  };
9420
9479
  }
9421
- 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) {
9422
9486
  return {
9423
9487
  branch: state.branch,
9424
9488
  path: state.path,
@@ -9428,21 +9492,51 @@ function mapWorktreeSnapshot(state, now, findLinearIssue) {
9428
9492
  mux: state.session.exists,
9429
9493
  dirty: state.git.dirty || state.git.aheadCount > 0,
9430
9494
  paneCount: state.session.paneCount,
9431
- status: state.agent.lifecycle,
9495
+ status: creating ? "creating" : state.agent.lifecycle,
9432
9496
  elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
9433
9497
  services: state.services.map((service) => ({ ...service })),
9434
9498
  prs: state.prs.map((pr) => clonePrEntry(pr)),
9435
- 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)
9436
9519
  };
9437
9520
  }
9438
9521
  function buildProjectSnapshot(input) {
9439
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));
9440
9534
  return {
9441
9535
  project: {
9442
9536
  name: input.projectName,
9443
9537
  mainBranch: input.mainBranch
9444
9538
  },
9445
- worktrees: input.runtime.listWorktrees().map((state) => mapWorktreeSnapshot(state, now, input.findLinearIssue)),
9539
+ worktrees,
9446
9540
  notifications: input.notifications.map((notification) => ({ ...notification }))
9447
9541
  };
9448
9542
  }
@@ -9857,47 +9951,14 @@ class BunPortProbe {
9857
9951
  }
9858
9952
 
9859
9953
  // backend/src/services/auto-name-service.ts
9860
- var BRANCH_NAME_SCHEMA = {
9861
- type: "object",
9862
- properties: {
9863
- branch_name: {
9864
- type: "string",
9865
- description: "A lowercase kebab-case git branch name with no prefix"
9866
- }
9867
- },
9868
- required: ["branch_name"],
9869
- additionalProperties: false
9870
- };
9871
- var GEMINI_BRANCH_NAME_SCHEMA = {
9872
- ...BRANCH_NAME_SCHEMA,
9873
- propertyOrdering: ["branch_name"]
9874
- };
9954
+ var MAX_BRANCH_LENGTH = 40;
9875
9955
  var DEFAULT_SYSTEM_PROMPT = [
9876
9956
  "Generate a concise git branch name from the task description.",
9877
9957
  "Return only the branch name.",
9878
9958
  "Use lowercase kebab-case.",
9959
+ `Maximum ${MAX_BRANCH_LENGTH} characters.`,
9879
9960
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
9880
9961
  ].join(" ");
9881
- function isRecord4(value) {
9882
- return typeof value === "object" && value !== null && !Array.isArray(value);
9883
- }
9884
- function buildPrompt(task) {
9885
- return `Task description:
9886
- ${task.trim()}`;
9887
- }
9888
- function parseBranchNamePayload(raw) {
9889
- if (!isRecord4(raw) || typeof raw.branch_name !== "string") {
9890
- throw new Error("Auto-name response did not include branch_name");
9891
- }
9892
- return raw.branch_name;
9893
- }
9894
- function parseJsonText(text) {
9895
- try {
9896
- return JSON.parse(text);
9897
- } catch {
9898
- throw new Error(`Auto-name response was not valid JSON: ${text}`);
9899
- }
9900
- }
9901
9962
  function normalizeGeneratedBranchName(raw) {
9902
9963
  let branch = raw.trim();
9903
9964
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -9909,6 +9970,7 @@ function normalizeGeneratedBranchName(raw) {
9909
9970
  branch = branch.replace(/[/.]+/g, "-");
9910
9971
  branch = branch.replace(/-+/g, "-");
9911
9972
  branch = branch.replace(/^-+|-+$/g, "");
9973
+ branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
9912
9974
  if (!branch) {
9913
9975
  throw new Error("Auto-name model returned an empty branch name");
9914
9976
  }
@@ -9917,231 +9979,87 @@ function normalizeGeneratedBranchName(raw) {
9917
9979
  }
9918
9980
  return branch;
9919
9981
  }
9920
- function resolveAutoNameModel(modelSpec) {
9921
- const trimmed = modelSpec.trim();
9922
- const slashIndex = trimmed.indexOf("/");
9923
- if (slashIndex > 0) {
9924
- const provider = trimmed.slice(0, slashIndex);
9925
- const model = trimmed.slice(slashIndex + 1).trim().replace(/^models\//, "");
9926
- if (!model) {
9927
- throw new Error(`Invalid auto_name model: ${modelSpec}`);
9928
- }
9929
- if (provider === "anthropic" || provider === "google" || provider === "openai") {
9930
- return { provider, model };
9931
- }
9932
- if (provider === "gemini") {
9933
- return { provider: "google", model };
9934
- }
9935
- }
9936
- if (trimmed.startsWith("claude-")) {
9937
- return { provider: "anthropic", model: trimmed };
9938
- }
9939
- if (trimmed.startsWith("gemini-") || trimmed.startsWith("models/gemini-")) {
9940
- return { provider: "google", model: trimmed.replace(/^models\//, "") };
9941
- }
9942
- if (/^(gpt-|chatgpt-|o\d)/.test(trimmed)) {
9943
- return { provider: "openai", model: trimmed };
9944
- }
9945
- throw new Error(`Unsupported auto_name model provider for ${modelSpec}. Use an anthropic/, gemini/, google/, or openai/ prefix, or a known model name.`);
9946
- }
9947
9982
  function getSystemPrompt(config) {
9948
9983
  return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
9949
9984
  }
9950
- function extractAnthropicText(raw) {
9951
- if (!isRecord4(raw) || !Array.isArray(raw.content))
9952
- return null;
9953
- for (const item of raw.content) {
9954
- if (!isRecord4(item))
9955
- continue;
9956
- if (item.type === "text" && typeof item.text === "string" && item.text.trim()) {
9957
- return item.text;
9958
- }
9959
- }
9960
- return null;
9985
+ async function defaultSpawn(args) {
9986
+ const proc = Bun.spawn(args, {
9987
+ stdout: "pipe",
9988
+ stderr: "pipe"
9989
+ });
9990
+ const [stdout, stderr, exitCode] = await Promise.all([
9991
+ new Response(proc.stdout).text(),
9992
+ new Response(proc.stderr).text(),
9993
+ proc.exited
9994
+ ]);
9995
+ return { exitCode, stdout, stderr };
9961
9996
  }
9962
- function extractGoogleText(raw) {
9963
- if (!isRecord4(raw) || !Array.isArray(raw.candidates))
9964
- return null;
9965
- for (const candidate of raw.candidates) {
9966
- if (!isRecord4(candidate) || !isRecord4(candidate.content) || !Array.isArray(candidate.content.parts))
9967
- continue;
9968
- for (const part of candidate.content.parts) {
9969
- if (isRecord4(part) && typeof part.text === "string" && part.text.trim()) {
9970
- return part.text;
9971
- }
9972
- }
9997
+ function buildClaudeArgs(model, systemPrompt, prompt) {
9998
+ const args = [
9999
+ "claude",
10000
+ "-p",
10001
+ "--system-prompt",
10002
+ systemPrompt,
10003
+ "--output-format",
10004
+ "text",
10005
+ "--no-session-persistence"
10006
+ ];
10007
+ if (model) {
10008
+ args.push("--model", model);
9973
10009
  }
9974
- return null;
10010
+ args.push(prompt);
10011
+ return args;
9975
10012
  }
9976
- function extractOpenAiText(raw) {
9977
- if (!isRecord4(raw))
9978
- return null;
9979
- if (typeof raw.output_text === "string" && raw.output_text.trim()) {
9980
- return raw.output_text;
9981
- }
9982
- if (!Array.isArray(raw.output))
9983
- return null;
9984
- for (const item of raw.output) {
9985
- if (!isRecord4(item) || !Array.isArray(item.content))
9986
- continue;
9987
- for (const content of item.content) {
9988
- if (!isRecord4(content))
9989
- continue;
9990
- if (typeof content.text === "string" && content.text.trim()) {
9991
- return content.text;
9992
- }
9993
- }
9994
- }
9995
- return null;
10013
+ function escapeTomlString(s) {
10014
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
9996
10015
  }
9997
- async function readErrorBody(response) {
9998
- const text = (await response.text()).trim();
9999
- return text || `HTTP ${response.status}`;
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
+ }
10019
+ function buildCodexArgs(model, systemPrompt, prompt) {
10020
+ const args = [
10021
+ "codex",
10022
+ "-c",
10023
+ `developer_instructions="${escapeTomlString(systemPrompt)}"`,
10024
+ "exec",
10025
+ "--ephemeral"
10026
+ ];
10027
+ if (model) {
10028
+ args.push("-m", model);
10029
+ }
10030
+ args.push(prompt);
10031
+ return args;
10000
10032
  }
10001
10033
 
10002
10034
  class AutoNameService {
10003
- fetchImpl;
10004
- anthropicApiKey;
10005
- geminiApiKey;
10006
- openaiApiKey;
10035
+ spawnImpl;
10007
10036
  constructor(deps = {}) {
10008
- this.fetchImpl = deps.fetchImpl ?? fetch;
10009
- this.anthropicApiKey = deps.anthropicApiKey ?? Bun.env.ANTHROPIC_API_KEY;
10010
- this.geminiApiKey = deps.geminiApiKey ?? Bun.env.GEMINI_API_KEY;
10011
- this.openaiApiKey = deps.openaiApiKey ?? Bun.env.OPENAI_API_KEY;
10037
+ this.spawnImpl = deps.spawnImpl ?? defaultSpawn;
10012
10038
  }
10013
10039
  async generateBranchName(config, task) {
10014
10040
  const prompt = task.trim();
10015
10041
  if (!prompt) {
10016
10042
  throw new Error("Auto-name requires a prompt");
10017
10043
  }
10018
- const resolved = resolveAutoNameModel(config.model);
10019
- const branchName = resolved.provider === "anthropic" ? await this.generateWithAnthropic(resolved.model, getSystemPrompt(config), prompt) : resolved.provider === "google" ? await this.generateWithGoogle(resolved.model, getSystemPrompt(config), prompt) : await this.generateWithOpenAI(resolved.model, getSystemPrompt(config), prompt);
10020
- return normalizeGeneratedBranchName(branchName);
10021
- }
10022
- async generateWithAnthropic(model, systemPrompt, task) {
10023
- if (!this.anthropicApiKey) {
10024
- throw new Error("ANTHROPIC_API_KEY is required for auto_name with Anthropic models");
10025
- }
10026
- const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
10027
- method: "POST",
10028
- headers: {
10029
- "content-type": "application/json",
10030
- "x-api-key": this.anthropicApiKey,
10031
- "anthropic-version": "2023-06-01"
10032
- },
10033
- body: JSON.stringify({
10034
- model,
10035
- system: systemPrompt,
10036
- max_tokens: 64,
10037
- messages: [{ role: "user", content: buildPrompt(task) }],
10038
- output_config: {
10039
- format: {
10040
- type: "json_schema",
10041
- schema: BRANCH_NAME_SCHEMA
10042
- }
10043
- }
10044
- })
10045
- });
10046
- if (!response.ok) {
10047
- throw new Error(`Anthropic auto-name request failed: ${await readErrorBody(response)}`);
10048
- }
10049
- const json = await response.json();
10050
- if (isRecord4(json) && json.stop_reason === "refusal") {
10051
- throw new Error("Anthropic auto-name request was refused");
10052
- }
10053
- if (isRecord4(json) && json.stop_reason === "max_tokens") {
10054
- throw new Error("Anthropic auto-name response hit max_tokens before completing");
10055
- }
10056
- const text = extractAnthropicText(json);
10057
- if (!text) {
10058
- throw new Error("Anthropic auto-name response did not include text");
10059
- }
10060
- return parseBranchNamePayload(parseJsonText(text));
10061
- }
10062
- async generateWithGoogle(model, systemPrompt, task) {
10063
- if (!this.geminiApiKey) {
10064
- throw new Error("GEMINI_API_KEY is required for auto_name with Gemini models");
10065
- }
10066
- const response = await this.fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
10067
- method: "POST",
10068
- headers: {
10069
- "content-type": "application/json",
10070
- "x-goog-api-key": this.geminiApiKey
10071
- },
10072
- body: JSON.stringify({
10073
- systemInstruction: {
10074
- parts: [{ text: systemPrompt }]
10075
- },
10076
- contents: [
10077
- {
10078
- role: "user",
10079
- parts: [{ text: buildPrompt(task) }]
10080
- }
10081
- ],
10082
- generationConfig: {
10083
- responseMimeType: "application/json",
10084
- responseJsonSchema: GEMINI_BRANCH_NAME_SCHEMA
10085
- }
10086
- })
10087
- });
10088
- if (!response.ok) {
10089
- throw new Error(`Google auto-name request failed: ${await readErrorBody(response)}`);
10090
- }
10091
- const json = await response.json();
10092
- const text = extractGoogleText(json);
10093
- if (!text) {
10094
- throw new Error("Google auto-name response did not include text");
10095
- }
10096
- return parseBranchNamePayload(parseJsonText(text));
10097
- }
10098
- async generateWithOpenAI(model, systemPrompt, task) {
10099
- if (!this.openaiApiKey) {
10100
- throw new Error("OPENAI_API_KEY is required for auto_name with OpenAI models");
10101
- }
10102
- const response = await this.fetchImpl("https://api.openai.com/v1/responses", {
10103
- method: "POST",
10104
- headers: {
10105
- "content-type": "application/json",
10106
- authorization: `Bearer ${this.openaiApiKey}`
10107
- },
10108
- body: JSON.stringify({
10109
- model,
10110
- input: [
10111
- { role: "system", content: systemPrompt },
10112
- { role: "user", content: buildPrompt(task) }
10113
- ],
10114
- max_output_tokens: 64,
10115
- text: {
10116
- format: {
10117
- type: "json_schema",
10118
- name: "branch_name_response",
10119
- strict: true,
10120
- schema: BRANCH_NAME_SCHEMA
10121
- }
10122
- }
10123
- })
10124
- });
10125
- if (!response.ok) {
10126
- throw new Error(`OpenAI auto-name request failed: ${await readErrorBody(response)}`);
10044
+ const systemPrompt = getSystemPrompt(config);
10045
+ const userPrompt = buildPrompt(prompt);
10046
+ const args = config.provider === "claude" ? buildClaudeArgs(config.model, systemPrompt, userPrompt) : buildCodexArgs(config.model, systemPrompt, userPrompt);
10047
+ const cli = config.provider === "claude" ? "claude" : "codex";
10048
+ let result;
10049
+ try {
10050
+ result = await this.spawnImpl(args);
10051
+ } catch {
10052
+ throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
10127
10053
  }
10128
- const json = await response.json();
10129
- if (isRecord4(json) && Array.isArray(json.output)) {
10130
- for (const item of json.output) {
10131
- if (!isRecord4(item) || !Array.isArray(item.content))
10132
- continue;
10133
- for (const content of item.content) {
10134
- if (isRecord4(content) && content.type === "refusal" && typeof content.refusal === "string") {
10135
- throw new Error(`OpenAI auto-name request was refused: ${content.refusal}`);
10136
- }
10137
- }
10138
- }
10054
+ if (result.exitCode !== 0) {
10055
+ const detail = result.stderr.trim() || `exit ${result.exitCode}`;
10056
+ throw new Error(`${cli} failed: ${detail}`);
10139
10057
  }
10140
- const text = extractOpenAiText(json);
10141
- if (!text) {
10142
- throw new Error("OpenAI auto-name response did not include text");
10058
+ const output = result.stdout.trim();
10059
+ if (!output) {
10060
+ throw new Error(`${cli} returned empty output`);
10143
10061
  }
10144
- return parseBranchNamePayload(parseJsonText(text));
10062
+ return normalizeGeneratedBranchName(output);
10145
10063
  }
10146
10064
  }
10147
10065
 
@@ -10524,9 +10442,33 @@ class ReconciliationService {
10524
10442
  }
10525
10443
  }
10526
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
+
10527
10469
  // backend/src/runtime.ts
10528
10470
  function createWebmuxRuntime(options = {}) {
10529
- const port = options.port ?? parseInt(Bun.env.BACKEND_PORT || "5111", 10);
10471
+ const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
10530
10472
  const projectDir = gitRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
10531
10473
  const config = loadConfig(projectDir);
10532
10474
  const git = new BunGitGateway;
@@ -10536,6 +10478,7 @@ function createWebmuxRuntime(options = {}) {
10536
10478
  const hooks = new BunLifecycleHookRunner;
10537
10479
  const autoName = new AutoNameService;
10538
10480
  const projectRuntime = new ProjectRuntime;
10481
+ const worktreeCreationTracker = new WorktreeCreationTracker;
10539
10482
  const runtimeNotifications = new NotificationService;
10540
10483
  const reconciliationService = new ReconciliationService({
10541
10484
  config,
@@ -10554,7 +10497,13 @@ function createWebmuxRuntime(options = {}) {
10554
10497
  docker,
10555
10498
  reconciliation: reconciliationService,
10556
10499
  hooks,
10557
- autoName
10500
+ autoName,
10501
+ onCreateProgress: (progress) => {
10502
+ worktreeCreationTracker.set(progress);
10503
+ },
10504
+ onCreateFinished: (branch) => {
10505
+ worktreeCreationTracker.clear(branch);
10506
+ }
10558
10507
  });
10559
10508
  return {
10560
10509
  port,
@@ -10567,6 +10516,7 @@ function createWebmuxRuntime(options = {}) {
10567
10516
  hooks,
10568
10517
  autoName,
10569
10518
  projectRuntime,
10519
+ worktreeCreationTracker,
10570
10520
  runtimeNotifications,
10571
10521
  reconciliationService,
10572
10522
  lifecycleService
@@ -10574,7 +10524,7 @@ function createWebmuxRuntime(options = {}) {
10574
10524
  }
10575
10525
 
10576
10526
  // backend/src/server.ts
10577
- var PORT = parseInt(Bun.env.BACKEND_PORT || "5111", 10);
10527
+ var PORT = parseInt(Bun.env.PORT || "5111", 10);
10578
10528
  var STATIC_DIR = Bun.env.WEBMUX_STATIC_DIR || "";
10579
10529
  var runtime = createWebmuxRuntime({
10580
10530
  port: PORT,
@@ -10585,6 +10535,7 @@ var config = runtime.config;
10585
10535
  var git = runtime.git;
10586
10536
  var tmux = runtime.tmux;
10587
10537
  var projectRuntime = runtime.projectRuntime;
10538
+ var worktreeCreationTracker = runtime.worktreeCreationTracker;
10588
10539
  var runtimeNotifications = runtime.runtimeNotifications;
10589
10540
  var reconciliationService = runtime.reconciliationService;
10590
10541
  var removingBranches = new Set;
@@ -10607,7 +10558,11 @@ function getFrontendConfig() {
10607
10558
  })),
10608
10559
  defaultProfileName,
10609
10560
  autoName: config.autoName !== null,
10610
- 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
+ }))
10611
10566
  };
10612
10567
  }
10613
10568
  function parseWsMessage(raw) {
@@ -10665,8 +10620,17 @@ function ensureBranchNotRemoving(branch) {
10665
10620
  throw new LifecycleError(`Worktree is being removed: ${branch}`, 409);
10666
10621
  }
10667
10622
  }
10668
- 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) {
10669
10629
  ensureBranchNotRemoving(branch);
10630
+ ensureBranchNotCreating(branch);
10631
+ }
10632
+ async function withRemovingBranch(branch, fn) {
10633
+ ensureBranchNotBusy(branch);
10670
10634
  removingBranches.add(branch);
10671
10635
  try {
10672
10636
  return await fn();
@@ -10675,7 +10639,7 @@ async function withRemovingBranch(branch, fn) {
10675
10639
  }
10676
10640
  }
10677
10641
  async function resolveTerminalWorktree(branch) {
10678
- ensureBranchNotRemoving(branch);
10642
+ ensureBranchNotBusy(branch);
10679
10643
  await reconciliationService.reconcile(PROJECT_DIR);
10680
10644
  const state = projectRuntime.getWorktreeByBranch(branch);
10681
10645
  if (!state) {
@@ -10738,6 +10702,7 @@ async function apiGetProject() {
10738
10702
  projectName: config.name,
10739
10703
  mainBranch: config.workspace.mainBranch,
10740
10704
  runtime: projectRuntime,
10705
+ creatingWorktrees: worktreeCreationTracker.list(),
10741
10706
  notifications: runtimeNotifications.list(),
10742
10707
  findLinearIssue: (branch) => {
10743
10708
  const match = linearIssues.find((issue) => branchMatchesIssue(branch, issue.branchName));
@@ -10799,6 +10764,9 @@ async function apiCreateWorktree(req) {
10799
10764
  const prompt = typeof body.prompt === "string" ? body.prompt : undefined;
10800
10765
  const profile = typeof body.profile === "string" ? body.profile : undefined;
10801
10766
  const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
10767
+ if (branch) {
10768
+ ensureBranchNotCreating(branch);
10769
+ }
10802
10770
  log.info(`[worktree:add]${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10803
10771
  const result = await lifecycleService.createWorktree({
10804
10772
  branch,
@@ -10819,19 +10787,21 @@ async function apiDeleteWorktree(name) {
10819
10787
  });
10820
10788
  }
10821
10789
  async function apiOpenWorktree(name) {
10822
- ensureBranchNotRemoving(name);
10790
+ ensureBranchNotBusy(name);
10823
10791
  log.info(`[worktree:open] name=${name}`);
10824
10792
  const result = await lifecycleService.openWorktree(name);
10825
10793
  log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
10826
10794
  return jsonResponse({ ok: true });
10827
10795
  }
10828
10796
  async function apiCloseWorktree(name) {
10797
+ ensureBranchNotBusy(name);
10829
10798
  log.info(`[worktree:close] name=${name}`);
10830
10799
  await lifecycleService.closeWorktree(name);
10831
10800
  log.debug(`[worktree:close] done name=${name}`);
10832
10801
  return jsonResponse({ ok: true });
10833
10802
  }
10834
10803
  async function apiSendPrompt(name, req) {
10804
+ ensureBranchNotBusy(name);
10835
10805
  const raw = await req.json();
10836
10806
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
10837
10807
  return errorResponse("Invalid request body", 400);
@@ -10849,6 +10819,7 @@ async function apiSendPrompt(name, req) {
10849
10819
  return jsonResponse({ ok: true });
10850
10820
  }
10851
10821
  async function apiMergeWorktree(name) {
10822
+ ensureBranchNotBusy(name);
10852
10823
  log.info(`[worktree:merge] name=${name}`);
10853
10824
  await lifecycleService.mergeWorktree(name);
10854
10825
  log.debug(`[worktree:merge] done name=${name}`);