webmux 0.28.1 → 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
 
@@ -10999,9 +10999,45 @@ var EnabledResponseSchema = exports_external.object({
10999
10999
  ok: exports_external.literal(true),
11000
11000
  enabled: exports_external.boolean()
11001
11001
  });
11002
- var AgentKindSchema = exports_external.enum(["claude", "codex"]);
11003
- var CreateWorktreeAgentSelectionSchema = exports_external.enum(["claude", "codex", "both"]);
11002
+ var BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
11003
+ var AgentIdSchema = exports_external.string().trim().min(1);
11004
11004
  var WorktreeCreateModeSchema = exports_external.enum(["new", "existing"]);
11005
+ var AgentCapabilitiesSchema = exports_external.object({
11006
+ terminal: exports_external.literal(true),
11007
+ inAppChat: exports_external.boolean(),
11008
+ conversationHistory: exports_external.boolean(),
11009
+ interrupt: exports_external.boolean(),
11010
+ resume: exports_external.boolean()
11011
+ });
11012
+ var AgentSummarySchema = exports_external.object({
11013
+ id: AgentIdSchema,
11014
+ label: exports_external.string(),
11015
+ kind: exports_external.enum(["builtin", "custom"]),
11016
+ capabilities: AgentCapabilitiesSchema
11017
+ });
11018
+ var AgentDetailsSchema = exports_external.object({
11019
+ id: AgentIdSchema,
11020
+ label: exports_external.string(),
11021
+ kind: exports_external.enum(["builtin", "custom"]),
11022
+ capabilities: AgentCapabilitiesSchema,
11023
+ startCommand: exports_external.string().nullable(),
11024
+ resumeCommand: exports_external.string().nullable()
11025
+ });
11026
+ var AgentListResponseSchema = exports_external.object({
11027
+ agents: exports_external.array(AgentDetailsSchema)
11028
+ });
11029
+ var UpsertCustomAgentRequestSchema = exports_external.object({
11030
+ label: exports_external.string().trim().min(1),
11031
+ startCommand: exports_external.string().trim().min(1),
11032
+ resumeCommand: exports_external.string().trim().optional()
11033
+ });
11034
+ var AgentResponseSchema = exports_external.object({
11035
+ agent: AgentDetailsSchema
11036
+ });
11037
+ var ValidateCustomAgentResponseSchema = exports_external.object({
11038
+ normalizedId: AgentIdSchema,
11039
+ warnings: exports_external.array(exports_external.string())
11040
+ });
11005
11041
  var WorktreeCreationPhaseSchema = exports_external.enum([
11006
11042
  "creating_worktree",
11007
11043
  "preparing_runtime",
@@ -11027,7 +11063,8 @@ var CreateWorktreeRequestSchema = exports_external.object({
11027
11063
  branch: exports_external.string().optional(),
11028
11064
  baseBranch: exports_external.string().optional(),
11029
11065
  profile: exports_external.string().optional(),
11030
- agent: CreateWorktreeAgentSelectionSchema.optional(),
11066
+ agent: AgentIdSchema.optional(),
11067
+ agents: exports_external.array(AgentIdSchema).min(1).optional(),
11031
11068
  prompt: exports_external.string().optional(),
11032
11069
  envOverrides: exports_external.record(exports_external.string()).optional(),
11033
11070
  createLinearTicket: exports_external.literal(true).optional(),
@@ -11158,7 +11195,8 @@ var ProjectWorktreeSnapshotSchema = exports_external.object({
11158
11195
  dir: exports_external.string(),
11159
11196
  archived: exports_external.boolean(),
11160
11197
  profile: exports_external.string().nullable(),
11161
- agentName: AgentKindSchema.nullable(),
11198
+ agentName: AgentIdSchema.nullable(),
11199
+ agentLabel: exports_external.string().nullable(),
11162
11200
  mux: exports_external.boolean(),
11163
11201
  dirty: exports_external.boolean(),
11164
11202
  unpushed: exports_external.boolean(),
@@ -11203,7 +11241,8 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
11203
11241
  path: exports_external.string(),
11204
11242
  archived: exports_external.boolean(),
11205
11243
  profile: exports_external.string().nullable(),
11206
- agentName: AgentKindSchema.nullable(),
11244
+ agentName: AgentIdSchema.nullable(),
11245
+ agentLabel: exports_external.string().nullable(),
11207
11246
  mux: exports_external.boolean(),
11208
11247
  status: exports_external.string(),
11209
11248
  dirty: exports_external.boolean(),
@@ -11295,7 +11334,9 @@ var AppConfigSchema = exports_external.object({
11295
11334
  name: exports_external.string(),
11296
11335
  services: exports_external.array(ServiceConfigSchema),
11297
11336
  profiles: exports_external.array(ProfileConfigSchema),
11337
+ agents: exports_external.array(AgentSummarySchema),
11298
11338
  defaultProfileName: exports_external.string(),
11339
+ defaultAgentId: BuiltInAgentIdSchema,
11299
11340
  autoName: exports_external.boolean(),
11300
11341
  linearCreateTicketOption: exports_external.boolean(),
11301
11342
  startupEnvs: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
@@ -11314,6 +11355,9 @@ var WorktreeNameParamsSchema = exports_external.object({
11314
11355
  var NotificationIdParamsSchema = exports_external.object({
11315
11356
  id: NumberLikePathParamSchema
11316
11357
  });
11358
+ var AgentIdParamsSchema = exports_external.object({
11359
+ id: AgentIdSchema
11360
+ });
11317
11361
  var RunIdParamsSchema = exports_external.object({
11318
11362
  runId: NumberLikePathParamSchema
11319
11363
  });
@@ -11325,6 +11369,11 @@ var apiPaths = {
11325
11369
  fetchAvailableBranches: "/api/branches",
11326
11370
  fetchBaseBranches: "/api/base-branches",
11327
11371
  fetchProject: "/api/project",
11372
+ fetchAgents: "/api/agents",
11373
+ createAgent: "/api/agents",
11374
+ updateAgent: "/api/agents/:id",
11375
+ deleteAgent: "/api/agents/:id",
11376
+ validateAgent: "/api/agents/validate",
11328
11377
  attachAgentsWorktreeConversation: "/api/agents/worktrees/:name/attach",
11329
11378
  fetchAgentsWorktreeConversationHistory: "/api/agents/worktrees/:name/history",
11330
11379
  sendAgentsWorktreeConversationMessage: "/api/agents/worktrees/:name/messages",
@@ -11389,6 +11438,60 @@ var apiContract = c.router({
11389
11438
  502: ErrorResponseSchema
11390
11439
  }
11391
11440
  },
11441
+ fetchAgents: {
11442
+ method: "GET",
11443
+ path: apiPaths.fetchAgents,
11444
+ responses: {
11445
+ 200: AgentListResponseSchema,
11446
+ 500: ErrorResponseSchema
11447
+ }
11448
+ },
11449
+ createAgent: {
11450
+ method: "POST",
11451
+ path: apiPaths.createAgent,
11452
+ body: UpsertCustomAgentRequestSchema,
11453
+ responses: {
11454
+ 200: AgentResponseSchema,
11455
+ 400: ErrorResponseSchema,
11456
+ 409: ErrorResponseSchema,
11457
+ 500: ErrorResponseSchema
11458
+ }
11459
+ },
11460
+ updateAgent: {
11461
+ method: "PUT",
11462
+ path: apiPaths.updateAgent,
11463
+ pathParams: AgentIdParamsSchema,
11464
+ body: UpsertCustomAgentRequestSchema,
11465
+ responses: {
11466
+ 200: AgentResponseSchema,
11467
+ 400: ErrorResponseSchema,
11468
+ 404: ErrorResponseSchema,
11469
+ 409: ErrorResponseSchema,
11470
+ 500: ErrorResponseSchema
11471
+ }
11472
+ },
11473
+ deleteAgent: {
11474
+ method: "DELETE",
11475
+ path: apiPaths.deleteAgent,
11476
+ pathParams: AgentIdParamsSchema,
11477
+ body: c.noBody(),
11478
+ responses: {
11479
+ 200: OkResponseSchema,
11480
+ 400: ErrorResponseSchema,
11481
+ 404: ErrorResponseSchema,
11482
+ 500: ErrorResponseSchema
11483
+ }
11484
+ },
11485
+ validateAgent: {
11486
+ method: "POST",
11487
+ path: apiPaths.validateAgent,
11488
+ body: UpsertCustomAgentRequestSchema,
11489
+ responses: {
11490
+ 200: ValidateCustomAgentResponseSchema,
11491
+ 400: ErrorResponseSchema,
11492
+ 500: ErrorResponseSchema
11493
+ }
11494
+ },
11392
11495
  attachAgentsWorktreeConversation: {
11393
11496
  method: "POST",
11394
11497
  path: apiPaths.attachAgentsWorktreeConversation,
@@ -11866,7 +11969,7 @@ async function sendPrompt(worktreeId, target, text, paneIndex = 0, preamble, sub
11866
11969
  if (load.exitCode !== 0) {
11867
11970
  return { ok: false, error: `load-buffer failed${load.stderr ? `: ${load.stderr}` : ""}` };
11868
11971
  }
11869
- 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"]);
11870
11973
  if (paste.exitCode !== 0) {
11871
11974
  return { ok: false, error: `paste-buffer failed${paste.stderr ? `: ${paste.stderr}` : ""}` };
11872
11975
  }
@@ -12691,6 +12794,7 @@ var DEFAULT_CONFIG = {
12691
12794
  panes: clonePanes(DEFAULT_PANES)
12692
12795
  }
12693
12796
  },
12797
+ agents: {},
12694
12798
  services: [],
12695
12799
  startupEnvs: {},
12696
12800
  integrations: {
@@ -12808,6 +12912,38 @@ function parseProfiles(raw, includeDefaultProfile) {
12808
12912
  }
12809
12913
  return profiles;
12810
12914
  }
12915
+ function cloneAgentConfig(agent) {
12916
+ return { ...agent };
12917
+ }
12918
+ function cloneAgents(agents) {
12919
+ return Object.fromEntries(Object.entries(agents).map(([id, agent]) => [id, cloneAgentConfig(agent)]));
12920
+ }
12921
+ function parseCustomAgent(raw) {
12922
+ if (!isRecord3(raw))
12923
+ return null;
12924
+ if (typeof raw.label !== "string" || !raw.label.trim())
12925
+ return null;
12926
+ if (typeof raw.startCommand !== "string" || !raw.startCommand.trim())
12927
+ return null;
12928
+ return {
12929
+ label: raw.label.trim(),
12930
+ startCommand: raw.startCommand.trim(),
12931
+ ...typeof raw.resumeCommand === "string" && raw.resumeCommand.trim() ? { resumeCommand: raw.resumeCommand.trim() } : {}
12932
+ };
12933
+ }
12934
+ function parseCustomAgents(raw) {
12935
+ if (!isRecord3(raw))
12936
+ return {};
12937
+ return Object.entries(raw).reduce((acc, [id, value]) => {
12938
+ if (!id.trim())
12939
+ return acc;
12940
+ const parsed = parseCustomAgent(value);
12941
+ if (parsed) {
12942
+ acc[id.trim()] = parsed;
12943
+ }
12944
+ return acc;
12945
+ }, {});
12946
+ }
12811
12947
  function parseServices(raw) {
12812
12948
  if (!Array.isArray(raw))
12813
12949
  return [];
@@ -12900,6 +13036,7 @@ function parseProjectConfig(parsed) {
12900
13036
  autoPull: isRecord3(parsed.workspace) ? parseAutoPull(parsed.workspace.autoPull) : DEFAULT_CONFIG.workspace.autoPull
12901
13037
  },
12902
13038
  profiles: parseProfiles(parsed.profiles, true),
13039
+ agents: {},
12903
13040
  services: parseServices(parsed.services),
12904
13041
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
12905
13042
  integrations: {
@@ -12967,20 +13104,21 @@ function loadLocalProjectConfigOverlay(root) {
12967
13104
  try {
12968
13105
  const text = readLocalConfigFile(root).trim();
12969
13106
  if (!text) {
12970
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
13107
+ return { worktreeRoot: null, profiles: {}, agents: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
12971
13108
  }
12972
13109
  const parsed = parseConfigDocument(text);
12973
13110
  const ws = isRecord3(parsed.workspace) ? parsed.workspace : null;
12974
13111
  return {
12975
13112
  worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
12976
13113
  profiles: parseProfiles(parsed.profiles, false),
13114
+ agents: parseCustomAgents(parsed.agents),
12977
13115
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
12978
13116
  linear: parseLocalLinearOverlay(parsed),
12979
13117
  github: parseLocalGitHubOverlay(parsed),
12980
13118
  autoPull: parseLocalAutoPullOverlay(parsed)
12981
13119
  };
12982
13120
  } catch {
12983
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
13121
+ return { worktreeRoot: null, profiles: {}, agents: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
12984
13122
  }
12985
13123
  }
12986
13124
  function mergeHookCommand(projectCommand, localCommand) {
@@ -13040,12 +13178,15 @@ function loadConfig(dir, options = {}) {
13040
13178
  ...cloneProfiles(projectConfig.profiles),
13041
13179
  ...cloneProfiles(localOverlay.profiles)
13042
13180
  },
13181
+ agents: {
13182
+ ...cloneAgents(projectConfig.agents),
13183
+ ...cloneAgents(localOverlay.agents)
13184
+ },
13043
13185
  lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks),
13044
13186
  integrations
13045
13187
  };
13046
13188
  }
13047
- async function persistLocalLinearConfig(dir, changes) {
13048
- const root = projectRoot(dir);
13189
+ function readLocalConfigDocument(root) {
13049
13190
  const localPath = join2(root, ".webmux.local.yaml");
13050
13191
  let existing = {};
13051
13192
  try {
@@ -13053,6 +13194,11 @@ async function persistLocalLinearConfig(dir, changes) {
13053
13194
  if (text)
13054
13195
  existing = parseConfigDocument(text);
13055
13196
  } catch {}
13197
+ return { localPath, existing };
13198
+ }
13199
+ async function persistLocalLinearConfig(dir, changes) {
13200
+ const root = projectRoot(dir);
13201
+ const { localPath, existing } = readLocalConfigDocument(root);
13056
13202
  const integrations = isRecord3(existing.integrations) ? { ...existing.integrations } : {};
13057
13203
  const linear = isRecord3(integrations.linear) ? { ...integrations.linear } : {};
13058
13204
  Object.assign(linear, changes);
@@ -13062,13 +13208,7 @@ async function persistLocalLinearConfig(dir, changes) {
13062
13208
  }
13063
13209
  async function persistLocalGitHubConfig(dir, changes) {
13064
13210
  const root = projectRoot(dir);
13065
- const localPath = join2(root, ".webmux.local.yaml");
13066
- let existing = {};
13067
- try {
13068
- const text = readFileSync(localPath, "utf8").trim();
13069
- if (text)
13070
- existing = parseConfigDocument(text);
13071
- } catch {}
13211
+ const { localPath, existing } = readLocalConfigDocument(root);
13072
13212
  const integrations = isRecord3(existing.integrations) ? { ...existing.integrations } : {};
13073
13213
  const github = isRecord3(integrations.github) ? { ...integrations.github } : {};
13074
13214
  Object.assign(github, changes);
@@ -13076,6 +13216,33 @@ async function persistLocalGitHubConfig(dir, changes) {
13076
13216
  existing.integrations = integrations;
13077
13217
  await Bun.write(localPath, $stringify(existing));
13078
13218
  }
13219
+ async function persistLocalCustomAgent(dir, agentId, agent) {
13220
+ const root = projectRoot(dir);
13221
+ const { localPath, existing } = readLocalConfigDocument(root);
13222
+ const agents = isRecord3(existing.agents) ? { ...existing.agents } : {};
13223
+ agents[agentId] = {
13224
+ label: agent.label,
13225
+ startCommand: agent.startCommand,
13226
+ ...agent.resumeCommand ? { resumeCommand: agent.resumeCommand } : {}
13227
+ };
13228
+ existing.agents = agents;
13229
+ await Bun.write(localPath, $stringify(existing));
13230
+ }
13231
+ async function removeLocalCustomAgent(dir, agentId) {
13232
+ const root = projectRoot(dir);
13233
+ const { localPath, existing } = readLocalConfigDocument(root);
13234
+ if (!isRecord3(existing.agents) || !(agentId in existing.agents)) {
13235
+ return;
13236
+ }
13237
+ const agents = { ...existing.agents };
13238
+ delete agents[agentId];
13239
+ if (Object.keys(agents).length === 0) {
13240
+ delete existing.agents;
13241
+ } else {
13242
+ existing.agents = agents;
13243
+ }
13244
+ await Bun.write(localPath, $stringify(existing));
13245
+ }
13079
13246
  function expandTemplate(template, env) {
13080
13247
  return template.replace(/\$\{(\w+)\}/g, (_, key) => env[key] ?? "");
13081
13248
  }
@@ -13208,6 +13375,188 @@ function pruneArchivedWorktreeState(input) {
13208
13375
  return createArchiveState(input.state.entries.filter((entry) => validPaths.has(normalizeArchivePath(entry.path))));
13209
13376
  }
13210
13377
 
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
+ }
13385
+ function resolveAgentChatSupport(input) {
13386
+ if (!input.agentId) {
13387
+ return {
13388
+ ok: false,
13389
+ error: "This worktree has no agent configured",
13390
+ status: 409
13391
+ };
13392
+ }
13393
+ if (!input.agent) {
13394
+ return {
13395
+ ok: false,
13396
+ error: `Unknown agent: ${input.agentId}`,
13397
+ status: 404
13398
+ };
13399
+ }
13400
+ const agentLabel = input.agentLabel ?? input.agent.label;
13401
+ if (!input.agent.capabilities.inAppChat || !input.agent.capabilities.conversationHistory) {
13402
+ return {
13403
+ ok: false,
13404
+ error: `${agentLabel} does not support in-app chat`,
13405
+ status: 409
13406
+ };
13407
+ }
13408
+ if (input.action === "interrupt" && !input.agent.capabilities.interrupt) {
13409
+ return {
13410
+ ok: false,
13411
+ error: `${agentLabel} cannot be interrupted from the dashboard`,
13412
+ status: 409
13413
+ };
13414
+ }
13415
+ if (input.agent.kind === "builtin") {
13416
+ return {
13417
+ ok: true,
13418
+ data: {
13419
+ provider: input.agent.implementation.agent,
13420
+ submitDelayMs: resolveAgentTerminalSubmitDelayMs(input)
13421
+ }
13422
+ };
13423
+ }
13424
+ return {
13425
+ ok: false,
13426
+ error: `Dashboard chat is not available for ${agentLabel}`,
13427
+ status: 409
13428
+ };
13429
+ }
13430
+
13431
+ // backend/src/services/agent-registry.ts
13432
+ var BUILTIN_AGENT_DEFINITIONS = [
13433
+ {
13434
+ id: "claude",
13435
+ label: "Claude",
13436
+ kind: "builtin",
13437
+ capabilities: {
13438
+ terminal: true,
13439
+ inAppChat: true,
13440
+ conversationHistory: true,
13441
+ interrupt: true,
13442
+ resume: true
13443
+ },
13444
+ implementation: {
13445
+ type: "builtin",
13446
+ agent: "claude"
13447
+ }
13448
+ },
13449
+ {
13450
+ id: "codex",
13451
+ label: "Codex",
13452
+ kind: "builtin",
13453
+ capabilities: {
13454
+ terminal: true,
13455
+ inAppChat: true,
13456
+ conversationHistory: true,
13457
+ interrupt: true,
13458
+ resume: true
13459
+ },
13460
+ implementation: {
13461
+ type: "builtin",
13462
+ agent: "codex"
13463
+ }
13464
+ }
13465
+ ];
13466
+ function cloneCapabilities(capabilities) {
13467
+ return { ...capabilities };
13468
+ }
13469
+ function isBuiltInAgentId(agentId) {
13470
+ return BUILTIN_AGENT_DEFINITIONS.some((agent) => agent.id === agentId);
13471
+ }
13472
+ function normalizeCustomAgentId(label) {
13473
+ const normalized = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
13474
+ return normalized || "agent";
13475
+ }
13476
+ function cloneDefinition(definition) {
13477
+ if (definition.kind === "builtin") {
13478
+ return {
13479
+ ...definition,
13480
+ capabilities: cloneCapabilities(definition.capabilities),
13481
+ implementation: { ...definition.implementation }
13482
+ };
13483
+ }
13484
+ return {
13485
+ ...definition,
13486
+ capabilities: cloneCapabilities(definition.capabilities),
13487
+ implementation: {
13488
+ type: "custom",
13489
+ config: { ...definition.implementation.config }
13490
+ }
13491
+ };
13492
+ }
13493
+ function buildCustomAgentDefinition(id, config) {
13494
+ return {
13495
+ id,
13496
+ label: config.label,
13497
+ kind: "custom",
13498
+ capabilities: {
13499
+ terminal: true,
13500
+ inAppChat: false,
13501
+ conversationHistory: false,
13502
+ interrupt: false,
13503
+ resume: config.resumeCommand !== undefined
13504
+ },
13505
+ implementation: {
13506
+ type: "custom",
13507
+ config: { ...config }
13508
+ }
13509
+ };
13510
+ }
13511
+ function listAgentDefinitions(config) {
13512
+ const builtInIds = new Set(BUILTIN_AGENT_DEFINITIONS.map((agent) => agent.id));
13513
+ const customDefinitions = Object.entries(config.agents).filter(([id]) => !builtInIds.has(id)).sort(([leftId, left], [rightId, right]) => {
13514
+ const labelCompare = left.label.localeCompare(right.label);
13515
+ return labelCompare !== 0 ? labelCompare : leftId.localeCompare(rightId);
13516
+ }).map(([id, agent]) => buildCustomAgentDefinition(id, agent));
13517
+ return [
13518
+ ...BUILTIN_AGENT_DEFINITIONS.map((definition) => cloneDefinition(definition)),
13519
+ ...customDefinitions
13520
+ ];
13521
+ }
13522
+ function getAgentDefinition(config, agentId) {
13523
+ const definition = listAgentDefinitions(config).find((agent) => agent.id === agentId);
13524
+ return definition ?? null;
13525
+ }
13526
+ function listAgentSummaries(config) {
13527
+ return listAgentDefinitions(config).map((agent) => ({
13528
+ id: agent.id,
13529
+ label: agent.label,
13530
+ kind: agent.kind,
13531
+ capabilities: cloneCapabilities(agent.capabilities)
13532
+ }));
13533
+ }
13534
+ function listAgentDetails(config) {
13535
+ return listAgentDefinitions(config).map((agent) => ({
13536
+ id: agent.id,
13537
+ label: agent.label,
13538
+ kind: agent.kind,
13539
+ capabilities: cloneCapabilities(agent.capabilities),
13540
+ startCommand: agent.kind === "custom" ? agent.implementation.config.startCommand : null,
13541
+ resumeCommand: agent.kind === "custom" ? agent.implementation.config.resumeCommand ?? null : null
13542
+ }));
13543
+ }
13544
+
13545
+ // backend/src/services/agent-validation-service.ts
13546
+ function validateCustomAgentInput(input) {
13547
+ const warnings = [];
13548
+ if (!input.startCommand.includes("${PROMPT}") && !input.startCommand.includes("${SYSTEM_PROMPT}")) {
13549
+ warnings.push("Start command does not reference ${PROMPT} or ${SYSTEM_PROMPT}; initial prompts will not be passed automatically");
13550
+ }
13551
+ if (!input.resumeCommand?.trim()) {
13552
+ warnings.push("Resume command is not configured; reopening the worktree will restart the agent");
13553
+ }
13554
+ return {
13555
+ normalizedId: normalizeCustomAgentId(input.label),
13556
+ warnings
13557
+ };
13558
+ }
13559
+
13211
13560
  // backend/src/services/linear-service.ts
13212
13561
  var ASSIGNED_ISSUES_QUERY = `
13213
13562
  query AssignedIssues {
@@ -13537,11 +13886,11 @@ async function createLinearIssue(input) {
13537
13886
 
13538
13887
  // backend/src/services/lifecycle-service.ts
13539
13888
  import { mkdir as mkdir4 } from "fs/promises";
13540
- import { dirname as dirname4, resolve as resolve6 } from "path";
13889
+ import { dirname as dirname4, resolve as resolve7 } from "path";
13541
13890
 
13542
13891
  // backend/src/adapters/agent-runtime.ts
13543
13892
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
13544
- import { dirname as dirname3, join as join4 } from "path";
13893
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
13545
13894
 
13546
13895
  // backend/src/adapters/fs.ts
13547
13896
  import { mkdir as mkdir2 } from "fs/promises";
@@ -13754,6 +14103,7 @@ async function writeWorktreePrs(gitDir, prs) {
13754
14103
  }
13755
14104
 
13756
14105
  // backend/src/adapters/agent-runtime.ts
14106
+ var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
13757
14107
  function shellQuote(value) {
13758
14108
  return `'${value.replaceAll("'", "'\\''")}'`;
13759
14109
  }
@@ -13772,6 +14122,7 @@ from pathlib import Path
13772
14122
 
13773
14123
 
13774
14124
  CONTROL_ENV_PATH = Path(__file__).resolve().with_name("control.env")
14125
+ CONTROL_REQUEST_TIMEOUT_SECONDS = 2
13775
14126
 
13776
14127
 
13777
14128
  def read_control_env():
@@ -13801,6 +14152,7 @@ def build_parser():
13801
14152
 
13802
14153
  status_changed = subparsers.add_parser("status-changed")
13803
14154
  status_changed.add_argument("--lifecycle", choices=["starting", "running", "idle", "stopped"], required=True)
14155
+ status_changed.add_argument("--best-effort", action="store_true")
13804
14156
 
13805
14157
  pr_opened = subparsers.add_parser("pr-opened")
13806
14158
  pr_opened.add_argument("--url")
@@ -13810,6 +14162,11 @@ def build_parser():
13810
14162
 
13811
14163
  subparsers.add_parser("claude-user-prompt-submit")
13812
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")
13813
14170
 
13814
14171
  return parser
13815
14172
 
@@ -13852,6 +14209,41 @@ def read_hook_payload():
13852
14209
  return parsed if isinstance(parsed, dict) else {}
13853
14210
 
13854
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
+
13855
14247
  def send_payload(payload, control_env):
13856
14248
  request = urllib.request.Request(
13857
14249
  control_env["WEBMUX_CONTROL_URL"],
@@ -13864,7 +14256,7 @@ def send_payload(payload, control_env):
13864
14256
  )
13865
14257
 
13866
14258
  try:
13867
- with urllib.request.urlopen(request, timeout=10) as response:
14259
+ with urllib.request.urlopen(request, timeout=CONTROL_REQUEST_TIMEOUT_SECONDS) as response:
13868
14260
  if response.status < 200 or response.status >= 300:
13869
14261
  print(f"control endpoint returned HTTP {response.status}", file=sys.stderr)
13870
14262
  return False
@@ -13898,34 +14290,40 @@ def main():
13898
14290
  print(f"missing control env keys: {', '.join(missing)}", file=sys.stderr)
13899
14291
  return 1
13900
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
+
13901
14301
  if parsed.command == "claude-user-prompt-submit":
13902
14302
  if not send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="running"), control_env), control_env):
13903
14303
  return 1
13904
14304
  return 0
13905
14305
 
13906
- if parsed.command == "claude-post-tool-use":
13907
- hook_payload = read_hook_payload()
13908
- tool_name = hook_payload.get("tool_name")
13909
- tool_input = hook_payload.get("tool_input")
13910
- if not isinstance(tool_input, dict) or tool_name != "Bash":
13911
- 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
13912
14309
 
13913
- command = tool_input.get("command")
13914
- if not isinstance(command, str) or "gh pr create" not in command:
13915
- 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
13916
14314
 
13917
- pr_args = argparse.Namespace(url=None)
13918
- tool_response = hook_payload.get("tool_response")
13919
- if isinstance(tool_response, str):
13920
- match = re.search(r"https://github\\.com/[^\\s\\"]+/pull/\\d+", tool_response)
13921
- if match:
13922
- 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
13923
14318
 
13924
- 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
13925
14323
 
13926
14324
  payload = build_payload(parsed.command, parsed, control_env)
13927
14325
  if not send_payload(payload, control_env):
13928
- return 1
14326
+ return 0 if getattr(parsed, "best_effort", False) else 1
13929
14327
 
13930
14328
  return 0
13931
14329
 
@@ -13995,6 +14393,81 @@ function buildClaudeHookSettings(input) {
13995
14393
  }
13996
14394
  };
13997
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
+ }
13998
14471
  async function mergeClaudeSettings(settingsPath, hookSettings) {
13999
14472
  let existing = {};
14000
14473
  try {
@@ -14014,13 +14487,77 @@ async function mergeClaudeSettings(settingsPath, hookSettings) {
14014
14487
  await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + `
14015
14488
  `);
14016
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
+ }
14017
14552
  async function ensureAgentRuntimeArtifacts(input) {
14018
14553
  const storagePaths = getWorktreeStoragePaths(input.gitDir);
14019
14554
  const artifacts = {
14020
14555
  agentCtlPath: join4(storagePaths.webmuxDir, "webmux-agentctl"),
14021
- 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")
14022
14558
  };
14023
14559
  await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
14560
+ await mkdir3(dirname3(artifacts.codexHooksPath), { recursive: true });
14024
14561
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
14025
14562
  await chmod2(artifacts.agentCtlPath, 493);
14026
14563
  const hookSettings = buildClaudeHookSettings(artifacts);
@@ -14029,12 +14566,14 @@ async function ensureAgentRuntimeArtifacts(input) {
14029
14566
  throw new Error("Invalid Claude hook settings");
14030
14567
  }
14031
14568
  await mergeClaudeSettings(artifacts.claudeSettingsPath, hooks);
14569
+ await ensureGeneratedCodexHooksIgnored(input.gitDir);
14570
+ await mergeCodexHooksFile(artifacts.codexHooksPath, buildCodexHookSettings(artifacts).hooks, artifacts.agentCtlPath);
14032
14571
  return artifacts;
14033
14572
  }
14034
14573
 
14035
14574
  // backend/src/adapters/tmux.ts
14036
14575
  import { createHash } from "crypto";
14037
- import { basename as basename2, resolve as resolve3 } from "path";
14576
+ import { basename as basename2, resolve as resolve4 } from "path";
14038
14577
  function runTmux(args) {
14039
14578
  const result = Bun.spawnSync(["tmux", ...args], {
14040
14579
  stdout: "pipe",
@@ -14062,7 +14601,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
14062
14601
  return trimmed || "x";
14063
14602
  }
14064
14603
  function buildProjectSessionName(projectRoot2) {
14065
- const resolved = resolve3(projectRoot2);
14604
+ const resolved = resolve4(projectRoot2);
14066
14605
  const base = sanitizeTmuxNameSegment(basename2(resolved), 18);
14067
14606
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
14068
14607
  return `wm-${base}-${hash}`;
@@ -14184,6 +14723,14 @@ function allocateServicePorts(existingMetas, services) {
14184
14723
 
14185
14724
  // backend/src/services/agent-service.ts
14186
14725
  var DOCKER_PATH_FALLBACK = "/root/.local/bin:/usr/local/bin:/root/.bun/bin:/root/.cargo/bin";
14726
+ var CUSTOM_AGENT_TEMPLATE_VARS = {
14727
+ PROMPT: "WEBMUX_AGENT_PROMPT",
14728
+ SYSTEM_PROMPT: "WEBMUX_AGENT_SYSTEM_PROMPT",
14729
+ WORKTREE_PATH: "WEBMUX_AGENT_WORKTREE_PATH",
14730
+ REPO_PATH: "WEBMUX_AGENT_REPO_PATH",
14731
+ BRANCH: "WEBMUX_AGENT_BRANCH",
14732
+ PROFILE: "WEBMUX_AGENT_PROFILE"
14733
+ };
14187
14734
  function quoteShell(value) {
14188
14735
  return `'${value.replaceAll("'", "'\\''")}'`;
14189
14736
  }
@@ -14193,17 +14740,18 @@ function buildRuntimeBootstrap(runtimeEnvPath) {
14193
14740
  function buildDockerRuntimeBootstrap(runtimeEnvPath) {
14194
14741
  return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
14195
14742
  }
14196
- function buildAgentInvocation(input) {
14743
+ function buildBuiltInAgentInvocation(input) {
14197
14744
  if (input.agent === "codex") {
14745
+ const hooksFlag = " --enable codex_hooks";
14198
14746
  const yoloFlag2 = input.yolo ? " --yolo" : "";
14199
14747
  if (input.launchMode === "resume") {
14200
- return `codex${yoloFlag2} resume --last`;
14748
+ return `codex${hooksFlag}${yoloFlag2} resume --last`;
14201
14749
  }
14202
14750
  const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
14203
14751
  if (input.systemPrompt) {
14204
- return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
14752
+ return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
14205
14753
  }
14206
- return `codex${yoloFlag2}${promptSuffix2}`;
14754
+ return `codex${hooksFlag}${yoloFlag2}${promptSuffix2}`;
14207
14755
  }
14208
14756
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
14209
14757
  if (input.launchMode === "resume") {
@@ -14215,6 +14763,47 @@ function buildAgentInvocation(input) {
14215
14763
  }
14216
14764
  return `claude${yoloFlag}${promptSuffix}`;
14217
14765
  }
14766
+ function renderCustomCommandTemplate(template) {
14767
+ return template.replaceAll("${PROMPT}", `$${CUSTOM_AGENT_TEMPLATE_VARS.PROMPT}`).replaceAll("${SYSTEM_PROMPT}", `$${CUSTOM_AGENT_TEMPLATE_VARS.SYSTEM_PROMPT}`).replaceAll("${WORKTREE_PATH}", `$${CUSTOM_AGENT_TEMPLATE_VARS.WORKTREE_PATH}`).replaceAll("${REPO_PATH}", `$${CUSTOM_AGENT_TEMPLATE_VARS.REPO_PATH}`).replaceAll("${BRANCH}", `$${CUSTOM_AGENT_TEMPLATE_VARS.BRANCH}`).replaceAll("${PROFILE}", `$${CUSTOM_AGENT_TEMPLATE_VARS.PROFILE}`);
14768
+ }
14769
+ function buildCustomAgentExports(input) {
14770
+ const envEntries = [
14771
+ [CUSTOM_AGENT_TEMPLATE_VARS.PROMPT, input.prompt ?? ""],
14772
+ [CUSTOM_AGENT_TEMPLATE_VARS.SYSTEM_PROMPT, input.systemPrompt ?? ""],
14773
+ [CUSTOM_AGENT_TEMPLATE_VARS.WORKTREE_PATH, input.worktreePath],
14774
+ [CUSTOM_AGENT_TEMPLATE_VARS.REPO_PATH, input.repoRoot],
14775
+ [CUSTOM_AGENT_TEMPLATE_VARS.BRANCH, input.branch],
14776
+ [CUSTOM_AGENT_TEMPLATE_VARS.PROFILE, input.profileName]
14777
+ ];
14778
+ return envEntries.map(([key, value]) => `export ${key}=${quoteShell(value)}`).join("; ");
14779
+ }
14780
+ function buildCustomAgentInvocation(input) {
14781
+ const template = input.launchMode === "resume" && input.agent.implementation.config.resumeCommand ? input.agent.implementation.config.resumeCommand : input.agent.implementation.config.startCommand;
14782
+ const exports = buildCustomAgentExports(input);
14783
+ const renderedCommand = renderCustomCommandTemplate(template);
14784
+ return `${exports}; ${renderedCommand}`;
14785
+ }
14786
+ function buildAgentInvocation(input) {
14787
+ if (input.agent.kind === "builtin") {
14788
+ return buildBuiltInAgentInvocation({
14789
+ agent: input.agent.implementation.agent,
14790
+ yolo: input.yolo,
14791
+ systemPrompt: input.systemPrompt,
14792
+ prompt: input.prompt,
14793
+ launchMode: input.launchMode
14794
+ });
14795
+ }
14796
+ return buildCustomAgentInvocation({
14797
+ agent: input.agent,
14798
+ systemPrompt: input.systemPrompt,
14799
+ prompt: input.prompt,
14800
+ worktreePath: input.worktreePath,
14801
+ repoRoot: input.repoRoot,
14802
+ branch: input.branch,
14803
+ profileName: input.profileName,
14804
+ launchMode: input.launchMode
14805
+ });
14806
+ }
14218
14807
  function buildAgentCommand(input, bootstrap = buildRuntimeBootstrap) {
14219
14808
  return `${bootstrap(input.runtimeEnvPath)}; ${buildAgentInvocation(input)}`;
14220
14809
  }
@@ -14235,7 +14824,7 @@ function buildDockerAgentPaneCommand(input) {
14235
14824
  }
14236
14825
 
14237
14826
  // backend/src/services/session-service.ts
14238
- import { resolve as resolve4 } from "path";
14827
+ import { resolve as resolve5 } from "path";
14239
14828
  function quoteShell2(value) {
14240
14829
  return `'${value.replaceAll("'", "'\\''")}'`;
14241
14830
  }
@@ -14249,7 +14838,7 @@ function buildCommandPaneStartupCommand(template, ctx) {
14249
14838
  if (!template.workingDir) {
14250
14839
  return template.command;
14251
14840
  }
14252
- const workingDir = resolve4(resolvePaneCwd(template, ctx), template.workingDir);
14841
+ const workingDir = resolve5(resolvePaneCwd(template, ctx), template.workingDir);
14253
14842
  return `cd -- ${quoteShell2(workingDir)} && ${template.command}`;
14254
14843
  }
14255
14844
  function resolvePaneStartupCommand(template, ctx) {
@@ -14329,7 +14918,7 @@ import { randomUUID } from "crypto";
14329
14918
 
14330
14919
  // backend/src/adapters/git.ts
14331
14920
  import { readdirSync, rmSync, statSync } from "fs";
14332
- import { resolve as resolve5, join as join5 } from "path";
14921
+ import { resolve as resolve6, join as join5 } from "path";
14333
14922
  function runGit(args, cwd) {
14334
14923
  const result = Bun.spawnSync(["git", ...args], {
14335
14924
  cwd,
@@ -14363,8 +14952,8 @@ function errorMessage(error) {
14363
14952
  return error instanceof Error ? error.message : String(error);
14364
14953
  }
14365
14954
  function isRegisteredWorktree(entries, worktreePath) {
14366
- const resolvedPath = resolve5(worktreePath);
14367
- return entries.some((entry) => resolve5(entry.path) === resolvedPath);
14955
+ const resolvedPath = resolve6(worktreePath);
14956
+ return entries.some((entry) => resolve6(entry.path) === resolvedPath);
14368
14957
  }
14369
14958
  function removeDirectory(path) {
14370
14959
  rmSync(path, {
@@ -14388,7 +14977,7 @@ function currentCheckoutRef(cwd) {
14388
14977
  function resolveRepoRoot(dir) {
14389
14978
  const direct = tryRunGit(["rev-parse", "--show-toplevel"], dir);
14390
14979
  if (direct.ok)
14391
- return resolve5(dir, direct.stdout);
14980
+ return resolve6(dir, direct.stdout);
14392
14981
  let entries;
14393
14982
  try {
14394
14983
  entries = readdirSync(dir);
@@ -14405,17 +14994,17 @@ function resolveRepoRoot(dir) {
14405
14994
  }
14406
14995
  const childResult = tryRunGit(["rev-parse", "--show-toplevel"], child);
14407
14996
  if (childResult.ok)
14408
- return resolve5(child, childResult.stdout);
14997
+ return resolve6(child, childResult.stdout);
14409
14998
  }
14410
14999
  return null;
14411
15000
  }
14412
15001
  function resolveWorktreeRoot(cwd) {
14413
15002
  const output = runGit(["rev-parse", "--show-toplevel"], cwd);
14414
- return resolve5(cwd, output);
15003
+ return resolve6(cwd, output);
14415
15004
  }
14416
15005
  function resolveWorktreeGitDir(cwd) {
14417
15006
  const output = runGit(["rev-parse", "--git-dir"], cwd);
14418
- return resolve5(cwd, output);
15007
+ return resolve6(cwd, output);
14419
15008
  }
14420
15009
  function parseGitWorktreePorcelain(output) {
14421
15010
  const entries = [];
@@ -14807,14 +15396,15 @@ function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
14807
15396
  function prefixAgentBranch(agent, branch) {
14808
15397
  return `${agent}-${branch}`;
14809
15398
  }
14810
- function buildCreateWorktreeTargets(branch, agentSelection) {
14811
- if (agentSelection === "both") {
14812
- return [
14813
- { branch: prefixAgentBranch("claude", branch), agent: "claude" },
14814
- { branch: prefixAgentBranch("codex", branch), agent: "codex" }
14815
- ];
15399
+ function buildCreateWorktreeTargets(branch, agentIds) {
15400
+ if (agentIds.length <= 1) {
15401
+ const agent = agentIds[0];
15402
+ return agent ? [{ branch, agent }] : [];
14816
15403
  }
14817
- return [{ branch, agent: agentSelection }];
15404
+ return agentIds.map((agent) => ({
15405
+ branch: prefixAgentBranch(agent, branch),
15406
+ agent
15407
+ }));
14818
15408
  }
14819
15409
 
14820
15410
  class LifecycleError extends Error {
@@ -14832,12 +15422,12 @@ class LifecycleService {
14832
15422
  }
14833
15423
  async createWorktrees(input) {
14834
15424
  const mode = input.mode ?? "new";
14835
- const agentSelection = input.agent ?? this.deps.config.workspace.defaultAgent;
14836
- if (agentSelection === "both" && mode === "existing") {
14837
- throw new LifecycleError("Creating both agents is only supported for new worktrees", 400);
15425
+ const agentIds = this.resolveSelectedAgents(input);
15426
+ if (agentIds.length > 1 && mode === "existing") {
15427
+ throw new LifecycleError("Creating multiple agents is only supported for new worktrees", 400);
14838
15428
  }
14839
15429
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
14840
- const targets = buildCreateWorktreeTargets(branch, agentSelection);
15430
+ const targets = buildCreateWorktreeTargets(branch, agentIds);
14841
15431
  const createdBranches = [];
14842
15432
  try {
14843
15433
  for (const target of targets) {
@@ -14864,28 +15454,30 @@ class LifecycleService {
14864
15454
  async createWorktree(input) {
14865
15455
  const mode = input.mode ?? "new";
14866
15456
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
14867
- const agent = this.resolveAgent(input.agent);
15457
+ const agent = this.resolveAgentDefinition(input.agent);
14868
15458
  return await this.createResolvedWorktree({
14869
15459
  ...input,
14870
15460
  mode,
14871
15461
  branch,
14872
- agent
15462
+ agent: agent.id
14873
15463
  });
14874
15464
  }
14875
15465
  async openWorktree(branch) {
14876
15466
  try {
14877
15467
  const resolved = await this.resolveExistingWorktree(branch);
14878
- const launchMode = resolved.meta ? "resume" : "fresh";
14879
15468
  const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
14880
- const { profile } = this.resolveProfile(initialized.meta.profile);
15469
+ const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
15470
+ const agent = this.resolveAgentDefinition(initialized.meta.agent);
15471
+ const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
14881
15472
  await ensureAgentRuntimeArtifacts({
14882
15473
  gitDir: initialized.paths.gitDir,
14883
15474
  worktreePath: resolved.entry.path
14884
15475
  });
14885
15476
  await this.materializeRuntimeSession({
14886
15477
  branch,
15478
+ profileName,
14887
15479
  profile,
14888
- agent: initialized.meta.agent,
15480
+ agent,
14889
15481
  initialized,
14890
15482
  worktreePath: resolved.entry.path,
14891
15483
  launchMode
@@ -15019,14 +15611,22 @@ class LifecycleService {
15019
15611
  profile
15020
15612
  };
15021
15613
  }
15022
- resolveAgent(agent) {
15023
- if (!agent)
15024
- return this.deps.config.workspace.defaultAgent;
15025
- if (agent !== "claude" && agent !== "codex") {
15026
- throw new LifecycleError(`Unknown agent: ${agent}`, 400);
15614
+ resolveAgentDefinition(agentId) {
15615
+ const resolvedAgentId = agentId ?? this.deps.config.workspace.defaultAgent;
15616
+ const agent = getAgentDefinition(this.deps.config, resolvedAgentId);
15617
+ if (!agent) {
15618
+ throw new LifecycleError(`Unknown agent: ${resolvedAgentId}`, 400);
15027
15619
  }
15028
15620
  return agent;
15029
15621
  }
15622
+ resolveSelectedAgents(input) {
15623
+ const selectedAgents = input.agents && input.agents.length > 0 ? input.agents : [input.agent ?? this.deps.config.workspace.defaultAgent];
15624
+ const dedupedAgentIds = [...new Set(selectedAgents.map((agent) => agent.trim()).filter((agent) => agent.length > 0))];
15625
+ if (dedupedAgentIds.length === 0) {
15626
+ throw new LifecycleError("At least one agent must be selected", 400);
15627
+ }
15628
+ return dedupedAgentIds.map((agentId) => this.resolveAgentDefinition(agentId).id);
15629
+ }
15030
15630
  async buildStartupEnvValues(envOverrides) {
15031
15631
  const startupEnvValues = Object.fromEntries(Object.entries(this.deps.config.startupEnvs).map(([key, value]) => [key, stringifyStartupEnvValue(value)]));
15032
15632
  for (const [key, value] of Object.entries(envOverrides ?? {})) {
@@ -15042,20 +15642,20 @@ class LifecycleService {
15042
15642
  return allocateServicePorts(metas, this.deps.config.services);
15043
15643
  }
15044
15644
  resolveWorktreePath(branch) {
15045
- return resolve6(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
15645
+ return resolve7(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
15046
15646
  }
15047
15647
  listLocalBranches() {
15048
- return this.deps.git.listLocalBranches(resolve6(this.deps.projectRoot));
15648
+ return this.deps.git.listLocalBranches(resolve7(this.deps.projectRoot));
15049
15649
  }
15050
15650
  listRemoteBranches() {
15051
- return this.deps.git.listRemoteBranches(resolve6(this.deps.projectRoot));
15651
+ return this.deps.git.listRemoteBranches(resolve7(this.deps.projectRoot));
15052
15652
  }
15053
15653
  listCheckedOutBranches() {
15054
- 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));
15055
15655
  }
15056
15656
  listProjectWorktrees() {
15057
- const projectRoot2 = resolve6(this.deps.projectRoot);
15058
- 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);
15059
15659
  }
15060
15660
  async readManagedMetas() {
15061
15661
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -15148,6 +15748,7 @@ class LifecycleService {
15148
15748
  });
15149
15749
  ensureSessionLayout(this.deps.tmux, this.buildSessionLayout({
15150
15750
  branch: input.branch,
15751
+ profileName: input.profileName,
15151
15752
  profile: input.profile,
15152
15753
  agent: input.agent,
15153
15754
  initialized: input.initialized,
@@ -15160,6 +15761,7 @@ class LifecycleService {
15160
15761
  }
15161
15762
  ensureSessionLayout(this.deps.tmux, this.buildSessionLayout({
15162
15763
  branch: input.branch,
15764
+ profileName: input.profileName,
15163
15765
  profile: input.profile,
15164
15766
  agent: input.agent,
15165
15767
  initialized: input.initialized,
@@ -15178,6 +15780,10 @@ class LifecycleService {
15178
15780
  agent: buildDockerAgentPaneCommand({
15179
15781
  agent: input.agent,
15180
15782
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
15783
+ repoRoot: this.deps.projectRoot,
15784
+ worktreePath: input.worktreePath,
15785
+ branch: input.branch,
15786
+ profileName: input.profileName,
15181
15787
  yolo: input.profile.yolo === true,
15182
15788
  systemPrompt,
15183
15789
  prompt: input.launchMode === "fresh" ? input.prompt : undefined,
@@ -15188,6 +15794,10 @@ class LifecycleService {
15188
15794
  agent: buildAgentPaneCommand({
15189
15795
  agent: input.agent,
15190
15796
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
15797
+ repoRoot: this.deps.projectRoot,
15798
+ worktreePath: input.worktreePath,
15799
+ branch: input.branch,
15800
+ profileName: input.profileName,
15191
15801
  yolo: input.profile.yolo === true,
15192
15802
  systemPrompt,
15193
15803
  prompt: input.launchMode === "fresh" ? input.prompt : undefined,
@@ -15312,6 +15922,7 @@ class LifecycleService {
15312
15922
  const baseBranch = input.mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
15313
15923
  const branchAvailability = this.resolveBranchAvailability(input.branch, input.mode);
15314
15924
  const { profileName, profile } = this.resolveProfile(input.profile);
15925
+ const agent = this.resolveAgentDefinition(input.agent);
15315
15926
  const worktreePath = this.resolveWorktreePath(input.branch);
15316
15927
  const createProgressBase = {
15317
15928
  branch: input.branch,
@@ -15336,7 +15947,7 @@ class LifecycleService {
15336
15947
  ...baseBranch ? { baseBranch } : {},
15337
15948
  ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
15338
15949
  profile: profileName,
15339
- agent: input.agent,
15950
+ agent: agent.id,
15340
15951
  runtime: profile.runtime,
15341
15952
  startupEnvValues: await this.buildStartupEnvValues(input.envOverrides),
15342
15953
  allocatedPorts: await this.allocatePorts(),
@@ -15376,8 +15987,9 @@ class LifecycleService {
15376
15987
  });
15377
15988
  await this.materializeRuntimeSession({
15378
15989
  branch: input.branch,
15990
+ profileName,
15379
15991
  profile,
15380
- agent: input.agent,
15992
+ agent,
15381
15993
  initialized,
15382
15994
  worktreePath,
15383
15995
  prompt: input.prompt,
@@ -16114,7 +16726,7 @@ function mapCreationSnapshot(creating) {
16114
16726
  phase: creating.phase
16115
16727
  } : null;
16116
16728
  }
16117
- function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue) {
16729
+ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
16118
16730
  return {
16119
16731
  branch: state.branch,
16120
16732
  ...state.baseBranch ? { baseBranch: state.baseBranch } : {},
@@ -16123,6 +16735,7 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue)
16123
16735
  archived: isArchived(state.path),
16124
16736
  profile: state.profile,
16125
16737
  agentName: state.agentName,
16738
+ agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
16126
16739
  mux: state.session.exists,
16127
16740
  dirty: state.git.dirty,
16128
16741
  unpushed: state.git.aheadCount > 0,
@@ -16135,7 +16748,7 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue)
16135
16748
  creation: mapCreationSnapshot(creating)
16136
16749
  };
16137
16750
  }
16138
- function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
16751
+ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
16139
16752
  return {
16140
16753
  branch: creating.branch,
16141
16754
  ...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
@@ -16144,6 +16757,7 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
16144
16757
  archived: isArchived(creating.path),
16145
16758
  profile: creating.profile,
16146
16759
  agentName: creating.agentName,
16760
+ agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
16147
16761
  mux: false,
16148
16762
  dirty: false,
16149
16763
  unpushed: false,
@@ -16163,10 +16777,10 @@ function buildWorktreeSnapshots(input) {
16163
16777
  const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
16164
16778
  const runtimeWorktrees = input.runtime.listWorktrees();
16165
16779
  const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
16166
- const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue));
16780
+ const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
16167
16781
  for (const creating of creatingWorktrees) {
16168
16782
  if (!runtimeBranches.has(creating.branch)) {
16169
- worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue));
16783
+ worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
16170
16784
  }
16171
16785
  }
16172
16786
  worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
@@ -16195,6 +16809,7 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
16195
16809
  archived: worktree.archived,
16196
16810
  profile: worktree.profile,
16197
16811
  agentName: worktree.agentName,
16812
+ agentLabel: worktree.agentLabel,
16198
16813
  mux: worktree.mux,
16199
16814
  status: worktree.status,
16200
16815
  dirty: worktree.dirty,
@@ -16899,7 +17514,7 @@ class BunPortProbe {
16899
17514
  this.hostnames = hostnames;
16900
17515
  }
16901
17516
  isListening(port) {
16902
- return new Promise((resolve7) => {
17517
+ return new Promise((resolve8) => {
16903
17518
  let settled = false;
16904
17519
  let pending = this.hostnames.length;
16905
17520
  const settle = (result) => {
@@ -16908,20 +17523,20 @@ class BunPortProbe {
16908
17523
  if (result) {
16909
17524
  settled = true;
16910
17525
  clearTimeout(timer);
16911
- resolve7(true);
17526
+ resolve8(true);
16912
17527
  return;
16913
17528
  }
16914
17529
  pending--;
16915
17530
  if (pending === 0) {
16916
17531
  settled = true;
16917
17532
  clearTimeout(timer);
16918
- resolve7(false);
17533
+ resolve8(false);
16919
17534
  }
16920
17535
  };
16921
17536
  const timer = setTimeout(() => {
16922
17537
  if (!settled) {
16923
17538
  settled = true;
16924
- resolve7(false);
17539
+ resolve8(false);
16925
17540
  }
16926
17541
  }, this.timeoutMs);
16927
17542
  for (const hostname of this.hostnames) {
@@ -16947,7 +17562,7 @@ class BunPortProbe {
16947
17562
  // backend/src/services/auto-name-service.ts
16948
17563
  var MAX_BRANCH_LENGTH = 40;
16949
17564
  var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
16950
- var AUTO_NAME_TIMEOUT_MS = 1e4;
17565
+ var AUTO_NAME_TIMEOUT_MS = 15000;
16951
17566
  var DEFAULT_SYSTEM_PROMPT = [
16952
17567
  "Generate a concise git branch name from the task description.",
16953
17568
  "Return only the branch name.",
@@ -16999,7 +17614,7 @@ async function defaultSpawn(args, options = {}) {
16999
17614
  if (options.timeoutMs === undefined) {
17000
17615
  return await resultPromise;
17001
17616
  }
17002
- return await new Promise((resolve7, reject) => {
17617
+ return await new Promise((resolve8, reject) => {
17003
17618
  let settled = false;
17004
17619
  const timeoutId = setTimeout(() => {
17005
17620
  if (settled)
@@ -17015,7 +17630,7 @@ async function defaultSpawn(args, options = {}) {
17015
17630
  return;
17016
17631
  settled = true;
17017
17632
  clearTimeout(timeoutId);
17018
- resolve7(result);
17633
+ resolve8(result);
17019
17634
  }, (error) => {
17020
17635
  if (settled)
17021
17636
  return;
@@ -17153,8 +17768,8 @@ class ArchiveStateService {
17153
17768
  async withMutationLock(operation) {
17154
17769
  const previous = this.mutationQueue;
17155
17770
  let release = () => {};
17156
- this.mutationQueue = new Promise((resolve7) => {
17157
- release = resolve7;
17771
+ this.mutationQueue = new Promise((resolve8) => {
17772
+ release = resolve8;
17158
17773
  });
17159
17774
  await previous.catch(() => {});
17160
17775
  try {
@@ -17429,9 +18044,9 @@ class ProjectRuntime {
17429
18044
  }
17430
18045
 
17431
18046
  // backend/src/services/reconciliation-service.ts
17432
- import { basename as basename3, resolve as resolve7 } from "path";
18047
+ import { basename as basename3, resolve as resolve8 } from "path";
17433
18048
  function makeUnmanagedWorktreeId(path) {
17434
- return `unmanaged:${resolve7(path)}`;
18049
+ return `unmanaged:${resolve8(path)}`;
17435
18050
  }
17436
18051
  function isValidPort2(port) {
17437
18052
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -17488,7 +18103,7 @@ class ReconciliationService {
17488
18103
  if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
17489
18104
  return;
17490
18105
  }
17491
- const normalizedRepoRoot = resolve7(repoRoot);
18106
+ const normalizedRepoRoot = resolve8(repoRoot);
17492
18107
  const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
17493
18108
  this.lastReconciledAt = this.now();
17494
18109
  });
@@ -17507,7 +18122,7 @@ class ReconciliationService {
17507
18122
  windows = [];
17508
18123
  }
17509
18124
  const seenWorktreeIds = new Set;
17510
- const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve7(entry.path) !== normalizedRepoRoot);
18125
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve8(entry.path) !== normalizedRepoRoot);
17511
18126
  const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
17512
18127
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
17513
18128
  const meta = await readWorktreeMeta(gitDir);
@@ -17668,7 +18283,6 @@ function createWebmuxRuntime(options = {}) {
17668
18283
  // backend/src/server.ts
17669
18284
  var PORT = parseInt(Bun.env.PORT || "5111", 10);
17670
18285
  var STATIC_DIR = Bun.env.WEBMUX_STATIC_DIR || "";
17671
- var CODEX_TMUX_PROMPT_SUBMIT_DELAY_MS = 200;
17672
18286
  var runtime = createWebmuxRuntime({
17673
18287
  port: PORT,
17674
18288
  projectDir: Bun.env.WEBMUX_PROJECT_DIR || process.cwd()
@@ -17741,13 +18355,15 @@ function getFrontendConfig() {
17741
18355
  name,
17742
18356
  ...profile.systemPrompt ? { systemPrompt: profile.systemPrompt } : {}
17743
18357
  })),
18358
+ agents: listAgentSummaries(config),
17744
18359
  defaultProfileName,
18360
+ defaultAgentId: config.workspace.defaultAgent,
17745
18361
  autoName: config.autoName !== null,
17746
18362
  linearCreateTicketOption: config.integrations.linear.enabled && config.integrations.linear.createTicketOption,
17747
18363
  startupEnvs: config.startupEnvs,
17748
18364
  linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
17749
18365
  alias: lr.alias,
17750
- ...lr.dir ? { dir: resolve8(PROJECT_DIR, lr.dir) } : {}
18366
+ ...lr.dir ? { dir: resolve9(PROJECT_DIR, lr.dir) } : {}
17751
18367
  })),
17752
18368
  linearAutoCreateWorktrees: linearAutoCreateEnabled,
17753
18369
  autoRemoveOnMerge: autoRemoveOnMergeEnabled,
@@ -17851,7 +18467,8 @@ async function resolveTerminalWorktree(branch) {
17851
18467
  attachTarget: {
17852
18468
  ownerSessionName: state.session.sessionName,
17853
18469
  windowName: state.session.windowName
17854
- }
18470
+ },
18471
+ agentName: state.agentName
17855
18472
  };
17856
18473
  }
17857
18474
  async function resolveAgentsTerminalWorktree(branch) {
@@ -17899,9 +18516,9 @@ async function hasValidControlToken(req) {
17899
18516
  }
17900
18517
  async function getWorktreeGitDirs() {
17901
18518
  const gitDirs = new Map;
17902
- const projectRoot2 = resolve8(PROJECT_DIR);
18519
+ const projectRoot2 = resolve9(PROJECT_DIR);
17903
18520
  for (const entry of git.listWorktrees(projectRoot2)) {
17904
- if (entry.bare || resolve8(entry.path) === projectRoot2 || !entry.branch)
18521
+ if (entry.bare || resolve9(entry.path) === projectRoot2 || !entry.branch)
17905
18522
  continue;
17906
18523
  gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
17907
18524
  }
@@ -17941,6 +18558,11 @@ async function readProjectSnapshot() {
17941
18558
  url: match.url,
17942
18559
  state: match.state
17943
18560
  } : null;
18561
+ },
18562
+ findAgentLabel: (agentId) => {
18563
+ if (!agentId)
18564
+ return null;
18565
+ return getAgentDefinition(config, agentId)?.label ?? agentId;
17944
18566
  }
17945
18567
  });
17946
18568
  }
@@ -17971,12 +18593,30 @@ async function resolveAgentsWorktree(branch) {
17971
18593
  worktree
17972
18594
  };
17973
18595
  }
18596
+ function resolveWorktreeAgentChatSupport(worktree, action) {
18597
+ return resolveAgentChatSupport({
18598
+ agentId: worktree.agentName,
18599
+ agentLabel: worktree.agentLabel,
18600
+ agent: worktree.agentName ? getAgentDefinition(config, worktree.agentName) : null,
18601
+ action
18602
+ });
18603
+ }
18604
+ function resolveWorktreeTerminalSubmitDelayMs(agentName) {
18605
+ return resolveAgentTerminalSubmitDelayMs({
18606
+ agentId: agentName,
18607
+ agent: agentName ? getAgentDefinition(config, agentName) : null
18608
+ });
18609
+ }
17974
18610
  async function apiAttachAgentsWorktree(branch) {
17975
18611
  touchDashboardActivity();
17976
18612
  const resolved = await resolveAgentsWorktree(branch);
17977
18613
  if (!resolved.ok)
17978
18614
  return resolved.response;
17979
- const result = resolved.worktree.agentName === "claude" ? await claudeConversationService.attachWorktreeConversation(resolved.worktree) : await worktreeConversationService.attachWorktreeConversation(resolved.worktree);
18615
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18616
+ if (!chatSupport.ok) {
18617
+ return errorResponse(chatSupport.error, chatSupport.status);
18618
+ }
18619
+ const result = chatSupport.data.provider === "claude" ? await claudeConversationService.attachWorktreeConversation(resolved.worktree) : await worktreeConversationService.attachWorktreeConversation(resolved.worktree);
17980
18620
  return result.ok ? jsonResponse(result.data) : errorResponse(result.error, result.status);
17981
18621
  }
17982
18622
  async function apiGetAgentsWorktreeHistory(branch) {
@@ -17984,7 +18624,11 @@ async function apiGetAgentsWorktreeHistory(branch) {
17984
18624
  const resolved = await resolveAgentsWorktree(branch);
17985
18625
  if (!resolved.ok)
17986
18626
  return resolved.response;
17987
- const result = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18627
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18628
+ if (!chatSupport.ok) {
18629
+ return errorResponse(chatSupport.error, chatSupport.status);
18630
+ }
18631
+ const result = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
17988
18632
  return result.ok ? jsonResponse(result.data) : errorResponse(result.error, result.status);
17989
18633
  }
17990
18634
  async function apiSendAgentsWorktreeMessage(branch, req) {
@@ -17998,14 +18642,18 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
17998
18642
  if (!resolved.worktree.mux) {
17999
18643
  return errorResponse("Open this worktree in the main dashboard before sending messages here", 409);
18000
18644
  }
18001
- const conversationResult = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18645
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18646
+ if (!chatSupport.ok) {
18647
+ return errorResponse(chatSupport.error, chatSupport.status);
18648
+ }
18649
+ const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18002
18650
  if (!conversationResult.ok) {
18003
18651
  return errorResponse(conversationResult.error, conversationResult.status);
18004
18652
  }
18005
18653
  const terminalWorktree = await resolveAgentsTerminalWorktree(branch);
18006
18654
  if (!terminalWorktree.ok)
18007
18655
  return terminalWorktree.response;
18008
- const sendResult = await sendPrompt(terminalWorktree.data.worktreeId, terminalWorktree.data.attachTarget, parsed.data.text, 0, undefined, resolved.worktree.agentName === "codex" ? CODEX_TMUX_PROMPT_SUBMIT_DELAY_MS : 0);
18656
+ const sendResult = await sendPrompt(terminalWorktree.data.worktreeId, terminalWorktree.data.attachTarget, parsed.data.text, 0, undefined, chatSupport.data.submitDelayMs);
18009
18657
  if (!sendResult.ok) {
18010
18658
  return errorResponse(sendResult.error, 503);
18011
18659
  }
@@ -18023,7 +18671,11 @@ async function apiInterruptAgentsWorktree(branch) {
18023
18671
  if (!resolved.worktree.mux) {
18024
18672
  return errorResponse("Open this worktree in the main dashboard before interrupting it here", 409);
18025
18673
  }
18026
- const conversationResult = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18674
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "interrupt");
18675
+ if (!chatSupport.ok) {
18676
+ return errorResponse(chatSupport.error, chatSupport.status);
18677
+ }
18678
+ const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18027
18679
  if (!conversationResult.ok) {
18028
18680
  return errorResponse(conversationResult.error, conversationResult.status);
18029
18681
  }
@@ -18048,7 +18700,14 @@ async function loadAgentsConversationSnapshot(branch) {
18048
18700
  message: await readErrorMessage(resolved.response)
18049
18701
  };
18050
18702
  }
18051
- const result = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18703
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18704
+ if (!chatSupport.ok) {
18705
+ return {
18706
+ ok: false,
18707
+ message: chatSupport.error
18708
+ };
18709
+ }
18710
+ const result = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18052
18711
  return result.ok ? { ok: true, data: result.data } : { ok: false, message: result.error };
18053
18712
  }
18054
18713
  async function readErrorMessage(response) {
@@ -18167,10 +18826,11 @@ async function apiCreateWorktree(req) {
18167
18826
  const prompt = body.prompt?.trim() ? body.prompt.trim() : undefined;
18168
18827
  const profile = body.profile;
18169
18828
  const agent = body.agent;
18829
+ const agents = body.agents;
18170
18830
  const createLinearTicket = body.createLinearTicket === true;
18171
18831
  const linearTitle = body.linearTitle?.trim() ? body.linearTitle.trim() : undefined;
18172
18832
  const mode = body.mode;
18173
- const agentSelection = agent ?? config.workspace.defaultAgent;
18833
+ const selectedAgents = agents ? agents : agent ? [agent] : [config.workspace.defaultAgent];
18174
18834
  if (baseBranch && !isValidBranchName(baseBranch)) {
18175
18835
  return errorResponse("Invalid base branch name", 400);
18176
18836
  }
@@ -18211,7 +18871,7 @@ async function apiCreateWorktree(req) {
18211
18871
  log.info(`[linear] created ticket ${linearResult.data.identifier} branch=${linearResult.data.branchName} title="${linearResult.data.title.slice(0, 80)}"`);
18212
18872
  }
18213
18873
  if (resolvedBranch) {
18214
- const targetBranches = buildCreateWorktreeTargets(resolvedBranch, agentSelection).map((target) => target.branch);
18874
+ const targetBranches = buildCreateWorktreeTargets(resolvedBranch, selectedAgents).map((target) => target.branch);
18215
18875
  for (const targetBranch of targetBranches) {
18216
18876
  ensureBranchNotBusy(targetBranch);
18217
18877
  }
@@ -18219,14 +18879,14 @@ async function apiCreateWorktree(req) {
18219
18879
  return errorResponse("Base branch must differ from branch name", 400);
18220
18880
  }
18221
18881
  }
18222
- log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""} agent=${agentSelection}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
18882
+ log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""} agents=${selectedAgents.join(",")}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
18223
18883
  const result = await lifecycleService.createWorktrees({
18224
18884
  mode,
18225
18885
  branch: resolvedBranch,
18226
18886
  baseBranch,
18227
18887
  prompt,
18228
18888
  profile,
18229
- agent: agentSelection,
18889
+ ...agents && agents.length > 0 ? { agents } : { agent },
18230
18890
  envOverrides
18231
18891
  });
18232
18892
  log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
@@ -18278,7 +18938,8 @@ async function apiSendPrompt(name, req) {
18278
18938
  const preamble = body.preamble;
18279
18939
  log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
18280
18940
  const terminalWorktree = await resolveTerminalWorktree(name);
18281
- 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);
18282
18943
  if (!result.ok)
18283
18944
  return errorResponse(result.error, 503);
18284
18945
  return jsonResponse({ ok: true });
@@ -18290,6 +18951,72 @@ async function apiMergeWorktree(name) {
18290
18951
  log.debug(`[worktree:merge] done name=${name}`);
18291
18952
  return jsonResponse({ ok: true });
18292
18953
  }
18954
+ async function apiListAgents() {
18955
+ return jsonResponse({ agents: listAgentDetails(config) });
18956
+ }
18957
+ async function apiValidateAgent(req) {
18958
+ const parsed = await parseJsonBody(req, UpsertCustomAgentRequestSchema);
18959
+ if (!parsed.ok)
18960
+ return parsed.response;
18961
+ return jsonResponse(validateCustomAgentInput(parsed.data));
18962
+ }
18963
+ async function apiCreateAgent(req) {
18964
+ const parsed = await parseJsonBody(req, UpsertCustomAgentRequestSchema);
18965
+ if (!parsed.ok)
18966
+ return parsed.response;
18967
+ const body = parsed.data;
18968
+ const agentId = normalizeCustomAgentId(body.label);
18969
+ if (isBuiltInAgentId(agentId) || config.agents[agentId]) {
18970
+ return errorResponse(`Agent already exists: ${agentId}`, 409);
18971
+ }
18972
+ const agentConfig = {
18973
+ label: body.label,
18974
+ startCommand: body.startCommand,
18975
+ ...body.resumeCommand?.trim() ? { resumeCommand: body.resumeCommand.trim() } : {}
18976
+ };
18977
+ await persistLocalCustomAgent(PROJECT_DIR, agentId, agentConfig);
18978
+ config.agents[agentId] = agentConfig;
18979
+ const agent = listAgentDetails(config).find((entry) => entry.id === agentId);
18980
+ if (!agent) {
18981
+ return errorResponse(`Created agent could not be loaded: ${agentId}`, 500);
18982
+ }
18983
+ return jsonResponse({ agent });
18984
+ }
18985
+ async function apiUpdateAgent(agentId, req) {
18986
+ if (isBuiltInAgentId(agentId)) {
18987
+ return errorResponse(`Built-in agent cannot be edited: ${agentId}`, 400);
18988
+ }
18989
+ if (!config.agents[agentId]) {
18990
+ return errorResponse(`Unknown agent: ${agentId}`, 404);
18991
+ }
18992
+ const parsed = await parseJsonBody(req, UpsertCustomAgentRequestSchema);
18993
+ if (!parsed.ok)
18994
+ return parsed.response;
18995
+ const body = parsed.data;
18996
+ const agentConfig = {
18997
+ label: body.label,
18998
+ startCommand: body.startCommand,
18999
+ ...body.resumeCommand?.trim() ? { resumeCommand: body.resumeCommand.trim() } : {}
19000
+ };
19001
+ await persistLocalCustomAgent(PROJECT_DIR, agentId, agentConfig);
19002
+ config.agents[agentId] = agentConfig;
19003
+ const agent = listAgentDetails(config).find((entry) => entry.id === agentId);
19004
+ if (!agent) {
19005
+ return errorResponse(`Updated agent could not be loaded: ${agentId}`, 500);
19006
+ }
19007
+ return jsonResponse({ agent });
19008
+ }
19009
+ async function apiDeleteAgent(agentId) {
19010
+ if (isBuiltInAgentId(agentId)) {
19011
+ return errorResponse(`Built-in agent cannot be deleted: ${agentId}`, 400);
19012
+ }
19013
+ if (!config.agents[agentId]) {
19014
+ return errorResponse(`Unknown agent: ${agentId}`, 404);
19015
+ }
19016
+ await removeLocalCustomAgent(PROJECT_DIR, agentId);
19017
+ delete config.agents[agentId];
19018
+ return jsonResponse({ ok: true });
19019
+ }
18293
19020
  async function apiSetLinearAutoCreate(req) {
18294
19021
  const parsed = await parseJsonBody(req, ToggleEnabledRequestSchema);
18295
19022
  if (!parsed.ok)
@@ -18332,7 +19059,7 @@ async function apiPullMain(req) {
18332
19059
  return errorResponse(`Unknown linked repo: ${repo}`, 404);
18333
19060
  if (!linkedRepo.dir)
18334
19061
  return errorResponse(`Linked repo "${repo}" has no dir configured`, 400);
18335
- const resolvedDir = resolve8(PROJECT_DIR, linkedRepo.dir);
19062
+ const resolvedDir = resolve9(PROJECT_DIR, linkedRepo.dir);
18336
19063
  const repoRoot = git.resolveRepoRoot(resolvedDir);
18337
19064
  if (!repoRoot)
18338
19065
  return errorResponse(`Linked repo "${repo}" dir is not a git repository: ${resolvedDir}`, 400);
@@ -18423,7 +19150,7 @@ async function apiUploadFiles(name, req) {
18423
19150
  }
18424
19151
  const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
18425
19152
  const destPath = join8(uploadDir, safeName);
18426
- if (!resolve8(destPath).startsWith(uploadDir + "/")) {
19153
+ if (!resolve9(destPath).startsWith(uploadDir + "/")) {
18427
19154
  return errorResponse("Invalid filename", 400);
18428
19155
  }
18429
19156
  await Bun.write(destPath, entry);
@@ -18465,6 +19192,22 @@ function parseNotificationIdParam(params) {
18465
19192
  data: parsed.data.id
18466
19193
  };
18467
19194
  }
19195
+ function parseAgentIdParam(params) {
19196
+ const parsed = parseParams(params, AgentIdParamsSchema);
19197
+ if (!parsed.ok)
19198
+ return parsed;
19199
+ const agentId = parsed.data.id.trim();
19200
+ if (!agentId) {
19201
+ return {
19202
+ ok: false,
19203
+ response: errorResponse("Invalid agent id", 400)
19204
+ };
19205
+ }
19206
+ return {
19207
+ ok: true,
19208
+ data: agentId
19209
+ };
19210
+ }
18468
19211
  Bun.serve({
18469
19212
  port: PORT,
18470
19213
  idleTimeout: 255,
@@ -18491,6 +19234,27 @@ Bun.serve({
18491
19234
  [apiPaths.fetchProject]: {
18492
19235
  GET: () => catching("GET /api/project", () => apiGetProject())
18493
19236
  },
19237
+ [apiPaths.fetchAgents]: {
19238
+ GET: () => catching("GET /api/agents", () => apiListAgents()),
19239
+ POST: (req) => catching("POST /api/agents", () => apiCreateAgent(req))
19240
+ },
19241
+ [apiPaths.validateAgent]: {
19242
+ POST: (req) => catching("POST /api/agents/validate", () => apiValidateAgent(req))
19243
+ },
19244
+ [apiPaths.updateAgent]: {
19245
+ PUT: (req) => {
19246
+ const parsed = parseAgentIdParam(req.params);
19247
+ if (!parsed.ok)
19248
+ return parsed.response;
19249
+ return catching("PUT /api/agents/:id", () => apiUpdateAgent(parsed.data, req));
19250
+ },
19251
+ DELETE: (req) => {
19252
+ const parsed = parseAgentIdParam(req.params);
19253
+ if (!parsed.ok)
19254
+ return parsed.response;
19255
+ return catching("DELETE /api/agents/:id", () => apiDeleteAgent(parsed.data));
19256
+ }
19257
+ },
18494
19258
  [apiPaths.attachAgentsWorktreeConversation]: {
18495
19259
  POST: (req) => {
18496
19260
  const parsed = parseWorktreeNameParam(req.params);
@@ -18655,8 +19419,8 @@ Bun.serve({
18655
19419
  if (STATIC_DIR) {
18656
19420
  const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
18657
19421
  const filePath = join8(STATIC_DIR, rawPath);
18658
- const staticRoot = resolve8(STATIC_DIR);
18659
- if (!resolve8(filePath).startsWith(staticRoot + "/")) {
19422
+ const staticRoot = resolve9(STATIC_DIR);
19423
+ if (!resolve9(filePath).startsWith(staticRoot + "/")) {
18660
19424
  return new Response("Forbidden", { status: 403 });
18661
19425
  }
18662
19426
  const file = Bun.file(filePath);