webmux 0.39.0 → 0.41.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.
@@ -6959,13 +6959,13 @@ var require_public_api = __commonJS((exports) => {
6959
6959
 
6960
6960
  // backend/src/server.ts
6961
6961
  import { randomUUID as randomUUID4 } from "crypto";
6962
- import { join as join12, resolve as resolve9 } from "path";
6962
+ import { join as join12, resolve as resolve10 } from "path";
6963
6963
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
6964
6964
  import { networkInterfaces } from "os";
6965
6965
  // package.json
6966
6966
  var package_default = {
6967
6967
  name: "webmux",
6968
- version: "0.39.0",
6968
+ version: "0.41.0",
6969
6969
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
6970
6970
  type: "module",
6971
6971
  repository: {
@@ -10994,7 +10994,7 @@ var coerce = {
10994
10994
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
10995
10995
  };
10996
10996
  var NEVER = INVALID;
10997
- // node_modules/.bun/@ts-rest+core@3.52.1+f5aac5c7d31a6dcb/node_modules/@ts-rest/core/index.esm.mjs
10997
+ // node_modules/.bun/@ts-rest+core@3.52.1+38b2c5aebf202339/node_modules/@ts-rest/core/index.esm.mjs
10998
10998
  var isZodObjectStrict = (obj) => {
10999
10999
  return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
11000
11000
  };
@@ -12395,6 +12395,7 @@ import { join } from "path";
12395
12395
  // backend/src/domain/model.ts
12396
12396
  var WORKTREE_META_SCHEMA_VERSION = 1;
12397
12397
  var WORKTREE_ARCHIVE_STATE_VERSION = 1;
12398
+ var OPEN_SESSIONS_STATE_VERSION = 1;
12398
12399
  var ROOT_TAB_ID = "root";
12399
12400
  function conversationSessionId(conversation) {
12400
12401
  if (!conversation)
@@ -12456,6 +12457,9 @@ function getWorktreeStoragePaths(gitDir) {
12456
12457
  function getProjectArchiveStatePath(gitDir) {
12457
12458
  return join(gitDir, "webmux", "archive.json");
12458
12459
  }
12460
+ function getProjectOpenSessionsStatePath(gitDir) {
12461
+ return join(gitDir, "webmux", "open-sessions.json");
12462
+ }
12459
12463
  async function ensureWorktreeStorageDirs(gitDir) {
12460
12464
  const paths = getWorktreeStoragePaths(gitDir);
12461
12465
  await mkdir2(paths.webmuxDir, { recursive: true });
@@ -12505,6 +12509,12 @@ async function writeWorktreeArchiveState(gitDir, state) {
12505
12509
  await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
12506
12510
  `);
12507
12511
  }
12512
+ async function writeOpenSessionsState(gitDir, state) {
12513
+ const statePath = getProjectOpenSessionsStatePath(gitDir);
12514
+ await ensureWorktreeStorageDirs(gitDir);
12515
+ await Bun.write(statePath, JSON.stringify(state, null, 2) + `
12516
+ `);
12517
+ }
12508
12518
  function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
12509
12519
  return {
12510
12520
  ...dotenvValues,
@@ -18304,9 +18314,13 @@ class LifecycleService {
18304
18314
  async pruneWorktrees() {
18305
18315
  try {
18306
18316
  const resolvedWorktrees = await this.resolveAllWorktrees();
18317
+ const sessionName = buildProjectSessionName(this.deps.projectRoot);
18307
18318
  const removedBranches = [];
18308
18319
  for (const resolved of resolvedWorktrees) {
18309
18320
  const branch = resolved.entry.branch ?? resolved.entry.path;
18321
+ if (this.deps.tmux.hasWindow(sessionName, buildWorktreeWindowName(branch))) {
18322
+ continue;
18323
+ }
18310
18324
  await this.removeResolvedWorktree(resolved);
18311
18325
  removedBranches.push(branch);
18312
18326
  }
@@ -19601,6 +19615,58 @@ function startAutoPullMonitor(deps, intervalMs) {
19601
19615
  return startSerializedInterval(run2, intervalMs);
19602
19616
  }
19603
19617
 
19618
+ // backend/src/services/session-restore-service.ts
19619
+ import { basename as basename5, resolve as resolve8 } from "path";
19620
+ function entryBranch(entry) {
19621
+ return entry.branch ?? basename5(entry.path);
19622
+ }
19623
+ function computeOpenBranches(input) {
19624
+ const resolvedProjectDir = resolve8(input.projectDir);
19625
+ const openWindowNames = new Set(input.windows.filter((window) => window.sessionName === input.sessionName).map((window) => window.windowName));
19626
+ return input.worktrees.filter((entry) => !entry.bare && resolve8(entry.path) !== resolvedProjectDir).map((entry) => entryBranch(entry)).filter((branch) => openWindowNames.has(buildWorktreeWindowName(branch))).sort((left, right) => left.localeCompare(right));
19627
+ }
19628
+ function buildOpenSessionsState(branches, savedAt) {
19629
+ return {
19630
+ schemaVersion: OPEN_SESSIONS_STATE_VERSION,
19631
+ savedAt: savedAt.toISOString(),
19632
+ branches
19633
+ };
19634
+ }
19635
+ var DEFAULT_SESSION_SNAPSHOT_INTERVAL_MS = 30000;
19636
+ async function saveOpenSessionsSnapshot(deps) {
19637
+ const projectRoot2 = resolve8(deps.projectRoot);
19638
+ const sessionName = buildProjectSessionName(projectRoot2);
19639
+ let windows = [];
19640
+ try {
19641
+ windows = deps.tmux.listWindows();
19642
+ } catch {
19643
+ windows = [];
19644
+ }
19645
+ const worktrees = deps.git.listLiveWorktrees(projectRoot2);
19646
+ const branches = computeOpenBranches({ worktrees, windows, sessionName, projectDir: projectRoot2 });
19647
+ if (branches.length === 0)
19648
+ return null;
19649
+ const now = deps.now ?? (() => new Date);
19650
+ const writeState = deps.writeState ?? writeOpenSessionsState;
19651
+ const gitDir = deps.git.resolveWorktreeGitDir(projectRoot2);
19652
+ await writeState(gitDir, buildOpenSessionsState(branches, now()));
19653
+ return branches;
19654
+ }
19655
+ function startSessionSnapshotMonitor(deps, intervalMs = DEFAULT_SESSION_SNAPSHOT_INTERVAL_MS) {
19656
+ log.info(`[session-snapshot] monitor started (interval: ${intervalMs}ms)`);
19657
+ const run2 = async () => {
19658
+ try {
19659
+ const branches = await saveOpenSessionsSnapshot(deps);
19660
+ if (branches) {
19661
+ log.debug(`[session-snapshot] saved ${branches.length} open session(s): ${branches.join(", ")}`);
19662
+ }
19663
+ } catch (error) {
19664
+ log.warn(`[session-snapshot] save failed: ${error instanceof Error ? error.message : String(error)}`);
19665
+ }
19666
+ };
19667
+ return startSerializedInterval(run2, intervalMs);
19668
+ }
19669
+
19604
19670
  // backend/src/services/agents-ui-service.ts
19605
19671
  function cloneConversationMeta(meta) {
19606
19672
  return meta ? { ...meta } : null;
@@ -21660,7 +21726,7 @@ class BunPortProbe {
21660
21726
  this.hostnames = hostnames;
21661
21727
  }
21662
21728
  isListening(port) {
21663
- return new Promise((resolve8) => {
21729
+ return new Promise((resolve9) => {
21664
21730
  let settled = false;
21665
21731
  let pending = this.hostnames.length;
21666
21732
  const settle = (result) => {
@@ -21669,20 +21735,20 @@ class BunPortProbe {
21669
21735
  if (result) {
21670
21736
  settled = true;
21671
21737
  clearTimeout(timer);
21672
- resolve8(true);
21738
+ resolve9(true);
21673
21739
  return;
21674
21740
  }
21675
21741
  pending--;
21676
21742
  if (pending === 0) {
21677
21743
  settled = true;
21678
21744
  clearTimeout(timer);
21679
- resolve8(false);
21745
+ resolve9(false);
21680
21746
  }
21681
21747
  };
21682
21748
  const timer = setTimeout(() => {
21683
21749
  if (!settled) {
21684
21750
  settled = true;
21685
- resolve8(false);
21751
+ resolve9(false);
21686
21752
  }
21687
21753
  }, this.timeoutMs);
21688
21754
  for (const hostname of this.hostnames) {
@@ -21832,8 +21898,8 @@ class ArchiveStateService {
21832
21898
  async withMutationLock(operation) {
21833
21899
  const previous = this.mutationQueue;
21834
21900
  let release = () => {};
21835
- this.mutationQueue = new Promise((resolve8) => {
21836
- release = resolve8;
21901
+ this.mutationQueue = new Promise((resolve9) => {
21902
+ release = resolve9;
21837
21903
  });
21838
21904
  await previous.catch(() => {});
21839
21905
  try {
@@ -22136,9 +22202,9 @@ class ProjectRuntime {
22136
22202
  }
22137
22203
 
22138
22204
  // backend/src/services/reconciliation-service.ts
22139
- import { basename as basename5, resolve as resolve8 } from "path";
22205
+ import { basename as basename6, resolve as resolve9 } from "path";
22140
22206
  function makeUnmanagedWorktreeId(path) {
22141
- return `unmanaged:${resolve8(path)}`;
22207
+ return `unmanaged:${resolve9(path)}`;
22142
22208
  }
22143
22209
  function isValidPort2(port) {
22144
22210
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -22171,7 +22237,7 @@ function findWindow(windows, sessionName, branch) {
22171
22237
  return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
22172
22238
  }
22173
22239
  function resolveBranch(entry, metaBranch) {
22174
- const fallback = basename5(entry.path);
22240
+ const fallback = basename6(entry.path);
22175
22241
  return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
22176
22242
  }
22177
22243
 
@@ -22195,7 +22261,7 @@ class ReconciliationService {
22195
22261
  if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
22196
22262
  return;
22197
22263
  }
22198
- const normalizedRepoRoot = resolve8(repoRoot);
22264
+ const normalizedRepoRoot = resolve9(repoRoot);
22199
22265
  const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
22200
22266
  this.lastReconciledAt = this.now();
22201
22267
  });
@@ -22214,7 +22280,7 @@ class ReconciliationService {
22214
22280
  windows = [];
22215
22281
  }
22216
22282
  const seenWorktreeIds = new Set;
22217
- const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve8(entry.path) !== normalizedRepoRoot);
22283
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve9(entry.path) !== normalizedRepoRoot);
22218
22284
  const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
22219
22285
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
22220
22286
  const meta = await readWorktreeMeta(gitDir);
@@ -22689,6 +22755,7 @@ function createProjectApp(runtime, instancePrefix) {
22689
22755
  let stopPrMonitor = null;
22690
22756
  let stopOneshotWatcher = null;
22691
22757
  let stopAutoPullMonitor = null;
22758
+ let stopSessionSnapshot = null;
22692
22759
  async function runOneshotForIssue(issueId) {
22693
22760
  const seed = await buildSeedFromLinear({ issueId }, defaultSeedFromLinearDeps);
22694
22761
  if (!seed.ok) {
@@ -22798,7 +22865,7 @@ function createProjectApp(runtime, instancePrefix) {
22798
22865
  startupEnvs: config.startupEnvs,
22799
22866
  linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
22800
22867
  alias: lr.alias,
22801
- ...lr.dir ? { dir: resolve9(PROJECT_DIR, lr.dir) } : {}
22868
+ ...lr.dir ? { dir: resolve10(PROJECT_DIR, lr.dir) } : {}
22802
22869
  })),
22803
22870
  linearAutoCreateWorktrees: linearAutoCreateEnabled,
22804
22871
  autoRemoveOnMerge: autoRemoveOnMergeEnabled,
@@ -22966,9 +23033,9 @@ function createProjectApp(runtime, instancePrefix) {
22966
23033
  }
22967
23034
  async function getWorktreeGitDirs() {
22968
23035
  const gitDirs = new Map;
22969
- const projectRoot2 = resolve9(PROJECT_DIR);
23036
+ const projectRoot2 = resolve10(PROJECT_DIR);
22970
23037
  for (const entry of git.listLiveWorktrees(projectRoot2)) {
22971
- if (entry.bare || resolve9(entry.path) === projectRoot2 || !entry.branch)
23038
+ if (entry.bare || resolve10(entry.path) === projectRoot2 || !entry.branch)
22972
23039
  continue;
22973
23040
  gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
22974
23041
  }
@@ -23763,7 +23830,7 @@ ${resolvedPrompt}` : conversationContext;
23763
23830
  return errorResponse(`Unknown linked repo: ${repo}`, 404);
23764
23831
  if (!linkedRepo.dir)
23765
23832
  return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
23766
- const resolvedDir = resolve9(PROJECT_DIR, linkedRepo.dir);
23833
+ const resolvedDir = resolve10(PROJECT_DIR, linkedRepo.dir);
23767
23834
  const repoRoot = git.resolveRepoRoot(resolvedDir);
23768
23835
  if (!repoRoot)
23769
23836
  return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
@@ -23928,7 +23995,7 @@ ${resolvedPrompt}` : conversationContext;
23928
23995
  }
23929
23996
  const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
23930
23997
  const destPath = join12(uploadDir, safeName);
23931
- if (!resolve9(destPath).startsWith(uploadDir + "/")) {
23998
+ if (!resolve10(destPath).startsWith(uploadDir + "/")) {
23932
23999
  return errorResponse("Invalid filename", 400);
23933
24000
  }
23934
24001
  await Bun.write(destPath, entry);
@@ -24383,6 +24450,7 @@ ${resolvedPrompt}` : conversationContext;
24383
24450
  if (config.workspace.autoPull.enabled) {
24384
24451
  stopAutoPullMonitor = startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
24385
24452
  }
24453
+ stopSessionSnapshot = startSessionSnapshotMonitor({ git, tmux, projectRoot: PROJECT_DIR });
24386
24454
  }
24387
24455
  function stopLight() {
24388
24456
  stopPrMonitor?.();
@@ -24392,6 +24460,8 @@ ${resolvedPrompt}` : conversationContext;
24392
24460
  stopOneshotWatcher = null;
24393
24461
  stopAutoPullMonitor?.();
24394
24462
  stopAutoPullMonitor = null;
24463
+ stopSessionSnapshot?.();
24464
+ stopSessionSnapshot = null;
24395
24465
  }
24396
24466
  return { prefix: instancePrefix, routes, wsHandlers, startLight, stopLight };
24397
24467
  }
@@ -24565,8 +24635,8 @@ async function globalFetch(req) {
24565
24635
  if (STATIC_DIR) {
24566
24636
  const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
24567
24637
  const filePath = join12(STATIC_DIR, rawPath);
24568
- const staticRoot = resolve9(STATIC_DIR);
24569
- if (!resolve9(filePath).startsWith(staticRoot + "/")) {
24638
+ const staticRoot = resolve10(STATIC_DIR);
24639
+ if (!resolve10(filePath).startsWith(staticRoot + "/")) {
24570
24640
  return new Response("Forbidden", { status: 403 });
24571
24641
  }
24572
24642
  const file = Bun.file(filePath);