webmux 0.25.0 → 0.27.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.
@@ -6919,7 +6919,7 @@ var require_public_api = __commonJS((exports) => {
6919
6919
 
6920
6920
  // backend/src/server.ts
6921
6921
  import { randomUUID as randomUUID3 } from "crypto";
6922
- import { join as join7, resolve as resolve7 } from "path";
6922
+ import { join as join7, resolve as resolve8 } from "path";
6923
6923
  import { mkdirSync } from "fs";
6924
6924
  import { networkInterfaces } from "os";
6925
6925
 
@@ -7713,6 +7713,43 @@ function hasRecentDashboardActivity() {
7713
7713
  return Date.now() - lastActivityAt < ACTIVITY_TIMEOUT_MS;
7714
7714
  }
7715
7715
 
7716
+ // backend/src/services/archive-service.ts
7717
+ import { resolve as resolve2 } from "path";
7718
+
7719
+ // backend/src/domain/model.ts
7720
+ var WORKTREE_META_SCHEMA_VERSION = 1;
7721
+ var WORKTREE_ARCHIVE_STATE_VERSION = 1;
7722
+
7723
+ // backend/src/services/archive-service.ts
7724
+ function createArchiveState(entries) {
7725
+ return {
7726
+ schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
7727
+ entries: [...entries].sort((left, right) => left.path.localeCompare(right.path))
7728
+ };
7729
+ }
7730
+ function normalizeArchivePath(path) {
7731
+ return resolve2(path);
7732
+ }
7733
+ function buildArchivedWorktreePathSet(state) {
7734
+ return new Set(state.entries.map((entry) => normalizeArchivePath(entry.path)));
7735
+ }
7736
+ function setArchivedWorktreeState(input) {
7737
+ const normalizedPath = normalizeArchivePath(input.path);
7738
+ const entries = input.state.entries.filter((entry) => normalizeArchivePath(entry.path) !== normalizedPath);
7739
+ if (!input.archived) {
7740
+ return createArchiveState(entries);
7741
+ }
7742
+ entries.push({
7743
+ path: normalizedPath,
7744
+ archivedAt: (input.now ?? (() => new Date))().toISOString()
7745
+ });
7746
+ return createArchiveState(entries);
7747
+ }
7748
+ function pruneArchivedWorktreeState(input) {
7749
+ const validPaths = new Set(input.paths.map((path) => normalizeArchivePath(path)));
7750
+ return createArchiveState(input.state.entries.filter((entry) => validPaths.has(normalizeArchivePath(entry.path))));
7751
+ }
7752
+
7716
7753
  // backend/src/services/linear-service.ts
7717
7754
  var ASSIGNED_ISSUES_QUERY = `
7718
7755
  query AssignedIssues {
@@ -8041,9 +8078,8 @@ async function createLinearIssue(input) {
8041
8078
  }
8042
8079
 
8043
8080
  // backend/src/services/lifecycle-service.ts
8044
- import { randomUUID as randomUUID2 } from "crypto";
8045
8081
  import { mkdir as mkdir4 } from "fs/promises";
8046
- import { dirname as dirname4, resolve as resolve5 } from "path";
8082
+ import { dirname as dirname4, resolve as resolve6 } from "path";
8047
8083
 
8048
8084
  // backend/src/adapters/agent-runtime.ts
8049
8085
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
@@ -8102,6 +8138,9 @@ function getWorktreeStoragePaths(gitDir) {
8102
8138
  prsPath: join2(webmuxDir, "prs.json")
8103
8139
  };
8104
8140
  }
8141
+ function getProjectArchiveStatePath(gitDir) {
8142
+ return join2(gitDir, "webmux", "archive.json");
8143
+ }
8105
8144
  async function ensureWorktreeStorageDirs(gitDir) {
8106
8145
  const paths = getWorktreeStoragePaths(gitDir);
8107
8146
  await mkdir2(paths.webmuxDir, { recursive: true });
@@ -8120,6 +8159,36 @@ async function writeWorktreeMeta(gitDir, meta) {
8120
8159
  await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
8121
8160
  `);
8122
8161
  }
