codex-relay 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src.js CHANGED
@@ -1,11 +1,11 @@
1
- import { A as PairResponseSchema, At as stripPromptSkillMentions, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, E as ListWorkspaceDirectoriesResponseSchema, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Q as RuntimePreferencesSchema, S as ListModelsResponseSchema, St as createOpenApiDocument, T as ListThreadsResponseSchema, Tt as promptMarkdownWithSkills, U as RateLimitsResponseSchema, X as RuntimePreferencesByWorkspacePathSchema, Z as RuntimePreferencesResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as ThreadContextWindowResponseSchema, b as InterruptThreadRunResponseSchema, bt as apiPaths, ct as ThreadMessageDetailResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadSummarySchema, et as StatusResponseSchema, ft as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, ht as WorkspaceChangesResponseSchema, k as PairRequestSchema, l as ChatMessageSchema, m as ContextWindowUsageSchema, nt as StreamThreadRunRequestSchema, ot as ThreadDetailResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as VersionResponseSchema, q as RunThreadRequestSchema, rt as SubmitThreadInputResponseSchema, st as ThreadMessageDetailFieldSchema, tt as StreamThreadRunEventSchema, vt as WorkspaceGitActionResponseSchema, w as ListSkillsResponseSchema, wt as normalizePromptContext, xt as chatMessageDetailsFromPromptContext, y as ImageAttachmentUploadResponseSchema, z as QueuedThreadInputActionResponseSchema } from "./api-schema2.js";
1
+ import { A as PairResponseSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as apiPaths, E as ListWorkspaceDirectoriesResponseSchema, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Lt as stripPromptSkillMentions, Mt as promptMarkdownWithSkills, Ot as chatMessageDetailsFromPromptContext, Q as RuntimePreferencesSchema, S as ListModelsResponseSchema, St as WorkspaceGitActionResponseSchema, T as ListThreadsResponseSchema, U as RateLimitsResponseSchema, X as RuntimePreferencesByWorkspacePathSchema, Z as RuntimePreferencesResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as ThreadContextWindowResponseSchema, b as InterruptThreadRunResponseSchema, bt as WorkspaceFileContentResponseSchema, ct as ThreadMessageDetailResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadSummarySchema, et as StatusResponseSchema, ft as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, jt as normalizePromptContext, k as PairRequestSchema, kt as createOpenApiDocument, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as VersionResponseSchema, nt as StreamThreadRunRequestSchema, ot as ThreadDetailResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as UpdateWorkspaceFileContentRequestSchema, q as RunThreadRequestSchema, rt as SubmitThreadInputResponseSchema, st as ThreadMessageDetailFieldSchema, tt as StreamThreadRunEventSchema, vt as WorkspaceChangesResponseSchema, w as ListSkillsResponseSchema, y as ImageAttachmentUploadResponseSchema, z as QueuedThreadInputActionResponseSchema } from "./api-schema2.js";
2
2
  import { r as legacyCodexRelayDataPath, t as codexRelayDataPath } from "./paths.js";
3
3
  import { createRequire } from "node:module";
4
4
  import qrcode from "qrcode-terminal";
5
5
  import { execFile, execFileSync, spawn } from "node:child_process";
6
6
  import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
7
- import { access, appendFile, copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
8
- import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
7
+ import { access, appendFile, copyFile, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
8
+ import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { z } from "zod";
11
11
  import os, { homedir, hostname, networkInterfaces } from "node:os";
@@ -539,14 +539,16 @@ function updatePreferencesFile(current, patch, workspacePath) {
539
539
  });
540
540
  }
541
541
  function mergeRuntimePreferences(current, patch) {
542
- const { model, reasoningEffort, threadId: _threadId, workspacePath: _workspacePath, ...rest } = UpdateRuntimePreferencesRequestSchema.parse(patch);
542
+ const { model, serviceTier, reasoningEffort, threadId: _threadId, workspacePath: _workspacePath, ...rest } = UpdateRuntimePreferencesRequestSchema.parse(patch);
543
543
  const next = {
544
544
  ...current,
545
545
  ...rest,
546
546
  ...model === null ? {} : { model },
547
+ ...serviceTier === null ? {} : { serviceTier },
547
548
  ...reasoningEffort === null ? {} : { reasoningEffort }
548
549
  };
549
550
  if (model === null) delete next.model;
551
+ if (serviceTier === null) delete next.serviceTier;
550
552
  if (reasoningEffort === null) delete next.reasoningEffort;
551
553
  return RuntimePreferencesSchema.parse({ ...next });
552
554
  }
@@ -798,6 +800,9 @@ const defaultWorkspacePath = process.cwd();
798
800
  const defaultCodexModel = "gpt-5.5";
799
801
  const execFileAsync = promisify(execFile);
800
802
  const IMAGE_ATTACHMENT_MAX_BYTES = 8 * 1024 * 1024;
803
+ const WORKSPACE_FILE_PREVIEW_MAX_BYTES = 256 * 1024;
804
+ const LOCAL_MARKDOWN_IMAGE_PATTERN = /!\[([^\]]*)\]\(([^)]*)\)/g;
805
+ const LOCAL_IMAGE_REFERENCE_PATTERN = /\.(gif|heic|heif|jpe?g|png|webp)$/i;
801
806
  const imageAttachmentDirectory = codexRelayDataPath("attachments/images");
802
807
  const relayPackage = createRequire(import.meta.url)("../package.json");
