codex-relay 1.0.3 → 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 {
@@ -1299,7 +1342,7 @@ function createApp(options = {}) {
1299
1342
  let loadedMessages = false;
1300
1343
  let messages = cachedMessages;
1301
1344
  let responseThread = preserveKnownRunningThreadState(mappedThread, wasKnownRunning);
1302
- const rolloutHistory = readRolloutThreadMessages(threadId);
1345
+ const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
1303
1346
  if (rolloutHistory.messages.length > 0) {
1304
1347
  messages = mergeThreadMessagePages(rolloutHistory.messages, cachedMessages);
1305
1348
  responseThread = rememberRolloutThreadMessages(threads, responseThread, messages, rolloutHistory.messageCountLowerBound);
@@ -1338,7 +1381,7 @@ function createApp(options = {}) {
1338
1381
  });
1339
1382
  }
1340
1383
  const cachedMessages = messagesByThreadId.get(threadId) ?? [];
1341
- const rolloutHistory = readRolloutThreadMessages(threadId);
1384
+ const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
1342
1385
  const baseThread = threads.get(threadId) ?? knownThread ?? (rolloutHistory.rolloutPath ? rolloutThreadMetadata(threadId, workspacePath, rolloutHistory.rolloutPath, [...rolloutHistory.messages, ...cachedMessages]) : void 0);
1343
1386
  if (baseThread && (cachedMessages.length > 0 || rolloutHistory.messages.length > 0)) {
1344
1387
  const messages = rolloutHistory.messages.length > 0 ? mergeThreadMessagePages(rolloutHistory.messages, cachedMessages) : dedupeThreadMessages(cachedMessages);
@@ -1880,7 +1923,8 @@ async function createAppServerThreadRecord(input) {
1880
1923
  experimentalRawEvents: false,
1881
1924
  model: input.options.model ?? null,
1882
1925
  persistExtendedHistory: true,
1883
- sandbox: runtime.sandbox
1926
+ sandbox: runtime.sandbox,
1927
+ serviceTier: input.options.serviceTier ?? null
1884
1928
  });
1885
1929
  const metadata = ThreadSummarySchema.parse({
1886
1930
  ...mapAppServerThread({
@@ -2133,6 +2177,7 @@ async function startAppServerTurn(appServer, threadId, input) {
2133
2177
  input: appServerTurnInput(input.prompt, input.attachments, input.skills),
2134
2178
  model: input.runOptions.model ?? null,
2135
2179
  sandboxPolicy: runtime.sandboxPolicy,
2180
+ serviceTier: input.runOptions.serviceTier ?? null,
2136
2181
  threadId
2137
2182
  };
2138
2183
  await resumeAppServerThreadIfNeeded(appServer, threadId, input, runtime);
@@ -2164,6 +2209,7 @@ async function resumeAppServerThread(appServer, threadId, input, runtime) {
2164
2209
  model: input.runOptions.model ?? null,
2165
2210
  persistExtendedHistory: true,
2166
2211
  sandbox: runtime.sandbox,
2212
+ serviceTier: input.runOptions.serviceTier ?? null,
2167
2213
  threadId
2168
2214
  });
2169
2215
  }
@@ -2918,7 +2964,8 @@ async function recoverMissingAppServerThread(input) {
2918
2964
  experimentalRawEvents: false,
2919
2965
  model: input.runOptions.model ?? null,
2920
2966
  persistExtendedHistory: true,
2921
- sandbox: runtime.sandbox
2967
+ sandbox: runtime.sandbox,
2968
+ serviceTier: input.runOptions.serviceTier ?? null
2922
2969
  });
2923
2970
  const recoveredThread = mapAppServerThread({
2924
2971
  ...thread,
@@ -3109,6 +3156,9 @@ function appServerUserMessageText(item) {
3109
3156
  case "text": return content.text;
3110
3157
  case "image":
3111
3158
  case "localImage":
3159
+ case "document":
3160
+ case "file":
3161
+ case "localFile":
3112
3162
  case "mention":
3113
3163
  case "skill": return "";
3114
3164
  }
@@ -3127,11 +3177,86 @@ function appServerUserMessageDetails(item) {
3127
3177
  type: "image"
3128
3178
  }];
3129
3179
  case "localImage": return localImageAttachmentDetails(content.path);
3180
+ case "document":
3181
+ case "file":
3182
+ case "localFile": return appServerDocumentAttachmentDetails(content);
3130
3183
  default: return [];
3131
3184
  }
3132
3185
  });
