webmux 0.29.0 → 0.30.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/backend/dist/server.js +270 -65
- package/bin/webmux.js +239 -48
- package/frontend/dist/assets/{DiffDialog-1-WiuIC4.js → DiffDialog-CjMdljRY.js} +1 -1
- package/frontend/dist/assets/index-FETEudgI.js +33 -0
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/frontend/dist/assets/index-D4JuH244.js +0 -33
package/backend/dist/server.js
CHANGED
|
@@ -6933,7 +6933,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6933
6933
|
|
|
6934
6934
|
// backend/src/server.ts
|
|
6935
6935
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6936
|
-
import { join as join8, resolve as
|
|
6936
|
+
import { join as join8, resolve as resolve9 } from "path";
|
|
6937
6937
|
import { mkdirSync } from "fs";
|
|
6938
6938
|
import { networkInterfaces } from "os";
|
|
6939
6939
|
|
|
@@ -11969,7 +11969,7 @@ async function sendPrompt(worktreeId, target, text, paneIndex = 0, preamble, sub
|
|
|
11969
11969
|
if (load.exitCode !== 0) {
|
|
11970
11970
|
return { ok: false, error: `load-buffer failed${load.stderr ? `: ${load.stderr}` : ""}` };
|
|
11971
11971
|
}
|
|
11972
|
-
const paste = await tmuxExec(["tmux", "paste-buffer", "-b", bufferName, "-t", paneTarget, "-d"]);
|
|
11972
|
+
const paste = await tmuxExec(["tmux", "paste-buffer", "-rp", "-b", bufferName, "-t", paneTarget, "-d"]);
|
|
11973
11973
|
if (paste.exitCode !== 0) {
|
|
11974
11974
|
return { ok: false, error: `paste-buffer failed${paste.stderr ? `: ${paste.stderr}` : ""}` };
|
|
11975
11975
|
}
|
|
@@ -13376,6 +13376,12 @@ function pruneArchivedWorktreeState(input) {
|
|
|
13376
13376
|
}
|
|
13377
13377
|
|
|
13378
13378
|
// backend/src/services/agent-chat-service.ts
|
|
13379
|
+
var CODEX_SUBMIT_DELAY_MS = 200;
|
|
13380
|
+
function resolveAgentTerminalSubmitDelayMs(input) {
|
|
13381
|
+
if (!input.agentId || !input.agent || input.agent.kind !== "builtin")
|
|
13382
|
+
return 0;
|
|
13383
|
+
return input.agent.implementation.agent === "codex" ? CODEX_SUBMIT_DELAY_MS : 0;
|
|
13384
|
+
}
|
|
13379
13385
|
function resolveAgentChatSupport(input) {
|
|
13380
13386
|
if (!input.agentId) {
|
|
13381
13387
|
return {
|
|
@@ -13411,7 +13417,7 @@ function resolveAgentChatSupport(input) {
|
|
|
13411
13417
|
ok: true,
|
|
13412
13418
|
data: {
|
|
13413
13419
|
provider: input.agent.implementation.agent,
|
|
13414
|
-
submitDelayMs: input
|
|
13420
|
+
submitDelayMs: resolveAgentTerminalSubmitDelayMs(input)
|
|
13415
13421
|
}
|
|
13416
13422
|
};
|
|
13417
13423
|
}
|
|
@@ -13880,11 +13886,11 @@ async function createLinearIssue(input) {
|
|
|
13880
13886
|
|
|
13881
13887
|
// backend/src/services/lifecycle-service.ts
|
|
13882
13888
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
13883
|
-
import { dirname as dirname4, resolve as
|
|
13889
|
+
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
13884
13890
|
|
|
13885
13891
|
// backend/src/adapters/agent-runtime.ts
|
|
13886
13892
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
13887
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
13893
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
13888
13894
|
|
|
13889
13895
|
// backend/src/adapters/fs.ts
|
|
13890
13896
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
@@ -14097,6 +14103,7 @@ async function writeWorktreePrs(gitDir, prs) {
|
|
|
14097
14103
|
}
|
|
14098
14104
|
|
|
14099
14105
|
// backend/src/adapters/agent-runtime.ts
|
|
14106
|
+
var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
|
|
14100
14107
|
function shellQuote(value) {
|
|
14101
14108
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14102
14109
|
}
|
|
@@ -14115,6 +14122,7 @@ from pathlib import Path
|
|
|
14115
14122
|
|
|
14116
14123
|
|
|
14117
14124
|
CONTROL_ENV_PATH = Path(__file__).resolve().with_name("control.env")
|
|
14125
|
+
CONTROL_REQUEST_TIMEOUT_SECONDS = 2
|
|
14118
14126
|
|
|
14119
14127
|
|
|
14120
14128
|
def read_control_env():
|
|
@@ -14144,6 +14152,7 @@ def build_parser():
|
|
|
14144
14152
|
|
|
14145
14153
|
status_changed = subparsers.add_parser("status-changed")
|
|
14146
14154
|
status_changed.add_argument("--lifecycle", choices=["starting", "running", "idle", "stopped"], required=True)
|
|
14155
|
+
status_changed.add_argument("--best-effort", action="store_true")
|
|
14147
14156
|
|
|
14148
14157
|
pr_opened = subparsers.add_parser("pr-opened")
|
|
14149
14158
|
pr_opened.add_argument("--url")
|
|
@@ -14153,6 +14162,11 @@ def build_parser():
|
|
|
14153
14162
|
|
|
14154
14163
|
subparsers.add_parser("claude-user-prompt-submit")
|
|
14155
14164
|
subparsers.add_parser("claude-post-tool-use")
|
|
14165
|
+
subparsers.add_parser("codex-session-start")
|
|
14166
|
+
subparsers.add_parser("codex-user-prompt-submit")
|
|
14167
|
+
subparsers.add_parser("codex-permission-request")
|
|
14168
|
+
subparsers.add_parser("codex-post-tool-use")
|
|
14169
|
+
subparsers.add_parser("codex-stop")
|
|
14156
14170
|
|
|
14157
14171
|
return parser
|
|
14158
14172
|
|
|
@@ -14195,6 +14209,41 @@ def read_hook_payload():
|
|
|
14195
14209
|
return parsed if isinstance(parsed, dict) else {}
|
|
14196
14210
|
|
|
14197
14211
|
|
|
14212
|
+
def iter_string_values(value):
|
|
14213
|
+
if isinstance(value, str):
|
|
14214
|
+
yield value
|
|
14215
|
+
return
|
|
14216
|
+
if isinstance(value, dict):
|
|
14217
|
+
for child in value.values():
|
|
14218
|
+
yield from iter_string_values(child)
|
|
14219
|
+
return
|
|
14220
|
+
if isinstance(value, list):
|
|
14221
|
+
for child in value:
|
|
14222
|
+
yield from iter_string_values(child)
|
|
14223
|
+
|
|
14224
|
+
|
|
14225
|
+
def find_pr_url(value):
|
|
14226
|
+
for text in iter_string_values(value):
|
|
14227
|
+
match = re.search(r"https://github\\.com/[^\\s\\"]+/pull/\\d+", text)
|
|
14228
|
+
if match:
|
|
14229
|
+
return match.group(0)
|
|
14230
|
+
return None
|
|
14231
|
+
|
|
14232
|
+
|
|
14233
|
+
def maybe_send_pr_opened(hook_payload, control_env):
|
|
14234
|
+
tool_name = hook_payload.get("tool_name")
|
|
14235
|
+
tool_input = hook_payload.get("tool_input")
|
|
14236
|
+
if not isinstance(tool_input, dict) or tool_name != "Bash":
|
|
14237
|
+
return True
|
|
14238
|
+
|
|
14239
|
+
command = tool_input.get("command")
|
|
14240
|
+
if not isinstance(command, str) or "gh pr create" not in command:
|
|
14241
|
+
return True
|
|
14242
|
+
|
|
14243
|
+
pr_args = argparse.Namespace(url=find_pr_url(hook_payload.get("tool_response")))
|
|
14244
|
+
return send_payload(build_payload("pr-opened", pr_args, control_env), control_env)
|
|
14245
|
+
|
|
14246
|
+
|
|
14198
14247
|
def send_payload(payload, control_env):
|
|
14199
14248
|
request = urllib.request.Request(
|
|
14200
14249
|
control_env["WEBMUX_CONTROL_URL"],
|
|
@@ -14207,7 +14256,7 @@ def send_payload(payload, control_env):
|
|
|
14207
14256
|
)
|
|
14208
14257
|
|
|
14209
14258
|
try:
|
|
14210
|
-
with urllib.request.urlopen(request, timeout=
|
|
14259
|
+
with urllib.request.urlopen(request, timeout=CONTROL_REQUEST_TIMEOUT_SECONDS) as response:
|
|
14211
14260
|
if response.status < 200 or response.status >= 300:
|
|
14212
14261
|
print(f"control endpoint returned HTTP {response.status}", file=sys.stderr)
|
|
14213
14262
|
return False
|
|
@@ -14241,34 +14290,40 @@ def main():
|
|
|
14241
14290
|
print(f"missing control env keys: {', '.join(missing)}", file=sys.stderr)
|
|
14242
14291
|
return 1
|
|
14243
14292
|
|
|
14293
|
+
if parsed.command == "codex-session-start":
|
|
14294
|
+
send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="idle"), control_env), control_env)
|
|
14295
|
+
return 0
|
|
14296
|
+
|
|
14297
|
+
if parsed.command == "codex-user-prompt-submit":
|
|
14298
|
+
send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="running"), control_env), control_env)
|
|
14299
|
+
return 0
|
|
14300
|
+
|
|
14244
14301
|
if parsed.command == "claude-user-prompt-submit":
|
|
14245
14302
|
if not send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="running"), control_env), control_env):
|
|
14246
14303
|
return 1
|
|
14247
14304
|
return 0
|
|
14248
14305
|
|
|
14249
|
-
if parsed.command == "
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
tool_input = hook_payload.get("tool_input")
|
|
14253
|
-
if not isinstance(tool_input, dict) or tool_name != "Bash":
|
|
14254
|
-
return 0
|
|
14306
|
+
if parsed.command == "codex-permission-request":
|
|
14307
|
+
send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="idle"), control_env), control_env)
|
|
14308
|
+
return 0
|
|
14255
14309
|
|
|
14256
|
-
|
|
14257
|
-
|
|
14258
|
-
|
|
14310
|
+
if parsed.command == "codex-post-tool-use":
|
|
14311
|
+
hook_payload = read_hook_payload()
|
|
14312
|
+
maybe_send_pr_opened(hook_payload, control_env)
|
|
14313
|
+
return 0
|
|
14259
14314
|
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
if
|
|
14263
|
-
match = re.search(r"https://github\\.com/[^\\s\\"]+/pull/\\d+", tool_response)
|
|
14264
|
-
if match:
|
|
14265
|
-
pr_args.url = match.group(0)
|
|
14315
|
+
if parsed.command == "claude-post-tool-use":
|
|
14316
|
+
hook_payload = read_hook_payload()
|
|
14317
|
+
return 0 if maybe_send_pr_opened(hook_payload, control_env) else 1
|
|
14266
14318
|
|
|
14267
|
-
|
|
14319
|
+
if parsed.command == "codex-stop":
|
|
14320
|
+
send_payload(build_payload("agent-stopped", parsed, control_env), control_env)
|
|
14321
|
+
print(json.dumps({}))
|
|
14322
|
+
return 0
|
|
14268
14323
|
|
|
14269
14324
|
payload = build_payload(parsed.command, parsed, control_env)
|
|
14270
14325
|
if not send_payload(payload, control_env):
|
|
14271
|
-
return 1
|
|
14326
|
+
return 0 if getattr(parsed, "best_effort", False) else 1
|
|
14272
14327
|
|
|
14273
14328
|
return 0
|
|
14274
14329
|
|
|
@@ -14338,6 +14393,81 @@ function buildClaudeHookSettings(input) {
|
|
|
14338
14393
|
}
|
|
14339
14394
|
};
|
|
14340
14395
|
}
|
|
14396
|
+
function buildCodexHookSettings(input) {
|
|
14397
|
+
const statusCommand = `${shellQuote(input.agentCtlPath)} status-changed --lifecycle running --best-effort`;
|
|
14398
|
+
return {
|
|
14399
|
+
hooks: {
|
|
14400
|
+
SessionStart: [
|
|
14401
|
+
{
|
|
14402
|
+
matcher: "startup|resume|clear",
|
|
14403
|
+
hooks: [
|
|
14404
|
+
{
|
|
14405
|
+
type: "command",
|
|
14406
|
+
command: `${shellQuote(input.agentCtlPath)} codex-session-start`,
|
|
14407
|
+
timeout: 30
|
|
14408
|
+
}
|
|
14409
|
+
]
|
|
14410
|
+
}
|
|
14411
|
+
],
|
|
14412
|
+
UserPromptSubmit: [
|
|
14413
|
+
{
|
|
14414
|
+
hooks: [
|
|
14415
|
+
{
|
|
14416
|
+
type: "command",
|
|
14417
|
+
command: `${shellQuote(input.agentCtlPath)} codex-user-prompt-submit`,
|
|
14418
|
+
timeout: 30
|
|
14419
|
+
}
|
|
14420
|
+
]
|
|
14421
|
+
}
|
|
14422
|
+
],
|
|
14423
|
+
PermissionRequest: [
|
|
14424
|
+
{
|
|
14425
|
+
hooks: [
|
|
14426
|
+
{
|
|
14427
|
+
type: "command",
|
|
14428
|
+
command: `${shellQuote(input.agentCtlPath)} codex-permission-request`,
|
|
14429
|
+
timeout: 30
|
|
14430
|
+
}
|
|
14431
|
+
]
|
|
14432
|
+
}
|
|
14433
|
+
],
|
|
14434
|
+
PreToolUse: [
|
|
14435
|
+
{
|
|
14436
|
+
hooks: [
|
|
14437
|
+
{
|
|
14438
|
+
type: "command",
|
|
14439
|
+
command: statusCommand,
|
|
14440
|
+
timeout: 30
|
|
14441
|
+
}
|
|
14442
|
+
]
|
|
14443
|
+
}
|
|
14444
|
+
],
|
|
14445
|
+
PostToolUse: [
|
|
14446
|
+
{
|
|
14447
|
+
matcher: "Bash",
|
|
14448
|
+
hooks: [
|
|
14449
|
+
{
|
|
14450
|
+
type: "command",
|
|
14451
|
+
command: `${shellQuote(input.agentCtlPath)} codex-post-tool-use`,
|
|
14452
|
+
timeout: 30
|
|
14453
|
+
}
|
|
14454
|
+
]
|
|
14455
|
+
}
|
|
14456
|
+
],
|
|
14457
|
+
Stop: [
|
|
14458
|
+
{
|
|
14459
|
+
hooks: [
|
|
14460
|
+
{
|
|
14461
|
+
type: "command",
|
|
14462
|
+
command: `${shellQuote(input.agentCtlPath)} codex-stop`,
|
|
14463
|
+
timeout: 30
|
|
14464
|
+
}
|
|
14465
|
+
]
|
|
14466
|
+
}
|
|
14467
|
+
]
|
|
14468
|
+
}
|
|
14469
|
+
};
|
|
14470
|
+
}
|
|
14341
14471
|
async function mergeClaudeSettings(settingsPath, hookSettings) {
|
|
14342
14472
|
let existing = {};
|
|
14343
14473
|
try {
|
|
@@ -14357,13 +14487,77 @@ async function mergeClaudeSettings(settingsPath, hookSettings) {
|
|
|
14357
14487
|
await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + `
|
|
14358
14488
|
`);
|
|
14359
14489
|
}
|
|
14490
|
+
function commandStartsWithAgentCtl(command, agentCtlPath) {
|
|
14491
|
+
const trimmedCommand = command.trimStart();
|
|
14492
|
+
const quotedAgentCtlPath = shellQuote(agentCtlPath);
|
|
14493
|
+
return trimmedCommand === agentCtlPath || trimmedCommand.startsWith(`${agentCtlPath} `) || trimmedCommand === quotedAgentCtlPath || trimmedCommand.startsWith(`${quotedAgentCtlPath} `);
|
|
14494
|
+
}
|
|
14495
|
+
function isWebmuxHookGroup(group, agentCtlPath) {
|
|
14496
|
+
if (!isRecord5(group) || !Array.isArray(group.hooks))
|
|
14497
|
+
return false;
|
|
14498
|
+
return group.hooks.some((hook) => isRecord5(hook) && typeof hook.command === "string" && commandStartsWithAgentCtl(hook.command, agentCtlPath));
|
|
14499
|
+
}
|
|
14500
|
+
async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
|
|
14501
|
+
let existing = {};
|
|
14502
|
+
try {
|
|
14503
|
+
const file = Bun.file(hooksPath);
|
|
14504
|
+
if (await file.exists()) {
|
|
14505
|
+
const parsed = await file.json();
|
|
14506
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
14507
|
+
existing = parsed;
|
|
14508
|
+
}
|
|
14509
|
+
}
|
|
14510
|
+
} catch {
|
|
14511
|
+
existing = {};
|
|
14512
|
+
}
|
|
14513
|
+
const existingHooks = isRecord5(existing.hooks) ? existing.hooks : {};
|
|
14514
|
+
const mergedHooks = { ...existingHooks };
|
|
14515
|
+
for (const [eventName, groups] of Object.entries(hookSettings)) {
|
|
14516
|
+
const eventGroups = existingHooks[eventName];
|
|
14517
|
+
const preservedGroups = Array.isArray(eventGroups) ? eventGroups.filter((group) => !isWebmuxHookGroup(group, agentCtlPath)) : [];
|
|
14518
|
+
mergedHooks[eventName] = [...preservedGroups, ...groups];
|
|
14519
|
+
}
|
|
14520
|
+
await Bun.write(hooksPath, JSON.stringify({ ...existing, hooks: mergedHooks }, null, 2) + `
|
|
14521
|
+
`);
|
|
14522
|
+
}
|
|
14523
|
+
async function resolveGitCommonDir(gitDir) {
|
|
14524
|
+
try {
|
|
14525
|
+
const commonDir = (await Bun.file(join4(gitDir, "commondir")).text()).trim();
|
|
14526
|
+
if (!commonDir)
|
|
14527
|
+
return gitDir;
|
|
14528
|
+
return commonDir.startsWith("/") ? commonDir : resolve3(gitDir, commonDir);
|
|
14529
|
+
} catch {
|
|
14530
|
+
return gitDir;
|
|
14531
|
+
}
|
|
14532
|
+
}
|
|
14533
|
+
async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
14534
|
+
const commonDir = await resolveGitCommonDir(gitDir);
|
|
14535
|
+
const excludePath = join4(commonDir, "info", "exclude");
|
|
14536
|
+
let existing = "";
|
|
14537
|
+
try {
|
|
14538
|
+
existing = await Bun.file(excludePath).text();
|
|
14539
|
+
} catch {
|
|
14540
|
+
existing = "";
|
|
14541
|
+
}
|
|
14542
|
+
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
14543
|
+
if (lines.includes(GENERATED_CODEX_HOOKS_EXCLUDE))
|
|
14544
|
+
return;
|
|
14545
|
+
await mkdir3(dirname3(excludePath), { recursive: true });
|
|
14546
|
+
const separator = existing.length > 0 && !existing.endsWith(`
|
|
14547
|
+
`) ? `
|
|
14548
|
+
` : "";
|
|
14549
|
+
await Bun.write(excludePath, `${existing}${separator}${GENERATED_CODEX_HOOKS_EXCLUDE}
|
|
14550
|
+
`);
|
|
14551
|
+
}
|
|
14360
14552
|
async function ensureAgentRuntimeArtifacts(input) {
|
|
14361
14553
|
const storagePaths = getWorktreeStoragePaths(input.gitDir);
|
|
14362
14554
|
const artifacts = {
|
|
14363
14555
|
agentCtlPath: join4(storagePaths.webmuxDir, "webmux-agentctl"),
|
|
14364
|
-
claudeSettingsPath: join4(input.worktreePath, ".claude", "settings.local.json")
|
|
14556
|
+
claudeSettingsPath: join4(input.worktreePath, ".claude", "settings.local.json"),
|
|
14557
|
+
codexHooksPath: join4(input.worktreePath, ".codex", "hooks.json")
|
|
14365
14558
|
};
|
|
14366
14559
|
await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
|
|
14560
|
+
await mkdir3(dirname3(artifacts.codexHooksPath), { recursive: true });
|
|
14367
14561
|
await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
|
|
14368
14562
|
await chmod2(artifacts.agentCtlPath, 493);
|
|
14369
14563
|
const hookSettings = buildClaudeHookSettings(artifacts);
|
|
@@ -14372,12 +14566,14 @@ async function ensureAgentRuntimeArtifacts(input) {
|
|
|
14372
14566
|
throw new Error("Invalid Claude hook settings");
|
|
14373
14567
|
}
|
|
14374
14568
|
await mergeClaudeSettings(artifacts.claudeSettingsPath, hooks);
|
|
14569
|
+
await ensureGeneratedCodexHooksIgnored(input.gitDir);
|
|
14570
|
+
await mergeCodexHooksFile(artifacts.codexHooksPath, buildCodexHookSettings(artifacts).hooks, artifacts.agentCtlPath);
|
|
14375
14571
|
return artifacts;
|
|
14376
14572
|
}
|
|
14377
14573
|
|
|
14378
14574
|
// backend/src/adapters/tmux.ts
|
|
14379
14575
|
import { createHash } from "crypto";
|
|
14380
|
-
import { basename as basename2, resolve as
|
|
14576
|
+
import { basename as basename2, resolve as resolve4 } from "path";
|
|
14381
14577
|
function runTmux(args) {
|
|
14382
14578
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
14383
14579
|
stdout: "pipe",
|
|
@@ -14405,7 +14601,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
|
14405
14601
|
return trimmed || "x";
|
|
14406
14602
|
}
|
|
14407
14603
|
function buildProjectSessionName(projectRoot2) {
|
|
14408
|
-
const resolved =
|
|
14604
|
+
const resolved = resolve4(projectRoot2);
|
|
14409
14605
|
const base = sanitizeTmuxNameSegment(basename2(resolved), 18);
|
|
14410
14606
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
14411
14607
|
return `wm-${base}-${hash}`;
|
|
@@ -14546,15 +14742,16 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
14546
14742
|
}
|
|
14547
14743
|
function buildBuiltInAgentInvocation(input) {
|
|
14548
14744
|
if (input.agent === "codex") {
|
|
14745
|
+
const hooksFlag = " --enable codex_hooks";
|
|
14549
14746
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
14550
14747
|
if (input.launchMode === "resume") {
|
|
14551
|
-
return `codex${yoloFlag2} resume --last`;
|
|
14748
|
+
return `codex${hooksFlag}${yoloFlag2} resume --last`;
|
|
14552
14749
|
}
|
|
14553
14750
|
const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
14554
14751
|
if (input.systemPrompt) {
|
|
14555
|
-
return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
|
|
14752
|
+
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
|
|
14556
14753
|
}
|
|
14557
|
-
return `codex${yoloFlag2}${promptSuffix2}`;
|
|
14754
|
+
return `codex${hooksFlag}${yoloFlag2}${promptSuffix2}`;
|
|
14558
14755
|
}
|
|
14559
14756
|
const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
|
|
14560
14757
|
if (input.launchMode === "resume") {
|
|
@@ -14627,7 +14824,7 @@ function buildDockerAgentPaneCommand(input) {
|
|
|
14627
14824
|
}
|
|
14628
14825
|
|
|
14629
14826
|
// backend/src/services/session-service.ts
|
|
14630
|
-
import { resolve as
|
|
14827
|
+
import { resolve as resolve5 } from "path";
|
|
14631
14828
|
function quoteShell2(value) {
|
|
14632
14829
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14633
14830
|
}
|
|
@@ -14641,7 +14838,7 @@ function buildCommandPaneStartupCommand(template, ctx) {
|
|
|
14641
14838
|
if (!template.workingDir) {
|
|
14642
14839
|
return template.command;
|
|
14643
14840
|
}
|
|
14644
|
-
const workingDir =
|
|
14841
|
+
const workingDir = resolve5(resolvePaneCwd(template, ctx), template.workingDir);
|
|
14645
14842
|
return `cd -- ${quoteShell2(workingDir)} && ${template.command}`;
|
|
14646
14843
|
}
|
|
14647
14844
|
function resolvePaneStartupCommand(template, ctx) {
|
|
@@ -14721,7 +14918,7 @@ import { randomUUID } from "crypto";
|
|
|
14721
14918
|
|
|
14722
14919
|
// backend/src/adapters/git.ts
|
|
14723
14920
|
import { readdirSync, rmSync, statSync } from "fs";
|
|
14724
|
-
import { resolve as
|
|
14921
|
+
import { resolve as resolve6, join as join5 } from "path";
|
|
14725
14922
|
function runGit(args, cwd) {
|
|
14726
14923
|
const result = Bun.spawnSync(["git", ...args], {
|
|
14727
14924
|
cwd,
|
|
@@ -14755,8 +14952,8 @@ function errorMessage(error) {
|
|
|
14755
14952
|
return error instanceof Error ? error.message : String(error);
|
|
14756
14953
|
}
|
|
14757
14954
|
function isRegisteredWorktree(entries, worktreePath) {
|
|
14758
|
-
const resolvedPath =
|
|
14759
|
-
return entries.some((entry) =>
|
|
14955
|
+
const resolvedPath = resolve6(worktreePath);
|
|
14956
|
+
return entries.some((entry) => resolve6(entry.path) === resolvedPath);
|
|
14760
14957
|
}
|
|
14761
14958
|
function removeDirectory(path) {
|
|
14762
14959
|
rmSync(path, {
|
|
@@ -14780,7 +14977,7 @@ function currentCheckoutRef(cwd) {
|
|
|
14780
14977
|
function resolveRepoRoot(dir) {
|
|
14781
14978
|
const direct = tryRunGit(["rev-parse", "--show-toplevel"], dir);
|
|
14782
14979
|
if (direct.ok)
|
|
14783
|
-
return
|
|
14980
|
+
return resolve6(dir, direct.stdout);
|
|
14784
14981
|
let entries;
|
|
14785
14982
|
try {
|
|
14786
14983
|
entries = readdirSync(dir);
|
|
@@ -14797,17 +14994,17 @@ function resolveRepoRoot(dir) {
|
|
|
14797
14994
|
}
|
|
14798
14995
|
const childResult = tryRunGit(["rev-parse", "--show-toplevel"], child);
|
|
14799
14996
|
if (childResult.ok)
|
|
14800
|
-
return
|
|
14997
|
+
return resolve6(child, childResult.stdout);
|
|
14801
14998
|
}
|
|
14802
14999
|
return null;
|
|
14803
15000
|
}
|
|
14804
15001
|
function resolveWorktreeRoot(cwd) {
|
|
14805
15002
|
const output = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
14806
|
-
return
|
|
15003
|
+
return resolve6(cwd, output);
|
|
14807
15004
|
}
|
|
14808
15005
|
function resolveWorktreeGitDir(cwd) {
|
|
14809
15006
|
const output = runGit(["rev-parse", "--git-dir"], cwd);
|
|
14810
|
-
return
|
|
15007
|
+
return resolve6(cwd, output);
|
|
14811
15008
|
}
|
|
14812
15009
|
function parseGitWorktreePorcelain(output) {
|
|
14813
15010
|
const entries = [];
|
|
@@ -15445,20 +15642,20 @@ class LifecycleService {
|
|
|
15445
15642
|
return allocateServicePorts(metas, this.deps.config.services);
|
|
15446
15643
|
}
|
|
15447
15644
|
resolveWorktreePath(branch) {
|
|
15448
|
-
return
|
|
15645
|
+
return resolve7(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
|
|
15449
15646
|
}
|
|
15450
15647
|
listLocalBranches() {
|
|
15451
|
-
return this.deps.git.listLocalBranches(
|
|
15648
|
+
return this.deps.git.listLocalBranches(resolve7(this.deps.projectRoot));
|
|
15452
15649
|
}
|
|
15453
15650
|
listRemoteBranches() {
|
|
15454
|
-
return this.deps.git.listRemoteBranches(
|
|
15651
|
+
return this.deps.git.listRemoteBranches(resolve7(this.deps.projectRoot));
|
|
15455
15652
|
}
|
|
15456
15653
|
listCheckedOutBranches() {
|
|
15457
|
-
return new Set(this.deps.git.listWorktrees(
|
|
15654
|
+
return new Set(this.deps.git.listWorktrees(resolve7(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
|
|
15458
15655
|
}
|
|
15459
15656
|
listProjectWorktrees() {
|
|
15460
|
-
const projectRoot2 =
|
|
15461
|
-
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare &&
|
|
15657
|
+
const projectRoot2 = resolve7(this.deps.projectRoot);
|
|
15658
|
+
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve7(entry.path) !== projectRoot2);
|
|
15462
15659
|
}
|
|
15463
15660
|
async readManagedMetas() {
|
|
15464
15661
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -17317,7 +17514,7 @@ class BunPortProbe {
|
|
|
17317
17514
|
this.hostnames = hostnames;
|
|
17318
17515
|
}
|
|
17319
17516
|
isListening(port) {
|
|
17320
|
-
return new Promise((
|
|
17517
|
+
return new Promise((resolve8) => {
|
|
17321
17518
|
let settled = false;
|
|
17322
17519
|
let pending = this.hostnames.length;
|
|
17323
17520
|
const settle = (result) => {
|
|
@@ -17326,20 +17523,20 @@ class BunPortProbe {
|
|
|
17326
17523
|
if (result) {
|
|
17327
17524
|
settled = true;
|
|
17328
17525
|
clearTimeout(timer);
|
|
17329
|
-
|
|
17526
|
+
resolve8(true);
|
|
17330
17527
|
return;
|
|
17331
17528
|
}
|
|
17332
17529
|
pending--;
|
|
17333
17530
|
if (pending === 0) {
|
|
17334
17531
|
settled = true;
|
|
17335
17532
|
clearTimeout(timer);
|
|
17336
|
-
|
|
17533
|
+
resolve8(false);
|
|
17337
17534
|
}
|
|
17338
17535
|
};
|
|
17339
17536
|
const timer = setTimeout(() => {
|
|
17340
17537
|
if (!settled) {
|
|
17341
17538
|
settled = true;
|
|
17342
|
-
|
|
17539
|
+
resolve8(false);
|
|
17343
17540
|
}
|
|
17344
17541
|
}, this.timeoutMs);
|
|
17345
17542
|
for (const hostname of this.hostnames) {
|
|
@@ -17417,7 +17614,7 @@ async function defaultSpawn(args, options = {}) {
|
|
|
17417
17614
|
if (options.timeoutMs === undefined) {
|
|
17418
17615
|
return await resultPromise;
|
|
17419
17616
|
}
|
|
17420
|
-
return await new Promise((
|
|
17617
|
+
return await new Promise((resolve8, reject) => {
|
|
17421
17618
|
let settled = false;
|
|
17422
17619
|
const timeoutId = setTimeout(() => {
|
|
17423
17620
|
if (settled)
|
|
@@ -17433,7 +17630,7 @@ async function defaultSpawn(args, options = {}) {
|
|
|
17433
17630
|
return;
|
|
17434
17631
|
settled = true;
|
|
17435
17632
|
clearTimeout(timeoutId);
|
|
17436
|
-
|
|
17633
|
+
resolve8(result);
|
|
17437
17634
|
}, (error) => {
|
|
17438
17635
|
if (settled)
|
|
17439
17636
|
return;
|
|
@@ -17571,8 +17768,8 @@ class ArchiveStateService {
|
|
|
17571
17768
|
async withMutationLock(operation) {
|
|
17572
17769
|
const previous = this.mutationQueue;
|
|
17573
17770
|
let release = () => {};
|
|
17574
|
-
this.mutationQueue = new Promise((
|
|
17575
|
-
release =
|
|
17771
|
+
this.mutationQueue = new Promise((resolve8) => {
|
|
17772
|
+
release = resolve8;
|
|
17576
17773
|
});
|
|
17577
17774
|
await previous.catch(() => {});
|
|
17578
17775
|
try {
|
|
@@ -17847,9 +18044,9 @@ class ProjectRuntime {
|
|
|
17847
18044
|
}
|
|
17848
18045
|
|
|
17849
18046
|
// backend/src/services/reconciliation-service.ts
|
|
17850
|
-
import { basename as basename3, resolve as
|
|
18047
|
+
import { basename as basename3, resolve as resolve8 } from "path";
|
|
17851
18048
|
function makeUnmanagedWorktreeId(path) {
|
|
17852
|
-
return `unmanaged:${
|
|
18049
|
+
return `unmanaged:${resolve8(path)}`;
|
|
17853
18050
|
}
|
|
17854
18051
|
function isValidPort2(port) {
|
|
17855
18052
|
return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
@@ -17906,7 +18103,7 @@ class ReconciliationService {
|
|
|
17906
18103
|
if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
|
|
17907
18104
|
return;
|
|
17908
18105
|
}
|
|
17909
|
-
const normalizedRepoRoot =
|
|
18106
|
+
const normalizedRepoRoot = resolve8(repoRoot);
|
|
17910
18107
|
const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
|
|
17911
18108
|
this.lastReconciledAt = this.now();
|
|
17912
18109
|
});
|
|
@@ -17925,7 +18122,7 @@ class ReconciliationService {
|
|
|
17925
18122
|
windows = [];
|
|
17926
18123
|
}
|
|
17927
18124
|
const seenWorktreeIds = new Set;
|
|
17928
|
-
const candidateEntries = worktrees.filter((entry) => !entry.bare &&
|
|
18125
|
+
const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve8(entry.path) !== normalizedRepoRoot);
|
|
17929
18126
|
const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
|
|
17930
18127
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
17931
18128
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -18166,7 +18363,7 @@ function getFrontendConfig() {
|
|
|
18166
18363
|
startupEnvs: config.startupEnvs,
|
|
18167
18364
|
linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
|
|
18168
18365
|
alias: lr.alias,
|
|
18169
|
-
...lr.dir ? { dir:
|
|
18366
|
+
...lr.dir ? { dir: resolve9(PROJECT_DIR, lr.dir) } : {}
|
|
18170
18367
|
})),
|
|
18171
18368
|
linearAutoCreateWorktrees: linearAutoCreateEnabled,
|
|
18172
18369
|
autoRemoveOnMerge: autoRemoveOnMergeEnabled,
|
|
@@ -18270,7 +18467,8 @@ async function resolveTerminalWorktree(branch) {
|
|
|
18270
18467
|
attachTarget: {
|
|
18271
18468
|
ownerSessionName: state.session.sessionName,
|
|
18272
18469
|
windowName: state.session.windowName
|
|
18273
|
-
}
|
|
18470
|
+
},
|
|
18471
|
+
agentName: state.agentName
|
|
18274
18472
|
};
|
|
18275
18473
|
}
|
|
18276
18474
|
async function resolveAgentsTerminalWorktree(branch) {
|
|
@@ -18318,9 +18516,9 @@ async function hasValidControlToken(req) {
|
|
|
18318
18516
|
}
|
|
18319
18517
|
async function getWorktreeGitDirs() {
|
|
18320
18518
|
const gitDirs = new Map;
|
|
18321
|
-
const projectRoot2 =
|
|
18519
|
+
const projectRoot2 = resolve9(PROJECT_DIR);
|
|
18322
18520
|
for (const entry of git.listWorktrees(projectRoot2)) {
|
|
18323
|
-
if (entry.bare ||
|
|
18521
|
+
if (entry.bare || resolve9(entry.path) === projectRoot2 || !entry.branch)
|
|
18324
18522
|
continue;
|
|
18325
18523
|
gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
|
|
18326
18524
|
}
|
|
@@ -18403,6 +18601,12 @@ function resolveWorktreeAgentChatSupport(worktree, action) {
|
|
|
18403
18601
|
action
|
|
18404
18602
|
});
|
|
18405
18603
|
}
|
|
18604
|
+
function resolveWorktreeTerminalSubmitDelayMs(agentName) {
|
|
18605
|
+
return resolveAgentTerminalSubmitDelayMs({
|
|
18606
|
+
agentId: agentName,
|
|
18607
|
+
agent: agentName ? getAgentDefinition(config, agentName) : null
|
|
18608
|
+
});
|
|
18609
|
+
}
|
|
18406
18610
|
async function apiAttachAgentsWorktree(branch) {
|
|
18407
18611
|
touchDashboardActivity();
|
|
18408
18612
|
const resolved = await resolveAgentsWorktree(branch);
|
|
@@ -18734,7 +18938,8 @@ async function apiSendPrompt(name, req) {
|
|
|
18734
18938
|
const preamble = body.preamble;
|
|
18735
18939
|
log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
|
|
18736
18940
|
const terminalWorktree = await resolveTerminalWorktree(name);
|
|
18737
|
-
const
|
|
18941
|
+
const submitDelayMs = resolveWorktreeTerminalSubmitDelayMs(terminalWorktree.agentName);
|
|
18942
|
+
const result = await sendPrompt(terminalWorktree.worktreeId, terminalWorktree.attachTarget, text, 0, preamble, submitDelayMs);
|
|
18738
18943
|
if (!result.ok)
|
|
18739
18944
|
return errorResponse(result.error, 503);
|
|
18740
18945
|
return jsonResponse({ ok: true });
|
|
@@ -18854,7 +19059,7 @@ async function apiPullMain(req) {
|
|
|
18854
19059
|
return errorResponse(`Unknown linked repo: ${repo}`, 404);
|
|
18855
19060
|
if (!linkedRepo.dir)
|
|
18856
19061
|
return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
|
|
18857
|
-
const resolvedDir =
|
|
19062
|
+
const resolvedDir = resolve9(PROJECT_DIR, linkedRepo.dir);
|
|
18858
19063
|
const repoRoot = git.resolveRepoRoot(resolvedDir);
|
|
18859
19064
|
if (!repoRoot)
|
|
18860
19065
|
return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
|
|
@@ -18945,7 +19150,7 @@ async function apiUploadFiles(name, req) {
|
|
|
18945
19150
|
}
|
|
18946
19151
|
const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
|
|
18947
19152
|
const destPath = join8(uploadDir, safeName);
|
|
18948
|
-
if (!
|
|
19153
|
+
if (!resolve9(destPath).startsWith(uploadDir + "/")) {
|
|
18949
19154
|
return errorResponse("Invalid filename", 400);
|
|
18950
19155
|
}
|
|
18951
19156
|
await Bun.write(destPath, entry);
|
|
@@ -19214,8 +19419,8 @@ Bun.serve({
|
|
|
19214
19419
|
if (STATIC_DIR) {
|
|
19215
19420
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
19216
19421
|
const filePath = join8(STATIC_DIR, rawPath);
|
|
19217
|
-
const staticRoot =
|
|
19218
|
-
if (!
|
|
19422
|
+
const staticRoot = resolve9(STATIC_DIR);
|
|
19423
|
+
if (!resolve9(filePath).startsWith(staticRoot + "/")) {
|
|
19219
19424
|
return new Response("Forbidden", { status: 403 });
|
|
19220
19425
|
}
|
|
19221
19426
|
const file = Bun.file(filePath);
|