803
808
  const collaborationModeTemplateNames = [
@@ -1157,8 +1162,12 @@ function createApp(options = {}) {
1157
1162
  const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
1158
1163
  if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
1159
1164
  const query = c.req.query("query")?.trim() ?? "";
1165
+ const directoryPath = normalizeWorkspaceDirectoryPath(c.req.query("directory") ?? "");
1166
+ if (!directoryPath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_file_path", directoryPath.error), 400);
1160
1167
  const response = ListWorkspaceFilesResponseSchema.parse({
1161
- files: await listWorkspaceFiles(selectedWorkspacePath.path, query),
1168
+ directory: directoryPath.path,
1169
+ files: await listWorkspaceFiles(selectedWorkspacePath.path, query, directoryPath.path),
1170
+ parentDirectory: parentWorkspaceDirectory(directoryPath.path),
1162
1171
  query,
1163
1172
  workspacePath: selectedWorkspacePath.path
1164
1173
  });
@@ -1167,6 +1176,40 @@ function createApp(options = {}) {
1167
1176
  return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_files_unavailable", errorMessage(error)), 502);
1168
1177
  }
1169
1178
  });
1179
+ app.get(apiPaths.workspaceFileContent, async (c) => {
1180
+ try {
1181
+ const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
1182
+ if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
1183
+ const requestedPath = c.req.query("path")?.trim();
1184
+ if (!requestedPath) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("missing_workspace_file_path", "Workspace file path is required."), 400);
1185
+ const file = await readWorkspaceFileContent(selectedWorkspacePath.path, requestedPath);
1186
+ if (!file.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError(file.code, file.error), file.status);
1187
+ const response = WorkspaceFileContentResponseSchema.parse({
1188
+ workspacePath: selectedWorkspacePath.path,
1189
+ ...file.content
1190
+ });
1191
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
1192
+ } catch (error) {
1193
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_file_unavailable", errorMessage(error)), 502);
1194
+ }
1195
+ });
1196
+ app.put(apiPaths.workspaceFileContent, async (c) => {
1197
+ const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, UpdateWorkspaceFileContentRequestSchema);
1198
+ if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
1199
+ try {
1200
+ const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
1201
+ if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
1202
+ const file = await updateWorkspaceFileContent(selectedWorkspacePath.path, parsed.data);
1203
+ if (!file.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError(file.code, file.error), file.status);
1204
+ const response = WorkspaceFileContentResponseSchema.parse({
1205
+ workspacePath: selectedWorkspacePath.path,
1206
+ ...file.content
1207
+ });
1208
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
1209
+ } catch (error) {
1210
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_file_update_unavailable", errorMessage(error)), 502);
1211
+ }
1212
+ });
1170
1213
  app.get(apiPaths.rateLimits, async (c) => {
1171
1214
  if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, RateLimitsResponseSchema.parse({ buckets: [] }));
1172
1215
  try {
@@ -1217,14 +1260,24 @@ function createApp(options = {}) {
1217
1260
  return c.json(apiError("not_found", "Image attachment not found."), 404);
1218
1261
  }
1219
1262
  const fileStat = statSync(filePath);
1263
+ if (!fileStat.isFile() || fileStat.size === 0) {
1264
+ relayDebugLog("image_attachment.rejected", {
1265
+ attachmentId,
1266
+ reason: fileStat.isFile() ? "empty_file" : "not_file",
1267
+ userAgent: c.req.header("user-agent")
1268
+ });
1269
+ return c.json(apiError("not_found", "Image attachment not found."), 404);
1270
+ }
1271
+ const imageBytes = await readFile(filePath);
1220
1272
  relayDebugLog("image_attachment.served", {
1221
1273
  attachmentId,
1222
1274
  mimeType: imageMimeType(filePath),
1223
- size: fileStat.size,
1275
+ size: imageBytes.length,
1224
1276
  userAgent: c.req.header("user-agent")
1225
1277
  });
1226
- return new Response(await readFile(filePath), { headers: {
1278
+ return new Response(imageBytes, { headers: {
1227
1279
  "cache-control": "private, max-age=31536000, immutable",
1280
+ "content-length": String(imageBytes.length),
1228
1281
  "content-type": imageMimeType(filePath)
1229
1282
  } });
1230
1283
  });
@@ -1289,7 +1342,7 @@ function createApp(options = {}) {
1289
1342
  let loadedMessages = false;
1290
1343
  let messages = cachedMessages;
1291
1344
  let responseThread = preserveKnownRunningThreadState(mappedThread, wasKnownRunning);
1292
- const rolloutHistory = readRolloutThreadMessages(threadId);
1345
+ const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
1293
1346
  if (rolloutHistory.messages.length > 0) {
1294
1347
  messages = mergeThreadMessagePages(rolloutHistory.messages, cachedMessages);
1295
1348
  responseThread = rememberRolloutThreadMessages(threads, responseThread, messages, rolloutHistory.messageCountLowerBound);
@@ -1328,7 +1381,7 @@ function createApp(options = {}) {
1328
1381
  });
1329
1382
  }
1330
1383
  const cachedMessages = messagesByThreadId.get(threadId) ?? [];
1331
- const rolloutHistory = readRolloutThreadMessages(threadId);
1384
+ const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
1332
1385
  const baseThread = threads.get(threadId) ?? knownThread ?? (rolloutHistory.rolloutPath ? rolloutThreadMetadata(threadId, workspacePath, rolloutHistory.rolloutPath, [...rolloutHistory.messages, ...cachedMessages]) : void 0);
1333
1386
  if (baseThread && (cachedMessages.length > 0 || rolloutHistory.messages.length > 0)) {
1334
1387
  const messages = rolloutHistory.messages.length > 0 ? mergeThreadMessagePages(rolloutHistory.messages, cachedMessages) : dedupeThreadMessages(cachedMessages);
@@ -1870,7 +1923,8 @@ async function createAppServerThreadRecord(input) {
1870
1923
  experimentalRawEvents: false,
1871
1924
  model: input.options.model ?? null,
1872
1925
  persistExtendedHistory: true,
1873
- sandbox: runtime.sandbox
1926
+ sandbox: runtime.sandbox,
1927
+ serviceTier: input.options.serviceTier ?? null
1874
1928
  });
1875
1929
  const metadata = ThreadSummarySchema.parse({
1876
1930
  ...mapAppServerThread({
@@ -2123,6 +2177,7 @@ async function startAppServerTurn(appServer, threadId, input) {
2123
2177
  input: appServerTurnInput(input.prompt, input.attachments, input.skills),
2124
2178
  model: input.runOptions.model ?? null,
2125
2179
  sandboxPolicy: runtime.sandboxPolicy,
2180
+ serviceTier: input.runOptions.serviceTier ?? null,
2126
2181
  threadId
2127
2182
  };
2128
2183
  await resumeAppServerThreadIfNeeded(appServer, threadId, input, runtime);
@@ -2154,6 +2209,7 @@ async function resumeAppServerThread(appServer, threadId, input, runtime) {
2154
2209
  model: input.runOptions.model ?? null,
2155
2210
  persistExtendedHistory: true,
2156
2211
  sandbox: runtime.sandbox,
2212
+ serviceTier: input.runOptions.serviceTier ?? null,
2157
2213
  threadId
2158
2214
  });
2159
2215
  }
@@ -2517,7 +2573,11 @@ async function runAppServerPromptStreamed(input) {
2517
2573
  case "item/completed": {
2518
2574
  const item = params?.item;
2519
2575
  if (!item || typeof item !== "object") return;
2520
- if (isDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, item, userMessage.id, prompt)) return;
2576
+ const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, firstString(params, ["turnId"]) ?? activeTurnId, item, userMessage.id, prompt);
2577
+ if (canonicalUserMessage) {
2578
+ userMessage = canonicalUserMessage;
2579
+ return;
2580
+ }
2521
2581
  const turnId = firstString(params, ["turnId"]) ?? activeTurnId;
2522
2582
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turnId, item);
2523
2583
  if (!message) return;
@@ -2750,7 +2810,11 @@ async function runAppServerPromptStreamed(input) {
2750
2810
  debugStream("start turn complete", activeThreadId, activeTurnId);
2751
2811
  let streamedReturnedItem = false;
2752
2812
  for (const item of turn.items) {
2753
- if (isDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, item, userMessage.id, displayPrompt)) continue;
2813
+ const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, activeTurnId, item, userMessage.id, displayPrompt);
2814
+ if (canonicalUserMessage) {
2815
+ userMessage = canonicalUserMessage;
2816
+ continue;
2817
+ }
2754
2818
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, activeTurnId, item);
2755
2819
  if (!message) continue;
2756
2820
  streamedReturnedItem = true;
@@ -2900,7 +2964,8 @@ async function recoverMissingAppServerThread(input) {
2900
2964
  experimentalRawEvents: false,
2901
2965
  model: input.runOptions.model ?? null,
2902
2966
  persistExtendedHistory: true,
2903
- sandbox: runtime.sandbox
2967
+ sandbox: runtime.sandbox,
2968
+ serviceTier: input.runOptions.serviceTier ?? null
2904
2969
  });
2905
2970
  const recoveredThread = mapAppServerThread({
2906
2971
  ...thread,
@@ -3062,6 +3127,12 @@ function upsertAppServerItemMessage(messagesByThreadId, threadId, turnId, item)
3062
3127
  turnId: message.turnId
3063
3128
  });
3064
3129
  }
3130
+ function replaceDuplicateInitialUserMessage(messagesByThreadId, threadId, turnId, item, localMessageId, prompt) {
3131
+ if (!isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt)) return;
3132
+ const message = mapAppServerItem(threadId, appServerTurnShell(turnId), item);
3133
+ if (!message) return;
3134
+ return replaceMessage(messagesByThreadId, threadId, localMessageId, messageWithReplacementDetail(message, localMessageId));
3135
+ }
3065
3136
  function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt) {
3066
3137
  if (item.type !== "userMessage" || !("content" in item) || !Array.isArray(item.content)) return false;
3067
3138
  const localMessage = messagesByThreadId.get(threadId)?.find((message) => message.id === localMessageId);
@@ -3069,6 +3140,15 @@ function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, local
3069
3140
  const normalizedPrompt = stripPromptSkillMentions(prompt, skills);
3070
3141
  return stripPromptSkillMentions(localMessage?.content ?? "", skills) === normalizedPrompt && stripPromptSkillMentions(appServerUserMessageText(item), skills) === normalizedPrompt;
3071
3142
  }
