webmux 0.34.0 → 0.36.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 +2081 -846
- package/bin/webmux.js +1098 -324
- package/frontend/dist/assets/{DiffDialog-Dv77nDf4.js → DiffDialog-8G3iLEAt.js} +6 -6
- package/frontend/dist/assets/index-Bz43HwWi.css +1 -0
- package/frontend/dist/assets/index-OF0sLw7N.js +35 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-BgvCuf9J.js +0 -34
- package/frontend/dist/assets/index-EO_hEDxL.css +0 -1
package/backend/dist/server.js
CHANGED
|
@@ -6965,7 +6965,7 @@ import { networkInterfaces } from "os";
|
|
|
6965
6965
|
// package.json
|
|
6966
6966
|
var package_default = {
|
|
6967
6967
|
name: "webmux",
|
|
6968
|
-
version: "0.
|
|
6968
|
+
version: "0.36.0",
|
|
6969
6969
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
6970
6970
|
type: "module",
|
|
6971
6971
|
repository: {
|
|
@@ -10994,7 +10994,7 @@ var coerce = {
|
|
|
10994
10994
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
10995
10995
|
};
|
|
10996
10996
|
var NEVER = INVALID;
|
|
10997
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
10997
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+c185e43edea803d3/node_modules/@ts-rest/core/index.esm.mjs
|
|
10998
10998
|
var isZodObjectStrict = (obj) => {
|
|
10999
10999
|
return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
|
|
11000
11000
|
};
|
|
@@ -11301,6 +11301,15 @@ var LinearIssuesResponseSchema = exports_external.object({
|
|
|
11301
11301
|
availability: LinearIssueAvailabilitySchema,
|
|
11302
11302
|
issues: exports_external.array(LinearIssueSchema)
|
|
11303
11303
|
});
|
|
11304
|
+
var AutoNameProviderSchema = exports_external.enum(["claude", "codex"]);
|
|
11305
|
+
var AutoNameConfigResponseSchema = exports_external.object({
|
|
11306
|
+
autoName: exports_external.object({
|
|
11307
|
+
provider: AutoNameProviderSchema,
|
|
11308
|
+
model: exports_external.string().optional(),
|
|
11309
|
+
systemPrompt: exports_external.string().optional()
|
|
11310
|
+
}).nullable(),
|
|
11311
|
+
linearAvailability: LinearIssueAvailabilitySchema
|
|
11312
|
+
});
|
|
11304
11313
|
var WorktreeCreationStateSchema = exports_external.object({
|
|
11305
11314
|
phase: WorktreeCreationPhaseSchema
|
|
11306
11315
|
});
|
|
@@ -11322,6 +11331,7 @@ var ProjectWorktreeSnapshotSchema = exports_external.object({
|
|
|
11322
11331
|
profile: exports_external.string().nullable(),
|
|
11323
11332
|
agentName: AgentIdSchema.nullable(),
|
|
11324
11333
|
agentLabel: exports_external.string().nullable(),
|
|
11334
|
+
agentTerminalStale: exports_external.boolean(),
|
|
11325
11335
|
mux: exports_external.boolean(),
|
|
11326
11336
|
dirty: exports_external.boolean(),
|
|
11327
11337
|
unpushed: exports_external.boolean(),
|
|
@@ -11370,6 +11380,7 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
|
|
|
11370
11380
|
profile: exports_external.string().nullable(),
|
|
11371
11381
|
agentName: AgentIdSchema.nullable(),
|
|
11372
11382
|
agentLabel: exports_external.string().nullable(),
|
|
11383
|
+
agentTerminalStale: exports_external.boolean(),
|
|
11373
11384
|
mux: exports_external.boolean(),
|
|
11374
11385
|
status: exports_external.string(),
|
|
11375
11386
|
dirty: exports_external.boolean(),
|
|
@@ -11381,17 +11392,24 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
|
|
|
11381
11392
|
conversation: WorktreeConversationRefSchema.nullable()
|
|
11382
11393
|
});
|
|
11383
11394
|
var AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
|
|
11384
|
-
var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
|
|
11385
|
-
var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
|
|
11395
|
+
var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress", "failed"]);
|
|
11396
|
+
var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "thinking", "toolUse", "toolResult"]);
|
|
11386
11397
|
var AgentsUiConversationMessageSchema = exports_external.object({
|
|
11387
11398
|
id: exports_external.string(),
|
|
11388
11399
|
turnId: exports_external.string(),
|
|
11400
|
+
order: exports_external.number().int().nonnegative(),
|
|
11389
11401
|
role: AgentsUiConversationMessageRoleSchema,
|
|
11390
11402
|
text: exports_external.string(),
|
|
11391
11403
|
status: AgentsUiConversationMessageStatusSchema,
|
|
11392
11404
|
createdAt: exports_external.string().nullable(),
|
|
11393
|
-
kind: AgentsUiConversationMessageKindSchema
|
|
11394
|
-
|
|
11405
|
+
kind: AgentsUiConversationMessageKindSchema,
|
|
11406
|
+
phase: exports_external.string().optional(),
|
|
11407
|
+
toolName: exports_external.string().optional(),
|
|
11408
|
+
toolCallId: exports_external.string().optional(),
|
|
11409
|
+
command: exports_external.string().optional(),
|
|
11410
|
+
cwd: exports_external.string().optional(),
|
|
11411
|
+
exitCode: exports_external.number().nullable().optional(),
|
|
11412
|
+
durationMs: exports_external.number().nullable().optional()
|
|
11395
11413
|
});
|
|
11396
11414
|
var AgentsUiConversationStateSchema = exports_external.object({
|
|
11397
11415
|
provider: WorktreeConversationProviderSchema,
|
|
@@ -11415,24 +11433,36 @@ var AgentsUiInterruptResponseSchema = exports_external.object({
|
|
|
11415
11433
|
turnId: exports_external.string(),
|
|
11416
11434
|
interrupted: exports_external.literal(true)
|
|
11417
11435
|
});
|
|
11418
|
-
var AgentsUiConversationSnapshotEventSchema = exports_external.object({
|
|
11419
|
-
type: exports_external.literal("snapshot"),
|
|
11420
|
-
data: AgentsUiWorktreeConversationResponseSchema
|
|
11421
|
-
});
|
|
11422
11436
|
var AgentsUiConversationMessageDeltaEventSchema = exports_external.object({
|
|
11423
11437
|
type: exports_external.literal("messageDelta"),
|
|
11438
|
+
revision: exports_external.number().int().nonnegative(),
|
|
11424
11439
|
conversationId: exports_external.string(),
|
|
11425
11440
|
turnId: exports_external.string(),
|
|
11426
11441
|
itemId: exports_external.string(),
|
|
11442
|
+
order: exports_external.number().int().nonnegative(),
|
|
11427
11443
|
delta: exports_external.string()
|
|
11428
11444
|
});
|
|
11445
|
+
var AgentsUiConversationMessageUpsertEventSchema = exports_external.object({
|
|
11446
|
+
type: exports_external.literal("messageUpsert"),
|
|
11447
|
+
revision: exports_external.number().int().nonnegative(),
|
|
11448
|
+
conversationId: exports_external.string(),
|
|
11449
|
+
message: AgentsUiConversationMessageSchema
|
|
11450
|
+
});
|
|
11451
|
+
var AgentsUiConversationStatusEventSchema = exports_external.object({
|
|
11452
|
+
type: exports_external.literal("conversationStatus"),
|
|
11453
|
+
revision: exports_external.number().int().nonnegative(),
|
|
11454
|
+
conversationId: exports_external.string(),
|
|
11455
|
+
running: exports_external.boolean(),
|
|
11456
|
+
activeTurnId: exports_external.string().nullable()
|
|
11457
|
+
});
|
|
11429
11458
|
var AgentsUiConversationErrorEventSchema = exports_external.object({
|
|
11430
11459
|
type: exports_external.literal("error"),
|
|
11431
11460
|
message: exports_external.string()
|
|
11432
11461
|
});
|
|
11433
11462
|
var AgentsUiConversationEventSchema = exports_external.discriminatedUnion("type", [
|
|
11434
|
-
AgentsUiConversationSnapshotEventSchema,
|
|
11435
11463
|
AgentsUiConversationMessageDeltaEventSchema,
|
|
11464
|
+
AgentsUiConversationMessageUpsertEventSchema,
|
|
11465
|
+
AgentsUiConversationStatusEventSchema,
|
|
11436
11466
|
AgentsUiConversationErrorEventSchema
|
|
11437
11467
|
]);
|
|
11438
11468
|
var WorktreeListResponseSchema = exports_external.object({
|
|
@@ -11523,6 +11553,7 @@ var apiPaths = {
|
|
|
11523
11553
|
removeWorktree: "/api/worktrees/:name",
|
|
11524
11554
|
openWorktree: "/api/worktrees/:name/open",
|
|
11525
11555
|
closeWorktree: "/api/worktrees/:name/close",
|
|
11556
|
+
refreshWorktreeAgentTerminal: "/api/worktrees/:name/agent-terminal/refresh",
|
|
11526
11557
|
setWorktreeArchived: "/api/worktrees/:name/archive",
|
|
11527
11558
|
syncWorktreePrs: "/api/worktrees/:name/sync-prs",
|
|
11528
11559
|
postWorktreeToLinear: "/api/worktrees/:name/linear/post",
|
|
@@ -11531,6 +11562,7 @@ var apiPaths = {
|
|
|
11531
11562
|
mergeWorktree: "/api/worktrees/:name/merge",
|
|
11532
11563
|
fetchWorktreeDiff: "/api/worktrees/:name/diff",
|
|
11533
11564
|
fetchLinearIssues: "/api/linear/issues",
|
|
11565
|
+
fetchAutoNameConfig: "/api/project/auto-name",
|
|
11534
11566
|
setLinearAutoCreate: "/api/linear/auto-create",
|
|
11535
11567
|
setAutoRemoveOnMerge: "/api/github/auto-remove-on-merge",
|
|
11536
11568
|
pullMain: "/api/pull-main",
|
|
@@ -11721,6 +11753,16 @@ var apiContract = c.router({
|
|
|
11721
11753
|
...commonErrorResponses
|
|
11722
11754
|
}
|
|
11723
11755
|
},
|
|
11756
|
+
refreshWorktreeAgentTerminal: {
|
|
11757
|
+
method: "POST",
|
|
11758
|
+
path: apiPaths.refreshWorktreeAgentTerminal,
|
|
11759
|
+
pathParams: WorktreeNameParamsSchema,
|
|
11760
|
+
body: c.noBody(),
|
|
11761
|
+
responses: {
|
|
11762
|
+
200: OkResponseSchema,
|
|
11763
|
+
...commonErrorResponses
|
|
11764
|
+
}
|
|
11765
|
+
},
|
|
11724
11766
|
setWorktreeArchived: {
|
|
11725
11767
|
method: "PUT",
|
|
11726
11768
|
path: apiPaths.setWorktreeArchived,
|
|
@@ -11799,6 +11841,14 @@ var apiContract = c.router({
|
|
|
11799
11841
|
502: ErrorResponseSchema
|
|
11800
11842
|
}
|
|
11801
11843
|
},
|
|
11844
|
+
fetchAutoNameConfig: {
|
|
11845
|
+
method: "GET",
|
|
11846
|
+
path: apiPaths.fetchAutoNameConfig,
|
|
11847
|
+
responses: {
|
|
11848
|
+
200: AutoNameConfigResponseSchema,
|
|
11849
|
+
500: ErrorResponseSchema
|
|
11850
|
+
}
|
|
11851
|
+
},
|
|
11802
11852
|
setLinearAutoCreate: {
|
|
11803
11853
|
method: "PUT",
|
|
11804
11854
|
path: apiPaths.setLinearAutoCreate,
|
|
@@ -12193,10 +12243,237 @@ async function loadControlToken() {
|
|
|
12193
12243
|
return controlToken;
|
|
12194
12244
|
}
|
|
12195
12245
|
|
|
12246
|
+
// backend/src/adapters/fs.ts
|
|
12247
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
12248
|
+
import { join } from "path";
|
|
12249
|
+
|
|
12250
|
+
// backend/src/domain/model.ts
|
|
12251
|
+
var WORKTREE_META_SCHEMA_VERSION = 1;
|
|
12252
|
+
var WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
12253
|
+
|
|
12254
|
+
// backend/src/adapters/fs.ts
|
|
12255
|
+
var SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
|
|
12256
|
+
var DOTENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/;
|
|
12257
|
+
function stringifyAllocatedPorts(ports) {
|
|
12258
|
+
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
12259
|
+
return Object.fromEntries(entries);
|
|
12260
|
+
}
|
|
12261
|
+
function quoteEnvValue(value) {
|
|
12262
|
+
if (value.length > 0 && SAFE_ENV_VALUE_RE.test(value))
|
|
12263
|
+
return value;
|
|
12264
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
12265
|
+
}
|
|
12266
|
+
function parseDotenv(content) {
|
|
12267
|
+
const env = {};
|
|
12268
|
+
for (const line of content.split(`
|
|
12269
|
+
`)) {
|
|
12270
|
+
if (line.trimStart().startsWith("#"))
|
|
12271
|
+
continue;
|
|
12272
|
+
const match = DOTENV_LINE_RE.exec(line);
|
|
12273
|
+
if (!match)
|
|
12274
|
+
continue;
|
|
12275
|
+
const key = match[1];
|
|
12276
|
+
let value = match[2];
|
|
12277
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
12278
|
+
value = value.slice(1, -1);
|
|
12279
|
+
} else {
|
|
12280
|
+
value = value.trimEnd();
|
|
12281
|
+
}
|
|
12282
|
+
env[key] = value;
|
|
12283
|
+
}
|
|
12284
|
+
return env;
|
|
12285
|
+
}
|
|
12286
|
+
async function loadDotenvLocal(worktreePath) {
|
|
12287
|
+
try {
|
|
12288
|
+
const content = await Bun.file(join(worktreePath, ".env.local")).text();
|
|
12289
|
+
return parseDotenv(content);
|
|
12290
|
+
} catch {
|
|
12291
|
+
return {};
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
function getWorktreeStoragePaths(gitDir) {
|
|
12295
|
+
const webmuxDir = join(gitDir, "webmux");
|
|
12296
|
+
return {
|
|
12297
|
+
gitDir,
|
|
12298
|
+
webmuxDir,
|
|
12299
|
+
metaPath: join(webmuxDir, "meta.json"),
|
|
12300
|
+
runtimeEnvPath: join(webmuxDir, "runtime.env"),
|
|
12301
|
+
controlEnvPath: join(webmuxDir, "control.env"),
|
|
12302
|
+
prsPath: join(webmuxDir, "prs.json")
|
|
12303
|
+
};
|
|
12304
|
+
}
|
|
12305
|
+
function getProjectArchiveStatePath(gitDir) {
|
|
12306
|
+
return join(gitDir, "webmux", "archive.json");
|
|
12307
|
+
}
|
|
12308
|
+
async function ensureWorktreeStorageDirs(gitDir) {
|
|
12309
|
+
const paths = getWorktreeStoragePaths(gitDir);
|
|
12310
|
+
await mkdir2(paths.webmuxDir, { recursive: true });
|
|
12311
|
+
return paths;
|
|
12312
|
+
}
|
|
12313
|
+
async function readWorktreeMeta(gitDir) {
|
|
12314
|
+
const { metaPath } = getWorktreeStoragePaths(gitDir);
|
|
12315
|
+
try {
|
|
12316
|
+
const raw = await Bun.file(metaPath).json();
|
|
12317
|
+
return normalizeWorktreeMeta(raw);
|
|
12318
|
+
} catch {
|
|
12319
|
+
return null;
|
|
12320
|
+
}
|
|
12321
|
+
}
|
|
12322
|
+
async function writeWorktreeMeta(gitDir, meta) {
|
|
12323
|
+
const { metaPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12324
|
+
await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
|
|
12325
|
+
`);
|
|
12326
|
+
}
|
|
12327
|
+
function isArchivedWorktreeEntry(raw) {
|
|
12328
|
+
return isRecord(raw) && typeof raw.path === "string" && typeof raw.archivedAt === "string";
|
|
12329
|
+
}
|
|
12330
|
+
function emptyWorktreeArchiveState() {
|
|
12331
|
+
return {
|
|
12332
|
+
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
12333
|
+
entries: []
|
|
12334
|
+
};
|
|
12335
|
+
}
|
|
12336
|
+
function isWorktreeArchiveState(raw) {
|
|
12337
|
+
return isRecord(raw) && typeof raw.schemaVersion === "number" && Array.isArray(raw.entries) && raw.entries.every((entry) => isArchivedWorktreeEntry(entry));
|
|
12338
|
+
}
|
|
12339
|
+
async function readWorktreeArchiveState(gitDir) {
|
|
12340
|
+
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
12341
|
+
try {
|
|
12342
|
+
const raw = await Bun.file(archivePath).json();
|
|
12343
|
+
return isWorktreeArchiveState(raw) ? {
|
|
12344
|
+
schemaVersion: raw.schemaVersion,
|
|
12345
|
+
entries: raw.entries.map((entry) => ({ ...entry }))
|
|
12346
|
+
} : emptyWorktreeArchiveState();
|
|
12347
|
+
} catch {
|
|
12348
|
+
return emptyWorktreeArchiveState();
|
|
12349
|
+
}
|
|
12350
|
+
}
|
|
12351
|
+
async function writeWorktreeArchiveState(gitDir, state) {
|
|
12352
|
+
const archivePath = getProjectArchiveStatePath(gitDir);
|
|
12353
|
+
await ensureWorktreeStorageDirs(gitDir);
|
|
12354
|
+
await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
|
|
12355
|
+
`);
|
|
12356
|
+
}
|
|
12357
|
+
function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
|
|
12358
|
+
return {
|
|
12359
|
+
...dotenvValues,
|
|
12360
|
+
...meta.startupEnvValues,
|
|
12361
|
+
...stringifyAllocatedPorts(meta.allocatedPorts),
|
|
12362
|
+
...extraEnv,
|
|
12363
|
+
WEBMUX_WORKTREE_ID: meta.worktreeId,
|
|
12364
|
+
WEBMUX_BRANCH: meta.branch,
|
|
12365
|
+
WEBMUX_PROFILE: meta.profile,
|
|
12366
|
+
WEBMUX_AGENT: meta.agent,
|
|
12367
|
+
WEBMUX_RUNTIME: meta.runtime
|
|
12368
|
+
};
|
|
12369
|
+
}
|
|
12370
|
+
function buildControlEnvMap(input) {
|
|
12371
|
+
return {
|
|
12372
|
+
WEBMUX_CONTROL_URL: input.controlUrl,
|
|
12373
|
+
WEBMUX_CONTROL_TOKEN: input.controlToken,
|
|
12374
|
+
WEBMUX_WORKTREE_ID: input.worktreeId,
|
|
12375
|
+
WEBMUX_BRANCH: input.branch
|
|
12376
|
+
};
|
|
12377
|
+
}
|
|
12378
|
+
function renderEnvFile(env) {
|
|
12379
|
+
const lines = Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${quoteEnvValue(value)}`);
|
|
12380
|
+
return lines.join(`
|
|
12381
|
+
`) + `
|
|
12382
|
+
`;
|
|
12383
|
+
}
|
|
12384
|
+
async function writeRuntimeEnv(gitDir, env) {
|
|
12385
|
+
const { runtimeEnvPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12386
|
+
await Bun.write(runtimeEnvPath, renderEnvFile(env));
|
|
12387
|
+
}
|
|
12388
|
+
async function writeControlEnv(gitDir, env) {
|
|
12389
|
+
const { controlEnvPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12390
|
+
await Bun.write(controlEnvPath, renderEnvFile(env));
|
|
12391
|
+
}
|
|
12392
|
+
function isRecord(raw) {
|
|
12393
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
12394
|
+
}
|
|
12395
|
+
function normalizeConversationMeta(raw) {
|
|
12396
|
+
if (!raw)
|
|
12397
|
+
return raw;
|
|
12398
|
+
if (raw.provider === "codexAppServer") {
|
|
12399
|
+
const conversationId2 = raw.conversationId || raw.threadId;
|
|
12400
|
+
const threadId = raw.threadId || raw.conversationId;
|
|
12401
|
+
if (!conversationId2 || !threadId)
|
|
12402
|
+
return;
|
|
12403
|
+
const normalized2 = {
|
|
12404
|
+
provider: "codexAppServer",
|
|
12405
|
+
conversationId: conversationId2,
|
|
12406
|
+
threadId,
|
|
12407
|
+
cwd: raw.cwd,
|
|
12408
|
+
lastSeenAt: raw.lastSeenAt
|
|
12409
|
+
};
|
|
12410
|
+
return normalized2;
|
|
12411
|
+
}
|
|
12412
|
+
const conversationId = raw.conversationId || raw.sessionId;
|
|
12413
|
+
const sessionId = raw.sessionId || raw.conversationId;
|
|
12414
|
+
if (!conversationId || !sessionId)
|
|
12415
|
+
return;
|
|
12416
|
+
const normalized = {
|
|
12417
|
+
provider: "claudeCode",
|
|
12418
|
+
conversationId,
|
|
12419
|
+
sessionId,
|
|
12420
|
+
cwd: raw.cwd,
|
|
12421
|
+
lastSeenAt: raw.lastSeenAt
|
|
12422
|
+
};
|
|
12423
|
+
return normalized;
|
|
12424
|
+
}
|
|
12425
|
+
function normalizeOptionalString(raw) {
|
|
12426
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
12427
|
+
}
|
|
12428
|
+
function normalizeWorktreeMeta(meta) {
|
|
12429
|
+
const conversation = normalizeConversationMeta(meta.conversation);
|
|
12430
|
+
const normalizedLabel = normalizeOptionalString(meta.label);
|
|
12431
|
+
if (conversation === meta.conversation && normalizedLabel === meta.label) {
|
|
12432
|
+
return meta;
|
|
12433
|
+
}
|
|
12434
|
+
const rest = { ...meta };
|
|
12435
|
+
delete rest.label;
|
|
12436
|
+
delete rest.conversation;
|
|
12437
|
+
return {
|
|
12438
|
+
...rest,
|
|
12439
|
+
...normalizedLabel ? { label: normalizedLabel } : {},
|
|
12440
|
+
...conversation !== undefined ? { conversation } : {}
|
|
12441
|
+
};
|
|
12442
|
+
}
|
|
12443
|
+
function isPrComment(raw) {
|
|
12444
|
+
if (!isRecord(raw))
|
|
12445
|
+
return false;
|
|
12446
|
+
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");
|
|
12447
|
+
}
|
|
12448
|
+
function isCiCheck(raw) {
|
|
12449
|
+
if (!isRecord(raw))
|
|
12450
|
+
return false;
|
|
12451
|
+
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");
|
|
12452
|
+
}
|
|
12453
|
+
function isPrEntry(raw) {
|
|
12454
|
+
if (!isRecord(raw))
|
|
12455
|
+
return false;
|
|
12456
|
+
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));
|
|
12457
|
+
}
|
|
12458
|
+
async function readWorktreePrs(gitDir) {
|
|
12459
|
+
const { prsPath } = getWorktreeStoragePaths(gitDir);
|
|
12460
|
+
try {
|
|
12461
|
+
const raw = await Bun.file(prsPath).json();
|
|
12462
|
+
return Array.isArray(raw) && raw.every((entry) => isPrEntry(entry)) ? raw : [];
|
|
12463
|
+
} catch {
|
|
12464
|
+
return [];
|
|
12465
|
+
}
|
|
12466
|
+
}
|
|
12467
|
+
async function writeWorktreePrs(gitDir, prs) {
|
|
12468
|
+
const { prsPath } = await ensureWorktreeStorageDirs(gitDir);
|
|
12469
|
+
await Bun.write(prsPath, JSON.stringify(prs, null, 2) + `
|
|
12470
|
+
`);
|
|
12471
|
+
}
|
|
12472
|
+
|
|
12196
12473
|
// backend/src/adapters/claude-cli.ts
|
|
12197
12474
|
import { readdir, stat } from "fs/promises";
|
|
12198
|
-
import { basename, join } from "path";
|
|
12199
|
-
function
|
|
12475
|
+
import { basename, join as join2 } from "path";
|
|
12476
|
+
function isRecord2(raw) {
|
|
12200
12477
|
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
12201
12478
|
}
|
|
12202
12479
|
function readString(raw) {
|
|
@@ -12221,7 +12498,7 @@ function extractToolResultText(content) {
|
|
|
12221
12498
|
if (!Array.isArray(content))
|
|
12222
12499
|
return truncate(compactJson(content));
|
|
12223
12500
|
const text = content.map((entry) => {
|
|
12224
|
-
if (!
|
|
12501
|
+
if (!isRecord2(entry))
|
|
12225
12502
|
return "";
|
|
12226
12503
|
if (entry.type === "text" && typeof entry.text === "string")
|
|
12227
12504
|
return entry.text;
|
|
@@ -12230,17 +12507,17 @@ function extractToolResultText(content) {
|
|
|
12230
12507
|
return truncate(text);
|
|
12231
12508
|
}
|
|
12232
12509
|
function isTopLevelClaudeUserPrompt(raw) {
|
|
12233
|
-
if (raw.type !== "user" || !
|
|
12510
|
+
if (raw.type !== "user" || !isRecord2(raw.message))
|
|
12234
12511
|
return false;
|
|
12235
12512
|
return raw.message.role === "user" && typeof raw.message.content === "string" && typeof raw.uuid === "string" && raw.message.content.trim().length > 0;
|
|
12236
12513
|
}
|
|
12237
12514
|
function isClaudeUserToolResultRecord(raw) {
|
|
12238
|
-
if (raw.type !== "user" || !
|
|
12515
|
+
if (raw.type !== "user" || !isRecord2(raw.message))
|
|
12239
12516
|
return false;
|
|
12240
12517
|
return raw.message.role === "user" && Array.isArray(raw.message.content) && typeof raw.uuid === "string";
|
|
12241
12518
|
}
|
|
12242
12519
|
function isClaudeAssistantRecord(raw) {
|
|
12243
|
-
if (raw.type !== "assistant" || !
|
|
12520
|
+
if (raw.type !== "assistant" || !isRecord2(raw.message))
|
|
12244
12521
|
return false;
|
|
12245
12522
|
return raw.message.role === "assistant" && typeof raw.uuid === "string";
|
|
12246
12523
|
}
|
|
@@ -12252,19 +12529,19 @@ function readClaudeProjectsRoot() {
|
|
|
12252
12529
|
if (!home) {
|
|
12253
12530
|
throw new Error("HOME is required to resolve Claude sessions");
|
|
12254
12531
|
}
|
|
12255
|
-
return
|
|
12532
|
+
return join2(home, ".claude", "projects");
|
|
12256
12533
|
}
|
|
12257
12534
|
async function listJsonlFiles(dir) {
|
|
12258
12535
|
try {
|
|
12259
12536
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
12260
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) =>
|
|
12537
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join2(dir, entry.name));
|
|
12261
12538
|
} catch {
|
|
12262
12539
|
return [];
|
|
12263
12540
|
}
|
|
12264
12541
|
}
|
|
12265
12542
|
async function findClaudeSessionPath(sessionId, cwd) {
|
|
12266
12543
|
const projectsRoot = readClaudeProjectsRoot();
|
|
12267
|
-
const primaryPath =
|
|
12544
|
+
const primaryPath = join2(projectsRoot, encodeClaudeProjectDir(cwd), `${sessionId}.jsonl`);
|
|
12268
12545
|
try {
|
|
12269
12546
|
await stat(primaryPath);
|
|
12270
12547
|
return primaryPath;
|
|
@@ -12273,7 +12550,7 @@ async function findClaudeSessionPath(sessionId, cwd) {
|
|
|
12273
12550
|
for (const entry of projectDirs) {
|
|
12274
12551
|
if (!entry.isDirectory())
|
|
12275
12552
|
continue;
|
|
12276
|
-
const candidate =
|
|
12553
|
+
const candidate = join2(projectsRoot, entry.name, `${sessionId}.jsonl`);
|
|
12277
12554
|
try {
|
|
12278
12555
|
await stat(candidate);
|
|
12279
12556
|
return candidate;
|
|
@@ -12331,7 +12608,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12331
12608
|
continue;
|
|
12332
12609
|
if (isClaudeUserToolResultRecord(record)) {
|
|
12333
12610
|
for (const entry of record.message.content) {
|
|
12334
|
-
if (!
|
|
12611
|
+
if (!isRecord2(entry) || entry.type !== "tool_result")
|
|
12335
12612
|
continue;
|
|
12336
12613
|
const text = extractToolResultText(entry.content);
|
|
12337
12614
|
if (text.length === 0)
|
|
@@ -12352,7 +12629,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12352
12629
|
if (!Array.isArray(record.message.content))
|
|
12353
12630
|
continue;
|
|
12354
12631
|
for (const block of record.message.content) {
|
|
12355
|
-
if (!
|
|
12632
|
+
if (!isRecord2(block))
|
|
12356
12633
|
continue;
|
|
12357
12634
|
if (block.type === "text" && typeof block.text === "string") {
|
|
12358
12635
|
const text = block.text.trim();
|
|
@@ -12398,7 +12675,7 @@ function buildClaudeSessionFromText(input) {
|
|
|
12398
12675
|
class ClaudeCliClient {
|
|
12399
12676
|
async listSessions(cwd) {
|
|
12400
12677
|
const projectsRoot = readClaudeProjectsRoot();
|
|
12401
|
-
const primaryDir =
|
|
12678
|
+
const primaryDir = join2(projectsRoot, encodeClaudeProjectDir(cwd));
|
|
12402
12679
|
const primaryFiles = await listJsonlFiles(primaryDir);
|
|
12403
12680
|
if (primaryFiles.length > 0) {
|
|
12404
12681
|
return await this.summarizeSessionFiles(primaryFiles, cwd);
|
|
@@ -12408,7 +12685,7 @@ class ClaudeCliClient {
|
|
|
12408
12685
|
for (const entry of projectDirs) {
|
|
12409
12686
|
if (!entry.isDirectory())
|
|
12410
12687
|
continue;
|
|
12411
|
-
const files = await listJsonlFiles(
|
|
12688
|
+
const files = await listJsonlFiles(join2(projectsRoot, entry.name));
|
|
12412
12689
|
for (const filePath of files) {
|
|
12413
12690
|
const session = await this.readSessionFile(filePath);
|
|
12414
12691
|
if (session?.cwd === cwd) {
|
|
@@ -12547,15 +12824,15 @@ class ClaudeCliClient {
|
|
|
12547
12824
|
log.warn(`[agents] failed to parse Claude stream line: ${line.slice(0, 120)}`);
|
|
12548
12825
|
return;
|
|
12549
12826
|
}
|
|
12550
|
-
if (!
|
|
12827
|
+
if (!isRecord2(parsed))
|
|
12551
12828
|
return;
|
|
12552
12829
|
const sessionId = readString(parsed.session_id);
|
|
12553
12830
|
if (sessionId) {
|
|
12554
12831
|
resolveSessionId(sessionId);
|
|
12555
12832
|
}
|
|
12556
|
-
if (parsed.type === "stream_event" &&
|
|
12833
|
+
if (parsed.type === "stream_event" && isRecord2(parsed.event)) {
|
|
12557
12834
|
const event = parsed.event;
|
|
12558
|
-
if (event.type === "content_block_delta" &&
|
|
12835
|
+
if (event.type === "content_block_delta" && isRecord2(event.delta) && event.delta.type === "text_delta") {
|
|
12559
12836
|
const delta = readString(event.delta.text);
|
|
12560
12837
|
if (delta) {
|
|
12561
12838
|
callbacks.onAssistantDelta?.(delta);
|
|
@@ -12581,7 +12858,7 @@ class ClaudeCliClient {
|
|
|
12581
12858
|
}
|
|
12582
12859
|
|
|
12583
12860
|
// backend/src/lib/type-guards.ts
|
|
12584
|
-
function
|
|
12861
|
+
function isRecord3(raw) {
|
|
12585
12862
|
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
12586
12863
|
}
|
|
12587
12864
|
function isStringArray(raw) {
|
|
@@ -12591,6 +12868,14 @@ function isStringArray(raw) {
|
|
|
12591
12868
|
// backend/src/adapters/codex-app-server.ts
|
|
12592
12869
|
var CodexAppServerApprovalPolicySchema = exports_external.enum(["untrusted", "on-failure", "on-request", "never"]);
|
|
12593
12870
|
var UnknownValueSchema = exports_external.custom(() => true);
|
|
12871
|
+
var JsonValueSchema = exports_external.union([
|
|
12872
|
+
exports_external.string(),
|
|
12873
|
+
exports_external.number(),
|
|
12874
|
+
exports_external.boolean(),
|
|
12875
|
+
exports_external.null(),
|
|
12876
|
+
exports_external.array(UnknownValueSchema),
|
|
12877
|
+
exports_external.record(UnknownValueSchema)
|
|
12878
|
+
]);
|
|
12594
12879
|
var CodexAppServerContentItemSchema = exports_external.object({
|
|
12595
12880
|
type: exports_external.string(),
|
|
12596
12881
|
text: exports_external.string().optional()
|
|
@@ -12603,26 +12888,121 @@ var CodexAppServerUserMessageItemSchema = exports_external.object({
|
|
|
12603
12888
|
var CodexAppServerAgentMessageItemSchema = exports_external.object({
|
|
12604
12889
|
type: exports_external.literal("agentMessage"),
|
|
12605
12890
|
id: exports_external.string(),
|
|
12606
|
-
text: exports_external.string(),
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
text: value.text,
|
|
12613
|
-
phase: value.phase,
|
|
12614
|
-
memoryCitation: value.memoryCitation
|
|
12615
|
-
}));
|
|
12616
|
-
var CodexAppServerGenericItemSchema = exports_external.object({
|
|
12891
|
+
text: exports_external.string().optional(),
|
|
12892
|
+
message: exports_external.string().optional(),
|
|
12893
|
+
phase: exports_external.string().nullable().optional(),
|
|
12894
|
+
memoryCitation: UnknownValueSchema.optional()
|
|
12895
|
+
});
|
|
12896
|
+
var CodexAppServerCommandActionSchema = exports_external.object({
|
|
12617
12897
|
type: exports_external.string(),
|
|
12618
|
-
|
|
12898
|
+
command: exports_external.string().optional(),
|
|
12899
|
+
path: exports_external.string().nullable().optional()
|
|
12619
12900
|
});
|
|
12620
|
-
var
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12901
|
+
var CodexAppServerCommandExecutionItemSchema = exports_external.object({
|
|
12902
|
+
type: exports_external.literal("commandExecution"),
|
|
12903
|
+
id: exports_external.string(),
|
|
12904
|
+
command: exports_external.string(),
|
|
12905
|
+
cwd: exports_external.string().nullable(),
|
|
12906
|
+
status: exports_external.enum(["inProgress", "completed", "failed", "declined"]),
|
|
12907
|
+
commandActions: exports_external.array(CodexAppServerCommandActionSchema).default([]),
|
|
12908
|
+
aggregatedOutput: exports_external.string().nullable(),
|
|
12909
|
+
exitCode: exports_external.number().nullable(),
|
|
12910
|
+
durationMs: exports_external.number().nullable()
|
|
12911
|
+
});
|
|
12912
|
+
var CodexAppServerPatchChangeKindSchema = exports_external.union([
|
|
12913
|
+
exports_external.object({ type: exports_external.literal("add") }),
|
|
12914
|
+
exports_external.object({ type: exports_external.literal("delete") }),
|
|
12915
|
+
exports_external.object({ type: exports_external.literal("update"), move_path: exports_external.string().nullable() })
|
|
12624
12916
|
]);
|
|
12625
|
-
var
|
|
12917
|
+
var CodexAppServerFileUpdateChangeSchema = exports_external.object({
|
|
12918
|
+
path: exports_external.string(),
|
|
12919
|
+
kind: CodexAppServerPatchChangeKindSchema,
|
|
12920
|
+
diff: exports_external.string()
|
|
12921
|
+
});
|
|
12922
|
+
var CodexAppServerFileChangeItemSchema = exports_external.object({
|
|
12923
|
+
type: exports_external.literal("fileChange"),
|
|
12924
|
+
id: exports_external.string(),
|
|
12925
|
+
changes: exports_external.array(CodexAppServerFileUpdateChangeSchema),
|
|
12926
|
+
status: exports_external.enum(["inProgress", "completed", "failed", "declined"])
|
|
12927
|
+
});
|
|
12928
|
+
var CodexAppServerMcpToolCallResultSchema = exports_external.object({
|
|
12929
|
+
content: exports_external.array(JsonValueSchema),
|
|
12930
|
+
structuredContent: JsonValueSchema,
|
|
12931
|
+
_meta: JsonValueSchema
|
|
12932
|
+
});
|
|
12933
|
+
var CodexAppServerMcpToolCallErrorSchema = exports_external.object({
|
|
12934
|
+
message: exports_external.string()
|
|
12935
|
+
});
|
|
12936
|
+
var CodexAppServerMcpToolCallItemSchema = exports_external.object({
|
|
12937
|
+
type: exports_external.literal("mcpToolCall"),
|
|
12938
|
+
id: exports_external.string(),
|
|
12939
|
+
server: exports_external.string(),
|
|
12940
|
+
tool: exports_external.string(),
|
|
12941
|
+
status: exports_external.enum(["inProgress", "completed", "failed"]),
|
|
12942
|
+
arguments: JsonValueSchema,
|
|
12943
|
+
mcpAppResourceUri: exports_external.string().optional(),
|
|
12944
|
+
pluginId: exports_external.string().nullable(),
|
|
12945
|
+
result: CodexAppServerMcpToolCallResultSchema.nullable(),
|
|
12946
|
+
error: CodexAppServerMcpToolCallErrorSchema.nullable(),
|
|
12947
|
+
durationMs: exports_external.number().nullable()
|
|
12948
|
+
});
|
|
12949
|
+
var CodexAppServerDynamicToolCallContentItemSchema = exports_external.union([
|
|
12950
|
+
exports_external.object({ type: exports_external.literal("inputText"), text: exports_external.string() }),
|
|
12951
|
+
exports_external.object({ type: exports_external.literal("inputImage"), imageUrl: exports_external.string() })
|
|
12952
|
+
]);
|
|
12953
|
+
var CodexAppServerDynamicToolCallItemSchema = exports_external.object({
|
|
12954
|
+
type: exports_external.literal("dynamicToolCall"),
|
|
12955
|
+
id: exports_external.string(),
|
|
12956
|
+
namespace: exports_external.string().nullable(),
|
|
12957
|
+
tool: exports_external.string(),
|
|
12958
|
+
arguments: JsonValueSchema,
|
|
12959
|
+
status: exports_external.enum(["inProgress", "completed", "failed"]),
|
|
12960
|
+
contentItems: exports_external.array(CodexAppServerDynamicToolCallContentItemSchema).nullable(),
|
|
12961
|
+
success: exports_external.boolean().nullable(),
|
|
12962
|
+
durationMs: exports_external.number().nullable()
|
|
12963
|
+
});
|
|
12964
|
+
var CodexAppServerWebSearchActionSchema = exports_external.union([
|
|
12965
|
+
exports_external.object({ type: exports_external.literal("search"), query: exports_external.string().nullable(), queries: exports_external.array(exports_external.string()).nullable() }),
|
|
12966
|
+
exports_external.object({ type: exports_external.literal("openPage"), url: exports_external.string().nullable() }),
|
|
12967
|
+
exports_external.object({ type: exports_external.literal("findInPage"), url: exports_external.string().nullable(), pattern: exports_external.string().nullable() }),
|
|
12968
|
+
exports_external.object({ type: exports_external.literal("other") })
|
|
12969
|
+
]);
|
|
12970
|
+
var CodexAppServerWebSearchItemSchema = exports_external.object({
|
|
12971
|
+
type: exports_external.literal("webSearch"),
|
|
12972
|
+
id: exports_external.string(),
|
|
12973
|
+
query: exports_external.string(),
|
|
12974
|
+
action: CodexAppServerWebSearchActionSchema.nullable()
|
|
12975
|
+
});
|
|
12976
|
+
var CodexAppServerIgnoredItemSchema = exports_external.object({
|
|
12977
|
+
type: exports_external.enum([
|
|
12978
|
+
"hookPrompt",
|
|
12979
|
+
"plan",
|
|
12980
|
+
"reasoning",
|
|
12981
|
+
"collabAgentToolCall",
|
|
12982
|
+
"imageView",
|
|
12983
|
+
"imageGeneration",
|
|
12984
|
+
"enteredReviewMode",
|
|
12985
|
+
"exitedReviewMode",
|
|
12986
|
+
"contextCompaction"
|
|
12987
|
+
]),
|
|
12988
|
+
id: exports_external.string()
|
|
12989
|
+
});
|
|
12990
|
+
var CodexAppServerGenericItemSchema = exports_external.object({
|
|
12991
|
+
type: exports_external.string(),
|
|
12992
|
+
id: exports_external.string()
|
|
12993
|
+
});
|
|
12994
|
+
var CodexAppServerThreadItemSchema = exports_external.union([
|
|
12995
|
+
CodexAppServerUserMessageItemSchema,
|
|
12996
|
+
CodexAppServerAgentMessageItemSchema,
|
|
12997
|
+
CodexAppServerCommandExecutionItemSchema,
|
|
12998
|
+
CodexAppServerFileChangeItemSchema,
|
|
12999
|
+
CodexAppServerMcpToolCallItemSchema,
|
|
13000
|
+
CodexAppServerDynamicToolCallItemSchema,
|
|
13001
|
+
CodexAppServerWebSearchItemSchema,
|
|
13002
|
+
CodexAppServerIgnoredItemSchema,
|
|
13003
|
+
CodexAppServerGenericItemSchema
|
|
13004
|
+
]);
|
|
13005
|
+
var CodexAppServerTurnSchema = exports_external.object({
|
|
12626
13006
|
id: exports_external.string(),
|
|
12627
13007
|
items: exports_external.array(CodexAppServerThreadItemSchema),
|
|
12628
13008
|
status: exports_external.string(),
|
|
@@ -12709,7 +13089,31 @@ var CodexAppServerInitializeResponseSchema = exports_external.object({
|
|
|
12709
13089
|
platformFamily: exports_external.string(),
|
|
12710
13090
|
platformOs: exports_external.string()
|
|
12711
13091
|
});
|
|
12712
|
-
|
|
13092
|
+
function readCodexAppServerStdoutLines(input) {
|
|
13093
|
+
let buffer = input.buffer + (input.chunk ? input.decoder.decode(input.chunk, { stream: true }) : input.decoder.decode());
|
|
13094
|
+
const lines = [];
|
|
13095
|
+
while (true) {
|
|
13096
|
+
const newlineIndex = buffer.indexOf(`
|
|
13097
|
+
`);
|
|
13098
|
+
if (newlineIndex === -1)
|
|
13099
|
+
break;
|
|
13100
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
13101
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
13102
|
+
if (line.length > 0)
|
|
13103
|
+
lines.push(line);
|
|
13104
|
+
}
|
|
13105
|
+
if (!input.chunk) {
|
|
13106
|
+
const finalLine = buffer.trim();
|
|
13107
|
+
buffer = "";
|
|
13108
|
+
if (finalLine.length > 0)
|
|
13109
|
+
lines.push(finalLine);
|
|
13110
|
+
}
|
|
13111
|
+
return { buffer, lines };
|
|
13112
|
+
}
|
|
13113
|
+
function parseCodexAppServerThreadItem(raw) {
|
|
13114
|
+
const parsed = CodexAppServerThreadItemSchema.safeParse(raw);
|
|
13115
|
+
return parsed.success ? parsed.data : null;
|
|
13116
|
+
}
|
|
12713
13117
|
class CodexAppServerRequestError extends Error {
|
|
12714
13118
|
code;
|
|
12715
13119
|
data;
|
|
@@ -12723,7 +13127,6 @@ class CodexAppServerRequestError extends Error {
|
|
|
12723
13127
|
class CodexAppServerClient {
|
|
12724
13128
|
opts;
|
|
12725
13129
|
encoder = new TextEncoder;
|
|
12726
|
-
decoder = new TextDecoder;
|
|
12727
13130
|
listeners = new Set;
|
|
12728
13131
|
pending = new Map;
|
|
12729
13132
|
nextId = 1;
|
|
@@ -12810,25 +13213,30 @@ class CodexAppServerClient {
|
|
|
12810
13213
|
startStdoutLoop(proc) {
|
|
12811
13214
|
(async () => {
|
|
12812
13215
|
const reader = proc.stdout.getReader();
|
|
13216
|
+
const decoder = new TextDecoder;
|
|
12813
13217
|
let buffer = "";
|
|
12814
13218
|
try {
|
|
12815
13219
|
while (true) {
|
|
12816
13220
|
const { done, value } = await reader.read();
|
|
12817
13221
|
if (done)
|
|
12818
13222
|
break;
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
12827
|
-
if (line.length === 0)
|
|
12828
|
-
continue;
|
|
13223
|
+
const decoded2 = readCodexAppServerStdoutLines({
|
|
13224
|
+
decoder,
|
|
13225
|
+
buffer,
|
|
13226
|
+
chunk: value
|
|
13227
|
+
});
|
|
13228
|
+
buffer = decoded2.buffer;
|
|
13229
|
+
for (const line of decoded2.lines) {
|
|
12829
13230
|
this.handleStdoutLine(line);
|
|
12830
13231
|
}
|
|
12831
13232
|
}
|
|
13233
|
+
const decoded = readCodexAppServerStdoutLines({
|
|
13234
|
+
decoder,
|
|
13235
|
+
buffer
|
|
13236
|
+
});
|
|
13237
|
+
for (const line of decoded.lines) {
|
|
13238
|
+
this.handleStdoutLine(line);
|
|
13239
|
+
}
|
|
12832
13240
|
} catch (error) {
|
|
12833
13241
|
if (this.proc === proc) {
|
|
12834
13242
|
log.error("[agents] codex app-server stdout reader failed", error);
|
|
@@ -12839,12 +13247,13 @@ class CodexAppServerClient {
|
|
|
12839
13247
|
startStderrLoop(proc) {
|
|
12840
13248
|
(async () => {
|
|
12841
13249
|
const reader = proc.stderr.getReader();
|
|
13250
|
+
const decoder = new TextDecoder;
|
|
12842
13251
|
try {
|
|
12843
13252
|
while (true) {
|
|
12844
13253
|
const { done, value } = await reader.read();
|
|
12845
13254
|
if (done)
|
|
12846
13255
|
break;
|
|
12847
|
-
const chunk =
|
|
13256
|
+
const chunk = decoder.decode(value, { stream: true }).trim();
|
|
12848
13257
|
if (chunk.length > 0) {
|
|
12849
13258
|
log.debug(`[agents] codex app-server stderr: ${chunk}`);
|
|
12850
13259
|
}
|
|
@@ -12864,7 +13273,7 @@ class CodexAppServerClient {
|
|
|
12864
13273
|
log.error(`[agents] failed to parse codex app-server line: ${line}`, error);
|
|
12865
13274
|
return;
|
|
12866
13275
|
}
|
|
12867
|
-
if (!
|
|
13276
|
+
if (!isRecord3(parsed)) {
|
|
12868
13277
|
log.warn(`[agents] unexpected codex app-server payload: ${line}`);
|
|
12869
13278
|
return;
|
|
12870
13279
|
}
|
|
@@ -12952,7 +13361,7 @@ class CodexAppServerClient {
|
|
|
12952
13361
|
this.readyPromise = null;
|
|
12953
13362
|
}
|
|
12954
13363
|
readResponseError(raw) {
|
|
12955
|
-
if (!
|
|
13364
|
+
if (!isRecord3(raw.error))
|
|
12956
13365
|
return null;
|
|
12957
13366
|
return typeof raw.error.code === "number" && typeof raw.error.message === "string" ? {
|
|
12958
13367
|
code: raw.error.code,
|
|
@@ -12964,7 +13373,7 @@ class CodexAppServerClient {
|
|
|
12964
13373
|
|
|
12965
13374
|
// backend/src/adapters/config.ts
|
|
12966
13375
|
import { readFileSync } from "fs";
|
|
12967
|
-
import { dirname as dirname2, join as
|
|
13376
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
12968
13377
|
|
|
12969
13378
|
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
|
|
12970
13379
|
var composer = require_composer();
|
|
@@ -13072,7 +13481,7 @@ function cloneProfiles(profiles) {
|
|
|
13072
13481
|
function defaultProfiles() {
|
|
13073
13482
|
return { default: cloneProfile(DEFAULT_CONFIG.profiles.default) };
|
|
13074
13483
|
}
|
|
13075
|
-
function
|
|
13484
|
+
function isRecord4(value) {
|
|
13076
13485
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13077
13486
|
}
|
|
13078
13487
|
function isStringArray2(value) {
|
|
@@ -13088,7 +13497,7 @@ function parsePanes(raw) {
|
|
|
13088
13497
|
return panes.length > 0 ? panes : clonePanes(DEFAULT_PANES);
|
|
13089
13498
|
}
|
|
13090
13499
|
function parsePane(raw, index) {
|
|
13091
|
-
if (!
|
|
13500
|
+
if (!isRecord4(raw))
|
|
13092
13501
|
return null;
|
|
13093
13502
|
if (raw.kind !== "agent" && raw.kind !== "shell" && raw.kind !== "command")
|
|
13094
13503
|
return null;
|
|
@@ -13117,7 +13526,7 @@ function parsePane(raw, index) {
|
|
|
13117
13526
|
function parseMounts(raw) {
|
|
13118
13527
|
if (!Array.isArray(raw))
|
|
13119
13528
|
return;
|
|
13120
|
-
const mounts = raw.filter(
|
|
13529
|
+
const mounts = raw.filter(isRecord4).filter((entry) => typeof entry.hostPath === "string" && entry.hostPath.length > 0).map((entry) => ({
|
|
13121
13530
|
hostPath: entry.hostPath,
|
|
13122
13531
|
...typeof entry.guestPath === "string" && entry.guestPath.length > 0 ? { guestPath: entry.guestPath } : {},
|
|
13123
13532
|
...typeof entry.writable === "boolean" ? { writable: entry.writable } : {}
|
|
@@ -13125,7 +13534,7 @@ function parseMounts(raw) {
|
|
|
13125
13534
|
return mounts.length > 0 ? mounts : undefined;
|
|
13126
13535
|
}
|
|
13127
13536
|
function parseProfile(raw, fallbackRuntime) {
|
|
13128
|
-
if (!
|
|
13537
|
+
if (!isRecord4(raw)) {
|
|
13129
13538
|
return {
|
|
13130
13539
|
runtime: fallbackRuntime,
|
|
13131
13540
|
envPassthrough: [],
|
|
@@ -13148,7 +13557,7 @@ function parseProfile(raw, fallbackRuntime) {
|
|
|
13148
13557
|
};
|
|
13149
13558
|
}
|
|
13150
13559
|
function parseProfiles(raw, includeDefaultProfile) {
|
|
13151
|
-
if (!
|
|
13560
|
+
if (!isRecord4(raw))
|
|
13152
13561
|
return includeDefaultProfile ? defaultProfiles() : {};
|
|
13153
13562
|
const profiles = Object.entries(raw).reduce((acc, [name, value]) => {
|
|
13154
13563
|
const fallbackRuntime = name === "sandbox" ? "docker" : "host";
|
|
@@ -13167,7 +13576,7 @@ function cloneAgents(agents) {
|
|
|
13167
13576
|
return Object.fromEntries(Object.entries(agents).map(([id, agent]) => [id, cloneAgentConfig(agent)]));
|
|
13168
13577
|
}
|
|
13169
13578
|
function parseCustomAgent(raw) {
|
|
13170
|
-
if (!
|
|
13579
|
+
if (!isRecord4(raw))
|
|
13171
13580
|
return null;
|
|
13172
13581
|
if (typeof raw.label !== "string" || !raw.label.trim())
|
|
13173
13582
|
return null;
|
|
@@ -13180,7 +13589,7 @@ function parseCustomAgent(raw) {
|
|
|
13180
13589
|
};
|
|
13181
13590
|
}
|
|
13182
13591
|
function parseCustomAgents(raw) {
|
|
13183
|
-
if (!
|
|
13592
|
+
if (!isRecord4(raw))
|
|
13184
13593
|
return {};
|
|
13185
13594
|
return Object.entries(raw).reduce((acc, [id, value]) => {
|
|
13186
13595
|
if (!id.trim())
|
|
@@ -13195,7 +13604,7 @@ function parseCustomAgents(raw) {
|
|
|
13195
13604
|
function parseServices(raw) {
|
|
13196
13605
|
if (!Array.isArray(raw))
|
|
13197
13606
|
return [];
|
|
13198
|
-
return raw.filter(
|
|
13607
|
+
return raw.filter(isRecord4).filter((entry) => typeof entry.name === "string" && typeof entry.portEnv === "string").map((entry) => ({
|
|
13199
13608
|
name: entry.name,
|
|
13200
13609
|
portEnv: entry.portEnv,
|
|
13201
13610
|
...typeof entry.portStart === "number" && Number.isFinite(entry.portStart) ? { portStart: entry.portStart } : {},
|
|
@@ -13204,7 +13613,7 @@ function parseServices(raw) {
|
|
|
13204
13613
|
}));
|
|
13205
13614
|
}
|
|
13206
13615
|
function parseStartupEnvs(raw) {
|
|
13207
|
-
if (!
|
|
13616
|
+
if (!isRecord4(raw))
|
|
13208
13617
|
return {};
|
|
13209
13618
|
const startupEnvs = {};
|
|
13210
13619
|
for (const [key, value] of Object.entries(raw)) {
|
|
@@ -13217,7 +13626,7 @@ function parseStartupEnvs(raw) {
|
|
|
13217
13626
|
return startupEnvs;
|
|
13218
13627
|
}
|
|
13219
13628
|
function parseLifecycleHooks(raw) {
|
|
13220
|
-
if (!
|
|
13629
|
+
if (!isRecord4(raw))
|
|
13221
13630
|
return {};
|
|
13222
13631
|
const hooks = {};
|
|
13223
13632
|
if (typeof raw.postCreate === "string" && raw.postCreate.trim()) {
|
|
@@ -13229,13 +13638,13 @@ function parseLifecycleHooks(raw) {
|
|
|
13229
13638
|
return hooks;
|
|
13230
13639
|
}
|
|
13231
13640
|
function parseOneshot(raw) {
|
|
13232
|
-
if (!
|
|
13641
|
+
if (!isRecord4(raw))
|
|
13233
13642
|
return { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() };
|
|
13234
13643
|
const systemPrompt = typeof raw.systemPrompt === "string" && raw.systemPrompt.trim() ? raw.systemPrompt.trim() : DEFAULT_ONESHOT_SYSTEM_PROMPT();
|
|
13235
13644
|
return { systemPrompt };
|
|
13236
13645
|
}
|
|
13237
13646
|
function parseAutoName(raw) {
|
|
13238
|
-
if (!
|
|
13647
|
+
if (!isRecord4(raw))
|
|
13239
13648
|
return null;
|
|
13240
13649
|
const provider = raw.provider;
|
|
13241
13650
|
if (provider !== "claude" && provider !== "codex")
|
|
@@ -13247,7 +13656,7 @@ function parseAutoName(raw) {
|
|
|
13247
13656
|
};
|
|
13248
13657
|
}
|
|
13249
13658
|
function parseAutoPull(raw) {
|
|
13250
|
-
if (!
|
|
13659
|
+
if (!isRecord4(raw))
|
|
13251
13660
|
return DEFAULT_CONFIG.workspace.autoPull;
|
|
13252
13661
|
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : false;
|
|
13253
13662
|
const interval = typeof raw.intervalSeconds === "number" && Number.isFinite(raw.intervalSeconds) && raw.intervalSeconds >= 30 ? raw.intervalSeconds : 300;
|
|
@@ -13256,7 +13665,7 @@ function parseAutoPull(raw) {
|
|
|
13256
13665
|
function parseLinkedRepos(raw) {
|
|
13257
13666
|
if (!Array.isArray(raw))
|
|
13258
13667
|
return [];
|
|
13259
|
-
return raw.filter(
|
|
13668
|
+
return raw.filter(isRecord4).filter((entry) => typeof entry.repo === "string").map((entry) => ({
|
|
13260
13669
|
repo: entry.repo,
|
|
13261
13670
|
alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo",
|
|
13262
13671
|
...typeof entry.dir === "string" && entry.dir.trim() ? { dir: entry.dir.trim() } : {}
|
|
@@ -13271,23 +13680,23 @@ function getDefaultProfileName(config) {
|
|
|
13271
13680
|
return Object.keys(config.profiles)[0] ?? "default";
|
|
13272
13681
|
}
|
|
13273
13682
|
function readConfigFile(root) {
|
|
13274
|
-
return readFileSync(
|
|
13683
|
+
return readFileSync(join3(root, ".webmux.yaml"), "utf8");
|
|
13275
13684
|
}
|
|
13276
13685
|
function readLocalConfigFile(root) {
|
|
13277
|
-
return readFileSync(
|
|
13686
|
+
return readFileSync(join3(root, ".webmux.local.yaml"), "utf8");
|
|
13278
13687
|
}
|
|
13279
13688
|
function parseConfigDocument(text) {
|
|
13280
13689
|
const parsed = $parse(text);
|
|
13281
|
-
return
|
|
13690
|
+
return isRecord4(parsed) ? parsed : {};
|
|
13282
13691
|
}
|
|
13283
13692
|
function parseProjectConfig(parsed) {
|
|
13284
13693
|
return {
|
|
13285
13694
|
name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : DEFAULT_CONFIG.name,
|
|
13286
13695
|
workspace: {
|
|
13287
|
-
mainBranch:
|
|
13288
|
-
worktreeRoot:
|
|
13289
|
-
defaultAgent:
|
|
13290
|
-
autoPull:
|
|
13696
|
+
mainBranch: isRecord4(parsed.workspace) && typeof parsed.workspace.mainBranch === "string" ? parsed.workspace.mainBranch : DEFAULT_CONFIG.workspace.mainBranch,
|
|
13697
|
+
worktreeRoot: isRecord4(parsed.workspace) && typeof parsed.workspace.worktreeRoot === "string" ? parsed.workspace.worktreeRoot : DEFAULT_CONFIG.workspace.worktreeRoot,
|
|
13698
|
+
defaultAgent: isRecord4(parsed.workspace) ? parseAgentKind(parsed.workspace.defaultAgent) : DEFAULT_CONFIG.workspace.defaultAgent,
|
|
13699
|
+
autoPull: isRecord4(parsed.workspace) ? parseAutoPull(parsed.workspace.autoPull) : DEFAULT_CONFIG.workspace.autoPull
|
|
13291
13700
|
},
|
|
13292
13701
|
profiles: parseProfiles(parsed.profiles, true),
|
|
13293
13702
|
agents: {},
|
|
@@ -13295,8 +13704,8 @@ function parseProjectConfig(parsed) {
|
|
|
13295
13704
|
startupEnvs: parseStartupEnvs(parsed.startupEnvs),
|
|
13296
13705
|
integrations: {
|
|
13297
13706
|
github: {
|
|
13298
|
-
linkedRepos:
|
|
13299
|
-
autoRemoveOnMerge:
|
|
13707
|
+
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) : [],
|
|
13708
|
+
autoRemoveOnMerge: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) && typeof parsed.integrations.github.autoRemoveOnMerge === "boolean" ? parsed.integrations.github.autoRemoveOnMerge : DEFAULT_CONFIG.integrations.github.autoRemoveOnMerge
|
|
13300
13709
|
},
|
|
13301
13710
|
linear: parseLinearIntegration(parsed)
|
|
13302
13711
|
},
|
|
@@ -13317,7 +13726,7 @@ function parseTeamKeyList(raw) {
|
|
|
13317
13726
|
var warnedLegacyLinearTeamId = false;
|
|
13318
13727
|
function parseLinearIntegration(parsed) {
|
|
13319
13728
|
const defaults = DEFAULT_CONFIG.integrations.linear;
|
|
13320
|
-
const linear =
|
|
13729
|
+
const linear = isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) ? parsed.integrations.linear : null;
|
|
13321
13730
|
if (!linear)
|
|
13322
13731
|
return { ...defaults };
|
|
13323
13732
|
if (typeof linear.teamId === "string" && !warnedLegacyLinearTeamId) {
|
|
@@ -13333,10 +13742,10 @@ function parseLinearIntegration(parsed) {
|
|
|
13333
13742
|
};
|
|
13334
13743
|
}
|
|
13335
13744
|
function parseLocalLinearOverlay(parsed) {
|
|
13336
|
-
if (!
|
|
13745
|
+
if (!isRecord4(parsed.integrations))
|
|
13337
13746
|
return null;
|
|
13338
13747
|
const linear = parsed.integrations.linear;
|
|
13339
|
-
if (!
|
|
13748
|
+
if (!isRecord4(linear))
|
|
13340
13749
|
return null;
|
|
13341
13750
|
const overlay = {};
|
|
13342
13751
|
if (typeof linear.enabled === "boolean")
|
|
@@ -13351,10 +13760,10 @@ function parseLocalLinearOverlay(parsed) {
|
|
|
13351
13760
|
return Object.keys(overlay).length > 0 ? overlay : null;
|
|
13352
13761
|
}
|
|
13353
13762
|
function parseLocalGitHubOverlay(parsed) {
|
|
13354
|
-
if (!
|
|
13763
|
+
if (!isRecord4(parsed.integrations))
|
|
13355
13764
|
return null;
|
|
13356
13765
|
const github = parsed.integrations.github;
|
|
13357
|
-
if (!
|
|
13766
|
+
if (!isRecord4(github))
|
|
13358
13767
|
return null;
|
|
13359
13768
|
const overlay = {};
|
|
13360
13769
|
if (typeof github.autoRemoveOnMerge === "boolean")
|
|
@@ -13362,10 +13771,10 @@ function parseLocalGitHubOverlay(parsed) {
|
|
|
13362
13771
|
return Object.keys(overlay).length > 0 ? overlay : null;
|
|
13363
13772
|
}
|
|
13364
13773
|
function parseLocalAutoPullOverlay(parsed) {
|
|
13365
|
-
if (!
|
|
13774
|
+
if (!isRecord4(parsed.workspace))
|
|
13366
13775
|
return null;
|
|
13367
13776
|
const autoPull = parsed.workspace.autoPull;
|
|
13368
|
-
if (!
|
|
13777
|
+
if (!isRecord4(autoPull))
|
|
13369
13778
|
return null;
|
|
13370
13779
|
const overlay = {};
|
|
13371
13780
|
if (typeof autoPull.enabled === "boolean")
|
|
@@ -13382,7 +13791,7 @@ function loadLocalProjectConfigOverlay(root) {
|
|
|
13382
13791
|
return { worktreeRoot: null, profiles: {}, agents: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
|
|
13383
13792
|
}
|
|
13384
13793
|
const parsed = parseConfigDocument(text);
|
|
13385
|
-
const ws =
|
|
13794
|
+
const ws = isRecord4(parsed.workspace) ? parsed.workspace : null;
|
|
13386
13795
|
return {
|
|
13387
13796
|
worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
|
|
13388
13797
|
profiles: parseProfiles(parsed.profiles, false),
|
|
@@ -13462,7 +13871,7 @@ function loadConfig(dir, options = {}) {
|
|
|
13462
13871
|
};
|
|
13463
13872
|
}
|
|
13464
13873
|
function readLocalConfigDocument(root) {
|
|
13465
|
-
const localPath =
|
|
13874
|
+
const localPath = join3(root, ".webmux.local.yaml");
|
|
13466
13875
|
let existing = {};
|
|
13467
13876
|
try {
|
|
13468
13877
|
const text = readFileSync(localPath, "utf8").trim();
|
|
@@ -13474,8 +13883,8 @@ function readLocalConfigDocument(root) {
|
|
|
13474
13883
|
async function persistLocalLinearConfig(dir, changes) {
|
|
13475
13884
|
const root = projectRoot(dir);
|
|
13476
13885
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13477
|
-
const integrations =
|
|
13478
|
-
const linear =
|
|
13886
|
+
const integrations = isRecord4(existing.integrations) ? { ...existing.integrations } : {};
|
|
13887
|
+
const linear = isRecord4(integrations.linear) ? { ...integrations.linear } : {};
|
|
13479
13888
|
Object.assign(linear, changes);
|
|
13480
13889
|
integrations.linear = linear;
|
|
13481
13890
|
existing.integrations = integrations;
|
|
@@ -13484,8 +13893,8 @@ async function persistLocalLinearConfig(dir, changes) {
|
|
|
13484
13893
|
async function persistLocalGitHubConfig(dir, changes) {
|
|
13485
13894
|
const root = projectRoot(dir);
|
|
13486
13895
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13487
|
-
const integrations =
|
|
13488
|
-
const github =
|
|
13896
|
+
const integrations = isRecord4(existing.integrations) ? { ...existing.integrations } : {};
|
|
13897
|
+
const github = isRecord4(integrations.github) ? { ...integrations.github } : {};
|
|
13489
13898
|
Object.assign(github, changes);
|
|
13490
13899
|
integrations.github = github;
|
|
13491
13900
|
existing.integrations = integrations;
|
|
@@ -13494,7 +13903,7 @@ async function persistLocalGitHubConfig(dir, changes) {
|
|
|
13494
13903
|
async function persistLocalCustomAgent(dir, agentId, agent) {
|
|
13495
13904
|
const root = projectRoot(dir);
|
|
13496
13905
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13497
|
-
const agents =
|
|
13906
|
+
const agents = isRecord4(existing.agents) ? { ...existing.agents } : {};
|
|
13498
13907
|
agents[agentId] = {
|
|
13499
13908
|
label: agent.label,
|
|
13500
13909
|
startCommand: agent.startCommand,
|
|
@@ -13506,7 +13915,7 @@ async function persistLocalCustomAgent(dir, agentId, agent) {
|
|
|
13506
13915
|
async function removeLocalCustomAgent(dir, agentId) {
|
|
13507
13916
|
const root = projectRoot(dir);
|
|
13508
13917
|
const { localPath, existing } = readLocalConfigDocument(root);
|
|
13509
|
-
if (!
|
|
13918
|
+
if (!isRecord4(existing.agents) || !(agentId in existing.agents)) {
|
|
13510
13919
|
return;
|
|
13511
13920
|
}
|
|
13512
13921
|
const agents = { ...existing.agents };
|
|
@@ -13615,12 +14024,6 @@ function hasRecentDashboardActivity() {
|
|
|
13615
14024
|
|
|
13616
14025
|
// backend/src/services/archive-service.ts
|
|
13617
14026
|
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
14027
|
function createArchiveState(entries) {
|
|
13625
14028
|
return {
|
|
13626
14029
|
schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
|
|
@@ -14300,6 +14703,14 @@ function findLinkedGitHubPr(issue) {
|
|
|
14300
14703
|
});
|
|
14301
14704
|
return indexed[0].pr;
|
|
14302
14705
|
}
|
|
14706
|
+
function buildLinearPickupMarkdown(input) {
|
|
14707
|
+
return [
|
|
14708
|
+
`**Webmux pickup \u2014 branch \`${input.branch}\`**`,
|
|
14709
|
+
"",
|
|
14710
|
+
`- Picked up: ${input.pickedUpAt.toISOString()}`
|
|
14711
|
+
].join(`
|
|
14712
|
+
`);
|
|
14713
|
+
}
|
|
14303
14714
|
function buildLinearSummaryMarkdown(input) {
|
|
14304
14715
|
const lines = [
|
|
14305
14716
|
`**Webmux session \u2014 branch \`${input.branch}\`**`,
|
|
@@ -14472,14 +14883,26 @@ async function createLinearIssue(input) {
|
|
|
14472
14883
|
}
|
|
14473
14884
|
|
|
14474
14885
|
// backend/src/services/conversation-export-service.ts
|
|
14886
|
+
var WebmuxConversationAttachmentMessageSchema = AgentsUiConversationMessageSchema.extend({
|
|
14887
|
+
order: exports_external.number().int().nonnegative().optional(),
|
|
14888
|
+
kind: AgentsUiConversationMessageKindSchema.optional()
|
|
14889
|
+
});
|
|
14475
14890
|
var WebmuxConversationAttachmentPayloadSchema = exports_external.object({
|
|
14476
14891
|
webmux: exports_external.literal(1),
|
|
14477
14892
|
branch: exports_external.string(),
|
|
14478
14893
|
baseBranch: exports_external.string().nullable(),
|
|
14479
14894
|
agent: AgentIdSchema.nullable(),
|
|
14480
14895
|
createdAt: exports_external.string(),
|
|
14481
|
-
conversation: exports_external.array(
|
|
14896
|
+
conversation: exports_external.array(WebmuxConversationAttachmentMessageSchema).transform((messages) => messages.map((message, order) => ({
|
|
14897
|
+
...message,
|
|
14898
|
+
order: message.order ?? order,
|
|
14899
|
+
kind: message.kind ?? "text"
|
|
14900
|
+
})))
|
|
14482
14901
|
});
|
|
14902
|
+
function parseWebmuxConversationAttachmentPayload(raw) {
|
|
14903
|
+
const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(raw);
|
|
14904
|
+
return parsed.success ? parsed.data : null;
|
|
14905
|
+
}
|
|
14483
14906
|
var defaultSeedFromLinearDeps = {
|
|
14484
14907
|
fetchIssueWithAttachments,
|
|
14485
14908
|
downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
|
|
@@ -14656,11 +15079,11 @@ async function downloadWebmuxAttachmentDefault(url) {
|
|
|
14656
15079
|
return { ok: false, error: `Asset download failed ${res.status}` };
|
|
14657
15080
|
}
|
|
14658
15081
|
const text = await res.text();
|
|
14659
|
-
const parsed =
|
|
14660
|
-
if (!parsed
|
|
15082
|
+
const parsed = parseWebmuxConversationAttachmentPayload(JSON.parse(text));
|
|
15083
|
+
if (!parsed) {
|
|
14661
15084
|
return { ok: false, error: "Asset is not a webmux conversation payload" };
|
|
14662
15085
|
}
|
|
14663
|
-
return { ok: true, data: parsed
|
|
15086
|
+
return { ok: true, data: parsed };
|
|
14664
15087
|
} catch (err) {
|
|
14665
15088
|
const msg = err instanceof Error ? err.message : String(err);
|
|
14666
15089
|
return { ok: false, error: msg };
|
|
@@ -14674,235 +15097,12 @@ import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
|
14674
15097
|
// backend/src/adapters/agent-runtime.ts
|
|
14675
15098
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
14676
15099
|
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
14677
|
-
|
|
14678
|
-
|
|
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;
|
|
15100
|
+
var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
|
|
15101
|
+
function shellQuote(value) {
|
|
14690
15102
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14691
15103
|
}
|
|
14692
|
-
function
|
|
14693
|
-
|
|
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
|
-
var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
|
|
14901
|
-
function shellQuote(value) {
|
|
14902
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14903
|
-
}
|
|
14904
|
-
function isRecord5(value) {
|
|
14905
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15104
|
+
function isRecord5(value) {
|
|
15105
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14906
15106
|
}
|
|
14907
15107
|
function buildAgentCtlScript() {
|
|
14908
15108
|
return `#!/usr/bin/env python3
|
|
@@ -15472,9 +15672,9 @@ class BunTmuxGateway {
|
|
|
15472
15672
|
|
|
15473
15673
|
// backend/src/domain/policies.ts
|
|
15474
15674
|
var INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
|
|
15475
|
-
var UNSAFE_ENV_KEY_RE = /^[
|
|
15476
|
-
var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]
|
|
15477
|
-
var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]
|
|
15675
|
+
var UNSAFE_ENV_KEY_RE = /^[a-z_][a-z0-9_]*$/i;
|
|
15676
|
+
var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/i;
|
|
15677
|
+
var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/i;
|
|
15478
15678
|
function sanitizeBranchName(raw) {
|
|
15479
15679
|
return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
|
|
15480
15680
|
}
|
|
@@ -15561,7 +15761,8 @@ function buildBuiltInAgentInvocation(input) {
|
|
|
15561
15761
|
const hooksFlag = " --enable hooks";
|
|
15562
15762
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
15563
15763
|
if (input.launchMode === "resume") {
|
|
15564
|
-
|
|
15764
|
+
const resumeTarget = input.resumeConversationId ? ` ${quoteShell(input.resumeConversationId)}` : " --last";
|
|
15765
|
+
return `codex${hooksFlag}${yoloFlag2} resume${resumeTarget}${promptSuffix}`;
|
|
15565
15766
|
}
|
|
15566
15767
|
if (input.systemPrompt) {
|
|
15567
15768
|
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
|
|
@@ -15604,7 +15805,8 @@ function buildAgentInvocation(input) {
|
|
|
15604
15805
|
yolo: input.yolo,
|
|
15605
15806
|
systemPrompt: input.systemPrompt,
|
|
15606
15807
|
prompt: input.prompt,
|
|
15607
|
-
launchMode: input.launchMode
|
|
15808
|
+
launchMode: input.launchMode,
|
|
15809
|
+
resumeConversationId: input.resumeConversationId
|
|
15608
15810
|
});
|
|
15609
15811
|
}
|
|
15610
15812
|
return buildCustomAgentInvocation({
|
|
@@ -16248,6 +16450,17 @@ function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
|
|
|
16248
16450
|
return trimmed;
|
|
16249
16451
|
}
|
|
16250
16452
|
}
|
|
16453
|
+
function resolveCodexResumeConversationId(meta, agent, launchMode) {
|
|
16454
|
+
if (launchMode !== "resume")
|
|
16455
|
+
return;
|
|
16456
|
+
if (meta.agentTerminalStale !== true)
|
|
16457
|
+
return;
|
|
16458
|
+
if (agent.kind !== "builtin" || agent.implementation.agent !== "codex")
|
|
16459
|
+
return;
|
|
16460
|
+
if (meta.conversation?.provider !== "codexAppServer")
|
|
16461
|
+
return;
|
|
16462
|
+
return meta.conversation.threadId;
|
|
16463
|
+
}
|
|
16251
16464
|
function prefixAgentBranch(agent, branch) {
|
|
16252
16465
|
return `${agent}-${branch}`;
|
|
16253
16466
|
}
|
|
@@ -16329,6 +16542,7 @@ class LifecycleService {
|
|
|
16329
16542
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
16330
16543
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
16331
16544
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
16545
|
+
const resumeConversationId = resolveCodexResumeConversationId(initialized.meta, agent, launchMode);
|
|
16332
16546
|
await ensureAgentRuntimeArtifacts({
|
|
16333
16547
|
gitDir: initialized.paths.gitDir,
|
|
16334
16548
|
worktreePath: resolved.entry.path
|
|
@@ -16341,7 +16555,57 @@ class LifecycleService {
|
|
|
16341
16555
|
initialized,
|
|
16342
16556
|
worktreePath: resolved.entry.path,
|
|
16343
16557
|
launchMode,
|
|
16344
|
-
followUpPrompt: options.prompt
|
|
16558
|
+
followUpPrompt: options.prompt,
|
|
16559
|
+
resumeConversationId
|
|
16560
|
+
});
|
|
16561
|
+
if (initialized.meta.agentTerminalStale === true) {
|
|
16562
|
+
await writeWorktreeMeta(resolved.gitDir, {
|
|
16563
|
+
...initialized.meta,
|
|
16564
|
+
agentTerminalStale: false
|
|
16565
|
+
});
|
|
16566
|
+
}
|
|
16567
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
16568
|
+
return {
|
|
16569
|
+
branch,
|
|
16570
|
+
worktreeId: initialized.meta.worktreeId
|
|
16571
|
+
};
|
|
16572
|
+
} catch (error) {
|
|
16573
|
+
throw this.wrapOperationError(error);
|
|
16574
|
+
}
|
|
16575
|
+
}
|
|
16576
|
+
async refreshAgentTerminal(branch) {
|
|
16577
|
+
try {
|
|
16578
|
+
const resolved = await this.resolveExistingWorktree(branch);
|
|
16579
|
+
if (!resolved.meta) {
|
|
16580
|
+
throw new LifecycleError(`Worktree ${branch} has no managed metadata to refresh`, 409);
|
|
16581
|
+
}
|
|
16582
|
+
const initialized = await this.refreshManagedArtifacts(resolved);
|
|
16583
|
+
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
16584
|
+
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
16585
|
+
if (agent.kind !== "builtin" || agent.implementation.agent !== "codex") {
|
|
16586
|
+
throw new LifecycleError("Refreshing the agent terminal is only available for Codex worktrees", 409);
|
|
16587
|
+
}
|
|
16588
|
+
const conversation = initialized.meta.conversation;
|
|
16589
|
+
if (conversation?.provider !== "codexAppServer") {
|
|
16590
|
+
throw new LifecycleError("No Codex conversation is available to refresh", 409);
|
|
16591
|
+
}
|
|
16592
|
+
await ensureAgentRuntimeArtifacts({
|
|
16593
|
+
gitDir: initialized.paths.gitDir,
|
|
16594
|
+
worktreePath: resolved.entry.path
|
|
16595
|
+
});
|
|
16596
|
+
await this.materializeRuntimeSession({
|
|
16597
|
+
branch,
|
|
16598
|
+
profileName,
|
|
16599
|
+
profile,
|
|
16600
|
+
agent,
|
|
16601
|
+
initialized,
|
|
16602
|
+
worktreePath: resolved.entry.path,
|
|
16603
|
+
launchMode: "resume",
|
|
16604
|
+
resumeConversationId: conversation.threadId
|
|
16605
|
+
});
|
|
16606
|
+
await writeWorktreeMeta(resolved.gitDir, {
|
|
16607
|
+
...initialized.meta,
|
|
16608
|
+
agentTerminalStale: false
|
|
16345
16609
|
});
|
|
16346
16610
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
16347
16611
|
return {
|
|
@@ -16656,6 +16920,7 @@ class LifecycleService {
|
|
|
16656
16920
|
followUpPrompt: input.followUpPrompt,
|
|
16657
16921
|
launchMode: input.launchMode,
|
|
16658
16922
|
source: input.source,
|
|
16923
|
+
resumeConversationId: input.resumeConversationId,
|
|
16659
16924
|
containerName
|
|
16660
16925
|
}));
|
|
16661
16926
|
return;
|
|
@@ -16670,7 +16935,8 @@ class LifecycleService {
|
|
|
16670
16935
|
creationPrompt: input.creationPrompt,
|
|
16671
16936
|
followUpPrompt: input.followUpPrompt,
|
|
16672
16937
|
launchMode: input.launchMode,
|
|
16673
|
-
source: input.source
|
|
16938
|
+
source: input.source,
|
|
16939
|
+
resumeConversationId: input.resumeConversationId
|
|
16674
16940
|
}));
|
|
16675
16941
|
}
|
|
16676
16942
|
buildSessionLayout(input) {
|
|
@@ -16695,7 +16961,8 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
|
16695
16961
|
yolo: input.profile.yolo === true,
|
|
16696
16962
|
systemPrompt,
|
|
16697
16963
|
prompt,
|
|
16698
|
-
launchMode: input.launchMode
|
|
16964
|
+
launchMode: input.launchMode,
|
|
16965
|
+
resumeConversationId: input.resumeConversationId
|
|
16699
16966
|
}),
|
|
16700
16967
|
shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
16701
16968
|
} : {
|
|
@@ -16709,7 +16976,8 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
|
16709
16976
|
yolo: input.profile.yolo === true,
|
|
16710
16977
|
systemPrompt,
|
|
16711
16978
|
prompt,
|
|
16712
|
-
launchMode: input.launchMode
|
|
16979
|
+
launchMode: input.launchMode,
|
|
16980
|
+
resumeConversationId: input.resumeConversationId
|
|
16713
16981
|
}),
|
|
16714
16982
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
16715
16983
|
}
|
|
@@ -17445,9 +17713,10 @@ async function runLinearAutoCreateOnce(deps) {
|
|
|
17445
17713
|
for (const issue of oneshotIssues) {
|
|
17446
17714
|
try {
|
|
17447
17715
|
log.info(`[linear-auto-create] launching oneshot for ${issue.identifier}: ${issue.title}`);
|
|
17448
|
-
await deps.runOneshotForIssue(issue.identifier);
|
|
17716
|
+
const { branch } = await deps.runOneshotForIssue(issue.identifier);
|
|
17449
17717
|
processedIssueIds.add(issue.id);
|
|
17450
|
-
log.info(`[linear-auto-create] launched oneshot for ${issue.identifier}`);
|
|
17718
|
+
log.info(`[linear-auto-create] launched oneshot for ${issue.identifier} on ${branch}`);
|
|
17719
|
+
await notifyOneshotPickup(deps, issue, branch);
|
|
17451
17720
|
} catch (err) {
|
|
17452
17721
|
const msg = err instanceof Error ? err.message : String(err);
|
|
17453
17722
|
log.error(`[linear-auto-create] failed to launch oneshot for ${issue.identifier}: ${msg}`);
|
|
@@ -17477,6 +17746,16 @@ ${issue.description ?? ""}`.trim()
|
|
|
17477
17746
|
}
|
|
17478
17747
|
}
|
|
17479
17748
|
}
|
|
17749
|
+
async function notifyOneshotPickup(deps, issue, branch) {
|
|
17750
|
+
if (!deps.onOneshotPickedUp)
|
|
17751
|
+
return;
|
|
17752
|
+
try {
|
|
17753
|
+
await deps.onOneshotPickedUp({ issue, branch });
|
|
17754
|
+
} catch (err) {
|
|
17755
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17756
|
+
log.warn(`[linear-auto-create] pickup notification failed for ${issue.identifier}: ${msg}`);
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17480
17759
|
function startLinearAutoCreateMonitor(deps, options = {}) {
|
|
17481
17760
|
log.info(`[linear-auto-create] monitor started (interval: ${LINEAR_AUTO_CREATE_POLL_INTERVAL_MS}ms)`);
|
|
17482
17761
|
return startSerializedInterval(() => runLinearAutoCreateOnce(deps), LINEAR_AUTO_CREATE_POLL_INTERVAL_MS, options.intervalDeps);
|
|
@@ -17662,182 +17941,6 @@ function startAutoPullMonitor(deps, intervalMs) {
|
|
|
17662
17941
|
return startSerializedInterval(run, intervalMs);
|
|
17663
17942
|
}
|
|
17664
17943
|
|
|
17665
|
-
// backend/src/services/agents-ui-stream-service.ts
|
|
17666
|
-
function readNotificationParams(raw) {
|
|
17667
|
-
return isRecord2(raw) ? raw : null;
|
|
17668
|
-
}
|
|
17669
|
-
function readThreadId(raw) {
|
|
17670
|
-
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
17671
|
-
}
|
|
17672
|
-
function readNotificationItemType(raw) {
|
|
17673
|
-
if (!isRecord2(raw))
|
|
17674
|
-
return null;
|
|
17675
|
-
return typeof raw.type === "string" ? raw.type : null;
|
|
17676
|
-
}
|
|
17677
|
-
function readAgentsNotificationThreadId(notification) {
|
|
17678
|
-
const params = readNotificationParams(notification.params);
|
|
17679
|
-
if (!params)
|
|
17680
|
-
return null;
|
|
17681
|
-
return readThreadId(params.threadId);
|
|
17682
|
-
}
|
|
17683
|
-
function buildAgentsUiMessageDeltaEvent(notification) {
|
|
17684
|
-
if (notification.method !== "item/agentMessage/delta")
|
|
17685
|
-
return null;
|
|
17686
|
-
const params = readNotificationParams(notification.params);
|
|
17687
|
-
if (!params)
|
|
17688
|
-
return null;
|
|
17689
|
-
const threadId = readThreadId(params.threadId);
|
|
17690
|
-
const turnId = readThreadId(params.turnId);
|
|
17691
|
-
const itemId = readThreadId(params.itemId);
|
|
17692
|
-
const delta = typeof params.delta === "string" ? params.delta : null;
|
|
17693
|
-
if (!threadId || !turnId || !itemId || delta === null)
|
|
17694
|
-
return null;
|
|
17695
|
-
return {
|
|
17696
|
-
type: "messageDelta",
|
|
17697
|
-
conversationId: threadId,
|
|
17698
|
-
turnId,
|
|
17699
|
-
itemId,
|
|
17700
|
-
delta
|
|
17701
|
-
};
|
|
17702
|
-
}
|
|
17703
|
-
function shouldRefreshAgentsConversationSnapshot(notification) {
|
|
17704
|
-
switch (notification.method) {
|
|
17705
|
-
case "turn/started":
|
|
17706
|
-
case "turn/completed":
|
|
17707
|
-
case "thread/status/changed":
|
|
17708
|
-
return readAgentsNotificationThreadId(notification) !== null;
|
|
17709
|
-
case "item/completed": {
|
|
17710
|
-
const params = readNotificationParams(notification.params);
|
|
17711
|
-
if (!params)
|
|
17712
|
-
return false;
|
|
17713
|
-
const itemType = readNotificationItemType(params.item);
|
|
17714
|
-
return itemType === "userMessage" || itemType === "agentMessage";
|
|
17715
|
-
}
|
|
17716
|
-
default:
|
|
17717
|
-
return false;
|
|
17718
|
-
}
|
|
17719
|
-
}
|
|
17720
|
-
|
|
17721
|
-
// backend/src/services/agents-ui-action-service.ts
|
|
17722
|
-
function classifyAgentsTerminalWorktreeError(error) {
|
|
17723
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
17724
|
-
if (message.startsWith("No open tmux window found for worktree: ")) {
|
|
17725
|
-
return { status: 409, error: message };
|
|
17726
|
-
}
|
|
17727
|
-
if (message.startsWith("Worktree not found: ")) {
|
|
17728
|
-
return { status: 404, error: message };
|
|
17729
|
-
}
|
|
17730
|
-
return null;
|
|
17731
|
-
}
|
|
17732
|
-
|
|
17733
|
-
// backend/src/services/snapshot-service.ts
|
|
17734
|
-
function formatElapsedSince(startedAt, now) {
|
|
17735
|
-
if (!startedAt)
|
|
17736
|
-
return "";
|
|
17737
|
-
const startedMs = Date.parse(startedAt);
|
|
17738
|
-
if (Number.isNaN(startedMs))
|
|
17739
|
-
return "";
|
|
17740
|
-
const diffMs = Math.max(0, now().getTime() - startedMs);
|
|
17741
|
-
const diffMinutes = Math.floor(diffMs / 60000);
|
|
17742
|
-
if (diffMinutes < 1)
|
|
17743
|
-
return "0m";
|
|
17744
|
-
if (diffMinutes < 60)
|
|
17745
|
-
return `${diffMinutes}m`;
|
|
17746
|
-
const diffHours = Math.floor(diffMinutes / 60);
|
|
17747
|
-
if (diffHours < 24)
|
|
17748
|
-
return `${diffHours}h`;
|
|
17749
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
17750
|
-
return `${diffDays}d`;
|
|
17751
|
-
}
|
|
17752
|
-
function clonePrEntry(pr) {
|
|
17753
|
-
return {
|
|
17754
|
-
...pr,
|
|
17755
|
-
ciChecks: pr.ciChecks.map((check) => ({ ...check })),
|
|
17756
|
-
comments: pr.comments.map((comment) => ({ ...comment }))
|
|
17757
|
-
};
|
|
17758
|
-
}
|
|
17759
|
-
function mapCreationSnapshot(creating) {
|
|
17760
|
-
return creating ? {
|
|
17761
|
-
phase: creating.phase
|
|
17762
|
-
} : null;
|
|
17763
|
-
}
|
|
17764
|
-
function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
17765
|
-
return {
|
|
17766
|
-
branch: state.branch,
|
|
17767
|
-
label: state.label,
|
|
17768
|
-
...state.baseBranch ? { baseBranch: state.baseBranch } : {},
|
|
17769
|
-
path: state.path,
|
|
17770
|
-
dir: state.path,
|
|
17771
|
-
archived: isArchived(state.path),
|
|
17772
|
-
profile: state.profile,
|
|
17773
|
-
agentName: state.agentName,
|
|
17774
|
-
agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
|
|
17775
|
-
mux: state.session.exists,
|
|
17776
|
-
dirty: state.git.dirty,
|
|
17777
|
-
unpushed: state.git.aheadCount > 0,
|
|
17778
|
-
paneCount: state.session.paneCount,
|
|
17779
|
-
status: creating ? "creating" : state.agent.lifecycle,
|
|
17780
|
-
elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
|
|
17781
|
-
services: state.services.map((service) => ({ ...service })),
|
|
17782
|
-
prs: state.prs.map((pr) => clonePrEntry(pr)),
|
|
17783
|
-
linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
|
|
17784
|
-
creation: mapCreationSnapshot(creating),
|
|
17785
|
-
source: state.source,
|
|
17786
|
-
oneshot: state.oneshot
|
|
17787
|
-
};
|
|
17788
|
-
}
|
|
17789
|
-
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
17790
|
-
return {
|
|
17791
|
-
branch: creating.branch,
|
|
17792
|
-
label: null,
|
|
17793
|
-
...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
|
|
17794
|
-
path: creating.path,
|
|
17795
|
-
dir: creating.path,
|
|
17796
|
-
archived: isArchived(creating.path),
|
|
17797
|
-
profile: creating.profile,
|
|
17798
|
-
agentName: creating.agentName,
|
|
17799
|
-
agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
|
|
17800
|
-
mux: false,
|
|
17801
|
-
dirty: false,
|
|
17802
|
-
unpushed: false,
|
|
17803
|
-
paneCount: 0,
|
|
17804
|
-
status: "creating",
|
|
17805
|
-
elapsed: "",
|
|
17806
|
-
services: [],
|
|
17807
|
-
prs: [],
|
|
17808
|
-
linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
|
|
17809
|
-
creation: mapCreationSnapshot(creating),
|
|
17810
|
-
source: creating.source,
|
|
17811
|
-
oneshot: null
|
|
17812
|
-
};
|
|
17813
|
-
}
|
|
17814
|
-
function buildWorktreeSnapshots(input) {
|
|
17815
|
-
const now = input.now ?? (() => new Date);
|
|
17816
|
-
const isArchived = input.isArchived ?? (() => false);
|
|
17817
|
-
const creatingWorktrees = input.creatingWorktrees ?? [];
|
|
17818
|
-
const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
|
|
17819
|
-
const runtimeWorktrees = input.runtime.listWorktrees();
|
|
17820
|
-
const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
|
|
17821
|
-
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
17822
|
-
for (const creating of creatingWorktrees) {
|
|
17823
|
-
if (!runtimeBranches.has(creating.branch)) {
|
|
17824
|
-
worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
17825
|
-
}
|
|
17826
|
-
}
|
|
17827
|
-
worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
|
|
17828
|
-
return worktrees;
|
|
17829
|
-
}
|
|
17830
|
-
function buildProjectSnapshot(input) {
|
|
17831
|
-
return {
|
|
17832
|
-
project: {
|
|
17833
|
-
name: input.projectName,
|
|
17834
|
-
mainBranch: input.mainBranch
|
|
17835
|
-
},
|
|
17836
|
-
worktrees: buildWorktreeSnapshots(input),
|
|
17837
|
-
notifications: input.notifications.map((notification) => ({ ...notification }))
|
|
17838
|
-
};
|
|
17839
|
-
}
|
|
17840
|
-
|
|
17841
17944
|
// backend/src/services/agents-ui-service.ts
|
|
17842
17945
|
function cloneConversationMeta(meta) {
|
|
17843
17946
|
return meta ? { ...meta } : null;
|
|
@@ -17851,6 +17954,7 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
|
|
|
17851
17954
|
profile: worktree.profile,
|
|
17852
17955
|
agentName: worktree.agentName,
|
|
17853
17956
|
agentLabel: worktree.agentLabel,
|
|
17957
|
+
agentTerminalStale: worktree.agentTerminalStale,
|
|
17854
17958
|
mux: worktree.mux,
|
|
17855
17959
|
status: worktree.status,
|
|
17856
17960
|
dirty: worktree.dirty,
|
|
@@ -17867,134 +17971,309 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
|
|
|
17867
17971
|
};
|
|
17868
17972
|
}
|
|
17869
17973
|
|
|
17870
|
-
// backend/src/services/
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
function err(status, error) {
|
|
17875
|
-
return { ok: false, status, error };
|
|
17876
|
-
}
|
|
17877
|
-
|
|
17878
|
-
// backend/src/services/claude-conversation-service.ts
|
|
17879
|
-
function isClaudeWorktree(worktree) {
|
|
17880
|
-
return worktree.agentName === "claude";
|
|
17974
|
+
// backend/src/services/codex-session-log-service.ts
|
|
17975
|
+
var TOOL_OUTPUT_TRUNCATE_LIMIT = 12000;
|
|
17976
|
+
function readString2(raw) {
|
|
17977
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
17881
17978
|
}
|
|
17882
|
-
function
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17979
|
+
function parseLogLine(line) {
|
|
17980
|
+
let parsed;
|
|
17981
|
+
try {
|
|
17982
|
+
parsed = JSON.parse(line);
|
|
17983
|
+
} catch {
|
|
17984
|
+
return null;
|
|
17985
|
+
}
|
|
17986
|
+
if (!isRecord3(parsed))
|
|
17987
|
+
return null;
|
|
17889
17988
|
return {
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
cwd,
|
|
17894
|
-
lastSeenAt: now.toISOString()
|
|
17989
|
+
timestamp: readString2(parsed.timestamp),
|
|
17990
|
+
type: readString2(parsed.type),
|
|
17991
|
+
payload: isRecord3(parsed.payload) ? parsed.payload : null
|
|
17895
17992
|
};
|
|
17896
17993
|
}
|
|
17897
|
-
function
|
|
17898
|
-
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
return messages.map((message) => ({
|
|
17902
|
-
...message,
|
|
17903
|
-
status: "completed"
|
|
17904
|
-
}));
|
|
17994
|
+
function truncate2(text, limit = TOOL_OUTPUT_TRUNCATE_LIMIT) {
|
|
17995
|
+
if (text.length <= limit)
|
|
17996
|
+
return text;
|
|
17997
|
+
return `${text.slice(0, limit)}... (truncated, ${text.length - limit} more chars)`;
|
|
17905
17998
|
}
|
|
17906
|
-
function
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
activeTurnId: null,
|
|
17913
|
-
messages: normalizeSessionMessages(session?.messages ?? [])
|
|
17914
|
-
};
|
|
17999
|
+
function compactJson2(value) {
|
|
18000
|
+
try {
|
|
18001
|
+
return JSON.stringify(value);
|
|
18002
|
+
} catch {
|
|
18003
|
+
return String(value);
|
|
18004
|
+
}
|
|
17915
18005
|
}
|
|
17916
|
-
function
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
18006
|
+
function readReasoningSummary(raw) {
|
|
18007
|
+
if (!Array.isArray(raw))
|
|
18008
|
+
return "";
|
|
18009
|
+
return raw.map((entry) => {
|
|
18010
|
+
if (typeof entry === "string")
|
|
18011
|
+
return entry;
|
|
18012
|
+
if (!isRecord3(entry))
|
|
18013
|
+
return "";
|
|
18014
|
+
if (typeof entry.text === "string")
|
|
18015
|
+
return entry.text;
|
|
18016
|
+
if (typeof entry.summary === "string")
|
|
18017
|
+
return entry.summary;
|
|
18018
|
+
return "";
|
|
18019
|
+
}).filter((text) => text.trim().length > 0).join(`
|
|
18020
|
+
`).trim();
|
|
17921
18021
|
}
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
this.now = deps.now ?? (() => new Date);
|
|
17931
|
-
this.readMeta = deps.readMeta ?? readWorktreeMeta;
|
|
17932
|
-
this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
|
|
17933
|
-
}
|
|
17934
|
-
async attachWorktreeConversation(worktree) {
|
|
17935
|
-
return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse(worktree, resolved.conversationMeta, resolved.session)));
|
|
17936
|
-
}
|
|
17937
|
-
async readWorktreeConversation(worktree) {
|
|
17938
|
-
return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse(worktree, resolved.conversationMeta, resolved.session)));
|
|
18022
|
+
function parseArgumentsRecord(raw) {
|
|
18023
|
+
if (!raw)
|
|
18024
|
+
return null;
|
|
18025
|
+
let parsed;
|
|
18026
|
+
try {
|
|
18027
|
+
parsed = JSON.parse(raw);
|
|
18028
|
+
} catch {
|
|
18029
|
+
return null;
|
|
17939
18030
|
}
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
18031
|
+
return isRecord3(parsed) ? parsed : null;
|
|
18032
|
+
}
|
|
18033
|
+
function readToolCommand(toolName, argumentsText) {
|
|
18034
|
+
if (toolName === "apply_patch")
|
|
18035
|
+
return "apply_patch";
|
|
18036
|
+
const args = parseArgumentsRecord(argumentsText);
|
|
18037
|
+
if (!args)
|
|
18038
|
+
return null;
|
|
18039
|
+
if (toolName === "exec_command" && typeof args.cmd === "string")
|
|
18040
|
+
return args.cmd;
|
|
18041
|
+
return null;
|
|
18042
|
+
}
|
|
18043
|
+
function readToolCwd(argumentsText) {
|
|
18044
|
+
const args = parseArgumentsRecord(argumentsText);
|
|
18045
|
+
if (!args)
|
|
18046
|
+
return null;
|
|
18047
|
+
return typeof args.workdir === "string" ? args.workdir : null;
|
|
18048
|
+
}
|
|
18049
|
+
function buildToolUseText(toolName, argumentsText) {
|
|
18050
|
+
const command = readToolCommand(toolName, argumentsText);
|
|
18051
|
+
if (command)
|
|
18052
|
+
return command;
|
|
18053
|
+
return argumentsText?.trim() ?? "";
|
|
18054
|
+
}
|
|
18055
|
+
function readOutputExitCode(output) {
|
|
18056
|
+
const processMatch = output.match(/Process exited with code (-?\d+)/);
|
|
18057
|
+
if (processMatch?.[1])
|
|
18058
|
+
return Number(processMatch[1]);
|
|
18059
|
+
const exitMatch = output.match(/^Exit code: (-?\d+)/m);
|
|
18060
|
+
if (exitMatch?.[1])
|
|
18061
|
+
return Number(exitMatch[1]);
|
|
18062
|
+
return null;
|
|
18063
|
+
}
|
|
18064
|
+
function readOutputStatus(output) {
|
|
18065
|
+
const exitCode = readOutputExitCode(output);
|
|
18066
|
+
if (exitCode !== null)
|
|
18067
|
+
return exitCode === 0 ? "completed" : "failed";
|
|
18068
|
+
return output.startsWith("apply_patch verification failed") ? "failed" : "completed";
|
|
18069
|
+
}
|
|
18070
|
+
function pushMessage(messages, message) {
|
|
18071
|
+
messages.push({
|
|
18072
|
+
...message,
|
|
18073
|
+
order: messages.length
|
|
18074
|
+
});
|
|
18075
|
+
}
|
|
18076
|
+
function hasDuplicateTextMessage(input) {
|
|
18077
|
+
return input.messages.some((message) => message.turnId === input.turnId && message.role === input.role && message.kind === "text" && message.text === input.text && message.phase === input.phase);
|
|
18078
|
+
}
|
|
18079
|
+
function finalizeToolStatuses(messages) {
|
|
18080
|
+
const resultByCallId = new Map;
|
|
18081
|
+
for (const message of messages) {
|
|
18082
|
+
if (message.kind === "toolResult" && message.toolCallId) {
|
|
18083
|
+
resultByCallId.set(message.toolCallId, message);
|
|
17952
18084
|
}
|
|
17953
18085
|
}
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
18086
|
+
return messages.map((message) => {
|
|
18087
|
+
if (message.kind !== "toolUse" || !message.toolCallId)
|
|
18088
|
+
return message;
|
|
18089
|
+
const result = resultByCallId.get(message.toolCallId);
|
|
18090
|
+
if (!result)
|
|
18091
|
+
return message;
|
|
18092
|
+
return {
|
|
18093
|
+
...message,
|
|
18094
|
+
status: result.status,
|
|
18095
|
+
...result.exitCode !== undefined ? { exitCode: result.exitCode } : {},
|
|
18096
|
+
...result.durationMs !== undefined ? { durationMs: result.durationMs } : {}
|
|
18097
|
+
};
|
|
18098
|
+
});
|
|
18099
|
+
}
|
|
18100
|
+
function parseCodexSessionMessages(text) {
|
|
18101
|
+
const messages = [];
|
|
18102
|
+
const toolCallMetadata = new Map;
|
|
18103
|
+
let currentTurnId = null;
|
|
18104
|
+
let blockIndex = 0;
|
|
18105
|
+
for (const line of text.split(`
|
|
18106
|
+
`)) {
|
|
18107
|
+
const trimmed = line.trim();
|
|
18108
|
+
if (trimmed.length === 0)
|
|
18109
|
+
continue;
|
|
18110
|
+
const record = parseLogLine(trimmed);
|
|
18111
|
+
if (!record?.payload)
|
|
18112
|
+
continue;
|
|
18113
|
+
if (record.type === "event_msg") {
|
|
18114
|
+
const eventType = readString2(record.payload.type);
|
|
18115
|
+
if (eventType === "task_started") {
|
|
18116
|
+
currentTurnId = readString2(record.payload.turn_id);
|
|
18117
|
+
blockIndex = 0;
|
|
18118
|
+
continue;
|
|
18119
|
+
}
|
|
18120
|
+
if (eventType === "task_complete" || eventType === "turn_aborted") {
|
|
18121
|
+
currentTurnId = null;
|
|
18122
|
+
continue;
|
|
18123
|
+
}
|
|
18124
|
+
if (eventType === "user_message" && currentTurnId) {
|
|
18125
|
+
const text2 = readString2(record.payload.message);
|
|
18126
|
+
if (!text2 || hasDuplicateTextMessage({ messages, turnId: currentTurnId, role: "user", text: text2 }))
|
|
18127
|
+
continue;
|
|
18128
|
+
pushMessage(messages, {
|
|
18129
|
+
id: `user:${currentTurnId}:${blockIndex}`,
|
|
18130
|
+
turnId: currentTurnId,
|
|
18131
|
+
role: "user",
|
|
18132
|
+
kind: "text",
|
|
18133
|
+
text: text2,
|
|
18134
|
+
status: "completed",
|
|
18135
|
+
createdAt: record.timestamp
|
|
18136
|
+
});
|
|
18137
|
+
blockIndex += 1;
|
|
18138
|
+
continue;
|
|
18139
|
+
}
|
|
18140
|
+
if (eventType === "agent_message" && currentTurnId) {
|
|
18141
|
+
const text2 = readString2(record.payload.message);
|
|
18142
|
+
if (!text2)
|
|
18143
|
+
continue;
|
|
18144
|
+
const phase = readString2(record.payload.phase) ?? undefined;
|
|
18145
|
+
if (hasDuplicateTextMessage({ messages, turnId: currentTurnId, role: "assistant", text: text2, phase }))
|
|
18146
|
+
continue;
|
|
18147
|
+
pushMessage(messages, {
|
|
18148
|
+
id: `assistant:${currentTurnId}:${blockIndex}`,
|
|
18149
|
+
turnId: currentTurnId,
|
|
18150
|
+
role: "assistant",
|
|
18151
|
+
kind: phase === "analysis" ? "thinking" : "text",
|
|
18152
|
+
...phase ? { phase } : {},
|
|
18153
|
+
text: text2,
|
|
18154
|
+
status: "completed",
|
|
18155
|
+
createdAt: record.timestamp
|
|
18156
|
+
});
|
|
18157
|
+
blockIndex += 1;
|
|
18158
|
+
}
|
|
18159
|
+
continue;
|
|
17959
18160
|
}
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
|
|
18161
|
+
if (record.type !== "response_item" || !currentTurnId)
|
|
18162
|
+
continue;
|
|
18163
|
+
const payloadType = readString2(record.payload.type);
|
|
18164
|
+
if (payloadType === "reasoning") {
|
|
18165
|
+
const summary = readReasoningSummary(record.payload.summary);
|
|
18166
|
+
if (summary.length === 0)
|
|
18167
|
+
continue;
|
|
18168
|
+
pushMessage(messages, {
|
|
18169
|
+
id: `reasoning:${currentTurnId}:${blockIndex}`,
|
|
18170
|
+
turnId: currentTurnId,
|
|
18171
|
+
role: "assistant",
|
|
18172
|
+
kind: "thinking",
|
|
18173
|
+
phase: "analysis",
|
|
18174
|
+
text: summary,
|
|
18175
|
+
status: "completed",
|
|
18176
|
+
createdAt: record.timestamp
|
|
18177
|
+
});
|
|
18178
|
+
blockIndex += 1;
|
|
18179
|
+
continue;
|
|
17974
18180
|
}
|
|
17975
|
-
if (
|
|
17976
|
-
const
|
|
17977
|
-
if (
|
|
17978
|
-
|
|
17979
|
-
|
|
18181
|
+
if (payloadType === "function_call" || payloadType === "custom_tool_call") {
|
|
18182
|
+
const callId = readString2(record.payload.call_id);
|
|
18183
|
+
if (!callId)
|
|
18184
|
+
continue;
|
|
18185
|
+
const toolName = readString2(record.payload.name) ?? "tool";
|
|
18186
|
+
const argumentsText = payloadType === "custom_tool_call" ? typeof record.payload.input === "string" ? record.payload.input : compactJson2(record.payload.input ?? {}) : typeof record.payload.arguments === "string" ? record.payload.arguments : compactJson2(record.payload.arguments ?? {});
|
|
18187
|
+
const command = readToolCommand(toolName, argumentsText);
|
|
18188
|
+
const cwd = payloadType === "custom_tool_call" ? null : readToolCwd(argumentsText);
|
|
18189
|
+
toolCallMetadata.set(callId, {
|
|
18190
|
+
toolName,
|
|
18191
|
+
...command ? { command } : {},
|
|
18192
|
+
...cwd ? { cwd } : {}
|
|
18193
|
+
});
|
|
18194
|
+
pushMessage(messages, {
|
|
18195
|
+
id: callId,
|
|
18196
|
+
turnId: currentTurnId,
|
|
18197
|
+
role: "assistant",
|
|
18198
|
+
kind: "toolUse",
|
|
18199
|
+
toolName,
|
|
18200
|
+
toolCallId: callId,
|
|
18201
|
+
text: payloadType === "custom_tool_call" ? toolName : buildToolUseText(toolName, argumentsText),
|
|
18202
|
+
...command ? { command } : {},
|
|
18203
|
+
...cwd ? { cwd } : {},
|
|
18204
|
+
status: record.payload.status === "failed" ? "failed" : "completed",
|
|
18205
|
+
createdAt: record.timestamp
|
|
18206
|
+
});
|
|
18207
|
+
blockIndex += 1;
|
|
18208
|
+
continue;
|
|
17980
18209
|
}
|
|
17981
|
-
if (
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17987
|
-
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
18210
|
+
if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
|
|
18211
|
+
const callId = readString2(record.payload.call_id);
|
|
18212
|
+
if (!callId)
|
|
18213
|
+
continue;
|
|
18214
|
+
const metadata = toolCallMetadata.get(callId);
|
|
18215
|
+
const output = typeof record.payload.output === "string" ? record.payload.output.trimEnd() : compactJson2(record.payload.output ?? "");
|
|
18216
|
+
const exitCode = readOutputExitCode(output);
|
|
18217
|
+
pushMessage(messages, {
|
|
18218
|
+
id: `${callId}:result`,
|
|
18219
|
+
turnId: currentTurnId,
|
|
18220
|
+
role: "user",
|
|
18221
|
+
kind: "toolResult",
|
|
18222
|
+
...metadata?.toolName ? { toolName: metadata.toolName } : {},
|
|
18223
|
+
toolCallId: callId,
|
|
18224
|
+
text: truncate2(output),
|
|
18225
|
+
...metadata?.command ? { command: metadata.command } : {},
|
|
18226
|
+
...metadata?.cwd ? { cwd: metadata.cwd } : {},
|
|
18227
|
+
status: readOutputStatus(output),
|
|
18228
|
+
createdAt: record.timestamp,
|
|
18229
|
+
exitCode
|
|
17991
18230
|
});
|
|
18231
|
+
blockIndex += 1;
|
|
17992
18232
|
}
|
|
17993
|
-
return nextConversation;
|
|
17994
18233
|
}
|
|
18234
|
+
return finalizeToolStatuses(messages);
|
|
18235
|
+
}
|
|
18236
|
+
async function readCodexSessionMessages(thread) {
|
|
18237
|
+
if (!thread.path)
|
|
18238
|
+
return [];
|
|
18239
|
+
try {
|
|
18240
|
+
const file = Bun.file(thread.path);
|
|
18241
|
+
if (!await file.exists())
|
|
18242
|
+
return [];
|
|
18243
|
+
return parseCodexSessionMessages(await file.text());
|
|
18244
|
+
} catch {
|
|
18245
|
+
return [];
|
|
18246
|
+
}
|
|
18247
|
+
}
|
|
18248
|
+
|
|
18249
|
+
// backend/src/services/worktree-conversation-result.ts
|
|
18250
|
+
function ok(data) {
|
|
18251
|
+
return { ok: true, data };
|
|
18252
|
+
}
|
|
18253
|
+
function err(status, error) {
|
|
18254
|
+
return { ok: false, status, error };
|
|
17995
18255
|
}
|
|
17996
18256
|
|
|
17997
18257
|
// backend/src/services/worktree-conversation-service.ts
|
|
18258
|
+
function resolveCodexAppServerLaunchContext(input) {
|
|
18259
|
+
if (input.worktree.agentName !== "codex" || input.meta.agent !== "codex") {
|
|
18260
|
+
return err(409, "Codex web chat is only available for Codex worktrees");
|
|
18261
|
+
}
|
|
18262
|
+
if (!input.profile) {
|
|
18263
|
+
return err(409, `Profile is missing for Codex web chat: ${input.meta.profile}`);
|
|
18264
|
+
}
|
|
18265
|
+
if (input.meta.runtime !== "host" || input.profile.runtime !== "host") {
|
|
18266
|
+
return err(409, "Codex web chat is only available for host-runtime worktrees. Use the terminal for Docker worktrees.");
|
|
18267
|
+
}
|
|
18268
|
+
if (input.profile.yolo !== true) {
|
|
18269
|
+
return err(409, "Codex web chat requires a yolo profile to preserve the Codex approval policy. Use the terminal for this worktree.");
|
|
18270
|
+
}
|
|
18271
|
+
return ok({
|
|
18272
|
+
approvalPolicy: "never",
|
|
18273
|
+
personality: "pragmatic",
|
|
18274
|
+
sandbox: "danger-full-access"
|
|
18275
|
+
});
|
|
18276
|
+
}
|
|
17998
18277
|
function isCodexWorktree(worktree) {
|
|
17999
18278
|
return worktree.agentName === "codex";
|
|
18000
18279
|
}
|
|
@@ -18012,9 +18291,126 @@ function isUserMessageItem(item) {
|
|
|
18012
18291
|
function isAgentMessageItem(item) {
|
|
18013
18292
|
return item.type === "agentMessage";
|
|
18014
18293
|
}
|
|
18294
|
+
function isCommandExecutionItem(item) {
|
|
18295
|
+
return item.type === "commandExecution";
|
|
18296
|
+
}
|
|
18297
|
+
function isFileChangeItem(item) {
|
|
18298
|
+
return item.type === "fileChange";
|
|
18299
|
+
}
|
|
18300
|
+
function isMcpToolCallItem(item) {
|
|
18301
|
+
return item.type === "mcpToolCall";
|
|
18302
|
+
}
|
|
18303
|
+
function isDynamicToolCallItem(item) {
|
|
18304
|
+
return item.type === "dynamicToolCall";
|
|
18305
|
+
}
|
|
18306
|
+
function isWebSearchItem(item) {
|
|
18307
|
+
return item.type === "webSearch";
|
|
18308
|
+
}
|
|
18015
18309
|
function extractUserText(item) {
|
|
18016
18310
|
return item.content.map((contentItem) => contentItem.text ?? "").join("").trim();
|
|
18017
18311
|
}
|
|
18312
|
+
function extractAgentText(item) {
|
|
18313
|
+
return item.text ?? item.message ?? "";
|
|
18314
|
+
}
|
|
18315
|
+
function commandExecutionStatus(item) {
|
|
18316
|
+
switch (item.status) {
|
|
18317
|
+
case "inProgress":
|
|
18318
|
+
return "inProgress";
|
|
18319
|
+
case "completed":
|
|
18320
|
+
return item.exitCode !== null && item.exitCode !== 0 ? "failed" : "completed";
|
|
18321
|
+
case "failed":
|
|
18322
|
+
case "declined":
|
|
18323
|
+
return "failed";
|
|
18324
|
+
}
|
|
18325
|
+
}
|
|
18326
|
+
function commandExecutionDisplayText(item) {
|
|
18327
|
+
const commands = item.commandActions.map((action) => action.command ?? "").filter((command) => command.length > 0);
|
|
18328
|
+
return commands.length > 0 ? commands.join(" && ") : item.command;
|
|
18329
|
+
}
|
|
18330
|
+
function toolStatus(status) {
|
|
18331
|
+
switch (status) {
|
|
18332
|
+
case "inProgress":
|
|
18333
|
+
return "inProgress";
|
|
18334
|
+
case "completed":
|
|
18335
|
+
return "completed";
|
|
18336
|
+
case "failed":
|
|
18337
|
+
case "declined":
|
|
18338
|
+
return "failed";
|
|
18339
|
+
}
|
|
18340
|
+
}
|
|
18341
|
+
function jsonDisplayText(value) {
|
|
18342
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? "";
|
|
18343
|
+
}
|
|
18344
|
+
function patchChangeLabel(change) {
|
|
18345
|
+
switch (change.kind.type) {
|
|
18346
|
+
case "add":
|
|
18347
|
+
return `add ${change.path}`;
|
|
18348
|
+
case "delete":
|
|
18349
|
+
return `delete ${change.path}`;
|
|
18350
|
+
case "update":
|
|
18351
|
+
return change.kind.move_path ? `move ${change.kind.move_path} -> ${change.path}` : `update ${change.path}`;
|
|
18352
|
+
}
|
|
18353
|
+
}
|
|
18354
|
+
function fileChangeDisplayText(item) {
|
|
18355
|
+
return item.changes.map(patchChangeLabel).join(`
|
|
18356
|
+
`);
|
|
18357
|
+
}
|
|
18358
|
+
function fileChangeResultText(item) {
|
|
18359
|
+
return item.changes.map((change) => change.diff.trimEnd()).filter((diff) => diff.length > 0).join(`
|
|
18360
|
+
|
|
18361
|
+
`);
|
|
18362
|
+
}
|
|
18363
|
+
function mcpContentText(content) {
|
|
18364
|
+
if (isRecord3(content) && typeof content.text === "string")
|
|
18365
|
+
return content.text;
|
|
18366
|
+
return jsonDisplayText(content);
|
|
18367
|
+
}
|
|
18368
|
+
function mcpToolResultText(item) {
|
|
18369
|
+
if (item.error)
|
|
18370
|
+
return item.error.message;
|
|
18371
|
+
if (!item.result)
|
|
18372
|
+
return "";
|
|
18373
|
+
const parts = item.result.content.map(mcpContentText);
|
|
18374
|
+
if (item.result.structuredContent !== null) {
|
|
18375
|
+
parts.push(jsonDisplayText(item.result.structuredContent));
|
|
18376
|
+
}
|
|
18377
|
+
return parts.join(`
|
|
18378
|
+
|
|
18379
|
+
`).trim();
|
|
18380
|
+
}
|
|
18381
|
+
function dynamicToolName(item) {
|
|
18382
|
+
return item.namespace ? `${item.namespace}.${item.tool}` : item.tool;
|
|
18383
|
+
}
|
|
18384
|
+
function dynamicToolContentText(content) {
|
|
18385
|
+
switch (content.type) {
|
|
18386
|
+
case "inputText":
|
|
18387
|
+
return content.text;
|
|
18388
|
+
case "inputImage":
|
|
18389
|
+
return content.imageUrl;
|
|
18390
|
+
}
|
|
18391
|
+
}
|
|
18392
|
+
function dynamicToolResultText(item) {
|
|
18393
|
+
return (item.contentItems ?? []).map(dynamicToolContentText).join(`
|
|
18394
|
+
|
|
18395
|
+
`).trim();
|
|
18396
|
+
}
|
|
18397
|
+
function webSearchDisplayText(item) {
|
|
18398
|
+
const action = item.action;
|
|
18399
|
+
if (!action)
|
|
18400
|
+
return item.query;
|
|
18401
|
+
switch (action.type) {
|
|
18402
|
+
case "search":
|
|
18403
|
+
return action.queries?.join(`
|
|
18404
|
+
`) ?? action.query ?? item.query;
|
|
18405
|
+
case "openPage":
|
|
18406
|
+
return action.url ?? item.query;
|
|
18407
|
+
case "findInPage":
|
|
18408
|
+
return [action.url, action.pattern].filter((part) => part !== null).join(`
|
|
18409
|
+
`);
|
|
18410
|
+
case "other":
|
|
18411
|
+
return item.query;
|
|
18412
|
+
}
|
|
18413
|
+
}
|
|
18018
18414
|
function isActiveTurnStatus(status) {
|
|
18019
18415
|
return status === "inProgress" || status === "active" || status === "running" || status === "pending" || status === "queued";
|
|
18020
18416
|
}
|
|
@@ -18026,76 +18422,823 @@ function findActiveTurn(thread) {
|
|
|
18026
18422
|
}
|
|
18027
18423
|
return null;
|
|
18028
18424
|
}
|
|
18425
|
+
function buildCommandExecutionMessages(input) {
|
|
18426
|
+
const { item, turnId, createdAt, order } = input;
|
|
18427
|
+
const status = commandExecutionStatus(item);
|
|
18428
|
+
const toolUse = {
|
|
18429
|
+
id: item.id,
|
|
18430
|
+
turnId,
|
|
18431
|
+
order,
|
|
18432
|
+
role: "assistant",
|
|
18433
|
+
kind: "toolUse",
|
|
18434
|
+
toolName: "shell",
|
|
18435
|
+
toolCallId: item.id,
|
|
18436
|
+
text: commandExecutionDisplayText(item),
|
|
18437
|
+
command: item.command,
|
|
18438
|
+
cwd: item.cwd ?? undefined,
|
|
18439
|
+
status,
|
|
18440
|
+
createdAt,
|
|
18441
|
+
exitCode: item.exitCode,
|
|
18442
|
+
durationMs: item.durationMs
|
|
18443
|
+
};
|
|
18444
|
+
const output = item.aggregatedOutput?.trimEnd() ?? "";
|
|
18445
|
+
if (output.length === 0)
|
|
18446
|
+
return [toolUse];
|
|
18447
|
+
return [
|
|
18448
|
+
toolUse,
|
|
18449
|
+
{
|
|
18450
|
+
id: `${item.id}:result`,
|
|
18451
|
+
turnId,
|
|
18452
|
+
order: order + 1,
|
|
18453
|
+
role: "user",
|
|
18454
|
+
kind: "toolResult",
|
|
18455
|
+
toolName: "shell",
|
|
18456
|
+
toolCallId: item.id,
|
|
18457
|
+
text: output,
|
|
18458
|
+
command: item.command,
|
|
18459
|
+
cwd: item.cwd ?? undefined,
|
|
18460
|
+
status,
|
|
18461
|
+
createdAt,
|
|
18462
|
+
exitCode: item.exitCode,
|
|
18463
|
+
durationMs: item.durationMs
|
|
18464
|
+
}
|
|
18465
|
+
];
|
|
18466
|
+
}
|
|
18467
|
+
function buildFileChangeMessages(input) {
|
|
18468
|
+
const { item, turnId, createdAt, order } = input;
|
|
18469
|
+
const status = toolStatus(item.status);
|
|
18470
|
+
const toolUse = {
|
|
18471
|
+
id: item.id,
|
|
18472
|
+
turnId,
|
|
18473
|
+
order,
|
|
18474
|
+
role: "assistant",
|
|
18475
|
+
kind: "toolUse",
|
|
18476
|
+
toolName: "file change",
|
|
18477
|
+
toolCallId: item.id,
|
|
18478
|
+
text: fileChangeDisplayText(item),
|
|
18479
|
+
status,
|
|
18480
|
+
createdAt
|
|
18481
|
+
};
|
|
18482
|
+
const resultText = fileChangeResultText(item);
|
|
18483
|
+
if (resultText.length === 0)
|
|
18484
|
+
return [toolUse];
|
|
18485
|
+
return [
|
|
18486
|
+
toolUse,
|
|
18487
|
+
{
|
|
18488
|
+
id: `${item.id}:result`,
|
|
18489
|
+
turnId,
|
|
18490
|
+
order: order + 1,
|
|
18491
|
+
role: "user",
|
|
18492
|
+
kind: "toolResult",
|
|
18493
|
+
toolName: "file change",
|
|
18494
|
+
toolCallId: item.id,
|
|
18495
|
+
text: resultText,
|
|
18496
|
+
status,
|
|
18497
|
+
createdAt
|
|
18498
|
+
}
|
|
18499
|
+
];
|
|
18500
|
+
}
|
|
18501
|
+
function buildMcpToolCallMessages(input) {
|
|
18502
|
+
const { item, turnId, createdAt, order } = input;
|
|
18503
|
+
const status = item.error ? "failed" : toolStatus(item.status);
|
|
18504
|
+
const toolName = `${item.server}.${item.tool}`;
|
|
18505
|
+
const toolUse = {
|
|
18506
|
+
id: item.id,
|
|
18507
|
+
turnId,
|
|
18508
|
+
order,
|
|
18509
|
+
role: "assistant",
|
|
18510
|
+
kind: "toolUse",
|
|
18511
|
+
toolName,
|
|
18512
|
+
toolCallId: item.id,
|
|
18513
|
+
text: jsonDisplayText(item.arguments),
|
|
18514
|
+
status,
|
|
18515
|
+
createdAt,
|
|
18516
|
+
durationMs: item.durationMs
|
|
18517
|
+
};
|
|
18518
|
+
const resultText = mcpToolResultText(item);
|
|
18519
|
+
if (resultText.length === 0)
|
|
18520
|
+
return [toolUse];
|
|
18521
|
+
return [
|
|
18522
|
+
toolUse,
|
|
18523
|
+
{
|
|
18524
|
+
id: `${item.id}:result`,
|
|
18525
|
+
turnId,
|
|
18526
|
+
order: order + 1,
|
|
18527
|
+
role: "user",
|
|
18528
|
+
kind: "toolResult",
|
|
18529
|
+
toolName,
|
|
18530
|
+
toolCallId: item.id,
|
|
18531
|
+
text: resultText,
|
|
18532
|
+
status,
|
|
18533
|
+
createdAt,
|
|
18534
|
+
durationMs: item.durationMs
|
|
18535
|
+
}
|
|
18536
|
+
];
|
|
18537
|
+
}
|
|
18538
|
+
function buildDynamicToolCallMessages(input) {
|
|
18539
|
+
const { item, turnId, createdAt, order } = input;
|
|
18540
|
+
const status = item.success === false ? "failed" : toolStatus(item.status);
|
|
18541
|
+
const toolName = dynamicToolName(item);
|
|
18542
|
+
const toolUse = {
|
|
18543
|
+
id: item.id,
|
|
18544
|
+
turnId,
|
|
18545
|
+
order,
|
|
18546
|
+
role: "assistant",
|
|
18547
|
+
kind: "toolUse",
|
|
18548
|
+
toolName,
|
|
18549
|
+
toolCallId: item.id,
|
|
18550
|
+
text: jsonDisplayText(item.arguments),
|
|
18551
|
+
status,
|
|
18552
|
+
createdAt,
|
|
18553
|
+
durationMs: item.durationMs
|
|
18554
|
+
};
|
|
18555
|
+
const resultText = dynamicToolResultText(item);
|
|
18556
|
+
if (resultText.length === 0)
|
|
18557
|
+
return [toolUse];
|
|
18558
|
+
return [
|
|
18559
|
+
toolUse,
|
|
18560
|
+
{
|
|
18561
|
+
id: `${item.id}:result`,
|
|
18562
|
+
turnId,
|
|
18563
|
+
order: order + 1,
|
|
18564
|
+
role: "user",
|
|
18565
|
+
kind: "toolResult",
|
|
18566
|
+
toolName,
|
|
18567
|
+
toolCallId: item.id,
|
|
18568
|
+
text: resultText,
|
|
18569
|
+
status,
|
|
18570
|
+
createdAt,
|
|
18571
|
+
durationMs: item.durationMs
|
|
18572
|
+
}
|
|
18573
|
+
];
|
|
18574
|
+
}
|
|
18575
|
+
function buildWebSearchMessages(input) {
|
|
18576
|
+
const { item, turnId, createdAt, order } = input;
|
|
18577
|
+
return [{
|
|
18578
|
+
id: item.id,
|
|
18579
|
+
turnId,
|
|
18580
|
+
order,
|
|
18581
|
+
role: "assistant",
|
|
18582
|
+
kind: "toolUse",
|
|
18583
|
+
toolName: "web search",
|
|
18584
|
+
toolCallId: item.id,
|
|
18585
|
+
text: webSearchDisplayText(item),
|
|
18586
|
+
status: "completed",
|
|
18587
|
+
createdAt
|
|
18588
|
+
}];
|
|
18589
|
+
}
|
|
18590
|
+
function buildCodexItemConversationMessages(input) {
|
|
18591
|
+
const { item, turnId, turnStatus, createdAt, order, includeEmptyText = false } = input;
|
|
18592
|
+
if (isUserMessageItem(item)) {
|
|
18593
|
+
const text = extractUserText(item);
|
|
18594
|
+
if (text.length === 0 && !includeEmptyText)
|
|
18595
|
+
return [];
|
|
18596
|
+
return [{
|
|
18597
|
+
id: item.id,
|
|
18598
|
+
turnId,
|
|
18599
|
+
order,
|
|
18600
|
+
role: "user",
|
|
18601
|
+
kind: "text",
|
|
18602
|
+
text,
|
|
18603
|
+
status: "completed",
|
|
18604
|
+
createdAt
|
|
18605
|
+
}];
|
|
18606
|
+
}
|
|
18607
|
+
if (isAgentMessageItem(item)) {
|
|
18608
|
+
const text = extractAgentText(item);
|
|
18609
|
+
if (text.length === 0 && !includeEmptyText)
|
|
18610
|
+
return [];
|
|
18611
|
+
const phase = item.phase ?? undefined;
|
|
18612
|
+
const isThinking = phase === "analysis";
|
|
18613
|
+
return [{
|
|
18614
|
+
id: item.id,
|
|
18615
|
+
turnId,
|
|
18616
|
+
order,
|
|
18617
|
+
role: "assistant",
|
|
18618
|
+
kind: isThinking ? "thinking" : "text",
|
|
18619
|
+
phase,
|
|
18620
|
+
text,
|
|
18621
|
+
status: isActiveTurnStatus(turnStatus) ? "inProgress" : "completed",
|
|
18622
|
+
createdAt
|
|
18623
|
+
}];
|
|
18624
|
+
}
|
|
18625
|
+
if (isCommandExecutionItem(item))
|
|
18626
|
+
return buildCommandExecutionMessages({ item, turnId, createdAt, order });
|
|
18627
|
+
if (isFileChangeItem(item))
|
|
18628
|
+
return buildFileChangeMessages({ item, turnId, createdAt, order });
|
|
18629
|
+
if (isMcpToolCallItem(item))
|
|
18630
|
+
return buildMcpToolCallMessages({ item, turnId, createdAt, order });
|
|
18631
|
+
if (isDynamicToolCallItem(item))
|
|
18632
|
+
return buildDynamicToolCallMessages({ item, turnId, createdAt, order });
|
|
18633
|
+
if (isWebSearchItem(item))
|
|
18634
|
+
return buildWebSearchMessages({ item, turnId, createdAt, order });
|
|
18635
|
+
return [];
|
|
18636
|
+
}
|
|
18029
18637
|
function buildConversationMessages(thread) {
|
|
18030
18638
|
const messages = [];
|
|
18639
|
+
let order = 0;
|
|
18031
18640
|
for (const turn of thread.turns) {
|
|
18032
18641
|
for (const item of turn.items) {
|
|
18033
|
-
|
|
18034
|
-
|
|
18035
|
-
if (text.length === 0)
|
|
18036
|
-
continue;
|
|
18037
|
-
messages.push({
|
|
18038
|
-
id: item.id,
|
|
18039
|
-
turnId: turn.id,
|
|
18040
|
-
role: "user",
|
|
18041
|
-
text,
|
|
18042
|
-
status: "completed",
|
|
18043
|
-
createdAt: toIsoTimestamp(turn.startedAt)
|
|
18044
|
-
});
|
|
18045
|
-
continue;
|
|
18046
|
-
}
|
|
18047
|
-
if (!isAgentMessageItem(item))
|
|
18048
|
-
continue;
|
|
18049
|
-
if (item.text.length === 0)
|
|
18050
|
-
continue;
|
|
18051
|
-
messages.push({
|
|
18052
|
-
id: item.id,
|
|
18642
|
+
const itemMessages = buildCodexItemConversationMessages({
|
|
18643
|
+
item,
|
|
18053
18644
|
turnId: turn.id,
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
|
|
18057
|
-
createdAt: toIsoTimestamp(turn.completedAt ?? turn.startedAt)
|
|
18645
|
+
turnStatus: turn.status,
|
|
18646
|
+
createdAt: toIsoTimestamp(isUserMessageItem(item) ? turn.startedAt : turn.completedAt ?? turn.startedAt),
|
|
18647
|
+
order
|
|
18058
18648
|
});
|
|
18649
|
+
messages.push(...itemMessages);
|
|
18650
|
+
order += itemMessages.length;
|
|
18059
18651
|
}
|
|
18060
18652
|
}
|
|
18061
18653
|
return messages;
|
|
18062
18654
|
}
|
|
18063
|
-
function
|
|
18655
|
+
function buildConversationState(thread, sessionMessages = []) {
|
|
18064
18656
|
const activeTurn = findActiveTurn(thread);
|
|
18657
|
+
const messages = sessionMessages.length > 0 ? sessionMessages : buildConversationMessages(thread);
|
|
18065
18658
|
return {
|
|
18066
18659
|
provider: "codexAppServer",
|
|
18067
18660
|
conversationId: thread.id,
|
|
18068
18661
|
cwd: thread.cwd,
|
|
18069
18662
|
running: thread.status.type === "active" || activeTurn !== null,
|
|
18070
18663
|
activeTurnId: activeTurn?.id ?? null,
|
|
18071
|
-
messages
|
|
18664
|
+
messages
|
|
18072
18665
|
};
|
|
18073
18666
|
}
|
|
18074
|
-
function selectDiscoveredThread(threads) {
|
|
18075
|
-
if (threads.length === 0)
|
|
18076
|
-
return null;
|
|
18077
|
-
return [...threads].sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
|
|
18667
|
+
function selectDiscoveredThread(threads) {
|
|
18668
|
+
if (threads.length === 0)
|
|
18669
|
+
return null;
|
|
18670
|
+
return [...threads].sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
|
|
18671
|
+
}
|
|
18672
|
+
function buildConversationMeta(thread, now) {
|
|
18673
|
+
return {
|
|
18674
|
+
provider: "codexAppServer",
|
|
18675
|
+
conversationId: thread.id,
|
|
18676
|
+
threadId: thread.id,
|
|
18677
|
+
cwd: thread.cwd,
|
|
18678
|
+
lastSeenAt: now.toISOString()
|
|
18679
|
+
};
|
|
18680
|
+
}
|
|
18681
|
+
function sameConversationMeta(left, right) {
|
|
18682
|
+
return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
|
|
18683
|
+
}
|
|
18684
|
+
function toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages) {
|
|
18685
|
+
return {
|
|
18686
|
+
worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
|
|
18687
|
+
conversation: buildConversationState(thread, sessionMessages)
|
|
18688
|
+
};
|
|
18689
|
+
}
|
|
18690
|
+
|
|
18691
|
+
class WorktreeConversationService {
|
|
18692
|
+
deps;
|
|
18693
|
+
now;
|
|
18694
|
+
readSessionMessages;
|
|
18695
|
+
readMeta;
|
|
18696
|
+
writeMeta;
|
|
18697
|
+
constructor(deps) {
|
|
18698
|
+
this.deps = deps;
|
|
18699
|
+
this.now = deps.now ?? (() => new Date);
|
|
18700
|
+
this.readSessionMessages = deps.readSessionMessages ?? readCodexSessionMessages;
|
|
18701
|
+
this.readMeta = deps.readMeta ?? readWorktreeMeta;
|
|
18702
|
+
this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
|
|
18703
|
+
}
|
|
18704
|
+
async attachWorktreeConversation(worktree) {
|
|
18705
|
+
return await this.withResolvedConversation(worktree, true, async ({ conversationMeta, thread }) => {
|
|
18706
|
+
const sessionMessages = await this.readSessionMessages(thread);
|
|
18707
|
+
return ok(toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages));
|
|
18708
|
+
});
|
|
18709
|
+
}
|
|
18710
|
+
async readWorktreeConversation(worktree) {
|
|
18711
|
+
return await this.withResolvedConversation(worktree, false, async ({ conversationMeta, thread }) => {
|
|
18712
|
+
const sessionMessages = await this.readSessionMessages(thread);
|
|
18713
|
+
return ok(toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages));
|
|
18714
|
+
});
|
|
18715
|
+
}
|
|
18716
|
+
async sendWorktreeConversationMessage(worktree, text) {
|
|
18717
|
+
return await this.withResolvedConversation(worktree, true, async ({ thread, launchContext }) => {
|
|
18718
|
+
const started = await this.deps.appServer.turnStart({
|
|
18719
|
+
threadId: thread.id,
|
|
18720
|
+
cwd: worktree.path,
|
|
18721
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18722
|
+
input: [{ type: "text", text }]
|
|
18723
|
+
});
|
|
18724
|
+
return ok({
|
|
18725
|
+
conversationId: thread.id,
|
|
18726
|
+
turnId: started.turn.id,
|
|
18727
|
+
running: true
|
|
18728
|
+
});
|
|
18729
|
+
});
|
|
18730
|
+
}
|
|
18731
|
+
async interruptWorktreeConversation(worktree) {
|
|
18732
|
+
return await this.withResolvedConversation(worktree, false, async ({ thread }) => {
|
|
18733
|
+
const conversation = buildConversationState(thread);
|
|
18734
|
+
const turnId = conversation.activeTurnId;
|
|
18735
|
+
if (!turnId) {
|
|
18736
|
+
return err(409, "No active Codex turn to interrupt");
|
|
18737
|
+
}
|
|
18738
|
+
await this.deps.appServer.turnInterrupt({
|
|
18739
|
+
threadId: thread.id,
|
|
18740
|
+
turnId
|
|
18741
|
+
});
|
|
18742
|
+
return ok({
|
|
18743
|
+
conversationId: thread.id,
|
|
18744
|
+
turnId,
|
|
18745
|
+
interrupted: true
|
|
18746
|
+
});
|
|
18747
|
+
});
|
|
18748
|
+
}
|
|
18749
|
+
async withResolvedConversation(worktree, allowCreate, fn) {
|
|
18750
|
+
if (!isCodexWorktree(worktree)) {
|
|
18751
|
+
return err(409, "Worktree chat is only available for Codex worktrees");
|
|
18752
|
+
}
|
|
18753
|
+
try {
|
|
18754
|
+
const resolved = await this.resolveConversation(worktree, allowCreate);
|
|
18755
|
+
if (!resolved.ok)
|
|
18756
|
+
return resolved;
|
|
18757
|
+
return await fn(resolved.data);
|
|
18758
|
+
} catch (error) {
|
|
18759
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18760
|
+
return err(502, message);
|
|
18761
|
+
}
|
|
18762
|
+
}
|
|
18763
|
+
async resolveConversation(worktree, allowCreate) {
|
|
18764
|
+
const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
|
|
18765
|
+
const meta = await this.readMeta(gitDir);
|
|
18766
|
+
if (!meta) {
|
|
18767
|
+
return err(409, "Worktree metadata is missing");
|
|
18768
|
+
}
|
|
18769
|
+
const launchContextResult = await this.deps.resolveLaunchContext({ worktree, meta });
|
|
18770
|
+
if (!launchContextResult.ok)
|
|
18771
|
+
return launchContextResult;
|
|
18772
|
+
const launchContext = launchContextResult.data;
|
|
18773
|
+
const now = this.now();
|
|
18774
|
+
const thread = await this.resolveThread(meta, worktree.path, allowCreate, launchContext);
|
|
18775
|
+
if (!thread) {
|
|
18776
|
+
return err(404, "No Codex thread could be resolved for this worktree");
|
|
18777
|
+
}
|
|
18778
|
+
const conversationMeta = buildConversationMeta(thread, now);
|
|
18779
|
+
const nextMeta = sameConversationMeta(meta.conversation, conversationMeta) ? { ...meta, conversation: { ...conversationMeta, lastSeenAt: meta.conversation?.lastSeenAt ?? conversationMeta.lastSeenAt } } : { ...meta, conversation: conversationMeta };
|
|
18780
|
+
if (!sameConversationMeta(meta.conversation, conversationMeta)) {
|
|
18781
|
+
await this.writeMeta(gitDir, nextMeta);
|
|
18782
|
+
}
|
|
18783
|
+
return ok({
|
|
18784
|
+
gitDir,
|
|
18785
|
+
meta: nextMeta,
|
|
18786
|
+
thread,
|
|
18787
|
+
conversationMeta: nextMeta.conversation ?? conversationMeta,
|
|
18788
|
+
launchContext
|
|
18789
|
+
});
|
|
18790
|
+
}
|
|
18791
|
+
async resolveThread(meta, cwd, allowCreate, launchContext) {
|
|
18792
|
+
const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
|
|
18793
|
+
if (savedThreadId) {
|
|
18794
|
+
const savedThread = await this.tryLoadThread(savedThreadId, cwd, launchContext);
|
|
18795
|
+
if (savedThread)
|
|
18796
|
+
return savedThread;
|
|
18797
|
+
log.warn(`[agents] saved codex thread missing, starting fresh conversation cwd=${cwd} threadId=${savedThreadId}`);
|
|
18798
|
+
} else {
|
|
18799
|
+
const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
|
|
18800
|
+
cwd,
|
|
18801
|
+
limit: 20,
|
|
18802
|
+
sortKey: "updated_at"
|
|
18803
|
+
})).data);
|
|
18804
|
+
if (discoveredThread) {
|
|
18805
|
+
return await this.ensureThreadLoaded(discoveredThread.id, cwd, launchContext);
|
|
18806
|
+
}
|
|
18807
|
+
}
|
|
18808
|
+
if (!allowCreate)
|
|
18809
|
+
return null;
|
|
18810
|
+
const started = await this.deps.appServer.threadStart({
|
|
18811
|
+
cwd,
|
|
18812
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18813
|
+
personality: launchContext.personality,
|
|
18814
|
+
sandbox: launchContext.sandbox
|
|
18815
|
+
});
|
|
18816
|
+
return started.thread;
|
|
18817
|
+
}
|
|
18818
|
+
async tryLoadThread(threadId, cwd, launchContext) {
|
|
18819
|
+
try {
|
|
18820
|
+
return await this.ensureThreadLoaded(threadId, cwd, launchContext);
|
|
18821
|
+
} catch {
|
|
18822
|
+
return null;
|
|
18823
|
+
}
|
|
18824
|
+
}
|
|
18825
|
+
async ensureThreadLoaded(threadId, cwd, launchContext) {
|
|
18826
|
+
const initial = await this.deps.appServer.threadRead(threadId, false);
|
|
18827
|
+
if (initial.thread.status.type === "notLoaded") {
|
|
18828
|
+
await this.deps.appServer.threadResume({
|
|
18829
|
+
threadId,
|
|
18830
|
+
cwd,
|
|
18831
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18832
|
+
personality: launchContext.personality,
|
|
18833
|
+
sandbox: launchContext.sandbox
|
|
18834
|
+
});
|
|
18835
|
+
}
|
|
18836
|
+
return (await this.deps.appServer.threadRead(threadId, true)).thread;
|
|
18837
|
+
}
|
|
18838
|
+
}
|
|
18839
|
+
|
|
18840
|
+
// backend/src/services/agents-ui-stream-service.ts
|
|
18841
|
+
function readNotificationParams(raw) {
|
|
18842
|
+
return isRecord3(raw) ? raw : null;
|
|
18843
|
+
}
|
|
18844
|
+
function readThreadId(raw) {
|
|
18845
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
18846
|
+
}
|
|
18847
|
+
function readStatusType(raw) {
|
|
18848
|
+
if (typeof raw === "string")
|
|
18849
|
+
return raw;
|
|
18850
|
+
if (!isRecord3(raw))
|
|
18851
|
+
return null;
|
|
18852
|
+
return typeof raw.type === "string" ? raw.type : null;
|
|
18853
|
+
}
|
|
18854
|
+
function readNotificationStatusType(notification) {
|
|
18855
|
+
const params = readNotificationParams(notification.params);
|
|
18856
|
+
if (!params)
|
|
18857
|
+
return null;
|
|
18858
|
+
return readStatusType(params.status) ?? (isRecord3(params.thread) ? readStatusType(params.thread.status) : null);
|
|
18859
|
+
}
|
|
18860
|
+
function isTerminalThreadStatus(statusType) {
|
|
18861
|
+
return statusType === "idle" || statusType === "completed" || statusType === "interrupted" || statusType === "failed" || statusType === "systemError";
|
|
18862
|
+
}
|
|
18863
|
+
function readNumber(raw) {
|
|
18864
|
+
return typeof raw === "number" ? raw : null;
|
|
18865
|
+
}
|
|
18866
|
+
function toIsoTimestampMs(epochMs) {
|
|
18867
|
+
if (epochMs === null)
|
|
18868
|
+
return null;
|
|
18869
|
+
return new Date(epochMs).toISOString();
|
|
18870
|
+
}
|
|
18871
|
+
function readNotificationItem(notification) {
|
|
18872
|
+
const params = readNotificationParams(notification.params);
|
|
18873
|
+
if (!params)
|
|
18874
|
+
return null;
|
|
18875
|
+
return parseCodexAppServerThreadItem(params.item);
|
|
18876
|
+
}
|
|
18877
|
+
function orderSpanForItem(item) {
|
|
18878
|
+
switch (item.type) {
|
|
18879
|
+
case "userMessage":
|
|
18880
|
+
case "agentMessage":
|
|
18881
|
+
case "webSearch":
|
|
18882
|
+
return 1;
|
|
18883
|
+
case "commandExecution":
|
|
18884
|
+
case "fileChange":
|
|
18885
|
+
case "mcpToolCall":
|
|
18886
|
+
case "dynamicToolCall":
|
|
18887
|
+
return 2;
|
|
18888
|
+
default:
|
|
18889
|
+
return null;
|
|
18890
|
+
}
|
|
18891
|
+
}
|
|
18892
|
+
function readAgentsNotificationThreadId(notification) {
|
|
18893
|
+
const params = readNotificationParams(notification.params);
|
|
18894
|
+
if (!params)
|
|
18895
|
+
return null;
|
|
18896
|
+
return readThreadId(params.threadId);
|
|
18897
|
+
}
|
|
18898
|
+
function buildAgentsUiMessageDeltaEvent(notification, order) {
|
|
18899
|
+
if (notification.method !== "item/agentMessage/delta")
|
|
18900
|
+
return null;
|
|
18901
|
+
const params = readNotificationParams(notification.params);
|
|
18902
|
+
if (!params)
|
|
18903
|
+
return null;
|
|
18904
|
+
const threadId = readThreadId(params.threadId);
|
|
18905
|
+
const turnId = readThreadId(params.turnId);
|
|
18906
|
+
const itemId = readThreadId(params.itemId);
|
|
18907
|
+
const delta = typeof params.delta === "string" ? params.delta : null;
|
|
18908
|
+
if (!threadId || !turnId || !itemId || delta === null)
|
|
18909
|
+
return null;
|
|
18910
|
+
return {
|
|
18911
|
+
type: "messageDelta",
|
|
18912
|
+
conversationId: threadId,
|
|
18913
|
+
turnId,
|
|
18914
|
+
itemId,
|
|
18915
|
+
order,
|
|
18916
|
+
delta
|
|
18917
|
+
};
|
|
18918
|
+
}
|
|
18919
|
+
function buildAgentsUiMessageUpsertEvents(notification, order) {
|
|
18920
|
+
if (notification.method !== "item/started" && notification.method !== "item/completed")
|
|
18921
|
+
return [];
|
|
18922
|
+
const params = readNotificationParams(notification.params);
|
|
18923
|
+
if (!params)
|
|
18924
|
+
return [];
|
|
18925
|
+
const threadId = readThreadId(params.threadId);
|
|
18926
|
+
const turnId = readThreadId(params.turnId);
|
|
18927
|
+
if (!threadId || !turnId)
|
|
18928
|
+
return [];
|
|
18929
|
+
const item = readNotificationItem(notification);
|
|
18930
|
+
if (!item)
|
|
18931
|
+
return [];
|
|
18932
|
+
const createdAt = toIsoTimestampMs(notification.method === "item/started" ? readNumber(params.startedAtMs) : readNumber(params.completedAtMs));
|
|
18933
|
+
return buildCodexItemConversationMessages({
|
|
18934
|
+
item,
|
|
18935
|
+
turnId,
|
|
18936
|
+
turnStatus: notification.method === "item/started" ? "inProgress" : "completed",
|
|
18937
|
+
createdAt,
|
|
18938
|
+
order,
|
|
18939
|
+
includeEmptyText: true
|
|
18940
|
+
}).map((message) => ({
|
|
18941
|
+
type: "messageUpsert",
|
|
18942
|
+
conversationId: threadId,
|
|
18943
|
+
message
|
|
18944
|
+
}));
|
|
18945
|
+
}
|
|
18946
|
+
function buildAgentsUiConversationStatusEvent(notification) {
|
|
18947
|
+
if (notification.method !== "turn/started" && notification.method !== "turn/completed" && notification.method !== "thread/status/changed")
|
|
18948
|
+
return null;
|
|
18949
|
+
const params = readNotificationParams(notification.params);
|
|
18950
|
+
if (!params)
|
|
18951
|
+
return null;
|
|
18952
|
+
const conversationId = readThreadId(params.threadId);
|
|
18953
|
+
if (!conversationId)
|
|
18954
|
+
return null;
|
|
18955
|
+
if (notification.method === "thread/status/changed") {
|
|
18956
|
+
if (!isTerminalThreadStatus(readNotificationStatusType(notification)))
|
|
18957
|
+
return null;
|
|
18958
|
+
return {
|
|
18959
|
+
type: "conversationStatus",
|
|
18960
|
+
conversationId,
|
|
18961
|
+
running: false,
|
|
18962
|
+
activeTurnId: null
|
|
18963
|
+
};
|
|
18964
|
+
}
|
|
18965
|
+
if (notification.method === "turn/started") {
|
|
18966
|
+
const activeTurnId = readThreadId(params.turnId);
|
|
18967
|
+
if (!activeTurnId)
|
|
18968
|
+
return null;
|
|
18969
|
+
return {
|
|
18970
|
+
type: "conversationStatus",
|
|
18971
|
+
conversationId,
|
|
18972
|
+
running: true,
|
|
18973
|
+
activeTurnId
|
|
18974
|
+
};
|
|
18975
|
+
}
|
|
18976
|
+
return {
|
|
18977
|
+
type: "conversationStatus",
|
|
18978
|
+
conversationId,
|
|
18979
|
+
running: false,
|
|
18980
|
+
activeTurnId: null
|
|
18981
|
+
};
|
|
18982
|
+
}
|
|
18983
|
+
|
|
18984
|
+
class AgentsConversationStreamSession {
|
|
18985
|
+
deps;
|
|
18986
|
+
revision = 0;
|
|
18987
|
+
conversationId;
|
|
18988
|
+
closed = false;
|
|
18989
|
+
nextLiveOrder;
|
|
18990
|
+
itemOrders = new Map;
|
|
18991
|
+
constructor(deps) {
|
|
18992
|
+
this.deps = deps;
|
|
18993
|
+
this.conversationId = deps.conversationId;
|
|
18994
|
+
this.nextLiveOrder = deps.nextOrder;
|
|
18995
|
+
}
|
|
18996
|
+
currentConversationId() {
|
|
18997
|
+
return this.conversationId;
|
|
18998
|
+
}
|
|
18999
|
+
close() {
|
|
19000
|
+
this.closed = true;
|
|
19001
|
+
}
|
|
19002
|
+
handleNotification(notification) {
|
|
19003
|
+
if (this.closed)
|
|
19004
|
+
return;
|
|
19005
|
+
const notificationThreadId = readAgentsNotificationThreadId(notification);
|
|
19006
|
+
if (!notificationThreadId || notificationThreadId !== this.conversationId)
|
|
19007
|
+
return;
|
|
19008
|
+
const statusEvent = buildAgentsUiConversationStatusEvent(notification);
|
|
19009
|
+
if (statusEvent) {
|
|
19010
|
+
this.deps.send({
|
|
19011
|
+
...statusEvent,
|
|
19012
|
+
revision: this.nextRevision()
|
|
19013
|
+
});
|
|
19014
|
+
return;
|
|
19015
|
+
}
|
|
19016
|
+
const deltaOrder = this.orderForDeltaNotification(notification);
|
|
19017
|
+
const deltaEvent = deltaOrder === null ? null : buildAgentsUiMessageDeltaEvent(notification, deltaOrder);
|
|
19018
|
+
if (deltaEvent) {
|
|
19019
|
+
this.deps.send({
|
|
19020
|
+
...deltaEvent,
|
|
19021
|
+
revision: this.nextRevision()
|
|
19022
|
+
});
|
|
19023
|
+
return;
|
|
19024
|
+
}
|
|
19025
|
+
const upsertOrder = this.orderForUpsertNotification(notification);
|
|
19026
|
+
if (upsertOrder !== null) {
|
|
19027
|
+
for (const upsertEvent of buildAgentsUiMessageUpsertEvents(notification, upsertOrder)) {
|
|
19028
|
+
this.deps.send({
|
|
19029
|
+
...upsertEvent,
|
|
19030
|
+
revision: this.nextRevision()
|
|
19031
|
+
});
|
|
19032
|
+
}
|
|
19033
|
+
}
|
|
19034
|
+
}
|
|
19035
|
+
nextRevision() {
|
|
19036
|
+
this.revision += 1;
|
|
19037
|
+
return this.revision;
|
|
19038
|
+
}
|
|
19039
|
+
reserveOrder(itemId, span) {
|
|
19040
|
+
const existing = this.itemOrders.get(itemId);
|
|
19041
|
+
if (existing !== undefined)
|
|
19042
|
+
return existing;
|
|
19043
|
+
const order = this.nextLiveOrder;
|
|
19044
|
+
this.nextLiveOrder += span;
|
|
19045
|
+
this.itemOrders.set(itemId, order);
|
|
19046
|
+
return order;
|
|
19047
|
+
}
|
|
19048
|
+
orderForDeltaNotification(notification) {
|
|
19049
|
+
if (notification.method !== "item/agentMessage/delta")
|
|
19050
|
+
return null;
|
|
19051
|
+
const params = readNotificationParams(notification.params);
|
|
19052
|
+
if (!params)
|
|
19053
|
+
return null;
|
|
19054
|
+
const itemId = readThreadId(params.itemId);
|
|
19055
|
+
return itemId ? this.reserveOrder(itemId, 1) : null;
|
|
19056
|
+
}
|
|
19057
|
+
orderForUpsertNotification(notification) {
|
|
19058
|
+
if (notification.method !== "item/started" && notification.method !== "item/completed")
|
|
19059
|
+
return null;
|
|
19060
|
+
const params = readNotificationParams(notification.params);
|
|
19061
|
+
if (!params || !isRecord3(params.item))
|
|
19062
|
+
return null;
|
|
19063
|
+
const itemId = readThreadId(params.item.id);
|
|
19064
|
+
const item = readNotificationItem(notification);
|
|
19065
|
+
if (!itemId || !item)
|
|
19066
|
+
return null;
|
|
19067
|
+
const orderSpan = orderSpanForItem(item);
|
|
19068
|
+
return orderSpan === null ? null : this.reserveOrder(itemId, orderSpan);
|
|
19069
|
+
}
|
|
19070
|
+
}
|
|
19071
|
+
|
|
19072
|
+
// backend/src/services/agents-ui-action-service.ts
|
|
19073
|
+
function classifyAgentsTerminalWorktreeError(error) {
|
|
19074
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19075
|
+
if (message.startsWith("No open tmux window found for worktree: ")) {
|
|
19076
|
+
return { status: 409, error: message };
|
|
19077
|
+
}
|
|
19078
|
+
if (message.startsWith("Worktree not found: ")) {
|
|
19079
|
+
return { status: 404, error: message };
|
|
19080
|
+
}
|
|
19081
|
+
return null;
|
|
19082
|
+
}
|
|
19083
|
+
|
|
19084
|
+
// backend/src/services/snapshot-service.ts
|
|
19085
|
+
function formatElapsedSince(startedAt, now) {
|
|
19086
|
+
if (!startedAt)
|
|
19087
|
+
return "";
|
|
19088
|
+
const startedMs = Date.parse(startedAt);
|
|
19089
|
+
if (Number.isNaN(startedMs))
|
|
19090
|
+
return "";
|
|
19091
|
+
const diffMs = Math.max(0, now().getTime() - startedMs);
|
|
19092
|
+
const diffMinutes = Math.floor(diffMs / 60000);
|
|
19093
|
+
if (diffMinutes < 1)
|
|
19094
|
+
return "0m";
|
|
19095
|
+
if (diffMinutes < 60)
|
|
19096
|
+
return `${diffMinutes}m`;
|
|
19097
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
19098
|
+
if (diffHours < 24)
|
|
19099
|
+
return `${diffHours}h`;
|
|
19100
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
19101
|
+
return `${diffDays}d`;
|
|
19102
|
+
}
|
|
19103
|
+
function clonePrEntry(pr) {
|
|
19104
|
+
return {
|
|
19105
|
+
...pr,
|
|
19106
|
+
ciChecks: pr.ciChecks.map((check) => ({ ...check })),
|
|
19107
|
+
comments: pr.comments.map((comment) => ({ ...comment }))
|
|
19108
|
+
};
|
|
19109
|
+
}
|
|
19110
|
+
function mapCreationSnapshot(creating) {
|
|
19111
|
+
return creating ? {
|
|
19112
|
+
phase: creating.phase
|
|
19113
|
+
} : null;
|
|
19114
|
+
}
|
|
19115
|
+
function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
19116
|
+
return {
|
|
19117
|
+
branch: state.branch,
|
|
19118
|
+
label: state.label,
|
|
19119
|
+
...state.baseBranch ? { baseBranch: state.baseBranch } : {},
|
|
19120
|
+
path: state.path,
|
|
19121
|
+
dir: state.path,
|
|
19122
|
+
archived: isArchived(state.path),
|
|
19123
|
+
profile: state.profile,
|
|
19124
|
+
agentName: state.agentName,
|
|
19125
|
+
agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
|
|
19126
|
+
agentTerminalStale: state.agentTerminalStale,
|
|
19127
|
+
mux: state.session.exists,
|
|
19128
|
+
dirty: state.git.dirty,
|
|
19129
|
+
unpushed: state.git.aheadCount > 0,
|
|
19130
|
+
paneCount: state.session.paneCount,
|
|
19131
|
+
status: creating ? "creating" : state.agent.lifecycle,
|
|
19132
|
+
elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
|
|
19133
|
+
services: state.services.map((service) => ({ ...service })),
|
|
19134
|
+
prs: state.prs.map((pr) => clonePrEntry(pr)),
|
|
19135
|
+
linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
|
|
19136
|
+
creation: mapCreationSnapshot(creating),
|
|
19137
|
+
source: state.source,
|
|
19138
|
+
oneshot: state.oneshot
|
|
19139
|
+
};
|
|
19140
|
+
}
|
|
19141
|
+
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
19142
|
+
return {
|
|
19143
|
+
branch: creating.branch,
|
|
19144
|
+
label: null,
|
|
19145
|
+
...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
|
|
19146
|
+
path: creating.path,
|
|
19147
|
+
dir: creating.path,
|
|
19148
|
+
archived: isArchived(creating.path),
|
|
19149
|
+
profile: creating.profile,
|
|
19150
|
+
agentName: creating.agentName,
|
|
19151
|
+
agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
|
|
19152
|
+
agentTerminalStale: false,
|
|
19153
|
+
mux: false,
|
|
19154
|
+
dirty: false,
|
|
19155
|
+
unpushed: false,
|
|
19156
|
+
paneCount: 0,
|
|
19157
|
+
status: "creating",
|
|
19158
|
+
elapsed: "",
|
|
19159
|
+
services: [],
|
|
19160
|
+
prs: [],
|
|
19161
|
+
linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
|
|
19162
|
+
creation: mapCreationSnapshot(creating),
|
|
19163
|
+
source: creating.source,
|
|
19164
|
+
oneshot: null
|
|
19165
|
+
};
|
|
19166
|
+
}
|
|
19167
|
+
function buildWorktreeSnapshots(input) {
|
|
19168
|
+
const now = input.now ?? (() => new Date);
|
|
19169
|
+
const isArchived = input.isArchived ?? (() => false);
|
|
19170
|
+
const creatingWorktrees = input.creatingWorktrees ?? [];
|
|
19171
|
+
const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
|
|
19172
|
+
const runtimeWorktrees = input.runtime.listWorktrees();
|
|
19173
|
+
const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
|
|
19174
|
+
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
19175
|
+
for (const creating of creatingWorktrees) {
|
|
19176
|
+
if (!runtimeBranches.has(creating.branch)) {
|
|
19177
|
+
worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
19178
|
+
}
|
|
19179
|
+
}
|
|
19180
|
+
worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
|
|
19181
|
+
return worktrees;
|
|
19182
|
+
}
|
|
19183
|
+
function buildProjectSnapshot(input) {
|
|
19184
|
+
return {
|
|
19185
|
+
project: {
|
|
19186
|
+
name: input.projectName,
|
|
19187
|
+
mainBranch: input.mainBranch
|
|
19188
|
+
},
|
|
19189
|
+
worktrees: buildWorktreeSnapshots(input),
|
|
19190
|
+
notifications: input.notifications.map((notification) => ({ ...notification }))
|
|
19191
|
+
};
|
|
19192
|
+
}
|
|
19193
|
+
|
|
19194
|
+
// backend/src/services/claude-conversation-service.ts
|
|
19195
|
+
function isClaudeWorktree(worktree) {
|
|
19196
|
+
return worktree.agentName === "claude";
|
|
19197
|
+
}
|
|
19198
|
+
function isClaudeConversationMeta(meta) {
|
|
19199
|
+
return meta?.provider === "claudeCode";
|
|
18078
19200
|
}
|
|
18079
|
-
function
|
|
19201
|
+
function buildPendingConversationId(worktree) {
|
|
19202
|
+
return `claude-pending:${worktree.path}`;
|
|
19203
|
+
}
|
|
19204
|
+
function buildClaudeConversationMeta(sessionId, cwd, now) {
|
|
18080
19205
|
return {
|
|
18081
|
-
provider: "
|
|
18082
|
-
conversationId:
|
|
18083
|
-
|
|
18084
|
-
cwd
|
|
19206
|
+
provider: "claudeCode",
|
|
19207
|
+
conversationId: sessionId,
|
|
19208
|
+
sessionId,
|
|
19209
|
+
cwd,
|
|
18085
19210
|
lastSeenAt: now.toISOString()
|
|
18086
19211
|
};
|
|
18087
19212
|
}
|
|
18088
19213
|
function sameConversationMeta2(left, right) {
|
|
18089
19214
|
return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
|
|
18090
19215
|
}
|
|
18091
|
-
function
|
|
19216
|
+
function normalizeSessionMessages(messages) {
|
|
19217
|
+
return messages.map((message, order) => ({
|
|
19218
|
+
...message,
|
|
19219
|
+
order,
|
|
19220
|
+
kind: message.kind ?? "text",
|
|
19221
|
+
status: "completed"
|
|
19222
|
+
}));
|
|
19223
|
+
}
|
|
19224
|
+
function buildConversationState2(worktree, session) {
|
|
19225
|
+
return {
|
|
19226
|
+
provider: "claudeCode",
|
|
19227
|
+
conversationId: session?.sessionId ?? buildPendingConversationId(worktree),
|
|
19228
|
+
cwd: worktree.path,
|
|
19229
|
+
running: false,
|
|
19230
|
+
activeTurnId: null,
|
|
19231
|
+
messages: normalizeSessionMessages(session?.messages ?? [])
|
|
19232
|
+
};
|
|
19233
|
+
}
|
|
19234
|
+
function toWorktreeConversationResponse2(worktree, conversationMeta, session) {
|
|
18092
19235
|
return {
|
|
18093
19236
|
worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
|
|
18094
|
-
conversation: buildConversationState2(
|
|
19237
|
+
conversation: buildConversationState2(worktree, session)
|
|
18095
19238
|
};
|
|
18096
19239
|
}
|
|
18097
19240
|
|
|
18098
|
-
class
|
|
19241
|
+
class ClaudeConversationService {
|
|
18099
19242
|
deps;
|
|
18100
19243
|
now;
|
|
18101
19244
|
readMeta;
|
|
@@ -18107,17 +19250,17 @@ class WorktreeConversationService {
|
|
|
18107
19250
|
this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
|
|
18108
19251
|
}
|
|
18109
19252
|
async attachWorktreeConversation(worktree) {
|
|
18110
|
-
return await this.withResolvedConversation(worktree,
|
|
19253
|
+
return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
|
|
18111
19254
|
}
|
|
18112
19255
|
async readWorktreeConversation(worktree) {
|
|
18113
|
-
return await this.withResolvedConversation(worktree,
|
|
19256
|
+
return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
|
|
18114
19257
|
}
|
|
18115
|
-
async withResolvedConversation(worktree,
|
|
18116
|
-
if (!
|
|
18117
|
-
return err(409, "Worktree chat is only available for
|
|
19258
|
+
async withResolvedConversation(worktree, fn) {
|
|
19259
|
+
if (!isClaudeWorktree(worktree)) {
|
|
19260
|
+
return err(409, "Worktree chat is only available for Claude worktrees");
|
|
18118
19261
|
}
|
|
18119
19262
|
try {
|
|
18120
|
-
const resolved = await this.resolveConversation(worktree
|
|
19263
|
+
const resolved = await this.resolveConversation(worktree);
|
|
18121
19264
|
if (!resolved.ok)
|
|
18122
19265
|
return resolved;
|
|
18123
19266
|
return await fn(resolved.data);
|
|
@@ -18126,73 +19269,46 @@ class WorktreeConversationService {
|
|
|
18126
19269
|
return err(502, message);
|
|
18127
19270
|
}
|
|
18128
19271
|
}
|
|
18129
|
-
async resolveConversation(worktree
|
|
19272
|
+
async resolveConversation(worktree) {
|
|
18130
19273
|
const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
|
|
18131
19274
|
const meta = await this.readMeta(gitDir);
|
|
18132
19275
|
if (!meta) {
|
|
18133
19276
|
return err(409, "Worktree metadata is missing");
|
|
18134
19277
|
}
|
|
18135
|
-
const
|
|
18136
|
-
const
|
|
18137
|
-
if (!thread) {
|
|
18138
|
-
return err(404, "No Codex thread could be resolved for this worktree");
|
|
18139
|
-
}
|
|
18140
|
-
const conversationMeta = buildConversationMeta(thread, now);
|
|
18141
|
-
const nextMeta = sameConversationMeta2(meta.conversation, conversationMeta) ? { ...meta, conversation: { ...conversationMeta, lastSeenAt: meta.conversation?.lastSeenAt ?? conversationMeta.lastSeenAt } } : { ...meta, conversation: conversationMeta };
|
|
18142
|
-
if (!sameConversationMeta2(meta.conversation, conversationMeta)) {
|
|
18143
|
-
await this.writeMeta(gitDir, nextMeta);
|
|
18144
|
-
}
|
|
19278
|
+
const session = await this.resolveSession(meta, worktree.path);
|
|
19279
|
+
const conversationMeta = session ? await this.persistConversationMeta(gitDir, meta, worktree.path, session.sessionId) : null;
|
|
18145
19280
|
return ok({
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
thread,
|
|
18149
|
-
conversationMeta: nextMeta.conversation ?? conversationMeta
|
|
19281
|
+
conversationMeta,
|
|
19282
|
+
session
|
|
18150
19283
|
});
|
|
18151
19284
|
}
|
|
18152
|
-
async
|
|
18153
|
-
const
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
return await this.ensureThreadLoaded(discoveredThread.id, cwd);
|
|
19285
|
+
async resolveSession(meta, cwd) {
|
|
19286
|
+
const savedSessionId = isClaudeConversationMeta(meta.conversation) ? meta.conversation.sessionId : null;
|
|
19287
|
+
const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
|
|
19288
|
+
if (discovered && discovered.sessionId !== savedSessionId) {
|
|
19289
|
+
const session = await this.deps.claude.readSession(discovered.sessionId, cwd);
|
|
19290
|
+
if (session)
|
|
19291
|
+
return session;
|
|
18160
19292
|
}
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
log.warn(`[agents] saved codex thread missing, rediscovering cwd=${cwd} threadId=${savedThreadId}`);
|
|
19293
|
+
if (savedSessionId) {
|
|
19294
|
+
const savedSession = await this.deps.claude.readSession(savedSessionId, cwd);
|
|
19295
|
+
if (savedSession)
|
|
19296
|
+
return savedSession;
|
|
19297
|
+
log.warn(`[agents] saved Claude session missing, rediscovering cwd=${cwd} sessionId=${savedSessionId}`);
|
|
18167
19298
|
}
|
|
18168
|
-
if (!
|
|
18169
|
-
return null;
|
|
18170
|
-
const started = await this.deps.appServer.threadStart({
|
|
18171
|
-
cwd,
|
|
18172
|
-
approvalPolicy: "never",
|
|
18173
|
-
personality: "pragmatic",
|
|
18174
|
-
sandbox: "danger-full-access"
|
|
18175
|
-
});
|
|
18176
|
-
return started.thread;
|
|
18177
|
-
}
|
|
18178
|
-
async tryLoadThread(threadId, cwd) {
|
|
18179
|
-
try {
|
|
18180
|
-
return await this.ensureThreadLoaded(threadId, cwd);
|
|
18181
|
-
} catch {
|
|
19299
|
+
if (!discovered)
|
|
18182
19300
|
return null;
|
|
18183
|
-
|
|
19301
|
+
return await this.deps.claude.readSession(discovered.sessionId, cwd);
|
|
18184
19302
|
}
|
|
18185
|
-
async
|
|
18186
|
-
const
|
|
18187
|
-
if (
|
|
18188
|
-
await this.
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
approvalPolicy: "never",
|
|
18192
|
-
personality: "pragmatic"
|
|
19303
|
+
async persistConversationMeta(gitDir, meta, cwd, sessionId) {
|
|
19304
|
+
const nextConversation = buildClaudeConversationMeta(sessionId, cwd, this.now());
|
|
19305
|
+
if (!sameConversationMeta2(meta.conversation, nextConversation)) {
|
|
19306
|
+
await this.writeMeta(gitDir, {
|
|
19307
|
+
...meta,
|
|
19308
|
+
conversation: nextConversation
|
|
18193
19309
|
});
|
|
18194
19310
|
}
|
|
18195
|
-
return
|
|
19311
|
+
return nextConversation;
|
|
18196
19312
|
}
|
|
18197
19313
|
}
|
|
18198
19314
|
|
|
@@ -18605,49 +19721,15 @@ class BunPortProbe {
|
|
|
18605
19721
|
}
|
|
18606
19722
|
}
|
|
18607
19723
|
|
|
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 {
|
|
19724
|
+
// backend/src/services/llm-spawn.ts
|
|
19725
|
+
class LlmSpawnTimeoutError extends Error {
|
|
18644
19726
|
timeoutMs;
|
|
18645
19727
|
constructor(timeoutMs) {
|
|
18646
|
-
super(`
|
|
19728
|
+
super(`LLM spawn timed out after ${timeoutMs}ms`);
|
|
18647
19729
|
this.timeoutMs = timeoutMs;
|
|
18648
19730
|
}
|
|
18649
19731
|
}
|
|
18650
|
-
async function
|
|
19732
|
+
async function defaultLlmSpawn(args, options = {}) {
|
|
18651
19733
|
const proc = Bun.spawn(args, {
|
|
18652
19734
|
stdout: "pipe",
|
|
18653
19735
|
stderr: "pipe"
|
|
@@ -18657,7 +19739,8 @@ async function defaultSpawn(args, options = {}) {
|
|
|
18657
19739
|
new Response(proc.stderr).text(),
|
|
18658
19740
|
proc.exited
|
|
18659
19741
|
]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
|
|
18660
|
-
|
|
19742
|
+
const timeoutMs = options.timeoutMs;
|
|
19743
|
+
if (timeoutMs === undefined) {
|
|
18661
19744
|
return await resultPromise;
|
|
18662
19745
|
}
|
|
18663
19746
|
return await new Promise((resolve8, reject) => {
|
|
@@ -18669,8 +19752,8 @@ async function defaultSpawn(args, options = {}) {
|
|
|
18669
19752
|
try {
|
|
18670
19753
|
proc.kill("SIGKILL");
|
|
18671
19754
|
} catch {}
|
|
18672
|
-
reject(new
|
|
18673
|
-
},
|
|
19755
|
+
reject(new LlmSpawnTimeoutError(timeoutMs));
|
|
19756
|
+
}, timeoutMs);
|
|
18674
19757
|
resultPromise.then((result) => {
|
|
18675
19758
|
if (settled)
|
|
18676
19759
|
return;
|
|
@@ -18686,30 +19769,27 @@ async function defaultSpawn(args, options = {}) {
|
|
|
18686
19769
|
});
|
|
18687
19770
|
});
|
|
18688
19771
|
}
|
|
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
19772
|
function escapeTomlString(s) {
|
|
18707
19773
|
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
18708
19774
|
}
|
|
18709
|
-
|
|
18710
|
-
|
|
18711
|
-
|
|
18712
|
-
|
|
19775
|
+
var DEFAULT_CLAUDE_MODEL = "claude-haiku-4-5-20251001";
|
|
19776
|
+
function buildLlmArgs(config, systemPrompt, userPrompt) {
|
|
19777
|
+
if (config.provider === "claude") {
|
|
19778
|
+
return [
|
|
19779
|
+
"claude",
|
|
19780
|
+
"-p",
|
|
19781
|
+
"--system-prompt",
|
|
19782
|
+
systemPrompt,
|
|
19783
|
+
"--output-format",
|
|
19784
|
+
"text",
|
|
19785
|
+
"--no-session-persistence",
|
|
19786
|
+
"--model",
|
|
19787
|
+
config.model || DEFAULT_CLAUDE_MODEL,
|
|
19788
|
+
"--effort",
|
|
19789
|
+
"low",
|
|
19790
|
+
userPrompt
|
|
19791
|
+
];
|
|
19792
|
+
}
|
|
18713
19793
|
const args = [
|
|
18714
19794
|
"codex",
|
|
18715
19795
|
"-c",
|
|
@@ -18717,18 +19797,82 @@ function buildCodexArgs(model, systemPrompt, prompt) {
|
|
|
18717
19797
|
"exec",
|
|
18718
19798
|
"--ephemeral"
|
|
18719
19799
|
];
|
|
18720
|
-
if (model) {
|
|
18721
|
-
args.push("-m", model);
|
|
19800
|
+
if (config.model) {
|
|
19801
|
+
args.push("-m", config.model);
|
|
18722
19802
|
}
|
|
18723
|
-
args.push(
|
|
19803
|
+
args.push(userPrompt);
|
|
18724
19804
|
return args;
|
|
18725
19805
|
}
|
|
19806
|
+
async function runShortLlmTask(config, systemPrompt, userPrompt, options = {}) {
|
|
19807
|
+
const args = buildLlmArgs(config, systemPrompt, userPrompt);
|
|
19808
|
+
const spawnImpl = options.spawnImpl ?? defaultLlmSpawn;
|
|
19809
|
+
let result;
|
|
19810
|
+
try {
|
|
19811
|
+
result = await spawnImpl(args, { timeoutMs: options.timeoutMs });
|
|
19812
|
+
} catch (error) {
|
|
19813
|
+
if (error instanceof LlmSpawnTimeoutError) {
|
|
19814
|
+
return { ok: false, kind: "timeout", timeoutMs: error.timeoutMs, args };
|
|
19815
|
+
}
|
|
19816
|
+
return { ok: false, kind: "spawn_error", error, args };
|
|
19817
|
+
}
|
|
19818
|
+
if (result.exitCode !== 0) {
|
|
19819
|
+
return {
|
|
19820
|
+
ok: false,
|
|
19821
|
+
kind: "exit_nonzero",
|
|
19822
|
+
exitCode: result.exitCode,
|
|
19823
|
+
stdout: result.stdout,
|
|
19824
|
+
stderr: result.stderr,
|
|
19825
|
+
args
|
|
19826
|
+
};
|
|
19827
|
+
}
|
|
19828
|
+
return { ok: true, stdout: result.stdout, stderr: result.stderr, args };
|
|
19829
|
+
}
|
|
19830
|
+
function llmProviderLabel(config) {
|
|
19831
|
+
return config.provider === "claude" ? "claude" : "codex";
|
|
19832
|
+
}
|
|
19833
|
+
|
|
19834
|
+
// backend/src/services/auto-name-service.ts
|
|
19835
|
+
var MAX_BRANCH_LENGTH = 40;
|
|
19836
|
+
var AUTO_NAME_TIMEOUT_MS = 15000;
|
|
19837
|
+
var DEFAULT_SYSTEM_PROMPT = [
|
|
19838
|
+
"Generate a concise git branch name from the task description.",
|
|
19839
|
+
"Return only the branch name.",
|
|
19840
|
+
"Use lowercase kebab-case.",
|
|
19841
|
+
`Maximum ${MAX_BRANCH_LENGTH} characters.`,
|
|
19842
|
+
"Do not include quotes, code fences, or prefixes like feature/ or fix/."
|
|
19843
|
+
].join(" ");
|
|
19844
|
+
function normalizeGeneratedBranchName(raw) {
|
|
19845
|
+
let branch = raw.trim();
|
|
19846
|
+
branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
|
|
19847
|
+
branch = branch.split(/\r?\n/)[0]?.trim() ?? "";
|
|
19848
|
+
branch = branch.replace(/^branch(?:\s+name)?\s*:\s*/i, "");
|
|
19849
|
+
branch = branch.replace(/^["'`]+|["'`]+$/g, "");
|
|
19850
|
+
branch = branch.toLowerCase();
|
|
19851
|
+
branch = branch.replace(/[^a-z0-9._/-]+/g, "-");
|
|
19852
|
+
branch = branch.replace(/[/.]+/g, "-");
|
|
19853
|
+
branch = branch.replace(/-+/g, "-");
|
|
19854
|
+
branch = branch.replace(/^-+|-+$/g, "");
|
|
19855
|
+
branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
|
|
19856
|
+
if (!branch) {
|
|
19857
|
+
throw new Error("Auto-name model returned an empty branch name");
|
|
19858
|
+
}
|
|
19859
|
+
if (!isValidBranchName(branch)) {
|
|
19860
|
+
throw new Error(`Auto-name model returned an invalid branch name: ${branch}`);
|
|
19861
|
+
}
|
|
19862
|
+
return branch;
|
|
19863
|
+
}
|
|
19864
|
+
function getSystemPrompt(config) {
|
|
19865
|
+
return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
19866
|
+
}
|
|
19867
|
+
function buildPrompt(prompt) {
|
|
19868
|
+
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.`;
|
|
19869
|
+
}
|
|
18726
19870
|
|
|
18727
19871
|
class AutoNameService {
|
|
18728
19872
|
spawnImpl;
|
|
18729
19873
|
timeoutMs;
|
|
18730
19874
|
constructor(deps = {}) {
|
|
18731
|
-
this.spawnImpl = deps.spawnImpl
|
|
19875
|
+
this.spawnImpl = deps.spawnImpl;
|
|
18732
19876
|
this.timeoutMs = deps.timeoutMs ?? AUTO_NAME_TIMEOUT_MS;
|
|
18733
19877
|
}
|
|
18734
19878
|
async generateBranchName(config, task) {
|
|
@@ -18738,24 +19882,24 @@ class AutoNameService {
|
|
|
18738
19882
|
}
|
|
18739
19883
|
const systemPrompt = getSystemPrompt(config);
|
|
18740
19884
|
const userPrompt = buildPrompt(prompt);
|
|
18741
|
-
const
|
|
18742
|
-
const
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
if (
|
|
19885
|
+
const cli = llmProviderLabel(config);
|
|
19886
|
+
const runOptions = { timeoutMs: this.timeoutMs };
|
|
19887
|
+
if (this.spawnImpl)
|
|
19888
|
+
runOptions.spawnImpl = this.spawnImpl;
|
|
19889
|
+
const result = await runShortLlmTask(config, systemPrompt, userPrompt, runOptions);
|
|
19890
|
+
if (!result.ok) {
|
|
19891
|
+
if (result.kind === "timeout") {
|
|
18748
19892
|
const fallback = generateFallbackBranchName();
|
|
18749
19893
|
log.warn(`[auto-name] ${cli} timed out after ${this.timeoutMs}ms; using fallback branch ${fallback}`);
|
|
18750
19894
|
return fallback;
|
|
18751
19895
|
}
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
19896
|
+
if (result.kind === "spawn_error") {
|
|
19897
|
+
throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
|
|
19898
|
+
}
|
|
18755
19899
|
const stderr = result.stderr.trim();
|
|
18756
19900
|
const stdout = result.stdout.trim();
|
|
18757
19901
|
const output2 = stderr || stdout || `exit ${result.exitCode}`;
|
|
18758
|
-
const command = args.join(" ");
|
|
19902
|
+
const command = result.args.join(" ");
|
|
18759
19903
|
throw new Error(`${cli} failed (command: ${command}): ${output2}`);
|
|
18760
19904
|
}
|
|
18761
19905
|
const output = result.stdout.trim();
|
|
@@ -18938,6 +20082,7 @@ function makeDefaultState(input) {
|
|
|
18938
20082
|
agentName: input.agentName ?? null,
|
|
18939
20083
|
source: input.source ?? "ui",
|
|
18940
20084
|
oneshot: input.oneshot ?? null,
|
|
20085
|
+
agentTerminalStale: input.agentTerminalStale === true,
|
|
18941
20086
|
git: {
|
|
18942
20087
|
exists: true,
|
|
18943
20088
|
branch: input.branch,
|
|
@@ -18985,6 +20130,8 @@ class ProjectRuntime {
|
|
|
18985
20130
|
existing.baseBranch = input.baseBranch;
|
|
18986
20131
|
existing.profile = input.profile ?? existing.profile;
|
|
18987
20132
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
20133
|
+
if (input.agentTerminalStale !== undefined)
|
|
20134
|
+
existing.agentTerminalStale = input.agentTerminalStale;
|
|
18988
20135
|
if (input.runtime)
|
|
18989
20136
|
existing.agent.runtime = input.runtime;
|
|
18990
20137
|
if (input.source !== undefined)
|
|
@@ -19046,6 +20193,11 @@ class ProjectRuntime {
|
|
|
19046
20193
|
state.prs = prs.map((pr) => clonePrEntry2(pr));
|
|
19047
20194
|
return state;
|
|
19048
20195
|
}
|
|
20196
|
+
setAgentTerminalStale(worktreeId, stale) {
|
|
20197
|
+
const state = this.requireWorktree(worktreeId);
|
|
20198
|
+
state.agentTerminalStale = stale;
|
|
20199
|
+
return state;
|
|
20200
|
+
}
|
|
19049
20201
|
applyEvent(event, now) {
|
|
19050
20202
|
const state = this.requireWorktree(event.worktreeId);
|
|
19051
20203
|
if (event.branch !== state.branch) {
|
|
@@ -19198,6 +20350,7 @@ class ReconciliationService {
|
|
|
19198
20350
|
path: entry.path,
|
|
19199
20351
|
profile: meta?.profile ?? null,
|
|
19200
20352
|
agentName: meta?.agent ?? null,
|
|
20353
|
+
agentTerminalStale: meta?.agentTerminalStale === true,
|
|
19201
20354
|
runtime: meta?.runtime ?? "host",
|
|
19202
20355
|
source: meta?.source ?? "ui",
|
|
19203
20356
|
oneshot: meta?.oneshot ?? null,
|
|
@@ -19233,6 +20386,7 @@ class ReconciliationService {
|
|
|
19233
20386
|
path: state.path,
|
|
19234
20387
|
profile: state.profile,
|
|
19235
20388
|
agentName: state.agentName,
|
|
20389
|
+
agentTerminalStale: state.agentTerminalStale,
|
|
19236
20390
|
runtime: state.runtime,
|
|
19237
20391
|
source: state.source,
|
|
19238
20392
|
oneshot: state.oneshot
|
|
@@ -19489,7 +20643,12 @@ var codexAppServerClient = new CodexAppServerClient({
|
|
|
19489
20643
|
var claudeCliClient = new ClaudeCliClient;
|
|
19490
20644
|
var worktreeConversationService = new WorktreeConversationService({
|
|
19491
20645
|
appServer: codexAppServerClient,
|
|
19492
|
-
git
|
|
20646
|
+
git,
|
|
20647
|
+
resolveLaunchContext: ({ worktree, meta }) => resolveCodexAppServerLaunchContext({
|
|
20648
|
+
worktree,
|
|
20649
|
+
meta,
|
|
20650
|
+
profile: config.profiles[meta.profile]
|
|
20651
|
+
})
|
|
19493
20652
|
});
|
|
19494
20653
|
var claudeConversationService = new ClaudeConversationService({
|
|
19495
20654
|
claude: claudeCliClient,
|
|
@@ -19521,6 +20680,7 @@ async function runOneshotForIssue(issueId) {
|
|
|
19521
20680
|
postToLinearOnDone: { kind: "issue", issueId }
|
|
19522
20681
|
}
|
|
19523
20682
|
});
|
|
20683
|
+
return { branch };
|
|
19524
20684
|
}
|
|
19525
20685
|
function startLinearAutoCreate() {
|
|
19526
20686
|
if (stopLinearAutoCreate)
|
|
@@ -19531,9 +20691,22 @@ function startLinearAutoCreate() {
|
|
|
19531
20691
|
git,
|
|
19532
20692
|
projectRoot: PROJECT_DIR,
|
|
19533
20693
|
runOneshotForIssue,
|
|
20694
|
+
onOneshotPickedUp: postLinearOneshotPickupComment,
|
|
19534
20695
|
...watchTeamKeys && watchTeamKeys.length > 0 ? { watchTeamKeys } : {}
|
|
19535
20696
|
});
|
|
19536
20697
|
}
|
|
20698
|
+
async function postLinearOneshotPickupComment(input) {
|
|
20699
|
+
const body = buildLinearPickupMarkdown({
|
|
20700
|
+
branch: input.branch,
|
|
20701
|
+
pickedUpAt: new Date
|
|
20702
|
+
});
|
|
20703
|
+
const result = await createIssueComment({ issueId: input.issue.id, body });
|
|
20704
|
+
if (!result.ok) {
|
|
20705
|
+
log.warn(`[linear-auto-create] failed to post pickup comment for ${input.issue.identifier}: ${result.error}`);
|
|
20706
|
+
return;
|
|
20707
|
+
}
|
|
20708
|
+
log.info(`[linear-auto-create] posted pickup comment for ${input.issue.identifier}: ${result.data.url}`);
|
|
20709
|
+
}
|
|
19537
20710
|
function normalizeOneshotConfig(input) {
|
|
19538
20711
|
if (!input)
|
|
19539
20712
|
return;
|
|
@@ -19607,7 +20780,7 @@ function parseWsMessage(raw) {
|
|
|
19607
20780
|
try {
|
|
19608
20781
|
const str = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
19609
20782
|
const msg = JSON.parse(str);
|
|
19610
|
-
if (!
|
|
20783
|
+
if (!isRecord3(msg))
|
|
19611
20784
|
return null;
|
|
19612
20785
|
const m = msg;
|
|
19613
20786
|
switch (m.type) {
|
|
@@ -19839,6 +21012,19 @@ function resolveWorktreeTerminalSubmitDelayMs(agentName) {
|
|
|
19839
21012
|
agent: agentName ? getAgentDefinition(config, agentName) : null
|
|
19840
21013
|
});
|
|
19841
21014
|
}
|
|
21015
|
+
async function setAgentTerminalStale(worktree, stale) {
|
|
21016
|
+
const gitDir = git.resolveWorktreeGitDir(worktree.path);
|
|
21017
|
+
const meta = await readWorktreeMeta(gitDir);
|
|
21018
|
+
if (!meta)
|
|
21019
|
+
return;
|
|
21020
|
+
await writeWorktreeMeta(gitDir, {
|
|
21021
|
+
...meta,
|
|
21022
|
+
agentTerminalStale: stale
|
|
21023
|
+
});
|
|
21024
|
+
if (projectRuntime.getWorktree(meta.worktreeId)) {
|
|
21025
|
+
projectRuntime.setAgentTerminalStale(meta.worktreeId, stale);
|
|
21026
|
+
}
|
|
21027
|
+
}
|
|
19842
21028
|
async function apiAttachAgentsWorktree(branch) {
|
|
19843
21029
|
touchDashboardActivity();
|
|
19844
21030
|
const resolved = await resolveAgentsWorktree(branch);
|
|
@@ -19879,7 +21065,15 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
|
|
|
19879
21065
|
if (!chatSupport.ok) {
|
|
19880
21066
|
return errorResponse(chatSupport.error, chatSupport.status);
|
|
19881
21067
|
}
|
|
19882
|
-
|
|
21068
|
+
if (chatSupport.data.provider === "codex") {
|
|
21069
|
+
const sendResult2 = await worktreeConversationService.sendWorktreeConversationMessage(resolved.worktree, parsed.data.text);
|
|
21070
|
+
if (!sendResult2.ok) {
|
|
21071
|
+
return errorResponse(sendResult2.error, sendResult2.status);
|
|
21072
|
+
}
|
|
21073
|
+
await setAgentTerminalStale(resolved.worktree, true);
|
|
21074
|
+
return jsonResponse(sendResult2.data);
|
|
21075
|
+
}
|
|
21076
|
+
const conversationResult = await claudeConversationService.readWorktreeConversation(resolved.worktree);
|
|
19883
21077
|
if (!conversationResult.ok) {
|
|
19884
21078
|
return errorResponse(conversationResult.error, conversationResult.status);
|
|
19885
21079
|
}
|
|
@@ -19909,7 +21103,15 @@ async function apiInterruptAgentsWorktree(branch) {
|
|
|
19909
21103
|
if (!chatSupport.ok) {
|
|
19910
21104
|
return errorResponse(chatSupport.error, chatSupport.status);
|
|
19911
21105
|
}
|
|
19912
|
-
|
|
21106
|
+
if (chatSupport.data.provider === "codex") {
|
|
21107
|
+
const interruptResult2 = await worktreeConversationService.interruptWorktreeConversation(resolved.worktree);
|
|
21108
|
+
if (!interruptResult2.ok) {
|
|
21109
|
+
return errorResponse(interruptResult2.error, interruptResult2.status);
|
|
21110
|
+
}
|
|
21111
|
+
await setAgentTerminalStale(resolved.worktree, true);
|
|
21112
|
+
return jsonResponse(interruptResult2.data);
|
|
21113
|
+
}
|
|
21114
|
+
const conversationResult = await claudeConversationService.readWorktreeConversation(resolved.worktree);
|
|
19913
21115
|
if (!conversationResult.ok) {
|
|
19914
21116
|
return errorResponse(conversationResult.error, conversationResult.status);
|
|
19915
21117
|
}
|
|
@@ -19926,7 +21128,13 @@ async function apiInterruptAgentsWorktree(branch) {
|
|
|
19926
21128
|
interrupted: true
|
|
19927
21129
|
});
|
|
19928
21130
|
}
|
|
19929
|
-
async function
|
|
21131
|
+
async function apiRefreshWorktreeAgentTerminal(branch) {
|
|
21132
|
+
touchDashboardActivity();
|
|
21133
|
+
ensureBranchNotBusy(branch);
|
|
21134
|
+
await lifecycleService.refreshAgentTerminal(branch);
|
|
21135
|
+
return jsonResponse({ ok: true });
|
|
21136
|
+
}
|
|
21137
|
+
async function loadAgentsConversationInitialState(branch) {
|
|
19930
21138
|
const resolved = await resolveAgentsWorktree(branch);
|
|
19931
21139
|
if (!resolved.ok) {
|
|
19932
21140
|
return {
|
|
@@ -19949,7 +21157,7 @@ async function readErrorMessage(response) {
|
|
|
19949
21157
|
if (contentType.includes("application/json")) {
|
|
19950
21158
|
try {
|
|
19951
21159
|
const body = await response.json();
|
|
19952
|
-
if (
|
|
21160
|
+
if (isRecord3(body) && typeof body.error === "string" && body.error.length > 0) {
|
|
19953
21161
|
return body.error;
|
|
19954
21162
|
}
|
|
19955
21163
|
} catch {}
|
|
@@ -19957,45 +21165,55 @@ async function readErrorMessage(response) {
|
|
|
19957
21165
|
const text = await response.text();
|
|
19958
21166
|
return text.length > 0 ? text : `HTTP ${response.status}`;
|
|
19959
21167
|
}
|
|
21168
|
+
function nextConversationMessageOrder(messages) {
|
|
21169
|
+
return messages.reduce((order, message) => Math.max(order, message.order + 1), 0);
|
|
21170
|
+
}
|
|
19960
21171
|
async function openAgentsSocket(ws, data) {
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
|
|
21172
|
+
let bufferingNotifications = true;
|
|
21173
|
+
let socketClosed = false;
|
|
21174
|
+
let streamSession = null;
|
|
21175
|
+
const bufferedNotifications = [];
|
|
21176
|
+
const unsubscribeNotifications = codexAppServerClient.onNotification((notification) => {
|
|
21177
|
+
if (bufferingNotifications || !streamSession) {
|
|
21178
|
+
bufferedNotifications.push(notification);
|
|
21179
|
+
return;
|
|
21180
|
+
}
|
|
21181
|
+
const notificationThreadId = readAgentsNotificationThreadId(notification);
|
|
21182
|
+
if (!notificationThreadId || notificationThreadId !== data.conversationId)
|
|
21183
|
+
return;
|
|
21184
|
+
streamSession.handleNotification(notification);
|
|
21185
|
+
data.conversationId = streamSession.currentConversationId();
|
|
21186
|
+
});
|
|
21187
|
+
data.unsubscribe = () => {
|
|
21188
|
+
socketClosed = true;
|
|
21189
|
+
streamSession?.close();
|
|
21190
|
+
unsubscribeNotifications();
|
|
21191
|
+
};
|
|
21192
|
+
const initialState = await loadAgentsConversationInitialState(data.branch);
|
|
21193
|
+
if (socketClosed)
|
|
21194
|
+
return;
|
|
21195
|
+
if (!initialState.ok) {
|
|
21196
|
+
unsubscribeNotifications();
|
|
21197
|
+
sendAgentsWs(ws, { type: "error", message: initialState.message });
|
|
21198
|
+
ws.close(1011, initialState.message.slice(0, 123));
|
|
19965
21199
|
return;
|
|
19966
21200
|
}
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
21201
|
+
streamSession = new AgentsConversationStreamSession({
|
|
21202
|
+
conversationId: initialState.data.conversation.conversationId,
|
|
21203
|
+
nextOrder: nextConversationMessageOrder(initialState.data.conversation.messages),
|
|
21204
|
+
send: (event) => sendAgentsWs(ws, event)
|
|
19971
21205
|
});
|
|
19972
|
-
|
|
21206
|
+
data.conversationId = streamSession.currentConversationId();
|
|
21207
|
+
if (initialState.data.conversation.provider !== "codexAppServer") {
|
|
21208
|
+
unsubscribeNotifications();
|
|
21209
|
+
data.unsubscribe = null;
|
|
19973
21210
|
return;
|
|
19974
21211
|
}
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
if (deltaEvent) {
|
|
19981
|
-
sendAgentsWs(ws, deltaEvent);
|
|
19982
|
-
return;
|
|
19983
|
-
}
|
|
19984
|
-
if (!shouldRefreshAgentsConversationSnapshot(notification))
|
|
19985
|
-
return;
|
|
19986
|
-
(async () => {
|
|
19987
|
-
const nextSnapshot = await loadAgentsConversationSnapshot(data.branch);
|
|
19988
|
-
if (!nextSnapshot.ok) {
|
|
19989
|
-
sendAgentsWs(ws, { type: "error", message: nextSnapshot.message });
|
|
19990
|
-
return;
|
|
19991
|
-
}
|
|
19992
|
-
data.conversationId = nextSnapshot.data.conversation.conversationId;
|
|
19993
|
-
sendAgentsWs(ws, {
|
|
19994
|
-
type: "snapshot",
|
|
19995
|
-
data: nextSnapshot.data
|
|
19996
|
-
});
|
|
19997
|
-
})();
|
|
19998
|
-
});
|
|
21212
|
+
bufferingNotifications = false;
|
|
21213
|
+
for (const notification of bufferedNotifications) {
|
|
21214
|
+
streamSession.handleNotification(notification);
|
|
21215
|
+
data.conversationId = streamSession.currentConversationId();
|
|
21216
|
+
}
|
|
19999
21217
|
}
|
|
20000
21218
|
async function apiRuntimeEvent(req) {
|
|
20001
21219
|
if (!await hasValidControlToken(req)) {
|
|
@@ -20442,6 +21660,11 @@ async function apiGetLinearIssues() {
|
|
|
20442
21660
|
return errorResponse(result.error, 502);
|
|
20443
21661
|
return jsonResponse(result.data);
|
|
20444
21662
|
}
|
|
21663
|
+
function apiGetAutoNameConfig() {
|
|
21664
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
21665
|
+
const linearAvailability = !config.integrations.linear.enabled ? "disabled" : !apiKey?.trim() ? "missing_api_key" : "ready";
|
|
21666
|
+
return jsonResponse({ autoName: config.autoName, linearAvailability });
|
|
21667
|
+
}
|
|
20445
21668
|
var MAX_DIFF_BYTES = 200 * 1024;
|
|
20446
21669
|
async function apiGetWorktreeDiff(name) {
|
|
20447
21670
|
await reconciliationService.reconcile(PROJECT_DIR);
|
|
@@ -20696,6 +21919,15 @@ function startServer(port) {
|
|
|
20696
21919
|
return catching(`POST /api/worktrees/${name}/close`, () => apiCloseWorktree(name));
|
|
20697
21920
|
}
|
|
20698
21921
|
},
|
|
21922
|
+
[apiPaths.refreshWorktreeAgentTerminal]: {
|
|
21923
|
+
POST: (req) => {
|
|
21924
|
+
const parsed = parseWorktreeNameParam(req.params);
|
|
21925
|
+
if (!parsed.ok)
|
|
21926
|
+
return parsed.response;
|
|
21927
|
+
const name = parsed.data;
|
|
21928
|
+
return catching(`POST /api/worktrees/${name}/agent-terminal/refresh`, () => apiRefreshWorktreeAgentTerminal(name));
|
|
21929
|
+
}
|
|
21930
|
+
},
|
|
20699
21931
|
[apiPaths.setWorktreeArchived]: {
|
|
20700
21932
|
PUT: (req) => {
|
|
20701
21933
|
const parsed = parseWorktreeNameParam(req.params);
|
|
@@ -20771,6 +22003,9 @@ function startServer(port) {
|
|
|
20771
22003
|
[apiPaths.fetchLinearIssues]: {
|
|
20772
22004
|
GET: () => catching("GET /api/linear/issues", () => apiGetLinearIssues())
|
|
20773
22005
|
},
|
|
22006
|
+
[apiPaths.fetchAutoNameConfig]: {
|
|
22007
|
+
GET: () => catching("GET /api/project/auto-name", async () => apiGetAutoNameConfig())
|
|
22008
|
+
},
|
|
20774
22009
|
[apiPaths.setLinearAutoCreate]: {
|
|
20775
22010
|
PUT: (req) => catching("PUT /api/linear/auto-create", () => apiSetLinearAutoCreate(req))
|
|
20776
22011
|
},
|