webmux 0.28.0 → 0.29.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.
@@ -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,
@@ -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,182 @@ 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
+ function resolveAgentChatSupport(input) {
13380
+ if (!input.agentId) {
13381
+ return {
13382
+ ok: false,
13383
+ error: "This worktree has no agent configured",
13384
+ status: 409
13385
+ };
13386
+ }
13387
+ if (!input.agent) {
13388
+ return {
13389
+ ok: false,
13390
+ error: `Unknown agent: ${input.agentId}`,
13391
+ status: 404
13392
+ };
13393
+ }
13394
+ const agentLabel = input.agentLabel ?? input.agent.label;
13395
+ if (!input.agent.capabilities.inAppChat || !input.agent.capabilities.conversationHistory) {
13396
+ return {
13397
+ ok: false,
13398
+ error: `${agentLabel} does not support in-app chat`,
13399
+ status: 409
13400
+ };
13401
+ }
13402
+ if (input.action === "interrupt" && !input.agent.capabilities.interrupt) {
13403
+ return {
13404
+ ok: false,
13405
+ error: `${agentLabel} cannot be interrupted from the dashboard`,
13406
+ status: 409
13407
+ };
13408
+ }
13409
+ if (input.agent.kind === "builtin") {
13410
+ return {
13411
+ ok: true,
13412
+ data: {
13413
+ provider: input.agent.implementation.agent,
13414
+ submitDelayMs: input.agent.implementation.agent === "codex" ? 200 : 0
13415
+ }
13416
+ };
13417
+ }
13418
+ return {
13419
+ ok: false,
13420
+ error: `Dashboard chat is not available for ${agentLabel}`,
13421
+ status: 409
13422
+ };
13423
+ }
13424
+
13425
+ // backend/src/services/agent-registry.ts
13426
+ var BUILTIN_AGENT_DEFINITIONS = [
13427
+ {
13428
+ id: "claude",
13429
+ label: "Claude",
13430
+ kind: "builtin",
13431
+ capabilities: {
13432
+ terminal: true,
13433
+ inAppChat: true,
13434
+ conversationHistory: true,
13435
+ interrupt: true,
13436
+ resume: true
13437
+ },
13438
+ implementation: {
13439
+ type: "builtin",
13440
+ agent: "claude"
13441
+ }
13442
+ },
13443
+ {
13444
+ id: "codex",
13445
+ label: "Codex",
13446
+ kind: "builtin",
13447
+ capabilities: {
13448
+ terminal: true,
13449
+ inAppChat: true,
13450
+ conversationHistory: true,
13451
+ interrupt: true,
13452
+ resume: true
13453
+ },
13454
+ implementation: {
13455
+ type: "builtin",
13456
+ agent: "codex"
13457
+ }
13458
+ }
13459
+ ];
13460
+ function cloneCapabilities(capabilities) {
13461
+ return { ...capabilities };
13462
+ }
13463
+ function isBuiltInAgentId(agentId) {
13464
+ return BUILTIN_AGENT_DEFINITIONS.some((agent) => agent.id === agentId);
13465
+ }
13466
+ function normalizeCustomAgentId(label) {
13467
+ const normalized = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
13468
+ return normalized || "agent";
13469
+ }
13470
+ function cloneDefinition(definition) {
13471
+ if (definition.kind === "builtin") {
13472
+ return {
13473
+ ...definition,
13474
+ capabilities: cloneCapabilities(definition.capabilities),
13475
+ implementation: { ...definition.implementation }
13476
+ };
13477
+ }
13478
+ return {
13479
+ ...definition,
13480
+ capabilities: cloneCapabilities(definition.capabilities),
13481
+ implementation: {
13482
+ type: "custom",
13483
+ config: { ...definition.implementation.config }
13484
+ }
13485
+ };
13486
+ }
13487
+ function buildCustomAgentDefinition(id, config) {
13488
+ return {
13489
+ id,
13490
+ label: config.label,
13491
+ kind: "custom",
13492
+ capabilities: {
13493
+ terminal: true,
13494
+ inAppChat: false,
13495
+ conversationHistory: false,
13496
+ interrupt: false,
13497
+ resume: config.resumeCommand !== undefined
13498
+ },
13499
+ implementation: {
13500
+ type: "custom",
13501
+ config: { ...config }
13502
+ }
13503
+ };
13504
+ }
13505
+ function listAgentDefinitions(config) {
13506
+ const builtInIds = new Set(BUILTIN_AGENT_DEFINITIONS.map((agent) => agent.id));
13507
+ const customDefinitions = Object.entries(config.agents).filter(([id]) => !builtInIds.has(id)).sort(([leftId, left], [rightId, right]) => {
13508
+ const labelCompare = left.label.localeCompare(right.label);
13509
+ return labelCompare !== 0 ? labelCompare : leftId.localeCompare(rightId);
13510
+ }).map(([id, agent]) => buildCustomAgentDefinition(id, agent));
13511
+ return [
13512
+ ...BUILTIN_AGENT_DEFINITIONS.map((definition) => cloneDefinition(definition)),
13513
+ ...customDefinitions
13514
+ ];
13515
+ }
13516
+ function getAgentDefinition(config, agentId) {
13517
+ const definition = listAgentDefinitions(config).find((agent) => agent.id === agentId);
13518
+ return definition ?? null;
13519
+ }
13520
+ function listAgentSummaries(config) {
13521
+ return listAgentDefinitions(config).map((agent) => ({
13522
+ id: agent.id,
13523
+ label: agent.label,
13524
+ kind: agent.kind,
13525
+ capabilities: cloneCapabilities(agent.capabilities)
13526
+ }));
13527
+ }
13528
+ function listAgentDetails(config) {
13529
+ return listAgentDefinitions(config).map((agent) => ({
13530
+ id: agent.id,
13531
+ label: agent.label,
13532
+ kind: agent.kind,
13533
+ capabilities: cloneCapabilities(agent.capabilities),
13534
+ startCommand: agent.kind === "custom" ? agent.implementation.config.startCommand : null,
13535
+ resumeCommand: agent.kind === "custom" ? agent.implementation.config.resumeCommand ?? null : null
13536
+ }));
13537
+ }
13538
+
13539
+ // backend/src/services/agent-validation-service.ts
13540
+ function validateCustomAgentInput(input) {
13541
+ const warnings = [];
13542
+ if (!input.startCommand.includes("${PROMPT}") && !input.startCommand.includes("${SYSTEM_PROMPT}")) {
13543
+ warnings.push("Start command does not reference ${PROMPT} or ${SYSTEM_PROMPT}; initial prompts will not be passed automatically");
13544
+ }
13545
+ if (!input.resumeCommand?.trim()) {
13546
+ warnings.push("Resume command is not configured; reopening the worktree will restart the agent");
13547
+ }
13548
+ return {
13549
+ normalizedId: normalizeCustomAgentId(input.label),
13550
+ warnings
13551
+ };
13552
+ }
13553
+
13211
13554
  // backend/src/services/linear-service.ts
