webmux 0.18.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.
- package/README.md +1 -1
- package/backend/dist/server.js +758 -185
- package/bin/webmux.js +205 -63
- package/frontend/dist/assets/index-CKvK6kRL.js +148 -0
- package/frontend/dist/assets/{index-Cy8MTH3L.css → index-N_4ol4EP.css} +1 -1
- package/frontend/dist/icon.svg +1 -0
- package/frontend/dist/index.html +5 -7
- package/frontend/dist/manifest.webmanifest +16 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-CzRnnD-H.js +0 -148
package/backend/dist/server.js
CHANGED
|
@@ -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(
|
|
7001
|
-
log.debug(`[term] attach(${
|
|
7002
|
-
if (sessions.has(
|
|
7003
|
-
await detach(
|
|
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(${
|
|
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(
|
|
7034
|
-
log.debug(`[term] attach(${
|
|
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(${
|
|
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(${
|
|
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(${
|
|
7072
|
-
if (sessions.get(
|
|
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(
|
|
7075
|
+
sessions.delete(attachId);
|
|
7075
7076
|
} else {
|
|
7076
|
-
log.debug(`[term] proc exited(${
|
|
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(
|
|
7082
|
-
const session = sessions.get(
|
|
7082
|
+
async function detach(attachId) {
|
|
7083
|
+
const session = sessions.get(attachId);
|
|
7083
7084
|
if (!session) {
|
|
7084
|
-
log.debug(`[term] detach(${
|
|
7085
|
+
log.debug(`[term] detach(${attachId}) no session found`);
|
|
7085
7086
|
return;
|
|
7086
7087
|
}
|
|
7087
|
-
log.debug(`[term] detach(${
|
|
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(
|
|
7091
|
+
sessions.delete(attachId);
|
|
7091
7092
|
killTmuxSession(session.groupedSessionName);
|
|
7092
7093
|
}
|
|
7093
|
-
function write(
|
|
7094
|
-
const session = sessions.get(
|
|
7094
|
+
function write(attachId, data) {
|
|
7095
|
+
const session = sessions.get(attachId);
|
|
7095
7096
|
if (!session) {
|
|
7096
|
-
log.warn(`[term] write(${
|
|
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(${
|
|
7104
|
+
log.error(`[term] write(${attachId}) stdin closed`, err);
|
|
7104
7105
|
}
|
|
7105
7106
|
}
|
|
7106
|
-
async function sendKeys(
|
|
7107
|
-
const session = sessions.get(
|
|
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(
|
|
7114
|
-
const session = sessions.get(
|
|
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(
|
|
7123
|
-
return sessions.get(
|
|
7123
|
+
function getScrollback(attachId) {
|
|
7124
|
+
return sessions.get(attachId)?.scrollback.join("") ?? "";
|
|
7124
7125
|
}
|
|
7125
|
-
function setCallbacks(
|
|
7126
|
-
const session = sessions.get(
|
|
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(
|
|
7133
|
-
const session = sessions.get(
|
|
7133
|
+
async function selectPane(attachId, paneIndex) {
|
|
7134
|
+
const session = sessions.get(attachId);
|
|
7134
7135
|
if (!session) {
|
|
7135
|
-
log.debug(`[term] selectPane(${
|
|
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(${
|
|
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(${
|
|
7145
|
+
log.debug(`[term] selectPane(${attachId}) select=${r1.exitCode} zoom=${r2.exitCode}`);
|
|
7145
7146
|
}
|
|
7146
|
-
function clearCallbacks(
|
|
7147
|
-
const session = sessions.get(
|
|
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
|
-
|
|
7606
|
-
|
|
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
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
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
|
-
|
|
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:
|
|
7845
|
+
body: JSON.stringify(variables ? { query, variables } : { query })
|
|
7670
7846
|
});
|
|
7671
7847
|
if (!res.ok) {
|
|
7672
7848
|
const text = await res.text();
|
|
7673
|
-
|
|
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
|
|
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";
|
|
@@ -8897,7 +9151,7 @@ class LifecycleService {
|
|
|
8897
9151
|
agent,
|
|
8898
9152
|
phase: "reconciling"
|
|
8899
9153
|
});
|
|
8900
|
-
await this.deps.reconciliation.reconcile(this.deps.projectRoot);
|
|
9154
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
8901
9155
|
return {
|
|
8902
9156
|
branch,
|
|
8903
9157
|
worktreeId: initialized.meta.worktreeId
|
|
@@ -8932,7 +9186,7 @@ class LifecycleService {
|
|
|
8932
9186
|
worktreePath: resolved.entry.path,
|
|
8933
9187
|
launchMode
|
|
8934
9188
|
});
|
|
8935
|
-
await this.deps.reconciliation.reconcile(this.deps.projectRoot);
|
|
9189
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
8936
9190
|
return {
|
|
8937
9191
|
branch,
|
|
8938
9192
|
worktreeId: initialized.meta.worktreeId
|
|
@@ -8945,7 +9199,7 @@ class LifecycleService {
|
|
|
8945
9199
|
try {
|
|
8946
9200
|
const resolved = await this.resolveExistingWorktree(branch);
|
|
8947
9201
|
this.deps.tmux.killWindow(buildProjectSessionName(this.deps.projectRoot), buildWorktreeWindowName(branch));
|
|
8948
|
-
await this.deps.reconciliation.reconcile(this.deps.projectRoot);
|
|
9202
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
8949
9203
|
} catch (error) {
|
|
8950
9204
|
throw this.wrapOperationError(error);
|
|
8951
9205
|
}
|
|
@@ -9271,15 +9525,15 @@ class LifecycleService {
|
|
|
9271
9525
|
deleteBranch: true,
|
|
9272
9526
|
deleteBranchForce: true
|
|
9273
9527
|
}, this.deps.git);
|
|
9274
|
-
await this.deps.reconciliation.reconcile(this.deps.projectRoot);
|
|
9528
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
9275
9529
|
}
|
|
9276
9530
|
async runLifecycleHook(input) {
|
|
9277
|
-
|
|
9531
|
+
log.debug(`[lifecycle-hook] name=${input.name} command=${input.command ?? "UNDEFINED"} meta=${input.meta ? "present" : "NULL"} cwd=${input.worktreePath}`);
|
|
9278
9532
|
if (!input.command || !input.meta) {
|
|
9279
|
-
|
|
9533
|
+
log.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
|
|
9280
9534
|
return;
|
|
9281
9535
|
}
|
|
9282
|
-
|
|
9536
|
+
log.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
|
|
9283
9537
|
const dotenvValues = await loadDotenvLocal(input.worktreePath);
|
|
9284
9538
|
await this.deps.hooks.run({
|
|
9285
9539
|
name: input.name,
|
|
@@ -9289,7 +9543,7 @@ class LifecycleService {
|
|
|
9289
9543
|
WEBMUX_WORKTREE_PATH: input.worktreePath
|
|
9290
9544
|
}, dotenvValues)
|
|
9291
9545
|
});
|
|
9292
|
-
|
|
9546
|
+
log.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
|
|
9293
9547
|
}
|
|
9294
9548
|
async reportCreateProgress(progress) {
|
|
9295
9549
|
await this.deps.onCreateProgress?.(progress);
|
|
@@ -9305,6 +9559,117 @@ class LifecycleService {
|
|
|
9305
9559
|
}
|
|
9306
9560
|
}
|
|
9307
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
|
+
|
|
9308
9673
|
// backend/src/services/pr-service.ts
|
|
9309
9674
|
var PR_FETCH_LIMIT = 50;
|
|
9310
9675
|
var GH_TIMEOUT_MS = 15000;
|
|
@@ -9424,18 +9789,6 @@ async function fetchAllPrs(repoSlug, repoLabel, cwd) {
|
|
|
9424
9789
|
return { ok: false, error: `failed to parse gh output for ${label}: ${err}` };
|
|
9425
9790
|
}
|
|
9426
9791
|
}
|
|
9427
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
9428
|
-
const results = new Array(items.length);
|
|
9429
|
-
let next = 0;
|
|
9430
|
-
async function worker() {
|
|
9431
|
-
while (next < items.length) {
|
|
9432
|
-
const idx = next++;
|
|
9433
|
-
results[idx] = await fn(items[idx]);
|
|
9434
|
-
}
|
|
9435
|
-
}
|
|
9436
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
9437
|
-
return results;
|
|
9438
|
-
}
|
|
9439
9792
|
async function fetchReviewComments(prNumber, repoSlug, cwd) {
|
|
9440
9793
|
const repoFlag = repoSlug ? repoSlug : "{owner}/{repo}";
|
|
9441
9794
|
const apiPath = `repos/${repoFlag}/pulls/${prNumber}/comments?per_page=100`;
|
|
@@ -9621,21 +9974,76 @@ async function syncPrStatus(getWorktreeGitDirs, linkedRepos, projectDir) {
|
|
|
9621
9974
|
etagCache.delete(key);
|
|
9622
9975
|
}
|
|
9623
9976
|
}
|
|
9624
|
-
function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs =
|
|
9625
|
-
const run = () => {
|
|
9977
|
+
function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs = 1e4, isActive) {
|
|
9978
|
+
const run = async () => {
|
|
9626
9979
|
if (isActive && !isActive()) {
|
|
9627
9980
|
log.debug("[pr] skipping PR sync: no active clients");
|
|
9628
9981
|
return;
|
|
9629
9982
|
}
|
|
9630
|
-
syncPrStatus(getWorktreeGitDirs, linkedRepos, projectDir).catch((err) => {
|
|
9983
|
+
await syncPrStatus(getWorktreeGitDirs, linkedRepos, projectDir).catch((err) => {
|
|
9631
9984
|
log.error(`[pr] sync error: ${err}`);
|
|
9632
9985
|
});
|
|
9633
9986
|
};
|
|
9634
|
-
run
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
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();
|
|
9639
10047
|
}
|
|
9640
10048
|
|
|
9641
10049
|
// backend/src/services/snapshot-service.ts
|
|
@@ -10058,8 +10466,8 @@ class BunLifecycleHookRunner {
|
|
|
10058
10466
|
}
|
|
10059
10467
|
async run(input) {
|
|
10060
10468
|
const cmd = await this.buildCommand(input.cwd, input.command);
|
|
10061
|
-
|
|
10062
|
-
|
|
10469
|
+
log.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
|
|
10470
|
+
log.debug(`[hook-runner] envKeys=${Object.keys(input.env).length}`);
|
|
10063
10471
|
const proc = Bun.spawn(cmd, {
|
|
10064
10472
|
cwd: input.cwd,
|
|
10065
10473
|
env: {
|
|
@@ -10074,11 +10482,11 @@ class BunLifecycleHookRunner {
|
|
|
10074
10482
|
new Response(proc.stdout).text(),
|
|
10075
10483
|
new Response(proc.stderr).text()
|
|
10076
10484
|
]);
|
|
10077
|
-
|
|
10485
|
+
log.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
|
|
10078
10486
|
if (stdout.trim())
|
|
10079
|
-
|
|
10487
|
+
log.debug(`[hook-runner] stdout: ${stdout.trim()}`);
|
|
10080
10488
|
if (stderr.trim())
|
|
10081
|
-
|
|
10489
|
+
log.debug(`[hook-runner] stderr: ${stderr.trim()}`);
|
|
10082
10490
|
if (exitCode !== 0) {
|
|
10083
10491
|
throw new Error(buildErrorMessage(input.name, exitCode, stdout, stderr));
|
|
10084
10492
|
}
|
|
@@ -10562,11 +10970,34 @@ function resolveBranch(entry, metaBranch) {
|
|
|
10562
10970
|
|
|
10563
10971
|
class ReconciliationService {
|
|
10564
10972
|
deps;
|
|
10565
|
-
|
|
10973
|
+
freshnessMs;
|
|
10974
|
+
now;
|
|
10975
|
+
concurrency;
|
|
10976
|
+
inFlight = null;
|
|
10977
|
+
lastReconciledAt = 0;
|
|
10978
|
+
constructor(deps, options = {}) {
|
|
10566
10979
|
this.deps = deps;
|
|
10980
|
+
this.freshnessMs = options.freshnessMs ?? 500;
|
|
10981
|
+
this.now = options.now ?? Date.now;
|
|
10982
|
+
this.concurrency = options.concurrency ?? 4;
|
|
10567
10983
|
}
|
|
10568
|
-
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
|
+
}
|
|
10569
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) {
|
|
10570
11001
|
const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
|
|
10571
11002
|
const sessionName = buildProjectSessionName(normalizedRepoRoot);
|
|
10572
11003
|
let windows = [];
|
|
@@ -10576,40 +11007,32 @@ class ReconciliationService {
|
|
|
10576
11007
|
windows = [];
|
|
10577
11008
|
}
|
|
10578
11009
|
const seenWorktreeIds = new Set;
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
continue;
|
|
10582
|
-
if (resolve5(entry.path) === normalizedRepoRoot)
|
|
10583
|
-
continue;
|
|
11010
|
+
const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve5(entry.path) !== normalizedRepoRoot);
|
|
11011
|
+
const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
|
|
10584
11012
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
10585
11013
|
const meta = await readWorktreeMeta(gitDir);
|
|
10586
11014
|
const branch = resolveBranch(entry, meta?.branch ?? null);
|
|
10587
11015
|
const worktreeId = meta?.worktreeId ?? makeUnmanagedWorktreeId(entry.path);
|
|
10588
|
-
|
|
10589
|
-
|
|
11016
|
+
const gitStatus = this.deps.git.readWorktreeStatus(entry.path);
|
|
11017
|
+
const window = findWindow(windows, sessionName, branch);
|
|
11018
|
+
return {
|
|
10590
11019
|
worktreeId,
|
|
10591
11020
|
branch,
|
|
10592
11021
|
path: entry.path,
|
|
10593
11022
|
profile: meta?.profile ?? null,
|
|
10594
11023
|
agentName: meta?.agent ?? null,
|
|
10595
|
-
runtime: meta?.runtime ?? "host"
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
exists: window !== null,
|
|
10608
|
-
sessionName: window?.sessionName ?? null,
|
|
10609
|
-
paneCount: window?.paneCount ?? 0
|
|
10610
|
-
});
|
|
10611
|
-
if (meta) {
|
|
10612
|
-
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, {
|
|
10613
11036
|
allocatedPorts: meta.allocatedPorts,
|
|
10614
11037
|
startupEnvValues: meta.startupEnvValues,
|
|
10615
11038
|
worktreeId: meta.worktreeId,
|
|
@@ -10617,11 +11040,34 @@ class ReconciliationService {
|
|
|
10617
11040
|
profile: meta.profile,
|
|
10618
11041
|
agent: meta.agent,
|
|
10619
11042
|
runtime: meta.runtime
|
|
10620
|
-
})
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
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);
|
|
10625
11071
|
}
|
|
10626
11072
|
for (const state of this.deps.runtime.listWorktrees()) {
|
|
10627
11073
|
if (!seenWorktreeIds.has(state.worktreeId)) {
|
|
@@ -10729,6 +11175,24 @@ var runtimeNotifications = runtime.runtimeNotifications;
|
|
|
10729
11175
|
var reconciliationService = runtime.reconciliationService;
|
|
10730
11176
|
var removingBranches = new Set;
|
|
10731
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
|
+
}
|
|
10732
11196
|
function getFrontendConfig() {
|
|
10733
11197
|
const defaultProfileName = getDefaultProfileName(config);
|
|
10734
11198
|
const orderedProfileEntries = Object.entries(config.profiles).sort(([left], [right]) => {
|
|
@@ -10747,11 +11211,13 @@ function getFrontendConfig() {
|
|
|
10747
11211
|
})),
|
|
10748
11212
|
defaultProfileName,
|
|
10749
11213
|
autoName: config.autoName !== null,
|
|
11214
|
+
linearCreateTicketOption: config.integrations.linear.enabled && config.integrations.linear.createTicketOption,
|
|
10750
11215
|
startupEnvs: config.startupEnvs,
|
|
10751
11216
|
linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
|
|
10752
11217
|
alias: lr.alias,
|
|
10753
11218
|
...lr.dir ? { dir: resolve6(PROJECT_DIR, lr.dir) } : {}
|
|
10754
|
-
}))
|
|
11219
|
+
})),
|
|
11220
|
+
linearAutoCreateWorktrees: linearAutoCreateEnabled
|
|
10755
11221
|
};
|
|
10756
11222
|
}
|
|
10757
11223
|
function parseWsMessage(raw) {
|
|
@@ -10829,8 +11295,11 @@ async function withRemovingBranch(branch, fn) {
|
|
|
10829
11295
|
}
|
|
10830
11296
|
async function resolveTerminalWorktree(branch) {
|
|
10831
11297
|
ensureBranchNotBusy(branch);
|
|
10832
|
-
|
|
10833
|
-
|
|
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
|
+
}
|
|
10834
11303
|
if (!state) {
|
|
10835
11304
|
throw new Error(`Worktree not found: ${branch}`);
|
|
10836
11305
|
}
|
|
@@ -10845,9 +11314,24 @@ async function resolveTerminalWorktree(branch) {
|
|
|
10845
11314
|
}
|
|
10846
11315
|
};
|
|
10847
11316
|
}
|
|
10848
|
-
function
|
|
10849
|
-
|
|
10850
|
-
|
|
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;
|
|
10851
11335
|
}
|
|
10852
11336
|
sendWs(ws, { type: "error", message: "Terminal not attached" });
|
|
10853
11337
|
return null;
|
|
@@ -10881,7 +11365,8 @@ function makeCallbacks(ws) {
|
|
|
10881
11365
|
}
|
|
10882
11366
|
async function apiGetProject() {
|
|
10883
11367
|
touchDashboardActivity();
|
|
10884
|
-
const
|
|
11368
|
+
const linearApiKey = Bun.env.LINEAR_API_KEY;
|
|
11369
|
+
const linearIssuesPromise = config.integrations.linear.enabled && linearApiKey?.trim() ? fetchAssignedIssues() : Promise.resolve({ ok: true, data: [] });
|
|
10885
11370
|
const [, linearResult] = await Promise.all([
|
|
10886
11371
|
reconciliationService.reconcile(PROJECT_DIR),
|
|
10887
11372
|
linearIssuesPromise
|
|
@@ -10916,15 +11401,24 @@ async function apiRuntimeEvent(req) {
|
|
|
10916
11401
|
const event = parseRuntimeEvent(raw);
|
|
10917
11402
|
if (!event)
|
|
10918
11403
|
return errorResponse("Invalid runtime event body", 400);
|
|
10919
|
-
await reconciliationService.reconcile(PROJECT_DIR);
|
|
10920
11404
|
try {
|
|
10921
11405
|
projectRuntime.applyEvent(event);
|
|
10922
11406
|
} catch (error) {
|
|
10923
11407
|
const message = error instanceof Error ? error.message : String(error);
|
|
10924
11408
|
if (message.includes("Unknown worktree id")) {
|
|
10925
|
-
|
|
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;
|
|
10926
11421
|
}
|
|
10927
|
-
throw error;
|
|
10928
11422
|
}
|
|
10929
11423
|
const notification = runtimeNotifications.recordEvent(event);
|
|
10930
11424
|
return jsonResponse({
|
|
@@ -10954,21 +11448,56 @@ async function apiCreateWorktree(req) {
|
|
|
10954
11448
|
if (Object.keys(parsed).length > 0)
|
|
10955
11449
|
envOverrides = parsed;
|
|
10956
11450
|
}
|
|
10957
|
-
const branch = typeof body.branch === "string" ? body.branch : undefined;
|
|
10958
|
-
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;
|
|
10959
11453
|
const profile = typeof body.profile === "string" ? body.profile : undefined;
|
|
10960
11454
|
const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
|
|
10961
|
-
const
|
|
10962
|
-
|
|
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") {
|
|
10963
11459
|
return errorResponse("Invalid worktree create mode", 400);
|
|
10964
11460
|
}
|
|
10965
|
-
if (
|
|
10966
|
-
|
|
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);
|
|
10967
11496
|
}
|
|
10968
|
-
log.info(`[worktree:add] mode=${mode ?? "new"}${
|
|
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)}"` : ""}`);
|
|
10969
11498
|
const result = await lifecycleService.createWorktree({
|
|
10970
11499
|
mode,
|
|
10971
|
-
branch,
|
|
11500
|
+
branch: resolvedBranch,
|
|
10972
11501
|
prompt,
|
|
10973
11502
|
profile,
|
|
10974
11503
|
agent,
|
|
@@ -11024,8 +11553,35 @@ async function apiMergeWorktree(name) {
|
|
|
11024
11553
|
log.debug(`[worktree:merge] done name=${name}`);
|
|
11025
11554
|
return jsonResponse({ ok: true });
|
|
11026
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
|
+
}
|
|
11027
11577
|
async function apiGetLinearIssues() {
|
|
11028
|
-
const
|
|
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
|
+
});
|
|
11029
11585
|
if (!result.ok)
|
|
11030
11586
|
return errorResponse(result.error, 502);
|
|
11031
11587
|
return jsonResponse(result.data);
|
|
@@ -11113,7 +11669,7 @@ Bun.serve({
|
|
|
11113
11669
|
routes: {
|
|
11114
11670
|
"/ws/:worktree": (req, server) => {
|
|
11115
11671
|
const branch = decodeURIComponent(req.params.worktree);
|
|
11116
|
-
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 });
|
|
11117
11673
|
},
|
|
11118
11674
|
"/api/config": {
|
|
11119
11675
|
GET: () => jsonResponse(getFrontendConfig())
|
|
@@ -11146,6 +11702,14 @@ Bun.serve({
|
|
|
11146
11702
|
return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name));
|
|
11147
11703
|
}
|
|
11148
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
|
+
},
|
|
11149
11713
|
"/api/worktrees/:name/close": {
|
|
11150
11714
|
POST: (req) => {
|
|
11151
11715
|
const name = decodeURIComponent(req.params.name);
|
|
@@ -11189,6 +11753,9 @@ Bun.serve({
|
|
|
11189
11753
|
"/api/linear/issues": {
|
|
11190
11754
|
GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
|
|
11191
11755
|
},
|
|
11756
|
+
"/api/linear/auto-create": {
|
|
11757
|
+
PUT: (req) => catching("PUT /api/linear/auto-create", () => apiSetLinearAutoCreate(req))
|
|
11758
|
+
},
|
|
11192
11759
|
"/api/ci-logs/:runId": {
|
|
11193
11760
|
GET: (req) => catching(`GET /api/ci-logs/${req.params.runId}`, () => apiCiLogs(req.params.runId))
|
|
11194
11761
|
},
|
|
@@ -11242,26 +11809,26 @@ Bun.serve({
|
|
|
11242
11809
|
const { branch } = ws.data;
|
|
11243
11810
|
switch (msg.type) {
|
|
11244
11811
|
case "input": {
|
|
11245
|
-
const
|
|
11246
|
-
if (!
|
|
11812
|
+
const attachId = getAttachedSessionId(ws);
|
|
11813
|
+
if (!attachId)
|
|
11247
11814
|
return;
|
|
11248
|
-
write(
|
|
11815
|
+
write(attachId, msg.data);
|
|
11249
11816
|
break;
|
|
11250
11817
|
}
|
|
11251
11818
|
case "sendKeys": {
|
|
11252
|
-
const
|
|
11253
|
-
if (!
|
|
11819
|
+
const attachId = getAttachedSessionId(ws);
|
|
11820
|
+
if (!attachId)
|
|
11254
11821
|
return;
|
|
11255
|
-
await sendKeys(
|
|
11822
|
+
await sendKeys(attachId, msg.hexBytes);
|
|
11256
11823
|
break;
|
|
11257
11824
|
}
|
|
11258
11825
|
case "selectPane":
|
|
11259
11826
|
{
|
|
11260
|
-
const
|
|
11261
|
-
if (!
|
|
11827
|
+
const attachId = getAttachedSessionId(ws);
|
|
11828
|
+
if (!attachId)
|
|
11262
11829
|
return;
|
|
11263
|
-
log.debug(`[ws] selectPane pane=${msg.pane} branch=${branch}
|
|
11264
|
-
await selectPane(
|
|
11830
|
+
log.debug(`[ws] selectPane pane=${msg.pane} branch=${branch} attachId=${attachId}`);
|
|
11831
|
+
await selectPane(attachId, msg.pane);
|
|
11265
11832
|
}
|
|
11266
11833
|
break;
|
|
11267
11834
|
case "resize":
|
|
@@ -11273,12 +11840,14 @@ Bun.serve({
|
|
|
11273
11840
|
log.debug(`[ws] initialPane=${msg.initialPane} branch=${branch}`);
|
|
11274
11841
|
}
|
|
11275
11842
|
const terminalWorktree = await resolveTerminalWorktree(branch);
|
|
11843
|
+
const attachId = `${terminalWorktree.worktreeId}:${randomUUID3()}`;
|
|
11276
11844
|
ws.data.worktreeId = terminalWorktree.worktreeId;
|
|
11277
|
-
|
|
11845
|
+
ws.data.attachId = attachId;
|
|
11846
|
+
await attach(attachId, terminalWorktree.attachTarget, msg.cols, msg.rows, msg.initialPane);
|
|
11278
11847
|
const { onData, onExit } = makeCallbacks(ws);
|
|
11279
|
-
setCallbacks(
|
|
11280
|
-
const scrollback = getScrollback(
|
|
11281
|
-
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`);
|
|
11282
11851
|
if (scrollback.length > 0) {
|
|
11283
11852
|
sendWs(ws, { type: "scrollback", data: scrollback });
|
|
11284
11853
|
}
|
|
@@ -11286,24 +11855,25 @@ Bun.serve({
|
|
|
11286
11855
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
11287
11856
|
ws.data.attached = false;
|
|
11288
11857
|
ws.data.worktreeId = null;
|
|
11858
|
+
ws.data.attachId = null;
|
|
11289
11859
|
log.error(`[ws] attach failed branch=${branch}: ${errMsg}`);
|
|
11290
11860
|
sendWs(ws, { type: "error", message: errMsg });
|
|
11291
11861
|
ws.close(1011, errMsg.slice(0, 123));
|
|
11292
11862
|
}
|
|
11293
11863
|
} else {
|
|
11294
|
-
const
|
|
11295
|
-
if (!
|
|
11864
|
+
const attachId = getAttachedSessionId(ws);
|
|
11865
|
+
if (!attachId)
|
|
11296
11866
|
return;
|
|
11297
|
-
await resize(
|
|
11867
|
+
await resize(attachId, msg.cols, msg.rows);
|
|
11298
11868
|
}
|
|
11299
11869
|
break;
|
|
11300
11870
|
}
|
|
11301
11871
|
},
|
|
11302
11872
|
async close(ws, code, reason) {
|
|
11303
|
-
log.debug(`[ws] close branch=${ws.data.branch} code=${code} reason=${reason} attached=${ws.data.attached} worktreeId=${ws.data.worktreeId}`);
|
|
11304
|
-
if (ws.data.
|
|
11305
|
-
clearCallbacks(ws.data.
|
|
11306
|
-
await detach(ws.data.
|
|
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);
|
|
11307
11877
|
}
|
|
11308
11878
|
}
|
|
11309
11879
|
}
|
|
@@ -11315,6 +11885,9 @@ if (tmuxCheck.exitCode !== 0) {
|
|
|
11315
11885
|
}
|
|
11316
11886
|
cleanupStaleSessions();
|
|
11317
11887
|
startPrMonitor(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJECT_DIR, undefined, hasRecentDashboardActivity);
|
|
11888
|
+
if (linearAutoCreateEnabled) {
|
|
11889
|
+
startLinearAutoCreate();
|
|
11890
|
+
}
|
|
11318
11891
|
log.info(`Dev Dashboard API running at http://localhost:${PORT}`);
|
|
11319
11892
|
var nets = networkInterfaces();
|
|
11320
11893
|
for (const addrs of Object.values(nets)) {
|