@spencer-kit/coder-studio 0.4.4 → 0.4.5

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.
@@ -1097,6 +1097,281 @@ var init_session_store = __esm({
1097
1097
  }
1098
1098
  });
1099
1099
 
1100
+ // packages/server/src/uploads/paths.ts
1101
+ import { randomUUID as randomUUID2 } from "node:crypto";
1102
+ import { lstat, mkdir } from "node:fs/promises";
1103
+ import path4 from "node:path";
1104
+ function sanitizeOriginalName(input) {
1105
+ let sanitized = "";
1106
+ for (const char of input.trim()) {
1107
+ sanitized += KEEP_FILENAME_CHAR.test(char) ? char : "_";
1108
+ }
1109
+ sanitized = sanitized.replace(/^\.+/, "");
1110
+ if (sanitized.length === 0 || /^[_\s]*$/.test(sanitized)) {
1111
+ return "file";
1112
+ }
1113
+ if (sanitized.length <= MAX_FILENAME_LENGTH) {
1114
+ return sanitized;
1115
+ }
1116
+ const lastDot = sanitized.lastIndexOf(".");
1117
+ if (lastDot > 0 && sanitized.length - lastDot <= 16) {
1118
+ const ext = sanitized.slice(lastDot);
1119
+ const stem = sanitized.slice(0, MAX_FILENAME_LENGTH - ext.length);
1120
+ return stem + ext;
1121
+ }
1122
+ return sanitized.slice(0, MAX_FILENAME_LENGTH);
1123
+ }
1124
+ function validateWorkspaceId(id) {
1125
+ if (!WORKSPACE_ID_RE.test(id)) {
1126
+ throw new Error(`invalid workspace id: ${JSON.stringify(id)}`);
1127
+ }
1128
+ }
1129
+ async function assertDirectorySegmentSafe(segmentPath) {
1130
+ const info = await lstat(segmentPath);
1131
+ if (info.isSymbolicLink()) {
1132
+ throw new Error(`symlinked upload path segment is not allowed: ${segmentPath}`);
1133
+ }
1134
+ if (!info.isDirectory()) {
1135
+ throw new Error(`upload path segment is not a directory: ${segmentPath}`);
1136
+ }
1137
+ }
1138
+ async function ensureSafeUploadDir(rootDir, targetDir2) {
1139
+ const resolvedRoot = path4.resolve(rootDir);
1140
+ const resolvedTarget = path4.resolve(targetDir2);
1141
+ if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path4.sep}`)) {
1142
+ throw new Error(`target dir escaped uploads root: ${resolvedTarget}`);
1143
+ }
1144
+ try {
1145
+ await assertDirectorySegmentSafe(resolvedRoot);
1146
+ } catch (error) {
1147
+ const code = error.code;
1148
+ if (code !== "ENOENT") {
1149
+ throw error;
1150
+ }
1151
+ await mkdir(resolvedRoot, { recursive: true });
1152
+ await assertDirectorySegmentSafe(resolvedRoot);
1153
+ }
1154
+ const relative5 = path4.relative(resolvedRoot, resolvedTarget);
1155
+ if (!relative5) {
1156
+ return;
1157
+ }
1158
+ let current = resolvedRoot;
1159
+ for (const segment of relative5.split(path4.sep)) {
1160
+ current = path4.join(current, segment);
1161
+ try {
1162
+ await assertDirectorySegmentSafe(current);
1163
+ continue;
1164
+ } catch (error) {
1165
+ const code = error.code;
1166
+ if (code !== "ENOENT") {
1167
+ throw error;
1168
+ }
1169
+ }
1170
+ try {
1171
+ await mkdir(current);
1172
+ } catch (error) {
1173
+ const code = error.code;
1174
+ if (code !== "EEXIST") {
1175
+ throw error;
1176
+ }
1177
+ }
1178
+ await assertDirectorySegmentSafe(current);
1179
+ }
1180
+ }
1181
+ function generateBucketPath(input) {
1182
+ validateWorkspaceId(input.workspaceId);
1183
+ const now = input.now ?? /* @__PURE__ */ new Date();
1184
+ const dateStr = now.toISOString().slice(0, 10);
1185
+ const dir = path4.join(input.uploadsDir, input.workspaceId, dateStr);
1186
+ const sanitizedName = sanitizeOriginalName(input.originalName);
1187
+ const uuid8 = randomUUID2().replace(/-/g, "").slice(0, 8);
1188
+ const absolutePath = path4.resolve(dir, `${uuid8}-${sanitizedName}`);
1189
+ const uploadsRoot = `${path4.resolve(input.uploadsDir)}${path4.sep}`;
1190
+ if (!absolutePath.startsWith(uploadsRoot)) {
1191
+ throw new Error(`generated upload path escaped uploads root: ${absolutePath}`);
1192
+ }
1193
+ return {
1194
+ dir,
1195
+ absolutePath,
1196
+ uuid8,
1197
+ sanitizedName
1198
+ };
1199
+ }
1200
+ var MAX_FILENAME_LENGTH, KEEP_FILENAME_CHAR, WORKSPACE_ID_RE;
1201
+ var init_paths = __esm({
1202
+ "packages/server/src/uploads/paths.ts"() {
1203
+ "use strict";
1204
+ MAX_FILENAME_LENGTH = 64;
1205
+ KEEP_FILENAME_CHAR = /[a-zA-Z0-9._一-鿿 \-]/;
1206
+ WORKSPACE_ID_RE = /^[a-zA-Z0-9_-]+$/;
1207
+ }
1208
+ });
1209
+
1210
+ // packages/server/src/routes/appearance-assets.ts
1211
+ import { randomUUID as randomUUID3 } from "node:crypto";
1212
+ import { createReadStream, createWriteStream } from "node:fs";
1213
+ import { rm, stat } from "node:fs/promises";
1214
+ import { isAbsolute, join as join2, relative, resolve as resolve2, sep } from "node:path";
1215
+ import { pipeline } from "node:stream/promises";
1216
+ function isAllowedAppearanceMime(mime2) {
1217
+ return ALLOWED_APPEARANCE_MIME_TYPES.has(mime2);
1218
+ }
1219
+ function isPathInsideRoot(rootPath, targetPath) {
1220
+ const rel = relative(rootPath, targetPath);
1221
+ return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
1222
+ }
1223
+ function resolveAssetStoragePath(uploadsDir, storagePath) {
1224
+ const resolvedUploadsDir = resolve2(uploadsDir);
1225
+ const resolvedStoragePath = resolve2(storagePath);
1226
+ return isPathInsideRoot(resolvedUploadsDir, resolvedStoragePath) ? resolvedStoragePath : null;
1227
+ }
1228
+ async function cleanupWrittenFile(filePath) {
1229
+ if (!filePath) {
1230
+ return;
1231
+ }
1232
+ await rm(filePath, { force: true });
1233
+ }
1234
+ async function rejectAndCleanup(reply, filePath, statusCode, error) {
1235
+ await cleanupWrittenFile(filePath);
1236
+ return reply.status(statusCode).send({ ok: false, error });
1237
+ }
1238
+ function registerAppearanceAssetsRoutes(app, deps) {
1239
+ app.post("/api/appearance-assets", async (request, reply) => {
1240
+ if (!request.isMultipart()) {
1241
+ return reply.status(400).send({ ok: false, error: "expected_multipart" });
1242
+ }
1243
+ let writtenPath;
1244
+ let pendingRecord;
1245
+ try {
1246
+ const parts = request.parts();
1247
+ for await (const part of parts) {
1248
+ if (part.type !== "file") {
1249
+ continue;
1250
+ }
1251
+ if (part.fieldname !== "file") {
1252
+ part.file.resume();
1253
+ return rejectAndCleanup(reply, writtenPath, 400, "file_required");
1254
+ }
1255
+ if (pendingRecord) {
1256
+ part.file.resume();
1257
+ return rejectAndCleanup(reply, writtenPath, 400, "too_many_files");
1258
+ }
1259
+ if (!isAllowedAppearanceMime(part.mimetype)) {
1260
+ part.file.resume();
1261
+ return rejectAndCleanup(reply, writtenPath, 400, "invalid_file_type");
1262
+ }
1263
+ const assetId = randomUUID3();
1264
+ const createdAt = Date.now();
1265
+ const dateStr = new Date(createdAt).toISOString().slice(0, 10);
1266
+ const safeName = sanitizeOriginalName(part.filename || "file");
1267
+ const fileName = part.filename?.trim() ? part.filename.trim() : safeName;
1268
+ const dir = join2(deps.uploadsDir, APPEARANCE_ASSET_BUCKET, dateStr);
1269
+ const storagePath = join2(dir, `${assetId}-${safeName}`);
1270
+ try {
1271
+ await ensureSafeUploadDir(deps.uploadsDir, dir);
1272
+ await pipeline(part.file, createWriteStream(storagePath));
1273
+ } catch (error) {
1274
+ request.log.warn({ err: error }, "appearance asset write failed");
1275
+ return rejectAndCleanup(reply, storagePath, 500, "write_failed");
1276
+ }
1277
+ if (part.file.truncated) {
1278
+ return rejectAndCleanup(reply, storagePath, 413, "file_too_large");
1279
+ }
1280
+ let fileSize;
1281
+ try {
1282
+ const fileStat = await stat(storagePath);
1283
+ fileSize = fileStat.size;
1284
+ } catch (error) {
1285
+ request.log.warn({ err: error }, "appearance asset stat failed");
1286
+ return rejectAndCleanup(reply, storagePath, 500, "write_failed");
1287
+ }
1288
+ writtenPath = storagePath;
1289
+ pendingRecord = {
1290
+ id: assetId,
1291
+ fileName,
1292
+ mime: part.mimetype,
1293
+ size: fileSize,
1294
+ storagePath,
1295
+ createdAt
1296
+ };
1297
+ }
1298
+ } catch (error) {
1299
+ if (error.code === "FST_REQ_FILE_TOO_LARGE") {
1300
+ return rejectAndCleanup(reply, writtenPath, 413, "file_too_large");
1301
+ }
1302
+ request.log.warn({ err: error }, "appearance asset parse failed");
1303
+ return rejectAndCleanup(reply, writtenPath, 400, "parse_failed");
1304
+ }
1305
+ if (!pendingRecord) {
1306
+ return rejectAndCleanup(reply, writtenPath, 400, "file_required");
1307
+ }
1308
+ try {
1309
+ deps.repo.set(pendingRecord);
1310
+ } catch (error) {
1311
+ request.log.warn({ err: error }, "appearance asset metadata write failed");
1312
+ return rejectAndCleanup(reply, writtenPath, 500, "write_failed");
1313
+ }
1314
+ return reply.send({
1315
+ ok: true,
1316
+ asset: {
1317
+ assetId: pendingRecord.id,
1318
+ url: `/api/appearance-assets/${pendingRecord.id}`,
1319
+ mime: pendingRecord.mime,
1320
+ size: pendingRecord.size
1321
+ }
1322
+ });
1323
+ });
1324
+ app.get(
1325
+ "/api/appearance-assets/:assetId",
1326
+ async (request, reply) => {
1327
+ const record = deps.repo.get(request.params.assetId);
1328
+ if (!record) {
1329
+ return reply.status(404).send({ ok: false, error: "not_found" });
1330
+ }
1331
+ const storagePath = resolveAssetStoragePath(deps.uploadsDir, record.storagePath);
1332
+ if (!storagePath) {
1333
+ return reply.status(404).send({ ok: false, error: "not_found" });
1334
+ }
1335
+ let fileSize;
1336
+ try {
1337
+ const fileStat = await stat(storagePath);
1338
+ if (!fileStat.isFile()) {
1339
+ return reply.status(404).send({ ok: false, error: "not_found" });
1340
+ }
1341
+ fileSize = fileStat.size;
1342
+ } catch {
1343
+ return reply.status(404).send({ ok: false, error: "not_found" });
1344
+ }
1345
+ reply.header("Content-Type", record.mime).header("Content-Length", String(fileSize)).header("Cache-Control", "no-store").header("X-Content-Type-Options", "nosniff");
1346
+ return reply.send(createReadStream(storagePath));
1347
+ }
1348
+ );
1349
+ app.delete(
1350
+ "/api/appearance-assets/:assetId",
1351
+ async (request, reply) => {
1352
+ const record = deps.repo.get(request.params.assetId);
1353
+ if (!record) {
1354
+ return reply.status(404).send({ ok: false, error: "not_found" });
1355
+ }
1356
+ const storagePath = resolveAssetStoragePath(deps.uploadsDir, record.storagePath);
1357
+ if (storagePath) {
1358
+ await rm(storagePath, { force: true });
1359
+ }
1360
+ deps.repo.delete(request.params.assetId);
1361
+ return reply.send({ ok: true });
1362
+ }
1363
+ );
1364
+ }
1365
+ var ALLOWED_APPEARANCE_MIME_TYPES, APPEARANCE_ASSET_BUCKET;
1366
+ var init_appearance_assets = __esm({
1367
+ "packages/server/src/routes/appearance-assets.ts"() {
1368
+ "use strict";
1369
+ init_paths();
1370
+ ALLOWED_APPEARANCE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
1371
+ APPEARANCE_ASSET_BUCKET = "appearance/default";
1372
+ }
1373
+ });
1374
+
1100
1375
  // packages/server/src/fs/image.ts
1101
1376
  import { extname } from "path";
1102
1377
  function getImageTypeInfo(filePath) {
@@ -1121,8 +1396,8 @@ var init_image = __esm({
1121
1396
  });
1122
1397
 
1123
1398
  // packages/server/src/fs/path-safety.ts
1124
- import path4 from "node:path";
1125
- function isPathInsideRoot(rootPath, targetPath, pathApi = path4) {
1399
+ import path5 from "node:path";
1400
+ function isPathInsideRoot2(rootPath, targetPath, pathApi = path5) {
1126
1401
  const rel = pathApi.relative(rootPath, targetPath);
1127
1402
  return rel !== ".." && !rel.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(rel);
1128
1403
  }
@@ -1133,19 +1408,19 @@ var init_path_safety = __esm({
1133
1408
  });
1134
1409
 
1135
1410
  // packages/server/src/fs/file-io.ts
1136
- import * as path5 from "node:path";
1411
+ import * as path6 from "node:path";
1137
1412
  import { createHash } from "crypto";
1138
1413
  import {
1139
1414
  readFile as fsReadFile,
1140
1415
  rename as fsRename,
1141
1416
  writeFile as fsWriteFile,
1142
- mkdir,
1143
- rm,
1144
- stat
1417
+ mkdir as mkdir2,
1418
+ rm as rm2,
1419
+ stat as stat2
1145
1420
  } from "fs/promises";
1146
1421
  async function statSafe(path14) {
1147
1422
  try {
1148
- return await stat(path14);
1423
+ return await stat2(path14);
1149
1424
  } catch {
1150
1425
  return null;
1151
1426
  }
@@ -1156,7 +1431,7 @@ async function createFile(rootPath, relPath) {
1156
1431
  if (existing) {
1157
1432
  throw { code: "already_exists", message: "File already exists" };
1158
1433
  }
1159
- await mkdir(path5.dirname(abs), { recursive: true });
1434
+ await mkdir2(path6.dirname(abs), { recursive: true });
1160
1435
  await fsWriteFile(abs, "", "utf-8");
1161
1436
  }
1162
1437
  async function createDirectory(rootPath, relPath) {
@@ -1165,7 +1440,7 @@ async function createDirectory(rootPath, relPath) {
1165
1440
  if (existing) {
1166
1441
  throw { code: "already_exists", message: "Directory already exists" };
1167
1442
  }
1168
- await mkdir(abs, { recursive: true });
1443
+ await mkdir2(abs, { recursive: true });
1169
1444
  }
1170
1445
  async function deleteEntry(rootPath, relPath) {
1171
1446
  const abs = resolveSafe(rootPath, relPath);
@@ -1173,15 +1448,15 @@ async function deleteEntry(rootPath, relPath) {
1173
1448
  if (!existing) {
1174
1449
  throw { code: "not_found", message: "Target not found" };
1175
1450
  }
1176
- await rm(abs, { recursive: true });
1451
+ await rm2(abs, { recursive: true });
1177
1452
  }
1178
1453
  async function renameEntry(rootPath, fromPath, toPath) {
1179
1454
  const fromAbs = resolveSafe(rootPath, fromPath);
1180
1455
  const toAbs = resolveSafe(rootPath, toPath);
1181
1456
  const source = await statSafe(fromAbs);
1182
1457
  const target = await statSafe(toAbs);
1183
- const fromParent = path5.dirname(fromAbs);
1184
- const toParent = path5.dirname(toAbs);
1458
+ const fromParent = path6.dirname(fromAbs);
1459
+ const toParent = path6.dirname(toAbs);
1185
1460
  if (!source) {
1186
1461
  throw { code: "not_found", message: "Source not found" };
1187
1462
  }
@@ -1196,10 +1471,10 @@ async function renameEntry(rootPath, fromPath, toPath) {
1196
1471
  }
1197
1472
  await fsRename(fromAbs, toAbs);
1198
1473
  }
1199
- function resolveSafe(root, relPath, pathApi = path5) {
1474
+ function resolveSafe(root, relPath, pathApi = path6) {
1200
1475
  const absRoot = pathApi.resolve(root);
1201
1476
  const abs = pathApi.resolve(absRoot, relPath);
1202
- if (!isPathInsideRoot(absRoot, abs, pathApi)) {
1477
+ if (!isPathInsideRoot2(absRoot, abs, pathApi)) {
1203
1478
  throw { code: "path_escape", message: "Path escapes workspace root" };
1204
1479
  }
1205
1480
  return abs;
@@ -1247,7 +1522,7 @@ async function writeFile(rootPath, relPath, content, baseHash) {
1247
1522
  };
1248
1523
  }
1249
1524
  }
1250
- await mkdir(path5.dirname(abs), { recursive: true });
1525
+ await mkdir2(path6.dirname(abs), { recursive: true });
1251
1526
  await fsWriteFile(abs, content, "utf-8");
1252
1527
  const newHash = createHash("sha256").update(content).digest("hex");
1253
1528
  return { newHash };
@@ -1411,11 +1686,11 @@ var init_status_parser = __esm({
1411
1686
 
1412
1687
  // packages/server/src/git/cli.ts
1413
1688
  import { execFile } from "child_process";
1414
- import { mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1689
+ import { mkdir as mkdir3, mkdtemp, rm as rm3, writeFile as writeFile2 } from "fs/promises";
1415
1690
  import os2 from "os";
1416
- import path6 from "path";
1691
+ import path7 from "path";
1417
1692
  async function runGit(cwd, args, options = {}) {
1418
- return new Promise((resolve4, reject) => {
1693
+ return new Promise((resolve6, reject) => {
1419
1694
  const gitArgs = [
1420
1695
  ...options.config?.flatMap(([key, value]) => ["-c", `${key}=${value}`]) ?? [],
1421
1696
  ...args
@@ -1442,7 +1717,7 @@ async function runGit(cwd, args, options = {}) {
1442
1717
  if (err) {
1443
1718
  reject(new GitError(err.message, stderr));
1444
1719
  } else {
1445
- resolve4({ stdout, stderr });
1720
+ resolve6({ stdout, stderr });
1446
1721
  }
1447
1722
  }
1448
1723
  );
@@ -1901,10 +2176,10 @@ async function prepareGitAuthExecution(auth, remoteMetadata) {
1901
2176
  }
1902
2177
  };
1903
2178
  }
1904
- const tempDir = await mkdtemp(path6.join(os2.tmpdir(), "coder-studio-git-auth-"));
1905
- const hooksDir = path6.join(tempDir, "hooks");
1906
- const askPassPath = path6.join(tempDir, "askpass.sh");
1907
- await mkdir2(hooksDir, { recursive: true, mode: 448 });
2179
+ const tempDir = await mkdtemp(path7.join(os2.tmpdir(), "coder-studio-git-auth-"));
2180
+ const hooksDir = path7.join(tempDir, "hooks");
2181
+ const askPassPath = path7.join(tempDir, "askpass.sh");
2182
+ await mkdir3(hooksDir, { recursive: true, mode: 448 });
1908
2183
  await writeFile2(
1909
2184
  askPassPath,
1910
2185
  [
@@ -1933,7 +2208,7 @@ async function prepareGitAuthExecution(auth, remoteMetadata) {
1933
2208
  ["credential.username", auth.username]
1934
2209
  ],
1935
2210
  cleanup: async () => {
1936
- await rm2(tempDir, { recursive: true, force: true });
2211
+ await rm3(tempDir, { recursive: true, force: true });
1937
2212
  }
1938
2213
  };
1939
2214
  }
@@ -2181,8 +2456,8 @@ var init_image_revision = __esm({
2181
2456
  });
2182
2457
 
2183
2458
  // packages/server/src/routes/file-asset.ts
2184
- import { createReadStream } from "fs";
2185
- import { realpath, stat as stat2 } from "fs/promises";
2459
+ import { createReadStream as createReadStream2 } from "fs";
2460
+ import { realpath, stat as stat3 } from "fs/promises";
2186
2461
  function registerFileAssetRoutes(app, deps) {
2187
2462
  app.get(
2188
2463
  "/api/file",
@@ -2226,7 +2501,7 @@ function registerFileAssetRoutes(app, deps) {
2226
2501
  realpath(workspace.path),
2227
2502
  realpath(absPath)
2228
2503
  ]);
2229
- if (!isPathInsideRoot(realWorkspacePath, realAssetPath)) {
2504
+ if (!isPathInsideRoot2(realWorkspacePath, realAssetPath)) {
2230
2505
  return reply.status(400).send({ ok: false, error: "path_escape" });
2231
2506
  }
2232
2507
  } catch {
@@ -2234,7 +2509,7 @@ function registerFileAssetRoutes(app, deps) {
2234
2509
  }
2235
2510
  let fileSize;
2236
2511
  try {
2237
- const stats = await stat2(absPath);
2512
+ const stats = await stat3(absPath);
2238
2513
  if (!stats.isFile()) {
2239
2514
  return reply.status(404).send({ ok: false, error: "not_a_file" });
2240
2515
  }
@@ -2243,7 +2518,7 @@ function registerFileAssetRoutes(app, deps) {
2243
2518
  return reply.status(404).send({ ok: false, error: "not_found" });
2244
2519
  }
2245
2520
  reply.header("Content-Type", typeInfo.mime).header("Content-Length", String(fileSize)).header("Cache-Control", "no-store").header("X-Content-Type-Options", "nosniff");
2246
- return reply.send(createReadStream(absPath));
2521
+ return reply.send(createReadStream2(absPath));
2247
2522
  }
2248
2523
  );
2249
2524
  }
@@ -2313,7 +2588,7 @@ var init_render_markdown = __esm({
2313
2588
  });
2314
2589
 
2315
2590
  // packages/server/src/preview/resource-loader.ts
2316
- import { readFile as readFile2, realpath as realpath2, stat as stat3 } from "node:fs/promises";
2591
+ import { readFile as readFile2, realpath as realpath2, stat as stat4 } from "node:fs/promises";
2317
2592
  import { posix as posix2 } from "node:path";
2318
2593
  import mime from "mime-types";
2319
2594
  function resolvePreviewResourcePath(entryPath, requestedPath) {
@@ -2331,10 +2606,10 @@ async function loadPreviewResource(workspaceRootPath, workspaceRelativePath) {
2331
2606
  realpath2(workspaceRootPath),
2332
2607
  realpath2(absolutePath)
2333
2608
  ]);
2334
- if (!isPathInsideRoot(realWorkspacePath, realAssetPath)) {
2609
+ if (!isPathInsideRoot2(realWorkspacePath, realAssetPath)) {
2335
2610
  throw new Error("path_escape");
2336
2611
  }
2337
- const [bytes, stats] = await Promise.all([readFile2(realAssetPath), stat3(realAssetPath)]);
2612
+ const [bytes, stats] = await Promise.all([readFile2(realAssetPath), stat4(realAssetPath)]);
2338
2613
  if (!stats.isFile()) {
2339
2614
  throw new Error("not_a_file");
2340
2615
  }
@@ -2478,118 +2753,8 @@ var init_constants = __esm({
2478
2753
  }
2479
2754
  });
2480
2755
 
2481
- // packages/server/src/uploads/paths.ts
2482
- import { randomUUID as randomUUID2 } from "node:crypto";
2483
- import { lstat, mkdir as mkdir3 } from "node:fs/promises";
2484
- import path7 from "node:path";
2485
- function sanitizeOriginalName(input) {
2486
- let sanitized = "";
2487
- for (const char of input.trim()) {
2488
- sanitized += KEEP_FILENAME_CHAR.test(char) ? char : "_";
2489
- }
2490
- sanitized = sanitized.replace(/^\.+/, "");
2491
- if (sanitized.length === 0 || /^[_\s]*$/.test(sanitized)) {
2492
- return "file";
2493
- }
2494
- if (sanitized.length <= MAX_FILENAME_LENGTH) {
2495
- return sanitized;
2496
- }
2497
- const lastDot = sanitized.lastIndexOf(".");
2498
- if (lastDot > 0 && sanitized.length - lastDot <= 16) {
2499
- const ext = sanitized.slice(lastDot);
2500
- const stem = sanitized.slice(0, MAX_FILENAME_LENGTH - ext.length);
2501
- return stem + ext;
2502
- }
2503
- return sanitized.slice(0, MAX_FILENAME_LENGTH);
2504
- }
2505
- function validateWorkspaceId(id) {
2506
- if (!WORKSPACE_ID_RE.test(id)) {
2507
- throw new Error(`invalid workspace id: ${JSON.stringify(id)}`);
2508
- }
2509
- }
2510
- async function assertDirectorySegmentSafe(segmentPath) {
2511
- const info = await lstat(segmentPath);
2512
- if (info.isSymbolicLink()) {
2513
- throw new Error(`symlinked upload path segment is not allowed: ${segmentPath}`);
2514
- }
2515
- if (!info.isDirectory()) {
2516
- throw new Error(`upload path segment is not a directory: ${segmentPath}`);
2517
- }
2518
- }
2519
- async function ensureSafeUploadDir(rootDir, targetDir2) {
2520
- const resolvedRoot = path7.resolve(rootDir);
2521
- const resolvedTarget = path7.resolve(targetDir2);
2522
- if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path7.sep}`)) {
2523
- throw new Error(`target dir escaped uploads root: ${resolvedTarget}`);
2524
- }
2525
- try {
2526
- await assertDirectorySegmentSafe(resolvedRoot);
2527
- } catch (error) {
2528
- const code = error.code;
2529
- if (code !== "ENOENT") {
2530
- throw error;
2531
- }
2532
- await mkdir3(resolvedRoot, { recursive: true });
2533
- await assertDirectorySegmentSafe(resolvedRoot);
2534
- }
2535
- const relative4 = path7.relative(resolvedRoot, resolvedTarget);
2536
- if (!relative4) {
2537
- return;
2538
- }
2539
- let current = resolvedRoot;
2540
- for (const segment of relative4.split(path7.sep)) {
2541
- current = path7.join(current, segment);
2542
- try {
2543
- await assertDirectorySegmentSafe(current);
2544
- continue;
2545
- } catch (error) {
2546
- const code = error.code;
2547
- if (code !== "ENOENT") {
2548
- throw error;
2549
- }
2550
- }
2551
- try {
2552
- await mkdir3(current);
2553
- } catch (error) {
2554
- const code = error.code;
2555
- if (code !== "EEXIST") {
2556
- throw error;
2557
- }
2558
- }
2559
- await assertDirectorySegmentSafe(current);
2560
- }
2561
- }
2562
- function generateBucketPath(input) {
2563
- validateWorkspaceId(input.workspaceId);
2564
- const now = input.now ?? /* @__PURE__ */ new Date();
2565
- const dateStr = now.toISOString().slice(0, 10);
2566
- const dir = path7.join(input.uploadsDir, input.workspaceId, dateStr);
2567
- const sanitizedName = sanitizeOriginalName(input.originalName);
2568
- const uuid8 = randomUUID2().replace(/-/g, "").slice(0, 8);
2569
- const absolutePath = path7.resolve(dir, `${uuid8}-${sanitizedName}`);
2570
- const uploadsRoot = `${path7.resolve(input.uploadsDir)}${path7.sep}`;
2571
- if (!absolutePath.startsWith(uploadsRoot)) {
2572
- throw new Error(`generated upload path escaped uploads root: ${absolutePath}`);
2573
- }
2574
- return {
2575
- dir,
2576
- absolutePath,
2577
- uuid8,
2578
- sanitizedName
2579
- };
2580
- }
2581
- var MAX_FILENAME_LENGTH, KEEP_FILENAME_CHAR, WORKSPACE_ID_RE;
2582
- var init_paths = __esm({
2583
- "packages/server/src/uploads/paths.ts"() {
2584
- "use strict";
2585
- MAX_FILENAME_LENGTH = 64;
2586
- KEEP_FILENAME_CHAR = /[a-zA-Z0-9._一-鿿 \-]/;
2587
- WORKSPACE_ID_RE = /^[a-zA-Z0-9_-]+$/;
2588
- }
2589
- });
2590
-
2591
2756
  // packages/server/src/uploads/cleanup.ts