3143
+ function messageWithReplacementDetail(message, replacesMessageId) {
3144
+ return ChatMessageSchema.parse({
3145
+ ...message,
3146
+ details: {
3147
+ ...message.details,
3148
+ replacesMessageId
3149
+ }
3150
+ });
3151
+ }
3072
3152
  function appServerUserMessageText(item) {
3073
3153
  const skills = appServerUserMessageSkills(item);
3074
3154
  return promptMarkdownWithSkills(promptWithAppServerImageReferences(item.content.map((content) => {
@@ -3076,6 +3156,9 @@ function appServerUserMessageText(item) {
3076
3156
  case "text": return content.text;
3077
3157
  case "image":
3078
3158
  case "localImage":
3159
+ case "document":
3160
+ case "file":
3161
+ case "localFile":
3079
3162
  case "mention":
3080
3163
  case "skill": return "";
3081
3164
  }
@@ -3094,14 +3177,90 @@ function appServerUserMessageDetails(item) {
3094
3177
  type: "image"
3095
3178
  }];
3096
3179
  case "localImage": return localImageAttachmentDetails(content.path);
3180
+ case "document":
3181
+ case "file":
3182
+ case "localFile": return appServerDocumentAttachmentDetails(content);
3097
3183
  default: return [];
3098
3184
  }
3099
3185
  });
3100
3186
  return attachments.length > 0 ? { attachments } : void 0;
3101
3187
  }