13212
13555
  var ASSIGNED_ISSUES_QUERY = `
13213
13556
  query AssignedIssues {
@@ -14184,6 +14527,14 @@ function allocateServicePorts(existingMetas, services) {
14184
14527
 
14185
14528
  // backend/src/services/agent-service.ts
14186
14529
  var DOCKER_PATH_FALLBACK = "/root/.local/bin:/usr/local/bin:/root/.bun/bin:/root/.cargo/bin";
14530
+ var CUSTOM_AGENT_TEMPLATE_VARS = {
14531
+ PROMPT: "WEBMUX_AGENT_PROMPT",
14532
+ SYSTEM_PROMPT: "WEBMUX_AGENT_SYSTEM_PROMPT",
14533
+ WORKTREE_PATH: "WEBMUX_AGENT_WORKTREE_PATH",
14534
+ REPO_PATH: "WEBMUX_AGENT_REPO_PATH",
14535
+ BRANCH: "WEBMUX_AGENT_BRANCH",
14536
+ PROFILE: "WEBMUX_AGENT_PROFILE"
14537
+ };
14187
14538
  function quoteShell(value) {
14188
14539
  return `'${value.replaceAll("'", "'\\''")}'`;
14189
14540
  }
@@ -14193,7 +14544,7 @@ function buildRuntimeBootstrap(runtimeEnvPath) {
14193
14544
  function buildDockerRuntimeBootstrap(runtimeEnvPath) {
14194
14545
  return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
14195
14546
  }
14196
- function buildAgentInvocation(input) {
14547
+ function buildBuiltInAgentInvocation(input) {
14197
14548
  if (input.agent === "codex") {
14198
14549
  const yoloFlag2 = input.yolo ? " --yolo" : "";
14199
14550
  if (input.launchMode === "resume") {
@@ -14215,6 +14566,47 @@ function buildAgentInvocation(input) {
14215
14566
  }
14216
14567
  return `claude${yoloFlag}${promptSuffix}`;
14217
14568
  }
14569
+ function renderCustomCommandTemplate(template) {
14570
+ 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}`);
14571
+ }
14572
+ function buildCustomAgentExports(input) {
14573
+ const envEntries = [
14574
+ [CUSTOM_AGENT_TEMPLATE_VARS.PROMPT, input.prompt ?? ""],
14575
+ [CUSTOM_AGENT_TEMPLATE_VARS.SYSTEM_PROMPT, input.systemPrompt ?? ""],
14576
+ [CUSTOM_AGENT_TEMPLATE_VARS.WORKTREE_PATH, input.worktreePath],
14577
+ [CUSTOM_AGENT_TEMPLATE_VARS.REPO_PATH, input.repoRoot],
14578
+ [CUSTOM_AGENT_TEMPLATE_VARS.BRANCH, input.branch],
14579
+ [CUSTOM_AGENT_TEMPLATE_VARS.PROFILE, input.profileName]
14580
+ ];
14581
+ return envEntries.map(([key, value]) => `export ${key}=${quoteShell(value)}`).join("; ");
14582
+ }
14583
+ function buildCustomAgentInvocation(input) {
14584
+ const template = input.launchMode === "resume" && input.agent.implementation.config.resumeCommand ? input.agent.implementation.config.resumeCommand : input.agent.implementation.config.startCommand;
14585
+ const exports = buildCustomAgentExports(input);
14586
+ const renderedCommand = renderCustomCommandTemplate(template);
14587
+ return `${exports}; ${renderedCommand}`;
14588
+ }
14589
+ function buildAgentInvocation(input) {
14590
+ if (input.agent.kind === "builtin") {
14591
+ return buildBuiltInAgentInvocation({
14592
+ agent: input.agent.implementation.agent,
14593
+ yolo: input.yolo,
14594
+ systemPrompt: input.systemPrompt,
14595
+ prompt: input.prompt,
14596
+ launchMode: input.launchMode
14597
+ });
14598
+ }
14599
+ return buildCustomAgentInvocation({
14600
+ agent: input.agent,
14601
+ systemPrompt: input.systemPrompt,
14602
+ prompt: input.prompt,
14603
+ worktreePath: input.worktreePath,
14604
+ repoRoot: input.repoRoot,
14605
+ branch: input.branch,
14606
+ profileName: input.profileName,
14607
+ launchMode: input.launchMode
14608
+ });
14609
+ }
14218
14610
  function buildAgentCommand(input, bootstrap = buildRuntimeBootstrap) {
14219
14611
  return `${bootstrap(input.runtimeEnvPath)}; ${buildAgentInvocation(input)}`;
14220
14612
  }
