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.
- package/backend/dist/server.js +615 -56
- package/bin/webmux.js +364 -59
- package/frontend/dist/assets/{DiffDialog-DgXcbO3H.js → DiffDialog-1-WiuIC4.js} +9 -9
- package/frontend/dist/assets/index-CqUU8MH2.css +1 -0
- package/frontend/dist/assets/index-D4JuH244.js +33 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +3 -4
- package/frontend/dist/assets/index-D4MJ4sZs.css +0 -1
- package/frontend/dist/assets/index-DElupuYu.js +0 -33
package/backend/dist/server.js
CHANGED
|
@@ -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
|
|
11003
|
-
var
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
14811
|
-
if (
|
|
14812
|
-
|
|
14813
|
-
|
|
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
|
|
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
|
|
14836
|
-
if (
|
|
14837
|
-
throw new LifecycleError("Creating
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
if (agent
|
|
15026
|
-
throw new LifecycleError(`Unknown agent: ${
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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}` : ""}
|
|
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
|
-
|
|
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);
|