3133
3186
  return attachments.length > 0 ? { attachments } : void 0;
3134
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
+ }
3135
3260
  async function saveUploadedImageAttachment(file) {
3136
3261
  if (!file.type.startsWith("image/")) throw new Error(`Unsupported attachment type: ${file.type || "unknown"}`);
3137
3262
  if (file.size > IMAGE_ATTACHMENT_MAX_BYTES) throw new Error(`Image ${file.name || "attachment"} is too large.`);
@@ -3556,6 +3681,7 @@ function runtimeMetadataFromOptions(options) {
3556
3681
  const sandboxMode = options.sandboxMode ?? runtime.sandboxMode;
3557
3682
  return {
3558
3683
  ...options.model ? { model: options.model } : {},
3684
+ ...options.serviceTier ? { serviceTier: options.serviceTier } : {},
3559
3685
  ...options.runtimeMode ? { runtimeMode: options.runtimeMode } : {},
3560
3686
  ...options.collaborationMode ? { collaborationMode: options.collaborationMode } : {},
3561
3687
  ...approvalPolicy ? { approvalPolicy } : {},
@@ -3568,6 +3694,7 @@ function withRuntimePreferences(preferences, options) {
3568
3694
  ...options,
3569
3695
  approvalPolicy: options.approvalPolicy,
3570
3696
  model: options.model ?? preferences.model,
3697
+ serviceTier: options.serviceTier ?? preferences.serviceTier,
3571
3698
  reasoningEffort: options.reasoningEffort ?? preferences.reasoningEffort,
3572
3699
  runtimeMode: options.runtimeMode ?? preferences.runtimeMode,
3573
3700
  sandboxMode: options.sandboxMode
@@ -3615,7 +3742,7 @@ function sandboxPolicyForMode(sandboxMode, workspacePath) {
3615
3742
  };
3616
3743
  }
3617
3744
  function hasExplicitRunOptions(options) {
3618
- 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);
3619
3746
  }
3620
3747
  function mapAppServerThread(thread, fallbackMessageCount) {
3621
3748
  const createdAt = fromUnixSeconds(thread.createdAt);
@@ -3791,7 +3918,7 @@ function readSessionIndexThreadTitle(threadId) {
3791
3918
  } catch {}
3792
3919
  }
3793
3920
  }
3794
- function readRolloutThreadMessages(threadId) {
3921
+ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath) {
3795
3922
  const rolloutPath = findRolloutFileForThread(threadId);
3796
3923
  if (!rolloutPath) return {
3797
3924
  messageCountLowerBound: 0,
@@ -3799,6 +3926,8 @@ function readRolloutThreadMessages(threadId) {
3799
3926
  rolloutPath
3800
3927
  };
3801
3928
  const collected = [];
3929
+ const applyPatchInputs = /* @__PURE__ */ new Map();
3930
+ const pendingApplyPatchChanges = [];
3802
3931
  const lines = readFileSync(rolloutPath, "utf8").split("\n");
3803
3932
  for (let index = 0; index < lines.length; index += 1) {
3804
3933
  const line = lines[index];
@@ -3806,7 +3935,15 @@ function readRolloutThreadMessages(threadId) {
3806
3935
  if (!line.trim()) continue;
3807
3936
  if (!isRolloutMessageLine(line)) continue;
3808
3937
  try {
3809
- 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);
3810
3947
  if (!message) continue;
3811
3948
  collected.push(message);
3812
3949
  } catch {}
@@ -3833,7 +3970,10 @@ function findRolloutFileForThread(threadId) {
3833
3970
  }
3834
3971
  }
3835
3972
  }
3836
- 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) {
3837
3977
  const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
3838
3978
  const payload = record.payload;
3839
3979
  if (!payload) return;
@@ -3853,15 +3993,37 @@ function rolloutRecordMessage(threadId, record, messageKey) {
3853
3993
  if (record.type === "event_msg" && payload.type === "agent_message") {
3854
3994
  const content = firstString(payload, ["message"]);
3855
3995
  if (!content) return;
3996
+ const messageParts = agentMessageParts(content);
3856
3997
  return ChatMessageSchema.parse({
3857
3998
  id: `${messageKey}:assistant`,
3858
3999
  threadId,
3859
4000
  role: "assistant",
3860
- content,
4001
+ content: messageParts.content,
4002
+ details: messageParts.details,
3861
4003
  createdAt: timestamp,
3862
4004
  state: "completed"
3863
4005
  });
3864
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
+ }
3865
4027
  if (record.type === "event_msg" && payload.type === "exec_command_end") {
3866
4028
  const command = Array.isArray(payload.command) ? payload.command.map((part) => String(part)).join(" ") : firstString(payload, ["command"]) || "Command";
3867
4029
  const outputPreview = largeTextPreview(firstString(payload, ["aggregated_output"]));
@@ -3916,8 +4078,114 @@ function rolloutImageAttachments(value, source) {
3916
4078
  return localImageAttachmentDetails(image, name);
3917
4079
  });
3918
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
+ }
3919
4187
  function isRolloutMessageLine(line) {
3920
- 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\""));
3921
4189
  }
