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.
@@ -6965,7 +6965,7 @@ import { networkInterfaces } from "os";
6965
6965
  // package.json
6966
6966
  var package_default = {
6967
6967
  name: "webmux",
6968
- version: "0.34.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+94e40505b11febf1/node_modules/@ts-rest/core/index.esm.mjs
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.optional(),
11394
- toolName: exports_external.string().optional()
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 isRecord(raw) {
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 (!isRecord(entry))
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" || !isRecord(raw.message))
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" || !isRecord(raw.message))
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" || !isRecord(raw.message))
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 join(home, ".claude", "projects");
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) => join(dir, entry.name));
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 = join(projectsRoot, encodeClaudeProjectDir(cwd), `${sessionId}.jsonl`);
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 = join(projectsRoot, entry.name, `${sessionId}.jsonl`);
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 (!isRecord(entry) || entry.type !== "tool_result")
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 (!isRecord(block))
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 = join(projectsRoot, encodeClaudeProjectDir(cwd));
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(join(projectsRoot, entry.name));
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 (!isRecord(parsed))
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" && isRecord(parsed.event)) {
12833
+ if (parsed.type === "stream_event" && isRecord2(parsed.event)) {
12557
12834
  const event = parsed.event;
12558
- if (event.type === "content_block_delta" && isRecord(event.delta) && event.delta.type === "text_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 isRecord2(raw) {
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
- phase: exports_external.string(),
12608
- memoryCitation: UnknownValueSchema
12609
- }).transform((value) => ({
12610
- type: value.type,
12611
- id: value.id,
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
- id: exports_external.string()
12898
+ command: exports_external.string().optional(),
12899
+ path: exports_external.string().nullable().optional()
12619
12900
  });
12620
- var CodexAppServerThreadItemSchema = exports_external.union([
12621
- CodexAppServerUserMessageItemSchema,
12622
- CodexAppServerAgentMessageItemSchema,
12623
- CodexAppServerGenericItemSchema
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 CodexAppServerTurnSchema = exports_external.object({
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
- buffer += this.decoder.decode(value);
12820
- while (true) {
12821
- const newlineIndex = buffer.indexOf(`
12822
- `);
12823
- if (newlineIndex === -1)
12824
- break;
12825
- const line = buffer.slice(0, newlineIndex).trim();
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 = this.decoder.decode(value).trim();
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 (!isRecord2(parsed)) {
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 (!isRecord2(raw.error))
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 join2, resolve } from "path";
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 isRecord3(value) {
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 (!isRecord3(raw))
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(isRecord3).filter((entry) => typeof entry.hostPath === "string" && entry.hostPath.length > 0).map((entry) => ({
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 (!isRecord3(raw)) {
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 (!isRecord3(raw))
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 (!isRecord3(raw))
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 (!isRecord3(raw))
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(isRecord3).filter((entry) => typeof entry.name === "string" && typeof entry.portEnv === "string").map((entry) => ({
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 (!isRecord3(raw))
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 (!isRecord3(raw))
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 (!isRecord3(raw))
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 (!isRecord3(raw))
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 (!isRecord3(raw))
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(isRecord3).filter((entry) => typeof entry.repo === "string").map((entry) => ({
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(join2(root, ".webmux.yaml"), "utf8");
13683
+ return readFileSync(join3(root, ".webmux.yaml"), "utf8");
13275
13684
  }
13276
13685
  function readLocalConfigFile(root) {
13277
- return readFileSync(join2(root, ".webmux.local.yaml"), "utf8");
13686
+ return readFileSync(join3(root, ".webmux.local.yaml"), "utf8");
13278
13687
  }
13279
13688
  function parseConfigDocument(text) {
13280
13689
  const parsed = $parse(text);
13281
- return isRecord3(parsed) ? parsed : {};
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: isRecord3(parsed.workspace) && typeof parsed.workspace.mainBranch === "string" ? parsed.workspace.mainBranch : DEFAULT_CONFIG.workspace.mainBranch,
13288
- worktreeRoot: isRecord3(parsed.workspace) && typeof parsed.workspace.worktreeRoot === "string" ? parsed.workspace.worktreeRoot : DEFAULT_CONFIG.workspace.worktreeRoot,
13289
- defaultAgent: isRecord3(parsed.workspace) ? parseAgentKind(parsed.workspace.defaultAgent) : DEFAULT_CONFIG.workspace.defaultAgent,
13290
- autoPull: isRecord3(parsed.workspace) ? parseAutoPull(parsed.workspace.autoPull) : DEFAULT_CONFIG.workspace.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: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord3(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : [],
13299
- autoRemoveOnMerge: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) && typeof parsed.integrations.github.autoRemoveOnMerge === "boolean" ? parsed.integrations.github.autoRemoveOnMerge : DEFAULT_CONFIG.integrations.github.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 = isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) ? parsed.integrations.linear : null;
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 (!isRecord3(parsed.integrations))
13745
+ if (!isRecord4(parsed.integrations))
13337
13746
  return null;
13338
13747
  const linear = parsed.integrations.linear;
13339
- if (!isRecord3(linear))
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 (!isRecord3(parsed.integrations))
13763
+ if (!isRecord4(parsed.integrations))
13355
13764
  return null;
13356
13765
  const github = parsed.integrations.github;
13357
- if (!isRecord3(github))
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 (!isRecord3(parsed.workspace))
13774
+ if (!isRecord4(parsed.workspace))
13366
13775
  return null;
13367
13776
  const autoPull = parsed.workspace.autoPull;
13368
- if (!isRecord3(autoPull))
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 = isRecord3(parsed.workspace) ? parsed.workspace : null;
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 = join2(root, ".webmux.local.yaml");
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 = isRecord3(existing.integrations) ? { ...existing.integrations } : {};
13478
- const linear = isRecord3(integrations.linear) ? { ...integrations.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 = isRecord3(existing.integrations) ? { ...existing.integrations } : {};
13488
- const github = isRecord3(integrations.github) ? { ...integrations.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 = isRecord3(existing.agents) ? { ...existing.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 (!isRecord3(existing.agents) || !(agentId in existing.agents)) {
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(AgentsUiConversationMessageSchema)
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 = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
14660
- if (!parsed.success) {
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.data };
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
- // backend/src/adapters/fs.ts
14679
- import { mkdir as mkdir2 } from "fs/promises";
14680
- import { join as join3 } from "path";
14681
- var SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
14682
- var DOTENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/;
14683
- function stringifyAllocatedPorts(ports) {
14684
- const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
14685
- return Object.fromEntries(entries);
14686
- }
14687
- function quoteEnvValue(value) {
14688
- if (value.length > 0 && SAFE_ENV_VALUE_RE.test(value))
14689
- return value;
15100
+ var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
15101
+ function shellQuote(value) {
14690
15102
  return `'${value.replaceAll("'", "'\\''")}'`;
14691
15103
  }
14692
- function parseDotenv(content) {
14693
- const env = {};
14694
- for (const line of content.split(`
14695
- `)) {
14696
- if (line.trimStart().startsWith("#"))
14697
- continue;
14698
- const match = DOTENV_LINE_RE.exec(line);
14699
- if (!match)
14700
- continue;
14701
- const key = match[1];
14702
- let value = match[2];
14703
- if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
14704
- value = value.slice(1, -1);
14705
- } else {
14706
- value = value.trimEnd();
14707
- }
14708
- env[key] = value;
14709
- }
14710
- return env;
14711
- }
14712
- async function loadDotenvLocal(worktreePath) {
14713
- try {
14714
- const content = await Bun.file(join3(worktreePath, ".env.local")).text();
14715
- return parseDotenv(content);
14716
- } catch {
14717
- return {};
14718
- }
14719
- }
14720
- function getWorktreeStoragePaths(gitDir) {
14721
- const webmuxDir = join3(gitDir, "webmux");
14722
- return {
14723
- gitDir,
14724
- webmuxDir,
14725
- metaPath: join3(webmuxDir, "meta.json"),
14726
- runtimeEnvPath: join3(webmuxDir, "runtime.env"),
14727
- controlEnvPath: join3(webmuxDir, "control.env"),
14728
- prsPath: join3(webmuxDir, "prs.json")
14729
- };
14730
- }
14731
- function getProjectArchiveStatePath(gitDir) {
14732
- return join3(gitDir, "webmux", "archive.json");
14733
- }
14734
- async function ensureWorktreeStorageDirs(gitDir) {
14735
- const paths = getWorktreeStoragePaths(gitDir);
14736
- await mkdir2(paths.webmuxDir, { recursive: true });
14737
- return paths;
14738
- }
14739
- async function readWorktreeMeta(gitDir) {
14740
- const { metaPath } = getWorktreeStoragePaths(gitDir);
14741
- try {
14742
- const raw = await Bun.file(metaPath).json();
14743
- return normalizeWorktreeMeta(raw);
14744
- } catch {
14745
- return null;
14746
- }
14747
- }
14748
- async function writeWorktreeMeta(gitDir, meta) {
14749
- const { metaPath } = await ensureWorktreeStorageDirs(gitDir);
14750
- await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
14751
- `);
14752
- }
14753
- function isArchivedWorktreeEntry(raw) {
14754
- return isRecord4(raw) && typeof raw.path === "string" && typeof raw.archivedAt === "string";
14755
- }
14756
- function emptyWorktreeArchiveState() {
14757
- return {
14758
- schemaVersion: WORKTREE_ARCHIVE_STATE_VERSION,
14759
- entries: []
14760
- };
14761
- }
14762
- function isWorktreeArchiveState(raw) {
14763
- return isRecord4(raw) && typeof raw.schemaVersion === "number" && Array.isArray(raw.entries) && raw.entries.every((entry) => isArchivedWorktreeEntry(entry));
14764
- }
14765
- async function readWorktreeArchiveState(gitDir) {
14766
- const archivePath = getProjectArchiveStatePath(gitDir);
14767
- try {
14768
- const raw = await Bun.file(archivePath).json();
14769
- return isWorktreeArchiveState(raw) ? {
14770
- schemaVersion: raw.schemaVersion,
14771
- entries: raw.entries.map((entry) => ({ ...entry }))
14772
- } : emptyWorktreeArchiveState();
14773
- } catch {
14774
- return emptyWorktreeArchiveState();
14775
- }
14776
- }
14777
- async function writeWorktreeArchiveState(gitDir, state) {
14778
- const archivePath = getProjectArchiveStatePath(gitDir);
14779
- await ensureWorktreeStorageDirs(gitDir);
14780
- await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
14781
- `);
14782
- }
14783
- function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
14784
- return {
14785
- ...dotenvValues,
14786
- ...meta.startupEnvValues,
14787
- ...stringifyAllocatedPorts(meta.allocatedPorts),
14788
- ...extraEnv,
14789
- WEBMUX_WORKTREE_ID: meta.worktreeId,
14790
- WEBMUX_BRANCH: meta.branch,
14791
- WEBMUX_PROFILE: meta.profile,
14792
- WEBMUX_AGENT: meta.agent,
14793
- WEBMUX_RUNTIME: meta.runtime
14794
- };
14795
- }
14796
- function buildControlEnvMap(input) {
14797
- return {
14798
- WEBMUX_CONTROL_URL: input.controlUrl,
14799
- WEBMUX_CONTROL_TOKEN: input.controlToken,
14800
- WEBMUX_WORKTREE_ID: input.worktreeId,
14801
- WEBMUX_BRANCH: input.branch
14802
- };
14803
- }
14804
- function renderEnvFile(env) {
14805
- const lines = Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${quoteEnvValue(value)}`);
14806
- return lines.join(`
14807
- `) + `
14808
- `;
14809
- }
14810
- async function writeRuntimeEnv(gitDir, env) {
14811
- const { runtimeEnvPath } = await ensureWorktreeStorageDirs(gitDir);
14812
- await Bun.write(runtimeEnvPath, renderEnvFile(env));
14813
- }
14814
- async function writeControlEnv(gitDir, env) {
14815
- const { controlEnvPath } = await ensureWorktreeStorageDirs(gitDir);
14816
- await Bun.write(controlEnvPath, renderEnvFile(env));
14817
- }
14818
- function isRecord4(raw) {
14819
- return typeof raw === "object" && raw !== null && !Array.isArray(raw);
14820
- }
14821
- function normalizeConversationMeta(raw) {
14822
- if (!raw)
14823
- return raw;
14824
- if (raw.provider === "codexAppServer") {
14825
- const conversationId2 = raw.conversationId || raw.threadId;
14826
- const threadId = raw.threadId || raw.conversationId;
14827
- if (!conversationId2 || !threadId)
14828
- return;
14829
- const normalized2 = {
14830
- provider: "codexAppServer",
14831
- conversationId: conversationId2,
14832
- threadId,
14833
- cwd: raw.cwd,
14834
- lastSeenAt: raw.lastSeenAt
14835
- };
14836
- return normalized2;
14837
- }
14838
- const conversationId = raw.conversationId || raw.sessionId;
14839
- const sessionId = raw.sessionId || raw.conversationId;
14840
- if (!conversationId || !sessionId)
14841
- return;
14842
- const normalized = {
14843
- provider: "claudeCode",
14844
- conversationId,
14845
- sessionId,
14846
- cwd: raw.cwd,
14847
- lastSeenAt: raw.lastSeenAt
14848
- };
14849
- return normalized;
14850
- }
14851
- function normalizeOptionalString(raw) {
14852
- return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
14853
- }
14854
- function normalizeWorktreeMeta(meta) {
14855
- const conversation = normalizeConversationMeta(meta.conversation);
14856
- const normalizedLabel = normalizeOptionalString(meta.label);
14857
- if (conversation === meta.conversation && normalizedLabel === meta.label) {
14858
- return meta;
14859
- }
14860
- const rest = { ...meta };
14861
- delete rest.label;
14862
- delete rest.conversation;
14863
- return {
14864
- ...rest,
14865
- ...normalizedLabel ? { label: normalizedLabel } : {},
14866
- ...conversation !== undefined ? { conversation } : {}
14867
- };
14868
- }
14869
- function isPrComment(raw) {
14870
- if (!isRecord4(raw))
14871
- return false;
14872
- return (raw.type === "comment" || raw.type === "inline") && typeof raw.author === "string" && typeof raw.body === "string" && typeof raw.createdAt === "string" && (raw.path === undefined || typeof raw.path === "string") && (raw.line === undefined || raw.line === null || typeof raw.line === "number") && (raw.diffHunk === undefined || typeof raw.diffHunk === "string") && (raw.isReply === undefined || typeof raw.isReply === "boolean");
14873
- }
14874
- function isCiCheck(raw) {
14875
- if (!isRecord4(raw))
14876
- return false;
14877
- return typeof raw.name === "string" && (raw.status === "pending" || raw.status === "success" || raw.status === "failed" || raw.status === "skipped") && (raw.url === null || typeof raw.url === "string") && (raw.runId === null || typeof raw.runId === "number");
14878
- }
14879
- function isPrEntry(raw) {
14880
- if (!isRecord4(raw))
14881
- return false;
14882
- return typeof raw.repo === "string" && typeof raw.number === "number" && (raw.state === "open" || raw.state === "closed" || raw.state === "merged") && typeof raw.url === "string" && typeof raw.updatedAt === "string" && (raw.ciStatus === "none" || raw.ciStatus === "pending" || raw.ciStatus === "success" || raw.ciStatus === "failed") && Array.isArray(raw.ciChecks) && raw.ciChecks.every((check) => isCiCheck(check)) && Array.isArray(raw.comments) && raw.comments.every((comment) => isPrComment(comment));
14883
- }
14884
- async function readWorktreePrs(gitDir) {
14885
- const { prsPath } = getWorktreeStoragePaths(gitDir);
14886
- try {
14887
- const raw = await Bun.file(prsPath).json();
14888
- return Array.isArray(raw) && raw.every((entry) => isPrEntry(entry)) ? raw : [];
14889
- } catch {
14890
- return [];
14891
- }
14892
- }
14893
- async function writeWorktreePrs(gitDir, prs) {
14894
- const { prsPath } = await ensureWorktreeStorageDirs(gitDir);
14895
- await Bun.write(prsPath, JSON.stringify(prs, null, 2) + `
14896
- `);
14897
- }
14898
-
14899
- // backend/src/adapters/agent-runtime.ts
14900
- 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 = /^[A-Za-z_][A-Za-z0-9_]*$/;
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
- return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
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/worktree-conversation-result.ts
17871
- function ok(data) {
17872
- return { ok: true, data };
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 isClaudeConversationMeta(meta) {
17883
- return meta?.provider === "claudeCode";
17884
- }
17885
- function buildPendingConversationId(worktree) {
17886
- return `claude-pending:${worktree.path}`;
17887
- }
17888
- function buildClaudeConversationMeta(sessionId, cwd, now) {
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
- provider: "claudeCode",
17891
- conversationId: sessionId,
17892
- sessionId,
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 sameConversationMeta(left, right) {
17898
- return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
17899
- }
17900
- function normalizeSessionMessages(messages) {
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 buildConversationState(worktree, session) {
17907
- return {
17908
- provider: "claudeCode",
17909
- conversationId: session?.sessionId ?? buildPendingConversationId(worktree),
17910
- cwd: worktree.path,
17911
- running: false,
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 toWorktreeConversationResponse(worktree, conversationMeta, session) {
17917
- return {
17918
- worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
17919
- conversation: buildConversationState(worktree, session)
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
- class ClaudeConversationService {
17924
- deps;
17925
- now;
17926
- readMeta;
17927
- writeMeta;
17928
- constructor(deps) {
17929
- this.deps = deps;
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
- async withResolvedConversation(worktree, fn) {
17941
- if (!isClaudeWorktree(worktree)) {
17942
- return err(409, "Worktree chat is only available for Claude worktrees");
17943
- }
17944
- try {
17945
- const resolved = await this.resolveConversation(worktree);
17946
- if (!resolved.ok)
17947
- return resolved;
17948
- return await fn(resolved.data);
17949
- } catch (error) {
17950
- const message = error instanceof Error ? error.message : String(error);
17951
- return err(502, message);
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
- async resolveConversation(worktree) {
17955
- const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
17956
- const meta = await this.readMeta(gitDir);
17957
- if (!meta) {
17958
- return err(409, "Worktree metadata is missing");
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
- const session = await this.resolveSession(meta, worktree.path);
17961
- const conversationMeta = session ? await this.persistConversationMeta(gitDir, meta, worktree.path, session.sessionId) : null;
17962
- return ok({
17963
- conversationMeta,
17964
- session
17965
- });
17966
- }
17967
- async resolveSession(meta, cwd) {
17968
- const savedSessionId = isClaudeConversationMeta(meta.conversation) ? meta.conversation.sessionId : null;
17969
- const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
17970
- if (discovered && discovered.sessionId !== savedSessionId) {
17971
- const session = await this.deps.claude.readSession(discovered.sessionId, cwd);
17972
- if (session)
17973
- return session;
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 (savedSessionId) {
17976
- const savedSession = await this.deps.claude.readSession(savedSessionId, cwd);
17977
- if (savedSession)
17978
- return savedSession;
17979
- log.warn(`[agents] saved Claude session missing, rediscovering cwd=${cwd} sessionId=${savedSessionId}`);
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 (!discovered)
17982
- return null;
17983
- return await this.deps.claude.readSession(discovered.sessionId, cwd);
17984
- }
17985
- async persistConversationMeta(gitDir, meta, cwd, sessionId) {
17986
- const nextConversation = buildClaudeConversationMeta(sessionId, cwd, this.now());
17987
- if (!sameConversationMeta(meta.conversation, nextConversation)) {
17988
- await this.writeMeta(gitDir, {
17989
- ...meta,
17990
- conversation: nextConversation
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
- if (isUserMessageItem(item)) {
18034
- const text = extractUserText(item);
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
- role: "assistant",
18055
- text: item.text,
18056
- status: isActiveTurnStatus(turn.status) ? "inProgress" : "completed",
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 buildConversationState2(thread) {
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: buildConversationMessages(thread)
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 buildConversationMeta(thread, now) {
19201
+ function buildPendingConversationId(worktree) {
19202
+ return `claude-pending:${worktree.path}`;
19203
+ }
19204
+ function buildClaudeConversationMeta(sessionId, cwd, now) {
18080
19205
  return {
18081
- provider: "codexAppServer",
18082
- conversationId: thread.id,
18083
- threadId: thread.id,
18084
- cwd: thread.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 toWorktreeConversationResponse2(worktree, conversationMeta, thread) {
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(thread)
19237
+ conversation: buildConversationState2(worktree, session)
18095
19238
  };
18096
19239
  }
18097
19240
 
18098
- class WorktreeConversationService {
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, true, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
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, false, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
19256
+ return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
18114
19257
  }
18115
- async withResolvedConversation(worktree, allowCreate, fn) {
18116
- if (!isCodexWorktree(worktree)) {
18117
- return err(409, "Worktree chat is only available for Codex worktrees");
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, allowCreate);
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, allowCreate) {
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 now = this.now();
18136
- const thread = await this.resolveThread(meta, worktree.path, allowCreate);
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
- gitDir,
18147
- meta: nextMeta,
18148
- thread,
18149
- conversationMeta: nextMeta.conversation ?? conversationMeta
19281
+ conversationMeta,
19282
+ session
18150
19283
  });
18151
19284
  }
18152
- async resolveThread(meta, cwd, allowCreate) {
18153
- const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
18154
- cwd,
18155
- limit: 20,
18156
- sortKey: "updated_at"
18157
- })).data);
18158
- if (discoveredThread) {
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
- const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
18162
- if (savedThreadId) {
18163
- const savedThread = await this.tryLoadThread(savedThreadId, cwd);
18164
- if (savedThread)
18165
- return savedThread;
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 (!allowCreate)
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 ensureThreadLoaded(threadId, cwd) {
18186
- const initial = await this.deps.appServer.threadRead(threadId, false);
18187
- if (initial.thread.status.type === "notLoaded") {
18188
- await this.deps.appServer.threadResume({
18189
- threadId,
18190
- cwd,
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 (await this.deps.appServer.threadRead(threadId, true)).thread;
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/auto-name-service.ts
18609
- var MAX_BRANCH_LENGTH = 40;
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(`Auto-name timed out after ${timeoutMs}ms`);
19728
+ super(`LLM spawn timed out after ${timeoutMs}ms`);
18647
19729
  this.timeoutMs = timeoutMs;
18648
19730
  }
18649
19731
  }
18650
- async function defaultSpawn(args, options = {}) {
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
- if (options.timeoutMs === undefined) {
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 AutoNameTimeoutError(options.timeoutMs));
18673
- }, options.timeoutMs);
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
- function buildPrompt(prompt) {
18710
- 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.`;
18711
- }
18712
- function buildCodexArgs(model, systemPrompt, prompt) {
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(prompt);
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 ?? defaultSpawn;
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 args = config.provider === "claude" ? buildClaudeArgs(config.model, systemPrompt, userPrompt) : buildCodexArgs(config.model, systemPrompt, userPrompt);
18742
- const cli = config.provider === "claude" ? "claude" : "codex";
18743
- let result;
18744
- try {
18745
- result = await this.spawnImpl(args, { timeoutMs: this.timeoutMs });
18746
- } catch (error) {
18747
- if (error instanceof AutoNameTimeoutError) {
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
- throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
18753
- }
18754
- if (result.exitCode !== 0) {
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 (!isRecord2(msg))
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
- const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
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
- const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
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 loadAgentsConversationSnapshot(branch) {
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 (isRecord2(body) && typeof body.error === "string" && body.error.length > 0) {
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
- const snapshot = await loadAgentsConversationSnapshot(data.branch);
19962
- if (!snapshot.ok) {
19963
- sendAgentsWs(ws, { type: "error", message: snapshot.message });
19964
- ws.close(1011, snapshot.message.slice(0, 123));
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
- data.conversationId = snapshot.data.conversation.conversationId;
19968
- sendAgentsWs(ws, {
19969
- type: "snapshot",
19970
- data: snapshot.data
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
- if (snapshot.data.conversation.provider !== "codexAppServer") {
21206
+ data.conversationId = streamSession.currentConversationId();
21207
+ if (initialState.data.conversation.provider !== "codexAppServer") {
21208
+ unsubscribeNotifications();
21209
+ data.unsubscribe = null;
19973
21210
  return;
19974
21211
  }
19975
- data.unsubscribe = codexAppServerClient.onNotification((notification) => {
19976
- const notificationThreadId = readAgentsNotificationThreadId(notification);
19977
- if (!notificationThreadId || notificationThreadId !== data.conversationId)
19978
- return;
19979
- const deltaEvent = buildAgentsUiMessageDeltaEvent(notification);
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
  },