3188
+ function appServerAgentMessageParts(item) {
3189
+ return agentMessageParts(item.text);
3190
+ }
3191
+ function agentMessageParts(text) {
3192
+ const imageReferences = localMarkdownImageReferences(text);
3193
+ if (imageReferences.length === 0) return {
3194
+ content: text,
3195
+ details: void 0
3196
+ };
3197
+ const materializedReferences = /* @__PURE__ */ new Set();
3198
+ const attachments = imageReferences.flatMap((reference) => {
3199
+ const name = reference.alt || basename(localImageFilePath(reference.destination) ?? reference.destination);
3200
+ const details = localImageAttachmentDetails(reference.destination, name);
3201
+ if (details.length > 0) materializedReferences.add(reference.destination);
3202
+ return details;
3203
+ });
3204
+ return {
3205
+ content: attachments.length > 0 ? stripMaterializedMarkdownImages(text, materializedReferences) : text,
3206
+ details: attachments.length > 0 ? { attachments } : void 0
3207
+ };
3208
+ }
3209
+ function localMarkdownImageReferences(markdown) {
3210
+ const references = [];
3211
+ const seen = /* @__PURE__ */ new Set();
3212
+ for (const match of markdown.matchAll(LOCAL_MARKDOWN_IMAGE_PATTERN)) {
3213
+ const destination = markdownImageDestination(match[2] ?? "");
3214
+ if (!destination || seen.has(destination) || !isLocalMarkdownImageReference(destination)) continue;
3215
+ seen.add(destination);
3216
+ references.push({
3217
+ alt: (match[1] ?? "").trim(),
3218
+ destination
3219
+ });
3220
+ }
3221
+ return references;
3222
+ }
3223
+ function stripMaterializedMarkdownImages(markdown, destinations) {
3224
+ return markdown.replace(LOCAL_MARKDOWN_IMAGE_PATTERN, (match, _alt, rawDestination) => {
3225
+ const destination = markdownImageDestination(rawDestination);
3226
+ return destination && destinations.has(destination) ? "" : match;
3227
+ }).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
3228
+ }
3229
+ function markdownImageDestination(value) {
3230
+ let destination = value.trim();
3231
+ if (destination.startsWith("<")) {
3232
+ const endIndex = destination.indexOf(">");
3233
+ if (endIndex > 0) destination = destination.slice(1, endIndex);
3234
+ } else {
3235
+ const titleIndex = destination.search(/\s+["']/);
3236
+ if (titleIndex > 0) destination = destination.slice(0, titleIndex);
3237
+ }
3238
+ try {
3239
+ return decodeURI(destination);
3240
+ } catch {
3241
+ return destination;
3242
+ }
3243
+ }
3244
+ function isLocalMarkdownImageReference(reference) {
3245
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(reference) && !reference.startsWith("file://")) return false;
3246
+ const filePath = localImageFilePath(reference);
3247
+ return Boolean(filePath && LOCAL_IMAGE_REFERENCE_PATTERN.test(filePath));
3248
+ }
3249
+ function appServerDocumentAttachmentDetails(input) {
3250
+ const reference = input.path ?? input.url;
3251
+ if (!reference) return [];
3252
+ return [{
3253
+ mimeType: input.mimeType,
3254
+ name: input.name ?? basename(reference),
3255
+ path: input.path,
3256
+ type: "document",
3257
+ url: input.url
3258
+ }];
3259
+ }
3102
3260
  async function saveUploadedImageAttachment(file) {
3103
3261
  if (!file.type.startsWith("image/")) throw new Error(`Unsupported attachment type: ${file.type || "unknown"}`);
3104
3262
  if (file.size > IMAGE_ATTACHMENT_MAX_BYTES) throw new Error(`Image ${file.name || "attachment"} is too large.`);
3263
+ if (file.size === 0) throw new Error(`Image ${file.name || "attachment"} is empty.`);
3105
3264
  const attachmentId = `${Date.now()}-${randomUUID()}${imageExtension(file.name, file.type)}`;
3106
3265
  const filePath = resolve(imageAttachmentDirectory, attachmentId);
3107
3266
  await mkdir(imageAttachmentDirectory, { recursive: true });
@@ -3152,7 +3311,7 @@ function materializeLocalImageFile(path, name = basename(path)) {
3152
3311
  const filePath = localImageFilePath(path);
3153
3312
  if (!filePath || !existsSync(filePath)) return;
3154
3313
  const fileStat = statSync(filePath);
3155
- if (!fileStat.isFile() || fileStat.size > IMAGE_ATTACHMENT_MAX_BYTES) return;
3314
+ if (!fileStat.isFile() || fileStat.size === 0 || fileStat.size > IMAGE_ATTACHMENT_MAX_BYTES) return;
3156
3315
  const buffer = readFileSync(filePath);
3157
3316
  const mimeType = imageMimeType(filePath);
3158
3317
  const attachmentId = `${createHash("sha256").update(buffer).digest("hex").slice(0, 24)}${imageExtension(name, mimeType)}`;
@@ -3268,6 +3427,17 @@ function updateMessage(messagesByThreadId, threadId, messageId, update) {
3268
3427
  messages[index] = next;
3269
3428
  return next;
3270
3429
  }
3430
+ function replaceMessage(messagesByThreadId, threadId, messageId, replacement) {
3431
+ const messages = messagesByThreadId.get(threadId) ?? [];
3432
+ const index = messages.findIndex((message) => message.id === messageId);
3433
+ if (index === -1) throw new Error(`Unknown message: ${messageId}`);
3434
+ const next = ChatMessageSchema.parse({
3435
+ ...replacement,
3436
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3437
+ });
3438
+ messages[index] = next;
3439
+ return next;
3440
+ }
3271
3441
  function replaceLocalThreadId(threads, messagesByThreadId, liveThreads, currentThreadId, sdkThreadId) {
3272
3442
  if (!sdkThreadId || sdkThreadId === currentThreadId) return currentThreadId;
3273
3443
  const metadata = threads.get(currentThreadId);
@@ -3511,6 +3681,7 @@ function runtimeMetadataFromOptions(options) {
3511
3681
  const sandboxMode = options.sandboxMode ?? runtime.sandboxMode;
3512
3682
  return {
3513
3683
  ...options.model ? { model: options.model } : {},
3684
+ ...options.serviceTier ? { serviceTier: options.serviceTier } : {},
3514
3685
  ...options.runtimeMode ? { runtimeMode: options.runtimeMode } : {},
3515
3686
  ...options.collaborationMode ? { collaborationMode: options.collaborationMode } : {},
3516
3687
  ...approvalPolicy ? { approvalPolicy } : {},
@@ -3523,6 +3694,7 @@ function withRuntimePreferences(preferences, options) {
3523
3694
  ...options,
3524
3695
  approvalPolicy: options.approvalPolicy,
3525
3696
  model: options.model ?? preferences.model,
3697
+ serviceTier: options.serviceTier ?? preferences.serviceTier,
3526
3698
  reasoningEffort: options.reasoningEffort ?? preferences.reasoningEffort,
3527
3699
  runtimeMode: options.runtimeMode ?? preferences.runtimeMode,
3528
3700
  sandboxMode: options.sandboxMode
@@ -3570,7 +3742,7 @@ function sandboxPolicyForMode(sandboxMode, workspacePath) {
3570
3742
  };
3571
3743
  }
3572
3744
  function hasExplicitRunOptions(options) {
3573
- return Boolean(options.model || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
3745
+ return Boolean(options.model || options.serviceTier || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
3574
3746
  }
3575
3747
  function mapAppServerThread(thread, fallbackMessageCount) {
3576
3748
  const createdAt = fromUnixSeconds(thread.createdAt);
@@ -3628,26 +3800,58 @@ function mergeThreadMessagePages(incomingMessages, cachedMessages) {
3628
3800
  return dedupeThreadMessages(Array.from(byId.values()));
3629
3801
  }
3630
3802
  function dedupeThreadMessages(messages) {
3803
+ const byCrossSourceKey = /* @__PURE__ */ new Map();
3631
3804
  const byImageKey = /* @__PURE__ */ new Map();
3632
3805
  const deduped = [];
3633
3806
  const sortedMessages = [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
3634
3807
  for (const message of sortedMessages) {
3808
+ const crossSourceKey = crossSourceMessageKey(message);
3809
+ const crossSourceIndex = crossSourceKey ? byCrossSourceKey.get(crossSourceKey) : void 0;
3810
+ if (crossSourceIndex !== void 0) {
3811
+ const existingMessage = deduped[crossSourceIndex];
3812
+ if (existingMessage && shouldPreferDuplicateThreadMessage(message, existingMessage)) deduped[crossSourceIndex] = message;
3813
+ continue;
3814
+ }
3815
+ const previous = deduped[deduped.length - 1];
3816
+ if (previous && isDuplicateCrossSourceMessage(previous, message)) {
3817
+ if (shouldPreferDuplicateThreadMessage(message, previous)) deduped[deduped.length - 1] = message;
3818
+ continue;
3819
+ }
3635
3820
  const imageKey = userImageMessageKey(message);
3636
3821
  if (!imageKey) {
3822
+ if (crossSourceKey) byCrossSourceKey.set(crossSourceKey, deduped.length);
3637
3823
  deduped.push(message);
3638
3824
  continue;
3639
3825
  }
3640
3826
  const existingIndex = byImageKey.get(imageKey);
3641
3827
  if (existingIndex === void 0) {
3642
3828
  byImageKey.set(imageKey, deduped.length);
3829
+ if (crossSourceKey) byCrossSourceKey.set(crossSourceKey, deduped.length);
3643
3830
  deduped.push(message);
3644
3831
  continue;
3645
3832
  }
3646
3833
  const existingMessage = deduped[existingIndex];
3647
- if (existingMessage && shouldPreferDuplicateImageMessage(message, existingMessage)) deduped[existingIndex] = message;
3834
+ if (existingMessage && shouldPreferDuplicateThreadMessage(message, existingMessage)) deduped[existingIndex] = message;
3648
3835
  }
3649
3836
  return deduped;
3650
3837
  }
3838
+ function crossSourceMessageKey(message) {
3839
+ if (!isSyntheticHistoryMessageId(message.id)) return;
3840
+ if (message.role !== "user" && message.role !== "assistant") return;
3841
+ return [
3842
+ message.threadId,
3843
+ message.createdAt.slice(0, 19),
3844
+ message.role,
3845
+ message.kind,
3846
+ message.content
3847
+ ].join("\n");
3848
+ }
3849
+ function isDuplicateCrossSourceMessage(previous, next) {
3850
+ return previous.id !== next.id && (isSyntheticHistoryMessageId(previous.id) || isSyntheticHistoryMessageId(next.id)) && previous.threadId === next.threadId && previous.role === next.role && previous.kind === next.kind && previous.content === next.content;
3851
+ }
3852
+ function isSyntheticHistoryMessageId(id) {
3853
+ return id.startsWith("msg-") || id.startsWith("rollout:");
3854
+ }
3651
3855
  function userImageMessageKey(message) {
3652
3856
  if (message.role !== "user") return;
3653
3857
  const imageUris = imageAttachmentUris(message);
@@ -3665,9 +3869,11 @@ function imageAttachmentUris(message) {
3665
3869
  return ["url" in attachment ? attachment.url : void 0, "path" in attachment ? attachment.path : void 0].filter((value) => typeof value === "string" && value.length > 0);
3666
3870
  });
3667
3871
  }
3668
- function shouldPreferDuplicateImageMessage(candidate, existing) {
3872
+ function shouldPreferDuplicateThreadMessage(candidate, existing) {
3669
3873
  if (existing.id.startsWith("rollout") && !candidate.id.startsWith("rollout")) return true;
3670
3874
  if (!existing.id.startsWith("rollout") && candidate.id.startsWith("rollout")) return false;
3875
+ if (!existing.turnId && candidate.turnId) return true;
3876
+ if (existing.turnId && !candidate.turnId) return false;
3671
3877
  return candidate.content.length < existing.content.length;
3672
3878
  }
3673
3879
  function rememberRolloutThreadMessages(threads, thread, messages, messageCountLowerBound = messages.length) {
@@ -3712,7 +3918,7 @@ function readSessionIndexThreadTitle(threadId) {
3712
3918
  } catch {}
3713
3919
  }
3714
3920
  }
3715
- function readRolloutThreadMessages(threadId) {
3921
+ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath) {
3716
3922
  const rolloutPath = findRolloutFileForThread(threadId);
3717
3923
  if (!rolloutPath) return {
3718
3924
  messageCountLowerBound: 0,
@@ -3720,6 +3926,8 @@ function readRolloutThreadMessages(threadId) {
3720
3926
  rolloutPath
3721
3927
  };
3722
3928
  const collected = [];
3929
+ const applyPatchInputs = /* @__PURE__ */ new Map();
3930
+ const pendingApplyPatchChanges = [];
3723
3931
  const lines = readFileSync(rolloutPath, "utf8").split("\n");
3724
3932
  for (let index = 0; index < lines.length; index += 1) {
3725
3933
  const line = lines[index];
@@ -3727,7 +3935,15 @@ function readRolloutThreadMessages(threadId) {
3727
3935
  if (!line.trim()) continue;
3728
3936
  if (!isRolloutMessageLine(line)) continue;
3729
3937
  try {
3730
- const message = rolloutRecordMessage(threadId, JSON.parse(line), `rollout:${lineNumber}`);
3938
+ const record = JSON.parse(line);
3939
+ rememberRolloutApplyPatchInput(record, applyPatchInputs);
3940
+ collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges);
3941
+ if (isRolloutTaskComplete(record) && pendingApplyPatchChanges.length > 0) {
3942
+ collected.push(rolloutApplyPatchSummaryMessage(threadId, record, `rollout:${lineNumber}:apply_patch`, pendingApplyPatchChanges));
3943
+ pendingApplyPatchChanges.length = 0;
3944
+ continue;
3945
+ }
3946
+ const message = rolloutRecordMessage(threadId, record, `rollout:${lineNumber}`, workspacePath);
3731
3947
  if (!message) continue;
3732
3948
  collected.push(message);
3733
3949
  } catch {}
@@ -3754,7 +3970,10 @@ function findRolloutFileForThread(threadId) {
3754
3970
  }
3755
3971
  }
3756
3972
  }
3757
- function rolloutRecordMessage(threadId, record, messageKey) {
3973
+ function isRolloutTaskComplete(record) {
3974
+ return record.type === "event_msg" && record.payload?.type === "task_complete";
3975
+ }
3976
+ function rolloutRecordMessage(threadId, record, messageKey, workspacePath = defaultWorkspacePath) {
3758
3977
  const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
3759
3978
  const payload = record.payload;
3760
3979
  if (!payload) return;
@@ -3774,15 +3993,37 @@ function rolloutRecordMessage(threadId, record, messageKey) {
3774
3993
  if (record.type === "event_msg" && payload.type === "agent_message") {
3775
3994
  const content = firstString(payload, ["message"]);
3776
3995
  if (!content) return;
3996
+ const messageParts = agentMessageParts(content);
3777
3997
  return ChatMessageSchema.parse({
3778
3998
  id: `${messageKey}:assistant`,
3779
3999
  threadId,
3780
4000
  role: "assistant",
3781
- content,
4001
+ content: messageParts.content,
4002
+ details: messageParts.details,
3782
4003
  createdAt: timestamp,
3783
4004
  state: "completed"
3784
4005
  });
3785
4006
  }
4007
+ if (record.type === "event_msg" && payload.type === "patch_apply_end") {
4008
+ const changes = rolloutPatchApplyChanges(payload.changes, workspacePath);
4009
+ if (changes.length === 0) return;
4010
+ const patchPreview = largeTextPreview(changes.map((change) => change.patch).filter((patch) => Boolean(patch)).join("\n"));
4011
+ return ChatMessageSchema.parse({
4012
+ id: `${messageKey}:patch:${firstString(payload, ["call_id"]) ?? ""}`,
4013
+ threadId,
4014
+ role: "tool",
4015
+ kind: "fileChange",
4016
+ content: summarizeFileChanges(changes),
4017
+ createdAt: timestamp,
4018
+ state: "completed",
4019
+ details: {
4020
+ changes: changes.map(({ patch: _patch, ...change }) => change),
4021
+ patch: patchPreview?.text,
4022
+ patchOriginalLength: patchPreview?.originalLength,
4023
+ patchTruncated: patchPreview?.truncated
4024
+ }
4025
+ });
4026
+ }
3786
4027
  if (record.type === "event_msg" && payload.type === "exec_command_end") {
3787
4028
  const command = Array.isArray(payload.command) ? payload.command.map((part) => String(part)).join(" ") : firstString(payload, ["command"]) || "Command";
3788
4029
  const outputPreview = largeTextPreview(firstString(payload, ["aggregated_output"]));
@@ -3837,8 +4078,114 @@ function rolloutImageAttachments(value, source) {
3837
4078
  return localImageAttachmentDetails(image, name);
3838
4079
  });
3839
4080
  }
4081
+ function rememberRolloutApplyPatchInput(record, applyPatchInputs) {
4082
+ const payload = record.payload;
4083
+ if (record.type !== "response_item" || payload?.type !== "custom_tool_call" || firstString(payload, ["name"]) !== "apply_patch") return;
4084
+ const callId = firstString(payload, ["call_id"]);
4085
+ const input = firstString(payload, ["input"]);
4086
+ if (callId && input) applyPatchInputs.set(callId, input);
4087
+ }
4088
+ function collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges) {
4089
+ const payload = record.payload;
4090
+ if (record.type !== "response_item" || payload?.type !== "custom_tool_call_output" || !firstString(payload, ["call_id"])) return;
4091
+ const changes = rolloutApplyPatchOutputChanges(rolloutCustomToolOutputText(payload), workspacePath);
4092
+ if (changes.length === 0) return;
4093
+ const callId = firstString(payload, ["call_id"]);
4094
+ const patch = callId ? applyPatchInputs.get(callId) : void 0;
4095
+ for (const change of changes) pendingApplyPatchChanges.push({
4096
+ ...change,
4097
+ patch
4098
+ });
4099
+ }
4100
+ function rolloutApplyPatchSummaryMessage(threadId, record, messageKey, pendingApplyPatchChanges) {
4101
+ const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
4102
+ const patchPreview = largeTextPreview([...new Set(pendingApplyPatchChanges.map((change) => change.patch))].filter((patch) => Boolean(patch)).join("\n"));
4103
+ return ChatMessageSchema.parse({
4104
+ id: `${messageKey}:summary`,
4105
+ threadId,
4106
+ role: "tool",
4107
+ kind: "fileChange",
4108
+ content: summarizeFileChanges(pendingApplyPatchChanges),
4109
+ createdAt: timestamp,
4110
+ state: "completed",
4111
+ details: {
4112
+ changes: pendingApplyPatchChanges.map(({ patch: _patch, ...change }) => change),
4113
+ patch: patchPreview?.text,
4114
+ patchOriginalLength: patchPreview?.originalLength,
4115
+ patchTruncated: patchPreview?.truncated
4116
+ }
4117
+ });
4118
+ }
4119
+ function rolloutCustomToolOutputText(payload) {
4120
+ const rawOutput = firstString(payload, ["output"]);
4121
+ if (!rawOutput) return;
4122
+ try {
4123
+ const parsed = JSON.parse(rawOutput);
4124
+ return typeof parsed.output === "string" ? parsed.output : rawOutput;
4125
+ } catch {
4126
+ return rawOutput;
4127
+ }
4128
+ }
4129
+ function rolloutApplyPatchOutputChanges(value, workspacePath) {
4130
+ if (!value || !value.includes("Updated the following files")) return [];
4131
+ return value.split("\n").flatMap((line) => {
4132
+ const match = line.match(/^\s*([ADM])\s+(.+?)\s*$/);
4133
+ if (!match) return [];
4134
+ return [{
4135
+ kind: rolloutPatchChangeKind(match[1] === "A" ? "added" : match[1] === "D" ? "deleted" : "modified"),
4136
+ path: rolloutPatchDisplayPath(match[2] ?? "", workspacePath)
4137
+ }];
4138
+ });
4139
+ }
4140
+ function rolloutPatchApplyChanges(value, workspacePath = defaultWorkspacePath) {
4141
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
4142
+ return Object.entries(value).flatMap(([rawPath, rawChange]) => {
4143
+ if (!rawChange || typeof rawChange !== "object") return [];
4144
+ const record = rawChange;
4145
+ return [{
4146
+ kind: rolloutPatchChangeKind(firstString(record, ["type"]) ?? "modified"),
4147
+ patch: firstString(record, ["unified_diff", "patch"]),
4148
+ path: rolloutPatchDisplayPath(rawPath, workspacePath)
4149
+ }];
4150
+ });
4151
+ }
4152
+ function rolloutPatchChangeKind(type) {
4153
+ const normalized = type.toLowerCase();
4154
+ if ([
4155
+ "add",
4156
+ "added",
4157
+ "create",
4158
+ "created"
4159
+ ].includes(normalized)) return "added";
4160
+ if ([
4161
+ "delete",
4162
+ "deleted",
4163
+ "remove",
4164
+ "removed"
4165
+ ].includes(normalized)) return "deleted";
4166
+ if ([
4167
+ "move",
4168
+ "moved",
4169
+ "rename",
4170
+ "renamed"
4171
+ ].includes(normalized)) return "renamed";
4172
+ return "modified";
4173
+ }
4174
+ function rolloutPatchDisplayPath(value, workspacePath = defaultWorkspacePath) {
4175
+ const filePath = value.startsWith("file://") ? fileUrlPath(value) : value;
4176
+ if (!isAbsolute(filePath)) return filePath;
4177
+ const relativePath = relative(resolve(workspacePath), filePath);
4178
+ return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath) ? relativePath.split("\\").join("/") : filePath;
4179
+ }
4180
+ function fileUrlPath(value) {
4181
+ try {
4182
+ return fileURLToPath(value);
4183
+ } catch {
4184
+ return value;
4185
+ }
4186
+ }
3840
4187
  function isRolloutMessageLine(line) {
3841
- return line.includes("\"type\":\"event_msg\"") && (line.includes("\"type\":\"user_message\"") || line.includes("\"type\":\"agent_message\"") || line.includes("\"type\":\"exec_command_end\"") || line.includes("\"type\":\"mcp_tool_call_end\""));
4188
+ return line.includes("\"type\":\"event_msg\"") && (line.includes("\"type\":\"user_message\"") || line.includes("\"type\":\"agent_message\"") || line.includes("\"type\":\"patch_apply_end\"") || line.includes("\"type\":\"task_complete\"") || line.includes("\"type\":\"exec_command_end\"") || line.includes("\"type\":\"mcp_tool_call_end\"")) || line.includes("\"type\":\"response_item\"") && (line.includes("\"type\":\"custom_tool_call\"") || line.includes("\"type\":\"custom_tool_call_output\""));
3842
4189
  }
3843
4190
  function threadDetailResponse(input) {
3844
4191
  return ThreadDetailResponseSchema.parse({
@@ -3917,12 +4264,13 @@ function mapAppServerItem(threadId, turn, item) {
3917
4264
  case "agentMessage": {
3918
4265
  const agentItem = item;
3919
4266
  const planContent = proposedPlanContent(agentItem.text);
4267
+ const messageParts = appServerAgentMessageParts(agentItem);
3920
4268
  return ChatMessageSchema.parse({
3921
4269
  ...base,
3922
4270
  role: "assistant",
3923
4271
  kind: planContent ? "plan" : void 0,
3924
- content: planContent ?? agentItem.text,
3925
- details: planContent ? { raw: agentItem.text } : void 0
4272
+ content: planContent ?? messageParts.content,
4273
+ details: planContent ? { raw: agentItem.text } : messageParts.details
3926
4274
  });
3927
4275
  }
3928
4276
  case "reasoning": {
@@ -4036,6 +4384,11 @@ function appServerTurnShell(turnId) {
4036
4384
  };
4037
4385
  }
4038
4386
  function mapAppServerModel(model) {
4387
+ const serviceTiers = model.serviceTiers ?? model.additionalSpeedTiers?.map((tier) => ({
4388
+ id: tier,
4389
+ name: tier === "fast" ? "Fast" : tier,
4390
+ description: void 0
4391
+ })) ?? [];
4039
4392
  return {
4040
4393
  id: model.id,
4041
4394
  model: model.model,
@@ -4043,7 +4396,8 @@ function mapAppServerModel(model) {
4043
4396
  description: model.description,
4044
4397
  isDefault: Boolean(model.isDefault),
4045
4398
  defaultReasoningEffort: model.defaultReasoningEffort,
4046
- supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? []
4399
+ supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? [],
4400
+ serviceTiers
4047
4401
  };
4048
4402
  }
4049
4403
  function normalizeRateLimitBuckets(rateLimits) {
@@ -4104,7 +4458,13 @@ function fallbackModels() {
4104
4458
  { reasoningEffort: "medium" },
4105
4459
  { reasoningEffort: "high" },
4106
4460
  { reasoningEffort: "xhigh" }
4107
- ]
4461
+ ],
4462
+ additionalSpeedTiers: ["fast"],
4463
+ serviceTiers: [{
4464
+ id: "priority",
4465
+ name: "Fast",
4466
+ description: "1.5x speed, increased usage"
4467
+ }]
4108
4468
  }];
4109
4469
  }
4110
4470
  function mapAppServerThreadState(status, turns) {
@@ -4808,53 +5168,235 @@ async function git(cwd, args) {
4808
5168
  });
4809
5169
  return stdout.trimEnd();
4810
5170
  }
4811
- async function listWorkspaceFiles(workspacePath, query) {
5171
+ async function listWorkspaceFiles(workspacePath, query, directory) {
4812
5172
  const normalizedQuery = query.toLowerCase();
4813
5173
  const filePaths = await workspaceFilePaths(workspacePath);
4814
5174
  const entriesByPath = /* @__PURE__ */ new Map();
4815
5175
  for (const path of filePaths) {
4816
5176
  if (!path || path.startsWith("../") || path.includes("/.git/")) continue;
4817
- entriesByPath.set(path, {
4818
- directory: dirname(path) === "." ? "" : dirname(path),
5177
+ if (!directory && normalizedQuery) {
5178
+ entriesByPath.set(path, {
5179
+ directory: dirname(path) === "." ? "" : dirname(path),
5180
+ kind: "file",
5181
+ name: path.split("/").pop() ?? path,
5182
+ path
5183
+ });
5184
+ const parts = path.split("/");
5185
+ for (let index = 1; index < parts.length; index += 1) {
5186
+ const directoryPath = parts.slice(0, index).join("/");
5187
+ entriesByPath.set(directoryPath, {
5188
+ directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
5189
+ kind: "directory",
5190
+ name: parts[index - 1],
5191
+ path: directoryPath
5192
+ });
5193
+ }
5194
+ continue;
5195
+ }
5196
+ const prefix = directory ? `${directory}/` : "";
5197
+ if (!path.startsWith(prefix)) continue;
5198
+ const childParts = path.slice(prefix.length).split("/").filter(Boolean);
5199
+ if (childParts.length === 0) continue;
5200
+ if (childParts.length === 1) entriesByPath.set(path, {
5201
+ directory,
4819
5202
  kind: "file",
4820
- name: path.split("/").pop() ?? path,
5203
+ name: childParts[0],
4821
5204
  path
4822
5205
  });
4823
- const parts = path.split("/");
4824
- for (let index = 1; index < parts.length; index += 1) {
4825
- const directoryPath = parts.slice(0, index).join("/");
5206
+ else {
5207
+ const directoryPath = [...directory ? directory.split("/") : [], childParts[0]].join("/");
4826
5208
  entriesByPath.set(directoryPath, {
4827
- directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
5209
+ directory,
4828
5210
  kind: "directory",
4829
- name: parts[index - 1],
5211
+ name: childParts[0],
4830
5212
  path: directoryPath
4831
5213
  });
4832
5214
  }
4833
5215
  }
4834
5216
  return [...entriesByPath.values()].filter((entry) => {
4835
5217
  if (!normalizedQuery) return true;
4836
- return entry.path.toLowerCase().includes(normalizedQuery);
5218
+ return entry.name.toLowerCase().includes(normalizedQuery) || entry.path.toLowerCase().includes(normalizedQuery);
4837
5219
  }).sort((left, right) => {
4838
5220
  const leftScore = workspaceFileScore(left.path, normalizedQuery);
4839
5221
  const rightScore = workspaceFileScore(right.path, normalizedQuery);
4840
5222
  if (leftScore !== rightScore) return leftScore - rightScore;
4841
5223
  if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
4842
5224
  return left.path.localeCompare(right.path);
4843
- }).slice(0, 80);
5225
+ }).slice(0, directory || !normalizedQuery ? 160 : 80);
5226
+ }
5227
+ async function readWorkspaceFileContent(workspacePath, requestedPath) {
5228
+ const relativePath = normalizeWorkspaceRelativePath(requestedPath);
5229
+ if (!relativePath.success) return {
5230
+ code: "invalid_workspace_file_path",
5231
+ error: relativePath.error,
5232
+ status: 400,
5233
+ success: false
5234
+ };
5235
+ const rootPath = resolve(workspacePath);
5236
+ const absolutePath = resolve(rootPath, relativePath.path);
5237
+ if (!isPathInside(rootPath, absolutePath)) return {
5238
+ code: "invalid_workspace_file_path",
5239
+ error: "Workspace file path must stay inside the workspace.",
5240
+ status: 400,
5241
+ success: false
5242
+ };
5243
+ const fileStat = await stat(absolutePath).catch(() => null);
5244
+ if (!fileStat?.isFile()) return {
5245
+ code: "workspace_file_not_found",
5246
+ error: "Workspace file was not found.",
5247
+ status: 404,
5248
+ success: false
5249
+ };
5250
+ const bytesToRead = Math.min(fileStat.size, WORKSPACE_FILE_PREVIEW_MAX_BYTES);
5251
+ const buffer = Buffer.alloc(bytesToRead);
5252
+ if (bytesToRead > 0) {
5253
+ const handle = await open(absolutePath, "r");
5254
+ try {
5255
+ await handle.read(buffer, 0, bytesToRead, 0);
5256
+ } finally {
5257
+ await handle.close();
5258
+ }
5259
+ }
5260
+ const binary = buffer.includes(0);
5261
+ return {
5262
+ content: {
5263
+ binary,
5264
+ content: binary ? "" : buffer.toString("utf8"),
5265
+ directory: dirname(relativePath.path) === "." ? "" : dirname(relativePath.path),
5266
+ language: languageFromWorkspaceFile(relativePath.path),
5267
+ name: basename(relativePath.path),
5268
+ path: relativePath.path,
5269
+ size: fileStat.size,
5270
+ truncated: fileStat.size > WORKSPACE_FILE_PREVIEW_MAX_BYTES
5271
+ },
5272
+ success: true
5273
+ };
5274
+ }
5275
+ async function updateWorkspaceFileContent(workspacePath, input) {
5276
+ const relativePath = normalizeWorkspaceRelativePath(input.path);
5277
+ if (!relativePath.success) return {
5278
+ code: "invalid_workspace_file_path",
5279
+ error: relativePath.error,
5280
+ status: 400,
5281
+ success: false
5282
+ };
5283
+ const rootPath = resolve(workspacePath);
5284
+ const absolutePath = resolve(rootPath, relativePath.path);
5285
+ if (!isPathInside(rootPath, absolutePath)) return {
5286
+ code: "invalid_workspace_file_path",
5287
+ error: "Workspace file path must stay inside the workspace.",
5288
+ status: 400,
5289
+ success: false
5290
+ };
5291
+ if (!(await stat(absolutePath).catch(() => null))?.isFile()) return {
5292
+ code: "workspace_file_not_found",
5293
+ error: "Workspace file was not found.",
5294
+ status: 404,
5295
+ success: false
5296
+ };
5297
+ await writeFile(absolutePath, input.content, "utf8");
5298
+ return readWorkspaceFileContent(workspacePath, relativePath.path);
5299
+ }
5300
+ function normalizeWorkspaceRelativePath(requestedPath) {
5301
+ const normalized = requestedPath.trim().replaceAll("\\", "/").replace(/^\.\/+/, "");
5302
+ if (!normalized) return {
5303
+ error: "Workspace file path is required.",
5304
+ success: false
5305
+ };
5306
+ if (isAbsolute(normalized)) return {
5307
+ error: "Workspace file path must be relative.",
5308
+ success: false
5309
+ };
5310
+ const segments = normalized.split("/").filter(Boolean);
5311
+ if (segments.some((segment) => segment === "." || segment === ".." || segment === ".git")) return {
5312
+ error: "Workspace file path contains an unsupported segment.",
5313
+ success: false
5314
+ };
5315
+ return {
5316
+ path: segments.join("/"),
5317
+ success: true
5318
+ };
5319
+ }
5320
+ function normalizeWorkspaceDirectoryPath(requestedPath) {
5321
+ const normalized = requestedPath.trim().replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
5322
+ if (!normalized) return {
5323
+ path: "",
5324
+ success: true
5325
+ };
5326
+ if (isAbsolute(normalized)) return {
5327
+ error: "Workspace directory path must be relative.",
5328
+ success: false
5329
+ };
5330
+ const segments = normalized.split("/").filter(Boolean);
5331
+ if (segments.some((segment) => segment === "." || segment === ".." || segment === ".git")) return {
5332
+ error: "Workspace directory path contains an unsupported segment.",
5333
+ success: false
5334
+ };
5335
+ return {
5336
+ path: segments.join("/"),
5337
+ success: true
5338
+ };
5339
+ }
5340
+ function parentWorkspaceDirectory(directory) {
5341
+ if (!directory) return null;
5342
+ const parent = dirname(directory);
5343
+ return parent === "." ? "" : parent;
5344
+ }
5345
+ function isPathInside(rootPath, targetPath) {
5346
+ const pathFromRoot = relative(rootPath, targetPath);
5347
+ return Boolean(pathFromRoot) && !pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot);
5348
+ }
5349
+ function languageFromWorkspaceFile(path) {
5350
+ const name = basename(path).toLowerCase();
5351
+ const extension = extname(name).replace(/^\./, "");
5352
+ if (name === "dockerfile") return "dockerfile";
5353
+ if (name === "makefile") return "make";
5354
+ if (name === "package.json") return "json";
5355
+ return {
5356
+ cjs: "javascript",
5357
+ css: "css",
5358
+ diff: "diff",
5359
+ go: "go",
5360
+ htm: "html",
5361
+ html: "html",
5362
+ java: "java",
5363
+ js: "javascript",
5364
+ json: "json",
5365
+ jsx: "jsx",
5366
+ kt: "kotlin",
5367
+ lock: "yaml",
5368
+ log: "text",
5369
+ md: "markdown",
5370
+ mdx: "markdown",
5371
+ mjs: "javascript",
5372
+ plist: "xml",
5373
+ py: "python",
5374
+ rb: "ruby",
5375
+ rs: "rust",
5376
+ sh: "bash",
5377
+ swift: "swift",
5378
+ ts: "typescript",
5379
+ tsx: "tsx",
5380
+ txt: "text",
5381
+ xml: "xml",
5382
+ yaml: "yaml",
5383
+ yml: "yaml"
5384
+ }[extension] ?? extension;
4844
5385
  }
4845
5386
  async function workspaceFilePaths(workspacePath) {
5387
+ const isIgnored = await workspaceIgnoreMatcher(workspacePath);
4846
5388
  try {
4847
5389
  return (await git(workspacePath, [
4848
5390
  "ls-files",
4849
5391
  "--cached",
4850
5392
  "--others",
4851
5393
  "--exclude-standard"
4852
- ])).split("\n").filter(Boolean);
5394
+ ])).split("\n").filter(Boolean).filter((path) => !isIgnored(path));
4853
5395
  } catch {
4854
- return recursiveWorkspaceFilePaths(workspacePath);
5396
+ return recursiveWorkspaceFilePaths(workspacePath, isIgnored);
4855
5397
  }
4856
5398
  }
4857
- async function recursiveWorkspaceFilePaths(rootPath) {
5399
+ async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
4858
5400
  const ignoredDirectories = new Set([
4859
5401
  ".git",
4860
5402
  ".expo",
@@ -4868,17 +5410,45 @@ async function recursiveWorkspaceFilePaths(rootPath) {
4868
5410
  for (const entry of entries) {
4869
5411
  if (entry.name.startsWith(".") && entry.name !== ".github") continue;
4870
5412
  const absolutePath = resolve(directory, entry.name);
5413
+ const relativePath = relative(rootPath, absolutePath).split("\\").join("/");
5414
+ if (isIgnored(relativePath)) continue;
4871
5415
  if (entry.isDirectory()) {
4872
5416
  if (ignoredDirectories.has(entry.name)) continue;
4873
5417
  await visit(absolutePath);
4874
5418
  continue;
4875
5419
  }
4876
- if (entry.isFile()) results.push(relative(rootPath, absolutePath).split("\\").join("/"));
5420
+ if (entry.isFile()) results.push(relativePath);
4877
5421
  }
4878
5422
  }
4879
5423
  await visit(rootPath);
4880
5424
  return results;
4881
5425
  }
5426
+ async function workspaceIgnoreMatcher(workspacePath) {
5427
+ const matchers = (await readFile(join(workspacePath, ".gitignore"), "utf8").catch(() => "")).split(/\r?\n/).map((line) => gitignorePatternMatcher(line)).filter((matcher) => Boolean(matcher));
5428
+ return (path) => matchers.some((matcher) => matcher(path.split("\\").join("/")));
5429
+ }
5430
+ function gitignorePatternMatcher(line) {
5431
+ const trimmed = line.trim();
5432
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return;
5433
+ const directoryOnly = trimmed.endsWith("/");
5434
+ const anchored = trimmed.startsWith("/");
5435
+ const pattern = trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
5436
+ if (!pattern) return;
5437
+ const matcher = gitignoreGlobMatcher(pattern);
5438
+ const hasSlash = pattern.includes("/");
5439
+ return (path) => {
5440
+ const normalizedPath = path.replace(/^\/+/, "");
5441
+ return (anchored || hasSlash ? [normalizedPath] : normalizedPath.split("/")).some((candidate) => {
5442
+ if (directoryOnly) return matcher(candidate) || normalizedPath.startsWith(`${candidate}/`);
5443
+ return matcher(candidate);
5444
+ });
5445
+ };
5446
+ }
5447
+ function gitignoreGlobMatcher(pattern) {
5448
+ const expression = pattern.split("**").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*").replace(/\?/g, "[^/]");
5449
+ const regex = new RegExp(`^${expression}(?:/.*)?$`);
5450
+ return (path) => regex.test(path);
5451
+ }
4882
5452
  function workspaceFileScore(path, query) {
4883
5453
  if (!query) return path.split("/").length;
4884
5454
  const lowerPath = path.toLowerCase();