webmux 0.33.0 → 0.35.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/README.md +2 -0
- package/backend/dist/server.js +724 -423
- package/bin/webmux.js +1382 -329
- package/frontend/dist/assets/DiffDialog-BpDYIOfR.js +112 -0
- package/frontend/dist/assets/index-CU9BHgDe.js +89 -0
- package/frontend/dist/assets/index-DaGuNXKA.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/DiffDialog-BNK_hWb2.js +0 -112
- package/frontend/dist/assets/index-BvV7kChj.js +0 -34
- package/frontend/dist/assets/index-EO_hEDxL.css +0 -1
package/backend/dist/server.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
|
-
var __returnValue = (v) => v;
|
|
5
|
-
function __exportSetter(name, newValue) {
|
|
6
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
-
}
|
|
8
4
|
var __export = (target, all) => {
|
|
9
5
|
for (var name in all)
|
|
10
6
|
__defProp(target, name, {
|
|
11
7
|
get: all[name],
|
|
12
8
|
enumerable: true,
|
|
13
9
|
configurable: true,
|
|
14
|
-
set:
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
15
11
|
});
|
|
16
12
|
};
|
|
17
13
|
var __require = import.meta.require;
|
|
@@ -6965,7 +6961,7 @@ import { networkInterfaces } from "os";
|
|
|
6965
6961
|
// package.json
|
|
6966
6962
|
var package_default = {
|
|
6967
6963
|
name: "webmux",
|
|
6968
|
-
version: "0.
|
|
6964
|
+
version: "0.35.0",
|
|
6969
6965
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
6970
6966
|
type: "module",
|
|
6971
6967
|
repository: {
|
|
@@ -10994,7 +10990,7 @@ var coerce = {
|
|
|
10994
10990
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
10995
10991
|
};
|
|
10996
10992
|
var NEVER = INVALID;
|
|
10997
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
10993
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+c185e43edea803d3/node_modules/@ts-rest/core/index.esm.mjs
|
|
10998
10994
|
var isZodObjectStrict = (obj) => {
|
|
10999
10995
|
return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
|
|
11000
10996
|
};
|
|
@@ -11301,6 +11297,15 @@ var LinearIssuesResponseSchema = exports_external.object({
|
|
|
11301
11297
|
availability: LinearIssueAvailabilitySchema,
|
|
11302
11298
|
issues: exports_external.array(LinearIssueSchema)
|
|
11303
11299
|
});
|
|
11300
|
+
var AutoNameProviderSchema = exports_external.enum(["claude", "codex"]);
|
|
11301
|
+
var AutoNameConfigResponseSchema = exports_external.object({
|
|
11302
|
+
autoName: exports_external.object({
|
|
11303
|
+
provider: AutoNameProviderSchema,
|
|
11304
|
+
model: exports_external.string().optional(),
|
|
11305
|
+
systemPrompt: exports_external.string().optional()
|
|
11306
|
+
}).nullable(),
|
|
11307
|
+
linearAvailability: LinearIssueAvailabilitySchema
|
|
11308
|
+
});
|
|
11304
11309
|
var WorktreeCreationStateSchema = exports_external.object({
|
|
11305
11310
|
phase: WorktreeCreationPhaseSchema
|
|
11306
11311
|
});
|
|
@@ -11322,6 +11327,7 @@ var ProjectWorktreeSnapshotSchema = exports_external.object({
|
|
|
11322
11327
|
profile: exports_external.string().nullable(),
|
|
11323
11328
|
agentName: AgentIdSchema.nullable(),
|
|
11324
11329
|
agentLabel: exports_external.string().nullable(),
|
|
11330
|
+
agentTerminalStale: exports_external.boolean(),
|
|
11325
11331
|
mux: exports_external.boolean(),
|
|
11326
11332
|
dirty: exports_external.boolean(),
|
|
11327
11333
|
unpushed: exports_external.boolean(),
|
|
@@ -11370,6 +11376,7 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
|
|
|
11370
11376
|
profile: exports_external.string().nullable(),
|
|
11371
11377
|
agentName: AgentIdSchema.nullable(),
|
|
11372
11378
|
agentLabel: exports_external.string().nullable(),
|
|
11379
|
+
agentTerminalStale: exports_external.boolean(),
|
|
11373
11380
|
mux: exports_external.boolean(),
|
|
11374
11381
|
status: exports_external.string(),
|
|
11375
11382
|
dirty: exports_external.boolean(),
|
|
@@ -11523,6 +11530,7 @@ var apiPaths = {
|
|
|
11523
11530
|
removeWorktree: "/api/worktrees/:name",
|
|
11524
11531
|
openWorktree: "/api/worktrees/:name/open",
|
|
11525
11532
|
closeWorktree: "/api/worktrees/:name/close",
|
|
11533
|
+
refreshWorktreeAgentTerminal: "/api/worktrees/:name/agent-terminal/refresh",
|
|
11526
11534
|
setWorktreeArchived: "/api/worktrees/:name/archive",
|
|
11527
11535
|
syncWorktreePrs: "/api/worktrees/:name/sync-prs",
|
|
11528
11536
|
postWorktreeToLinear: "/api/worktrees/:name/linear/post",
|
|
@@ -11531,6 +11539,7 @@ var apiPaths = {
|
|
|
11531
11539
|
mergeWorktree: "/api/worktrees/:name/merge",
|
|
11532
11540
|
fetchWorktreeDiff: "/api/worktrees/:name/diff",
|
|
11533
11541
|
fetchLinearIssues: "/api/linear/issues",
|
|
11542
|
+
fetchAutoNameConfig: "/api/project/auto-name",
|
|
11534
11543
|
setLinearAutoCreate: "/api/linear/auto-create",
|
|
11535
11544
|
setAutoRemoveOnMerge: "/api/github/auto-remove-on-merge",
|
|
11536
11545
|
pullMain: "/api/pull-main",
|
|
@@ -11721,6 +11730,16 @@ var apiContract = c.router({
|
|
|
11721
11730
|
...commonErrorResponses
|
|
11722
11731
|
}
|
|
11723
11732
|
},
|
|
11733
|
+
refreshWorktreeAgentTerminal: {
|
|
11734
|
+
method: "POST",
|
|
11735
|
+
path: apiPaths.refreshWorktreeAgentTerminal,
|
|
11736
|
+
pathParams: WorktreeNameParamsSchema,
|
|
11737
|
+
body: c.noBody(),
|
|
11738
|
+
responses: {
|
|
11739
|
+
200: OkResponseSchema,
|
|
11740
|
+
...commonErrorResponses
|
|
11741
|
+
}
|
|
11742
|
+
},
|
|
11724
11743
|
setWorktreeArchived: {
|
|
11725
11744
|
method: "PUT",
|
|
11726
11745
|
path: apiPaths.setWorktreeArchived,
|
|
@@ -11799,6 +11818,14 @@ var apiContract = c.router({
|
|
|
11799
11818
|
502: ErrorResponseSchema
|
|
11800
11819
|
}
|
|
11801
11820
|
},
|
|
11821
|
+
fetchAutoNameConfig: {
|
|
11822
|
+
method: "GET",
|
|
11823
|
+
path: apiPaths.fetchAutoNameConfig,
|
|
11824
|
+
responses: {
|
|
11825
|
+
200: AutoNameConfigResponseSchema,
|
|
11826
|
+
500: ErrorResponseSchema
|
|
11827
|
+
}
|
|
11828
|
+
},
|
|
11802
11829
|
setLinearAutoCreate: {
|
|
11803
11830
|
method: "PUT",
|
|
11804
11831
|
path: apiPaths.setLinearAutoCreate,
|
|
@@ -12193,10 +12220,237 @@ async function loadControlToken() {
|
|
|
12193
12220
|
return controlToken;
|
|
12194
12221
|
}
|
|
12195
12222
|
|
|
12223
|
+
// backend/src/adapters/fs.ts
|
|
12224
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
12225
|
+
import { join } from "path";
|
|
12226
|
+
|
|
12227
|
+
// backend/src/domain/model.ts
|
|
12228
|
+
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
12229
|
+
var WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
12230
|
+
|
|
12231
|
+
// backend/src/adapters/fs.ts
|
|
12232
|
+
var SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
|
|
12233
|
+
var DOTENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/;
|
|
12234
|
+
function stringifyAllocatedPorts(ports) {
|
|
12235
|
+
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
12236
|
+
return Object.fromEntries(entries);
|
|
12237
|
+
}
|
|
12238
|
+
function quoteEnvValue(value) {
|
|
12239
|
+
if (value.length > 0 && SAFE_ENV_VALUE_RE.test(value))
|
|
12240
|
+
return value;
|
|
12241
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
12242
|
+
}
|
|
12243
|
+
function parseDotenv(content) {
|
|
12244
|
+
const env = {};
|
|
12245
|
+
for (const line of content.split(`
|
|
12246
|
+
`)) {
|
|
12247
|
+
if (line.trimStart().startsWith("#"))
|
|
12248
|
+
continue;
|
|
12249
|
+
const match = DOTENV_LINE_RE.exec(line);
|
|
12250
|
+
if (!match)
|
|
12251
|
+
continue;
|
|
12252
|
+
const key = match[1];
|
|
12253
|
+
let value = match[2];
|
|
12254
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
12255
|
+
value = value.slice(1, -1);
|
|
12256
|
+
} else {
|
|
12257
|
+
value = value.trimEnd();
|
|
12258
|
+
}
|
|
12259
|
+
env[key] = value;
|
|
12260
|
+
}
|
|
12261
|
+
return env;
|
|
12262
|
+
}
|
|
12263
|
+
async function loadDotenvLocal(worktreePath) {
|
|
12264
|
+
try {
|
|
12265
|
+
const content = await Bun.file(join(worktreePath, ".env.local")).text();
|
|
12266
|
+
return parseDotenv(content);
|
|
12267
|
+
} catch {
|
|
12268
|
+
return {};
|
|
12269
|
+
}
|
|
12270
|
+
}
|
|
12271
|
+
function getWorktreeStoragePaths(gitDir) {
|
|
12272
|
+
const webmuxDir = join(gitDir, "webmux");
|
|
12273
|
+
return {
|
|
12274
|
+
gitDir,
|
|
12275
|
+
webmuxDir,
|
|
12276
|
+
metaPath: join(webmuxDir, "meta.json"),
|
|
12277
|
+
runtimeEnvPath: join(webmuxDir, "runtime.env"),
|
|
12278
|
+
controlEnvPath: join(webmuxDir, "control.env"),
|
|
12279
|
+
prsPath: join(webmuxDir, "prs.json")
|
|
12280
|
+
};
|
|
12281
|
+
}
|
|
12282
|
+
function getProjectArchiveStatePath(gitDir) {
|
|
12283
|
+
return join(gitDir, "webmux", "archive.json");
|
|
12284
|
+
}
|
|
12285
|
+
async function ensureWorktreeStorageDirs(gitDir) {
|
|
12286
|
+
const paths = getWorktreeStoragePaths(gitDir);
|
|
12287
|
+
await mkdir2(paths.webmuxDir, { recursive: true });
|
|
12288
|
+
return paths;
|
|
12289
|
+
}
|
|
12290
|
+
async function readWorktreeMeta(gitDir) {
|
|
12291
|
+
const { metaPath } = getWorktreeStoragePaths(gitDir);
|
|
12292
|
+
try {
|
|
12293
|
+
const raw = await Bun.file(metaPath).json();
|
|
12294
|
+
return normalizeWorktreeMeta(raw);
|
|
12295
|
+
} catch {
|
|
12296
|
+
return null;
|
|
12297
|
+
}
|
|
12298
|
+
}
|
|
12299
|
+
async function writeWorktreeMeta(gitDir, meta) {
|
|
12300
|
+
const { metaPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12301
|
+
await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
|
|
12302
|
+
`);
|
|
12303
|
+
}
|
|
12304
|
+
function isArchivedWorktreeEntry(raw) {
|
|
12305
|
+
return isRecord(raw) && typeof raw.path === "string" && typeof raw.archivedAt === "string";
|
|
12306
|
+
}
|
|
12307
|
+
function emptyWorktreeArchiveState() {
|
|
12308
|
+
return {
|
|
12309
|
+
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
12310
|
+
entries: []
|
|
12311
|
+
};
|
|
12312
|
+
}
|
|
12313
|
+
function isWorktreeArchiveState(raw) {
|
|
12314
|
+
return isRecord(raw) && typeof raw.schemaVersion === "number" && Array.isArray(raw.entries) && raw.entries.every((entry) => isArchivedWorktreeEntry(entry));
|
|
12315
|
+
}
|
|
12316
|
+
async function readWorktreeArchiveState(gitDir) {
|
|
12317
|
+
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
12318
|
+
try {
|
|
12319
|
+
const raw = await Bun.file(archivePath).json();
|
|
12320
|
+
return isWorktreeArchiveState(raw) ? {
|
|
12321
|
+
schemaVersion: raw.schemaVersion,
|
|
12322
|
+
entries: raw.entries.map((entry) => ({ ...entry }))
|
|
12323
|
+
} : emptyWorktreeArchiveState();
|
|
12324
|
+
} catch {
|
|
12325
|
+
return emptyWorktreeArchiveState();
|
|
12326
|
+
}
|
|
12327
|
+
}
|
|
12328
|
+
async function writeWorktreeArchiveState(gitDir, state) {
|
|
12329
|
+
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
12330
|
+
await ensureWorktreeStorageDirs(gitDir);
|
|
12331
|
+
await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
|
|
12332
|
+
`);
|
|
12333
|
+
}
|
|
12334
|
+
function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
|
|
12335
|
+
return {
|
|
12336
|
+
...dotenvValues,
|
|
12337
|
+
...meta.startupEnvValues,
|
|
12338
|
+
...stringifyAllocatedPorts(meta.allocatedPorts),
|
|
12339
|
+
...extraEnv,
|
|
12340
|
+
WEBMUX_WORKTREE_ID: meta.worktreeId,
|
|
12341
|
+
WEBMUX_BRANCH: meta.branch,
|
|
12342
|
+
WEBMUX_PROFILE: meta.profile,
|
|
12343
|
+
WEBMUX_AGENT: meta.agent,
|
|
12344
|
+
WEBMUX_RUNTIME: meta.runtime
|
|
12345
|
+
};
|
|
12346
|
+
}
|
|
12347
|
+
function buildControlEnvMap(input) {
|
|
12348
|
+
return {
|
|
12349
|
+
WEBMUX_CONTROL_URL: input.controlUrl,
|
|
12350
|
+
WEBMUX_CONTROL_TOKEN: input.controlToken,
|
|
12351
|
+
WEBMUX_WORKTREE_ID: input.worktreeId,
|
|
12352
|
+
WEBMUX_BRANCH: input.branch
|
|
12353
|
+
};
|
|
12354
|
+
}
|
|
12355
|
+
function renderEnvFile(env) {
|
|
12356
|
+
const lines = Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${quoteEnvValue(value)}`);
|
|
12357
|
+
return lines.join(`
|
|
12358
|
+
`) + `
|
|
12359
|
+
`;
|
|
12360
|
+
}
|
|
12361
|
+
async function writeRuntimeEnv(gitDir, env) {
|
|
12362
|
+
const { runtimeEnvPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12363
|
+
await Bun.write(runtimeEnvPath, renderEnvFile(env));
|
|
12364
|
+
}
|
|
12365
|
+
async function writeControlEnv(gitDir, env) {
|
|
12366
|
+
const { controlEnvPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12367
|
+
await Bun.write(controlEnvPath, renderEnvFile(env));
|
|
12368
|
+
}
|
|
12369
|
+
function isRecord(raw) {
|
|
12370
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
12371
|
+
}
|
|
12372
|
+
function normalizeConversationMeta(raw) {
|
|
12373
|
+
if (!raw)
|
|
12374
|
+
return raw;
|
|
12375
|
+
if (raw.provider === "codexAppServer") {
|
|
12376
|
+
const conversationId2 = raw.conversationId || raw.threadId;
|
|
12377
|
+
const threadId = raw.threadId || raw.conversationId;
|
|
12378
|
+
if (!conversationId2 || !threadId)
|
|
12379
|
+
return;
|
|
12380
|
+
const normalized2 = {
|
|
12381
|
+
provider: "codexAppServer",
|
|
12382
|
+
conversationId: conversationId2,
|
|
12383
|
+
threadId,
|
|
12384
|
+
cwd: raw.cwd,
|
|
12385
|
+
lastSeenAt: raw.lastSeenAt
|
|
12386
|
+
};
|
|
12387
|
+
return normalized2;
|
|
12388
|
+
}
|
|
12389
|
+
const conversationId = raw.conversationId || raw.sessionId;
|
|
12390
|
+
const sessionId = raw.sessionId || raw.conversationId;
|
|
12391
|
+
if (!conversationId || !sessionId)
|
|
12392
|
+
return;
|
|
12393
|
+
const normalized = {
|
|
12394
|
+
provider: "claudeCode",
|
|
12395
|
+
conversationId,
|
|
12396
|
+
sessionId,
|
|
12397
|
+
cwd: raw.cwd,
|
|
12398
|
+
lastSeenAt: raw.lastSeenAt
|
|
12399
|
+
};
|
|
12400
|
+
return normalized;
|
|
12401
|
+
}
|
|
12402
|
+
function normalizeOptionalString(raw) {
|
|
12403
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
12404
|
+
}
|
|
12405
|
+
function normalizeWorktreeMeta(meta) {
|
|
12406
|
+
const conversation = normalizeConversationMeta(meta.conversation);
|
|
12407
|
+
const normalizedLabel = normalizeOptionalString(meta.label);
|
|
12408
|
+
if (conversation === meta.conversation && normalizedLabel === meta.label) {
|
|
12409
|
+
return meta;
|
|
12410
|
+
}
|
|
12411
|
+
const rest = { ...meta };
|
|
12412
|
+
delete rest.label;
|
|
12413
|
+
delete rest.conversation;
|
|
12414
|
+
return {
|
|
12415
|
+
...rest,
|
|
12416
|
+
...normalizedLabel ? { label: normalizedLabel } : {},
|
|
12417
|
+
...conversation !== undefined ? { conversation } : {}
|
|
12418
|
+
};
|
|
12419
|
+
}
|
|
12420
|
+
function isPrComment(raw) {
|
|
12421
|
+
if (!isRecord(raw))
|
|
12422
|
+
return false;
|
|
12423
|
+
return (raw.type === "comment" || raw.type === "inline") && typeof raw.author === "string" && typeof raw.body === "string" && typeof raw.createdAt === "string" && (raw.path === undefined || typeof raw.path === "string") && (raw.line === undefined || raw.line === null || typeof raw.line === "number") && (raw.diffHunk === undefined || typeof raw.diffHunk === "string") && (raw.isReply === undefined || typeof raw.isReply === "boolean");
|
|
12424
|
+
}
|
|
12425
|
+
function isCiCheck(raw) {
|
|
12426
|
+
if (!isRecord(raw))
|
|
12427
|
+
return false;
|
|
12428
|
+
return typeof raw.name === "string" && (raw.status === "pending" || raw.status === "success" || raw.status === "failed" || raw.status === "skipped") && (raw.url === null || typeof raw.url === "string") && (raw.runId === null || typeof raw.runId === "number");
|
|
12429
|
+
}
|
|
12430
|
+
function isPrEntry(raw) {
|
|
12431
|
+
if (!isRecord(raw))
|
|
12432
|
+
return false;
|
|
12433
|
+
return typeof raw.repo === "string" && typeof raw.number === "number" && (raw.state === "open" || raw.state === "closed" || raw.state === "merged") && typeof raw.url === "string" && typeof raw.updatedAt === "string" && (raw.ciStatus === "none" || raw.ciStatus === "pending" || raw.ciStatus === "success" || raw.ciStatus === "failed") && Array.isArray(raw.ciChecks) && raw.ciChecks.every((check) => isCiCheck(check)) && Array.isArray(raw.comments) && raw.comments.every((comment) => isPrComment(comment));
|
|
12434
|
+
}
|
|
12435
|
+
async function readWorktreePrs(gitDir) {
|
|
12436
|
+
const { prsPath } = getWorktreeStoragePaths(gitDir);
|
|
12437
|
+
try {
|
|
12438
|
+
const raw = await Bun.file(prsPath).json();
|
|
12439
|
+
return Array.isArray(raw) && raw.every((entry) => isPrEntry(entry)) ? raw : [];
|
|
12440
|
+
} catch {
|
|
12441
|
+
return [];
|
|
12442
|
+
}
|
|
12443
|
+
}
|
|
12444
|
+
async function writeWorktreePrs(gitDir, prs) {
|
|
12445
|
+
const { prsPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12446
|
+
await Bun.write(prsPath, JSON.stringify(prs, null, 2) + `
|
|
12447
|
+
`);
|
|
12448
|
+
}
|
|
12449
|
+
|
|
12196
12450
|
// backend/src/adapters/claude-cli.ts
|
|
12197
12451
|
import { readdir, stat } from "fs/promises";
|
|
12198
|
-
import { basename, join } from "path";
|
|
12199
|
-
function
|
|
12452
|
+
import { basename, join as join2 } from "path";
|
|
12453
|
+
function isRecord2(raw) {
|
|
12200
12454
|
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
12201
12455
|
}
|
|
12202
12456
|
function readString(raw) {
|
|
@@ -12221,7 +12475,7 @@ function extractToolResultText(content) {
|
|
|
12221
12475
|
if (!Array.isArray(content))
|
|
12222
12476
|
return truncate(compactJson(content));
|
|
12223
12477
|
const text = content.map((entry) => {
|
|
12224
|
-
if (!
|
|
12478
|
+
if (!isRecord2(entry))
|
|
12225
12479
|
return "";
|
|
12226
12480
|
if (entry.type === "text" && typeof entry.text === "string")
|
|
12227
12481
|
return entry.text;
|
|
@@ -12230,17 +12484,17 @@ function extractToolResultText(content) {
|
|
|
12230
12484
|
return truncate(text);
|
|
12231
12485
|
}
|
|
12232
12486
|
function isTopLevelClaudeUserPrompt(raw) {
|
|
12233
|
-
if (raw.type !== "user" || !
|
|
12487
|
+
if (raw.type !== "user" || !isRecord2(raw.message))
|
|
12234
12488
|
return false;
|
|
12235
12489
|
return raw.message.role === "user" && typeof raw.message.content === "string" && typeof raw.uuid === "string" && raw.message.content.trim().length > 0;
|
|
12236
12490
|
}
|
|
12237
12491
|
function isClaudeUserToolResultRecord(raw) {
|
|
12238
|
-
if (raw.type !== "user" || !
|
|
12492
|
+
if (raw.type !== "user" || !isRecord2(raw.message))
|
|
12239
12493
|
return false;
|
|
12240
12494
|
return raw.message.role === "user" && Array.isArray(raw.message.content) && typeof raw.uuid === "string";
|
|
12241
12495
|
}
|
|
12242
12496
|
function isClaudeAssistantRecord(raw) {
|
|
12243
|
-
if (raw.type !== "assistant" || !
|
|
12497
|
+
if (raw.type !== "assistant" || !isRecord2(raw.message))
|
|
12244
12498
|
return false;
|
|
12245
12499
|
return raw.message.role === "assistant" && typeof raw.uuid === "string";
|
|
12246
12500
|
}
|
|
@@ -12252,19 +12506,19 @@ function readClaudeProjectsRoot() {
|
|
|
12252
12506
|
if (!home) {
|
|
12253
12507
|
throw new Error("HOME is required to resolve Claude sessions");
|
|
12254
12508
|
}
|
|
12255
|
-
return
|
|
12509
|
+
return join2(home, ".claude", "projects");
|
|
12256
12510
|
}
|
|
12257
12511
|
async function listJsonlFiles(dir) {
|
|
12258
12512
|
try {
|
|
12259
12513
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
12260
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) =>
|
|
12514
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join2(dir, entry.name));
|
|
12261
12515
|
} catch {
|
|
12262
12516
|
return [];
|
|
12263
12517
|
}
|
|
12264
12518
|
}
|
|
12265
12519
|
async function findClaudeSessionPath(sessionId, cwd) {
|
|
12266
12520
|
const projectsRoot = readClaudeProjectsRoot();
|
|
12267
|
-
const primaryPath =
|
|
12521
|
+
const primaryPath = join2(projectsRoot, encodeClaudeProjectDir(cwd), `${sessionId}.jsonl`);
|
|
12268
12522
|
try {
|
|
12269
12523
|
await stat(primaryPath);
|
|
12270
12524
|
return primaryPath;
|
|
@@ -12273,7 +12527,7 @@ async function findClaudeSessionPath(sessionId, cwd) {
|
|
|
12273
12527
|
for (const entry of projectDirs) {
|
|
12274
12528
|
if (!entry.isDirectory())
|
|
12275
12529
|
continue;
|
|
12276
|
-
const candidate =
|
|
12530
|
+
const candidate = join2(projectsRoot, entry.name, `${sessionId}.jsonl`);
|
|
12277
12531
|
try {
|
|
12278
12532
|
await stat(candidate);
|
|
12279
12533
|
return candidate;
|
|
@@ -12331,7 +12585,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12331
12585
|
continue;
|
|
12332
12586
|
if (isClaudeUserToolResultRecord(record)) {
|
|
12333
12587
|
for (const entry of record.message.content) {
|
|
12334
|
-
if (!
|
|
12588
|
+
if (!isRecord2(entry) || entry.type !== "tool_result")
|
|
12335
12589
|
continue;
|
|
12336
12590
|
const text = extractToolResultText(entry.content);
|
|
12337
12591
|
if (text.length === 0)
|
|
@@ -12352,7 +12606,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12352
12606
|
if (!Array.isArray(record.message.content))
|
|
12353
12607
|
continue;
|
|
12354
12608
|
for (const block of record.message.content) {
|
|
12355
|
-
if (!
|
|
12609
|
+
if (!isRecord2(block))
|
|
12356
12610
|
continue;
|
|
12357
12611
|
if (block.type === "text" && typeof block.text === "string") {
|
|
12358
12612
|
const text = block.text.trim();
|
|
@@ -12398,7 +12652,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12398
12652
|
class ClaudeCliClient {
|
|
12399
12653
|
async listSessions(cwd) {
|
|
12400
12654
|
const projectsRoot = readClaudeProjectsRoot();
|
|
12401
|
-
const primaryDir =
|
|
12655
|
+
const primaryDir = join2(projectsRoot, encodeClaudeProjectDir(cwd));
|
|
12402
12656
|
const primaryFiles = await listJsonlFiles(primaryDir);
|
|
12403
12657
|
if (primaryFiles.length > 0) {
|
|
12404
12658
|
return await this.summarizeSessionFiles(primaryFiles, cwd);
|
|
@@ -12408,7 +12662,7 @@ class ClaudeCliClient {
|
|
|
12408
12662
|
for (const entry of projectDirs) {
|
|
12409
12663
|
if (!entry.isDirectory())
|
|
12410
12664
|
continue;
|
|
12411
|
-
const files = await listJsonlFiles(
|
|
12665
|
+
const files = await listJsonlFiles(join2(projectsRoot, entry.name));
|
|
12412
12666
|
for (const filePath of files) {
|
|
12413
12667
|
const session = await this.readSessionFile(filePath);
|
|
12414
12668
|
if (session?.cwd === cwd) {
|
|
@@ -12547,15 +12801,15 @@ class ClaudeCliClient {
|
|
|
12547
12801
|
log.warn(`[agents] failed to parse Claude stream line: ${line.slice(0, 120)}`);
|
|
12548
12802
|
return;
|
|
12549
12803
|
}
|
|
12550
|
-
if (!
|
|
12804
|
+
if (!isRecord2(parsed))
|
|
12551
12805
|
return;
|
|
12552
12806
|
const sessionId = readString(parsed.session_id);
|
|
12553
12807
|
if (sessionId) {
|
|
12554
12808
|
resolveSessionId(sessionId);
|
|
12555
12809
|
}
|
|
12556
|
-
if (parsed.type === "stream_event" &&
|
|
12810
|
+
if (parsed.type === "stream_event" && isRecord2(parsed.event)) {
|
|
12557
12811
|
const event = parsed.event;
|
|
12558
|
-
if (event.type === "content_block_delta" &&
|
|
12812
|
+
if (event.type === "content_block_delta" && isRecord2(event.delta) && event.delta.type === "text_delta") {
|
|
12559
12813
|
const delta = readString(event.delta.text);
|
|
12560
12814
|
if (delta) {
|
|
12561
12815
|
callbacks.onAssistantDelta?.(delta);
|
|
@@ -12581,7 +12835,7 @@ class ClaudeCliClient {
|
|
|
12581
12835
|
}
|
|
12582
12836
|
|
|
12583
12837
|
// backend/src/lib/type-guards.ts
|
|
12584
|
-
function
|
|
12838
|
+
function isRecord3(raw) {
|
|
12585
12839
|
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
12586
12840
|
}
|
|
12587
12841
|
function isStringArray(raw) {
|
|
@@ -12603,16 +12857,11 @@ var CodexAppServerUserMessageItemSchema = exports_external.object({
|
|
|
12603
12857
|
var CodexAppServerAgentMessageItemSchema = exports_external.object({
|
|
12604
12858
|
type: exports_external.literal("agentMessage"),
|
|
12605
12859
|
id: exports_external.string(),
|
|
12606
|
-
text: exports_external.string(),
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
id: value.id,
|
|
12612
|
-
text: value.text,
|
|
12613
|
-
phase: value.phase,
|
|
12614
|
-
memoryCitation: value.memoryCitation
|
|
12615
|
-
}));
|
|
12860
|
+
text: exports_external.string().optional(),
|
|
12861
|
+
message: exports_external.string().optional(),
|
|
12862
|
+
phase: exports_external.string().optional(),
|
|
12863
|
+
memoryCitation: UnknownValueSchema.optional()
|
|
12864
|
+
});
|
|
12616
12865
|
var CodexAppServerGenericItemSchema = exports_external.object({
|
|
12617
12866
|
type: exports_external.string(),
|
|
12618
12867
|
id: exports_external.string()
|
|
@@ -12864,7 +13113,7 @@ class CodexAppServerClient {
|
|
|
12864
13113
|
log.error(`[agents] failed to parse codex app-server line: ${line}`, error);
|
|
12865
13114
|
return;
|
|
12866
13115
|
}
|
|
12867
|
-
if (!
|
|
13116
|
+
if (!isRecord3(parsed)) {
|
|
12868
13117
|
log.warn(`[agents] unexpected codex app-server payload: ${line}`);
|
|
12869
13118
|
return;
|
|
12870
13119
|
}
|
|
@@ -12952,7 +13201,7 @@ class CodexAppServerClient {
|
|
|
12952
13201
|
this.readyPromise = null;
|
|
12953
13202
|
}
|
|
12954
13203
|
readResponseError(raw) {
|
|
12955
|
-
if (!
|
|
13204
|
+
if (!isRecord3(raw.error))
|
|
12956
13205
|
return null;
|
|
12957
13206
|
return typeof raw.error.code === "number" && typeof raw.error.message === "string" ? {
|
|
12958
13207
|
code: raw.error.code,
|
|
@@ -12964,7 +13213,7 @@ class CodexAppServerClient {
|
|
|
12964
13213
|
|
|
12965
13214
|
// backend/src/adapters/config.ts
|
|
12966
13215
|
import { readFileSync } from "fs";
|
|
12967
|
-
import { dirname as dirname2, join as
|
|
13216
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
12968
13217
|
|
|
12969
13218
|
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
|
|
12970
13219
|
var composer = require_composer();
|
|
@@ -13072,7 +13321,7 @@ function cloneProfiles(profiles) {
|
|
|
13072
13321
|
function defaultProfiles() {
|
|
13073
13322
|
return { default: cloneProfile(DEFAULT_CONFIG.profiles.default) };
|
|
13074
13323
|
}
|
|
13075
|
-
function
|
|
13324
|
+
function isRecord4(value) {
|
|
13076
13325
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13077
13326
|
}
|
|
13078
13327
|
function isStringArray2(value) {
|
|
@@ -13088,7 +13337,7 @@ function parsePanes(raw) {
|
|
|
13088
13337
|
return panes.length > 0 ? panes : clonePanes(DEFAULT_PANES);
|
|
13089
13338
|
}
|
|
13090
13339
|
function parsePane(raw, index) {
|
|
13091
|
-
if (!
|
|
13340
|
+
if (!isRecord4(raw))
|
|
13092
13341
|
return null;
|
|
13093
13342
|
if (raw.kind !== "agent" && raw.kind !== "shell" && raw.kind !== "command")
|
|
13094
13343
|
return null;
|
|
@@ -13117,7 +13366,7 @@ function parsePane(raw, index) {
|
|
|
13117
13366
|
function parseMounts(raw) {
|
|
13118
13367
|
if (!Array.isArray(raw))
|
|
13119
13368
|
return;
|
|
13120
|
-
const mounts = raw.filter(
|
|
13369
|
+
const mounts = raw.filter(isRecord4).filter((entry) => typeof entry.hostPath === "string" && entry.hostPath.length > 0).map((entry) => ({
|
|
13121
13370
|
hostPath: entry.hostPath,
|
|
13122
13371
|
...typeof entry.guestPath === "string" && entry.guestPath.length > 0 ? { guestPath: entry.guestPath } : {},
|
|
13123
13372
|
...typeof entry.writable === "boolean" ? { writable: entry.writable } : {}
|
|
@@ -13125,7 +13374,7 @@ function parseMounts(raw) {
|
|
|
13125
13374
|
return mounts.length > 0 ? mounts : undefined;
|
|
13126
13375
|
}
|
|
13127
13376
|
function parseProfile(raw, fallbackRuntime) {
|
|
13128
|
-
if (!
|
|
13377
|
+
if (!isRecord4(raw)) {
|
|
13129
13378
|
return {
|
|
13130
13379
|
runtime: fallbackRuntime,
|
|
13131
13380
|
envPassthrough: [],
|
|
@@ -13148,7 +13397,7 @@ function parseProfile(raw, fallbackRuntime) {
|
|
|
13148
13397
|
};
|
|
13149
13398
|
}
|
|
13150
13399
|
function parseProfiles(raw, includeDefaultProfile) {
|
|
13151
|
-
if (!
|
|
13400
|
+
if (!isRecord4(raw))
|
|
13152
13401
|
return includeDefaultProfile ? defaultProfiles() : {};
|
|
13153
13402
|
const profiles = Object.entries(raw).reduce((acc, [name, value]) => {
|
|
13154
13403
|
const fallbackRuntime = name === "sandbox" ? "docker" : "host";
|
|
@@ -13167,7 +13416,7 @@ function cloneAgents(agents) {
|
|
|
13167
13416
|
return Object.fromEntries(Object.entries(agents).map(([id, agent]) => [id, cloneAgentConfig(agent)]));
|
|
13168
13417
|
}
|
|
13169
13418
|
function parseCustomAgent(raw) {
|
|
13170
|
-
if (!
|
|
13419
|
+
if (!isRecord4(raw))
|
|
13171
13420
|
return null;
|
|
13172
13421
|
if (typeof raw.label !== "string" || !raw.label.trim())
|
|
13173
13422
|
return null;
|
|
@@ -13180,7 +13429,7 @@ function parseCustomAgent(raw) {
|
|
|
13180
13429
|
};
|
|
13181
13430
|
}
|
|
13182
13431
|
function parseCustomAgents(raw) {
|
|
13183
|
-
if (!
|
|
13432
|
+
if (!isRecord4(raw))
|
|
13184
13433
|
return {};
|
|
13185
13434
|
return Object.entries(raw).reduce((acc, [id, value]) => {
|
|
13186
13435
|
if (!id.trim())
|
|
@@ -13195,7 +13444,7 @@ function parseCustomAgents(raw) {
|
|
|
13195
13444
|
function parseServices(raw) {
|
|
13196
13445
|
if (!Array.isArray(raw))
|
|
13197
13446
|
return [];
|
|
13198
|
-
return raw.filter(
|
|
13447
|
+
return raw.filter(isRecord4).filter((entry) => typeof entry.name === "string" && typeof entry.portEnv === "string").map((entry) => ({
|
|
13199
13448
|
name: entry.name,
|
|
13200
13449
|
portEnv: entry.portEnv,
|
|
13201
13450
|
...typeof entry.portStart === "number" && Number.isFinite(entry.portStart) ? { portStart: entry.portStart } : {},
|
|
@@ -13204,7 +13453,7 @@ function parseServices(raw) {
|
|
|
13204
13453
|
}));
|
|
13205
13454
|
}
|
|
13206
13455
|
function parseStartupEnvs(raw) {
|
|
13207
|
-
if (!
|
|
13456
|
+
if (!isRecord4(raw))
|
|
13208
13457
|
return {};
|
|
13209
13458
|
const startupEnvs = {};
|
|
13210
13459
|
for (const [key, value] of Object.entries(raw)) {
|
|
@@ -13217,7 +13466,7 @@ function parseStartupEnvs(raw) {
|
|
|
13217
13466
|
return startupEnvs;
|
|
13218
13467
|
}
|
|
13219
13468
|
function parseLifecycleHooks(raw) {
|
|
13220
|
-
if (!
|
|
13469
|
+
if (!isRecord4(raw))
|
|
13221
13470
|
return {};
|
|
13222
13471
|
const hooks = {};
|
|
13223
13472
|
if (typeof raw.postCreate === "string" && raw.postCreate.trim()) {
|
|
@@ -13229,13 +13478,13 @@ function parseLifecycleHooks(raw) {
|
|
|
13229
13478
|
return hooks;
|
|
13230
13479
|
}
|
|
13231
13480
|
function parseOneshot(raw) {
|
|
13232
|
-
if (!
|
|
13481
|
+
if (!isRecord4(raw))
|
|
13233
13482
|
return { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() };
|
|
13234
13483
|
const systemPrompt = typeof raw.systemPrompt === "string" && raw.systemPrompt.trim() ? raw.systemPrompt.trim() : DEFAULT_ONESHOT_SYSTEM_PROMPT();
|
|
13235
13484
|
return { systemPrompt };
|
|
13236
13485
|
}
|
|
13237
13486
|
function parseAutoName(raw) {
|
|
13238
|
-
if (!
|
|
13487
|
+
if (!isRecord4(raw))
|
|
13239
13488
|
return null;
|
|
13240
13489
|
const provider = raw.provider;
|
|
13241
13490
|
if (provider !== "claude" && provider !== "codex")
|
|
@@ -13247,7 +13496,7 @@ function parseAutoName(raw) {
|
|
|
13247
13496
|
};
|
|
13248
13497
|
}
|
|
13249
13498
|
function parseAutoPull(raw) {
|
|
13250
|
-
if (!
|
|
13499
|
+
if (!isRecord4(raw))
|
|
13251
13500
|
return DEFAULT_CONFIG.workspace.autoPull;
|
|
13252
13501
|
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : false;
|
|
13253
13502
|
const interval = typeof raw.intervalSeconds === "number" && Number.isFinite(raw.intervalSeconds) && raw.intervalSeconds >= 30 ? raw.intervalSeconds : 300;
|
|
@@ -13256,7 +13505,7 @@ function parseAutoPull(raw) {
|
|
|
13256
13505
|
function parseLinkedRepos(raw) {
|
|
13257
13506
|
if (!Array.isArray(raw))
|
|
13258
13507
|
return [];
|
|
13259
|
-
return raw.filter(
|
|
13508
|
+
return raw.filter(isRecord4).filter((entry) => typeof entry.repo === "string").map((entry) => ({
|
|
13260
13509
|
repo: entry.repo,
|
|
13261
13510
|
alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo",
|
|
13262
13511
|
...typeof entry.dir === "string" && entry.dir.trim() ? { dir: entry.dir.trim() } : {}
|
|
@@ -13271,23 +13520,23 @@ function getDefaultProfileName(config) {
|
|
|
13271
13520
|
return Object.keys(config.profiles)[0] ?? "default";
|
|
13272
13521
|
}
|
|
13273
13522
|
function readConfigFile(root) {
|
|
13274
|
-
return readFileSync(
|
|
13523
|
+
return readFileSync(join3(root, ".webmux.yaml"), "utf8");
|
|
13275
13524
|
}
|
|
13276
13525
|
function readLocalConfigFile(root) {
|
|
13277
|
-
return readFileSync(
|
|
13526
|
+
return readFileSync(join3(root, ".webmux.local.yaml"), "utf8");
|
|
13278
13527
|
}
|
|
13279
13528
|
function parseConfigDocument(text) {
|
|
13280
13529
|
const parsed = $parse(text);
|
|
13281
|
-
return
|
|
13530
|
+
return isRecord4(parsed) ? parsed : {};
|
|
13282
13531
|
}
|
|
13283
13532
|
function parseProjectConfig(parsed) {
|
|
13284
13533
|
return {
|
|
13285
13534
|
name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : DEFAULT_CONFIG.name,
|
|
13286
13535
|
workspace: {
|
|
13287
|
-
mainBranch:
|
|
13288
|
-
worktreeRoot:
|
|
13289
|
-
defaultAgent:
|
|
13290
|
-
autoPull:
|
|
13536
|
+
mainBranch: isRecord4(parsed.workspace) && typeof parsed.workspace.mainBranch === "string" ? parsed.workspace.mainBranch : DEFAULT_CONFIG.workspace.mainBranch,
|
|
13537
|
+
worktreeRoot: isRecord4(parsed.workspace) && typeof parsed.workspace.worktreeRoot === "string" ? parsed.workspace.worktreeRoot : DEFAULT_CONFIG.workspace.worktreeRoot,
|
|
13538
|
+
defaultAgent: isRecord4(parsed.workspace) ? parseAgentKind(parsed.workspace.defaultAgent) : DEFAULT_CONFIG.workspace.defaultAgent,
|
|
13539
|
+
autoPull: isRecord4(parsed.workspace) ? parseAutoPull(parsed.workspace.autoPull) : DEFAULT_CONFIG.workspace.autoPull
|
|
13291
13540
|
},
|
|
13292
13541
|
profiles: parseProfiles(parsed.profiles, true),
|
|
13293
13542
|
agents: {},
|
|
@@ -13295,8 +13544,8 @@ function parseProjectConfig(parsed) {
|
|
|
13295
13544
|
startupEnvs: parseStartupEnvs(parsed.startupEnvs),
|
|
13296
13545
|
integrations: {
|
|
13297
13546
|
github: {
|
|
13298
|
-
linkedRepos:
|
|
13299
|
-
autoRemoveOnMerge:
|
|
13547
|
+
linkedRepos: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord4(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : [],
|
|
13548
|
+
autoRemoveOnMerge: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) && typeof parsed.integrations.github.autoRemoveOnMerge === "boolean" ? parsed.integrations.github.autoRemoveOnMerge : DEFAULT_CONFIG.integrations.github.autoRemoveOnMerge
|
|
13300
13549
|
},
|
|
13301
13550
|
linear: parseLinearIntegration(parsed)
|
|
13302
13551
|
},
|
|
@@ -13317,7 +13566,7 @@ function parseTeamKeyList(raw) {
|
|
|
13317
13566
|
var warnedLegacyLinearTeamId = false;
|
|
13318
13567
|
function parseLinearIntegration(parsed) {
|
|
13319
13568
|
const defaults = DEFAULT_CONFIG.integrations.linear;
|
|
13320
|
-
const linear =
|
|
13569
|
+
const linear = isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) ? parsed.integrations.linear : null;
|
|
13321
13570
|
if (!linear)
|
|
13322
13571
|
return { ...defaults };
|
|
13323
13572
|
if (typeof linear.teamId === "string" && !warnedLegacyLinearTeamId) {
|
|
@@ -13333,10 +13582,10 @@ function parseLinearIntegration(parsed) {
|
|
|
13333
13582
|
};
|
|
13334
13583
|
}
|
|
13335
13584
|
function parseLocalLinearOverlay(parsed) {
|
|
13336
|
-
if (!
|
|
13585
|
+
if (!isRecord4(parsed.integrations))
|
|
13337
13586
|
return null;
|
|
13338
13587
|
const linear = parsed.integrations.linear;
|
|
13339
|
-
if (!
|
|
13588
|
+
if (!isRecord4(linear))
|
|
13340
13589
|
return null;
|
|
13341
13590
|
const overlay = {};
|
|
13342
13591
|
if (typeof linear.enabled === "boolean")
|
|
@@ -13351,10 +13600,10 @@ function parseLocalLinearOverlay(parsed) {
|
|
|
13351
13600
|
return Object.keys(overlay).length > 0 ? overlay : null;
|
|
13352
13601
|
}
|
|
13353
13602
|
function parseLocalGitHubOverlay(parsed) {
|
|
13354
|
-
if (!
|
|
13603
|
+
if (!isRecord4(parsed.integrations))
|
|
13355
13604
|
return null;
|
|
13356
13605
|
const github = parsed.integrations.github;
|
|
13357
|
-
if (!
|
|
13606
|
+
if (!isRecord4(github))
|
|
13358
13607
|
return null;
|
|
13359
13608
|
const overlay = {};
|
|
13360
13609
|
if (typeof github.autoRemoveOnMerge === "boolean")
|
|
@@ -13362,10 +13611,10 @@ function parseLocalGitHubOverlay(parsed) {
|
|
|
13362
13611
|
return Object.keys(overlay).length > 0 ? overlay : null;
|
|
13363
13612
|
}
|
|
13364
13613
|
function parseLocalAutoPullOverlay(parsed) {
|
|
13365
|
-
if (!
|
|
13614
|
+
if (!isRecord4(parsed.workspace))
|
|
13366
13615
|
return null;
|
|
13367
13616
|
const autoPull = parsed.workspace.autoPull;
|
|
13368
|
-
if (!
|
|
13617
|
+
if (!isRecord4(autoPull))
|
|
13369
13618
|
return null;
|
|
13370
13619
|
const overlay = {};
|
|
13371
13620
|
if (typeof autoPull.enabled === "boolean")
|
|
@@ -13382,7 +13631,7 @@ function loadLocalProjectConfigOverlay(root) {
|
|
|
13382
13631
|
return { worktreeRoot: null, profiles: {}, agents: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
|
|
13383
13632
|
}
|
|
13384
13633
|
const parsed = parseConfigDocument(text);
|
|
13385
|
-
const ws =
|
|
13634
|
+
const ws = isRecord4(parsed.workspace) ? parsed.workspace : null;
|
|
13386
13635
|
return {
|
|
13387
13636
|
worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
|
|
13388
13637
|
profiles: parseProfiles(parsed.profiles, false),
|
|
@@ -13462,7 +13711,7 @@ function loadConfig(dir, options = {}) {
|
|
|
13462
13711
|
};
|
|
13463
13712
|
}
|
|
13464
13713
|
function readLocalConfigDocument(root) {
|
|
13465
|
-
const localPath =
|
|
13714
|
+
const localPath = join3(root, ".webmux.local.yaml");
|
|
13466
13715
|
let existing = {};
|
|
13467
13716
|
try {
|
|
13468
13717
|
const text = readFileSync(localPath, "utf8").trim();
|
|
@@ -13474,8 +13723,8 @@ function readLocalConfigDocument(root) {
|
|
|
13474
13723
|
async function persistLocalLinearConfig(dir, changes) {
|
|
13475
13724
|
const root = projectRoot(dir);
|
|
13476
13725
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13477
|
-
const integrations =
|
|
13478
|
-
const linear =
|
|
13726
|
+
const integrations = isRecord4(existing.integrations) ? { ...existing.integrations } : {};
|
|
13727
|
+
const linear = isRecord4(integrations.linear) ? { ...integrations.linear } : {};
|
|
13479
13728
|
Object.assign(linear, changes);
|
|
13480
13729
|
integrations.linear = linear;
|
|
13481
13730
|
existing.integrations = integrations;
|
|
@@ -13484,8 +13733,8 @@ async function persistLocalLinearConfig(dir, changes) {
|
|
|
13484
13733
|
async function persistLocalGitHubConfig(dir, changes) {
|
|
13485
13734
|
const root = projectRoot(dir);
|
|
13486
13735
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13487
|
-
const integrations =
|
|
13488
|
-
const github =
|
|
13736
|
+
const integrations = isRecord4(existing.integrations) ? { ...existing.integrations } : {};
|
|
13737
|
+
const github = isRecord4(integrations.github) ? { ...integrations.github } : {};
|
|
13489
13738
|
Object.assign(github, changes);
|
|
13490
13739
|
integrations.github = github;
|
|
13491
13740
|
existing.integrations = integrations;
|
|
@@ -13494,7 +13743,7 @@ async function persistLocalGitHubConfig(dir, changes) {
|
|
|
13494
13743
|
async function persistLocalCustomAgent(dir, agentId, agent) {
|
|
13495
13744
|
const root = projectRoot(dir);
|
|
13496
13745
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13497
|
-
const agents =
|
|
13746
|
+
const agents = isRecord4(existing.agents) ? { ...existing.agents } : {};
|
|
13498
13747
|
agents[agentId] = {
|
|
13499
13748
|
label: agent.label,
|
|
13500
13749
|
startCommand: agent.startCommand,
|
|
@@ -13506,7 +13755,7 @@ async function persistLocalCustomAgent(dir, agentId, agent) {
|
|
|
13506
13755
|
async function removeLocalCustomAgent(dir, agentId) {
|
|
13507
13756
|
const root = projectRoot(dir);
|
|
13508
13757
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13509
|
-
if (!
|
|
13758
|
+
if (!isRecord4(existing.agents) || !(agentId in existing.agents)) {
|
|
13510
13759
|
return;
|
|
13511
13760
|
}
|
|
13512
13761
|
const agents = { ...existing.agents };
|
|
@@ -13615,12 +13864,6 @@ function hasRecentDashboardActivity() {
|
|
|
13615
13864
|
|
|
13616
13865
|
// backend/src/services/archive-service.ts
|
|
13617
13866
|
import { resolve as resolve2 } from "path";
|
|
13618
|
-
|
|
13619
|
-
// backend/src/domain/model.ts
|
|
13620
|
-
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
13621
|
-
var WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
13622
|
-
|
|
13623
|
-
// backend/src/services/archive-service.ts
|
|
13624
13867
|
function createArchiveState(entries) {
|
|
13625
13868
|
return {
|
|
13626
13869
|
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
@@ -14300,6 +14543,14 @@ function findLinkedGitHubPr(issue) {
|
|
|
14300
14543
|
});
|
|
14301
14544
|
return indexed[0].pr;
|
|
14302
14545
|
}
|
|
14546
|
+
function buildLinearPickupMarkdown(input) {
|
|
14547
|
+
return [
|
|
14548
|
+
`**Webmux pickup \u2014 branch \`${input.branch}\`**`,
|
|
14549
|
+
"",
|
|
14550
|
+
`- Picked up: ${input.pickedUpAt.toISOString()}`
|
|
14551
|
+
].join(`
|
|
14552
|
+
`);
|
|
14553
|
+
}
|
|
14303
14554
|
function buildLinearSummaryMarkdown(input) {
|
|
14304
14555
|
const lines = [
|
|
14305
14556
|
`**Webmux session \u2014 branch \`${input.branch}\`**`,
|
|
@@ -14674,229 +14925,6 @@ import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
|
14674
14925
|
// backend/src/adapters/agent-runtime.ts
|
|
14675
14926
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
14676
14927
|
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
14677
|
-
|
|
14678
|
-
// backend/src/adapters/fs.ts
|
|
14679
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
14680
|
-
import { join as join3 } from "path";
|
|
14681
|
-
var SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
|
|
14682
|
-
var DOTENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/;
|
|
14683
|
-
function stringifyAllocatedPorts(ports) {
|
|
14684
|
-
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
14685
|
-
return Object.fromEntries(entries);
|
|
14686
|
-
}
|
|
14687
|
-
function quoteEnvValue(value) {
|
|
14688
|
-
if (value.length > 0 && SAFE_ENV_VALUE_RE.test(value))
|
|
14689
|
-
return value;
|
|
14690
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14691
|
-
}
|
|
14692
|
-
function parseDotenv(content) {
|
|
14693
|
-
const env = {};
|
|
14694
|
-
for (const line of content.split(`
|
|
14695
|
-
`)) {
|
|
14696
|
-
if (line.trimStart().startsWith("#"))
|
|
14697
|
-
continue;
|
|
14698
|
-
const match = DOTENV_LINE_RE.exec(line);
|
|
14699
|
-
if (!match)
|
|
14700
|
-
continue;
|
|
14701
|
-
const key = match[1];
|
|
14702
|
-
let value = match[2];
|
|
14703
|
-
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
14704
|
-
value = value.slice(1, -1);
|
|
14705
|
-
} else {
|
|
14706
|
-
value = value.trimEnd();
|
|
14707
|
-
}
|
|
14708
|
-
env[key] = value;
|
|
14709
|
-
}
|
|
14710
|
-
return env;
|
|
14711
|
-
}
|
|
14712
|
-
async function loadDotenvLocal(worktreePath) {
|
|
14713
|
-
try {
|
|
14714
|
-
const content = await Bun.file(join3(worktreePath, ".env.local")).text();
|
|
14715
|
-
return parseDotenv(content);
|
|
14716
|
-
} catch {
|
|
14717
|
-
return {};
|
|
14718
|
-
}
|
|
14719
|
-
}
|
|
14720
|
-
function getWorktreeStoragePaths(gitDir) {
|
|
14721
|
-
const webmuxDir = join3(gitDir, "webmux");
|
|
14722
|
-
return {
|
|
14723
|
-
gitDir,
|
|
14724
|
-
webmuxDir,
|
|
14725
|
-
metaPath: join3(webmuxDir, "meta.json"),
|
|
14726
|
-
runtimeEnvPath: join3(webmuxDir, "runtime.env"),
|
|
14727
|
-
controlEnvPath: join3(webmuxDir, "control.env"),
|
|
14728
|
-
prsPath: join3(webmuxDir, "prs.json")
|
|
14729
|
-
};
|
|
14730
|
-
}
|
|
14731
|
-
function getProjectArchiveStatePath(gitDir) {
|
|
14732
|
-
return join3(gitDir, "webmux", "archive.json");
|
|
14733
|
-
}
|
|
14734
|
-
async function ensureWorktreeStorageDirs(gitDir) {
|
|
14735
|
-
const paths = getWorktreeStoragePaths(gitDir);
|
|
14736
|
-
await mkdir2(paths.webmuxDir, { recursive: true });
|
|
14737
|
-
return paths;
|
|
14738
|
-
}
|
|
14739
|
-
async function readWorktreeMeta(gitDir) {
|
|
14740
|
-
const { metaPath } = getWorktreeStoragePaths(gitDir);
|
|
14741
|
-
try {
|
|
14742
|
-
const raw = await Bun.file(metaPath).json();
|
|
14743
|
-
return normalizeWorktreeMeta(raw);
|
|
14744
|
-
} catch {
|
|
14745
|
-
return null;
|
|
14746
|
-
}
|
|
14747
|
-
}
|
|
14748
|
-
async function writeWorktreeMeta(gitDir, meta) {
|
|
14749
|
-
const { metaPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
14750
|
-
await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
|
|
14751
|
-
`);
|
|
14752
|
-
}
|
|
14753
|
-
function isArchivedWorktreeEntry(raw) {
|
|
14754
|
-
return isRecord4(raw) && typeof raw.path === "string" && typeof raw.archivedAt === "string";
|
|
14755
|
-
}
|
|
14756
|
-
function emptyWorktreeArchiveState() {
|
|
14757
|
-
return {
|
|
14758
|
-
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
14759
|
-
entries: []
|
|
14760
|
-
};
|
|
14761
|
-
}
|
|
14762
|
-
function isWorktreeArchiveState(raw) {
|
|
14763
|
-
return isRecord4(raw) && typeof raw.schemaVersion === "number" && Array.isArray(raw.entries) && raw.entries.every((entry) => isArchivedWorktreeEntry(entry));
|
|
14764
|
-
}
|
|
14765
|
-
async function readWorktreeArchiveState(gitDir) {
|
|
14766
|
-
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
14767
|
-
try {
|
|
14768
|
-
const raw = await Bun.file(archivePath).json();
|
|
14769
|
-
return isWorktreeArchiveState(raw) ? {
|
|
14770
|
-
schemaVersion: raw.schemaVersion,
|
|
14771
|
-
entries: raw.entries.map((entry) => ({ ...entry }))
|
|
14772
|
-
} : emptyWorktreeArchiveState();
|
|
14773
|
-
} catch {
|
|
14774
|
-
return emptyWorktreeArchiveState();
|
|
14775
|
-
}
|
|
14776
|
-
}
|
|
14777
|
-
async function writeWorktreeArchiveState(gitDir, state) {
|
|
14778
|
-
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
14779
|
-
await ensureWorktreeStorageDirs(gitDir);
|
|
14780
|
-
await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
|
|
14781
|
-
`);
|
|
14782
|
-
}
|
|
14783
|
-
function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
|
|
14784
|
-
return {
|
|
14785
|
-
...dotenvValues,
|
|
14786
|
-
...meta.startupEnvValues,
|
|
14787
|
-
...stringifyAllocatedPorts(meta.allocatedPorts),
|
|
14788
|
-
...extraEnv,
|
|
14789
|
-
WEBMUX_WORKTREE_ID: meta.worktreeId,
|
|
14790
|
-
WEBMUX_BRANCH: meta.branch,
|
|
14791
|
-
WEBMUX_PROFILE: meta.profile,
|
|
14792
|
-
WEBMUX_AGENT: meta.agent,
|
|
14793
|
-
WEBMUX_RUNTIME: meta.runtime
|
|
14794
|
-
};
|
|
14795
|
-
}
|
|
14796
|
-
function buildControlEnvMap(input) {
|
|
14797
|
-
return {
|
|
14798
|
-
WEBMUX_CONTROL_URL: input.controlUrl,
|
|
14799
|
-
WEBMUX_CONTROL_TOKEN: input.controlToken,
|
|
14800
|
-
WEBMUX_WORKTREE_ID: input.worktreeId,
|
|
14801
|
-
WEBMUX_BRANCH: input.branch
|
|
14802
|
-
};
|
|
14803
|
-
}
|
|
14804
|
-
function renderEnvFile(env) {
|
|
14805
|
-
const lines = Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${quoteEnvValue(value)}`);
|
|
14806
|
-
return lines.join(`
|
|
14807
|
-
`) + `
|
|
14808
|
-
`;
|
|
14809
|
-
}
|
|
14810
|
-
async function writeRuntimeEnv(gitDir, env) {
|
|
14811
|
-
const { runtimeEnvPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
14812
|
-
await Bun.write(runtimeEnvPath, renderEnvFile(env));
|
|
14813
|
-
}
|
|
14814
|
-
async function writeControlEnv(gitDir, env) {
|
|
14815
|
-
const { controlEnvPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
14816
|
-
await Bun.write(controlEnvPath, renderEnvFile(env));
|
|
14817
|
-
}
|
|
14818
|
-
function isRecord4(raw) {
|
|
14819
|
-
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
14820
|
-
}
|
|
14821
|
-
function normalizeConversationMeta(raw) {
|
|
14822
|
-
if (!raw)
|
|
14823
|
-
return raw;
|
|
14824
|
-
if (raw.provider === "codexAppServer") {
|
|
14825
|
-
const conversationId2 = raw.conversationId || raw.threadId;
|
|
14826
|
-
const threadId = raw.threadId || raw.conversationId;
|
|
14827
|
-
if (!conversationId2 || !threadId)
|
|
14828
|
-
return;
|
|
14829
|
-
const normalized2 = {
|
|
14830
|
-
provider: "codexAppServer",
|
|
14831
|
-
conversationId: conversationId2,
|
|
14832
|
-
threadId,
|
|
14833
|
-
cwd: raw.cwd,
|
|
14834
|
-
lastSeenAt: raw.lastSeenAt
|
|
14835
|
-
};
|
|
14836
|
-
return normalized2;
|
|
14837
|
-
}
|
|
14838
|
-
const conversationId = raw.conversationId || raw.sessionId;
|
|
14839
|
-
const sessionId = raw.sessionId || raw.conversationId;
|
|
14840
|
-
if (!conversationId || !sessionId)
|
|
14841
|
-
return;
|
|
14842
|
-
const normalized = {
|
|
14843
|
-
provider: "claudeCode",
|
|
14844
|
-
conversationId,
|
|
14845
|
-
sessionId,
|
|
14846
|
-
cwd: raw.cwd,
|
|
14847
|
-
lastSeenAt: raw.lastSeenAt
|
|
14848
|
-
};
|
|
14849
|
-
return normalized;
|
|
14850
|
-
}
|
|
14851
|
-
function normalizeOptionalString(raw) {
|
|
14852
|
-
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
14853
|
-
}
|
|
14854
|
-
function normalizeWorktreeMeta(meta) {
|
|
14855
|
-
const conversation = normalizeConversationMeta(meta.conversation);
|
|
14856
|
-
const normalizedLabel = normalizeOptionalString(meta.label);
|
|
14857
|
-
if (conversation === meta.conversation && normalizedLabel === meta.label) {
|
|
14858
|
-
return meta;
|
|
14859
|
-
}
|
|
14860
|
-
const rest = { ...meta };
|
|
14861
|
-
delete rest.label;
|
|
14862
|
-
delete rest.conversation;
|
|
14863
|
-
return {
|
|
14864
|
-
...rest,
|
|
14865
|
-
...normalizedLabel ? { label: normalizedLabel } : {},
|
|
14866
|
-
...conversation !== undefined ? { conversation } : {}
|
|
14867
|
-
};
|
|
14868
|
-
}
|
|
14869
|
-
function isPrComment(raw) {
|
|
14870
|
-
if (!isRecord4(raw))
|
|
14871
|
-
return false;
|
|
14872
|
-
return (raw.type === "comment" || raw.type === "inline") && typeof raw.author === "string" && typeof raw.body === "string" && typeof raw.createdAt === "string" && (raw.path === undefined || typeof raw.path === "string") && (raw.line === undefined || raw.line === null || typeof raw.line === "number") && (raw.diffHunk === undefined || typeof raw.diffHunk === "string") && (raw.isReply === undefined || typeof raw.isReply === "boolean");
|
|
14873
|
-
}
|
|
14874
|
-
function isCiCheck(raw) {
|
|
14875
|
-
if (!isRecord4(raw))
|
|
14876
|
-
return false;
|
|
14877
|
-
return typeof raw.name === "string" && (raw.status === "pending" || raw.status === "success" || raw.status === "failed" || raw.status === "skipped") && (raw.url === null || typeof raw.url === "string") && (raw.runId === null || typeof raw.runId === "number");
|
|
14878
|
-
}
|
|
14879
|
-
function isPrEntry(raw) {
|
|
14880
|
-
if (!isRecord4(raw))
|
|
14881
|
-
return false;
|
|
14882
|
-
return typeof raw.repo === "string" && typeof raw.number === "number" && (raw.state === "open" || raw.state === "closed" || raw.state === "merged") && typeof raw.url === "string" && typeof raw.updatedAt === "string" && (raw.ciStatus === "none" || raw.ciStatus === "pending" || raw.ciStatus === "success" || raw.ciStatus === "failed") && Array.isArray(raw.ciChecks) && raw.ciChecks.every((check) => isCiCheck(check)) && Array.isArray(raw.comments) && raw.comments.every((comment) => isPrComment(comment));
|
|
14883
|
-
}
|
|
14884
|
-
async function readWorktreePrs(gitDir) {
|
|
14885
|
-
const { prsPath } = getWorktreeStoragePaths(gitDir);
|
|
14886
|
-
try {
|
|
14887
|
-
const raw = await Bun.file(prsPath).json();
|
|
14888
|
-
return Array.isArray(raw) && raw.every((entry) => isPrEntry(entry)) ? raw : [];
|
|
14889
|
-
} catch {
|
|
14890
|
-
return [];
|
|
14891
|
-
}
|
|
14892
|
-
}
|
|
14893
|
-
async function writeWorktreePrs(gitDir, prs) {
|
|
14894
|
-
const { prsPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
14895
|
-
await Bun.write(prsPath, JSON.stringify(prs, null, 2) + `
|
|
14896
|
-
`);
|
|
14897
|
-
}
|
|
14898
|
-
|
|
14899
|
-
// backend/src/adapters/agent-runtime.ts
|
|
14900
14928
|
var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
|
|
14901
14929
|
function shellQuote(value) {
|
|
14902
14930
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
@@ -15558,10 +15586,11 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
15558
15586
|
function buildBuiltInAgentInvocation(input) {
|
|
15559
15587
|
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
15560
15588
|
if (input.agent === "codex") {
|
|
15561
|
-
const hooksFlag = " --enable
|
|
15589
|
+
const hooksFlag = " --enable hooks";
|
|
15562
15590
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
15563
15591
|
if (input.launchMode === "resume") {
|
|
15564
|
-
|
|
15592
|
+
const resumeTarget = input.resumeConversationId ? ` ${quoteShell(input.resumeConversationId)}` : " --last";
|
|
15593
|
+
return `codex${hooksFlag}${yoloFlag2} resume${resumeTarget}${promptSuffix}`;
|
|
15565
15594
|
}
|
|
15566
15595
|
if (input.systemPrompt) {
|
|
15567
15596
|
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
|
|
@@ -15604,7 +15633,8 @@ function buildAgentInvocation(input) {
|
|
|
15604
15633
|
yolo: input.yolo,
|
|
15605
15634
|
systemPrompt: input.systemPrompt,
|
|
15606
15635
|
prompt: input.prompt,
|
|
15607
|
-
launchMode: input.launchMode
|
|
15636
|
+
launchMode: input.launchMode,
|
|
15637
|
+
resumeConversationId: input.resumeConversationId
|
|
15608
15638
|
});
|
|
15609
15639
|
}
|
|
15610
15640
|
return buildCustomAgentInvocation({
|
|
@@ -16248,6 +16278,17 @@ function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
|
|
|
16248
16278
|
return trimmed;
|
|
16249
16279
|
}
|
|
16250
16280
|
}
|
|
16281
|
+
function resolveCodexResumeConversationId(meta, agent, launchMode) {
|
|
16282
|
+
if (launchMode !== "resume")
|
|
16283
|
+
return;
|
|
16284
|
+
if (meta.agentTerminalStale !== true)
|
|
16285
|
+
return;
|
|
16286
|
+
if (agent.kind !== "builtin" || agent.implementation.agent !== "codex")
|
|
16287
|
+
return;
|
|
16288
|
+
if (meta.conversation?.provider !== "codexAppServer")
|
|
16289
|
+
return;
|
|
16290
|
+
return meta.conversation.threadId;
|
|
16291
|
+
}
|
|
16251
16292
|
function prefixAgentBranch(agent, branch) {
|
|
16252
16293
|
return `${agent}-${branch}`;
|
|
16253
16294
|
}
|
|
@@ -16329,6 +16370,7 @@ class LifecycleService {
|
|
|
16329
16370
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
16330
16371
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
16331
16372
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
16373
|
+
const resumeConversationId = resolveCodexResumeConversationId(initialized.meta, agent, launchMode);
|
|
16332
16374
|
await ensureAgentRuntimeArtifacts({
|
|
16333
16375
|
gitDir: initialized.paths.gitDir,
|
|
16334
16376
|
worktreePath: resolved.entry.path
|
|
@@ -16341,7 +16383,57 @@ class LifecycleService {
|
|
|
16341
16383
|
initialized,
|
|
16342
16384
|
worktreePath: resolved.entry.path,
|
|
16343
16385
|
launchMode,
|
|
16344
|
-
followUpPrompt: options.prompt
|
|
16386
|
+
followUpPrompt: options.prompt,
|
|
16387
|
+
resumeConversationId
|
|
16388
|
+
});
|
|
16389
|
+
if (initialized.meta.agentTerminalStale === true) {
|
|
16390
|
+
await writeWorktreeMeta(resolved.gitDir, {
|
|
16391
|
+
...initialized.meta,
|
|
16392
|
+
agentTerminalStale: false
|
|
16393
|
+
});
|
|
16394
|
+
}
|
|
16395
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
16396
|
+
return {
|
|
16397
|
+
branch,
|
|
16398
|
+
worktreeId: initialized.meta.worktreeId
|
|
16399
|
+
};
|
|
16400
|
+
} catch (error) {
|
|
16401
|
+
throw this.wrapOperationError(error);
|
|
16402
|
+
}
|
|
16403
|
+
}
|
|
16404
|
+
async refreshAgentTerminal(branch) {
|
|
16405
|
+
try {
|
|
16406
|
+
const resolved = await this.resolveExistingWorktree(branch);
|
|
16407
|
+
if (!resolved.meta) {
|
|
16408
|
+
throw new LifecycleError(`Worktree ${branch} has no managed metadata to refresh`, 409);
|
|
16409
|
+
}
|
|
16410
|
+
const initialized = await this.refreshManagedArtifacts(resolved);
|
|
16411
|
+
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
16412
|
+
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
16413
|
+
if (agent.kind !== "builtin" || agent.implementation.agent !== "codex") {
|
|
16414
|
+
throw new LifecycleError("Refreshing the agent terminal is only available for Codex worktrees", 409);
|
|
16415
|
+
}
|
|
16416
|
+
const conversation = initialized.meta.conversation;
|
|
16417
|
+
if (conversation?.provider !== "codexAppServer") {
|
|
16418
|
+
throw new LifecycleError("No Codex conversation is available to refresh", 409);
|
|
16419
|
+
}
|
|
16420
|
+
await ensureAgentRuntimeArtifacts({
|
|
16421
|
+
gitDir: initialized.paths.gitDir,
|
|
16422
|
+
worktreePath: resolved.entry.path
|
|
16423
|
+
});
|
|
16424
|
+
await this.materializeRuntimeSession({
|
|
16425
|
+
branch,
|
|
16426
|
+
profileName,
|
|
16427
|
+
profile,
|
|
16428
|
+
agent,
|
|
16429
|
+
initialized,
|
|
16430
|
+
worktreePath: resolved.entry.path,
|
|
16431
|
+
launchMode: "resume",
|
|
16432
|
+
resumeConversationId: conversation.threadId
|
|
16433
|
+
});
|
|
16434
|
+
await writeWorktreeMeta(resolved.gitDir, {
|
|
16435
|
+
...initialized.meta,
|
|
16436
|
+
agentTerminalStale: false
|
|
16345
16437
|
});
|
|
16346
16438
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
16347
16439
|
return {
|
|
@@ -16656,6 +16748,7 @@ class LifecycleService {
|
|
|
16656
16748
|
followUpPrompt: input.followUpPrompt,
|
|
16657
16749
|
launchMode: input.launchMode,
|
|
16658
16750
|
source: input.source,
|
|
16751
|
+
resumeConversationId: input.resumeConversationId,
|
|
16659
16752
|
containerName
|
|
16660
16753
|
}));
|
|
16661
16754
|
return;
|
|
@@ -16670,7 +16763,8 @@ class LifecycleService {
|
|
|
16670
16763
|
creationPrompt: input.creationPrompt,
|
|
16671
16764
|
followUpPrompt: input.followUpPrompt,
|
|
16672
16765
|
launchMode: input.launchMode,
|
|
16673
|
-
source: input.source
|
|
16766
|
+
source: input.source,
|
|
16767
|
+
resumeConversationId: input.resumeConversationId
|
|
16674
16768
|
}));
|
|
16675
16769
|
}
|
|
16676
16770
|
buildSessionLayout(input) {
|
|
@@ -16695,7 +16789,8 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
|
16695
16789
|
yolo: input.profile.yolo === true,
|
|
16696
16790
|
systemPrompt,
|
|
16697
16791
|
prompt,
|
|
16698
|
-
launchMode: input.launchMode
|
|
16792
|
+
launchMode: input.launchMode,
|
|
16793
|
+
resumeConversationId: input.resumeConversationId
|
|
16699
16794
|
}),
|
|
16700
16795
|
shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
16701
16796
|
} : {
|
|
@@ -16709,7 +16804,8 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
|
16709
16804
|
yolo: input.profile.yolo === true,
|
|
16710
16805
|
systemPrompt,
|
|
16711
16806
|
prompt,
|
|
16712
|
-
launchMode: input.launchMode
|
|
16807
|
+
launchMode: input.launchMode,
|
|
16808
|
+
resumeConversationId: input.resumeConversationId
|
|
16713
16809
|
}),
|
|
16714
16810
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
16715
16811
|
}
|
|
@@ -17445,9 +17541,10 @@ async function runLinearAutoCreateOnce(deps) {
|
|
|
17445
17541
|
for (const issue of oneshotIssues) {
|
|
17446
17542
|
try {
|
|
17447
17543
|
log.info(`[linear-auto-create] launching oneshot for ${issue.identifier}: ${issue.title}`);
|
|
17448
|
-
await deps.runOneshotForIssue(issue.identifier);
|
|
17544
|
+
const { branch } = await deps.runOneshotForIssue(issue.identifier);
|
|
17449
17545
|
processedIssueIds.add(issue.id);
|
|
17450
|
-
log.info(`[linear-auto-create] launched oneshot for ${issue.identifier}`);
|
|
17546
|
+
log.info(`[linear-auto-create] launched oneshot for ${issue.identifier} on ${branch}`);
|
|
17547
|
+
await notifyOneshotPickup(deps, issue, branch);
|
|
17451
17548
|
} catch (err) {
|
|
17452
17549
|
const msg = err instanceof Error ? err.message : String(err);
|
|
17453
17550
|
log.error(`[linear-auto-create] failed to launch oneshot for ${issue.identifier}: ${msg}`);
|
|
@@ -17477,6 +17574,16 @@ ${issue.description ?? ""}`.trim()
|
|
|
17477
17574
|
}
|
|
17478
17575
|
}
|
|
17479
17576
|
}
|
|
17577
|
+
async function notifyOneshotPickup(deps, issue, branch) {
|
|
17578
|
+
if (!deps.onOneshotPickedUp)
|
|
17579
|
+
return;
|
|
17580
|
+
try {
|
|
17581
|
+
await deps.onOneshotPickedUp({ issue, branch });
|
|
17582
|
+
} catch (err) {
|
|
17583
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17584
|
+
log.warn(`[linear-auto-create] pickup notification failed for ${issue.identifier}: ${msg}`);
|
|
17585
|
+
}
|
|
17586
|
+
}
|
|
17480
17587
|
function startLinearAutoCreateMonitor(deps, options = {}) {
|
|
17481
17588
|
log.info(`[linear-auto-create] monitor started (interval: ${LINEAR_AUTO_CREATE_POLL_INTERVAL_MS}ms)`);
|
|
17482
17589
|
return startSerializedInterval(() => runLinearAutoCreateOnce(deps), LINEAR_AUTO_CREATE_POLL_INTERVAL_MS, options.intervalDeps);
|
|
@@ -17664,13 +17771,13 @@ function startAutoPullMonitor(deps, intervalMs) {
|
|
|
17664
17771
|
|
|
17665
17772
|
// backend/src/services/agents-ui-stream-service.ts
|
|
17666
17773
|
function readNotificationParams(raw) {
|
|
17667
|
-
return
|
|
17774
|
+
return isRecord3(raw) ? raw : null;
|
|
17668
17775
|
}
|
|
17669
17776
|
function readThreadId(raw) {
|
|
17670
17777
|
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
17671
17778
|
}
|
|
17672
17779
|
function readNotificationItemType(raw) {
|
|
17673
|
-
if (!
|
|
17780
|
+
if (!isRecord3(raw))
|
|
17674
17781
|
return null;
|
|
17675
17782
|
return typeof raw.type === "string" ? raw.type : null;
|
|
17676
17783
|
}
|
|
@@ -17772,6 +17879,7 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue,
|
|
|
17772
17879
|
profile: state.profile,
|
|
17773
17880
|
agentName: state.agentName,
|
|
17774
17881
|
agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
|
|
17882
|
+
agentTerminalStale: state.agentTerminalStale,
|
|
17775
17883
|
mux: state.session.exists,
|
|
17776
17884
|
dirty: state.git.dirty,
|
|
17777
17885
|
unpushed: state.git.aheadCount > 0,
|
|
@@ -17797,6 +17905,7 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, find
|
|
|
17797
17905
|
profile: creating.profile,
|
|
17798
17906
|
agentName: creating.agentName,
|
|
17799
17907
|
agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
|
|
17908
|
+
agentTerminalStale: false,
|
|
17800
17909
|
mux: false,
|
|
17801
17910
|
dirty: false,
|
|
17802
17911
|
unpushed: false,
|
|
@@ -17851,6 +17960,7 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
|
|
|
17851
17960
|
profile: worktree.profile,
|
|
17852
17961
|
agentName: worktree.agentName,
|
|
17853
17962
|
agentLabel: worktree.agentLabel,
|
|
17963
|
+
agentTerminalStale: worktree.agentTerminalStale,
|
|
17854
17964
|
mux: worktree.mux,
|
|
17855
17965
|
status: worktree.status,
|
|
17856
17966
|
dirty: worktree.dirty,
|
|
@@ -17995,6 +18105,25 @@ class ClaudeConversationService {
|
|
|
17995
18105
|
}
|
|
17996
18106
|
|
|
17997
18107
|
// backend/src/services/worktree-conversation-service.ts
|
|
18108
|
+
function resolveCodexAppServerLaunchContext(input) {
|
|
18109
|
+
if (input.worktree.agentName !== "codex" || input.meta.agent !== "codex") {
|
|
18110
|
+
return err(409, "Codex web chat is only available for Codex worktrees");
|
|
18111
|
+
}
|
|
18112
|
+
if (!input.profile) {
|
|
18113
|
+
return err(409, `Profile is missing for Codex web chat: ${input.meta.profile}`);
|
|
18114
|
+
}
|
|
18115
|
+
if (input.meta.runtime !== "host" || input.profile.runtime !== "host") {
|
|
18116
|
+
return err(409, "Codex web chat is only available for host-runtime worktrees. Use the terminal for Docker worktrees.");
|
|
18117
|
+
}
|
|
18118
|
+
if (input.profile.yolo !== true) {
|
|
18119
|
+
return err(409, "Codex web chat requires a yolo profile to preserve the Codex approval policy. Use the terminal for this worktree.");
|
|
18120
|
+
}
|
|
18121
|
+
return ok({
|
|
18122
|
+
approvalPolicy: "never",
|
|
18123
|
+
personality: "pragmatic",
|
|
18124
|
+
sandbox: "danger-full-access"
|
|
18125
|
+
});
|
|
18126
|
+
}
|
|
17998
18127
|
function isCodexWorktree(worktree) {
|
|
17999
18128
|
return worktree.agentName === "codex";
|
|
18000
18129
|
}
|
|
@@ -18015,6 +18144,9 @@ function isAgentMessageItem(item) {
|
|
|
18015
18144
|
function extractUserText(item) {
|
|
18016
18145
|
return item.content.map((contentItem) => contentItem.text ?? "").join("").trim();
|
|
18017
18146
|
}
|
|
18147
|
+
function extractAgentText(item) {
|
|
18148
|
+
return item.text ?? item.message ?? "";
|
|
18149
|
+
}
|
|
18018
18150
|
function isActiveTurnStatus(status) {
|
|
18019
18151
|
return status === "inProgress" || status === "active" || status === "running" || status === "pending" || status === "queued";
|
|
18020
18152
|
}
|
|
@@ -18031,14 +18163,14 @@ function buildConversationMessages(thread) {
|
|
|
18031
18163
|
for (const turn of thread.turns) {
|
|
18032
18164
|
for (const item of turn.items) {
|
|
18033
18165
|
if (isUserMessageItem(item)) {
|
|
18034
|
-
const
|
|
18035
|
-
if (
|
|
18166
|
+
const text2 = extractUserText(item);
|
|
18167
|
+
if (text2.length === 0)
|
|
18036
18168
|
continue;
|
|
18037
18169
|
messages.push({
|
|
18038
18170
|
id: item.id,
|
|
18039
18171
|
turnId: turn.id,
|
|
18040
18172
|
role: "user",
|
|
18041
|
-
text,
|
|
18173
|
+
text: text2,
|
|
18042
18174
|
status: "completed",
|
|
18043
18175
|
createdAt: toIsoTimestamp(turn.startedAt)
|
|
18044
18176
|
});
|
|
@@ -18046,13 +18178,14 @@ function buildConversationMessages(thread) {
|
|
|
18046
18178
|
}
|
|
18047
18179
|
if (!isAgentMessageItem(item))
|
|
18048
18180
|
continue;
|
|
18049
|
-
|
|
18181
|
+
const text = extractAgentText(item);
|
|
18182
|
+
if (text.length === 0)
|
|
18050
18183
|
continue;
|
|
18051
18184
|
messages.push({
|
|
18052
18185
|
id: item.id,
|
|
18053
18186
|
turnId: turn.id,
|
|
18054
18187
|
role: "assistant",
|
|
18055
|
-
text
|
|
18188
|
+
text,
|
|
18056
18189
|
status: isActiveTurnStatus(turn.status) ? "inProgress" : "completed",
|
|
18057
18190
|
createdAt: toIsoTimestamp(turn.completedAt ?? turn.startedAt)
|
|
18058
18191
|
});
|
|
@@ -18112,6 +18245,39 @@ class WorktreeConversationService {
|
|
|
18112
18245
|
async readWorktreeConversation(worktree) {
|
|
18113
18246
|
return await this.withResolvedConversation(worktree, false, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
|
|
18114
18247
|
}
|
|
18248
|
+
async sendWorktreeConversationMessage(worktree, text) {
|
|
18249
|
+
return await this.withResolvedConversation(worktree, true, async ({ thread, launchContext }) => {
|
|
18250
|
+
const started = await this.deps.appServer.turnStart({
|
|
18251
|
+
threadId: thread.id,
|
|
18252
|
+
cwd: worktree.path,
|
|
18253
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18254
|
+
input: [{ type: "text", text }]
|
|
18255
|
+
});
|
|
18256
|
+
return ok({
|
|
18257
|
+
conversationId: thread.id,
|
|
18258
|
+
turnId: started.turn.id,
|
|
18259
|
+
running: true
|
|
18260
|
+
});
|
|
18261
|
+
});
|
|
18262
|
+
}
|
|
18263
|
+
async interruptWorktreeConversation(worktree) {
|
|
18264
|
+
return await this.withResolvedConversation(worktree, false, async ({ thread }) => {
|
|
18265
|
+
const conversation = buildConversationState2(thread);
|
|
18266
|
+
const turnId = conversation.activeTurnId;
|
|
18267
|
+
if (!turnId) {
|
|
18268
|
+
return err(409, "No active Codex turn to interrupt");
|
|
18269
|
+
}
|
|
18270
|
+
await this.deps.appServer.turnInterrupt({
|
|
18271
|
+
threadId: thread.id,
|
|
18272
|
+
turnId
|
|
18273
|
+
});
|
|
18274
|
+
return ok({
|
|
18275
|
+
conversationId: thread.id,
|
|
18276
|
+
turnId,
|
|
18277
|
+
interrupted: true
|
|
18278
|
+
});
|
|
18279
|
+
});
|
|
18280
|
+
}
|
|
18115
18281
|
async withResolvedConversation(worktree, allowCreate, fn) {
|
|
18116
18282
|
if (!isCodexWorktree(worktree)) {
|
|
18117
18283
|
return err(409, "Worktree chat is only available for Codex worktrees");
|
|
@@ -18132,8 +18298,12 @@ class WorktreeConversationService {
|
|
|
18132
18298
|
if (!meta) {
|
|
18133
18299
|
return err(409, "Worktree metadata is missing");
|
|
18134
18300
|
}
|
|
18301
|
+
const launchContextResult = await this.deps.resolveLaunchContext({ worktree, meta });
|
|
18302
|
+
if (!launchContextResult.ok)
|
|
18303
|
+
return launchContextResult;
|
|
18304
|
+
const launchContext = launchContextResult.data;
|
|
18135
18305
|
const now = this.now();
|
|
18136
|
-
const thread = await this.resolveThread(meta, worktree.path, allowCreate);
|
|
18306
|
+
const thread = await this.resolveThread(meta, worktree.path, allowCreate, launchContext);
|
|
18137
18307
|
if (!thread) {
|
|
18138
18308
|
return err(404, "No Codex thread could be resolved for this worktree");
|
|
18139
18309
|
}
|
|
@@ -18146,21 +18316,22 @@ class WorktreeConversationService {
|
|
|
18146
18316
|
gitDir,
|
|
18147
18317
|
meta: nextMeta,
|
|
18148
18318
|
thread,
|
|
18149
|
-
conversationMeta: nextMeta.conversation ?? conversationMeta
|
|
18319
|
+
conversationMeta: nextMeta.conversation ?? conversationMeta,
|
|
18320
|
+
launchContext
|
|
18150
18321
|
});
|
|
18151
18322
|
}
|
|
18152
|
-
async resolveThread(meta, cwd, allowCreate) {
|
|
18323
|
+
async resolveThread(meta, cwd, allowCreate, launchContext) {
|
|
18153
18324
|
const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
|
|
18154
18325
|
cwd,
|
|
18155
18326
|
limit: 20,
|
|
18156
18327
|
sortKey: "updated_at"
|
|
18157
18328
|
})).data);
|
|
18158
18329
|
if (discoveredThread) {
|
|
18159
|
-
return await this.ensureThreadLoaded(discoveredThread.id, cwd);
|
|
18330
|
+
return await this.ensureThreadLoaded(discoveredThread.id, cwd, launchContext);
|
|
18160
18331
|
}
|
|
18161
18332
|
const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
|
|
18162
18333
|
if (savedThreadId) {
|
|
18163
|
-
const savedThread = await this.tryLoadThread(savedThreadId, cwd);
|
|
18334
|
+
const savedThread = await this.tryLoadThread(savedThreadId, cwd, launchContext);
|
|
18164
18335
|
if (savedThread)
|
|
18165
18336
|
return savedThread;
|
|
18166
18337
|
log.warn(`[agents] saved codex thread missing, rediscovering cwd=${cwd} threadId=${savedThreadId}`);
|
|
@@ -18169,27 +18340,28 @@ class WorktreeConversationService {
|
|
|
18169
18340
|
return null;
|
|
18170
18341
|
const started = await this.deps.appServer.threadStart({
|
|
18171
18342
|
cwd,
|
|
18172
|
-
approvalPolicy:
|
|
18173
|
-
personality:
|
|
18174
|
-
sandbox:
|
|
18343
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18344
|
+
personality: launchContext.personality,
|
|
18345
|
+
sandbox: launchContext.sandbox
|
|
18175
18346
|
});
|
|
18176
18347
|
return started.thread;
|
|
18177
18348
|
}
|
|
18178
|
-
async tryLoadThread(threadId, cwd) {
|
|
18349
|
+
async tryLoadThread(threadId, cwd, launchContext) {
|
|
18179
18350
|
try {
|
|
18180
|
-
return await this.ensureThreadLoaded(threadId, cwd);
|
|
18351
|
+
return await this.ensureThreadLoaded(threadId, cwd, launchContext);
|
|
18181
18352
|
} catch {
|
|
18182
18353
|
return null;
|
|
18183
18354
|
}
|
|
18184
18355
|
}
|
|
18185
|
-
async ensureThreadLoaded(threadId, cwd) {
|
|
18356
|
+
async ensureThreadLoaded(threadId, cwd, launchContext) {
|
|
18186
18357
|
const initial = await this.deps.appServer.threadRead(threadId, false);
|
|
18187
18358
|
if (initial.thread.status.type === "notLoaded") {
|
|
18188
18359
|
await this.deps.appServer.threadResume({
|
|
18189
18360
|
threadId,
|
|
18190
18361
|
cwd,
|
|
18191
|
-
approvalPolicy:
|
|
18192
|
-
personality:
|
|
18362
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18363
|
+
personality: launchContext.personality,
|
|
18364
|
+
sandbox: launchContext.sandbox
|
|
18193
18365
|
});
|
|
18194
18366
|
}
|
|
18195
18367
|
return (await this.deps.appServer.threadRead(threadId, true)).thread;
|
|
@@ -18605,49 +18777,15 @@ class BunPortProbe {
|
|
|
18605
18777
|
}
|
|
18606
18778
|
}
|
|
18607
18779
|
|
|
18608
|
-
// backend/src/services/
|
|
18609
|
-
|
|
18610
|
-
var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
|
|
18611
|
-
var AUTO_NAME_TIMEOUT_MS = 15000;
|
|
18612
|
-
var DEFAULT_SYSTEM_PROMPT = [
|
|
18613
|
-
"Generate a concise git branch name from the task description.",
|
|
18614
|
-
"Return only the branch name.",
|
|
18615
|
-
"Use lowercase kebab-case.",
|
|
18616
|
-
`Maximum ${MAX_BRANCH_LENGTH} characters.`,
|
|
18617
|
-
"Do not include quotes, code fences, or prefixes like feature/ or fix/."
|
|
18618
|
-
].join(" ");
|
|
18619
|
-
function normalizeGeneratedBranchName(raw) {
|
|
18620
|
-
let branch = raw.trim();
|
|
18621
|
-
branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
|
|
18622
|
-
branch = branch.split(/\r?\n/)[0]?.trim() ?? "";
|
|
18623
|
-
branch = branch.replace(/^branch(?:\s+name)?\s*:\s*/i, "");
|
|
18624
|
-
branch = branch.replace(/^["'`]+|["'`]+$/g, "");
|
|
18625
|
-
branch = branch.toLowerCase();
|
|
18626
|
-
branch = branch.replace(/[^a-z0-9._/-]+/g, "-");
|
|
18627
|
-
branch = branch.replace(/[/.]+/g, "-");
|
|
18628
|
-
branch = branch.replace(/-+/g, "-");
|
|
18629
|
-
branch = branch.replace(/^-+|-+$/g, "");
|
|
18630
|
-
branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
|
|
18631
|
-
if (!branch) {
|
|
18632
|
-
throw new Error("Auto-name model returned an empty branch name");
|
|
18633
|
-
}
|
|
18634
|
-
if (!isValidBranchName(branch)) {
|
|
18635
|
-
throw new Error(`Auto-name model returned an invalid branch name: ${branch}`);
|
|
18636
|
-
}
|
|
18637
|
-
return branch;
|
|
18638
|
-
}
|
|
18639
|
-
function getSystemPrompt(config) {
|
|
18640
|
-
return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
18641
|
-
}
|
|
18642
|
-
|
|
18643
|
-
class AutoNameTimeoutError extends Error {
|
|
18780
|
+
// backend/src/services/llm-spawn.ts
|
|
18781
|
+
class LlmSpawnTimeoutError extends Error {
|
|
18644
18782
|
timeoutMs;
|
|
18645
18783
|
constructor(timeoutMs) {
|
|
18646
|
-
super(`
|
|
18784
|
+
super(`LLM spawn timed out after ${timeoutMs}ms`);
|
|
18647
18785
|
this.timeoutMs = timeoutMs;
|
|
18648
18786
|
}
|
|
18649
18787
|
}
|
|
18650
|
-
async function
|
|
18788
|
+
async function defaultLlmSpawn(args, options = {}) {
|
|
18651
18789
|
const proc = Bun.spawn(args, {
|
|
18652
18790
|
stdout: "pipe",
|
|
18653
18791
|
stderr: "pipe"
|
|
@@ -18657,7 +18795,8 @@ async function defaultSpawn(args, options = {}) {
|
|
|
18657
18795
|
new Response(proc.stderr).text(),
|
|
18658
18796
|
proc.exited
|
|
18659
18797
|
]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
|
|
18660
|
-
|
|
18798
|
+
const timeoutMs = options.timeoutMs;
|
|
18799
|
+
if (timeoutMs === undefined) {
|
|
18661
18800
|
return await resultPromise;
|
|
18662
18801
|
}
|
|
18663
18802
|
return await new Promise((resolve8, reject) => {
|
|
@@ -18669,8 +18808,8 @@ async function defaultSpawn(args, options = {}) {
|
|
|
18669
18808
|
try {
|
|
18670
18809
|
proc.kill("SIGKILL");
|
|
18671
18810
|
} catch {}
|
|
18672
|
-
reject(new
|
|
18673
|
-
},
|
|
18811
|
+
reject(new LlmSpawnTimeoutError(timeoutMs));
|
|
18812
|
+
}, timeoutMs);
|
|
18674
18813
|
resultPromise.then((result) => {
|
|
18675
18814
|
if (settled)
|
|
18676
18815
|
return;
|
|
@@ -18686,30 +18825,27 @@ async function defaultSpawn(args, options = {}) {
|
|
|
18686
18825
|
});
|
|
18687
18826
|
});
|
|
18688
18827
|
}
|
|
18689
|
-
function buildClaudeArgs(model, systemPrompt, prompt) {
|
|
18690
|
-
const args = [
|
|
18691
|
-
"claude",
|
|
18692
|
-
"-p",
|
|
18693
|
-
"--system-prompt",
|
|
18694
|
-
systemPrompt,
|
|
18695
|
-
"--output-format",
|
|
18696
|
-
"text",
|
|
18697
|
-
"--no-session-persistence",
|
|
18698
|
-
"--model",
|
|
18699
|
-
model || DEFAULT_AUTO_NAME_MODEL,
|
|
18700
|
-
"--effort",
|
|
18701
|
-
"low"
|
|
18702
|
-
];
|
|
18703
|
-
args.push(prompt);
|
|
18704
|
-
return args;
|
|
18705
|
-
}
|
|
18706
18828
|
function escapeTomlString(s) {
|
|
18707
18829
|
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
18708
18830
|
}
|
|
18709
|
-
|
|
18710
|
-
|
|
18711
|
-
|
|
18712
|
-
|
|
18831
|
+
var DEFAULT_CLAUDE_MODEL = "claude-haiku-4-5-20251001";
|
|
18832
|
+
function buildLlmArgs(config, systemPrompt, userPrompt) {
|
|
18833
|
+
if (config.provider === "claude") {
|
|
18834
|
+
return [
|
|
18835
|
+
"claude",
|
|
18836
|
+
"-p",
|
|
18837
|
+
"--system-prompt",
|
|
18838
|
+
systemPrompt,
|
|
18839
|
+
"--output-format",
|
|
18840
|
+
"text",
|
|
18841
|
+
"--no-session-persistence",
|
|
18842
|
+
"--model",
|
|
18843
|
+
config.model || DEFAULT_CLAUDE_MODEL,
|
|
18844
|
+
"--effort",
|
|
18845
|
+
"low",
|
|
18846
|
+
userPrompt
|
|
18847
|
+
];
|
|
18848
|
+
}
|
|
18713
18849
|
const args = [
|
|
18714
18850
|
"codex",
|
|
18715
18851
|
"-c",
|
|
@@ -18717,18 +18853,82 @@ function buildCodexArgs(model, systemPrompt, prompt) {
|
|
|
18717
18853
|
"exec",
|
|
18718
18854
|
"--ephemeral"
|
|
18719
18855
|
];
|
|
18720
|
-
if (model) {
|
|
18721
|
-
args.push("-m", model);
|
|
18856
|
+
if (config.model) {
|
|
18857
|
+
args.push("-m", config.model);
|
|
18722
18858
|
}
|
|
18723
|
-
args.push(
|
|
18859
|
+
args.push(userPrompt);
|
|
18724
18860
|
return args;
|
|
18725
18861
|
}
|
|
18862
|
+
async function runShortLlmTask(config, systemPrompt, userPrompt, options = {}) {
|
|
18863
|
+
const args = buildLlmArgs(config, systemPrompt, userPrompt);
|
|
18864
|
+
const spawnImpl = options.spawnImpl ?? defaultLlmSpawn;
|
|
18865
|
+
let result;
|
|
18866
|
+
try {
|
|
18867
|
+
result = await spawnImpl(args, { timeoutMs: options.timeoutMs });
|
|
18868
|
+
} catch (error) {
|
|
18869
|
+
if (error instanceof LlmSpawnTimeoutError) {
|
|
18870
|
+
return { ok: false, kind: "timeout", timeoutMs: error.timeoutMs, args };
|
|
18871
|
+
}
|
|
18872
|
+
return { ok: false, kind: "spawn_error", error, args };
|
|
18873
|
+
}
|
|
18874
|
+
if (result.exitCode !== 0) {
|
|
18875
|
+
return {
|
|
18876
|
+
ok: false,
|
|
18877
|
+
kind: "exit_nonzero",
|
|
18878
|
+
exitCode: result.exitCode,
|
|
18879
|
+
stdout: result.stdout,
|
|
18880
|
+
stderr: result.stderr,
|
|
18881
|
+
args
|
|
18882
|
+
};
|
|
18883
|
+
}
|
|
18884
|
+
return { ok: true, stdout: result.stdout, stderr: result.stderr, args };
|
|
18885
|
+
}
|
|
18886
|
+
function llmProviderLabel(config) {
|
|
18887
|
+
return config.provider === "claude" ? "claude" : "codex";
|
|
18888
|
+
}
|
|
18889
|
+
|
|
18890
|
+
// backend/src/services/auto-name-service.ts
|
|
18891
|
+
var MAX_BRANCH_LENGTH = 40;
|
|
18892
|
+
var AUTO_NAME_TIMEOUT_MS = 15000;
|
|
18893
|
+
var DEFAULT_SYSTEM_PROMPT = [
|
|
18894
|
+
"Generate a concise git branch name from the task description.",
|
|
18895
|
+
"Return only the branch name.",
|
|
18896
|
+
"Use lowercase kebab-case.",
|
|
18897
|
+
`Maximum ${MAX_BRANCH_LENGTH} characters.`,
|
|
18898
|
+
"Do not include quotes, code fences, or prefixes like feature/ or fix/."
|
|
18899
|
+
].join(" ");
|
|
18900
|
+
function normalizeGeneratedBranchName(raw) {
|
|
18901
|
+
let branch = raw.trim();
|
|
18902
|
+
branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
|
|
18903
|
+
branch = branch.split(/\r?\n/)[0]?.trim() ?? "";
|
|
18904
|
+
branch = branch.replace(/^branch(?:\s+name)?\s*:\s*/i, "");
|
|
18905
|
+
branch = branch.replace(/^["'`]+|["'`]+$/g, "");
|
|
18906
|
+
branch = branch.toLowerCase();
|
|
18907
|
+
branch = branch.replace(/[^a-z0-9._/-]+/g, "-");
|
|
18908
|
+
branch = branch.replace(/[/.]+/g, "-");
|
|
18909
|
+
branch = branch.replace(/-+/g, "-");
|
|
18910
|
+
branch = branch.replace(/^-+|-+$/g, "");
|
|
18911
|
+
branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
|
|
18912
|
+
if (!branch) {
|
|
18913
|
+
throw new Error("Auto-name model returned an empty branch name");
|
|
18914
|
+
}
|
|
18915
|
+
if (!isValidBranchName(branch)) {
|
|
18916
|
+
throw new Error(`Auto-name model returned an invalid branch name: ${branch}`);
|
|
18917
|
+
}
|
|
18918
|
+
return branch;
|
|
18919
|
+
}
|
|
18920
|
+
function getSystemPrompt(config) {
|
|
18921
|
+
return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
18922
|
+
}
|
|
18923
|
+
function buildPrompt(prompt) {
|
|
18924
|
+
return `Here is the task description: ${prompt}. You MUST return the branch name only, no other text or comments. Be fast, make it simple, and concise.`;
|
|
18925
|
+
}
|
|
18726
18926
|
|
|
18727
18927
|
class AutoNameService {
|
|
18728
18928
|
spawnImpl;
|
|
18729
18929
|
timeoutMs;
|
|
18730
18930
|
constructor(deps = {}) {
|
|
18731
|
-
this.spawnImpl = deps.spawnImpl
|
|
18931
|
+
this.spawnImpl = deps.spawnImpl;
|
|
18732
18932
|
this.timeoutMs = deps.timeoutMs ?? AUTO_NAME_TIMEOUT_MS;
|
|
18733
18933
|
}
|
|
18734
18934
|
async generateBranchName(config, task) {
|
|
@@ -18738,24 +18938,24 @@ class AutoNameService {
|
|
|
18738
18938
|
}
|
|
18739
18939
|
const systemPrompt = getSystemPrompt(config);
|
|
18740
18940
|
const userPrompt = buildPrompt(prompt);
|
|
18741
|
-
const
|
|
18742
|
-
const
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
if (
|
|
18941
|
+
const cli = llmProviderLabel(config);
|
|
18942
|
+
const runOptions = { timeoutMs: this.timeoutMs };
|
|
18943
|
+
if (this.spawnImpl)
|
|
18944
|
+
runOptions.spawnImpl = this.spawnImpl;
|
|
18945
|
+
const result = await runShortLlmTask(config, systemPrompt, userPrompt, runOptions);
|
|
18946
|
+
if (!result.ok) {
|
|
18947
|
+
if (result.kind === "timeout") {
|
|
18748
18948
|
const fallback = generateFallbackBranchName();
|
|
18749
18949
|
log.warn(`[auto-name] ${cli} timed out after ${this.timeoutMs}ms; using fallback branch ${fallback}`);
|
|
18750
18950
|
return fallback;
|
|
18751
18951
|
}
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18952
|
+
if (result.kind === "spawn_error") {
|
|
18953
|
+
throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
|
|
18954
|
+
}
|
|
18755
18955
|
const stderr = result.stderr.trim();
|
|
18756
18956
|
const stdout = result.stdout.trim();
|
|
18757
18957
|
const output2 = stderr || stdout || `exit ${result.exitCode}`;
|
|
18758
|
-
const command = args.join(" ");
|
|
18958
|
+
const command = result.args.join(" ");
|
|
18759
18959
|
throw new Error(`${cli} failed (command: ${command}): ${output2}`);
|
|
18760
18960
|
}
|
|
18761
18961
|
const output = result.stdout.trim();
|
|
@@ -18938,6 +19138,7 @@ function makeDefaultState(input) {
|
|
|
18938
19138
|
agentName: input.agentName ?? null,
|
|
18939
19139
|
source: input.source ?? "ui",
|
|
18940
19140
|
oneshot: input.oneshot ?? null,
|
|
19141
|
+
agentTerminalStale: input.agentTerminalStale === true,
|
|
18941
19142
|
git: {
|
|
18942
19143
|
exists: true,
|
|
18943
19144
|
branch: input.branch,
|
|
@@ -18985,6 +19186,8 @@ class ProjectRuntime {
|
|
|
18985
19186
|
existing.baseBranch = input.baseBranch;
|
|
18986
19187
|
existing.profile = input.profile ?? existing.profile;
|
|
18987
19188
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
19189
|
+
if (input.agentTerminalStale !== undefined)
|
|
19190
|
+
existing.agentTerminalStale = input.agentTerminalStale;
|
|
18988
19191
|
if (input.runtime)
|
|
18989
19192
|
existing.agent.runtime = input.runtime;
|
|
18990
19193
|
if (input.source !== undefined)
|
|
@@ -19046,6 +19249,11 @@ class ProjectRuntime {
|
|
|
19046
19249
|
state.prs = prs.map((pr) => clonePrEntry2(pr));
|
|
19047
19250
|
return state;
|
|
19048
19251
|
}
|
|
19252
|
+
setAgentTerminalStale(worktreeId, stale) {
|
|
19253
|
+
const state = this.requireWorktree(worktreeId);
|
|
19254
|
+
state.agentTerminalStale = stale;
|
|
19255
|
+
return state;
|
|
19256
|
+
}
|
|
19049
19257
|
applyEvent(event, now) {
|
|
19050
19258
|
const state = this.requireWorktree(event.worktreeId);
|
|
19051
19259
|
if (event.branch !== state.branch) {
|
|
@@ -19198,6 +19406,7 @@ class ReconciliationService {
|
|
|
19198
19406
|
path: entry.path,
|
|
19199
19407
|
profile: meta?.profile ?? null,
|
|
19200
19408
|
agentName: meta?.agent ?? null,
|
|
19409
|
+
agentTerminalStale: meta?.agentTerminalStale === true,
|
|
19201
19410
|
runtime: meta?.runtime ?? "host",
|
|
19202
19411
|
source: meta?.source ?? "ui",
|
|
19203
19412
|
oneshot: meta?.oneshot ?? null,
|
|
@@ -19233,6 +19442,7 @@ class ReconciliationService {
|
|
|
19233
19442
|
path: state.path,
|
|
19234
19443
|
profile: state.profile,
|
|
19235
19444
|
agentName: state.agentName,
|
|
19445
|
+
agentTerminalStale: state.agentTerminalStale,
|
|
19236
19446
|
runtime: state.runtime,
|
|
19237
19447
|
source: state.source,
|
|
19238
19448
|
oneshot: state.oneshot
|
|
@@ -19489,7 +19699,12 @@ var codexAppServerClient = new CodexAppServerClient({
|
|
|
19489
19699
|
var claudeCliClient = new ClaudeCliClient;
|
|
19490
19700
|
var worktreeConversationService = new WorktreeConversationService({
|
|
19491
19701
|
appServer: codexAppServerClient,
|
|
19492
|
-
git
|
|
19702
|
+
git,
|
|
19703
|
+
resolveLaunchContext: ({ worktree, meta }) => resolveCodexAppServerLaunchContext({
|
|
19704
|
+
worktree,
|
|
19705
|
+
meta,
|
|
19706
|
+
profile: config.profiles[meta.profile]
|
|
19707
|
+
})
|
|
19493
19708
|
});
|
|
19494
19709
|
var claudeConversationService = new ClaudeConversationService({
|
|
19495
19710
|
claude: claudeCliClient,
|
|
@@ -19521,6 +19736,7 @@ async function runOneshotForIssue(issueId) {
|
|
|
19521
19736
|
postToLinearOnDone: { kind: "issue", issueId }
|
|
19522
19737
|
}
|
|
19523
19738
|
});
|
|
19739
|
+
return { branch };
|
|
19524
19740
|
}
|
|
19525
19741
|
function startLinearAutoCreate() {
|
|
19526
19742
|
if (stopLinearAutoCreate)
|
|
@@ -19531,9 +19747,22 @@ function startLinearAutoCreate() {
|
|
|
19531
19747
|
git,
|
|
19532
19748
|
projectRoot: PROJECT_DIR,
|
|
19533
19749
|
runOneshotForIssue,
|
|
19750
|
+
onOneshotPickedUp: postLinearOneshotPickupComment,
|
|
19534
19751
|
...watchTeamKeys && watchTeamKeys.length > 0 ? { watchTeamKeys } : {}
|
|
19535
19752
|
});
|
|
19536
19753
|
}
|
|
19754
|
+
async function postLinearOneshotPickupComment(input) {
|
|
19755
|
+
const body = buildLinearPickupMarkdown({
|
|
19756
|
+
branch: input.branch,
|
|
19757
|
+
pickedUpAt: new Date
|
|
19758
|
+
});
|
|
19759
|
+
const result = await createIssueComment({ issueId: input.issue.id, body });
|
|
19760
|
+
if (!result.ok) {
|
|
19761
|
+
log.warn(`[linear-auto-create] failed to post pickup comment for ${input.issue.identifier}: ${result.error}`);
|
|
19762
|
+
return;
|
|
19763
|
+
}
|
|
19764
|
+
log.info(`[linear-auto-create] posted pickup comment for ${input.issue.identifier}: ${result.data.url}`);
|
|
19765
|
+
}
|
|
19537
19766
|
function normalizeOneshotConfig(input) {
|
|
19538
19767
|
if (!input)
|
|
19539
19768
|
return;
|
|
@@ -19607,7 +19836,7 @@ function parseWsMessage(raw) {
|
|
|
19607
19836
|
try {
|
|
19608
19837
|
const str = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
19609
19838
|
const msg = JSON.parse(str);
|
|
19610
|
-
if (!
|
|
19839
|
+
if (!isRecord3(msg))
|
|
19611
19840
|
return null;
|
|
19612
19841
|
const m = msg;
|
|
19613
19842
|
switch (m.type) {
|
|
@@ -19839,6 +20068,19 @@ function resolveWorktreeTerminalSubmitDelayMs(agentName) {
|
|
|
19839
20068
|
agent: agentName ? getAgentDefinition(config, agentName) : null
|
|
19840
20069
|
});
|
|
19841
20070
|
}
|
|
20071
|
+
async function setAgentTerminalStale(worktree, stale) {
|
|
20072
|
+
const gitDir = git.resolveWorktreeGitDir(worktree.path);
|
|
20073
|
+
const meta = await readWorktreeMeta(gitDir);
|
|
20074
|
+
if (!meta)
|
|
20075
|
+
return;
|
|
20076
|
+
await writeWorktreeMeta(gitDir, {
|
|
20077
|
+
...meta,
|
|
20078
|
+
agentTerminalStale: stale
|
|
20079
|
+
});
|
|
20080
|
+
if (projectRuntime.getWorktree(meta.worktreeId)) {
|
|
20081
|
+
projectRuntime.setAgentTerminalStale(meta.worktreeId, stale);
|
|
20082
|
+
}
|
|
20083
|
+
}
|
|
19842
20084
|
async function apiAttachAgentsWorktree(branch) {
|
|
19843
20085
|
touchDashboardActivity();
|
|
19844
20086
|
const resolved = await resolveAgentsWorktree(branch);
|
|
@@ -19879,7 +20121,15 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
|
|
|
19879
20121
|
if (!chatSupport.ok) {
|
|
19880
20122
|
return errorResponse(chatSupport.error, chatSupport.status);
|
|
19881
20123
|
}
|
|
19882
|
-
|
|
20124
|
+
if (chatSupport.data.provider === "codex") {
|
|
20125
|
+
const sendResult2 = await worktreeConversationService.sendWorktreeConversationMessage(resolved.worktree, parsed.data.text);
|
|
20126
|
+
if (!sendResult2.ok) {
|
|
20127
|
+
return errorResponse(sendResult2.error, sendResult2.status);
|
|
20128
|
+
}
|
|
20129
|
+
await setAgentTerminalStale(resolved.worktree, true);
|
|
20130
|
+
return jsonResponse(sendResult2.data);
|
|
20131
|
+
}
|
|
20132
|
+
const conversationResult = await claudeConversationService.readWorktreeConversation(resolved.worktree);
|
|
19883
20133
|
if (!conversationResult.ok) {
|
|
19884
20134
|
return errorResponse(conversationResult.error, conversationResult.status);
|
|
19885
20135
|
}
|
|
@@ -19909,7 +20159,15 @@ async function apiInterruptAgentsWorktree(branch) {
|
|
|
19909
20159
|
if (!chatSupport.ok) {
|
|
19910
20160
|
return errorResponse(chatSupport.error, chatSupport.status);
|
|
19911
20161
|
}
|
|
19912
|
-
|
|
20162
|
+
if (chatSupport.data.provider === "codex") {
|
|
20163
|
+
const interruptResult2 = await worktreeConversationService.interruptWorktreeConversation(resolved.worktree);
|
|
20164
|
+
if (!interruptResult2.ok) {
|
|
20165
|
+
return errorResponse(interruptResult2.error, interruptResult2.status);
|
|
20166
|
+
}
|
|
20167
|
+
await setAgentTerminalStale(resolved.worktree, true);
|
|
20168
|
+
return jsonResponse(interruptResult2.data);
|
|
20169
|
+
}
|
|
20170
|
+
const conversationResult = await claudeConversationService.readWorktreeConversation(resolved.worktree);
|
|
19913
20171
|
if (!conversationResult.ok) {
|
|
19914
20172
|
return errorResponse(conversationResult.error, conversationResult.status);
|
|
19915
20173
|
}
|
|
@@ -19926,6 +20184,12 @@ async function apiInterruptAgentsWorktree(branch) {
|
|
|
19926
20184
|
interrupted: true
|
|
19927
20185
|
});
|
|
19928
20186
|
}
|
|
20187
|
+
async function apiRefreshWorktreeAgentTerminal(branch) {
|
|
20188
|
+
touchDashboardActivity();
|
|
20189
|
+
ensureBranchNotBusy(branch);
|
|
20190
|
+
await lifecycleService.refreshAgentTerminal(branch);
|
|
20191
|
+
return jsonResponse({ ok: true });
|
|
20192
|
+
}
|
|
19929
20193
|
async function loadAgentsConversationSnapshot(branch) {
|
|
19930
20194
|
const resolved = await resolveAgentsWorktree(branch);
|
|
19931
20195
|
if (!resolved.ok) {
|
|
@@ -19949,7 +20213,7 @@ async function readErrorMessage(response) {
|
|
|
19949
20213
|
if (contentType.includes("application/json")) {
|
|
19950
20214
|
try {
|
|
19951
20215
|
const body = await response.json();
|
|
19952
|
-
if (
|
|
20216
|
+
if (isRecord3(body) && typeof body.error === "string" && body.error.length > 0) {
|
|
19953
20217
|
return body.error;
|
|
19954
20218
|
}
|
|
19955
20219
|
} catch {}
|
|
@@ -20442,6 +20706,11 @@ async function apiGetLinearIssues() {
|
|
|
20442
20706
|
return errorResponse(result.error, 502);
|
|
20443
20707
|
return jsonResponse(result.data);
|
|
20444
20708
|
}
|
|
20709
|
+
function apiGetAutoNameConfig() {
|
|
20710
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
20711
|
+
const linearAvailability = !config.integrations.linear.enabled ? "disabled" : !apiKey?.trim() ? "missing_api_key" : "ready";
|
|
20712
|
+
return jsonResponse({ autoName: config.autoName, linearAvailability });
|
|
20713
|
+
}
|
|
20445
20714
|
var MAX_DIFF_BYTES = 200 * 1024;
|
|
20446
20715
|
async function apiGetWorktreeDiff(name) {
|
|
20447
20716
|
await reconciliationService.reconcile(PROJECT_DIR);
|
|
@@ -20696,6 +20965,15 @@ function startServer(port) {
|
|
|
20696
20965
|
return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
|
|
20697
20966
|
}
|
|
20698
20967
|
},
|
|
20968
|
+
[apiPaths.refreshWorktreeAgentTerminal]: {
|
|
20969
|
+
POST: (req) => {
|
|
20970
|
+
const parsed = parseWorktreeNameParam(req.params);
|
|
20971
|
+
if (!parsed.ok)
|
|
20972
|
+
return parsed.response;
|
|
20973
|
+
const name = parsed.data;
|
|
20974
|
+
return catching(`POST /api/worktrees/${name}/agent-terminal/refresh`, () => apiRefreshWorktreeAgentTerminal(name));
|
|
20975
|
+
}
|
|
20976
|
+
},
|
|
20699
20977
|
[apiPaths.setWorktreeArchived]: {
|
|
20700
20978
|
PUT: (req) => {
|
|
20701
20979
|
const parsed = parseWorktreeNameParam(req.params);
|
|
@@ -20771,6 +21049,9 @@ function startServer(port) {
|
|
|
20771
21049
|
[apiPaths.fetchLinearIssues]: {
|
|
20772
21050
|
GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
|
|
20773
21051
|
},
|
|
21052
|
+
[apiPaths.fetchAutoNameConfig]: {
|
|
21053
|
+
GET: () => catching("GET /api/project/auto-name", async () => apiGetAutoNameConfig())
|
|
21054
|
+
},
|
|
20774
21055
|
[apiPaths.setLinearAutoCreate]: {
|
|
20775
21056
|
PUT: (req) => catching("PUT /api/linear/auto-create", () => apiSetLinearAutoCreate(req))
|
|
20776
21057
|
},
|
|
@@ -20949,16 +21230,36 @@ function startServer(port) {
|
|
|
20949
21230
|
function actualPort(server, requested) {
|
|
20950
21231
|
return server.port ?? requested;
|
|
20951
21232
|
}
|
|
21233
|
+
var MAX_INCREMENTAL_BIND_ATTEMPTS = 100;
|
|
21234
|
+
var PORT_STRICT = Bun.env.WEBMUX_PORT_STRICT === "1";
|
|
20952
21235
|
function bindServer() {
|
|
20953
|
-
|
|
20954
|
-
|
|
20955
|
-
|
|
20956
|
-
|
|
20957
|
-
|
|
21236
|
+
if (PORT_STRICT) {
|
|
21237
|
+
try {
|
|
21238
|
+
return actualPort(startServer(PORT), PORT);
|
|
21239
|
+
} catch (err2) {
|
|
21240
|
+
const code = err2?.code;
|
|
21241
|
+
if (code === "EADDRINUSE") {
|
|
21242
|
+
log.error(`[serve] port ${PORT} is in use and was set explicitly; drop --port / PORT to let webmux pick a free port`);
|
|
21243
|
+
}
|
|
20958
21244
|
throw err2;
|
|
20959
|
-
|
|
20960
|
-
return actualPort(startServer(0), 0);
|
|
21245
|
+
}
|
|
20961
21246
|
}
|
|
21247
|
+
let candidate = PORT;
|
|
21248
|
+
for (let attempt = 0;attempt < MAX_INCREMENTAL_BIND_ATTEMPTS; attempt++) {
|
|
21249
|
+
try {
|
|
21250
|
+
const server = startServer(candidate);
|
|
21251
|
+
if (attempt > 0)
|
|
21252
|
+
log.info(`[serve] port ${PORT} in use; bound to ${actualPort(server, candidate)}`);
|
|
21253
|
+
return actualPort(server, candidate);
|
|
21254
|
+
} catch (err2) {
|
|
21255
|
+
const code = err2?.code;
|
|
21256
|
+
if (code !== "EADDRINUSE")
|
|
21257
|
+
throw err2;
|
|
21258
|
+
candidate += 1;
|
|
21259
|
+
}
|
|
21260
|
+
}
|
|
21261
|
+
log.info(`[serve] ports ${PORT}..${PORT + MAX_INCREMENTAL_BIND_ATTEMPTS - 1} all in use; falling back to an OS-picked port`);
|
|
21262
|
+
return actualPort(startServer(0), 0);
|
|
20962
21263
|
}
|
|
20963
21264
|
var BOUND_PORT = bindServer();
|
|
20964
21265
|
var instanceRegistry = createInstanceRegistry();
|