webmux 0.17.0 → 0.19.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.
@@ -6905,6 +6905,7 @@ var require_public_api = __commonJS((exports) => {
6905
6905
  });
6906
6906
 
6907
6907
  // backend/src/server.ts
6908
+ import { randomUUID as randomUUID3 } from "crypto";
6908
6909
  import { join as join6, resolve as resolve6 } from "path";
6909
6910
  import { mkdirSync } from "fs";
6910
6911
  import { networkInterfaces } from "os";
@@ -6997,13 +6998,13 @@ function killTmuxSession(name) {
6997
6998
  }
6998
6999
  }
6999
7000
  }
7000
- async function attach(worktreeId, target, cols, rows, initialPane) {
7001
- log.debug(`[term] attach(${worktreeId}) cols=${cols} rows=${rows} existing=${sessions.has(worktreeId)}`);
7002
- if (sessions.has(worktreeId)) {
7003
- await detach(worktreeId);
7001
+ async function attach(attachId, target, cols, rows, initialPane) {
7002
+ log.debug(`[term] attach(${attachId}) cols=${cols} rows=${rows} existing=${sessions.has(attachId)}`);
7003
+ if (sessions.has(attachId)) {
7004
+ await detach(attachId);
7004
7005
  }
7005
7006
  const gName = groupedName();
7006
- log.debug(`[term] attach(${worktreeId}) ownerSession=${target.ownerSessionName} gName=${gName} window=${target.windowName}`);
7007
+ log.debug(`[term] attach(${attachId}) ownerSession=${target.ownerSessionName} gName=${gName} window=${target.windowName}`);
7007
7008
  killTmuxSession(gName);
7008
7009
  const cmd = buildAttachCmd({
7009
7010
  gName,
@@ -7030,8 +7031,8 @@ async function attach(worktreeId, target, cols, rows, initialPane) {
7030
7031
  onExit: null,
7031
7032
  cancelled: false
7032
7033
  };
7033
- sessions.set(worktreeId, session);
7034
- log.debug(`[term] attach(${worktreeId}) spawned pid=${proc.pid}`);
7034
+ sessions.set(attachId, session);
7035
+ log.debug(`[term] attach(${attachId}) spawned pid=${proc.pid}`);
7035
7036
  (async () => {
7036
7037
  const reader = proc.stdout.getReader();
7037
7038
  try {
@@ -7052,7 +7053,7 @@ async function attach(worktreeId, target, cols, rows, initialPane) {
7052
7053
  }
7053
7054
  } catch (err) {
7054
7055
  if (!session.cancelled) {
7055
- log.error(`[term] stdout reader error(${worktreeId})`, err);
7056
+ log.error(`[term] stdout reader error(${attachId})`, err);
7056
7057
  }
7057
7058
  }
7058
7059
  })();
@@ -7063,55 +7064,55 @@ async function attach(worktreeId, target, cols, rows, initialPane) {
7063
7064
  const { done, value } = await reader.read();
7064
7065
  if (done)
7065
7066
  break;
7066
- log.debug(`[term] stderr(${worktreeId}): ${textDecoder.decode(value).trimEnd()}`);
7067
+ log.debug(`[term] stderr(${attachId}): ${textDecoder.decode(value).trimEnd()}`);
7067
7068
  }
7068
7069
  } catch {}
7069
7070
  })();
7070
7071
  proc.exited.then((exitCode) => {
7071
- log.debug(`[term] proc exited(${worktreeId}) pid=${proc.pid} code=${exitCode}`);
7072
- if (sessions.get(worktreeId) === session) {
7072
+ log.debug(`[term] proc exited(${attachId}) pid=${proc.pid} code=${exitCode}`);
7073
+ if (sessions.get(attachId) === session) {
7073
7074
  session.onExit?.(exitCode);
7074
- sessions.delete(worktreeId);
7075
+ sessions.delete(attachId);
7075
7076
  } else {
7076
- log.debug(`[term] proc exited(${worktreeId}) stale session, skipping cleanup`);
7077
+ log.debug(`[term] proc exited(${attachId}) stale session, skipping cleanup`);
7077
7078
  }
7078
7079
  killTmuxSession(gName);
7079
7080
  });
7080
7081
  }
7081
- async function detach(worktreeId) {
7082
- const session = sessions.get(worktreeId);
7082
+ async function detach(attachId) {
7083
+ const session = sessions.get(attachId);
7083
7084
  if (!session) {
7084
- log.debug(`[term] detach(${worktreeId}) no session found`);
7085
+ log.debug(`[term] detach(${attachId}) no session found`);
7085
7086
  return;
7086
7087
  }
7087
- log.debug(`[term] detach(${worktreeId}) killing pid=${session.proc.pid} tmux=${session.groupedSessionName}`);
7088
+ log.debug(`[term] detach(${attachId}) killing pid=${session.proc.pid} tmux=${session.groupedSessionName}`);
7088
7089
  session.cancelled = true;
7089
7090
  session.proc.kill();
7090
- sessions.delete(worktreeId);
7091
+ sessions.delete(attachId);
7091
7092
  killTmuxSession(session.groupedSessionName);
7092
7093
  }
7093
- function write(worktreeId, data) {
7094
- const session = sessions.get(worktreeId);
7094
+ function write(attachId, data) {
7095
+ const session = sessions.get(attachId);
7095
7096
  if (!session) {
7096
- log.warn(`[term] write(${worktreeId}) NO SESSION - input dropped (${data.length} bytes)`);
7097
+ log.warn(`[term] write(${attachId}) NO SESSION - input dropped (${data.length} bytes)`);
7097
7098
  return;
7098
7099
  }
7099
7100
  try {
7100
7101
  session.proc.stdin.write(textEncoder.encode(data));
7101
7102
  session.proc.stdin.flush();
7102
7103
  } catch (err) {
7103
- log.error(`[term] write(${worktreeId}) stdin closed`, err);
7104
+ log.error(`[term] write(${attachId}) stdin closed`, err);
7104
7105
  }
7105
7106
  }
7106
- async function sendKeys(worktreeId, hexBytes) {
7107
- const session = sessions.get(worktreeId);
7107
+ async function sendKeys(attachId, hexBytes) {
7108
+ const session = sessions.get(attachId);
7108
7109
  if (!session)
7109
7110
  return;
7110
7111
  const windowTarget = `${session.groupedSessionName}:${session.windowName}`;
7111
7112
  await tmuxExec(["tmux", "send-keys", "-t", windowTarget, "-H", ...hexBytes]);
7112
7113
  }
7113
- async function resize(worktreeId, cols, rows) {
7114
- const session = sessions.get(worktreeId);
7114
+ async function resize(attachId, cols, rows) {
7115
+ const session = sessions.get(attachId);
7115
7116
  if (!session)
7116
7117
  return;
7117
7118
  const windowTarget = `${session.groupedSessionName}:${session.windowName}`;
@@ -7119,32 +7120,32 @@ async function resize(worktreeId, cols, rows) {
7119
7120
  if (result.exitCode !== 0)
7120
7121
  log.warn(`[term] resize failed: ${result.stderr}`);
7121
7122
  }
7122
- function getScrollback(worktreeId) {
7123
- return sessions.get(worktreeId)?.scrollback.join("") ?? "";
7123
+ function getScrollback(attachId) {
7124
+ return sessions.get(attachId)?.scrollback.join("") ?? "";
7124
7125
  }
7125
- function setCallbacks(worktreeId, onData, onExit) {
7126
- const session = sessions.get(worktreeId);
7126
+ function setCallbacks(attachId, onData, onExit) {
7127
+ const session = sessions.get(attachId);
7127
7128
  if (session) {
7128
7129
  session.onData = onData;
7129
7130
  session.onExit = onExit;
7130
7131
  }
7131
7132
  }
7132
- async function selectPane(worktreeId, paneIndex) {
7133
- const session = sessions.get(worktreeId);
7133
+ async function selectPane(attachId, paneIndex) {
7134
+ const session = sessions.get(attachId);
7134
7135
  if (!session) {
7135
- log.debug(`[term] selectPane(${worktreeId}) no session found`);
7136
+ log.debug(`[term] selectPane(${attachId}) no session found`);
7136
7137
  return;
7137
7138
  }
7138
7139
  const target = `${session.groupedSessionName}:${session.windowName}.${paneIndex}`;
7139
- log.debug(`[term] selectPane(${worktreeId}) pane=${paneIndex} target=${target}`);
7140
+ log.debug(`[term] selectPane(${attachId}) pane=${paneIndex} target=${target}`);
7140
7141
  const [r1, r2] = await Promise.all([
7141
7142
  tmuxExec(["tmux", "select-pane", "-t", target]),
7142
7143
  tmuxExec(["tmux", "resize-pane", "-Z", "-t", target])
7143
7144
  ]);
7144
- log.debug(`[term] selectPane(${worktreeId}) select=${r1.exitCode} zoom=${r2.exitCode}`);
7145
+ log.debug(`[term] selectPane(${attachId}) select=${r1.exitCode} zoom=${r2.exitCode}`);
7145
7146
  }
7146
- function clearCallbacks(worktreeId) {
7147
- const session = sessions.get(worktreeId);
7147
+ function clearCallbacks(attachId) {
7148
+ const session = sessions.get(attachId);
7148
7149
  if (session) {
7149
7150
  session.onData = null;
7150
7151
  session.onExit = null;
@@ -7268,7 +7269,7 @@ var DEFAULT_CONFIG = {
7268
7269
  startupEnvs: {},
7269
7270
  integrations: {
7270
7271
  github: { linkedRepos: [] },
7271
- linear: { enabled: true }
7272
+ linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
7272
7273
  },
7273
7274
  lifecycleHooks: {},
7274
7275
  autoName: null
@@ -7469,7 +7470,10 @@ function parseProjectConfig(parsed) {
7469
7470
  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) : []
7470
7471
  },
7471
7472
  linear: {
7472
- enabled: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
7473
+ enabled: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled,
7474
+ autoCreateWorktrees: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.autoCreateWorktrees === "boolean" ? parsed.integrations.linear.autoCreateWorktrees : DEFAULT_CONFIG.integrations.linear.autoCreateWorktrees,
7475
+ createTicketOption: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.createTicketOption === "boolean" ? parsed.integrations.linear.createTicketOption : DEFAULT_CONFIG.integrations.linear.createTicketOption,
7476
+ ...isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.teamId === "string" && parsed.integrations.linear.teamId.trim() ? { teamId: parsed.integrations.linear.teamId.trim() } : {}
7473
7477
  }
7474
7478
  },
7475
7479
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
@@ -7479,21 +7483,39 @@ function parseProjectConfig(parsed) {
7479
7483
  function defaultConfig() {
7480
7484
  return parseProjectConfig({});
7481
7485
  }
7486
+ function parseLocalLinearOverlay(parsed) {
7487
+ if (!isRecord(parsed.integrations))
7488
+ return null;
7489
+ const linear = parsed.integrations.linear;
7490
+ if (!isRecord(linear))
7491
+ return null;
7492
+ const overlay = {};
7493
+ if (typeof linear.enabled === "boolean")
7494
+ overlay.enabled = linear.enabled;
7495
+ if (typeof linear.autoCreateWorktrees === "boolean")
7496
+ overlay.autoCreateWorktrees = linear.autoCreateWorktrees;
7497
+ if (typeof linear.createTicketOption === "boolean")
7498
+ overlay.createTicketOption = linear.createTicketOption;
7499
+ if (typeof linear.teamId === "string" && linear.teamId.trim())
7500
+ overlay.teamId = linear.teamId.trim();
7501
+ return Object.keys(overlay).length > 0 ? overlay : null;
7502
+ }
7482
7503
  function loadLocalProjectConfigOverlay(root) {
7483
7504
  try {
7484
7505
  const text = readLocalConfigFile(root).trim();
7485
7506
  if (!text) {
7486
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
7507
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null };
7487
7508
  }
7488
7509
  const parsed = parseConfigDocument(text);
7489
7510
  const ws = isRecord(parsed.workspace) ? parsed.workspace : null;
7490
7511
  return {
7491
7512
  worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
7492
7513
  profiles: parseProfiles(parsed.profiles, false),
7493
- lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks)
7514
+ lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
7515
+ linear: parseLocalLinearOverlay(parsed)
7494
7516
  };
7495
7517
  } catch {
7496
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {} };
7518
+ return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null };
7497
7519
  }
7498
7520
  }
7499
7521
  function mergeHookCommand(projectCommand, localCommand) {
@@ -7544,9 +7566,31 @@ function loadConfig(dir, options = {}) {
7544
7566
  ...cloneProfiles(projectConfig.profiles),
7545
7567
  ...cloneProfiles(localOverlay.profiles)
7546
7568
  },
7547
- lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks)
7569
+ lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks),
7570
+ ...localOverlay.linear ? {
7571
+ integrations: {
7572
+ ...projectConfig.integrations,
7573
+ linear: { ...projectConfig.integrations.linear, ...localOverlay.linear }
7574
+ }
7575
+ } : {}
7548
7576
  };
7549
7577
  }
7578
+ async function persistLocalLinearConfig(dir, changes) {
7579
+ const root = projectRoot(dir);
7580
+ const localPath = join(root, ".webmux.local.yaml");
7581
+ let existing = {};
7582
+ try {
7583
+ const text = readFileSync(localPath, "utf8").trim();
7584
+ if (text)
7585
+ existing = parseConfigDocument(text);
7586
+ } catch {}
7587
+ const integrations = isRecord(existing.integrations) ? { ...existing.integrations } : {};
7588
+ const linear = isRecord(integrations.linear) ? { ...integrations.linear } : {};
7589
+ Object.assign(linear, changes);
7590
+ integrations.linear = linear;
7591
+ existing.integrations = integrations;
7592
+ await Bun.write(localPath, $stringify(existing));
7593
+ }
7550
7594
  function expandTemplate(template, env) {
7551
7595
  return template.replace(/\$\{(\w+)\}/g, (_, key) => env[key] ?? "");
7552
7596
  }
@@ -7601,32 +7645,166 @@ var ASSIGNED_ISSUES_QUERY = `
7601
7645
  }
7602
7646
  }
7603
7647
  `;
7648
+ var VIEWER_QUERY = `
7649
+ query Viewer {
7650
+ viewer {
7651
+ id
7652
+ }
7653
+ }
7654
+ `;
7655
+ var TEAM_STATES_QUERY = `
7656
+ query TeamStates($teamId: String!) {
7657
+ team(id: $teamId) {
7658
+ states {
7659
+ nodes {
7660
+ id
7661
+ name
7662
+ type
7663
+ }
7664
+ }
7665
+ }
7666
+ }
7667
+ `;
7668
+ var ISSUE_CREATE_MUTATION = `
7669
+ mutation IssueCreate($input: IssueCreateInput!) {
7670
+ issueCreate(input: $input) {
7671
+ success
7672
+ issue {
7673
+ id
7674
+ identifier
7675
+ title
7676
+ url
7677
+ branchName
7678
+ }
7679
+ }
7680
+ }
7681
+ `;
7682
+ function gqlErrorMessage(raw) {
7683
+ return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
7684
+ }
7604
7685
  function parseIssuesResponse(raw) {
7605
- if (raw.errors && raw.errors.length > 0) {
7606
- return { ok: false, error: raw.errors.map((e) => e.message).join("; ") };
7686
+ const error = gqlErrorMessage(raw);
7687
+ if (error) {
7688
+ return { ok: false, error };
7607
7689
  }
7608
7690
  if (!raw.data) {
7609
7691
  return { ok: false, error: "No data in response" };
7610
7692
  }
7611
- const nodes = raw.data.viewer.assignedIssues.nodes;
7612
- const issues = nodes.map((n) => ({
7613
- id: n.id,
7614
- identifier: n.identifier,
7615
- title: n.title,
7616
- description: n.description,
7617
- priority: n.priority,
7618
- priorityLabel: n.priorityLabel,
7619
- url: n.url,
7620
- branchName: n.branchName,
7621
- dueDate: n.dueDate,
7622
- updatedAt: n.updatedAt,
7623
- state: n.state,
7624
- team: n.team,
7625
- labels: n.labels.nodes,
7626
- project: n.project?.name ?? null
7693
+ const issues = raw.data.viewer.assignedIssues.nodes.map((node) => ({
7694
+ id: node.id,
7695
+ identifier: node.identifier,
7696
+ title: node.title,
7697
+ description: node.description,
7698
+ priority: node.priority,
7699
+ priorityLabel: node.priorityLabel,
7700
+ url: node.url,
7701
+ branchName: node.branchName,
7702
+ dueDate: node.dueDate,
7703
+ updatedAt: node.updatedAt,
7704
+ state: node.state,
7705
+ team: node.team,
7706
+ labels: node.labels.nodes,
7707
+ project: node.project?.name ?? null
7627
7708
  }));
7628
7709
  return { ok: true, data: issues };
7629
7710
  }
7711
+ function buildLinearIssuesResponse(input) {
7712
+ if (!input.integrationEnabled) {
7713
+ return {
7714
+ ok: true,
7715
+ data: {
7716
+ availability: "disabled",
7717
+ issues: []
7718
+ }
7719
+ };
7720
+ }
7721
+ if (!input.apiKey?.trim()) {
7722
+ return {
7723
+ ok: true,
7724
+ data: {
7725
+ availability: "missing_api_key",
7726
+ issues: []
7727
+ }
7728
+ };
7729
+ }
7730
+ if (!input.fetchResult) {
7731
+ return { ok: false, error: "Linear fetch result required when LINEAR_API_KEY is set" };
7732
+ }
7733
+ if (!input.fetchResult.ok) {
7734
+ return input.fetchResult;
7735
+ }
7736
+ return {
7737
+ ok: true,
7738
+ data: {
7739
+ availability: "ready",
7740
+ issues: input.fetchResult.data
7741
+ }
7742
+ };
7743
+ }
7744
+ function parseViewerIdResponse(raw) {
7745
+ const error = gqlErrorMessage(raw);
7746
+ if (error) {
7747
+ return { ok: false, error };
7748
+ }
7749
+ const viewerId = raw.data?.viewer.id;
7750
+ if (!viewerId) {
7751
+ return { ok: false, error: "No viewer id in response" };
7752
+ }
7753
+ return { ok: true, data: viewerId };
7754
+ }
7755
+ function parseInProgressStateIdResponse(raw) {
7756
+ const error = gqlErrorMessage(raw);
7757
+ if (error) {
7758
+ return { ok: false, error };
7759
+ }
7760
+ const states = raw.data?.team?.states.nodes;
7761
+ if (!states) {
7762
+ return { ok: false, error: "No team states in response" };
7763
+ }
7764
+ const preferredState = states.find((state) => state.type === "started" && state.name.trim().toLowerCase() === "in progress");
7765
+ if (preferredState) {
7766
+ return { ok: true, data: preferredState.id };
7767
+ }
7768
+ const startedState = states.find((state) => state.type === "started");
7769
+ if (!startedState) {
7770
+ return { ok: false, error: "No started workflow state found for team" };
7771
+ }
7772
+ return { ok: true, data: startedState.id };
7773
+ }
7774
+ function parseIssueCreateResponse(raw) {
7775
+ const error = gqlErrorMessage(raw);
7776
+ if (error) {
7777
+ return { ok: false, error };
7778
+ }
7779
+ const payload = raw.data?.issueCreate;
7780
+ if (!payload) {
7781
+ return { ok: false, error: "No issueCreate payload in response" };
7782
+ }
7783
+ if (!payload.success || !payload.issue) {
7784
+ return { ok: false, error: "Linear issue creation was not successful" };
7785
+ }
7786
+ if (!payload.issue.branchName) {
7787
+ return { ok: false, error: "Linear issue did not return a branch name" };
7788
+ }
7789
+ return {
7790
+ ok: true,
7791
+ data: {
7792
+ id: payload.issue.id,
7793
+ identifier: payload.issue.identifier,
7794
+ title: payload.issue.title,
7795
+ url: payload.issue.url,
7796
+ branchName: payload.issue.branchName
7797
+ }
7798
+ };
7799
+ }
7800
+ function deriveLinearIssueTitle(explicitTitle, prompt) {
7801
+ const trimmedTitle = explicitTitle?.trim();
7802
+ if (trimmedTitle) {
7803
+ return trimmedTitle;
7804
+ }
7805
+ const firstPromptLine = prompt?.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
7806
+ return firstPromptLine ?? null;
7807
+ }
7630
7808
  function branchMatchesIssue(worktreeBranch, issueBranchName) {
7631
7809
  if (!worktreeBranch || !issueBranchName)
7632
7810
  return false;
@@ -7650,15 +7828,13 @@ function branchMatchesIssue(worktreeBranch, issueBranchName) {
7650
7828
  }
7651
7829
  var CACHE_TTL_MS = 300000;
7652
7830
  var issueCache = null;
7653
- async function fetchAssignedIssues() {
7831
+ var viewerIdCache = null;
7832
+ var inProgressStateIdCache = new Map;
7833
+ async function postLinearGraphql(query, variables) {
7654
7834
  const apiKey = Bun.env.LINEAR_API_KEY;
7655
7835
  if (!apiKey) {
7656
7836
  return { ok: false, error: "LINEAR_API_KEY not set" };
7657
7837
  }
7658
- const now = Date.now();
7659
- if (issueCache && now < issueCache.expiry) {
7660
- return issueCache.data;
7661
- }
7662
7838
  try {
7663
7839
  const res = await fetch("https://api.linear.app/graphql", {
7664
7840
  method: "POST",
@@ -7666,28 +7842,106 @@ async function fetchAssignedIssues() {
7666
7842
  "Content-Type": "application/json",
7667
7843
  Authorization: apiKey
7668
7844
  },
7669
- body: JSON.stringify({ query: ASSIGNED_ISSUES_QUERY })
7845
+ body: JSON.stringify(variables ? { query, variables } : { query })
7670
7846
  });
7671
7847
  if (!res.ok) {
7672
7848
  const text = await res.text();
7673
- const result2 = { ok: false, error: `Linear API ${res.status}: ${text.slice(0, 200)}` };
7674
- return result2;
7675
- }
7676
- const json = await res.json();
7677
- const result = parseIssuesResponse(json);
7678
- if (result.ok) {
7679
- issueCache = { data: result, expiry: now + CACHE_TTL_MS };
7680
- log.debug(`[linear] fetched ${result.data.length} assigned issues`);
7681
- } else {
7682
- log.error(`[linear] GraphQL error: ${result.error}`);
7849
+ return { ok: false, error: `Linear API ${res.status}: ${text.slice(0, 200)}` };
7683
7850
  }
7684
- return result;
7851
+ return {
7852
+ ok: true,
7853
+ data: await res.json()
7854
+ };
7685
7855
  } catch (err) {
7686
7856
  const msg = err instanceof Error ? err.message : String(err);
7687
- log.error(`[linear] fetch failed: ${msg}`);
7688
7857
  return { ok: false, error: msg };
7689
7858
  }
7690
7859
  }
7860
+ async function fetchViewerId() {
7861
+ if (viewerIdCache) {
7862
+ return { ok: true, data: viewerIdCache };
7863
+ }
7864
+ const response = await postLinearGraphql(VIEWER_QUERY);
7865
+ if (!response.ok) {
7866
+ log.error(`[linear] viewer fetch failed: ${response.error}`);
7867
+ return { ok: false, error: response.error };
7868
+ }
7869
+ const result = parseViewerIdResponse(response.data);
7870
+ if (!result.ok) {
7871
+ log.error(`[linear] viewer GraphQL error: ${result.error}`);
7872
+ return result;
7873
+ }
7874
+ viewerIdCache = result.data;
7875
+ return result;
7876
+ }
7877
+ async function fetchInProgressStateId(teamId) {
7878
+ const cachedStateId = inProgressStateIdCache.get(teamId);
7879
+ if (cachedStateId) {
7880
+ return { ok: true, data: cachedStateId };
7881
+ }
7882
+ const response = await postLinearGraphql(TEAM_STATES_QUERY, { teamId });
7883
+ if (!response.ok) {
7884
+ log.error(`[linear] team states fetch failed: ${response.error}`);
7885
+ return { ok: false, error: response.error };
7886
+ }
7887
+ const result = parseInProgressStateIdResponse(response.data);
7888
+ if (!result.ok) {
7889
+ log.error(`[linear] team states GraphQL error: ${result.error}`);
7890
+ return result;
7891
+ }
7892
+ inProgressStateIdCache.set(teamId, result.data);
7893
+ return result;
7894
+ }
7895
+ async function fetchAssignedIssues(options) {
7896
+ const now = Date.now();
7897
+ if (!options?.skipCache && issueCache && now < issueCache.expiry) {
7898
+ return issueCache.data;
7899
+ }
7900
+ const response = await postLinearGraphql(ASSIGNED_ISSUES_QUERY);
7901
+ if (!response.ok) {
7902
+ log.error(`[linear] fetch failed: ${response.error}`);
7903
+ return { ok: false, error: response.error };
7904
+ }
7905
+ const result = parseIssuesResponse(response.data);
7906
+ if (result.ok) {
7907
+ issueCache = { data: result, expiry: now + CACHE_TTL_MS };
7908
+ log.debug(`[linear] fetched ${result.data.length} assigned issues`);
7909
+ } else {
7910
+ log.error(`[linear] GraphQL error: ${result.error}`);
7911
+ }
7912
+ return result;
7913
+ }
7914
+ async function createLinearIssue(input) {
7915
+ const viewerResult = await fetchViewerId();
7916
+ if (!viewerResult.ok) {
7917
+ return { ok: false, error: viewerResult.error };
7918
+ }
7919
+ const stateResult = await fetchInProgressStateId(input.teamId);
7920
+ if (!stateResult.ok) {
7921
+ return { ok: false, error: stateResult.error };
7922
+ }
7923
+ const response = await postLinearGraphql(ISSUE_CREATE_MUTATION, {
7924
+ input: {
7925
+ title: input.title,
7926
+ description: input.description,
7927
+ teamId: input.teamId,
7928
+ assigneeId: viewerResult.data,
7929
+ stateId: stateResult.data
7930
+ }
7931
+ });
7932
+ if (!response.ok) {
7933
+ log.error(`[linear] create failed: ${response.error}`);
7934
+ return { ok: false, error: response.error };
7935
+ }
7936
+ const result = parseIssueCreateResponse(response.data);
7937
+ if (result.ok) {
7938
+ issueCache = null;
7939
+ log.debug(`[linear] created issue ${result.data.identifier} branch=${result.data.branchName}`);
7940
+ } else {
7941
+ log.error(`[linear] issueCreate error: ${result.error}`);
7942
+ }
7943
+ return result;
7944
+ }
7691
7945
 
7692
7946
  // backend/src/services/lifecycle-service.ts
7693
7947
  import { randomUUID as randomUUID2 } from "crypto";
@@ -8521,7 +8775,10 @@ function listLocalGitBranches(cwd) {
8521
8775
  function readGitWorktreeStatus(cwd) {
8522
8776
  const dirtyOutput = runGit(["status", "--porcelain"], cwd);
8523
8777
  const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
8524
- const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
8778
+ let ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
8779
+ if (!ahead.ok) {
8780
+ ahead = tryRunGit(["rev-list", "--count", "HEAD", "--not", "--remotes=origin"], cwd);
8781
+ }
8525
8782
  return {
8526
8783
  dirty: dirtyOutput.length > 0,
8527
8784
  aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
@@ -8620,9 +8877,21 @@ class BunGitGateway {
8620
8877
  const result = tryRunGit(["diff", "HEAD", "--no-color"], cwd);
8621
8878
  return result.ok ? result.stdout : "";
8622
8879
  }
8623
- readUnpushedDiff(cwd) {
8624
- const result = tryRunGit(["diff", "@{upstream}..HEAD", "--no-color"], cwd);
8625
- return result.ok ? result.stdout : "";
8880
+ listUnpushedCommits(cwd) {
8881
+ let result = tryRunGit(["log", "--oneline", "@{upstream}..HEAD"], cwd);
8882
+ if (!result.ok) {
8883
+ result = tryRunGit(["log", "--oneline", "HEAD", "--not", "--remotes=origin"], cwd);
8884
+ }
8885
+ if (!result.ok || !result.stdout)
8886
+ return [];
8887
+ return result.stdout.split(`
8888
+ `).filter((line) => line.length > 0).map((line) => {
8889
+ const spaceIdx = line.indexOf(" ");
8890
+ return {
8891
+ hash: line.slice(0, spaceIdx),
8892
+ message: line.slice(spaceIdx + 1)
8893
+ };
8894
+ });
8626
8895
  }
8627
8896
  }
8628
8897
 
@@ -8882,7 +9151,7 @@ class LifecycleService {
8882
9151
  agent,
8883
9152
  phase: "reconciling"
8884
9153
  });
8885
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
9154
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
8886
9155
  return {
8887
9156
  branch,
8888
9157
  worktreeId: initialized.meta.worktreeId
@@ -8917,7 +9186,7 @@ class LifecycleService {
8917
9186
  worktreePath: resolved.entry.path,
8918
9187
  launchMode
8919
9188
  });
8920
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
9189
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
8921
9190
  return {
8922
9191
  branch,
8923
9192
  worktreeId: initialized.meta.worktreeId
@@ -8930,7 +9199,7 @@ class LifecycleService {
8930
9199
  try {
8931
9200
  const resolved = await this.resolveExistingWorktree(branch);
8932
9201
  this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
8933
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
9202
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
8934
9203
  } catch (error) {
8935
9204
  throw this.wrapOperationError(error);
8936
9205
  }
@@ -9256,15 +9525,15 @@ class LifecycleService {
9256
9525
  deleteBranch: true,
9257
9526
  deleteBranchForce: true
9258
9527
  }, this.deps.git);
9259
- await this.deps.reconciliation.reconcile(this.deps.projectRoot);
9528
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
9260
9529
  }
9261
9530
  async runLifecycleHook(input) {
9262
- console.debug(`[lifecycle-hook] name=${input.name} command=${input.command ?? "UNDEFINED"} meta=${input.meta ? "present" : "NULL"} cwd=${input.worktreePath}`);
9531
+ log.debug(`[lifecycle-hook] name=${input.name} command=${input.command ?? "UNDEFINED"} meta=${input.meta ? "present" : "NULL"} cwd=${input.worktreePath}`);
9263
9532
  if (!input.command || !input.meta) {
9264
- console.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
9533
+ log.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
9265
9534
  return;
9266
9535
  }
9267
- console.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
9536
+ log.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
9268
9537
  const dotenvValues = await loadDotenvLocal(input.worktreePath);
9269
9538
  await this.deps.hooks.run({
9270
9539
  name: input.name,
@@ -9274,7 +9543,7 @@ class LifecycleService {
9274
9543
  WEBMUX_WORKTREE_PATH: input.worktreePath
9275
9544
  }, dotenvValues)
9276
9545
  });
9277
- console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
9546
+ log.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
9278
9547
  }
9279
9548
  async reportCreateProgress(progress) {
9280
9549
  await this.deps.onCreateProgress?.(progress);
@@ -9290,6 +9559,117 @@ class LifecycleService {
9290
9559
  }
9291
9560
  }
9292
9561
 
9562
+ // backend/src/services/native-terminal-service.ts
9563
+ function quoteShell2(value) {
9564
+ return `'${value.replaceAll("'", "'\\''")}'`;
9565
+ }
9566
+ function sanitizeSessionSuffix(value) {
9567
+ const sanitized = value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/-{2,}/g, "-").replace(/^[.-]+|[.-]+$/g, "");
9568
+ const trimmed = sanitized.slice(0, 24);
9569
+ return trimmed || "x";
9570
+ }
9571
+ function buildNativeTerminalTmuxCommand(env) {
9572
+ const socket = env.WEBMUX_ISOLATED_TMUX_SOCKET;
9573
+ const config = env.WEBMUX_ISOLATED_TMUX_CONFIG;
9574
+ if (socket && config) {
9575
+ return `tmux -L ${quoteShell2(socket)} -f ${quoteShell2(config)}`;
9576
+ }
9577
+ if (socket) {
9578
+ return `tmux -L ${quoteShell2(socket)}`;
9579
+ }
9580
+ return "tmux";
9581
+ }
9582
+ function buildNativeTerminalLaunch(input) {
9583
+ const { branch, state, tmuxCommand } = input;
9584
+ if (!state || !state.git.exists) {
9585
+ return {
9586
+ ok: false,
9587
+ reason: "not_found",
9588
+ message: `Worktree not found: ${branch}`
9589
+ };
9590
+ }
9591
+ if (!state.session.exists || !state.session.sessionName) {
9592
+ return {
9593
+ ok: false,
9594
+ reason: "closed",
9595
+ message: `No open tmux window found for worktree: ${branch}`
9596
+ };
9597
+ }
9598
+ const sessionPrefix = input.sessionPrefix ?? "wm-native-launch-";
9599
+ const groupedSessionPrefix = `${sessionPrefix}${sanitizeSessionSuffix(state.worktreeId)}`;
9600
+ const attachScript = [
9601
+ `g_name="${groupedSessionPrefix}-${"$"}$-$(date +%s)"`,
9602
+ `owner_session_name=${quoteShell2(state.session.sessionName)}`,
9603
+ `window_name=${quoteShell2(state.session.windowName)}`,
9604
+ `grouped_window_target="${"$"}g_name:${"$"}window_name"`,
9605
+ `grouped_pane_target="${"$"}grouped_window_target.0"`,
9606
+ `cleanup() { ${tmuxCommand} kill-session -t "${"$"}g_name" >/dev/null 2>&1 || true; }`,
9607
+ "cleanup",
9608
+ `${tmuxCommand} new-session -d -s "${"$"}g_name" -t "${"$"}owner_session_name"`,
9609
+ `${tmuxCommand} set-option -t "${"$"}owner_session_name" window-size latest`,
9610
+ `${tmuxCommand} set-option -t "${"$"}g_name" mouse on`,
9611
+ `${tmuxCommand} set-option -t "${"$"}g_name" set-clipboard on`,
9612
+ `${tmuxCommand} select-window -t "${"$"}grouped_window_target"`,
9613
+ `if [ "$(${tmuxCommand} display-message -t "${"$"}grouped_window_target" -p '#{window_zoomed_flag}')" = "1" ]; then ${tmuxCommand} resize-pane -Z -t "${"$"}grouped_window_target"; fi`,
9614
+ `${tmuxCommand} select-pane -t "${"$"}grouped_pane_target"`,
9615
+ "trap cleanup EXIT INT TERM",
9616
+ `exec ${tmuxCommand} attach-session -t "${"$"}g_name"`
9617
+ ].join(" && ");
9618
+ return {
9619
+ ok: true,
9620
+ data: {
9621
+ worktreeId: state.worktreeId,
9622
+ branch: state.branch,
9623
+ path: state.path,
9624
+ shellCommand: `/bin/sh -lc ${quoteShell2(attachScript)}`
9625
+ }
9626
+ };
9627
+ }
9628
+
9629
+ // backend/src/lib/async.ts
9630
+ async function mapWithConcurrency(items, limit, fn) {
9631
+ const results = new Array(items.length);
9632
+ let next = 0;
9633
+ const concurrency = Math.max(1, Math.min(limit, items.length || 1));
9634
+ async function worker() {
9635
+ while (next < items.length) {
9636
+ const index = next++;
9637
+ results[index] = await fn(items[index]);
9638
+ }
9639
+ }
9640
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
9641
+ return results;
9642
+ }
9643
+ function startSerializedInterval(run, intervalMs, deps = {}) {
9644
+ const scheduleEvery = deps.scheduleEvery ?? ((handler, ms) => setInterval(handler, ms));
9645
+ const cancelSchedule = deps.cancelSchedule ?? ((handle2) => clearInterval(handle2));
9646
+ let running = false;
9647
+ let rerunRequested = false;
9648
+ let stopped = false;
9649
+ const execute = () => {
9650
+ if (stopped)
9651
+ return;
9652
+ if (running) {
9653
+ rerunRequested = true;
9654
+ return;
9655
+ }
9656
+ running = true;
9657
+ Promise.resolve().then(run).finally(() => {
9658
+ running = false;
9659
+ if (stopped || !rerunRequested)
9660
+ return;
9661
+ rerunRequested = false;
9662
+ execute();
9663
+ });
9664
+ };
9665
+ execute();
9666
+ const handle = scheduleEvery(execute, intervalMs);
9667
+ return () => {
9668
+ stopped = true;
9669
+ cancelSchedule(handle);
9670
+ };
9671
+ }
9672
+
9293
9673
  // backend/src/services/pr-service.ts
9294
9674
  var PR_FETCH_LIMIT = 50;
9295
9675
  var GH_TIMEOUT_MS = 15000;
@@ -9409,18 +9789,6 @@ async function fetchAllPrs(repoSlug, repoLabel, cwd) {
9409
9789
  return { ok: false, error: `failed to parse gh output for ${label}: ${err}` };
9410
9790
  }
9411
9791
  }
9412
- async function mapWithConcurrency(items, limit, fn) {
9413
- const results = new Array(items.length);
9414
- let next = 0;
9415
- async function worker() {
9416
- while (next < items.length) {
9417
- const idx = next++;
9418
- results[idx] = await fn(items[idx]);
9419
- }
9420
- }
9421
- await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
9422
- return results;
9423
- }
9424
9792
  async function fetchReviewComments(prNumber, repoSlug, cwd) {
9425
9793
  const repoFlag = repoSlug ? repoSlug : "{owner}/{repo}";
9426
9794
  const apiPath = `repos/${repoFlag}/pulls/${prNumber}/comments?per_page=100`;
@@ -9606,21 +9974,76 @@ async function syncPrStatus(getWorktreeGitDirs, linkedRepos, projectDir) {
9606
9974
  etagCache.delete(key);
9607
9975
  }
9608
9976
  }
9609
- function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs = 20000, isActive) {
9610
- const run = () => {
9977
+ function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs = 1e4, isActive) {
9978
+ const run = async () => {
9611
9979
  if (isActive && !isActive()) {
9612
9980
  log.debug("[pr] skipping PR sync: no active clients");
9613
9981
  return;
9614
9982
  }
9615
- syncPrStatus(getWorktreeGitDirs, linkedRepos, projectDir).catch((err) => {
9983
+ await syncPrStatus(getWorktreeGitDirs, linkedRepos, projectDir).catch((err) => {
9616
9984
  log.error(`[pr] sync error: ${err}`);
9617
9985
  });
9618
9986
  };
9619
- run();
9620
- const timer = setInterval(run, intervalMs);
9621
- return () => {
9622
- clearInterval(timer);
9623
- };
9987
+ return startSerializedInterval(run, intervalMs);
9988
+ }
9989
+
9990
+ // backend/src/services/linear-auto-create-service.ts
9991
+ var POLL_INTERVAL_MS = 15000;
9992
+ var processedIssueIds = new Set;
9993
+ var AUTO_CREATE_LABEL = "webmux";
9994
+ function filterAutoCreateIssues(issues, existingBranches) {
9995
+ return issues.filter((issue) => {
9996
+ if (issue.state.name !== "Todo")
9997
+ return false;
9998
+ if (!issue.labels.some((l) => l.name.toLowerCase() === AUTO_CREATE_LABEL))
9999
+ return false;
10000
+ if (processedIssueIds.has(issue.id))
10001
+ return false;
10002
+ return !existingBranches.some((branch) => branchMatchesIssue(branch, issue.branchName));
10003
+ });
10004
+ }
10005
+ async function runAutoCreate(deps) {
10006
+ if (!deps.isActive()) {
10007
+ log.debug("[linear-auto-create] skipping: no active clients");
10008
+ return;
10009
+ }
10010
+ const result = await fetchAssignedIssues({ skipCache: true });
10011
+ if (!result.ok) {
10012
+ log.error(`[linear-auto-create] failed to fetch issues: ${result.error}`);
10013
+ return;
10014
+ }
10015
+ const projectRoot2 = deps.projectRoot;
10016
+ const existingBranches = deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch);
10017
+ const newIssues = filterAutoCreateIssues(result.data, existingBranches);
10018
+ if (newIssues.length === 0) {
10019
+ log.debug(`[linear-auto-create] no new labeled issues (${result.data.length} assigned, ${existingBranches.length} worktrees)`);
10020
+ return;
10021
+ }
10022
+ log.info(`[linear-auto-create] found ${newIssues.length} new issue(s) with "${AUTO_CREATE_LABEL}" label`);
10023
+ for (const issue of newIssues) {
10024
+ try {
10025
+ log.info(`[linear-auto-create] creating worktree for ${issue.identifier}: ${issue.title}`);
10026
+ await deps.lifecycleService.createWorktree({
10027
+ mode: "new",
10028
+ branch: issue.branchName,
10029
+ prompt: `${issue.title}
10030
+
10031
+ ${issue.description ?? ""}`.trim()
10032
+ });
10033
+ processedIssueIds.add(issue.id);
10034
+ log.info(`[linear-auto-create] created worktree for ${issue.identifier}`);
10035
+ } catch (err) {
10036
+ const msg = err instanceof Error ? err.message : String(err);
10037
+ log.error(`[linear-auto-create] failed to create worktree for ${issue.identifier}: ${msg}`);
10038
+ }
10039
+ }
10040
+ }
10041
+ function startLinearAutoCreateMonitor(deps) {
10042
+ log.info("[linear-auto-create] monitor started");
10043
+ return startSerializedInterval(() => runAutoCreate(deps), POLL_INTERVAL_MS);
10044
+ }
10045
+ function resetProcessedIssues() {
10046
+ processedIssueIds.clear();
9624
10047
  }
9625
10048
 
9626
10049
  // backend/src/services/snapshot-service.ts
@@ -9662,7 +10085,8 @@ function mapWorktreeSnapshot(state, now, creating, findLinearIssue) {
9662
10085
  profile: state.profile,
9663
10086
  agentName: state.agentName,
9664
10087
  mux: state.session.exists,
9665
- dirty: state.git.dirty || state.git.aheadCount > 0,
10088
+ dirty: state.git.dirty,
10089
+ unpushed: state.git.aheadCount > 0,
9666
10090
  paneCount: state.session.paneCount,
9667
10091
  status: creating ? "creating" : state.agent.lifecycle,
9668
10092
  elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
@@ -9681,6 +10105,7 @@ function mapCreatingWorktreeSnapshot(creating, findLinearIssue) {
9681
10105
  agentName: creating.agentName,
9682
10106
  mux: false,
9683
10107
  dirty: false,
10108
+ unpushed: false,
9684
10109
  paneCount: 0,
9685
10110
  status: "creating",
9686
10111
  elapsed: "",
@@ -10041,8 +10466,8 @@ class BunLifecycleHookRunner {
10041
10466
  }
10042
10467
  async run(input) {
10043
10468
  const cmd = await this.buildCommand(input.cwd, input.command);
10044
- console.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
10045
- console.debug(`[hook-runner] Env keys: ${Object.keys(input.env).join(", ")}`);
10469
+ log.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
10470
+ log.debug(`[hook-runner] envKeys=${Object.keys(input.env).length}`);
10046
10471
  const proc = Bun.spawn(cmd, {
10047
10472
  cwd: input.cwd,
10048
10473
  env: {
@@ -10057,11 +10482,11 @@ class BunLifecycleHookRunner {
10057
10482
  new Response(proc.stdout).text(),
10058
10483
  new Response(proc.stderr).text()
10059
10484
  ]);
10060
- console.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
10485
+ log.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
10061
10486
  if (stdout.trim())
10062
- console.debug(`[hook-runner] stdout: ${stdout.trim()}`);
10487
+ log.debug(`[hook-runner] stdout: ${stdout.trim()}`);
10063
10488
  if (stderr.trim())
10064
- console.debug(`[hook-runner] stderr: ${stderr.trim()}`);
10489
+ log.debug(`[hook-runner] stderr: ${stderr.trim()}`);
10065
10490
  if (exitCode !== 0) {
10066
10491
  throw new Error(buildErrorMessage(input.name, exitCode, stdout, stderr));
10067
10492
  }
@@ -10545,11 +10970,34 @@ function resolveBranch(entry, metaBranch) {
10545
10970
 
10546
10971
  class ReconciliationService {
10547
10972
  deps;
10548
- constructor(deps) {
10973
+ freshnessMs;
10974
+ now;
10975
+ concurrency;
10976
+ inFlight = null;
10977
+ lastReconciledAt = 0;
10978
+ constructor(deps, options = {}) {
10549
10979
  this.deps = deps;
10980
+ this.freshnessMs = options.freshnessMs ?? 500;
10981
+ this.now = options.now ?? Date.now;
10982
+ this.concurrency = options.concurrency ?? 4;
10550
10983
  }
10551
- async reconcile(repoRoot) {
10984
+ async reconcile(repoRoot, options = {}) {
10985
+ if (this.inFlight) {
10986
+ return await this.inFlight;
10987
+ }
10988
+ if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
10989
+ return;
10990
+ }
10552
10991
  const normalizedRepoRoot = resolve5(repoRoot);
10992
+ const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
10993
+ this.lastReconciledAt = this.now();
10994
+ });
10995
+ this.inFlight = reconcilePromise.finally(() => {
10996
+ this.inFlight = null;
10997
+ });
10998
+ return await this.inFlight;
10999
+ }
11000
+ async runReconcile(normalizedRepoRoot) {
10553
11001
  const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
10554
11002
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
10555
11003
  let windows = [];
@@ -10559,40 +11007,32 @@ class ReconciliationService {
10559
11007
  windows = [];
10560
11008
  }
10561
11009
  const seenWorktreeIds = new Set;
10562
- for (const entry of worktrees) {
10563
- if (entry.bare)
10564
- continue;
10565
- if (resolve5(entry.path) === normalizedRepoRoot)
10566
- continue;
11010
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve5(entry.path) !== normalizedRepoRoot);
11011
+ const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
10567
11012
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
10568
11013
  const meta = await readWorktreeMeta(gitDir);
10569
11014
  const branch = resolveBranch(entry, meta?.branch ?? null);
10570
11015
  const worktreeId = meta?.worktreeId ?? makeUnmanagedWorktreeId(entry.path);
10571
- seenWorktreeIds.add(worktreeId);
10572
- this.deps.runtime.upsertWorktree({
11016
+ const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
11017
+ const window = findWindow(windows, sessionName, branch);
11018
+ return {
10573
11019
  worktreeId,
10574
11020
  branch,
10575
11021
  path: entry.path,
10576
11022
  profile: meta?.profile ?? null,
10577
11023
  agentName: meta?.agent ?? null,
10578
- runtime: meta?.runtime ?? "host"
10579
- });
10580
- const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
10581
- this.deps.runtime.setGitState(worktreeId, {
10582
- exists: true,
10583
- branch,
10584
- dirty: gitStatus.dirty,
10585
- aheadCount: gitStatus.aheadCount,
10586
- currentCommit: gitStatus.currentCommit
10587
- });
10588
- const window = findWindow(windows, sessionName, branch);
10589
- this.deps.runtime.setSessionState(worktreeId, {
10590
- exists: window !== null,
10591
- sessionName: window?.sessionName ?? null,
10592
- paneCount: window?.paneCount ?? 0
10593
- });
10594
- if (meta) {
10595
- this.deps.runtime.setServices(worktreeId, await buildServiceStates(this.deps, {
11024
+ runtime: meta?.runtime ?? "host",
11025
+ git: {
11026
+ dirty: gitStatus.dirty,
11027
+ aheadCount: gitStatus.aheadCount,
11028
+ currentCommit: gitStatus.currentCommit
11029
+ },
11030
+ session: {
11031
+ exists: window !== null,
11032
+ sessionName: window?.sessionName ?? null,
11033
+ paneCount: window?.paneCount ?? 0
11034
+ },
11035
+ services: meta ? await buildServiceStates(this.deps, {
10596
11036
  allocatedPorts: meta.allocatedPorts,
10597
11037
  startupEnvValues: meta.startupEnvValues,
10598
11038
  worktreeId: meta.worktreeId,
@@ -10600,11 +11040,34 @@ class ReconciliationService {
10600
11040
  profile: meta.profile,
10601
11041
  agent: meta.agent,
10602
11042
  runtime: meta.runtime
10603
- }));
10604
- } else {
10605
- this.deps.runtime.setServices(worktreeId, []);
10606
- }
10607
- this.deps.runtime.setPrs(worktreeId, await readWorktreePrs(gitDir));
11043
+ }) : [],
11044
+ prs: await readWorktreePrs(gitDir)
11045
+ };
11046
+ });
11047
+ for (const state of reconciledStates) {
11048
+ seenWorktreeIds.add(state.worktreeId);
11049
+ this.deps.runtime.upsertWorktree({
11050
+ worktreeId: state.worktreeId,
11051
+ branch: state.branch,
11052
+ path: state.path,
11053
+ profile: state.profile,
11054
+ agentName: state.agentName,
11055
+ runtime: state.runtime
11056
+ });
11057
+ this.deps.runtime.setGitState(state.worktreeId, {
11058
+ exists: true,
11059
+ branch: state.branch,
11060
+ dirty: state.git.dirty,
11061
+ aheadCount: state.git.aheadCount,
11062
+ currentCommit: state.git.currentCommit
11063
+ });
11064
+ this.deps.runtime.setSessionState(state.worktreeId, {
11065
+ exists: state.session.exists,
11066
+ sessionName: state.session.sessionName,
11067
+ paneCount: state.session.paneCount
11068
+ });
11069
+ this.deps.runtime.setServices(state.worktreeId, state.services);
11070
+ this.deps.runtime.setPrs(state.worktreeId, state.prs);
10608
11071
  }
10609
11072
  for (const state of this.deps.runtime.listWorktrees()) {
10610
11073
  if (!seenWorktreeIds.has(state.worktreeId)) {
@@ -10712,6 +11175,24 @@ var runtimeNotifications = runtime.runtimeNotifications;
10712
11175
  var reconciliationService = runtime.reconciliationService;
10713
11176
  var removingBranches = new Set;
10714
11177
  var lifecycleService = runtime.lifecycleService;
11178
+ var linearAutoCreateEnabled = config.integrations.linear.autoCreateWorktrees;
11179
+ var stopLinearAutoCreate = null;
11180
+ function startLinearAutoCreate() {
11181
+ if (stopLinearAutoCreate)
11182
+ return;
11183
+ stopLinearAutoCreate = startLinearAutoCreateMonitor({
11184
+ lifecycleService,
11185
+ git,
11186
+ projectRoot: PROJECT_DIR,
11187
+ isActive: hasRecentDashboardActivity
11188
+ });
11189
+ }
11190
+ function stopLinearAutoCreateMonitor() {
11191
+ if (stopLinearAutoCreate) {
11192
+ stopLinearAutoCreate();
11193
+ stopLinearAutoCreate = null;
11194
+ }
11195
+ }
10715
11196
  function getFrontendConfig() {
10716
11197
  const defaultProfileName = getDefaultProfileName(config);
10717
11198
  const orderedProfileEntries = Object.entries(config.profiles).sort(([left], [right]) => {
@@ -10730,11 +11211,13 @@ function getFrontendConfig() {
10730
11211
  })),
10731
11212
  defaultProfileName,
10732
11213
  autoName: config.autoName !== null,
11214
+ linearCreateTicketOption: config.integrations.linear.enabled && config.integrations.linear.createTicketOption,
10733
11215
  startupEnvs: config.startupEnvs,
10734
11216
  linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
10735
11217
  alias: lr.alias,
10736
11218
  ...lr.dir ? { dir: resolve6(PROJECT_DIR, lr.dir) } : {}
10737
- }))
11219
+ })),
11220
+ linearAutoCreateWorktrees: linearAutoCreateEnabled
10738
11221
  };
10739
11222
  }
10740
11223
  function parseWsMessage(raw) {
@@ -10812,8 +11295,11 @@ async function withRemovingBranch(branch, fn) {
10812
11295
  }
10813
11296
  async function resolveTerminalWorktree(branch) {
10814
11297
  ensureBranchNotBusy(branch);
10815
- await reconciliationService.reconcile(PROJECT_DIR);
10816
- const state = projectRuntime.getWorktreeByBranch(branch);
11298
+ let state = projectRuntime.getWorktreeByBranch(branch);
11299
+ if (!state || !state.session.exists || !state.session.sessionName) {
11300
+ await reconciliationService.reconcile(PROJECT_DIR);
11301
+ state = projectRuntime.getWorktreeByBranch(branch);
11302
+ }
10817
11303
  if (!state) {
10818
11304
  throw new Error(`Worktree not found: ${branch}`);
10819
11305
  }
@@ -10828,9 +11314,24 @@ async function resolveTerminalWorktree(branch) {
10828
11314
  }
10829
11315
  };
10830
11316
  }
10831
- function getAttachedWorktreeId(ws) {
10832
- if (ws.data.attached && ws.data.worktreeId) {
10833
- return ws.data.worktreeId;
11317
+ async function apiGetNativeTerminalLaunch(branch) {
11318
+ touchDashboardActivity();
11319
+ ensureBranchNotBusy(branch);
11320
+ await reconciliationService.reconcile(PROJECT_DIR);
11321
+ const launch = buildNativeTerminalLaunch({
11322
+ branch,
11323
+ state: projectRuntime.getWorktreeByBranch(branch),
11324
+ tmuxCommand: buildNativeTerminalTmuxCommand(Bun.env),
11325
+ sessionPrefix: `wm-native-${PORT}-`
11326
+ });
11327
+ if (!launch.ok) {
11328
+ return errorResponse(launch.message, launch.reason === "not_found" ? 404 : 409);
11329
+ }
11330
+ return jsonResponse(launch.data);
11331
+ }
11332
+ function getAttachedSessionId(ws) {
11333
+ if (ws.data.attached && ws.data.attachId) {
11334
+ return ws.data.attachId;
10834
11335
  }
10835
11336
  sendWs(ws, { type: "error", message: "Terminal not attached" });
10836
11337
  return null;
@@ -10864,7 +11365,8 @@ function makeCallbacks(ws) {
10864
11365
  }
10865
11366
  async function apiGetProject() {
10866
11367
  touchDashboardActivity();
10867
- const linearIssuesPromise = config.integrations.linear.enabled ? fetchAssignedIssues() : Promise.resolve({ ok: true, data: [] });
11368
+ const linearApiKey = Bun.env.LINEAR_API_KEY;
11369
+ const linearIssuesPromise = config.integrations.linear.enabled && linearApiKey?.trim() ? fetchAssignedIssues() : Promise.resolve({ ok: true, data: [] });
10868
11370
  const [, linearResult] = await Promise.all([
10869
11371
  reconciliationService.reconcile(PROJECT_DIR),
10870
11372
  linearIssuesPromise
@@ -10899,15 +11401,24 @@ async function apiRuntimeEvent(req) {
10899
11401
  const event = parseRuntimeEvent(raw);
10900
11402
  if (!event)
10901
11403
  return errorResponse("Invalid runtime event body", 400);
10902
- await reconciliationService.reconcile(PROJECT_DIR);
10903
11404
  try {
10904
11405
  projectRuntime.applyEvent(event);
10905
11406
  } catch (error) {
10906
11407
  const message = error instanceof Error ? error.message : String(error);
10907
11408
  if (message.includes("Unknown worktree id")) {
10908
- return errorResponse(message, 404);
11409
+ await reconciliationService.reconcile(PROJECT_DIR);
11410
+ try {
11411
+ projectRuntime.applyEvent(event);
11412
+ } catch (retryError) {
11413
+ const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
11414
+ if (retryMessage.includes("Unknown worktree id")) {
11415
+ return errorResponse(retryMessage, 404);
11416
+ }
11417
+ throw retryError;
11418
+ }
11419
+ } else {
11420
+ throw error;
10909
11421
  }
10910
- throw error;
10911
11422
  }
10912
11423
  const notification = runtimeNotifications.recordEvent(event);
10913
11424
  return jsonResponse({
@@ -10937,21 +11448,56 @@ async function apiCreateWorktree(req) {
10937
11448
  if (Object.keys(parsed).length > 0)
10938
11449
  envOverrides = parsed;
10939
11450
  }
10940
- const branch = typeof body.branch === "string" ? body.branch : undefined;
10941
- const prompt = typeof body.prompt === "string" ? body.prompt : undefined;
11451
+ const branch = typeof body.branch === "string" && body.branch.trim() ? body.branch.trim() : undefined;
11452
+ const prompt = typeof body.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined;
10942
11453
  const profile = typeof body.profile === "string" ? body.profile : undefined;
10943
11454
  const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
10944
- const mode = body.mode;
10945
- if (mode !== undefined && mode !== "new" && mode !== "existing") {
11455
+ const createLinearTicket = body.createLinearTicket === true;
11456
+ const linearTitle = typeof body.linearTitle === "string" && body.linearTitle.trim() ? body.linearTitle.trim() : undefined;
11457
+ const mode = body.mode === "new" || body.mode === "existing" ? body.mode : undefined;
11458
+ if (body.mode !== undefined && body.mode !== "new" && body.mode !== "existing") {
10946
11459
  return errorResponse("Invalid worktree create mode", 400);
10947
11460
  }
10948
- if (branch) {
10949
- ensureBranchNotCreating(branch);
11461
+ if (createLinearTicket && mode === "existing") {
11462
+ return errorResponse("Linear ticket creation is only supported for new branches", 400);
11463
+ }
11464
+ if (createLinearTicket && !config.integrations.linear.enabled) {
11465
+ return errorResponse("Linear integration is disabled", 400);
11466
+ }
11467
+ if (createLinearTicket && !config.integrations.linear.createTicketOption) {
11468
+ return errorResponse("Linear ticket creation is not enabled for this project", 400);
11469
+ }
11470
+ if (createLinearTicket && !prompt) {
11471
+ return errorResponse("Prompt is required when creating a Linear ticket", 400);
11472
+ }
11473
+ let resolvedBranch = branch;
11474
+ if (createLinearTicket) {
11475
+ const title = deriveLinearIssueTitle(linearTitle, prompt);
11476
+ if (!title) {
11477
+ return errorResponse("Linear ticket title could not be derived from the prompt", 400);
11478
+ }
11479
+ const teamId = config.integrations.linear.teamId;
11480
+ if (!teamId) {
11481
+ return errorResponse("Linear teamId is not configured", 503);
11482
+ }
11483
+ const linearResult = await createLinearIssue({
11484
+ title,
11485
+ description: prompt ?? "",
11486
+ teamId
11487
+ });
11488
+ if (!linearResult.ok) {
11489
+ return errorResponse(linearResult.error, 502);
11490
+ }
11491
+ resolvedBranch = linearResult.data.branchName;
11492
+ ensureBranchNotCreating(resolvedBranch);
11493
+ log.info(`[linear] created ticket ${linearResult.data.identifier} branch=${linearResult.data.branchName} title="${linearResult.data.title.slice(0, 80)}"`);
11494
+ } else if (resolvedBranch) {
11495
+ ensureBranchNotCreating(resolvedBranch);
10950
11496
  }
10951
- log.info(`[worktree:add] mode=${mode ?? "new"}${branch ? ` branch=${branch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
11497
+ log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
10952
11498
  const result = await lifecycleService.createWorktree({
10953
11499
  mode,
10954
- branch,
11500
+ branch: resolvedBranch,
10955
11501
  prompt,
10956
11502
  profile,
10957
11503
  agent,
@@ -11007,8 +11553,35 @@ async function apiMergeWorktree(name) {
11007
11553
  log.debug(`[worktree:merge] done name=${name}`);
11008
11554
  return jsonResponse({ ok: true });
11009
11555
  }
11556
+ async function apiSetLinearAutoCreate(req) {
11557
+ const raw = await req.json();
11558
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
11559
+ return errorResponse("Invalid request body", 400);
11560
+ }
11561
+ const body = raw;
11562
+ if (typeof body.enabled !== "boolean") {
11563
+ return errorResponse("Missing boolean 'enabled' field", 400);
11564
+ }
11565
+ linearAutoCreateEnabled = body.enabled;
11566
+ if (linearAutoCreateEnabled) {
11567
+ resetProcessedIssues();
11568
+ startLinearAutoCreate();
11569
+ log.info("[config] Linear auto-create worktrees enabled");
11570
+ } else {
11571
+ stopLinearAutoCreateMonitor();
11572
+ log.info("[config] Linear auto-create worktrees disabled");
11573
+ }
11574
+ await persistLocalLinearConfig(PROJECT_DIR, { autoCreateWorktrees: linearAutoCreateEnabled });
11575
+ return jsonResponse({ ok: true, enabled: linearAutoCreateEnabled });
11576
+ }
11010
11577
  async function apiGetLinearIssues() {
11011
- const result = await fetchAssignedIssues();
11578
+ const apiKey = Bun.env.LINEAR_API_KEY;
11579
+ const fetchResult = config.integrations.linear.enabled && apiKey?.trim() ? await fetchAssignedIssues() : undefined;
11580
+ const result = buildLinearIssuesResponse({
11581
+ integrationEnabled: config.integrations.linear.enabled,
11582
+ apiKey,
11583
+ fetchResult
11584
+ });
11012
11585
  if (!result.ok)
11013
11586
  return errorResponse(result.error, 502);
11014
11587
  return jsonResponse(result.data);
@@ -11020,18 +11593,12 @@ async function apiGetWorktreeDiff(name) {
11020
11593
  if (!state)
11021
11594
  return errorResponse(`Worktree not found: ${name}`, 404);
11022
11595
  const uncommitted = git.readDiff(state.path);
11023
- const unpushed = git.readUnpushedDiff(state.path);
11024
- function cap(raw) {
11025
- const truncated = raw.length > MAX_DIFF_BYTES;
11026
- return { diff: truncated ? raw.slice(0, MAX_DIFF_BYTES) : raw, truncated };
11027
- }
11028
- const u = cap(uncommitted);
11029
- const p = cap(unpushed);
11596
+ const unpushedCommits = git.listUnpushedCommits(state.path);
11597
+ const truncated = uncommitted.length > MAX_DIFF_BYTES;
11030
11598
  return jsonResponse({
11031
- uncommitted: u.diff,
11032
- uncommittedTruncated: u.truncated,
11033
- unpushed: p.diff,
11034
- unpushedTruncated: p.truncated
11599
+ uncommitted: truncated ? uncommitted.slice(0, MAX_DIFF_BYTES) : uncommitted,
11600
+ uncommittedTruncated: truncated,
11601
+ unpushedCommits
11035
11602
  });
11036
11603
  }
11037
11604
  async function apiCiLogs(runId) {
@@ -11102,7 +11669,7 @@ Bun.serve({
11102
11669
  routes: {
11103
11670
  "/ws/:worktree": (req, server) => {
11104
11671
  const branch = decodeURIComponent(req.params.worktree);
11105
- return server.upgrade(req, { data: { branch, worktreeId: null, attached: false } }) ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
11672
+ return server.upgrade(req, { data: { branch, worktreeId: null, attachId: null, attached: false } }) ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
11106
11673
  },
11107
11674
  "/api/config": {
11108
11675
  GET: () => jsonResponse(getFrontendConfig())
@@ -11135,6 +11702,14 @@ Bun.serve({
11135
11702
  return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name));
11136
11703
  }
11137
11704
  },
11705
+ "/api/worktrees/:name/terminal-launch": {
11706
+ GET: (req) => {
11707
+ const name = decodeURIComponent(req.params.name);
11708
+ if (!isValidWorktreeName(name))
11709
+ return errorResponse("Invalid worktree name", 400);
11710
+ return catching(`GET /api/worktrees/${name}/terminal-launch`, () => apiGetNativeTerminalLaunch(name));
11711
+ }
11712
+ },
11138
11713
  "/api/worktrees/:name/close": {
11139
11714
  POST: (req) => {
11140
11715
  const name = decodeURIComponent(req.params.name);
@@ -11178,6 +11753,9 @@ Bun.serve({
11178
11753
  "/api/linear/issues": {
11179
11754
  GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
11180
11755
  },
11756
+ "/api/linear/auto-create": {
11757
+ PUT: (req) => catching("PUT /api/linear/auto-create", () => apiSetLinearAutoCreate(req))
11758
+ },
11181
11759
  "/api/ci-logs/:runId": {
11182
11760
  GET: (req) => catching(`GET /api/ci-logs/${req.params.runId}`, () => apiCiLogs(req.params.runId))
11183
11761
  },
@@ -11231,26 +11809,26 @@ Bun.serve({
11231
11809
  const { branch } = ws.data;
11232
11810
  switch (msg.type) {
11233
11811
  case "input": {
11234
- const worktreeId = getAttachedWorktreeId(ws);
11235
- if (!worktreeId)
11812
+ const attachId = getAttachedSessionId(ws);
11813
+ if (!attachId)
11236
11814
  return;
11237
- write(worktreeId, msg.data);
11815
+ write(attachId, msg.data);
11238
11816
  break;
11239
11817
  }
11240
11818
  case "sendKeys": {
11241
- const worktreeId = getAttachedWorktreeId(ws);
11242
- if (!worktreeId)
11819
+ const attachId = getAttachedSessionId(ws);
11820
+ if (!attachId)
11243
11821
  return;
11244
- await sendKeys(worktreeId, msg.hexBytes);
11822
+ await sendKeys(attachId, msg.hexBytes);
11245
11823
  break;
11246
11824
  }
11247
11825
  case "selectPane":
11248
11826
  {
11249
- const worktreeId = getAttachedWorktreeId(ws);
11250
- if (!worktreeId)
11827
+ const attachId = getAttachedSessionId(ws);
11828
+ if (!attachId)
11251
11829
  return;
11252
- log.debug(`[ws] selectPane pane=${msg.pane} branch=${branch} worktreeId=${worktreeId}`);
11253
- await selectPane(worktreeId, msg.pane);
11830
+ log.debug(`[ws] selectPane pane=${msg.pane} branch=${branch} attachId=${attachId}`);
11831
+ await selectPane(attachId, msg.pane);
11254
11832
  }
11255
11833
  break;
11256
11834
  case "resize":
@@ -11262,12 +11840,14 @@ Bun.serve({
11262
11840
  log.debug(`[ws] initialPane=${msg.initialPane} branch=${branch}`);
11263
11841
  }
11264
11842
  const terminalWorktree = await resolveTerminalWorktree(branch);
11843
+ const attachId = `${terminalWorktree.worktreeId}:${randomUUID3()}`;
11265
11844
  ws.data.worktreeId = terminalWorktree.worktreeId;
11266
- await attach(terminalWorktree.worktreeId, terminalWorktree.attachTarget, msg.cols, msg.rows, msg.initialPane);
11845
+ ws.data.attachId = attachId;
11846
+ await attach(attachId, terminalWorktree.attachTarget, msg.cols, msg.rows, msg.initialPane);
11267
11847
  const { onData, onExit } = makeCallbacks(ws);
11268
- setCallbacks(terminalWorktree.worktreeId, onData, onExit);
11269
- const scrollback = getScrollback(terminalWorktree.worktreeId);
11270
- log.debug(`[ws] attached branch=${branch} worktreeId=${terminalWorktree.worktreeId} scrollback=${scrollback.length} bytes`);
11848
+ setCallbacks(attachId, onData, onExit);
11849
+ const scrollback = getScrollback(attachId);
11850
+ log.debug(`[ws] attached branch=${branch} worktreeId=${terminalWorktree.worktreeId} attachId=${attachId} scrollback=${scrollback.length} bytes`);
11271
11851
  if (scrollback.length > 0) {
11272
11852
  sendWs(ws, { type: "scrollback", data: scrollback });
11273
11853
  }
@@ -11275,24 +11855,25 @@ Bun.serve({
11275
11855
  const errMsg = err instanceof Error ? err.message : String(err);
11276
11856
  ws.data.attached = false;
11277
11857
  ws.data.worktreeId = null;
11858
+ ws.data.attachId = null;
11278
11859
  log.error(`[ws] attach failed branch=${branch}: ${errMsg}`);
11279
11860
  sendWs(ws, { type: "error", message: errMsg });
11280
11861
  ws.close(1011, errMsg.slice(0, 123));
11281
11862
  }
11282
11863
  } else {
11283
- const worktreeId = getAttachedWorktreeId(ws);
11284
- if (!worktreeId)
11864
+ const attachId = getAttachedSessionId(ws);
11865
+ if (!attachId)
11285
11866
  return;
11286
- await resize(worktreeId, msg.cols, msg.rows);
11867
+ await resize(attachId, msg.cols, msg.rows);
11287
11868
  }
11288
11869
  break;
11289
11870
  }
11290
11871
  },
11291
11872
  async close(ws, code, reason) {
11292
- log.debug(`[ws] close branch=${ws.data.branch} code=${code} reason=${reason} attached=${ws.data.attached} worktreeId=${ws.data.worktreeId}`);
11293
- if (ws.data.worktreeId) {
11294
- clearCallbacks(ws.data.worktreeId);
11295
- await detach(ws.data.worktreeId);
11873
+ log.debug(`[ws] close branch=${ws.data.branch} code=${code} reason=${reason} attached=${ws.data.attached} worktreeId=${ws.data.worktreeId} attachId=${ws.data.attachId}`);
11874
+ if (ws.data.attachId) {
11875
+ clearCallbacks(ws.data.attachId);
11876
+ await detach(ws.data.attachId);
11296
11877
  }
11297
11878
  }
11298
11879
  }
@@ -11304,6 +11885,9 @@ if (tmuxCheck.exitCode !== 0) {
11304
11885
  }
11305
11886
  cleanupStaleSessions();
11306
11887
  startPrMonitor(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJECT_DIR, undefined, hasRecentDashboardActivity);
11888
+ if (linearAutoCreateEnabled) {
11889
+ startLinearAutoCreate();
11890
+ }
11307
11891
  log.info(`Dev Dashboard API running at http://localhost:${PORT}`);
11308
11892
  var nets = networkInterfaces();
11309
11893
  for (const addrs of Object.values(nets)) {