@@ -14807,14 +15199,15 @@ function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
14807
15199
  function prefixAgentBranch(agent, branch) {
14808
15200
  return `${agent}-${branch}`;
14809
15201
  }
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
- ];
15202
+ function buildCreateWorktreeTargets(branch, agentIds) {
15203
+ if (agentIds.length <= 1) {
15204
+ const agent = agentIds[0];
15205
+ return agent ? [{ branch, agent }] : [];
14816
15206
  }
14817
- return [{ branch, agent: agentSelection }];
15207
+ return agentIds.map((agent) => ({
15208
+ branch: prefixAgentBranch(agent, branch),
15209
+ agent
15210
+ }));
14818
15211
  }
14819
15212
 
14820
15213
  class LifecycleError extends Error {
@@ -14832,12 +15225,12 @@ class LifecycleService {
14832
15225
  }
14833
15226
  async createWorktrees(input) {
14834
15227
  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);
15228
+ const agentIds = this.resolveSelectedAgents(input);
15229
+ if (agentIds.length > 1 && mode === "existing") {
15230
+ throw new LifecycleError("Creating multiple agents is only supported for new worktrees", 400);
14838
15231
  }
14839
15232
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
14840
- const targets = buildCreateWorktreeTargets(branch, agentSelection);
15233
+ const targets = buildCreateWorktreeTargets(branch, agentIds);
14841
15234
  const createdBranches = [];
14842
15235
  try {
14843
15236
  for (const target of targets) {
@@ -14864,28 +15257,30 @@ class LifecycleService {
14864
15257
  async createWorktree(input) {
14865
15258
  const mode = input.mode ?? "new";
14866
15259
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
14867
- const agent = this.resolveAgent(input.agent);
15260
+ const agent = this.resolveAgentDefinition(input.agent);
14868
15261
  return await this.createResolvedWorktree({
14869
15262
  ...input,
14870
15263
  mode,
14871
15264
  branch,
14872
- agent
15265
+ agent: agent.id
14873
15266
  });
14874
15267
  }
14875
15268
  async openWorktree(branch) {
14876
15269
  try {
14877
15270
  const resolved = await this.resolveExistingWorktree(branch);
14878
- const launchMode = resolved.meta ? "resume" : "fresh";
14879
15271
  const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
14880
- const { profile } = this.resolveProfile(initialized.meta.profile);
15272
+ const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
15273
+ const agent = this.resolveAgentDefinition(initialized.meta.agent);
15274
+ const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
14881
15275
  await ensureAgentRuntimeArtifacts({
14882
15276
  gitDir: initialized.paths.gitDir,
14883
15277
  worktreePath: resolved.entry.path
14884
15278
  });
14885
15279
  await this.materializeRuntimeSession({
14886
15280
  branch,
15281
+ profileName,
14887
15282
  profile,
14888
- agent: initialized.meta.agent,
15283
+ agent,
14889
15284
  initialized,
14890
15285
  worktreePath: resolved.entry.path,
14891
15286
  launchMode
@@ -15019,14 +15414,22 @@ class LifecycleService {
15019
15414
  profile
15020
15415
  };
15021
15416
  }
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);
15417
+ resolveAgentDefinition(agentId) {
15418
+ const resolvedAgentId = agentId ?? this.deps.config.workspace.defaultAgent;
15419
+ const agent = getAgentDefinition(this.deps.config, resolvedAgentId);
15420
+ if (!agent) {
15421
+ throw new LifecycleError(`Unknown agent: ${resolvedAgentId}`, 400);
15027
15422
  }
15028
15423
  return agent;
15029
15424
  }
15425
+ resolveSelectedAgents(input) {
15426
+ const selectedAgents = input.agents && input.agents.length > 0 ? input.agents : [input.agent ?? this.deps.config.workspace.defaultAgent];
15427
+ const dedupedAgentIds = [...new Set(selectedAgents.map((agent) => agent.trim()).filter((agent) => agent.length > 0))];
15428
+ if (dedupedAgentIds.length === 0) {
15429
+ throw new LifecycleError("At least one agent must be selected", 400);
15430
+ }
15431
+ return dedupedAgentIds.map((agentId) => this.resolveAgentDefinition(agentId).id);
15432
+ }
15030
15433
  async buildStartupEnvValues(envOverrides) {
15031
15434
  const startupEnvValues = Object.fromEntries(Object.entries(this.deps.config.startupEnvs).map(([key, value]) => [key, stringifyStartupEnvValue(value)]));
15032
15435
  for (const [key, value] of Object.entries(envOverrides ?? {})) {
@@ -15148,6 +15551,7 @@ class LifecycleService {
15148
15551
  });
15149
15552
  ensureSessionLayout(this.deps.tmux, this.buildSessionLayout({
15150
15553
  branch: input.branch,
15554
+ profileName: input.profileName,
15151
15555
  profile: input.profile,
15152
15556
  agent: input.agent,
15153
15557
  initialized: input.initialized,
@@ -15160,6 +15564,7 @@ class LifecycleService {
15160
15564
  }
15161
15565
  ensureSessionLayout(this.deps.tmux, this.buildSessionLayout({
15162
15566
  branch: input.branch,
15567
+ profileName: input.profileName,
15163
15568
  profile: input.profile,
15164
15569
  agent: input.agent,
15165
15570
  initialized: input.initialized,
@@ -15178,6 +15583,10 @@ class LifecycleService {
15178
15583
  agent: buildDockerAgentPaneCommand({
15179
15584
  agent: input.agent,
15180
15585
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
15586
+ repoRoot: this.deps.projectRoot,
15587
+ worktreePath: input.worktreePath,
15588
+ branch: input.branch,
15589
+ profileName: input.profileName,
15181
15590
  yolo: input.profile.yolo === true,
15182
15591
  systemPrompt,
15183
15592
  prompt: input.launchMode === "fresh" ? input.prompt : undefined,
@@ -15188,6 +15597,10 @@ class LifecycleService {
15188
15597
  agent: buildAgentPaneCommand({
15189
15598
  agent: input.agent,
15190
15599
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
15600
+ repoRoot: this.deps.projectRoot,
15601
+ worktreePath: input.worktreePath,
15602
+ branch: input.branch,
15603
+ profileName: input.profileName,
15191
15604
  yolo: input.profile.yolo === true,
15192
15605
  systemPrompt,
15193
15606
  prompt: input.launchMode === "fresh" ? input.prompt : undefined,
@@ -15312,6 +15725,7 @@ class LifecycleService {
15312
15725
  const baseBranch = input.mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
15313
15726
  const branchAvailability = this.resolveBranchAvailability(input.branch, input.mode);
15314
15727
  const { profileName, profile } = this.resolveProfile(input.profile);
15728
+ const agent = this.resolveAgentDefinition(input.agent);
15315
15729
  const worktreePath = this.resolveWorktreePath(input.branch);
15316
15730
  const createProgressBase = {
15317
15731
  branch: input.branch,
@@ -15336,7 +15750,7 @@ class LifecycleService {
15336
15750
  ...baseBranch ? { baseBranch } : {},
15337
15751
  ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
15338
15752
  profile: profileName,
15339
- agent: input.agent,
15753
+ agent: agent.id,
15340
15754
  runtime: profile.runtime,
15341
15755
  startupEnvValues: await this.buildStartupEnvValues(input.envOverrides),
15342
15756
  allocatedPorts: await this.allocatePorts(),
@@ -15376,8 +15790,9 @@ class LifecycleService {
15376
15790
  });
15377
15791
  await this.materializeRuntimeSession({
15378
15792
  branch: input.branch,
15793
+ profileName,
15379
15794
  profile,
15380
- agent: input.agent,
15795
+ agent,
15381
15796
  initialized,
15382
15797
  worktreePath,
15383
15798
  prompt: input.prompt,
@@ -16114,7 +16529,7 @@ function mapCreationSnapshot(creating) {
16114
16529
  phase: creating.phase
16115
16530
  } : null;
16116
16531
  }
16117
- function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue) {
16532
+ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
16118
16533
  return {
16119
16534
  branch: state.branch,
16120
16535
  ...state.baseBranch ? { baseBranch: state.baseBranch } : {},
@@ -16123,6 +16538,7 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue)
16123
16538
  archived: isArchived(state.path),
16124
16539
  profile: state.profile,
16125
16540
  agentName: state.agentName,
16541
+ agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
16126
16542
  mux: state.session.exists,
16127
16543
  dirty: state.git.dirty,
16128
16544
  unpushed: state.git.aheadCount > 0,
@@ -16135,7 +16551,7 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue)
16135
16551
  creation: mapCreationSnapshot(creating)
16136
16552
  };
16137
16553
  }
16138
- function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
16554
+ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
16139
16555
  return {
16140
16556
  branch: creating.branch,
16141
16557
  ...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
@@ -16144,6 +16560,7 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue) {
16144
16560
  archived: isArchived(creating.path),
16145
16561
  profile: creating.profile,
16146
16562
  agentName: creating.agentName,
16563
+ agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
16147
16564
  mux: false,
16148
16565
  dirty: false,
16149
16566
  unpushed: false,
@@ -16163,10 +16580,10 @@ function buildWorktreeSnapshots(input) {
16163
16580
  const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
16164
16581
  const runtimeWorktrees = input.runtime.listWorktrees();
16165
16582
  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));
16583
+ const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
16167
16584
  for (const creating of creatingWorktrees) {
16168
16585
  if (!runtimeBranches.has(creating.branch)) {
16169
- worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue));
16586
+ worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
16170
16587
  }
16171
16588
  }
16172
16589
  worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
@@ -16195,6 +16612,7 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
16195
16612
  archived: worktree.archived,
16196
16613
  profile: worktree.profile,
16197
16614
  agentName: worktree.agentName,
16615
+ agentLabel: worktree.agentLabel,
16198
16616
  mux: worktree.mux,
16199
16617
  status: worktree.status,
16200
16618
  dirty: worktree.dirty,
@@ -16947,7 +17365,7 @@ class BunPortProbe {
16947
17365
  // backend/src/services/auto-name-service.ts
16948
17366
  var MAX_BRANCH_LENGTH = 40;
16949
17367
  var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
16950
- var AUTO_NAME_TIMEOUT_MS = 1e4;
17368
+ var AUTO_NAME_TIMEOUT_MS = 15000;
16951
17369
  var DEFAULT_SYSTEM_PROMPT = [
16952
17370
  "Generate a concise git branch name from the task description.",
16953
17371
  "Return only the branch name.",
@@ -17668,7 +18086,6 @@ function createWebmuxRuntime(options = {}) {
17668
18086
  // backend/src/server.ts
17669
18087
  var PORT = parseInt(Bun.env.PORT || "5111", 10);
17670
18088
  var STATIC_DIR = Bun.env.WEBMUX_STATIC_DIR || "";
17671
- var CODEX_TMUX_PROMPT_SUBMIT_DELAY_MS = 200;
17672
18089
  var runtime = createWebmuxRuntime({
17673
18090
  port: PORT,
17674
18091
  projectDir: Bun.env.WEBMUX_PROJECT_DIR || process.cwd()
@@ -17741,7 +18158,9 @@ function getFrontendConfig() {
17741
18158
  name,
17742
18159
  ...profile.systemPrompt ? { systemPrompt: profile.systemPrompt } : {}
17743
18160
  })),
18161
+ agents: listAgentSummaries(config),
17744
18162
  defaultProfileName,
18163
+ defaultAgentId: config.workspace.defaultAgent,
17745
18164
  autoName: config.autoName !== null,
17746
18165
  linearCreateTicketOption: config.integrations.linear.enabled && config.integrations.linear.createTicketOption,
17747
18166
  startupEnvs: config.startupEnvs,
@@ -17941,6 +18360,11 @@ async function readProjectSnapshot() {
17941
18360
  url: match.url,
17942
18361
  state: match.state
17943
18362
  } : null;
18363
+ },
18364
+ findAgentLabel: (agentId) => {
18365
+ if (!agentId)
18366
+ return null;
18367
+ return getAgentDefinition(config, agentId)?.label ?? agentId;
17944
18368
  }
17945
18369
  });
17946
18370
  }
@@ -17971,12 +18395,24 @@ async function resolveAgentsWorktree(branch) {
17971
18395
  worktree
17972
18396
  };
17973
18397
  }
18398
+ function resolveWorktreeAgentChatSupport(worktree, action) {
18399
+ return resolveAgentChatSupport({
18400
+ agentId: worktree.agentName,
18401
+ agentLabel: worktree.agentLabel,
18402
+ agent: worktree.agentName ? getAgentDefinition(config, worktree.agentName) : null,
18403
+ action
18404
+ });
18405
+ }
17974
18406
  async function apiAttachAgentsWorktree(branch) {
17975
18407
  touchDashboardActivity();
17976
18408
  const resolved = await resolveAgentsWorktree(branch);
17977
18409
  if (!resolved.ok)
17978
18410
  return resolved.response;
17979
- const result = resolved.worktree.agentName === "claude" ? await claudeConversationService.attachWorktreeConversation(resolved.worktree) : await worktreeConversationService.attachWorktreeConversation(resolved.worktree);
18411
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18412
+ if (!chatSupport.ok) {
18413
+ return errorResponse(chatSupport.error, chatSupport.status);
18414
+ }
18415
+ const result = chatSupport.data.provider === "claude" ? await claudeConversationService.attachWorktreeConversation(resolved.worktree) : await worktreeConversationService.attachWorktreeConversation(resolved.worktree);
17980
18416
  return result.ok ? jsonResponse(result.data) : errorResponse(result.error, result.status);
17981
18417
  }
17982
18418
  async function apiGetAgentsWorktreeHistory(branch) {
@@ -17984,7 +18420,11 @@ async function apiGetAgentsWorktreeHistory(branch) {
17984
18420
  const resolved = await resolveAgentsWorktree(branch);
17985
18421
  if (!resolved.ok)
17986
18422
  return resolved.response;
17987
- const result = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18423
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18424
+ if (!chatSupport.ok) {
18425
+ return errorResponse(chatSupport.error, chatSupport.status);
18426
+ }
18427
+ const result = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
17988
18428
  return result.ok ? jsonResponse(result.data) : errorResponse(result.error, result.status);
17989
18429
  }
17990
18430
  async function apiSendAgentsWorktreeMessage(branch, req) {
@@ -17998,14 +18438,18 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
17998
18438
  if (!resolved.worktree.mux) {
17999
18439
  return errorResponse("Open this worktree in the main dashboard before sending messages here", 409);
18000
18440
  }
18001
- const conversationResult = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18441
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18442
+ if (!chatSupport.ok) {
18443
+ return errorResponse(chatSupport.error, chatSupport.status);
18444
+ }
18445
+ const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18002
18446
  if (!conversationResult.ok) {
18003
18447
  return errorResponse(conversationResult.error, conversationResult.status);
18004
18448
  }
18005
18449
  const terminalWorktree = await resolveAgentsTerminalWorktree(branch);
18006
18450
  if (!terminalWorktree.ok)
18007
18451
  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);
18452
+ const sendResult = await sendPrompt(terminalWorktree.data.worktreeId, terminalWorktree.data.attachTarget, parsed.data.text, 0, undefined, chatSupport.data.submitDelayMs);
18009
18453
  if (!sendResult.ok) {
18010
18454
  return errorResponse(sendResult.error, 503);
18011
18455
  }
@@ -18023,7 +18467,11 @@ async function apiInterruptAgentsWorktree(branch) {
18023
18467
  if (!resolved.worktree.mux) {
18024
18468
  return errorResponse("Open this worktree in the main dashboard before interrupting it here", 409);
18025
18469
  }
18026
- const conversationResult = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18470
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "interrupt");
18471
+ if (!chatSupport.ok) {
18472
+ return errorResponse(chatSupport.error, chatSupport.status);
18473
+ }
18474
+ const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18027
18475
  if (!conversationResult.ok) {
18028
18476
  return errorResponse(conversationResult.error, conversationResult.status);
18029
18477
  }
@@ -18048,7 +18496,14 @@ async function loadAgentsConversationSnapshot(branch) {
18048
18496
  message: await readErrorMessage(resolved.response)
18049
18497
  };
18050
18498
  }
18051
- const result = resolved.worktree.agentName === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18499
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
18500
+ if (!chatSupport.ok) {
18501
+ return {
18502
+ ok: false,
18503
+ message: chatSupport.error
18504
+ };
18505
+ }
18506
+ const result = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
18052
18507
  return result.ok ? { ok: true, data: result.data } : { ok: false, message: result.error };
18053
18508
  }
18054
18509
  async function readErrorMessage(response) {
@@ -18167,10 +18622,11 @@ async function apiCreateWorktree(req) {
18167
18622
  const prompt = body.prompt?.trim() ? body.prompt.trim() : undefined;
18168
18623
  const profile = body.profile;
18169
18624
  const agent = body.agent;
18625
+ const agents = body.agents;
18170
18626
  const createLinearTicket = body.createLinearTicket === true;
18171
18627
  const linearTitle = body.linearTitle?.trim() ? body.linearTitle.trim() : undefined;
18172
18628
  const mode = body.mode;
18173
- const agentSelection = agent ?? config.workspace.defaultAgent;
18629
+ const selectedAgents = agents ? agents : agent ? [agent] : [config.workspace.defaultAgent];
18174
18630
  if (baseBranch && !isValidBranchName(baseBranch)) {
18175
18631
  return errorResponse("Invalid base branch name", 400);
18176
18632
  }
@@ -18211,7 +18667,7 @@ async function apiCreateWorktree(req) {
18211
18667
  log.info(`[linear] created ticket ${linearResult.data.identifier} branch=${linearResult.data.branchName} title="${linearResult.data.title.slice(0, 80)}"`);
18212
18668
  }
18213
18669
  if (resolvedBranch) {
18214
- const targetBranches = buildCreateWorktreeTargets(resolvedBranch, agentSelection).map((target) => target.branch);
18670
+ const targetBranches = buildCreateWorktreeTargets(resolvedBranch, selectedAgents).map((target) => target.branch);
18215
18671
  for (const targetBranch of targetBranches) {
18216
18672
  ensureBranchNotBusy(targetBranch);
18217
18673
  }
@@ -18219,14 +18675,14 @@ async function apiCreateWorktree(req) {
18219
18675
  return errorResponse("Base branch must differ from branch name", 400);
18220
18676
  }
18221
18677
  }
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)}"` : ""}`);
18678
+ 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
18679
  const result = await lifecycleService.createWorktrees({
18224
18680
  mode,
18225
18681
  branch: resolvedBranch,
18226
18682
  baseBranch,
18227
18683
  prompt,
18228
18684
  profile,
18229
- agent: agentSelection,
18685
+ ...agents && agents.length > 0 ? { agents } : { agent },
18230
18686
  envOverrides
18231
18687
  });
18232
18688
  log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
@@ -18290,6 +18746,72 @@ async function apiMergeWorktree(name) {
18290
18746
  log.debug(`[worktree:merge] done name=${name}`);
18291
18747
  return jsonResponse({ ok: true });
18292
18748
  }
18749
+ async function apiListAgents() {
18750
+ return jsonResponse({ agents: listAgentDetails(config) });
18751
+ }
18752
+ async function apiValidateAgent(req) {
18753
+ const parsed = await parseJsonBody(req, UpsertCustomAgentRequestSchema);
18754
+ if (!parsed.ok)
18755
+ return parsed.response;
18756
+ return jsonResponse(validateCustomAgentInput(parsed.data));
18757
+ }
18758
+ async function apiCreateAgent(req) {
18759
+ const parsed = await parseJsonBody(req, UpsertCustomAgentRequestSchema);
18760
+ if (!parsed.ok)
18761
+ return parsed.response;
18762
+ const body = parsed.data;
18763
+ const agentId = normalizeCustomAgentId(body.label);
18764
+ if (isBuiltInAgentId(agentId) || config.agents[agentId]) {
18765
+ return errorResponse(`Agent already exists: ${agentId}`, 409);
18766
+ }
18767
+ const agentConfig = {
18768
+ label: body.label,
18769
+ startCommand: body.startCommand,
18770
+ ...body.resumeCommand?.trim() ? { resumeCommand: body.resumeCommand.trim() } : {}
18771
+ };
18772
+ await persistLocalCustomAgent(PROJECT_DIR, agentId, agentConfig);
18773
+ config.agents[agentId] = agentConfig;
18774
+ const agent = listAgentDetails(config).find((entry) => entry.id === agentId);
18775
+ if (!agent) {
18776
+ return errorResponse(`Created agent could not be loaded: ${agentId}`, 500);
18777
+ }
18778
+ return jsonResponse({ agent });
18779
+ }
18780
+ async function apiUpdateAgent(agentId, req) {
18781
+ if (isBuiltInAgentId(agentId)) {
18782
+ return errorResponse(`Built-in agent cannot be edited: ${agentId}`, 400);
18783
+ }
18784
+ if (!config.agents[agentId]) {
18785
+ return errorResponse(`Unknown agent: ${agentId}`, 404);
18786
+ }
18787
+ const parsed = await parseJsonBody(req, UpsertCustomAgentRequestSchema);
18788
+ if (!parsed.ok)
18789
+ return parsed.response;
18790
+ const body = parsed.data;
18791
+ const agentConfig = {
18792
+ label: body.label,
18793
+ startCommand: body.startCommand,
18794
+ ...body.resumeCommand?.trim() ? { resumeCommand: body.resumeCommand.trim() } : {}
18795
+ };
18796
+ await persistLocalCustomAgent(PROJECT_DIR, agentId, agentConfig);
18797
+ config.agents[agentId] = agentConfig;
18798
+ const agent = listAgentDetails(config).find((entry) => entry.id === agentId);
18799
+ if (!agent) {
18800
+ return errorResponse(`Updated agent could not be loaded: ${agentId}`, 500);
18801
+ }
18802
+ return jsonResponse({ agent });
18803
+ }
18804
+ async function apiDeleteAgent(agentId) {
18805
+ if (isBuiltInAgentId(agentId)) {
18806
+ return errorResponse(`Built-in agent cannot be deleted: ${agentId}`, 400);
18807
+ }
18808
+ if (!config.agents[agentId]) {
18809
+ return errorResponse(`Unknown agent: ${agentId}`, 404);
18810
+ }
18811
+ await removeLocalCustomAgent(PROJECT_DIR, agentId);
18812
+ delete config.agents[agentId];
18813
+ return jsonResponse({ ok: true });
18814
+ }
18293
18815
  async function apiSetLinearAutoCreate(req) {
18294
18816
  const parsed = await parseJsonBody(req, ToggleEnabledRequestSchema);
18295
18817
  if (!parsed.ok)
@@ -18465,6 +18987,22 @@ function parseNotificationIdParam(params) {
18465
18987
  data: parsed.data.id
18466
18988
  };
18467
18989
  }
18990
+ function parseAgentIdParam(params) {
18991
+ const parsed = parseParams(params, AgentIdParamsSchema);
18992
+ if (!parsed.ok)
18993
+ return parsed;
18994
+ const agentId = parsed.data.id.trim();
18995
+ if (!agentId) {
18996
+ return {
18997
+ ok: false,
18998
+ response: errorResponse("Invalid agent id", 400)
18999
+ };
19000
+ }
19001
+ return {
19002
+ ok: true,
19003
+ data: agentId
19004
+ };
19005
+ }
18468
19006
  Bun.serve({
18469
19007
  port: PORT,
18470
19008
  idleTimeout: 255,
@@ -18491,6 +19029,27 @@ Bun.serve({
18491
19029
  [apiPaths.fetchProject]: {
18492
19030
  GET: () => catching("GET /api/project", () => apiGetProject())
18493
19031
  },
19032
+ [apiPaths.fetchAgents]: {
19033
+ GET: () => catching("GET /api/agents", () => apiListAgents()),
19034
+ POST: (req) => catching("POST /api/agents", () => apiCreateAgent(req))
19035
+ },
19036
+ [apiPaths.validateAgent]: {
19037
+ POST: (req) => catching("POST /api/agents/validate", () => apiValidateAgent(req))
19038
+ },
19039
+ [apiPaths.updateAgent]: {
19040
+ PUT: (req) => {
19041
+ const parsed = parseAgentIdParam(req.params);
19042
+ if (!parsed.ok)
19043
+ return parsed.response;
19044
+ return catching("PUT /api/agents/:id", () => apiUpdateAgent(parsed.data, req));
19045
+ },
19046
+ DELETE: (req) => {
19047
+ const parsed = parseAgentIdParam(req.params);
19048
+ if (!parsed.ok)
19049
+ return parsed.response;
19050
+ return catching("DELETE /api/agents/:id", () => apiDeleteAgent(parsed.data));
19051
+ }
19052
+ },
18494
19053
  [apiPaths.attachAgentsWorktreeConversation]: {
18495
19054
  POST: (req) => {
18496
19055
  const parsed = parseWorktreeNameParam(req.params);