8162
+ function isArchivedWorktreeEntry(raw) {
8163
+ return isRecord2(raw) && typeof raw.path === "string" && typeof raw.archivedAt === "string";
8164
+ }
8165
+ function emptyWorktreeArchiveState() {
8166
+ return {
8167
+ schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
8168
+ entries: []
8169
+ };
8170
+ }
8171
+ function isWorktreeArchiveState(raw) {
8172
+ return isRecord2(raw) && typeof raw.schemaVersion === "number" && Array.isArray(raw.entries) && raw.entries.every((entry) => isArchivedWorktreeEntry(entry));
8173
+ }
8174
+ async function readWorktreeArchiveState(gitDir) {
8175
+ const archivePath = getProjectArchiveStatePath(gitDir);
8176
+ try {
8177
+ const raw = await Bun.file(archivePath).json();
8178
+ return isWorktreeArchiveState(raw) ? {
8179
+ schemaVersion: raw.schemaVersion,
8180
+ entries: raw.entries.map((entry) => ({ ...entry }))
8181
+ } : emptyWorktreeArchiveState();
8182
+ } catch {
8183
+ return emptyWorktreeArchiveState();
8184
+ }
8185
+ }
8186
+ async function writeWorktreeArchiveState(gitDir, state) {
8187
+ const archivePath = getProjectArchiveStatePath(gitDir);
8188
+ await ensureWorktreeStorageDirs(gitDir);
8189
+ await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
8190
+ `);
8191
+ }
8123
8192
  function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
8124
8193
  return {
8125
8194
  ...dotenvValues,
@@ -8469,7 +8538,7 @@ async function ensureAgentRuntimeArtifacts(input) {
8469
8538
 
8470
8539
  // backend/src/adapters/tmux.ts
8471
8540
  import { createHash } from "crypto";
8472
- import { basename, resolve as resolve2 } from "path";
8541
+ import { basename, resolve as resolve3 } from "path";
8473
8542
  function runTmux(args) {
8474
8543
  const result = Bun.spawnSync(["tmux", ...args], {
8475
8544
  stdout: "pipe",
@@ -8497,7 +8566,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
8497
8566
  return trimmed || "x";
8498
8567
  }
8499
8568
  function buildProjectSessionName(projectRoot2) {
8500
- const resolved = resolve2(projectRoot2);
8569
+ const resolved = resolve3(projectRoot2);
8501
8570
  const base = sanitizeTmuxNameSegment(basename(resolved), 18);
8502
8571
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
8503
8572
  return `wm-${base}-${hash}`;
@@ -8666,7 +8735,7 @@ function buildDockerAgentPaneCommand(input) {
8666
8735
  }
8667
8736
 
8668
8737
  // backend/src/services/session-service.ts
8669
- import { resolve as resolve3 } from "path";
8738
+ import { resolve as resolve4 } from "path";
8670
8739
  function quoteShell2(value) {
8671
8740
  return `'${value.replaceAll("'", "'\\''")}'`;
8672
8741
  }
@@ -8680,7 +8749,7 @@ function buildCommandPaneStartupCommand(template, ctx) {
8680
8749
  if (!template.workingDir) {
8681
8750
  return template.command;
8682
8751
  }
8683
- const workingDir = resolve3(resolvePaneCwd(template, ctx), template.workingDir);
8752
+ const workingDir = resolve4(resolvePaneCwd(template, ctx), template.workingDir);
8684
8753
  return `cd -- ${quoteShell2(workingDir)} && ${template.command}`;
8685
8754
  }
8686
8755
  function resolvePaneStartupCommand(template, ctx) {
@@ -8760,7 +8829,7 @@ import { randomUUID } from "crypto";
8760
8829
 
8761
8830
  // backend/src/adapters/git.ts
8762
8831
  import { readdirSync, rmSync, statSync } from "fs";
8763
- import { resolve as resolve4, join as join4 } from "path";
8832
+ import { resolve as resolve5, join as join4 } from "path";
8764
8833
  function runGit(args, cwd) {
8765
8834
  const result = Bun.spawnSync(["git", ...args], {
8766
8835
  cwd,
@@ -8794,8 +8863,8 @@ function errorMessage(error) {
8794
8863
  return error instanceof Error ? error.message : String(error);
8795
8864
  }
8796
8865
  function isRegisteredWorktree(entries, worktreePath) {
8797
- const resolvedPath = resolve4(worktreePath);
8798
- return entries.some((entry) => resolve4(entry.path) === resolvedPath);
8866
+ const resolvedPath = resolve5(worktreePath);
8867
+ return entries.some((entry) => resolve5(entry.path) === resolvedPath);
8799
8868
  }
8800
8869
  function removeDirectory(path) {
8801
8870
  rmSync(path, {
@@ -8819,7 +8888,7 @@ function currentCheckoutRef(cwd) {
8819
8888
  function resolveRepoRoot(dir) {
8820
8889
  const direct = tryRunGit(["rev-parse", "--show-toplevel"], dir);
8821
8890
  if (direct.ok)
8822
- return resolve4(dir, direct.stdout);
8891
+ return resolve5(dir, direct.stdout);
8823
8892
  let entries;
8824
8893
  try {
8825
8894
  entries = readdirSync(dir);
@@ -8836,17 +8905,17 @@ function resolveRepoRoot(dir) {
8836
8905
  }
8837
8906
  const childResult = tryRunGit(["rev-parse", "--show-toplevel"], child);
8838
8907
  if (childResult.ok)
8839
- return resolve4(child, childResult.stdout);
8908
+ return resolve5(child, childResult.stdout);
8840
8909
  }
8841
8910
  return null;
8842
8911
  }
8843
8912
  function resolveWorktreeRoot(cwd) {
8844
8913
  const output = runGit(["rev-parse", "--show-toplevel"], cwd);
8845
- return resolve4(cwd, output);
8914
+ return resolve5(cwd, output);
8846
8915
  }
8847
8916
  function resolveWorktreeGitDir(cwd) {
8848
8917
  const output = runGit(["rev-parse", "--git-dir"], cwd);
8849
- return resolve4(cwd, output);
8918
+ return resolve5(cwd, output);
8850
8919
  }
8851
8920
  function parseGitWorktreePorcelain(output) {
8852
8921
  const entries = [];
@@ -9057,9 +9126,6 @@ class BunGitGateway {
9057
9126
  }
9058
9127
  }
9059
9128
 
9060
- // backend/src/domain/model.ts
9061
- var WORKTREE_META_SCHEMA_VERSION = 1;
9062
-
9063
9129
  // backend/src/services/worktree-service.ts
9064
9130
  function toErrorMessage(error) {
9065
9131
  return error instanceof Error ? error.message : String(error);
@@ -9204,10 +9270,13 @@ function mergeManagedWorktree(opts, git = new BunGitGateway) {
9204
9270
  });
9205
9271
  }
9206
9272
 
9207
- // backend/src/services/lifecycle-service.ts
9208
- function generateBranchName() {
9273
+ // backend/src/lib/branch-name.ts
9274
+ import { randomUUID as randomUUID2 } from "crypto";
9275
+ function generateFallbackBranchName() {
9209
9276
  return `change-${randomUUID2().slice(0, 8)}`;
9210
9277
  }
9278
+
9279
+ // backend/src/services/lifecycle-service.ts
9211
9280
  function toErrorMessage2(error) {
9212
9281
  return error instanceof Error ? error.message : String(error);
9213
9282
  }
@@ -9311,9 +9380,8 @@ class LifecycleService {
9311
9380
  }
9312
9381
  async closeWorktree(branch) {
9313
9382
  try {
9314
- const resolved = await this.resolveExistingWorktree(branch);
9315
- this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
9316
- await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
9383
+ await this.resolveExistingWorktree(branch);
9384
+ await this.closeBranchWindow(branch);
9317
9385
  } catch (error) {
9318
9386
  throw this.wrapOperationError(error);
9319
9387
  }
@@ -9358,6 +9426,17 @@ class LifecycleService {
9358
9426
  throw this.wrapOperationError(error);
9359
9427
  }
9360
9428
  }
9429
+ async setWorktreeArchived(branch, archived) {
9430
+ try {
9431
+ const resolved = await this.resolveExistingWorktree(branch);
9432
+ if (archived) {
9433
+ await this.closeBranchWindow(branch);
9434
+ }
9435
+ await this.updateWorktreeArchivedState(resolved.entry.path, archived);
9436
+ } catch (error) {
9437
+ throw this.wrapOperationError(error);
9438
+ }
9439
+ }
9361
9440
  listAvailableBranches(options = {}) {
9362
9441
  const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
9363
9442
  const remoteBranches = options.includeRemote ? this.listRemoteBranches().filter((branch) => isValidBranchName(branch)) : [];
@@ -9370,7 +9449,7 @@ class LifecycleService {
9370
9449
  }
9371
9450
  async resolveBranch(rawBranch, prompt, mode) {
9372
9451
  const explicitBranch = rawBranch?.trim();
9373
- const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) || generateBranchName();
9452
+ const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) || generateFallbackBranchName();
9374
9453
  if (!branch) {
9375
9454
  throw new LifecycleError("Existing branch is required", 400);
9376
9455
  }
@@ -9442,20 +9521,20 @@ class LifecycleService {
9442
9521
  return allocateServicePorts(metas, this.deps.config.services);
9443
9522
  }
9444
9523
  resolveWorktreePath(branch) {
9445
- return resolve5(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
9524
+ return resolve6(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
9446
9525
  }
9447
9526
  listLocalBranches() {
9448
- return this.deps.git.listLocalBranches(resolve5(this.deps.projectRoot));
9527
+ return this.deps.git.listLocalBranches(resolve6(this.deps.projectRoot));
9449
9528
  }
9450
9529
  listRemoteBranches() {
9451
- return this.deps.git.listRemoteBranches(resolve5(this.deps.projectRoot));
9530
+ return this.deps.git.listRemoteBranches(resolve6(this.deps.projectRoot));
9452
9531
  }
9453
9532
  listCheckedOutBranches() {
9454
- return new Set(this.deps.git.listWorktrees(resolve5(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
9533
+ return new Set(this.deps.git.listWorktrees(resolve6(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
9455
9534
  }
9456
9535
  listProjectWorktrees() {
9457
- const projectRoot2 = resolve5(this.deps.projectRoot);
9458
- return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve5(entry.path) !== projectRoot2);
9536
+ const projectRoot2 = resolve6(this.deps.projectRoot);
9537
+ return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve6(entry.path) !== projectRoot2);
9459
9538
  }
9460
9539
  async readManagedMetas() {
9461
9540
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -9528,6 +9607,13 @@ class LifecycleService {
9528
9607
  controlEnv
9529
9608
  };
9530
9609
  }
9610
+ async updateWorktreeArchivedState(path, archived) {
9611
+ await this.deps.archiveState.setArchived(path, archived);
9612
+ }
9613
+ async closeBranchWindow(branch) {
9614
+ this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
9615
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
9616
+ }
9531
9617
  async materializeRuntimeSession(input) {
9532
9618
  if (input.profile.runtime === "docker") {
9533
9619
  const dockerProfile = this.requireDockerProfile(input.profile);
@@ -9655,6 +9741,7 @@ class LifecycleService {
9655
9741
  deleteBranch: true,
9656
9742
  deleteBranchForce: true
9657
9743
  }, this.deps.git);
9744
+ await this.updateWorktreeArchivedState(resolved.entry.path, false);
9658
9745
  await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
9659
9746
  }
9660
9747
  async runLifecycleHook(input) {
@@ -10440,12 +10527,13 @@ function mapCreationSnapshot(creating) {
10440
10527
  phase: creating.phase
10441
10528
  } : null;
10442
10529
  }
10443
- function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
10530
+ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue) {
10444
10531
  return {
10445
10532
  branch: state.branch,
10446
10533
  ...state.baseBranch ? { baseBranch: state.baseBranch } : {},
10447
10534
  path: state.path,
10448
10535
  dir: state.path,
10536
+ archived: isArchived(state.path),
10449
10537
  profile: state.profile,
10450
10538
  agentName: state.agentName,
10451
10539
  mux: state.session.exists,
@@ -10460,12 +10548,13 @@ function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
10460
10548
  creation: mapCreationSnapshot(creating)
10461
10549
  };
10462
10550
  }
10463
- function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
10551
+ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
10464
10552
  return {
10465
10553
  branch: creating.branch,
10466
10554
  ...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
10467
10555
  path: creating.path,
10468
10556
  dir: creating.path,
10557
+ archived: isArchived(creating.path),
10469
10558
  profile: creating.profile,
10470
10559
  agentName: creating.agentName,
10471
10560
  mux: false,
@@ -10482,14 +10571,15 @@ function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
10482
10571
  }
10483
10572
  function buildProjectSnapshot(input) {
10484
10573
  const now = input.now ?? (() => new Date);
10574
+ const isArchived = input.isArchived ?? (() => false);
10485
10575
  const creatingWorktrees = input.creatingWorktrees ?? [];
10486
10576
  const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
10487
10577
  const runtimeWorktrees = input.runtime.listWorktrees();
10488
10578
  const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
10489
- const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, input.findLinearIssue));
10579
+ const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue));
10490
10580
  for (const creating of creatingWorktrees) {
10491
10581
  if (!runtimeBranches.has(creating.branch)) {
10492
- worktrees.push(mapCreatingWorktreeSnapshot(creating, input.findLinearIssue));
10582
+ worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue));
10493
10583
  }
10494
10584
  }
10495
10585
  worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
@@ -10867,7 +10957,7 @@ class BunPortProbe {
10867
10957
  this.hostnames = hostnames;
10868
10958
  }
10869
10959
  isListening(port) {
10870
- return new Promise((resolve6) => {
10960
+ return new Promise((resolve7) => {
10871
10961
  let settled = false;
10872
10962
  let pending = this.hostnames.length;
10873
10963
  const settle = (result) => {
@@ -10876,20 +10966,20 @@ class BunPortProbe {
10876
10966
  if (result) {
10877
10967
  settled = true;
10878
10968
  clearTimeout(timer);
10879
- resolve6(true);
10969
+ resolve7(true);
10880
10970
  return;
10881
10971
  }
10882
10972
  pending--;
10883
10973
  if (pending === 0) {
10884
10974
  settled = true;
10885
10975
  clearTimeout(timer);
10886
- resolve6(false);
10976
+ resolve7(false);
10887
10977
  }
10888
10978
  };
10889
10979
  const timer = setTimeout(() => {
10890
10980
  if (!settled) {
10891
10981
  settled = true;
10892
- resolve6(false);
10982
+ resolve7(false);
10893
10983
  }
10894
10984
  }, this.timeoutMs);
10895
10985
  for (const hostname of this.hostnames) {
@@ -10915,6 +11005,7 @@ class BunPortProbe {
10915
11005
  // backend/src/services/auto-name-service.ts
10916
11006
  var MAX_BRANCH_LENGTH = 40;
10917
11007
  var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
11008
+ var AUTO_NAME_TIMEOUT_MS = 1e4;
10918
11009
  var DEFAULT_SYSTEM_PROMPT = [
10919
11010
  "Generate a concise git branch name from the task description.",
10920
11011
  "Return only the branch name.",
@@ -10945,17 +11036,52 @@ function normalizeGeneratedBranchName(raw) {
10945
11036
  function getSystemPrompt(config) {
10946
11037
  return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
10947
11038
  }
10948
- async function defaultSpawn(args) {
11039
+
11040
+ class AutoNameTimeoutError extends Error {
11041
+ timeoutMs;
11042
+ constructor(timeoutMs) {
11043
+ super(`Auto-name timed out after ${timeoutMs}ms`);
11044
+ this.timeoutMs = timeoutMs;
11045
+ }
11046
+ }
11047
+ async function defaultSpawn(args, options = {}) {
10949
11048
  const proc = Bun.spawn(args, {
10950
11049
  stdout: "pipe",
10951
11050
  stderr: "pipe"
10952
11051
  });
10953
- const [stdout, stderr, exitCode] = await Promise.all([
11052
+ const resultPromise = Promise.all([
10954
11053
  new Response(proc.stdout).text(),
10955
11054
  new Response(proc.stderr).text(),
10956
11055
  proc.exited
10957
- ]);
10958
- return { exitCode, stdout, stderr };
11056
+ ]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
11057
+ if (options.timeoutMs === undefined) {
11058
+ return await resultPromise;
11059
+ }
11060
+ return await new Promise((resolve7, reject) => {
11061
+ let settled = false;
11062
+ const timeoutId = setTimeout(() => {
11063
+ if (settled)
11064
+ return;
11065
+ settled = true;
11066
+ try {
11067
+ proc.kill("SIGKILL");
11068
+ } catch {}
11069
+ reject(new AutoNameTimeoutError(options.timeoutMs));
11070
+ }, options.timeoutMs);
11071
+ resultPromise.then((result) => {
11072
+ if (settled)
11073
+ return;
11074
+ settled = true;
11075
+ clearTimeout(timeoutId);
11076
+ resolve7(result);
11077
+ }, (error) => {
11078
+ if (settled)
11079
+ return;
11080
+ settled = true;
11081
+ clearTimeout(timeoutId);
11082
+ reject(error);
11083
+ });
11084
+ });
10959
11085
  }
10960
11086
  function buildClaudeArgs(model, systemPrompt, prompt) {
10961
11087
  const args = [
@@ -10997,8 +11123,10 @@ function buildCodexArgs(model, systemPrompt, prompt) {
10997
11123
 
10998
11124
  class AutoNameService {
10999
11125
  spawnImpl;
11126
+ timeoutMs;
11000
11127
  constructor(deps = {}) {
11001
11128
  this.spawnImpl = deps.spawnImpl ?? defaultSpawn;
11129
+ this.timeoutMs = deps.timeoutMs ?? AUTO_NAME_TIMEOUT_MS;
11002
11130
  }
11003
11131
  async generateBranchName(config, task) {
11004
11132
  const prompt = task.trim();
@@ -11011,8 +11139,13 @@ class AutoNameService {
11011
11139
  const cli = config.provider === "claude" ? "claude" : "codex";
11012
11140
  let result;
11013
11141
  try {
11014
- result = await this.spawnImpl(args);
11015
- } catch {
11142
+ result = await this.spawnImpl(args, { timeoutMs: this.timeoutMs });
11143
+ } catch (error) {
11144
+ if (error instanceof AutoNameTimeoutError) {
11145
+ const fallback = generateFallbackBranchName();
11146
+ log.warn(`[auto-name] ${cli} timed out after ${this.timeoutMs}ms; using fallback branch ${fallback}`);
11147
+ return fallback;
11148
+ }
11016
11149
  throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
11017
11150
  }
11018
11151
  if (result.exitCode !== 0) {
@@ -11030,6 +11163,66 @@ class AutoNameService {
11030
11163
  }
11031
11164
  }
11032
11165
 
11166
+ // backend/src/services/archive-state-service.ts
11167
+ function archiveStatesEqual(left, right) {
11168
+ if (left.schemaVersion !== right.schemaVersion)
11169
+ return false;
11170
+ if (left.entries.length !== right.entries.length)
11171
+ return false;
11172
+ return left.entries.every((entry, index) => entry.path === right.entries[index]?.path && entry.archivedAt === right.entries[index]?.archivedAt);
11173
+ }
11174
+
11175
+ class ArchiveStateService {
11176
+ gitDir;
11177
+ mutationQueue = Promise.resolve();
11178
+ readState;
11179
+ writeState;
11180
+ constructor(gitDir, deps = {}) {
11181
+ this.gitDir = gitDir;
11182
+ this.readState = deps.readState ?? readWorktreeArchiveState;
11183
+ this.writeState = deps.writeState ?? writeWorktreeArchiveState;
11184
+ }
11185
+ async read() {
11186
+ return await this.readState(this.gitDir);
11187
+ }
11188
+ async setArchived(path, archived) {
11189
+ return await this.mutate((state) => setArchivedWorktreeState({
11190
+ state,
11191
+ path,
11192
+ archived
11193
+ }));
11194
+ }
11195
+ async prune(paths) {
11196
+ return await this.mutate((state) => pruneArchivedWorktreeState({
11197
+ state,
11198
+ paths
11199
+ }));
11200
+ }
11201
+ async mutate(transform) {
11202
+ return await this.withMutationLock(async () => {
11203
+ const state = await this.readState(this.gitDir);
11204
+ const nextState = await transform(state);
11205
+ if (!archiveStatesEqual(state, nextState)) {
11206
+ await this.writeState(this.gitDir, nextState);
11207
+ }
11208
+ return nextState;
11209
+ });
11210
+ }
11211
+ async withMutationLock(operation) {
11212
+ const previous = this.mutationQueue;
11213
+ let release = () => {};
11214
+ this.mutationQueue = new Promise((resolve7) => {
11215
+ release = resolve7;
11216
+ });
11217
+ await previous.catch(() => {});
11218
+ try {
11219
+ return await operation();
11220
+ } finally {
11221
+ release();
11222
+ }
11223
+ }
11224
+ }
11225
+
11033
11226
  // backend/src/services/notification-service.ts
11034
11227
  function eventToNotificationInput(event) {
11035
11228
  switch (event.type) {
@@ -11294,9 +11487,9 @@ class ProjectRuntime {
11294
11487
  }
11295
11488
 
11296
11489
  // backend/src/services/reconciliation-service.ts
11297
- import { basename as basename2, resolve as resolve6 } from "path";
11490
+ import { basename as basename2, resolve as resolve7 } from "path";
11298
11491
  function makeUnmanagedWorktreeId(path) {
11299
- return `unmanaged:${resolve6(path)}`;
11492
+ return `unmanaged:${resolve7(path)}`;
11300
11493
  }
11301
11494
  function isValidPort2(port) {
11302
11495
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -11353,7 +11546,7 @@ class ReconciliationService {
11353
11546
  if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
11354
11547
  return;
11355
11548
  }
11356
- const normalizedRepoRoot = resolve6(repoRoot);
11549
+ const normalizedRepoRoot = resolve7(repoRoot);
11357
11550
  const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
11358
11551
  this.lastReconciledAt = this.now();
11359
11552
  });
@@ -11372,7 +11565,7 @@ class ReconciliationService {
11372
11565
  windows = [];
11373
11566
  }
11374
11567
  const seenWorktreeIds = new Set;
11375
- const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve6(entry.path) !== normalizedRepoRoot);
11568
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve7(entry.path) !== normalizedRepoRoot);
11376
11569
  const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
11377
11570
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11378
11571
  const meta = await readWorktreeMeta(gitDir);
@@ -11475,6 +11668,7 @@ function createWebmuxRuntime(options = {}) {
11475
11668
  const projectDir = projectRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
11476
11669
  const config = loadConfig(projectDir, { resolvedRoot: true });
11477
11670
  const git = new BunGitGateway;
11671
+ const archiveStateService = new ArchiveStateService(git.resolveWorktreeGitDir(projectDir));
11478
11672
  const portProbe = new BunPortProbe;
11479
11673
  const tmux = new BunTmuxGateway;
11480
11674
  const docker = new BunDockerGateway;
@@ -11495,6 +11689,7 @@ function createWebmuxRuntime(options = {}) {
11495
11689
  controlBaseUrl: `http://127.0.0.1:${port}`,
11496
11690
  getControlToken: loadControlToken,
11497
11691
  config,
11692
+ archiveState: archiveStateService,
11498
11693
  git,
11499
11694
  tmux,
11500
11695
  docker,
@@ -11513,6 +11708,7 @@ function createWebmuxRuntime(options = {}) {
11513
11708
  port,
11514
11709
  projectDir,
11515
11710
  config,
11711
+ archiveStateService,
11516
11712
  git,
11517
11713
  portProbe,
11518
11714
  tmux,
@@ -11537,6 +11733,7 @@ var runtime = createWebmuxRuntime({
11537
11733
  var PROJECT_DIR = runtime.projectDir;
11538
11734
  var config = runtime.config;
11539
11735
  var git = runtime.git;
11736
+ var archiveStateService = runtime.archiveStateService;
11540
11737
  var tmux = runtime.tmux;
11541
11738
  var projectRuntime = runtime.projectRuntime;
11542
11739
  var worktreeCreationTracker = runtime.worktreeCreationTracker;
@@ -11594,7 +11791,7 @@ function getFrontendConfig() {
11594
11791
  startupEnvs: config.startupEnvs,
11595
11792
  linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
11596
11793
  alias: lr.alias,
11597
- ...lr.dir ? { dir: resolve7(PROJECT_DIR, lr.dir) } : {}
11794
+ ...lr.dir ? { dir: resolve8(PROJECT_DIR, lr.dir) } : {}
11598
11795
  })),
11599
11796
  linearAutoCreateWorktrees: linearAutoCreateEnabled,
11600
11797
  autoRemoveOnMerge: autoRemoveOnMergeEnabled,
@@ -11725,9 +11922,9 @@ async function hasValidControlToken(req) {
11725
11922
  }
11726
11923
  async function getWorktreeGitDirs() {
11727
11924
  const gitDirs = new Map;
11728
- const projectRoot2 = resolve7(PROJECT_DIR);
11925
+ const projectRoot2 = resolve8(PROJECT_DIR);
11729
11926
  for (const entry of git.listWorktrees(projectRoot2)) {
11730
- if (entry.bare || resolve7(entry.path) === projectRoot2 || !entry.branch)
11927
+ if (entry.bare || resolve8(entry.path) === projectRoot2 || !entry.branch)
11731
11928
  continue;
11732
11929
  gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
11733
11930
  }
@@ -11749,10 +11946,10 @@ async function apiGetProject() {
11749
11946
  touchDashboardActivity();
11750
11947
  const linearApiKey = Bun.env.LINEAR_API_KEY;
11751
11948
  const linearIssuesPromise = config.integrations.linear.enabled && linearApiKey?.trim() ? fetchAssignedIssues() : Promise.resolve({ ok: true, data: [] });
11752
- const [, linearResult] = await Promise.all([
11753
- reconciliationService.reconcile(PROJECT_DIR),
11754
- linearIssuesPromise
11755
- ]);
11949
+ await reconciliationService.reconcile(PROJECT_DIR);
11950
+ const archiveState = await archiveStateService.prune(projectRuntime.listWorktrees().map((worktree) => worktree.path));
11951
+ const linearResult = await linearIssuesPromise;
11952
+ const archivedPaths = buildArchivedWorktreePathSet(archiveState);
11756
11953
  const linearIssues = linearResult.ok ? linearResult.data : [];
11757
11954
  return jsonResponse(buildProjectSnapshot({
11758
11955
  projectName: config.name,
@@ -11760,6 +11957,7 @@ async function apiGetProject() {
11760
11957
  runtime: projectRuntime,
11761
11958
  creatingWorktrees: worktreeCreationTracker.list(),
11762
11959
  notifications: runtimeNotifications.list(),
11960
+ isArchived: (path) => archivedPaths.has(normalizeArchivePath(path)),
11763
11961
  findLinearIssue: (branch) => {
11764
11962
  const match = linearIssues.find((issue) => branchMatchesIssue(branch, issue.branchName));
11765
11963
  return match ? {
@@ -11937,6 +12135,21 @@ async function apiCloseWorktree(name) {
11937
12135
  log.debug(`[worktree:close] done name=${name}`);
11938
12136
  return jsonResponse({ ok: true });
11939
12137
  }
12138
+ async function apiSetWorktreeArchived(name, req) {
12139
+ ensureBranchNotBusy(name);
12140
+ const raw = await req.json();
12141
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12142
+ return errorResponse("Invalid request body", 400);
12143
+ }
12144
+ const body = raw;
12145
+ if (typeof body.archived !== "boolean") {
12146
+ return errorResponse("Missing boolean 'archived' field", 400);
12147
+ }
12148
+ log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
12149
+ await lifecycleService.setWorktreeArchived(name, body.archived);
12150
+ log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
12151
+ return jsonResponse({ ok: true, archived: body.archived });
12152
+ }
11940
12153
  async function apiSendPrompt(name, req) {
11941
12154
  ensureBranchNotBusy(name);
11942
12155
  const raw = await req.json();
@@ -12009,7 +12222,7 @@ async function apiPullMain(req) {
12009
12222
  return errorResponse(`Unknown linked repo: ${repo}`, 404);
12010
12223
  if (!linkedRepo.dir)
12011
12224
  return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
12012
- const resolvedDir = resolve7(PROJECT_DIR, linkedRepo.dir);
12225
+ const resolvedDir = resolve8(PROJECT_DIR, linkedRepo.dir);
12013
12226
  const repoRoot = git.resolveRepoRoot(resolvedDir);
12014
12227
  if (!repoRoot)
12015
12228
  return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
@@ -12102,7 +12315,7 @@ async function apiUploadFiles(name, req) {
12102
12315
  }
12103
12316
  const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
12104
12317
  const destPath = join7(uploadDir, safeName);
12105
- if (!resolve7(destPath).startsWith(uploadDir + "/")) {
12318
+ if (!resolve8(destPath).startsWith(uploadDir + "/")) {
12106
12319
  return errorResponse("Invalid filename", 400);
12107
12320
  }
12108
12321
  await Bun.write(destPath, entry);
@@ -12169,6 +12382,14 @@ Bun.serve({
12169
12382
  return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
12170
12383
  }
12171
12384
  },
12385
+ "/api/worktrees/:name/archive": {
12386
+ PUT: (req) => {
12387
+ const name = decodeURIComponent(req.params.name);
12388
+ if (!isValidWorktreeName(name))
12389
+ return errorResponse("Invalid worktree name", 400);
12390
+ return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
12391
+ }
12392
+ },
12172
12393
  "/api/worktrees/:name/send": {
12173
12394
  POST: (req) => {
12174
12395
  const name = decodeURIComponent(req.params.name);
@@ -12235,8 +12456,8 @@ Bun.serve({
12235
12456
  const url = new URL(req.url);
12236
12457
  const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
12237
12458
  const filePath = join7(STATIC_DIR, rawPath);
12238
- const staticRoot = resolve7(STATIC_DIR);
12239
- if (!resolve7(filePath).startsWith(staticRoot + "/")) {
12459
+ const staticRoot = resolve8(STATIC_DIR);
12460
+ if (!resolve8(filePath).startsWith(staticRoot + "/")) {
12240
12461
  return new Response("Forbidden", { status: 403 });
12241
12462
  }
12242
12463
  const file = Bun.file(filePath);