3922
4190
  function threadDetailResponse(input) {
3923
4191
  return ThreadDetailResponseSchema.parse({
@@ -3996,12 +4264,13 @@ function mapAppServerItem(threadId, turn, item) {
3996
4264
  case "agentMessage": {
3997
4265
  const agentItem = item;
3998
4266
  const planContent = proposedPlanContent(agentItem.text);
4267
+ const messageParts = appServerAgentMessageParts(agentItem);
3999
4268
  return ChatMessageSchema.parse({
4000
4269
  ...base,
4001
4270
  role: "assistant",
4002
4271
  kind: planContent ? "plan" : void 0,
4003
- content: planContent ?? agentItem.text,
4004
- details: planContent ? { raw: agentItem.text } : void 0
4272
+ content: planContent ?? messageParts.content,
4273
+ details: planContent ? { raw: agentItem.text } : messageParts.details
4005
4274
  });
4006
4275
  }
4007
4276
  case "reasoning": {
@@ -4115,6 +4384,11 @@ function appServerTurnShell(turnId) {
4115
4384
  };
4116
4385
  }
4117
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
+ })) ?? [];
4118
4392
  return {
4119
4393
  id: model.id,
4120
4394
  model: model.model,
@@ -4122,7 +4396,8 @@ function mapAppServerModel(model) {
4122
4396
  description: model.description,
4123
4397
  isDefault: Boolean(model.isDefault),
4124
4398
  defaultReasoningEffort: model.defaultReasoningEffort,
4125
- supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? []
4399
+ supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? [],
4400
+ serviceTiers
4126
4401
  };
4127
4402
  }
4128
4403
  function normalizeRateLimitBuckets(rateLimits) {
@@ -4183,7 +4458,13 @@ function fallbackModels() {
4183
4458
  { reasoningEffort: "medium" },
4184
4459
  { reasoningEffort: "high" },
4185
4460
  { reasoningEffort: "xhigh" }
4186
- ]
4461
+ ],
4462
+ additionalSpeedTiers: ["fast"],
4463
+ serviceTiers: [{
4464
+ id: "priority",
4465
+ name: "Fast",
4466
+ description: "1.5x speed, increased usage"
4467
+ }]
4187
4468
  }];
4188
4469
  }
4189
4470
  function mapAppServerThreadState(status, turns) {
@@ -4887,53 +5168,235 @@ async function git(cwd, args) {
4887
5168
  });
4888
5169
  return stdout.trimEnd();
4889
5170
  }
