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