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