4890
- async function listWorkspaceFiles(workspacePath, query) {
5171
+ async function listWorkspaceFiles(workspacePath, query, directory) {
4891
5172
  const normalizedQuery = query.toLowerCase();
4892
5173
  const filePaths = await workspaceFilePaths(workspacePath);
4893
5174
  const entriesByPath = /* @__PURE__ */ new Map();
4894
5175
  for (const path of filePaths) {
4895
5176
  if (!path || path.startsWith("../") || path.includes("/.git/")) continue;
4896
- entriesByPath.set(path, {
4897
- 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,
4898
5202
  kind: "file",
4899
- name: path.split("/").pop() ?? path,
5203
+ name: childParts[0],
4900
5204
  path
4901
5205
  });
4902
- const parts = path.split("/");
4903
- for (let index = 1; index < parts.length; index += 1) {
4904
- const directoryPath = parts.slice(0, index).join("/");
5206
+ else {
5207
+ const directoryPath = [...directory ? directory.split("/") : [], childParts[0]].join("/");
4905
5208
  entriesByPath.set(directoryPath, {
4906
- directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
5209
+ directory,
4907
5210
  kind: "directory",
4908
- name: parts[index - 1],
5211
+ name: childParts[0],
4909
5212
  path: directoryPath
4910
5213
  });
4911
5214
  }
4912
5215
  }
4913
5216
  return [...entriesByPath.values()].filter((entry) => {
4914
5217
  if (!normalizedQuery) return true;
4915
- return entry.path.toLowerCase().includes(normalizedQuery);
5218
+ return entry.name.toLowerCase().includes(normalizedQuery) || entry.path.toLowerCase().includes(normalizedQuery);
4916
5219
  }).sort((left, right) => {
4917
5220
  const leftScore = workspaceFileScore(left.path, normalizedQuery);
4918
5221
  const rightScore = workspaceFileScore(right.path, normalizedQuery);
4919
5222
  if (leftScore !== rightScore) return leftScore - rightScore;
4920
5223
  if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
4921
5224
  return left.path.localeCompare(right.path);
4922
- }).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;
4923
5385
  }
4924
5386
  async function workspaceFilePaths(workspacePath) {
5387
+ const isIgnored = await workspaceIgnoreMatcher(workspacePath);
4925
5388
  try {
4926
5389
  return (await git(workspacePath, [
4927
5390
  "ls-files",
4928
5391
  "--cached",
4929
5392
  "--others",
4930
5393
  "--exclude-standard"
4931
- ])).split("\n").filter(Boolean);
5394
+ ])).split("\n").filter(Boolean).filter((path) => !isIgnored(path));
4932
5395
  } catch {
4933
- return recursiveWorkspaceFilePaths(workspacePath);
5396
+ return recursiveWorkspaceFilePaths(workspacePath, isIgnored);
4934
5397
  }
4935
5398
  }
4936
- async function recursiveWorkspaceFilePaths(rootPath) {
5399
+ async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
4937
5400
  const ignoredDirectories = new Set([
4938
5401
  ".git",
4939
5402
  ".expo",
@@ -4947,17 +5410,45 @@ async function recursiveWorkspaceFilePaths(rootPath) {
4947
5410
  for (const entry of entries) {
4948
5411
  if (entry.name.startsWith(".") && entry.name !== ".github") continue;
4949
5412
  const absolutePath = resolve(directory, entry.name);
5413
+ const relativePath = relative(rootPath, absolutePath).split("\\").join("/");
5414
+ if (isIgnored(relativePath)) continue;
4950
5415
  if (entry.isDirectory()) {
4951
5416
  if (ignoredDirectories.has(entry.name)) continue;
4952
5417
  await visit(absolutePath);
4953
5418
  continue;
4954
5419
  }
4955
- if (entry.isFile()) results.push(relative(rootPath, absolutePath).split("\\").join("/"));
5420
+ if (entry.isFile()) results.push(relativePath);
4956
5421
  }
4957
5422
  }
4958
5423
  await visit(rootPath);
4959
5424
  return results;
4960
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
+ }
4961
5452
  function workspaceFileScore(path, query) {
4962
5453
  if (!query) return path.split("/").length;
4963
5454
  const lowerPath = path.toLowerCase();