webmux 0.33.0 → 0.35.0

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