2592
- import { readdir, rm as rm3, rmdir, stat as stat4, unlink } from "node:fs/promises";
2757
+ import { readdir, rm as rm4, rmdir, stat as stat5, unlink } from "node:fs/promises";
2593
2758
  import path8 from "node:path";
2594
2759
  async function listFilesRecursive(root) {
2595
2760
  let entries;
@@ -2611,7 +2776,7 @@ async function listFilesRecursive(root) {
2611
2776
  if (!entry.isFile()) {
2612
2777
  continue;
2613
2778
  }
2614
- const fileStat = await stat4(childPath);
2779
+ const fileStat = await stat5(childPath);
2615
2780
  files.push({
2616
2781
  absPath: childPath,
2617
2782
  size: fileStat.size,
@@ -2644,7 +2809,7 @@ async function pruneEmptyDirectories(root) {
2644
2809
  async function deleteWorkspaceUploads(uploadsDir, workspaceId) {
2645
2810
  validateWorkspaceId(workspaceId);
2646
2811
  const bucket = path8.join(uploadsDir, workspaceId);
2647
- await rm3(bucket, { recursive: true, force: true });
2812
+ await rm4(bucket, { recursive: true, force: true });
2648
2813
  }
2649
2814
  async function enforceBucketCap(uploadsDir, workspaceId, capBytes, logger) {
2650
2815
  validateWorkspaceId(workspaceId);
@@ -2727,9 +2892,9 @@ var init_cleanup = __esm({
2727
2892
  });
2728
2893
 
2729
2894
  // packages/server/src/routes/uploads.ts
2730
- import { createWriteStream } from "node:fs";
2731
- import { rm as rm4, stat as stat5, writeFile as writeFile3 } from "node:fs/promises";
2732
- import { pipeline } from "node:stream/promises";
2895
+ import { createWriteStream as createWriteStream2 } from "node:fs";
2896
+ import { rm as rm5, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
2897
+ import { pipeline as pipeline2 } from "node:stream/promises";
2733
2898
  function inferClipboardFilename(filename, mimeType, now) {
2734
2899
  const trimmed = filename?.trim();
2735
2900
  if (trimmed) {
@@ -2749,7 +2914,7 @@ function inferClipboardFilename(filename, mimeType, now) {
2749
2914
  return `screenshot-${hhmmss}.${ext}`;
2750
2915
  }
2751
2916
  async function cleanupWrittenFiles(files) {
2752
- await Promise.all(files.map((file) => rm4(file.path, { force: true })));
2917
+ await Promise.all(files.map((file) => rm5(file.path, { force: true })));
2753
2918
  }
2754
2919
  function getRequestLogger(request) {
2755
2920
  const logger = request.log;
@@ -2758,7 +2923,7 @@ function getRequestLogger(request) {
2758
2923
  }
2759
2924
  return void 0;
2760
2925
  }
2761
- async function rejectAndCleanup(reply, written, statusCode, error) {
2926
+ async function rejectAndCleanup2(reply, written, statusCode, error) {
2762
2927
  await cleanupWrittenFiles(written);
2763
2928
  return reply.status(statusCode).send({ ok: false, error });
2764
2929
  }
@@ -2771,7 +2936,7 @@ function getActiveWorkspace(deps, workspaceId) {
2771
2936
  async function ensureWorkspaceStillActive(deps, workspaceId, reply, written) {
2772
2937
  const workspace = getActiveWorkspace(deps, workspaceId);
2773
2938
  if (!workspace) {
2774
- await rejectAndCleanup(reply, written, 404, "workspace_not_found");
2939
+ await rejectAndCleanup2(reply, written, 404, "workspace_not_found");
2775
2940
  return null;
2776
2941
  }
2777
2942
  return workspace;
@@ -2798,22 +2963,22 @@ function registerUploadsRoute(app, deps) {
2798
2963
  if (part.type === "field" && part.fieldname === "workspaceId") {
2799
2964
  const lockedWorkspaceId = lockWorkspaceId(workspaceId, String(part.value));
2800
2965
  if (lockedWorkspaceId === "mismatch") {
2801
- return rejectAndCleanup(reply, written, 400, "workspace_mismatch");
2966
+ return rejectAndCleanup2(reply, written, 400, "workspace_mismatch");
2802
2967
  }
2803
2968
  workspaceId = lockedWorkspaceId;
2804
2969
  if (!getActiveWorkspace(deps, workspaceId)) {
2805
- return rejectAndCleanup(reply, written, 404, "workspace_not_found");
2970
+ return rejectAndCleanup2(reply, written, 404, "workspace_not_found");
2806
2971
  }
2807
2972
  workspaceValidated = true;
2808
2973
  continue;
2809
2974
  }
2810
2975
  if (part.type === "field" && part.fieldname === "files") {
2811
2976
  if (!workspaceId) {
2812
- return rejectAndCleanup(reply, written, 400, "workspace_required");
2977
+ return rejectAndCleanup2(reply, written, 400, "workspace_required");
2813
2978
  }
2814
2979
  fileCount += 1;
2815
2980
  if (fileCount > MAX_FILES_PER_BATCH) {
2816
- return rejectAndCleanup(reply, written, 400, "too_many_files");
2981
+ return rejectAndCleanup2(reply, written, 400, "too_many_files");
2817
2982
  }
2818
2983
  const now2 = /* @__PURE__ */ new Date();
2819
2984
  const originalName2 = inferClipboardFilename(void 0, part.mimetype, now2);
@@ -2830,20 +2995,20 @@ function registerUploadsRoute(app, deps) {
2830
2995
  await ensureSafeUploadDir(deps.uploadsDir, target2.dir);
2831
2996
  await writeFile3(target2.absolutePath, String(part.value));
2832
2997
  } catch (error) {
2833
- await rm4(target2.absolutePath, { force: true });
2998
+ await rm5(target2.absolutePath, { force: true });
2834
2999
  await cleanupWrittenFiles(written);
2835
3000
  logger?.warn({ err: error }, "upload write failed");
2836
3001
  return reply.status(500).send({ ok: false, error: "write_failed" });
2837
3002
  }
2838
3003
  try {
2839
- const fileStat = await stat5(target2.absolutePath);
3004
+ const fileStat = await stat6(target2.absolutePath);
2840
3005
  written.push({
2841
3006
  path: target2.absolutePath,
2842
3007
  originalName: originalName2,
2843
3008
  size: fileStat.size
2844
3009
  });
2845
3010
  } catch (error) {
2846
- await rm4(target2.absolutePath, { force: true });
3011
+ await rm5(target2.absolutePath, { force: true });
2847
3012
  await cleanupWrittenFiles(written);
2848
3013
  logger?.warn({ err: error }, "upload stat failed");
2849
3014
  return reply.status(500).send({ ok: false, error: "write_failed" });
@@ -2858,12 +3023,12 @@ function registerUploadsRoute(app, deps) {
2858
3023
  }
2859
3024
  if (!workspaceId) {
2860
3025
  part.file.resume();
2861
- return rejectAndCleanup(reply, written, 400, "workspace_required");
3026
+ return rejectAndCleanup2(reply, written, 400, "workspace_required");
2862
3027
  }
2863
3028
  fileCount += 1;
2864
3029
  if (fileCount > MAX_FILES_PER_BATCH) {
2865
3030
  part.file.resume();
2866
- return rejectAndCleanup(reply, written, 400, "too_many_files");
3031
+ return rejectAndCleanup2(reply, written, 400, "too_many_files");
2867
3032
  }
2868
3033
  const now = /* @__PURE__ */ new Date();
2869
3034
  const originalName = inferClipboardFilename(part.filename, part.mimetype, now);
@@ -2879,68 +3044,166 @@ function registerUploadsRoute(app, deps) {
2879
3044
  }
2880
3045
  try {
2881
3046
  await ensureSafeUploadDir(deps.uploadsDir, target.dir);
2882
- await pipeline(part.file, createWriteStream(target.absolutePath));
3047
+ await pipeline2(part.file, createWriteStream2(target.absolutePath));
2883
3048
  } catch (error) {
2884
- await rm4(target.absolutePath, { force: true });
3049
+ await rm5(target.absolutePath, { force: true });
2885
3050
  await cleanupWrittenFiles(written);
2886
3051
  logger?.warn({ err: error }, "upload write failed");
2887
3052
  return reply.status(500).send({ ok: false, error: "write_failed" });
2888
3053
  }
2889
3054
  if (part.file.truncated) {
2890
- await rm4(target.absolutePath, { force: true });
3055
+ await rm5(target.absolutePath, { force: true });
2891
3056
  await cleanupWrittenFiles(written);
2892
3057
  return reply.status(413).send({ ok: false, error: "file_too_large" });
2893
3058
  }
2894
3059
  try {
2895
- const fileStat = await stat5(target.absolutePath);
3060
+ const fileStat = await stat6(target.absolutePath);
2896
3061
  written.push({
2897
3062
  path: target.absolutePath,
2898
3063
  originalName,
2899
3064
  size: fileStat.size
2900
3065
  });
2901
3066
  } catch (error) {
2902
- await rm4(target.absolutePath, { force: true });
3067
+ await rm5(target.absolutePath, { force: true });
2903
3068
  await cleanupWrittenFiles(written);
2904
3069
  logger?.warn({ err: error }, "upload stat failed");
2905
3070
  return reply.status(500).send({ ok: false, error: "write_failed" });
2906
3071
  }
2907
3072
  }
2908
- } catch (error) {
2909
- await cleanupWrittenFiles(written);
2910
- if (error.code === "FST_REQ_FILE_TOO_LARGE") {
2911
- return reply.status(413).send({ ok: false, error: "file_too_large" });
3073
+ } catch (error) {
3074
+ await cleanupWrittenFiles(written);
3075
+ if (error.code === "FST_REQ_FILE_TOO_LARGE") {
3076
+ return reply.status(413).send({ ok: false, error: "file_too_large" });
3077
+ }
3078
+ logger?.warn({ err: error }, "upload parse failed");
3079
+ return reply.status(400).send({ ok: false, error: "parse_failed" });
3080
+ }
3081
+ if (!workspaceId) {
3082
+ return rejectAndCleanup2(reply, written, 400, "workspace_required");
3083
+ }
3084
+ if (!workspaceValidated) {
3085
+ return rejectAndCleanup2(reply, written, 404, "workspace_not_found");
3086
+ }
3087
+ if (written.length === 0) {
3088
+ return rejectAndCleanup2(reply, written, 400, "no_files");
3089
+ }
3090
+ if (!await ensureWorkspaceStillActive(deps, workspaceId, reply, written)) {
3091
+ return;
3092
+ }
3093
+ void enforceBucketCap(deps.uploadsDir, workspaceId, UPLOAD_BUCKET_MAX_BYTES, logger).catch(
3094
+ (error) => logger?.warn({ err: error }, "bucket cap enforcement failed")
3095
+ );
3096
+ return reply.send({ ok: true, files: written });
3097
+ });
3098
+ }
3099
+ var init_uploads = __esm({
3100
+ "packages/server/src/routes/uploads.ts"() {
3101
+ "use strict";
3102
+ init_cleanup();
3103
+ init_constants();
3104
+ init_paths();
3105
+ }
3106
+ });
3107
+
3108
+ // packages/server/src/storage/repositories/json-file-store.ts
3109
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
3110
+ import { dirname as dirname3 } from "node:path";
3111
+ function hasCode(error, code) {
3112
+ return Boolean(
3113
+ error && typeof error === "object" && "code" in error && error.code === code
3114
+ );
3115
+ }
3116
+ function readJsonFile(filePath) {
3117
+ try {
3118
+ return JSON.parse(readFileSync3(filePath, "utf-8"));
3119
+ } catch (error) {
3120
+ if (hasCode(error, "ENOENT")) {
3121
+ return void 0;
3122
+ }
3123
+ throw error;
3124
+ }
3125
+ }
3126
+ function writeJsonFileAtomic(filePath, value) {
3127
+ mkdirSync2(dirname3(filePath), { recursive: true });
3128
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3129
+ try {
3130
+ writeFileSync2(tempPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
3131
+ renameSync(tempPath, filePath);
3132
+ } catch (error) {
3133
+ rmSync(tempPath, { force: true });
3134
+ throw error;
3135
+ }
3136
+ }
3137
+ var init_json_file_store = __esm({
3138
+ "packages/server/src/storage/repositories/json-file-store.ts"() {
3139
+ "use strict";
3140
+ }
3141
+ });
3142
+
3143
+ // packages/server/src/storage/repositories/appearance-asset-repo.ts
3144
+ function isRecord(value) {
3145
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3146
+ }
3147
+ function normalizeAppearanceAssetFile(value) {
3148
+ if (isRecord(value) && value.version === 1 && isRecord(value.assets)) {
3149
+ return value.assets;
3150
+ }
3151
+ if (isRecord(value)) {
3152
+ return value;
3153
+ }
3154
+ return {};
3155
+ }
3156
+ var AppearanceAssetRepo;
3157
+ var init_appearance_asset_repo = __esm({
3158
+ "packages/server/src/storage/repositories/appearance-asset-repo.ts"() {
3159
+ "use strict";
3160
+ init_json_file_store();
3161
+ AppearanceAssetRepo = class {
3162
+ constructor(options) {
3163
+ this.options = options;
3164
+ }
3165
+ options;
3166
+ loadFileAssets() {
3167
+ const parsed = readJsonFile(
3168
+ this.options.filePath
3169
+ );
3170
+ if (parsed !== void 0) {
3171
+ return { ...normalizeAppearanceAssetFile(parsed) };
3172
+ }
3173
+ return {};
3174
+ }
3175
+ saveFileAssets(assets) {
3176
+ const payload = {
3177
+ version: 1,
3178
+ assets
3179
+ };
3180
+ writeJsonFileAtomic(this.options.filePath, payload);
3181
+ }
3182
+ get(id) {
3183
+ return this.loadFileAssets()[id];
3184
+ }
3185
+ set(record) {
3186
+ const next = this.loadFileAssets();
3187
+ next[record.id] = record;
3188
+ this.saveFileAssets(next);
3189
+ }
3190
+ delete(id) {
3191
+ const next = this.loadFileAssets();
3192
+ if (!Object.prototype.hasOwnProperty.call(next, id)) {
3193
+ return;
3194
+ }
3195
+ delete next[id];
3196
+ this.saveFileAssets(next);
3197
+ }
3198
+ list() {
3199
+ return Object.values(this.loadFileAssets());
2912
3200
  }
2913
- logger?.warn({ err: error }, "upload parse failed");
2914
- return reply.status(400).send({ ok: false, error: "parse_failed" });
2915
- }
2916
- if (!workspaceId) {
2917
- return rejectAndCleanup(reply, written, 400, "workspace_required");
2918
- }
2919
- if (!workspaceValidated) {
2920
- return rejectAndCleanup(reply, written, 404, "workspace_not_found");
2921
- }
2922
- if (written.length === 0) {
2923
- return rejectAndCleanup(reply, written, 400, "no_files");
2924
- }
2925
- if (!await ensureWorkspaceStillActive(deps, workspaceId, reply, written)) {
2926
- return;
2927
- }
2928
- void enforceBucketCap(deps.uploadsDir, workspaceId, UPLOAD_BUCKET_MAX_BYTES, logger).catch(
2929
- (error) => logger?.warn({ err: error }, "bucket cap enforcement failed")
2930
- );
2931
- return reply.send({ ok: true, files: written });
2932
- });
2933
- }
2934
- var init_uploads = __esm({
2935
- "packages/server/src/routes/uploads.ts"() {
2936
- "use strict";
2937
- init_cleanup();
2938
- init_constants();
2939
- init_paths();
3201
+ };
2940
3202
  }
2941
3203
  });
2942
3204
 
2943
3205
  // packages/server/src/app.ts
3206
+ import { join as join3, resolve as resolve3 } from "node:path";
2944
3207
  import compress from "@fastify/compress";
2945
3208
  import cors from "@fastify/cors";
2946
3209
  import multipart from "@fastify/multipart";
@@ -2948,6 +3211,10 @@ import staticPlugin from "@fastify/static";
2948
3211
  import websocket from "@fastify/websocket";
2949
3212
  import Fastify from "fastify";
2950
3213
  async function buildFastifyApp(deps) {
3214
+ const stateRoot = deps.config.stateDir === IN_MEMORY_STATE_DIR ? resolve3(deps.config.uploadsDir, "..") : deps.config.stateDir;
3215
+ const appearanceAssetRepo = deps.appearanceAssetRepo ?? new AppearanceAssetRepo({
3216
+ filePath: join3(stateRoot, "state", "appearance-assets.json")
3217
+ });
2951
3218
  const app = Fastify({
2952
3219
  logger: deps.logger ?? {
2953
3220
  level: "info",
@@ -3030,6 +3297,10 @@ async function buildFastifyApp(deps) {
3030
3297
  registerFileAssetRoutes(app, {
3031
3298
  workspaceMgr: deps.workspaceMgr
3032
3299
  });
3300
+ registerAppearanceAssetsRoutes(app, {
3301
+ uploadsDir: deps.config.uploadsDir,
3302
+ repo: appearanceAssetRepo
3303
+ });
3033
3304
  const previewSessions = new PreviewSessionStore();
3034
3305
  registerPreviewRoutes(app, {
3035
3306
  workspaceMgr: deps.workspaceMgr,
@@ -3083,11 +3354,14 @@ async function buildFastifyApp(deps) {
3083
3354
  var init_app = __esm({
3084
3355
  "packages/server/src/app.ts"() {
3085
3356
  "use strict";
3357
+ init_state_paths();
3086
3358
  init_auth();
3087
3359
  init_session_store();
3360
+ init_appearance_assets();
3088
3361
  init_file_asset();
3089
3362
  init_preview();
3090
3363
  init_uploads();
3364
+ init_appearance_asset_repo();
3091
3365
  init_constants();
3092
3366
  init_web_ui_routing();
3093
3367
  }
@@ -3324,12 +3598,12 @@ var init_auto_fetch = __esm({
3324
3598
  }
3325
3599
  acquireWorkspaceOperation(workspaceId) {
3326
3600
  const state = this.getOrCreateState(workspaceId);
3327
- return new Promise((resolve4) => {
3601
+ return new Promise((resolve6) => {
3328
3602
  const grant = () => {
3329
3603
  state.inFlight = true;
3330
3604
  state.nextFetchAt = void 0;
3331
3605
  let released = false;
3332
- resolve4(() => {
3606
+ resolve6(() => {
3333
3607
  if (released) {
3334
3608
  return;
3335
3609
  }
@@ -4332,7 +4606,7 @@ var init_manager = __esm({
4332
4606
  // packages/server/src/provider-runtime/command-runner.ts
4333
4607
  import { spawn as spawn2 } from "node:child_process";
4334
4608
  async function runCommandAsString(file, args, options) {
4335
- return new Promise((resolve4, reject) => {
4609
+ return new Promise((resolve6, reject) => {
4336
4610
  const child = spawn2(file, args, {
4337
4611
  cwd: options?.cwd,
4338
4612
  env: options?.env,
@@ -4359,7 +4633,7 @@ async function runCommandAsString(file, args, options) {
4359
4633
  const stdout = Buffer.concat(stdoutChunks).toString("utf8");
4360
4634
  const stderr = Buffer.concat(stderrChunks).toString("utf8");
4361
4635
  if (code === 0) {
4362
- resolve4({ stdout, stderr });
4636
+ resolve6({ stdout, stderr });
4363
4637
  return;
4364
4638
  }
4365
4639
  reject(
@@ -4478,9 +4752,9 @@ var init_definitions = __esm({
4478
4752
  });
4479
4753
 
4480
4754
  // packages/server/src/lsp-tools/install-manager.ts
4481
- import { randomUUID as randomUUID3 } from "node:crypto";
4482
- import { mkdirSync as mkdirSync2 } from "node:fs";
4483
- import { dirname as dirname3, join as join2 } from "node:path";
4755
+ import { randomUUID as randomUUID4 } from "node:crypto";
4756
+ import { mkdirSync as mkdirSync3 } from "node:fs";
4757
+ import { dirname as dirname4, join as join4 } from "node:path";
4484
4758
  function toSnapshotStep(step) {
4485
4759
  return {
4486
4760
  id: step.id,
@@ -4644,7 +4918,7 @@ var init_install_manager = __esm({
4644
4918
  async prepare(input) {
4645
4919
  const definition = getLspToolDefinition(input.serverKind);
4646
4920
  const managed = definition.managed;
4647
- const jobId = randomUUID3();
4921
+ const jobId = randomUUID4();
4648
4922
  const platform = this.deps.platform ?? process.platform;
4649
4923
  if (!managed || input.workspace.targetRuntime !== "native") {
4650
4924
  return {
@@ -4713,13 +4987,13 @@ var init_install_manager = __esm({
4713
4987
  }
4714
4988
  };
4715
4989
  }
4716
- const installRoot = join2(this.deps.manifestStore.getRoot(), input.serverKind, managed.version);
4717
- const executablePath = input.serverKind === "python" ? join2(
4990
+ const installRoot = join4(this.deps.manifestStore.getRoot(), input.serverKind, managed.version);
4991
+ const executablePath = input.serverKind === "python" ? join4(
4718
4992
  installRoot,
4719
4993
  "venv",
4720
4994
  platform === "win32" ? "Scripts" : "bin",
4721
4995
  platform === "win32" ? "pylsp.exe" : "pylsp"
4722
- ) : input.serverKind === "go" ? join2(installRoot, "bin", platform === "win32" ? "gopls.exe" : "gopls") : join2(installRoot, "bin", platform === "win32" ? "rust-analyzer.exe" : "rust-analyzer");
4996
+ ) : input.serverKind === "go" ? join4(installRoot, "bin", platform === "win32" ? "gopls.exe" : "gopls") : join4(installRoot, "bin", platform === "win32" ? "rust-analyzer.exe" : "rust-analyzer");
4723
4997
  const plannedSteps = this.planInstallSteps({
4724
4998
  serverKind: input.serverKind,
4725
4999
  installRoot,
@@ -4746,16 +5020,16 @@ var init_install_manager = __esm({
4746
5020
  const platform = this.deps.platform ?? process.platform;
4747
5021
  job.status = "running";
4748
5022
  this.jobs.set(job.jobId, job);
4749
- const installRoot = join2(this.deps.manifestStore.getRoot(), serverKind, managed.version);
4750
- const executablePath = serverKind === "python" ? join2(
5023
+ const installRoot = join4(this.deps.manifestStore.getRoot(), serverKind, managed.version);
5024
+ const executablePath = serverKind === "python" ? join4(
4751
5025
  installRoot,
4752
5026
  "venv",
4753
5027
  platform === "win32" ? "Scripts" : "bin",
4754
5028
  platform === "win32" ? "pylsp.exe" : "pylsp"
4755
- ) : serverKind === "go" ? join2(installRoot, "bin", platform === "win32" ? "gopls.exe" : "gopls") : join2(installRoot, "bin", platform === "win32" ? "rust-analyzer.exe" : "rust-analyzer");
5029
+ ) : serverKind === "go" ? join4(installRoot, "bin", platform === "win32" ? "gopls.exe" : "gopls") : join4(installRoot, "bin", platform === "win32" ? "rust-analyzer.exe" : "rust-analyzer");
4756
5030
  const commandExists = this.deps.commandExists ?? ((command) => checkCommandAvailable(command, this.deps));
4757
5031
  const pythonCommand = serverKind === "python" ? await resolveManagedPythonCommand(commandExists, platform) : null;
4758
- mkdirSync2(dirname3(executablePath), { recursive: true });
5032
+ mkdirSync3(dirname4(executablePath), { recursive: true });
4759
5033
  for (const step of job.steps) {
4760
5034
  job.currentStepId = step.id;
4761
5035
  step.status = "running";
@@ -4822,9 +5096,9 @@ var init_install_manager = __esm({
4822
5096
  }
4823
5097
  planInstallSteps(input) {
4824
5098
  if (input.serverKind === "python") {
4825
- const venvRoot = join2(input.installRoot, "venv");
4826
- const binDir = join2(venvRoot, input.platform === "win32" ? "Scripts" : "bin");
4827
- const pipPath = join2(binDir, input.platform === "win32" ? "pip.exe" : "pip");
5099
+ const venvRoot = join4(input.installRoot, "venv");
5100
+ const binDir = join4(venvRoot, input.platform === "win32" ? "Scripts" : "bin");
5101
+ const pipPath = join4(binDir, input.platform === "win32" ? "pip.exe" : "pip");
4828
5102
  return [
4829
5103
  {
4830
5104
  id: "create-python-venv",
@@ -4861,7 +5135,7 @@ var init_install_manager = __esm({
4861
5135
  args: ["install", `golang.org/x/tools/gopls@${input.version}`],
4862
5136
  env: {
4863
5137
  ...process.env,
4864
- GOBIN: join2(input.installRoot, "bin")
5138
+ GOBIN: join4(input.installRoot, "bin")
4865
5139
  }
4866
5140
  },
4867
5141
  {
@@ -4921,7 +5195,7 @@ var init_install_manager = __esm({
4921
5195
  // packages/server/src/lsp-tools/manager.ts
4922
5196
  import { existsSync as existsSync3 } from "node:fs";
4923
5197
  import { createRequire as createRequire2 } from "node:module";
4924
- import { join as join3 } from "node:path";
5198
+ import { join as join5 } from "node:path";
4925
5199
  function parseOverrideArgs(raw, envVarName) {
4926
5200
  try {
4927
5201
  const parsed = JSON.parse(raw);
@@ -5055,8 +5329,8 @@ var init_manager2 = __esm({
5055
5329
  }
5056
5330
  try {
5057
5331
  const packageJsonPath = require3.resolve(`${definition.bundled.packageName}/package.json`);
5058
- const packageRoot = join3(packageJsonPath, "..");
5059
- const entryPath = join3(packageRoot, definition.bundled.entry);
5332
+ const packageRoot = join5(packageJsonPath, "..");
5333
+ const entryPath = join5(packageRoot, definition.bundled.entry);
5060
5334
  if (!existsSync3(entryPath)) {
5061
5335
  return null;
5062
5336
  }
@@ -5108,8 +5382,8 @@ var init_manager2 = __esm({
5108
5382
  });
5109
5383
 
5110
5384
  // packages/server/src/lsp-tools/manifest-store.ts
5111
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5112
- import { join as join4 } from "node:path";
5385
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
5386
+ import { join as join6 } from "node:path";
5113
5387
  var FileManifestStore;
5114
5388
  var init_manifest_store = __esm({
5115
5389
  "packages/server/src/lsp-tools/manifest-store.ts"() {
@@ -5128,29 +5402,29 @@ var init_manifest_store = __esm({
5128
5402
  return null;
5129
5403
  }
5130
5404
  try {
5131
- return JSON.parse(readFileSync3(path14, "utf8"));
5405
+ return JSON.parse(readFileSync4(path14, "utf8"));
5132
5406
  } catch {
5133
5407
  return null;
5134
5408
  }
5135
5409
  }
5136
5410
  write(serverKind, manifest) {
5137
5411
  const path14 = this.pathFor(serverKind);
5138
- mkdirSync3(join4(this.root, serverKind), { recursive: true });
5139
- writeFileSync2(path14, JSON.stringify(manifest, null, 2));
5412
+ mkdirSync4(join6(this.root, serverKind), { recursive: true });
5413
+ writeFileSync3(path14, JSON.stringify(manifest, null, 2));
5140
5414
  }
5141
5415
  pathFor(serverKind) {
5142
- return join4(this.root, serverKind, "manifest.json");
5416
+ return join6(this.root, serverKind, "manifest.json");
5143
5417
  }
5144
5418
  };
5145
5419
  }
5146
5420
  });
5147
5421
 
5148
5422
  // packages/server/src/lsp-tools/tool-root.ts
5149
- import { mkdirSync as mkdirSync4 } from "node:fs";
5150
- import { join as join5 } from "node:path";
5423
+ import { mkdirSync as mkdirSync5 } from "node:fs";
5424
+ import { join as join7 } from "node:path";
5151
5425
  function resolveLspToolRoot(stateDir) {
5152
- const root = join5(stateDir, "lsp-tools");
5153
- mkdirSync4(root, { recursive: true });
5426
+ const root = join7(stateDir, "lsp-tools");
5427
+ mkdirSync5(root, { recursive: true });
5154
5428
  return root;
5155
5429
  }
5156
5430
  var init_tool_root = __esm({
@@ -5160,8 +5434,8 @@ var init_tool_root = __esm({
5160
5434
  });
5161
5435
 
5162
5436
  // packages/server/src/provider-runtime/e2e-provider-mock.ts
5163
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
5164
- import { dirname as dirname4, join as join6 } from "node:path";
5437
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
5438
+ import { dirname as dirname5, join as join8 } from "node:path";
5165
5439
  function createE2EProviderMockOverrides(env = process.env) {
5166
5440
  const statePath = env.CODER_STUDIO_E2E_PROVIDER_STATE_PATH;
5167
5441
  if (!statePath) {
@@ -5246,7 +5520,7 @@ function readMockState(statePath) {
5246
5520
  if (!existsSync5(statePath)) {
5247
5521
  return {};
5248
5522
  }
5249
- const raw = readFileSync4(statePath, "utf8");
5523
+ const raw = readFileSync5(statePath, "utf8");
5250
5524
  if (!raw.trim()) {
5251
5525
  return {};
5252
5526
  }
@@ -5261,22 +5535,22 @@ function readMockState(statePath) {
5261
5535
  function writeMockState(statePath, updater) {
5262
5536
  const nextState = readMockState(statePath);
5263
5537
  updater(nextState);
5264
- mkdirSync5(dirname4(statePath), { recursive: true });
5265
- writeFileSync3(statePath, JSON.stringify(nextState, null, 2));
5538
+ mkdirSync6(dirname5(statePath), { recursive: true });
5539
+ writeFileSync4(statePath, JSON.stringify(nextState, null, 2));
5266
5540
  return nextState;
5267
5541
  }
5268
5542
  function ensureProviderCommand(binDir, providerId) {
5269
- mkdirSync5(binDir, { recursive: true });
5270
- const scriptPath = join6(binDir, providerId);
5271
- writeFileSync3(scriptPath, PROVIDER_COMMAND_SCRIPTS[providerId], "utf8");
5543
+ mkdirSync6(binDir, { recursive: true });
5544
+ const scriptPath = join8(binDir, providerId);
5545
+ writeFileSync4(scriptPath, PROVIDER_COMMAND_SCRIPTS[providerId], "utf8");
5272
5546
  chmodSync(scriptPath, 493);
5273
5547
  }
5274
5548
  function appendDebugLog(path14, line) {
5275
5549
  if (!path14) {
5276
5550
  return;
5277
5551
  }
5278
- mkdirSync5(dirname4(path14), { recursive: true });
5279
- writeFileSync3(path14, `${line}
5552
+ mkdirSync6(dirname5(path14), { recursive: true });
5553
+ writeFileSync4(path14, `${line}
5280
5554
  `, { flag: "a" });
5281
5555
  }
5282
5556
  var PROVIDER_INSTALL_PACKAGES, PROVIDER_COMMAND_SCRIPTS;
@@ -5311,7 +5585,7 @@ done
5311
5585
  });
5312
5586
 
5313
5587
  // packages/server/src/provider-runtime/install-manager.ts
5314
- import { randomUUID as randomUUID4 } from "node:crypto";
5588
+ import { randomUUID as randomUUID5 } from "node:crypto";
5315
5589
  function getErrorDetails2(error) {
5316
5590
  if (error instanceof Error) {
5317
5591
  const record = error;
@@ -5451,7 +5725,7 @@ var init_install_manager2 = __esm({
5451
5725
  );
5452
5726
  if (missingProviderCommands.length === 0) {
5453
5727
  return {
5454
- jobId: randomUUID4(),
5728
+ jobId: randomUUID5(),
5455
5729
  providerId: provider.id,
5456
5730
  strategyIds: [],
5457
5731
  status: "succeeded",
@@ -5515,7 +5789,7 @@ var init_install_manager2 = __esm({
5515
5789
  }
5516
5790
  }
5517
5791
  }
5518
- const jobId = randomUUID4();
5792
+ const jobId = randomUUID5();
5519
5793
  if (remainingPrerequisites.size > 0) {
5520
5794
  const failedStep = this.createCheckStep(
5521
5795
  "prerequisite",
@@ -6086,41 +6360,6 @@ var init_provider_config = __esm({
6086
6360
  }
6087
6361
  });
6088
6362
 
6089
- // packages/server/src/storage/repositories/json-file-store.ts
6090
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
6091
- import { dirname as dirname5 } from "node:path";
6092
- function hasCode(error, code) {
6093
- return Boolean(
6094
- error && typeof error === "object" && "code" in error && error.code === code
6095
- );
6096
- }
6097
- function readJsonFile(filePath) {
6098
- try {
6099
- return JSON.parse(readFileSync5(filePath, "utf-8"));
6100
- } catch (error) {
6101
- if (hasCode(error, "ENOENT")) {
6102
- return void 0;
6103
- }
6104
- throw error;
6105
- }
6106
- }
6107
- function writeJsonFileAtomic(filePath, value) {
6108
- mkdirSync6(dirname5(filePath), { recursive: true });
6109
- const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
6110
- try {
6111
- writeFileSync4(tempPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
6112
- renameSync(tempPath, filePath);
6113
- } catch (error) {
6114
- rmSync(tempPath, { force: true });
6115
- throw error;
6116
- }
6117
- }
6118
- var init_json_file_store = __esm({
6119
- "packages/server/src/storage/repositories/json-file-store.ts"() {
6120
- "use strict";
6121
- }
6122
- });
6123
-
6124
6363
  // packages/server/src/storage/repositories/session-repo.ts
6125
6364
  function rowToSession(row) {
6126
6365
  return {
@@ -6157,11 +6396,11 @@ function sessionToRow(session) {
6157
6396
  title: session.title ?? null
6158
6397
  };
6159
6398
  }
6160
- function isRecord(value) {
6399
+ function isRecord2(value) {
6161
6400
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6162
6401
  }
6163
6402
  function isSession(value) {
6164
- if (!isRecord(value)) {
6403
+ if (!isRecord2(value)) {
6165
6404
  return false;
6166
6405
  }
6167
6406
  return typeof value.id === "string" && typeof value.workspaceId === "string" && typeof value.terminalId === "string" && typeof value.providerId === "string" && typeof value.startedAt === "number" && typeof value.lastActiveAt === "number" && (value.capability === "full" || value.capability === "limited" || value.capability === "unsupported") && typeof value.state === "string";
@@ -6173,7 +6412,7 @@ function normalizeStoredSession(session) {
6173
6412
  };
6174
6413
  }
6175
6414
  function normalizeSessionFile(value) {
6176
- if (isRecord(value) && value.version === 1 && isRecord(value.sessions)) {
6415
+ if (isRecord2(value) && value.version === 1 && isRecord2(value.sessions)) {
6177
6416
  const normalized = {};
6178
6417
  for (const entry of Object.values(value.sessions)) {
6179
6418
  if (isSession(entry)) {
@@ -6191,7 +6430,7 @@ function normalizeSessionFile(value) {
6191
6430
  }
6192
6431
  return normalized;
6193
6432
  }
6194
- if (isRecord(value)) {
6433
+ if (isRecord2(value)) {
6195
6434
  const normalized = {};
6196
6435
  for (const [sessionId, entry] of Object.entries(value)) {
6197
6436
  if (isSession(entry)) {
@@ -6497,7 +6736,87 @@ var init_state_shadow_comparator = __esm({
6497
6736
  function generateSessionId() {
6498
6737
  return `sess_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
6499
6738
  }
6500
- var NOOP_SESSION_LOGGER, SessionManager, ActiveSession;
6739
+ function readTerminalEscapeSequence(text, start) {
6740
+ const match = text.slice(start).match(TERMINAL_ESCAPE_SEQUENCE_PATTERN);
6741
+ return match?.[0] ?? null;
6742
+ }
6743
+ function classifyRecentInputEcho(bytes) {
6744
+ const text = bytes.toString("utf8");
6745
+ let opaque = false;
6746
+ let remainingVisibleText = "";
6747
+ for (let index = 0; index < text.length; index += 1) {
6748
+ const char = text[index];
6749
+ if (char === "\x1B") {
6750
+ const escape = readTerminalEscapeSequence(text, index);
6751
+ if (escape) {
6752
+ opaque = true;
6753
+ index += escape.length - 1;
6754
+ continue;
6755
+ }
6756
+ }
6757
+ if (char === "\r" || char === "\n" || char === " ") {
6758
+ opaque = true;
6759
+ continue;
6760
+ }
6761
+ if (char === "\x7F" || char === "\b") {
6762
+ opaque = true;
6763
+ continue;
6764
+ }
6765
+ if (char < " ") {
6766
+ opaque = true;
6767
+ continue;
6768
+ }
6769
+ remainingVisibleText += char;
6770
+ }
6771
+ return { opaque, remainingVisibleText };
6772
+ }
6773
+ function extractVisibleTerminalText(bytes) {
6774
+ const text = bytes.toString("utf8");
6775
+ let visibleText = "";
6776
+ for (let index = 0; index < text.length; index += 1) {
6777
+ const char = text[index];
6778
+ if (char === "\x1B") {
6779
+ const escape = readTerminalEscapeSequence(text, index);
6780
+ if (escape) {
6781
+ index += escape.length - 1;
6782
+ continue;
6783
+ }
6784
+ continue;
6785
+ }
6786
+ if (char === "\x7F" || char === "\b") {
6787
+ visibleText = visibleText.slice(0, -1);
6788
+ continue;
6789
+ }
6790
+ if (char === "\r" || char === "\n") {
6791
+ visibleText += "\n";
6792
+ continue;
6793
+ }
6794
+ if (char === " ") {
6795
+ visibleText += " ";
6796
+ continue;
6797
+ }
6798
+ if (char < " ") {
6799
+ continue;
6800
+ }
6801
+ visibleText += char;
6802
+ }
6803
+ return visibleText;
6804
+ }
6805
+ function commonPrefixLength(left, right) {
6806
+ const maxLength = Math.min(left.length, right.length);
6807
+ let index = 0;
6808
+ while (index < maxLength && left[index] === right[index]) {
6809
+ index += 1;
6810
+ }
6811
+ return index;
6812
+ }
6813
+ function chunkHasLineRepaintControl(text) {
6814
+ return text.includes("\r") || /\x1b\[[0-9;?]*K/.test(text) || /\x1b\[[0-9;?]*[ABCDGHF]/.test(text);
6815
+ }
6816
+ function visibleOutputHasMultipleLines(visibleOutput) {
6817
+ return visibleOutput.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).length > 1;
6818
+ }
6819
+ var NOOP_SESSION_LOGGER, RECENT_INPUT_ECHO_WINDOW_MS, RECENT_INPUT_ECHO_MAX_EVENTS, RECENT_INPUT_ECHO_MAX_BYTES, RECENT_OPAQUE_INPUT_ECHO_WINDOW_MS, RESUME_OUTPUT_AGGREGATION_WINDOW_MS, RESUME_OUTPUT_AGGREGATION_MAX_BYTES, TERMINAL_ESCAPE_SEQUENCE_PATTERN, SessionManager, ActiveSession;
6501
6820
  var init_manager3 = __esm({
6502
6821
  "packages/server/src/session/manager.ts"() {
6503
6822
  "use strict";
@@ -6510,6 +6829,13 @@ var init_manager3 = __esm({
6510
6829
  warn: () => {
6511
6830
  }
6512
6831
  };
6832
+ RECENT_INPUT_ECHO_WINDOW_MS = 3e3;
6833
+ RECENT_INPUT_ECHO_MAX_EVENTS = 12;
6834
+ RECENT_INPUT_ECHO_MAX_BYTES = 8192;
6835
+ RECENT_OPAQUE_INPUT_ECHO_WINDOW_MS = 200;
6836
+ RESUME_OUTPUT_AGGREGATION_WINDOW_MS = 75;
6837
+ RESUME_OUTPUT_AGGREGATION_MAX_BYTES = 4096;
6838
+ TERMINAL_ESCAPE_SEQUENCE_PATTERN = /^\x1b(?:\[[0-9;?<>]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|P[\s\S]*?\x1b\\|[@-_])/;
6513
6839
  SessionManager = class {
6514
6840
  constructor(deps) {
6515
6841
  this.deps = deps;
@@ -6686,6 +7012,7 @@ var init_manager3 = __esm({
6686
7012
  this.applyTerminalInputActivity(session, activity, text, { armTurnCompletion: true });
6687
7013
  }
6688
7014
  applyTerminalInputActivity(session, activity, text, options) {
7015
+ const completedSynchronously = !options.armTurnCompletion && session.state === "idle" && !session.awaitingTurnCompletion;
6689
7016
  if (activity === "control" || activity === "typing") {
6690
7017
  return;
6691
7018
  }
@@ -6694,16 +7021,10 @@ var init_manager3 = __esm({
6694
7021
  session.awaitingTurnCompletion = true;
6695
7022
  session.sawOutputSinceTurnStart = false;
6696
7023
  }
6697
- const prev2 = session.state;
6698
- if (session.state !== "running") {
6699
- session.state = "running";
6700
- session.lastActiveAt = Date.now();
6701
- this.deps.db.update(session.id, {
6702
- state: "running",
6703
- lastActiveAt: session.lastActiveAt
6704
- });
6705
- this.emitStateChanged(session, prev2, "running");
7024
+ if (completedSynchronously) {
7025
+ return;
6706
7026
  }
7027
+ this.transitionSessionToRunning(session);
6707
7028
  return;
6708
7029
  }
6709
7030
  if (activity !== "submit") return;
@@ -6716,17 +7037,11 @@ var init_manager3 = __esm({
6716
7037
  session.sawOutputSinceTurnStart = false;
6717
7038
  }
6718
7039
  const titleChanged = this.maybeAssignTitle(session, submittedText);
6719
- const prev = session.state;
6720
7040
  const shouldResume = session.state === "idle" || session.state === "starting";
6721
- if (shouldResume) {
6722
- session.state = "running";
6723
- session.lastActiveAt = Date.now();
6724
- this.deps.db.update(session.id, {
6725
- state: "running",
6726
- lastActiveAt: session.lastActiveAt
6727
- });
6728
- this.emitStateChanged(session, prev, "running");
7041
+ if (shouldResume && !completedSynchronously) {
7042
+ this.transitionSessionToRunning(session);
6729
7043
  } else if (titleChanged) {
7044
+ const prev = session.state;
6730
7045
  this.emitStateChanged(session, prev, session.state);
6731
7046
  }
6732
7047
  }
@@ -6740,9 +7055,11 @@ var init_manager3 = __esm({
6740
7055
  }
6741
7056
  const text = activity === "submit" || activity === "internal_submit" ? submittedText ?? bytes.toString("utf-8") : void 0;
6742
7057
  const rollbackArm = this.armTurnCompletionBeforeWrite(session, activity);
7058
+ const rollbackRecentInput = this.recordRecentInputBeforeWrite(session, bytes, activity);
6743
7059
  try {
6744
7060
  this.deps.terminalMgr.write(session.terminalId, bytes);
6745
7061
  } catch (error) {
7062
+ rollbackRecentInput?.();
6746
7063
  rollbackArm?.();
6747
7064
  throw error;
6748
7065
  }
@@ -6804,6 +7121,19 @@ var init_manager3 = __esm({
6804
7121
  this.deps.db.update(session.id, { title });
6805
7122
  return true;
6806
7123
  }
7124
+ transitionSessionToRunning(session) {
7125
+ if (session.state === "running") {
7126
+ return;
7127
+ }
7128
+ const prev = session.state;
7129
+ session.state = "running";
7130
+ session.lastActiveAt = Date.now();
7131
+ this.deps.db.update(session.id, {
7132
+ state: "running",
7133
+ lastActiveAt: session.lastActiveAt
7134
+ });
7135
+ this.emitStateChanged(session, prev, "running");
7136
+ }
6807
7137
  /**
6808
7138
  * Handle terminal exit event
6809
7139
  */
@@ -6865,6 +7195,197 @@ var init_manager3 = __esm({
6865
7195
  session.sawOutputSinceTurnStart = previousSawOutputSinceTurnStart;
6866
7196
  };
6867
7197
  }
7198
+ recordRecentInputBeforeWrite(session, bytes, activity) {
7199
+ if (activity === "system") {
7200
+ return null;
7201
+ }
7202
+ const { opaque, remainingVisibleText } = classifyRecentInputEcho(bytes);
7203
+ if (!opaque && remainingVisibleText.length === 0) {
7204
+ return null;
7205
+ }
7206
+ const recentInput = {
7207
+ at: Date.now(),
7208
+ activity,
7209
+ byteLength: bytes.byteLength,
7210
+ opaque,
7211
+ remainingVisibleText
7212
+ };
7213
+ session.recentInputEchoes.push(recentInput);
7214
+ this.pruneRecentInputEchoes(session, recentInput.at);
7215
+ return () => {
7216
+ const index = session.recentInputEchoes.indexOf(recentInput);
7217
+ if (index !== -1) {
7218
+ session.recentInputEchoes.splice(index, 1);
7219
+ }
7220
+ };
7221
+ }
7222
+ pruneRecentInputEchoes(session, now = Date.now()) {
7223
+ session.recentInputEchoes = session.recentInputEchoes.filter(
7224
+ (entry) => now - entry.at <= RECENT_INPUT_ECHO_WINDOW_MS && (entry.opaque || entry.remainingVisibleText.length > 0)
7225
+ );
7226
+ let totalBytes = session.recentInputEchoes.reduce((sum, entry) => sum + entry.byteLength, 0);
7227
+ while (session.recentInputEchoes.length > RECENT_INPUT_ECHO_MAX_EVENTS || totalBytes > RECENT_INPUT_ECHO_MAX_BYTES) {
7228
+ const removed = session.recentInputEchoes.shift();
7229
+ if (!removed) {
7230
+ break;
7231
+ }
7232
+ totalBytes -= removed.byteLength;
7233
+ }
7234
+ }
7235
+ clearPendingResumeAggregation(session) {
7236
+ if (session.pendingResumeAggregation?.timer) {
7237
+ clearTimeout(session.pendingResumeAggregation.timer);
7238
+ }
7239
+ session.pendingResumeAggregation = null;
7240
+ }
7241
+ consumeRecentLiteralEcho(session, visibleOutput) {
7242
+ let offset = 0;
7243
+ let matchedLiteralEcho = false;
7244
+ for (const entry of session.recentInputEchoes) {
7245
+ if (offset >= visibleOutput.length) {
7246
+ break;
7247
+ }
7248
+ if (entry.remainingVisibleText.length === 0) {
7249
+ continue;
7250
+ }
7251
+ const currentVisibleText = entry.remainingVisibleText;
7252
+ const matchLength = commonPrefixLength(visibleOutput.slice(offset), currentVisibleText);
7253
+ if (matchLength === 0) {
7254
+ break;
7255
+ }
7256
+ entry.remainingVisibleText = currentVisibleText.slice(matchLength);
7257
+ offset += matchLength;
7258
+ matchedLiteralEcho = true;
7259
+ if (matchLength < currentVisibleText.length) {
7260
+ break;
7261
+ }
7262
+ }
7263
+ return {
7264
+ matchedLiteralEcho,
7265
+ leftoverVisibleOutput: visibleOutput.slice(offset)
7266
+ };
7267
+ }
7268
+ hasRecentNonSubmitInput(session, now) {
7269
+ return session.recentInputEchoes.some(
7270
+ (entry) => entry.activity !== "submit" && entry.activity !== "internal_submit" && now - entry.at <= RECENT_OPAQUE_INPUT_ECHO_WINDOW_MS
7271
+ );
7272
+ }
7273
+ hasRecentOpaqueNonSubmitInput(session, now) {
7274
+ return session.recentInputEchoes.some(
7275
+ (entry) => entry.opaque && entry.activity !== "submit" && entry.activity !== "internal_submit" && now - entry.at <= RECENT_OPAQUE_INPUT_ECHO_WINDOW_MS
7276
+ );
7277
+ }
7278
+ hasRecentControlInput(session, now) {
7279
+ return session.recentInputEchoes.some(
7280
+ (entry) => entry.activity === "control" && now - entry.at <= RECENT_OPAQUE_INPUT_ECHO_WINDOW_MS
7281
+ );
7282
+ }
7283
+ getRecentVisibleInputText(session) {
7284
+ return session.recentInputEchoes.filter((entry) => entry.activity !== "submit" && entry.activity !== "internal_submit").map((entry) => entry.remainingVisibleText).join("");
7285
+ }
7286
+ isLikelyPureInputRepaint(session, chunk, visibleOutput, now) {
7287
+ if (!this.hasRecentNonSubmitInput(session, now)) {
7288
+ return false;
7289
+ }
7290
+ if (!visibleOutput.trim()) {
7291
+ return true;
7292
+ }
7293
+ const chunkText = chunk.toString("utf8");
7294
+ if (chunkText.includes("\n")) {
7295
+ return false;
7296
+ }
7297
+ if (visibleOutputHasMultipleLines(visibleOutput)) {
7298
+ return false;
7299
+ }
7300
+ if (!chunkHasLineRepaintControl(chunkText)) {
7301
+ return false;
7302
+ }
7303
+ if (this.hasRecentOpaqueNonSubmitInput(session, now)) {
7304
+ return true;
7305
+ }
7306
+ const recentVisibleInputText = this.getRecentVisibleInputText(session);
7307
+ const trimmedVisibleOutput = visibleOutput.trim();
7308
+ if (!recentVisibleInputText || !trimmedVisibleOutput) {
7309
+ return false;
7310
+ }
7311
+ return recentVisibleInputText === trimmedVisibleOutput || recentVisibleInputText.startsWith(trimmedVisibleOutput) || trimmedVisibleOutput.endsWith(recentVisibleInputText);
7312
+ }
7313
+ assessTerminalOutput(session, chunk) {
7314
+ const now = Date.now();
7315
+ this.pruneRecentInputEchoes(session, now);
7316
+ if (session.recentInputEchoes.length === 0) {
7317
+ const hasVisibleText = extractVisibleTerminalText(chunk).trim().length > 0;
7318
+ return {
7319
+ shouldResumeRunning: hasVisibleText,
7320
+ countsAsTurnOutput: hasVisibleText,
7321
+ shouldAggregateForResume: false
7322
+ };
7323
+ }
7324
+ const visibleOutput = extractVisibleTerminalText(chunk);
7325
+ const { matchedLiteralEcho, leftoverVisibleOutput } = this.consumeRecentLiteralEcho(
7326
+ session,
7327
+ visibleOutput
7328
+ );
7329
+ const hasRecentNonSubmitInput = this.hasRecentNonSubmitInput(session, now);
7330
+ const hasRecentControlInput = this.hasRecentControlInput(session, now);
7331
+ this.pruneRecentInputEchoes(session, now);
7332
+ const trimmedLeftoverVisibleOutput = leftoverVisibleOutput.trim();
7333
+ const trimmedVisibleOutput = visibleOutput.trim();
7334
+ const suppressImmediateResumeAfterControl = hasRecentControlInput && !matchedLiteralEcho;
7335
+ const chunkText = chunk.toString("utf8");
7336
+ const shouldAggregateForResume = session.state === "idle" && hasRecentNonSubmitInput && !matchedLiteralEcho && trimmedVisibleOutput.length > 0 && chunkHasLineRepaintControl(chunkText) && !visibleOutputHasMultipleLines(visibleOutput);
7337
+ const shouldResumeRunning = suppressImmediateResumeAfterControl ? false : trimmedLeftoverVisibleOutput.length > 0 ? !this.isLikelyPureInputRepaint(session, chunk, leftoverVisibleOutput, now) : !matchedLiteralEcho && trimmedVisibleOutput.length > 0 && !this.isLikelyPureInputRepaint(session, chunk, visibleOutput, now);
7338
+ const countsAsTurnOutput = !suppressImmediateResumeAfterControl && (trimmedLeftoverVisibleOutput.length > 0 ? !this.isLikelyPureInputRepaint(session, chunk, leftoverVisibleOutput, now) : !matchedLiteralEcho && trimmedVisibleOutput.length > 0 && !this.isLikelyPureInputRepaint(session, chunk, visibleOutput, now));
7339
+ return {
7340
+ shouldResumeRunning,
7341
+ countsAsTurnOutput,
7342
+ shouldAggregateForResume
7343
+ };
7344
+ }
7345
+ flushPendingResumeAggregation(session) {
7346
+ const pending = session.pendingResumeAggregation;
7347
+ if (!pending) {
7348
+ return;
7349
+ }
7350
+ this.clearPendingResumeAggregation(session);
7351
+ if (session.state !== "idle") {
7352
+ return;
7353
+ }
7354
+ const combinedChunk = Buffer.concat(pending.chunks, pending.byteLength);
7355
+ const outputAssessment = this.assessTerminalOutput(session, combinedChunk);
7356
+ if (outputAssessment.countsAsTurnOutput) {
7357
+ session.sawOutputSinceTurnStart = true;
7358
+ }
7359
+ }
7360
+ schedulePendingResumeAggregation(session, chunk) {
7361
+ const pending = session.pendingResumeAggregation;
7362
+ if (!pending) {
7363
+ const nextPending = {
7364
+ startedAt: Date.now(),
7365
+ chunks: [chunk],
7366
+ byteLength: chunk.byteLength,
7367
+ timer: null
7368
+ };
7369
+ nextPending.timer = setTimeout(() => {
7370
+ const activeSession = this.sessions.get(session.id);
7371
+ if (!activeSession) {
7372
+ return;
7373
+ }
7374
+ this.flushPendingResumeAggregation(activeSession);
7375
+ }, RESUME_OUTPUT_AGGREGATION_WINDOW_MS);
7376
+ session.pendingResumeAggregation = nextPending;
7377
+ return;
7378
+ }
7379
+ pending.chunks.push(chunk);
7380
+ pending.byteLength += chunk.byteLength;
7381
+ const combinedChunk = Buffer.concat(pending.chunks, pending.byteLength);
7382
+ const visibleOutput = extractVisibleTerminalText(combinedChunk);
7383
+ const combinedText = combinedChunk.toString("utf8");
7384
+ const shouldFlushNow = pending.byteLength >= RESUME_OUTPUT_AGGREGATION_MAX_BYTES || combinedText.includes("\n") || visibleOutputHasMultipleLines(visibleOutput);
7385
+ if (shouldFlushNow) {
7386
+ this.flushPendingResumeAggregation(session);
7387
+ }
7388
+ }
6868
7389
  flushPendingPtyIdle(session) {
6869
7390
  const ptyState = this.comparators.get(session.id)?.snapshot().ptyState;
6870
7391
  if (ptyState !== "idle") {
@@ -6873,6 +7394,7 @@ var init_manager3 = __esm({
6873
7394
  this.transitionSessionToIdle(session);
6874
7395
  }
6875
7396
  transitionSessionToIdle(activeSession) {
7397
+ this.clearPendingResumeAggregation(activeSession);
6876
7398
  const prev = activeSession.state;
6877
7399
  if (prev !== "running" && prev !== "starting") {
6878
7400
  return;
@@ -6935,7 +7457,24 @@ var init_manager3 = __esm({
6935
7457
  return;
6936
7458
  }
6937
7459
  const activeSession = this.sessions.get(session.id);
6938
- if (activeSession?.awaitingTurnCompletion) {
7460
+ if (!activeSession) {
7461
+ return;
7462
+ }
7463
+ if (activeSession.pendingResumeAggregation && activeSession.state !== "idle") {
7464
+ this.clearPendingResumeAggregation(activeSession);
7465
+ }
7466
+ if (activeSession.pendingResumeAggregation) {
7467
+ this.schedulePendingResumeAggregation(activeSession, event.chunk);
7468
+ detector.feed(event.chunk);
7469
+ return;
7470
+ }
7471
+ const outputAssessment = this.assessTerminalOutput(activeSession, event.chunk);
7472
+ if (outputAssessment.shouldAggregateForResume) {
7473
+ this.schedulePendingResumeAggregation(activeSession, event.chunk);
7474
+ detector.feed(event.chunk);
7475
+ return;
7476
+ }
7477
+ if (outputAssessment.countsAsTurnOutput) {
6939
7478
  activeSession.sawOutputSinceTurnStart = true;
6940
7479
  }
6941
7480
  detector.feed(event.chunk);
@@ -6952,6 +7491,7 @@ var init_manager3 = __esm({
6952
7491
  this.comparators.delete(sessionId);
6953
7492
  }
6954
7493
  finishSession(session, exitCode) {
7494
+ this.clearPendingResumeAggregation(session);
6955
7495
  const prev = session.state;
6956
7496
  session.state = "ended";
6957
7497
  session.endedAt = Date.now();
@@ -6983,6 +7523,8 @@ var init_manager3 = __esm({
6983
7523
  latestSubmittedUserInput;
6984
7524
  awaitingTurnCompletion = false;
6985
7525
  sawOutputSinceTurnStart = false;
7526
+ recentInputEchoes = [];
7527
+ pendingResumeAggregation = null;
6986
7528
  constructor(data) {
6987
7529
  this.id = data.id;
6988
7530
  this.workspaceId = data.workspaceId;
@@ -7025,17 +7567,17 @@ var init_manager3 = __esm({
7025
7567
  });
7026
7568
 
7027
7569
  // packages/server/src/storage/repositories/auth-login-block-repo.ts
7028
- function isRecord2(value) {
7570
+ function isRecord3(value) {
7029
7571
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7030
7572
  }
7031
7573
  function isAuthLoginBlockRecord(value) {
7032
- if (!isRecord2(value)) {
7574
+ if (!isRecord3(value)) {
7033
7575
  return false;
7034
7576
  }
7035
7577
  return typeof value.ip === "string" && typeof value.failedCount === "number" && typeof value.firstFailedAt === "number" && typeof value.lastFailedAt === "number" && (typeof value.blockedUntil === "number" || value.blockedUntil === null);
7036
7578
  }
7037
7579
  function normalizeFailures(value) {
7038
- if (!isRecord2(value)) {
7580
+ if (!isRecord3(value)) {
7039
7581
  return {};
7040
7582
  }
7041
7583
  const normalized = {};
@@ -7049,9 +7591,9 @@ function normalizeFailures(value) {
7049
7591
  return normalized;
7050
7592
  }
7051
7593
  function normalizeState(value) {
7052
- if (isRecord2(value) && value.version === 1) {
7594
+ if (isRecord3(value) && value.version === 1) {
7053
7595
  const blocks = {};
7054
- if (isRecord2(value.blocks)) {
7596
+ if (isRecord3(value.blocks)) {
7055
7597
  for (const entry of Object.values(value.blocks)) {
7056
7598
  if (isAuthLoginBlockRecord(entry)) {
7057
7599
  blocks[entry.ip] = entry;
@@ -7064,7 +7606,7 @@ function normalizeState(value) {
7064
7606
  failures: normalizeFailures(value.failures)
7065
7607
  };
7066
7608
  }
7067
- if (isRecord2(value)) {
7609
+ if (isRecord3(value)) {
7068
7610
  const blocks = {};
7069
7611
  for (const [ip, entry] of Object.entries(value)) {
7070
7612
  if (isAuthLoginBlockRecord(entry)) {
@@ -7170,17 +7712,17 @@ var init_auth_login_block_repo = __esm({
7170
7712
  });
7171
7713
 
7172
7714
  // packages/server/src/storage/repositories/auth-session-repo.ts
7173
- function isRecord3(value) {
7715
+ function isRecord4(value) {
7174
7716
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7175
7717
  }
7176
7718
  function isAuthSession(value) {
7177
- if (!isRecord3(value)) {
7719
+ if (!isRecord4(value)) {
7178
7720
  return false;
7179
7721
  }
7180
7722
  return typeof value.token === "string" && typeof value.createdAt === "number" && typeof value.lastSeenAt === "number";
7181
7723
  }
7182
7724
  function normalizeSessionFile2(value) {
7183
- if (isRecord3(value) && value.version === 1 && isRecord3(value.sessions)) {
7725
+ if (isRecord4(value) && value.version === 1 && isRecord4(value.sessions)) {
7184
7726
  const normalized = {};
7185
7727
  for (const entry of Object.values(value.sessions)) {
7186
7728
  if (isAuthSession(entry)) {
@@ -7198,7 +7740,7 @@ function normalizeSessionFile2(value) {
7198
7740
  }
7199
7741
  return normalized;
7200
7742
  }
7201
- if (isRecord3(value)) {
7743
+ if (isRecord4(value)) {
7202
7744
  const normalized = {};
7203
7745
  for (const [token, entry] of Object.entries(value)) {
7204
7746
  if (isAuthSession(entry)) {
@@ -7273,14 +7815,14 @@ var init_auth_session_repo = __esm({
7273
7815
  });
7274
7816
 
7275
7817
  // packages/server/src/storage/repositories/provider-config-repo.ts
7276
- function isRecord4(value) {
7818
+ function isRecord5(value) {
7277
7819
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7278
7820
  }
7279
7821
  function normalizeProviderConfigFile(value) {
7280
- if (isRecord4(value) && value.version === 1 && isRecord4(value.providers)) {
7822
+ if (isRecord5(value) && value.version === 1 && isRecord5(value.providers)) {
7281
7823
  return value.providers;
7282
7824
  }
7283
- if (isRecord4(value)) {
7825
+ if (isRecord5(value)) {
7284
7826
  return value;
7285
7827
  }
7286
7828
  return {};
@@ -7354,14 +7896,14 @@ var init_provider_config_repo = __esm({
7354
7896
  });
7355
7897
 
7356
7898
  // packages/server/src/storage/repositories/settings-repo.ts
7357
- function isRecord5(value) {
7899
+ function isRecord6(value) {
7358
7900
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7359
7901
  }
7360
7902
  function normalizeSettingsFile(value) {
7361
- if (isRecord5(value) && value.version === 1 && isRecord5(value.settings)) {
7903
+ if (isRecord6(value) && value.version === 1 && isRecord6(value.settings)) {
7362
7904
  return { ...value.settings };
7363
7905
  }
7364
- if (isRecord5(value)) {
7906
+ if (isRecord6(value)) {
7365
7907
  return { ...value };
7366
7908
  }
7367
7909
  return {};
@@ -7512,11 +8054,11 @@ var init_supervisor_repo = __esm({
7512
8054
  });
7513
8055
 
7514
8056
  // packages/server/src/storage/repositories/terminal-repo.ts
7515
- function isRecord6(value) {
8057
+ function isRecord7(value) {
7516
8058
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7517
8059
  }
7518
8060
  function isTerminal(value) {
7519
- if (!isRecord6(value)) {
8061
+ if (!isRecord7(value)) {
7520
8062
  return false;
7521
8063
  }
7522
8064
  return typeof value.id === "string" && typeof value.workspaceId === "string" && (value.kind === "agent" || value.kind === "shell") && typeof value.cwd === "string" && Array.isArray(value.argv) && typeof value.cols === "number" && typeof value.rows === "number" && typeof value.alive === "boolean" && typeof value.createdAt === "number";
@@ -7528,7 +8070,7 @@ function normalizeTerminal(value) {
7528
8070
  };
7529
8071
  }
7530
8072
  function normalizeTerminalFile(value) {
7531
- if (isRecord6(value) && value.version === 1 && isRecord6(value.terminals)) {
8073
+ if (isRecord7(value) && value.version === 1 && isRecord7(value.terminals)) {
7532
8074
  const normalized = {};
7533
8075
  for (const entry of Object.values(value.terminals)) {
7534
8076
  if (isTerminal(entry)) {
@@ -7546,7 +8088,7 @@ function normalizeTerminalFile(value) {
7546
8088
  }
7547
8089
  return normalized;
7548
8090
  }
7549
- if (isRecord6(value)) {
8091
+ if (isRecord7(value)) {
7550
8092
  const normalized = {};
7551
8093
  for (const [terminalId, entry] of Object.entries(value)) {
7552
8094
  if (isTerminal(entry)) {
@@ -7712,12 +8254,12 @@ var init_terminal_repo = __esm({
7712
8254
  });
7713
8255
 
7714
8256
  // packages/server/src/storage/repositories/update-state-repo.ts
7715
- function isRecord7(value) {
8257
+ function isRecord8(value) {
7716
8258
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7717
8259
  }
7718
8260
  function normalizeUpdateState(value, currentVersion) {
7719
8261
  const defaults = createDefaultUpdateState(currentVersion);
7720
- if (!isRecord7(value)) {
8262
+ if (!isRecord8(value)) {
7721
8263
  return defaults;
7722
8264
  }
7723
8265
  return {
@@ -7780,17 +8322,17 @@ var init_update_state_repo = __esm({
7780
8322
  });
7781
8323
 
7782
8324
  // packages/server/src/storage/repositories/workspace-repo.ts
7783
- function isRecord8(value) {
8325
+ function isRecord9(value) {
7784
8326
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7785
8327
  }
7786
8328
  function isWorkspace(value) {
7787
- if (!isRecord8(value)) {
8329
+ if (!isRecord9(value)) {
7788
8330
  return false;
7789
8331
  }
7790
- return typeof value.id === "string" && typeof value.path === "string" && (value.targetRuntime === "native" || value.targetRuntime === "wsl") && typeof value.openedAt === "number" && typeof value.lastActiveAt === "number" && isRecord8(value.uiState);
8332
+ return typeof value.id === "string" && typeof value.path === "string" && (value.targetRuntime === "native" || value.targetRuntime === "wsl") && typeof value.openedAt === "number" && typeof value.lastActiveAt === "number" && isRecord9(value.uiState);
7791
8333
  }
7792
8334
  function normalizeWorkspaceFile(value) {
7793
- if (isRecord8(value) && value.version === 1 && isRecord8(value.workspaces)) {
8335
+ if (isRecord9(value) && value.version === 1 && isRecord9(value.workspaces)) {
7794
8336
  const normalized = {};
7795
8337
  for (const entry of Object.values(value.workspaces)) {
7796
8338
  if (isWorkspace(entry)) {
@@ -7808,7 +8350,7 @@ function normalizeWorkspaceFile(value) {
7808
8350
  }
7809
8351
  return normalized;
7810
8352
  }
7811
- if (isRecord8(value)) {
8353
+ if (isRecord9(value)) {
7812
8354
  const normalized = {};
7813
8355
  for (const [workspaceId, entry] of Object.entries(value)) {
7814
8356
  if (isWorkspace(entry)) {
@@ -8053,13 +8595,13 @@ function ensureNodePtySpawnHelperExecutable(deps = {}) {
8053
8595
  return;
8054
8596
  }
8055
8597
  const arch = deps.arch ?? process.arch;
8056
- const resolve4 = deps.resolve ?? ((id) => require4.resolve(id));
8598
+ const resolve6 = deps.resolve ?? ((id) => require4.resolve(id));
8057
8599
  const fileExists = deps.existsSync ?? existsSync6;
8058
- const stat10 = deps.statSync ?? statSync;
8600
+ const stat11 = deps.statSync ?? statSync;
8059
8601
  const chmod = deps.chmodSync ?? chmodSync2;
8060
8602
  let packageJsonPath;
8061
8603
  try {
8062
- packageJsonPath = resolve4(NODE_PTY_PKG);
8604
+ packageJsonPath = resolve6(NODE_PTY_PKG);
8063
8605
  } catch {
8064
8606
  return;
8065
8607
  }
@@ -8073,7 +8615,7 @@ function ensureNodePtySpawnHelperExecutable(deps = {}) {
8073
8615
  if (!fileExists(helperPath)) {
8074
8616
  return;
8075
8617
  }
8076
- const currentMode = stat10(helperPath).mode;
8618
+ const currentMode = stat11(helperPath).mode;
8077
8619
  const executableMode = currentMode | 73;
8078
8620
  if (executableMode === currentMode) {
8079
8621
  return;
@@ -8124,7 +8666,7 @@ async function escalateKillWithPolling(pid, signal, options) {
8124
8666
  const startTime = Date.now();
8125
8667
  const deadline = startTime + timeoutMs;
8126
8668
  while (Date.now() < deadline) {
8127
- await new Promise((resolve4) => setTimeout(resolve4, pollIntervalMs));
8669
+ await new Promise((resolve6) => setTimeout(resolve6, pollIntervalMs));
8128
8670
  if (!isProcessAlive(pid)) {
8129
8671
  return true;
8130
8672
  }
@@ -8488,7 +9030,7 @@ async function runCommand(command, timeoutMs, options = {}) {
8488
9030
  if (options.signal?.aborted) {
8489
9031
  throw createSupervisorEvalAbortedError();
8490
9032
  }
8491
- return await new Promise((resolve4, reject) => {
9033
+ return await new Promise((resolve6, reject) => {
8492
9034
  const child = spawn3(command.argv[0], command.argv.slice(1), {
8493
9035
  cwd: command.cwd,
8494
9036
  detached: process.platform !== "win32",
@@ -8518,7 +9060,7 @@ async function runCommand(command, timeoutMs, options = {}) {
8518
9060
  }
8519
9061
  settled = true;
8520
9062
  cleanup();
8521
- resolve4(value);
9063
+ resolve6(value);
8522
9064
  };
8523
9065
  const terminate = (error) => {
8524
9066
  if (terminationError) {
@@ -9346,12 +9888,12 @@ var init_scheduler = __esm({
9346
9888
 
9347
9889
  // packages/server/src/supervisor/manager.ts
9348
9890
  function createDeferredCompletion() {
9349
- let resolve4 = () => {
9891
+ let resolve6 = () => {
9350
9892
  };
9351
9893
  const promise = new Promise((innerResolve) => {
9352
- resolve4 = innerResolve;
9894
+ resolve6 = innerResolve;
9353
9895
  });
9354
- return { promise, resolve: resolve4 };
9896
+ return { promise, resolve: resolve6 };
9355
9897
  }
9356
9898
  function generateSupervisorId() {
9357
9899
  return `sup_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
@@ -10706,10 +11248,10 @@ var init_manager4 = __esm({
10706
11248
  if (signal?.aborted) {
10707
11249
  throw { code: "supervisor_eval_aborted", message: "Supervisor evaluator aborted" };
10708
11250
  }
10709
- await new Promise((resolve4, reject) => {
11251
+ await new Promise((resolve6, reject) => {
10710
11252
  const timer = setTimeout(() => {
10711
11253
  signal?.removeEventListener("abort", onAbort);
10712
- resolve4();
11254
+ resolve6();
10713
11255
  }, delayMs);
10714
11256
  timer.unref?.();
10715
11257
  const onAbort = () => {
@@ -10740,31 +11282,31 @@ __export(target_store_exports, {
10740
11282
  saveTargetMemory: () => saveTargetMemory,
10741
11283
  saveTargetMeta: () => saveTargetMeta
10742
11284
  });
10743
- import { mkdir as mkdir4, mkdtemp as mkdtemp2, readdir as readdir2, readFile as readFile3, rename, rm as rm5, writeFile as writeFile4 } from "node:fs/promises";
10744
- import { dirname as dirname6, join as join7 } from "node:path";
11285
+ import { mkdir as mkdir4, mkdtemp as mkdtemp2, readdir as readdir2, readFile as readFile3, rename, rm as rm6, writeFile as writeFile4 } from "node:fs/promises";
11286
+ import { dirname as dirname6, join as join9 } from "node:path";
10745
11287
  function targetDir(workspacePath, targetId) {
10746
- return join7(workspacePath, ".coder-studio", "supervisor", "targets", targetId);
11288
+ return join9(workspacePath, ".coder-studio", "supervisor", "targets", targetId);
10747
11289
  }
10748
11290
  function metaPath(workspacePath, targetId) {
10749
- return join7(targetDir(workspacePath, targetId), "meta.json");
11291
+ return join9(targetDir(workspacePath, targetId), "meta.json");
10750
11292
  }
10751
11293
  function memoryPath(workspacePath, targetId) {
10752
- return join7(targetDir(workspacePath, targetId), "memory.json");
11294
+ return join9(targetDir(workspacePath, targetId), "memory.json");
10753
11295
  }
10754
11296
  function cyclesPath(workspacePath, targetId) {
10755
- return join7(targetDir(workspacePath, targetId), "cycles.jsonl");
11297
+ return join9(targetDir(workspacePath, targetId), "cycles.jsonl");
10756
11298
  }
10757
11299
  function targetsRoot(workspacePath) {
10758
- return join7(workspacePath, ".coder-studio", "supervisor", "targets");
11300
+ return join9(workspacePath, ".coder-studio", "supervisor", "targets");
10759
11301
  }
10760
11302
  function metaFilePath(dirPath) {
10761
- return join7(dirPath, "meta.json");
11303
+ return join9(dirPath, "meta.json");
10762
11304
  }
10763
11305
  function memoryFilePath(dirPath) {
10764
- return join7(dirPath, "memory.json");
11306
+ return join9(dirPath, "memory.json");
10765
11307
  }
10766
11308
  function cyclesFilePath(dirPath) {
10767
- return join7(dirPath, "cycles.jsonl");
11309
+ return join9(dirPath, "cycles.jsonl");
10768
11310
  }
10769
11311
  function hasCode2(error, code) {
10770
11312
  return Boolean(
@@ -10783,7 +11325,7 @@ function errorMessage(error, fallback) {
10783
11325
  }
10784
11326
  return fallback;
10785
11327
  }
10786
- function isRecord9(value) {
11328
+ function isRecord10(value) {
10787
11329
  return Boolean(value) && typeof value === "object";
10788
11330
  }
10789
11331
  function readNonEmptyString(value) {
@@ -10827,7 +11369,7 @@ function fallbackAcceptanceCriteria(title) {
10827
11369
  return [`${title} is complete`];
10828
11370
  }
10829
11371
  function normalizeItem(value, fallbackKind) {
10830
- if (!isRecord9(value)) {
11372
+ if (!isRecord10(value)) {
10831
11373
  return null;
10832
11374
  }
10833
11375
  const id = readNonEmptyString(value.id);
@@ -10858,7 +11400,7 @@ function normalizeLegacyPlanItems(plan) {
10858
11400
  }
10859
11401
  return plan.flatMap((value) => {
10860
11402
  const item = normalizeItem(
10861
- isRecord9(value) ? {
11403
+ isRecord10(value) ? {
10862
11404
  id: value.id,
10863
11405
  kind: "stage",
10864
11406
  title: value.title,
@@ -10882,7 +11424,7 @@ function resolveActiveItemId(items, candidate) {
10882
11424
  return items.find((item) => item.status === "in_progress")?.id ?? items.find((item) => item.status === "pending")?.id ?? items[0]?.id;
10883
11425
  }
10884
11426
  function normalizeTargetMemory(raw, targetId) {
10885
- if (!isRecord9(raw)) {
11427
+ if (!isRecord10(raw)) {
10886
11428
  return buildTargetMemory(targetId, 0);
10887
11429
  }
10888
11430
  const updatedAt = readTimestamp(raw.updatedAt, 0);
@@ -10909,7 +11451,7 @@ function normalizeTargetMemory(raw, targetId) {
10909
11451
  };
10910
11452
  }
10911
11453
  function normalizePersistedSupervisor(raw, fallback) {
10912
- if (!isRecord9(raw)) {
11454
+ if (!isRecord10(raw)) {
10913
11455
  return void 0;
10914
11456
  }
10915
11457
  const id = readNonEmptyString(raw.id) ?? fallback.targetId;
@@ -10945,7 +11487,7 @@ function normalizePersistedSupervisor(raw, fallback) {
10945
11487
  };
10946
11488
  }
10947
11489
  function normalizeTargetMeta(raw, fallbackTargetId) {
10948
- if (!isRecord9(raw)) {
11490
+ if (!isRecord10(raw)) {
10949
11491
  const targetId2 = fallbackTargetId ?? "";
10950
11492
  return {
10951
11493
  targetId: targetId2,
@@ -11066,7 +11608,7 @@ async function resetTargetFiles(workspacePath, input) {
11066
11608
  const parentDir = dirname6(dir);
11067
11609
  const backupDir = `${dir}.backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11068
11610
  await mkdir4(parentDir, { recursive: true });
11069
- const stagingDir = await mkdtemp2(join7(parentDir, `${input.targetId}.reset-`));
11611
+ const stagingDir = await mkdtemp2(join9(parentDir, `${input.targetId}.reset-`));
11070
11612
  let backupCreated = false;
11071
11613
  let promoted = false;
11072
11614
  let restored = false;
@@ -11102,17 +11644,17 @@ async function resetTargetFiles(workspacePath, input) {
11102
11644
  }
11103
11645
  } catch (error) {
11104
11646
  if (restored || !backupCreated) {
11105
- await rm5(stagingDir, { recursive: true, force: true }).catch(() => {
11647
+ await rm6(stagingDir, { recursive: true, force: true }).catch(() => {
11106
11648
  });
11107
11649
  }
11108
11650
  throw error;
11109
11651
  }
11110
11652
  if (backupCreated) {
11111
- await rm5(backupDir, { recursive: true, force: true }).catch(() => {
11653
+ await rm6(backupDir, { recursive: true, force: true }).catch(() => {
11112
11654
  });
11113
11655
  }
11114
11656
  if (!promoted) {
11115
- await rm5(stagingDir, { recursive: true, force: true }).catch(() => {
11657
+ await rm6(stagingDir, { recursive: true, force: true }).catch(() => {
11116
11658
  });
11117
11659
  }
11118
11660
  }
@@ -11210,7 +11752,7 @@ async function cloneTargetFiles(workspacePath, input) {
11210
11752
  return nextCycles.filter(countsTowardCycleTotal).length;
11211
11753
  }
11212
11754
  async function deleteTarget(workspacePath, targetId) {
11213
- await rm5(targetDir(workspacePath, targetId), { recursive: true, force: true });
11755
+ await rm6(targetDir(workspacePath, targetId), { recursive: true, force: true });
11214
11756
  }
11215
11757
  async function markTargetSuperseded(workspacePath, targetId, nextTargetId, updatedAt) {
11216
11758
  const meta = await readTargetMeta(workspacePath, targetId);
@@ -11523,8 +12065,8 @@ var init_terminal_snapshot_buffer = __esm({
11523
12065
  if (this.pendingWriteCount === 0) {
11524
12066
  return Promise.resolve();
11525
12067
  }
11526
- return new Promise((resolve4) => {
11527
- this.drainResolvers.push(resolve4);
12068
+ return new Promise((resolve6) => {
12069
+ this.drainResolvers.push(resolve6);
11528
12070
  });
11529
12071
  }
11530
12072
  resolveDrainIfIdle() {
@@ -11533,8 +12075,8 @@ var init_terminal_snapshot_buffer = __esm({
11533
12075
  }
11534
12076
  const resolvers = this.drainResolvers;
11535
12077
  this.drainResolvers = [];
11536
- for (const resolve4 of resolvers) {
11537
- resolve4();
12078
+ for (const resolve6 of resolvers) {
12079
+ resolve6();
11538
12080
  }
11539
12081
  }
11540
12082
  requireTerminal() {
@@ -11853,10 +12395,10 @@ var init_manager5 = __esm({
11853
12395
  }
11854
12396
  return existing.promise;
11855
12397
  }
11856
- let resolve4 = () => {
12398
+ let resolve6 = () => {
11857
12399
  };
11858
12400
  const promise = new Promise((innerResolve) => {
11859
- resolve4 = innerResolve;
12401
+ resolve6 = innerResolve;
11860
12402
  });
11861
12403
  let markKillCompleted = () => {
11862
12404
  };
@@ -11869,7 +12411,7 @@ var init_manager5 = __esm({
11869
12411
  markKillCompleted,
11870
12412
  finalized: false,
11871
12413
  promise,
11872
- resolve: resolve4
12414
+ resolve: resolve6
11873
12415
  });
11874
12416
  void terminal.pty.kill(signal).finally(() => {
11875
12417
  const waiter = this.explicitCloseWaiters.get(terminalId);
@@ -12345,10 +12887,10 @@ var init_update_service = __esm({
12345
12887
 
12346
12888
  // packages/server/src/workspace/validator.ts
12347
12889
  import { constants } from "fs";
12348
- import { access, stat as stat6 } from "fs/promises";
12890
+ import { access, stat as stat7 } from "fs/promises";
12349
12891
  async function validatePath(path14) {
12350
12892
  try {
12351
- const stats = await stat6(path14);
12893
+ const stats = await stat7(path14);
12352
12894
  if (!stats.isDirectory()) {
12353
12895
  return { valid: false, error: "Path is not a directory" };
12354
12896
  }
@@ -12383,12 +12925,12 @@ var init_validator = __esm({
12383
12925
  // packages/server/src/fs/gitignore.ts
12384
12926
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
12385
12927
  import ignore from "ignore";
12386
- import { join as join8, relative } from "path";
12928
+ import { join as join10, relative as relative2 } from "path";
12387
12929
  function normalizePath(path14) {
12388
12930
  return path14.replace(/\\/g, "/");
12389
12931
  }
12390
12932
  function relativeToRoot(rootPath, path14) {
12391
- return normalizePath(relative(rootPath, path14));
12933
+ return normalizePath(relative2(rootPath, path14));
12392
12934
  }
12393
12935
  function isDefaultTreeIgnored(name) {
12394
12936
  return name.startsWith(".") || name === "node_modules" || name === ".git";
@@ -12406,7 +12948,7 @@ function isIgnoredByGitignore(ig, path14) {
12406
12948
  return ig.ignores(path14) || ig.ignores(`${path14}/`);
12407
12949
  }
12408
12950
  function createGitignoreFilter(rootPath, dirPath) {
12409
- const gitignorePath = join8(rootPath, ".gitignore");
12951
+ const gitignorePath = join10(rootPath, ".gitignore");
12410
12952
  if (!existsSync7(gitignorePath)) {
12411
12953
  return (name) => !isDefaultTreeIgnored(name);
12412
12954
  }
@@ -12416,7 +12958,7 @@ function createGitignoreFilter(rootPath, dirPath) {
12416
12958
  if (isAlwaysTreeIgnored(name)) {
12417
12959
  return false;
12418
12960
  }
12419
- const relativePath = relativeToRoot(rootPath, join8(dirPath, name));
12961
+ const relativePath = relativeToRoot(rootPath, join10(dirPath, name));
12420
12962
  return !isIgnoredByGitignore(ig, relativePath);
12421
12963
  };
12422
12964
  }
@@ -13087,8 +13629,8 @@ var init_fencing = __esm({
13087
13629
  });
13088
13630
 
13089
13631
  // packages/server/src/commands/terminal.ts
13090
- import { stat as stat7 } from "node:fs/promises";
13091
- import { basename, isAbsolute } from "node:path";
13632
+ import { stat as stat8 } from "node:fs/promises";
13633
+ import { basename, isAbsolute as isAbsolute2 } from "node:path";
13092
13634
  import { z as z6 } from "zod";
13093
13635
  function decodeTerminalInput(args) {
13094
13636
  if ("bytes" in args) {
@@ -13204,7 +13746,7 @@ var init_terminal = __esm({
13204
13746
  }
13205
13747
  let cwd = workspace.path;
13206
13748
  if (args.cwdPath && args.cwdPath !== ".") {
13207
- if (isAbsolute(args.cwdPath)) {
13749
+ if (isAbsolute2(args.cwdPath)) {
13208
13750
  throw { code: "invalid_cwd_path", message: "cwdPath must be workspace-relative" };
13209
13751
  }
13210
13752
  let resolvedCwd;
@@ -13216,7 +13758,7 @@ var init_terminal = __esm({
13216
13758
  }
13217
13759
  throw error;
13218
13760
  }
13219
- const cwdStats = await stat7(resolvedCwd).catch(() => null);
13761
+ const cwdStats = await stat8(resolvedCwd).catch(() => null);
13220
13762
  if (!cwdStats) {
13221
13763
  throw { code: "cwd_not_found", message: `Directory not found: ${args.cwdPath}` };
13222
13764
  }
@@ -13945,7 +14487,7 @@ var init_hub = __esm({
13945
14487
  }
13946
14488
  }
13947
14489
  awaitBinaryPayload(clientId) {
13948
- return new Promise((resolve4, reject) => {
14490
+ return new Promise((resolve6, reject) => {
13949
14491
  const timer = setTimeout(() => {
13950
14492
  const waiters = this.pendingBinaryWaiters.get(clientId);
13951
14493
  if (!waiters) return;
@@ -13957,7 +14499,7 @@ var init_hub = __esm({
13957
14499
  }
13958
14500
  reject(new Error("Timeout waiting for terminal input binary payload"));
13959
14501
  }, BINARY_PAYLOAD_TIMEOUT_MS);
13960
- const waiter = { resolve: resolve4, reject, timer };
14502
+ const waiter = { resolve: resolve6, reject, timer };
13961
14503
  const queue = this.pendingBinaryWaiters.get(clientId);
13962
14504
  if (queue) {
13963
14505
  queue.push(waiter);
@@ -14264,7 +14806,7 @@ var init_hub = __esm({
14264
14806
  // packages/server/src/commands/workspace.ts
14265
14807
  import { readdir as readdir3, realpath as realpath3 } from "node:fs/promises";
14266
14808
  import { homedir as homedir2 } from "node:os";
14267
- import { isAbsolute as isAbsolute2, join as join9, resolve as resolve2 } from "node:path";
14809
+ import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
14268
14810
  import { z as z7 } from "zod";
14269
14811
  function resolveBrowsePath(path14) {
14270
14812
  const home = homedir2();
@@ -14272,9 +14814,9 @@ function resolveBrowsePath(path14) {
14272
14814
  return home;
14273
14815
  }
14274
14816
  if (path14.startsWith("~/")) {
14275
- return join9(home, path14.slice(2));
14817
+ return join11(home, path14.slice(2));
14276
14818
  }
14277
- return isAbsolute2(path14) ? path14 : resolve2(home, path14);
14819
+ return isAbsolute3(path14) ? path14 : resolve4(home, path14);
14278
14820
  }
14279
14821
  async function buildRootPaths(currentPath) {
14280
14822
  const roots = /* @__PURE__ */ new Set(["/"]);
@@ -14307,11 +14849,11 @@ var init_workspace = __esm({
14307
14849
  const entries = await readdir3(basePath, { withFileTypes: true });
14308
14850
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
14309
14851
  name: entry.name,
14310
- path: join9(basePath, entry.name)
14852
+ path: join11(basePath, entry.name)
14311
14853
  })).sort((a, b) => a.name.localeCompare(b.name));
14312
14854
  return {
14313
14855
  currentPath: basePath,
14314
- parentPath: basePath !== "/" ? join9(basePath, "..") : null,
14856
+ parentPath: basePath !== "/" ? join11(basePath, "..") : null,
14315
14857
  directories,
14316
14858
  rootPaths: await buildRootPaths(basePath)
14317
14859
  };
@@ -14775,8 +15317,8 @@ var init_pane_layout = __esm({
14775
15317
  // packages/server/src/commands/session.ts
14776
15318
  import { z as z12 } from "zod";
14777
15319
  function delay(ms) {
14778
- return new Promise((resolve4) => {
14779
- setTimeout(resolve4, ms);
15320
+ return new Promise((resolve6) => {
15321
+ setTimeout(resolve6, ms);
14780
15322
  });
14781
15323
  }
14782
15324
  function getProviderFromRegistry(providerId, registry) {
@@ -14930,8 +15472,8 @@ var init_session2 = __esm({
14930
15472
  // packages/server/src/fs/content-search.ts
14931
15473
  import { spawn as spawn5 } from "child_process";
14932
15474
  import { existsSync as existsSync8 } from "fs";
14933
- import { readdir as readdir4, readFile as readFile4, stat as stat8 } from "fs/promises";
14934
- import { basename as basename2, join as join10, relative as relative2 } from "path";
15475
+ import { readdir as readdir4, readFile as readFile4, stat as stat9 } from "fs/promises";
15476
+ import { basename as basename2, join as join12, relative as relative3 } from "path";
14935
15477
  import { createInterface } from "readline";
14936
15478
  async function searchFileContents(rootPath, options) {
14937
15479
  const query = options.query.trim();
@@ -14954,7 +15496,7 @@ async function searchFileContents(rootPath, options) {
14954
15496
  return finalizeResults(result, options.maxFiles, options.maxMatchesPerFile);
14955
15497
  }
14956
15498
  async function searchWithRipgrep(rootPath, query, maxFiles) {
14957
- const hasGitignore = existsSync8(join10(rootPath, ".gitignore"));
15499
+ const hasGitignore = existsSync8(join12(rootPath, ".gitignore"));
14958
15500
  const args = [
14959
15501
  "--json",
14960
15502
  "--line-number",
@@ -14973,7 +15515,7 @@ async function searchWithRipgrep(rootPath, query, maxFiles) {
14973
15515
  args.push("--no-require-git");
14974
15516
  }
14975
15517
  args.push(query, ".");
14976
- return new Promise((resolve4, reject) => {
15518
+ return new Promise((resolve6, reject) => {
14977
15519
  const child = spawn5("rg", args, { cwd: rootPath, stdio: ["ignore", "pipe", "pipe"] });
14978
15520
  const stdout = createInterface({ input: child.stdout });
14979
15521
  const files = /* @__PURE__ */ new Map();
@@ -14992,7 +15534,7 @@ async function searchWithRipgrep(rootPath, query, maxFiles) {
14992
15534
  if (!rawPath) {
14993
15535
  return;
14994
15536
  }
14995
- const relativePath = normalizeRelativePath(relative2(rootPath, join10(rootPath, rawPath)));
15537
+ const relativePath = normalizeRelativePath(relative3(rootPath, join12(rootPath, rawPath)));
14996
15538
  const preview = (event.data?.lines?.text ?? "").replace(/\r?\n$/, "");
14997
15539
  const lineNumber = event.data?.line_number ?? 1;
14998
15540
  const submatches = event.data?.submatches ?? [];
@@ -15022,7 +15564,7 @@ async function searchWithRipgrep(rootPath, query, maxFiles) {
15022
15564
  child.on("close", (code) => {
15023
15565
  void stdout.close();
15024
15566
  if (code === 0 || code === 1) {
15025
- resolve4({
15567
+ resolve6({
15026
15568
  files: sortAccumulators(files),
15027
15569
  totalMatchCount,
15028
15570
  hasMoreFiles
@@ -15045,7 +15587,7 @@ async function searchWithNode(rootPath, query, maxFiles) {
15045
15587
  const filteredEntries = entries.filter((entry) => filter(entry.name));
15046
15588
  filteredEntries.sort((a, b) => a.name.localeCompare(b.name));
15047
15589
  for (const entry of filteredEntries) {
15048
- const fullPath = join10(dirPath, entry.name);
15590
+ const fullPath = join12(dirPath, entry.name);
15049
15591
  if (entry.isDirectory()) {
15050
15592
  await walk(fullPath);
15051
15593
  continue;
@@ -15053,7 +15595,7 @@ async function searchWithNode(rootPath, query, maxFiles) {
15053
15595
  if (!entry.isFile()) {
15054
15596
  continue;
15055
15597
  }
15056
- const fileStat = await stat8(fullPath);
15598
+ const fileStat = await stat9(fullPath);
15057
15599
  if (fileStat.size > FALLBACK_MAX_FILE_BYTES) {
15058
15600
  continue;
15059
15601
  }
@@ -15061,7 +15603,7 @@ async function searchWithNode(rootPath, query, maxFiles) {
15061
15603
  if (isBinaryFile(buffer)) {
15062
15604
  continue;
15063
15605
  }
15064
- const relativePath = normalizeRelativePath(relative2(rootPath, fullPath));
15606
+ const relativePath = normalizeRelativePath(relative3(rootPath, fullPath));
15065
15607
  const file = collectMatchesFromText(relativePath, buffer.toString("utf-8"), query);
15066
15608
  if (!file) {
15067
15609
  continue;
@@ -15168,10 +15710,10 @@ var init_content_search = __esm({
15168
15710
  });
15169
15711
 
15170
15712
  // packages/server/src/fs/tree.ts
15171
- import { readdir as readdir5, stat as stat9 } from "fs/promises";
15172
- import { join as join11, relative as relative3 } from "path";
15713
+ import { readdir as readdir5, stat as stat10 } from "fs/promises";
15714
+ import { join as join13, relative as relative4 } from "path";
15173
15715
  async function readTree(rootPath, subdir) {
15174
- const targetPath = subdir ? join11(rootPath, subdir) : rootPath;
15716
+ const targetPath = subdir ? join13(rootPath, subdir) : rootPath;
15175
15717
  const filter = createTreeVisibilityFilter();
15176
15718
  const entries = await readdir5(targetPath, { withFileTypes: true });
15177
15719
  const nodes = [];
@@ -15179,8 +15721,8 @@ async function readTree(rootPath, subdir) {
15179
15721
  if (!filter(entry.name)) {
15180
15722
  continue;
15181
15723
  }
15182
- const fullPath = join11(targetPath, entry.name);
15183
- const relPath = relative3(rootPath, fullPath);
15724
+ const fullPath = join13(targetPath, entry.name);
15725
+ const relPath = relative4(rootPath, fullPath);
15184
15726
  if (entry.isDirectory()) {
15185
15727
  nodes.push({
15186
15728
  name: entry.name,
@@ -15190,7 +15732,7 @@ async function readTree(rootPath, subdir) {
15190
15732
  // Not loaded yet - client will request on expand
15191
15733
  });
15192
15734
  } else if (entry.isFile()) {
15193
- const stats = await stat9(fullPath);
15735
+ const stats = await stat10(fullPath);
15194
15736
  nodes.push({
15195
15737
  name: entry.name,
15196
15738
  path: relPath,
@@ -15223,8 +15765,8 @@ async function searchFiles(rootPath, query, limit = 10) {
15223
15765
  const filteredEntries = entries.filter((entry) => filter(entry.name));
15224
15766
  filteredEntries.sort((a, b) => a.name.localeCompare(b.name));
15225
15767
  for (const entry of filteredEntries) {
15226
- const fullPath = join11(dirPath, entry.name);
15227
- const relPath = relative3(rootPath, fullPath);
15768
+ const fullPath = join13(dirPath, entry.name);
15769
+ const relPath = relative4(rootPath, fullPath);
15228
15770
  if (entry.isDirectory()) {
15229
15771
  await walk(fullPath);
15230
15772
  continue;
@@ -15259,7 +15801,7 @@ async function searchFiles(rootPath, query, limit = 10) {
15259
15801
  }
15260
15802
  return a.path.toLowerCase().localeCompare(b.path.toLowerCase());
15261
15803
  }).slice(0, limit)) {
15262
- const stats = await stat9(match.fullPath);
15804
+ const stats = await stat10(match.fullPath);
15263
15805
  files.push({
15264
15806
  name: match.name,
15265
15807
  path: match.path,
@@ -15518,7 +16060,7 @@ var init_file = __esm({
15518
16060
  });
15519
16061
 
15520
16062
  // packages/server/src/git/diff.ts
15521
- import { mkdtemp as mkdtemp3, readFile as readFile5, rm as rm6 } from "fs/promises";
16063
+ import { mkdtemp as mkdtemp3, readFile as readFile5, rm as rm7 } from "fs/promises";
15522
16064
  import os3 from "os";
15523
16065
  import path11 from "path";
15524
16066
  async function isTrackedPath(cwd, filePath) {
@@ -15553,7 +16095,7 @@ async function getUntrackedFileDiff(cwd, filePath) {
15553
16095
  });
15554
16096
  return result.stdout;
15555
16097
  } finally {
15556
- await rm6(tempDir, { recursive: true, force: true });
16098
+ await rm7(tempDir, { recursive: true, force: true });
15557
16099
  }
15558
16100
  }
15559
16101
  async function pathExists(cwd, filePath) {
@@ -15986,25 +16528,25 @@ var init_git2 = __esm({
15986
16528
  // packages/server/src/config/config-io.ts
15987
16529
  import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "node:fs";
15988
16530
  import { homedir as homedir3 } from "node:os";
15989
- import { basename as basename3, dirname as dirname7, join as join12 } from "node:path";
16531
+ import { basename as basename3, dirname as dirname7, join as join14 } from "node:path";
15990
16532
  function resolveConfigPath(configType) {
15991
16533
  if (configType === "codex") {
15992
16534
  const testHome = process.env.CODER_STUDIO_CODEX_HOME;
15993
16535
  if (testHome && testHome.trim()) {
15994
- return join12(testHome, "config.toml");
16536
+ return join14(testHome, "config.toml");
15995
16537
  }
15996
16538
  const codexHome = process.env.CODEX_HOME;
15997
16539
  if (codexHome && codexHome.trim()) {
15998
- return join12(codexHome, "config.toml");
16540
+ return join14(codexHome, "config.toml");
15999
16541
  }
16000
- return join12(homedir3(), ".codex", "config.toml");
16542
+ return join14(homedir3(), ".codex", "config.toml");
16001
16543
  }
16002
16544
  if (configType === "claude") {
16003
16545
  const testHome = process.env.CODER_STUDIO_CLAUDE_HOME;
16004
16546
  if (testHome && testHome.trim()) {
16005
- return join12(testHome, "settings.json");
16547
+ return join14(testHome, "settings.json");
16006
16548
  }
16007
- return join12(homedir3(), ".claude", "settings.json");
16549
+ return join14(homedir3(), ".claude", "settings.json");
16008
16550
  }
16009
16551
  throw new Error(`Unknown config type: ${configType}`);
16010
16552
  }
@@ -16049,7 +16591,7 @@ function createBackup(filePath) {
16049
16591
  const base = basename3(filePath, `.${ext}`);
16050
16592
  const dir = dirname7(filePath);
16051
16593
  const ts = formatTimestamp(/* @__PURE__ */ new Date());
16052
- const backupPath = join12(dir, `${base}.bak.${ts}.${ext}`);
16594
+ const backupPath = join14(dir, `${base}.bak.${ts}.${ext}`);
16053
16595
  writeFileSync5(backupPath, original, "utf-8");
16054
16596
  return backupPath;
16055
16597
  }
@@ -16077,7 +16619,36 @@ function flattenSettings(obj, prefix = "") {
16077
16619
  }
16078
16620
  return result;
16079
16621
  }
16080
- var SettingsSchema;
16622
+ function resolveAppearancePersonalizationOverrideKeysToDelete(settings) {
16623
+ const appearance = settings.appearance;
16624
+ if (!appearance || typeof appearance !== "object" || Array.isArray(appearance)) {
16625
+ return [];
16626
+ }
16627
+ const personalization = appearance.personalization;
16628
+ if (!personalization || typeof personalization !== "object" || Array.isArray(personalization)) {
16629
+ return [];
16630
+ }
16631
+ if (!isFullAppearancePersonalizationSnapshot(personalization)) {
16632
+ return [];
16633
+ }
16634
+ const keysToDelete = [];
16635
+ for (const branch of PERSONALIZATION_OVERRIDE_BRANCHES) {
16636
+ const overrides = personalization[branch];
16637
+ if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
16638
+ continue;
16639
+ }
16640
+ for (const field of PERSONALIZATION_OVERRIDE_FIELDS) {
16641
+ if (!Object.prototype.hasOwnProperty.call(overrides, field)) {
16642
+ keysToDelete.push(`appearance.personalization.${branch}.${field}`);
16643
+ }
16644
+ }
16645
+ }
16646
+ return keysToDelete;
16647
+ }
16648
+ function isFullAppearancePersonalizationSnapshot(personalization) {
16649
+ return Object.prototype.hasOwnProperty.call(personalization, "version") && Object.prototype.hasOwnProperty.call(personalization, "common") && Object.prototype.hasOwnProperty.call(personalization, "desktop") && Object.prototype.hasOwnProperty.call(personalization, "mobile");
16650
+ }
16651
+ var PersonalizationOverridesSchema, PERSONALIZATION_OVERRIDE_BRANCHES, PERSONALIZATION_OVERRIDE_FIELDS, SettingsSchema;
16081
16652
  var init_settings2 = __esm({
16082
16653
  "packages/server/src/commands/settings.ts"() {
16083
16654
  "use strict";
@@ -16086,6 +16657,23 @@ var init_settings2 = __esm({
16086
16657
  init_provider_config();
16087
16658
  init_settings();
16088
16659
  init_dispatch();
16660
+ PersonalizationOverridesSchema = z15.object({
16661
+ backgroundAssetId: z15.string().min(1).nullable().optional(),
16662
+ backgroundDimness: z15.number().int().min(0).max(100).optional(),
16663
+ backgroundBlur: z15.number().int().min(0).max(40).optional(),
16664
+ glassEnabled: z15.boolean().optional(),
16665
+ glassIntensity: z15.number().int().min(0).max(100).optional(),
16666
+ surfaceOpacity: z15.number().int().min(0).max(100).optional()
16667
+ });
16668
+ PERSONALIZATION_OVERRIDE_BRANCHES = ["desktop", "mobile"];
16669
+ PERSONALIZATION_OVERRIDE_FIELDS = [
16670
+ "backgroundAssetId",
16671
+ "backgroundDimness",
16672
+ "backgroundBlur",
16673
+ "glassEnabled",
16674
+ "glassIntensity",
16675
+ "surfaceOpacity"
16676
+ ];
16089
16677
  SettingsSchema = z15.object({
16090
16678
  defaultProviderId: z15.string().optional(),
16091
16679
  notifications: z15.object({
@@ -16112,7 +16700,22 @@ var init_settings2 = __esm({
16112
16700
  terminalFontSize: z15.number().int().min(10).max(18).optional(),
16113
16701
  desktopTerminalFontSize: z15.number().int().min(10).max(18).optional(),
16114
16702
  mobileTerminalFontSize: z15.number().int().min(10).max(18).optional(),
16115
- locale: z15.enum(["zh", "en"]).optional()
16703
+ locale: z15.enum(["zh", "en"]).optional(),
16704
+ personalization: z15.object({
16705
+ version: z15.literal(1).optional(),
16706
+ common: z15.object({
16707
+ backgroundMode: z15.enum(["none", "image"]).optional(),
16708
+ backgroundAssetId: z15.string().min(1).nullable().optional(),
16709
+ backgroundFit: z15.enum(["cover", "contain"]).optional(),
16710
+ backgroundDimness: z15.number().int().min(0).max(100).optional(),
16711
+ backgroundBlur: z15.number().int().min(0).max(40).optional(),
16712
+ glassEnabled: z15.boolean().optional(),
16713
+ glassIntensity: z15.number().int().min(0).max(100).optional(),
16714
+ surfaceOpacity: z15.number().int().min(0).max(100).optional()
16715
+ }).optional(),
16716
+ desktop: PersonalizationOverridesSchema.optional(),
16717
+ mobile: PersonalizationOverridesSchema.optional()
16718
+ }).optional()
16116
16719
  }).optional(),
16117
16720
  lsp: z15.object({
16118
16721
  mode: z15.enum(["auto", "off"]).optional()
@@ -16182,7 +16785,11 @@ var init_settings2 = __esm({
16182
16785
  const nextSettings = args.settings;
16183
16786
  const providers = nextSettings.providers && typeof nextSettings.providers === "object" && !Array.isArray(nextSettings.providers) ? nextSettings.providers : void 0;
16184
16787
  const { providers: _providers, ...nonProviderSettings } = nextSettings;
16788
+ const overrideKeysToDelete = resolveAppearancePersonalizationOverrideKeysToDelete(nextSettings);
16185
16789
  const flatSettings = flattenSettings(nonProviderSettings);
16790
+ for (const key of overrideKeysToDelete) {
16791
+ ctx.settingsRepo.delete(key);
16792
+ }
16186
16793
  for (const [key, value] of Object.entries(flatSettings)) {
16187
16794
  ctx.settingsRepo.set(key, value);
16188
16795
  }
@@ -16828,6 +17435,7 @@ async function listWorktrees(repoPath) {
16828
17435
  current.name = branch.split("/").pop() || branch;
16829
17436
  } else if (line === "detached") {
16830
17437
  current.branch = "detached HEAD";
17438
+ current.name = path12.basename(current.path ?? "") || "detached";
16831
17439
  } else if (line === "") {
16832
17440
  if (current.path) {
16833
17441
  worktrees.push(current);
@@ -17409,12 +18017,12 @@ var init_commands = __esm({
17409
18017
  // packages/server/src/server.ts
17410
18018
  import { mkdtempSync, rmSync as rmSync2 } from "node:fs";
17411
18019
  import { tmpdir } from "node:os";
17412
- import { join as join13 } from "node:path";
18020
+ import { join as join15 } from "node:path";
17413
18021
  async function createServer(configOverrides) {
17414
18022
  const config = parseServerConfig(configOverrides);
17415
18023
  const configuredStateDir = resolveConfiguredStateDir(config);
17416
18024
  const shouldCleanupStateRoot = configuredStateDir === IN_MEMORY_STATE_DIR;
17417
- const stateRoot = shouldCleanupStateRoot ? mkdtempSync(join13(tmpdir(), "coder-studio-state-")) : configuredStateDir;
18025
+ const stateRoot = shouldCleanupStateRoot ? mkdtempSync(join15(tmpdir(), "coder-studio-state-")) : configuredStateDir;
17418
18026
  ensureStateDir(config);
17419
18027
  const eventBus = new EventBus();
17420
18028
  const activationMgr = new ActivationManager();
@@ -17424,10 +18032,10 @@ async function createServer(configOverrides) {
17424
18032
  let commandContext;
17425
18033
  let lspMgr = null;
17426
18034
  const terminalRepo = new TerminalRepo({
17427
- filePath: join13(stateRoot, "state", "terminals.json")
18035
+ filePath: join15(stateRoot, "state", "terminals.json")
17428
18036
  });
17429
18037
  const sessionRepo = new SessionRepo({
17430
- filePath: join13(stateRoot, "state", "sessions.json")
18038
+ filePath: join15(stateRoot, "state", "sessions.json")
17431
18039
  });
17432
18040
  const terminalMgr = new TerminalManager({
17433
18041
  ptyHost: createPtyHost(),
@@ -17435,10 +18043,10 @@ async function createServer(configOverrides) {
17435
18043
  db: terminalRepo
17436
18044
  });
17437
18045
  const settingsRepo = new SettingsRepo({
17438
- filePath: join13(stateRoot, "state", "settings.json")
18046
+ filePath: join15(stateRoot, "state", "settings.json")
17439
18047
  });
17440
18048
  const updateStateRepo = new UpdateStateRepo({
17441
- filePath: join13(stateRoot, "state", "update-state.json"),
18049
+ filePath: join15(stateRoot, "state", "update-state.json"),
17442
18050
  currentVersion: config.appVersion ?? "0.0.0"
17443
18051
  });
17444
18052
  const autoFetch = new AutoFetchScheduler({
@@ -17471,10 +18079,10 @@ async function createServer(configOverrides) {
17471
18079
  }
17472
18080
  });
17473
18081
  const providerConfigRepo = new ProviderConfigRepo({
17474
- filePath: join13(stateRoot, "state", "provider-configs.json")
18082
+ filePath: join15(stateRoot, "state", "provider-configs.json")
17475
18083
  });
17476
18084
  const workspaceRepo = new WorkspaceRepo({
17477
- filePath: join13(stateRoot, "state", "workspaces.json")
18085
+ filePath: join15(stateRoot, "state", "workspaces.json")
17478
18086
  });
17479
18087
  const sessionMgr = new SessionManager({
17480
18088
  terminalMgr,
@@ -17509,10 +18117,13 @@ async function createServer(configOverrides) {
17509
18117
  )
17510
18118
  });
17511
18119
  const authSessionRepo = new AuthSessionRepo({
17512
- filePath: join13(stateRoot, "state", "auth-sessions.json")
18120
+ filePath: join15(stateRoot, "state", "auth-sessions.json")
17513
18121
  });
17514
18122
  const authLoginBlockRepo = new AuthLoginBlockRepo({
17515
- filePath: join13(stateRoot, "state", "auth-login-blocks.json")
18123
+ filePath: join15(stateRoot, "state", "auth-login-blocks.json")
18124
+ });
18125
+ const appearanceAssetRepo = new AppearanceAssetRepo({
18126
+ filePath: join15(stateRoot, "state", "appearance-assets.json")
17516
18127
  });
17517
18128
  const app = await buildFastifyApp({
17518
18129
  wsHub,
@@ -17521,6 +18132,7 @@ async function createServer(configOverrides) {
17521
18132
  config,
17522
18133
  authSessionRepo,
17523
18134
  authLoginBlockRepo,
18135
+ appearanceAssetRepo,
17524
18136
  logger: {
17525
18137
  level: "info",
17526
18138
  transport: {
@@ -17587,7 +18199,7 @@ async function createServer(configOverrides) {
17587
18199
  ...config.update,
17588
18200
  currentVersion: config.appVersion ?? "0.0.0"
17589
18201
  },
17590
- updateWorkerLogFilePath: join13(stateRoot, "logs", "update-worker.log"),
18202
+ updateWorkerLogFilePath: join15(stateRoot, "logs", "update-worker.log"),
17591
18203
  countRunningTerminals: () => terminalMgr.getAll().filter((terminal) => terminal.alive).length,
17592
18204
  countRunningSessions: () => sessionMgr.getAll().filter((session) => session.state === "starting" || session.state === "running").length,
17593
18205
  countActiveSupervisors: () => supervisorMgr?.countActive() ?? 0
@@ -17699,6 +18311,7 @@ var init_server = __esm({
17699
18311
  init_e2e_provider_mock();
17700
18312
  init_install_manager2();
17701
18313
  init_manager3();
18314
+ init_appearance_asset_repo();
17702
18315
  init_auth_login_block_repo();
17703
18316
  init_auth_session_repo();
17704
18317
  init_provider_config_repo();
@@ -17817,9 +18430,9 @@ import { fileURLToPath as fileURLToPath4 } from "url";
17817
18430
  init_state_paths();
17818
18431
  import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
17819
18432
  import { homedir as homedir4 } from "os";
17820
- import { join as join14 } from "path";
18433
+ import { join as join16 } from "path";
17821
18434
  function getCliConfigPath() {
17822
- return join14(homedir4(), ".coder-studio", "config.json");
18435
+ return join16(homedir4(), ".coder-studio", "config.json");
17823
18436
  }
17824
18437
  function normalizeLegacyDataDir(input) {
17825
18438
  return normalizeLegacyStateDir(input);
@@ -17847,11 +18460,11 @@ function readCliConfig() {
17847
18460
 
17848
18461
  // packages/cli/src/embed.ts
17849
18462
  import { existsSync as existsSync11 } from "fs";
17850
- import { dirname as dirname8, resolve as resolve3 } from "path";
18463
+ import { dirname as dirname8, resolve as resolve5 } from "path";
17851
18464
  import { fileURLToPath as fileURLToPath2 } from "url";
17852
18465
  var __filename = fileURLToPath2(import.meta.url);
17853
18466
  var __dirname = dirname8(__filename);
17854
- var WEB_ASSETS_DIR = resolve3(__dirname, "../web");
18467
+ var WEB_ASSETS_DIR = resolve5(__dirname, "../web");
17855
18468
  function getStaticAssetsDir() {
17856
18469
  return WEB_ASSETS_DIR;
17857
18470
  }
@@ -17915,13 +18528,13 @@ function getCliPackageName(importMetaUrl) {
17915
18528
 
17916
18529
  // packages/cli/src/update-runtime.ts
17917
18530
  import { existsSync as existsSync13 } from "node:fs";
17918
- import { dirname as dirname9, join as join15 } from "node:path";
18531
+ import { dirname as dirname9, join as join17 } from "node:path";
17919
18532
  import { fileURLToPath as fileURLToPath3 } from "node:url";
17920
18533
  function resolveWorkerEntryPath(importMetaUrl) {
17921
18534
  const currentDir = dirname9(fileURLToPath3(importMetaUrl));
17922
18535
  const candidates = [
17923
- join15(currentDir, "update-worker.mjs"),
17924
- join15(currentDir, "../src/update-worker.ts")
18536
+ join17(currentDir, "update-worker.mjs"),
18537
+ join17(currentDir, "../src/update-worker.ts")
17925
18538
  ];
17926
18539
  return candidates.find((candidate) => existsSync13(candidate));
17927
18540
  }