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.
@@ -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 resolve8 } from "path";
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.agent.implementation.agent === "codex" ? 200 : 0
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 resolve6 } from "path";
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=10) as response:
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 == "claude-post-tool-use":
14250
- hook_payload = read_hook_payload()
14251
- tool_name = hook_payload.get("tool_name")
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
- command = tool_input.get("command")
14257
- if not isinstance(command, str) or "gh pr create" not in command:
14258
- return 0
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
- pr_args = argparse.Namespace(url=None)
14261
- tool_response = hook_payload.get("tool_response")
14262
- if isinstance(tool_response, str):
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
- return 0 if send_payload(build_payload("pr-opened", pr_args, control_env), control_env) else 1
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 resolve3 } from "path";
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 = resolve3(projectRoot2);
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 resolve4 } from "path";
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 = resolve4(resolvePaneCwd(template, ctx), template.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 resolve5, join as join5 } from "path";
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 = resolve5(worktreePath);
14759
- return entries.some((entry) => resolve5(entry.path) === resolvedPath);
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 resolve5(dir, direct.stdout);
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 resolve5(child, childResult.stdout);
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 resolve5(cwd, output);
15003
+ return resolve6(cwd, output);
14807
15004
  }
14808
15005
  function resolveWorktreeGitDir(cwd) {
14809
15006
  const output = runGit(["rev-parse", "--git-dir"], cwd);
14810
- return resolve5(cwd, output);
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 resolve6(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
15645
+ return resolve7(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
15449
15646
  }
15450
15647
  listLocalBranches() {
15451
- return this.deps.git.listLocalBranches(resolve6(this.deps.projectRoot));
15648
+ return this.deps.git.listLocalBranches(resolve7(this.deps.projectRoot));
15452
15649
  }
15453
15650
  listRemoteBranches() {
15454
- return this.deps.git.listRemoteBranches(resolve6(this.deps.projectRoot));
15651
+ return this.deps.git.listRemoteBranches(resolve7(this.deps.projectRoot));
15455
15652
  }
15456
15653
  listCheckedOutBranches() {
15457
- return new Set(this.deps.git.listWorktrees(resolve6(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
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 = resolve6(this.deps.projectRoot);
15461
- return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve6(entry.path) !== projectRoot2);
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((resolve7) => {
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
- resolve7(true);
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
- resolve7(false);
17533
+ resolve8(false);
17337
17534
  }
17338
17535
  };
17339
17536
  const timer = setTimeout(() => {
17340
17537
  if (!settled) {
17341
17538
  settled = true;
17342
- resolve7(false);
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((resolve7, reject) => {
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
- resolve7(result);
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((resolve7) => {
17575
- release = resolve7;
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 resolve7 } from "path";
18047
+ import { basename as basename3, resolve as resolve8 } from "path";
17851
18048
  function makeUnmanagedWorktreeId(path) {
17852
- return `unmanaged:${resolve7(path)}`;
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 = resolve7(repoRoot);
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 && resolve7(entry.path) !== normalizedRepoRoot);
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: resolve8(PROJECT_DIR, lr.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 = resolve8(PROJECT_DIR);
18519
+ const projectRoot2 = resolve9(PROJECT_DIR);
18322
18520
  for (const entry of git.listWorktrees(projectRoot2)) {
18323
- if (entry.bare || resolve8(entry.path) === projectRoot2 || !entry.branch)
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 result = await sendPrompt(terminalWorktree.worktreeId, terminalWorktree.attachTarget, text, 0, preamble);
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 = resolve8(PROJECT_DIR, linkedRepo.dir);
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 (!resolve8(destPath).startsWith(uploadDir + "/")) {
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 = resolve8(STATIC_DIR);
19218
- if (!resolve8(filePath).startsWith(staticRoot + "/")) {
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);