codexui-android 0.1.83 → 0.1.88

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-cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-UUZOL7SL.js";
2
+ import "./chunk-PUR7OUAG.js";
3
3
 
4
4
  // src/cli/index.ts
5
5
  import { createServer as createServer2 } from "http";
@@ -217,9 +217,10 @@ function parseApprovalPolicy(value) {
217
217
 
218
218
  // src/server/httpServer.ts
219
219
  import { fileURLToPath } from "url";
220
- import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute3, join as join7 } from "path";
220
+ import { basename as basename5, dirname as dirname3, extname as extname3, isAbsolute as isAbsolute3, join as join7 } from "path";
221
221
  import { existsSync as existsSync3 } from "fs";
222
222
  import { writeFile as writeFile5, stat as stat6 } from "fs/promises";
223
+ import { createReadStream as createReadStream2 } from "fs";
223
224
  import express from "express";
224
225
 
225
226
  // src/server/codexAppServerBridge.ts
@@ -480,6 +481,22 @@ async function validateSwitchedAccount(appServer) {
480
481
  quotaSnapshot: pickCodexRateLimitSnapshot(quotaPayload)
481
482
  };
482
483
  }
484
+ async function validateSwitchedAccountWithTimeout(appServer) {
485
+ let timeoutHandle = null;
486
+ try {
487
+ return await Promise.race([
488
+ validateSwitchedAccount(appServer),
489
+ new Promise((_, reject) => {
490
+ timeoutHandle = setTimeout(() => {
491
+ reject(new Error(`Account switch validation timed out after ${ACCOUNT_INSPECTION_TIMEOUT_MS}ms`));
492
+ }, ACCOUNT_INSPECTION_TIMEOUT_MS);
493
+ timeoutHandle.unref?.();
494
+ })
495
+ ]);
496
+ } finally {
497
+ if (timeoutHandle) clearTimeout(timeoutHandle);
498
+ }
499
+ }
483
500
  async function restoreActiveAuth(raw) {
484
501
  const path = getActiveAuthPath();
485
502
  if (raw === null) {
@@ -794,7 +811,7 @@ async function handleAccountRoutes(req, res, url, context) {
794
811
  const imported = await importAccountFromAuthPath(getActiveAuthPath());
795
812
  try {
796
813
  appServer.dispose();
797
- const inspection = await validateSwitchedAccount(appServer);
814
+ const inspection = await validateSwitchedAccountWithTimeout(appServer);
798
815
  const state = await readStoredAccountsState();
799
816
  const importedAccountId = imported.importedAccountId;
800
817
  const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
@@ -893,7 +910,7 @@ async function handleAccountRoutes(req, res, url, context) {
893
910
  await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
894
911
  try {
895
912
  appServer.dispose();
896
- const inspection = await validateSwitchedAccount(appServer);
913
+ const inspection = await validateSwitchedAccountWithTimeout(appServer);
897
914
  const nextEntry = {
898
915
  ...target,
899
916
  email: inspection.metadata.email ?? target.email,
@@ -1034,7 +1051,7 @@ async function handleAccountRoutes(req, res, url, context) {
1034
1051
  await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
1035
1052
  try {
1036
1053
  appServer.dispose();
1037
- const inspection = await validateSwitchedAccount(appServer);
1054
+ const inspection = await validateSwitchedAccountWithTimeout(appServer);
1038
1055
  const activatedReplacement = {
1039
1056
  ...replacement,
1040
1057
  email: inspection.metadata.email ?? replacement.email,
@@ -3762,6 +3779,78 @@ function spawnSyncCommand(command, args = [], options = {}) {
3762
3779
  var PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5e3;
3763
3780
  var THREAD_RESPONSE_TURN_LIMIT = 10;
3764
3781
  var THREAD_METHODS_WITH_TURNS = /* @__PURE__ */ new Set(["thread/read", "thread/resume", "thread/fork", "thread/rollback"]);
3782
+ var THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT = 100;
3783
+ var API_PERF_LOGGING_ENV_KEY = "CODEXUI_API_PERF_LOGGING";
3784
+ var API_PERF_MS_THRESHOLD_ENV_KEY = "CODEXUI_API_PERF_MS_THRESHOLD";
3785
+ var API_PERF_BODY_MB_THRESHOLD_ENV_KEY = "CODEXUI_API_PERF_BODY_MB_THRESHOLD";
3786
+ var DEFAULT_API_PERF_MS_THRESHOLD = 300;
3787
+ var DEFAULT_API_PERF_BODY_MB_THRESHOLD = 1;
3788
+ var MB_DIVISOR = 1024 * 1024;
3789
+ function readEnvValueFromFile(filePath, key) {
3790
+ try {
3791
+ const content = readFileSync(filePath, "utf8");
3792
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3793
+ const match = content.match(new RegExp(`^\\s*${escapedKey}\\s*=\\s*(.+)\\s*$`, "m"));
3794
+ if (!match) return null;
3795
+ const rawValue = match[1]?.trim() ?? "";
3796
+ if (!rawValue) return null;
3797
+ if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
3798
+ return rawValue.slice(1, -1).trim();
3799
+ }
3800
+ return rawValue;
3801
+ } catch {
3802
+ return null;
3803
+ }
3804
+ }
3805
+ function parseBooleanEnvFlag(value) {
3806
+ if (!value) return null;
3807
+ const normalized = value.trim().toLowerCase();
3808
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
3809
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
3810
+ return null;
3811
+ }
3812
+ function resolveApiPerfLoggingEnabled() {
3813
+ const explicitValue = parseBooleanEnvFlag(process.env[API_PERF_LOGGING_ENV_KEY]);
3814
+ if (explicitValue !== null) return explicitValue;
3815
+ const fromEnvLocal = parseBooleanEnvFlag(readEnvValueFromFile(".env.local", API_PERF_LOGGING_ENV_KEY));
3816
+ if (fromEnvLocal !== null) return fromEnvLocal;
3817
+ const fromEnv = parseBooleanEnvFlag(readEnvValueFromFile(".env", API_PERF_LOGGING_ENV_KEY));
3818
+ if (fromEnv !== null) return fromEnv;
3819
+ return false;
3820
+ }
3821
+ var API_PERF_LOGGING_ENABLED = resolveApiPerfLoggingEnabled();
3822
+ function parseNumberEnvFlag(value) {
3823
+ if (!value) return null;
3824
+ const parsed = Number.parseFloat(value.trim());
3825
+ if (!Number.isFinite(parsed)) return null;
3826
+ return parsed;
3827
+ }
3828
+ function resolveNumericEnvConfig(envKey, fallback) {
3829
+ const fromProcess = parseNumberEnvFlag(process.env[envKey]);
3830
+ if (fromProcess !== null) return fromProcess;
3831
+ const fromEnvLocal = parseNumberEnvFlag(readEnvValueFromFile(".env.local", envKey));
3832
+ if (fromEnvLocal !== null) return fromEnvLocal;
3833
+ const fromEnv = parseNumberEnvFlag(readEnvValueFromFile(".env", envKey));
3834
+ if (fromEnv !== null) return fromEnv;
3835
+ return fallback;
3836
+ }
3837
+ var API_PERF_MS_THRESHOLD = resolveNumericEnvConfig(API_PERF_MS_THRESHOLD_ENV_KEY, DEFAULT_API_PERF_MS_THRESHOLD);
3838
+ var API_PERF_BODY_MB_THRESHOLD = resolveNumericEnvConfig(API_PERF_BODY_MB_THRESHOLD_ENV_KEY, DEFAULT_API_PERF_BODY_MB_THRESHOLD);
3839
+ function getChunkByteLength(chunk, encoding) {
3840
+ if (typeof chunk === "string") {
3841
+ return Buffer.byteLength(chunk, encoding);
3842
+ }
3843
+ if (chunk instanceof Uint8Array) {
3844
+ return chunk.byteLength;
3845
+ }
3846
+ if (ArrayBuffer.isView(chunk)) {
3847
+ return chunk.byteLength;
3848
+ }
3849
+ if (chunk instanceof ArrayBuffer) {
3850
+ return chunk.byteLength;
3851
+ }
3852
+ return 0;
3853
+ }
3765
3854
  function asRecord5(value) {
3766
3855
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3767
3856
  }
@@ -5949,10 +6038,21 @@ async function loadAllThreadsForSearch(appServer) {
5949
6038
  }
5950
6039
  cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
5951
6040
  } while (cursor);
5952
- const docs = [];
6041
+ const docs = threads.map((thread) => {
6042
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
6043
+ return {
6044
+ id: thread.id,
6045
+ title: thread.title,
6046
+ preview: thread.preview,
6047
+ messageText: "",
6048
+ searchableText
6049
+ };
6050
+ });
6051
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
6052
+ const fullTextThreads = threads.slice(0, THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT);
5953
6053
  const concurrency = 4;
5954
- for (let offset = 0; offset < threads.length; offset += concurrency) {
5955
- const batch = threads.slice(offset, offset + concurrency);
6054
+ for (let offset = 0; offset < fullTextThreads.length; offset += concurrency) {
6055
+ const batch = fullTextThreads.slice(offset, offset + concurrency);
5956
6056
  const loaded = await Promise.all(batch.map(async (thread) => {
5957
6057
  try {
5958
6058
  const readResponse = await appServer.rpc("thread/read", {
@@ -5961,27 +6061,23 @@ async function loadAllThreadsForSearch(appServer) {
5961
6061
  });
5962
6062
  const messageText = extractThreadMessageText(readResponse);
5963
6063
  const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
5964
- return {
6064
+ return [thread.id, {
5965
6065
  id: thread.id,
5966
6066
  title: thread.title,
5967
6067
  preview: thread.preview,
5968
6068
  messageText,
5969
6069
  searchableText
5970
- };
6070
+ }];
5971
6071
  } catch {
5972
- const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
5973
- return {
5974
- id: thread.id,
5975
- title: thread.title,
5976
- preview: thread.preview,
5977
- messageText: "",
5978
- searchableText
5979
- };
6072
+ return null;
5980
6073
  }
5981
6074
  }));
5982
- docs.push(...loaded);
6075
+ for (const row of loaded) {
6076
+ if (!row) continue;
6077
+ docsById.set(row[0], row[1]);
6078
+ }
5983
6079
  }
5984
- return docs;
6080
+ return Array.from(docsById.values());
5985
6081
  });
5986
6082
  }
5987
6083
  async function buildThreadSearchIndex(appServer) {
@@ -6014,6 +6110,42 @@ function createCodexBridgeMiddleware() {
6014
6110
  }).catch(() => {
6015
6111
  });
6016
6112
  const middleware = async (req, res, next) => {
6113
+ const requestStartNs = process.hrtime.bigint();
6114
+ const rawUrl = req.url ?? "";
6115
+ const parsedRequestUrl = rawUrl ? new URL(rawUrl, "http://localhost") : null;
6116
+ const requestPath = parsedRequestUrl?.pathname ?? "";
6117
+ const requestMethod = req.method ?? "UNKNOWN";
6118
+ const rawContentLength = Array.isArray(req.headers["content-length"]) ? req.headers["content-length"][0] : req.headers["content-length"];
6119
+ const parsedContentLength = rawContentLength ? Number.parseInt(rawContentLength, 10) : NaN;
6120
+ let requestBodyBytes = Number.isFinite(parsedContentLength) && parsedContentLength >= 0 ? parsedContentLength : null;
6121
+ let responseBodyBytes = 0;
6122
+ let rpcMethod = null;
6123
+ const originalWrite = res.write.bind(res);
6124
+ const originalEnd = res.end.bind(res);
6125
+ res.write = ((chunk, encoding, cb) => {
6126
+ const resolvedEncoding = typeof encoding === "string" ? encoding : void 0;
6127
+ responseBodyBytes += getChunkByteLength(chunk, resolvedEncoding);
6128
+ return originalWrite(chunk, encoding, cb);
6129
+ });
6130
+ res.end = ((chunk, encoding, cb) => {
6131
+ const resolvedEncoding = typeof encoding === "string" ? encoding : void 0;
6132
+ responseBodyBytes += getChunkByteLength(chunk, resolvedEncoding);
6133
+ return originalEnd(chunk, encoding, cb);
6134
+ });
6135
+ let didLog = false;
6136
+ const logApiRequestDuration = () => {
6137
+ if (!API_PERF_LOGGING_ENABLED || didLog || !requestPath.startsWith("/codex-api/")) return;
6138
+ const durationMs = Number((process.hrtime.bigint() - requestStartNs) / 1000000n);
6139
+ const requestBytes = requestBodyBytes ?? 0;
6140
+ const bodyMbValue = (requestBytes + responseBodyBytes) / MB_DIVISOR;
6141
+ const shouldLog = durationMs > API_PERF_MS_THRESHOLD || bodyMbValue > API_PERF_BODY_MB_THRESHOLD;
6142
+ if (!shouldLog) return;
6143
+ didLog = true;
6144
+ const rpcPart = rpcMethod ? `, rpcMethod=${rpcMethod}` : "";
6145
+ console.info(`[codex-api-perf] ${requestMethod} ${requestPath} -> ${res.statusCode} (${durationMs}ms, bodyMB=${bodyMbValue.toFixed(4)}${rpcPart})`);
6146
+ };
6147
+ res.once("finish", logApiRequestDuration);
6148
+ res.once("close", logApiRequestDuration);
6017
6149
  try {
6018
6150
  if (!req.url) {
6019
6151
  next();
@@ -7149,76 +7281,92 @@ function createAuthSession(password) {
7149
7281
  }
7150
7282
 
7151
7283
  // src/server/localBrowseUi.ts
7152
- import { dirname as dirname2, extname as extname2, join as join6 } from "path";
7284
+ import { basename as basename4, dirname as dirname2, extname as extname2, join as join6 } from "path";
7153
7285
  import { open, readFile as readFile4, readdir as readdir3, stat as stat5 } from "fs/promises";
7154
- var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
7155
- ".txt",
7156
- ".md",
7157
- ".json",
7158
- ".js",
7159
- ".ts",
7160
- ".tsx",
7161
- ".jsx",
7162
- ".css",
7163
- ".scss",
7164
- ".html",
7165
- ".htm",
7166
- ".xml",
7167
- ".yml",
7168
- ".yaml",
7169
- ".log",
7170
- ".csv",
7171
- ".env",
7172
- ".py",
7173
- ".sh",
7174
- ".toml",
7175
- ".ini",
7176
- ".conf",
7177
- ".sql",
7178
- ".bat",
7179
- ".cmd",
7180
- ".ps1"
7286
+ var TEXT_LANGUAGE = {
7287
+ label: "text",
7288
+ aceMode: "text"
7289
+ };
7290
+ var BAZEL_LANGUAGE = {
7291
+ label: "bazel",
7292
+ aceMode: "python"
7293
+ };
7294
+ var FILE_LANGUAGE_RULES_BY_NAME = /* @__PURE__ */ new Map([
7295
+ ["build", BAZEL_LANGUAGE],
7296
+ ["build.bazel", BAZEL_LANGUAGE],
7297
+ ["workspace", BAZEL_LANGUAGE],
7298
+ ["workspace.bazel", BAZEL_LANGUAGE],
7299
+ ["module.bazel", BAZEL_LANGUAGE],
7300
+ [".bazelrc", BAZEL_LANGUAGE],
7301
+ [".env", TEXT_LANGUAGE],
7302
+ [".gitignore", TEXT_LANGUAGE],
7303
+ [".npmrc", TEXT_LANGUAGE]
7181
7304
  ]);
7182
- function languageForPath(pathValue) {
7183
- const extension = extname2(pathValue).toLowerCase();
7184
- switch (extension) {
7185
- case ".js":
7186
- return "javascript";
7187
- case ".ts":
7188
- return "typescript";
7189
- case ".jsx":
7190
- return "javascript";
7191
- case ".tsx":
7192
- return "typescript";
7193
- case ".py":
7194
- return "python";
7195
- case ".sh":
7196
- return "sh";
7197
- case ".css":
7198
- case ".scss":
7199
- return "css";
7200
- case ".html":
7201
- case ".htm":
7202
- return "html";
7203
- case ".json":
7204
- return "json";
7205
- case ".md":
7206
- return "markdown";
7207
- case ".yaml":
7208
- case ".yml":
7209
- return "yaml";
7210
- case ".xml":
7211
- return "xml";
7212
- case ".sql":
7213
- return "sql";
7214
- case ".toml":
7215
- return "ini";
7216
- case ".ini":
7217
- case ".conf":
7218
- return "ini";
7219
- default:
7220
- return "plaintext";
7305
+ var FILE_LANGUAGE_RULES_BY_EXTENSION = /* @__PURE__ */ new Map([
7306
+ [".txt", TEXT_LANGUAGE],
7307
+ [".log", TEXT_LANGUAGE],
7308
+ [".csv", TEXT_LANGUAGE],
7309
+ [".md", { label: "markdown", aceMode: "markdown" }],
7310
+ [".markdown", { label: "markdown", aceMode: "markdown" }],
7311
+ [".json", { label: "json", aceMode: "json" }],
7312
+ [".jsonc", { label: "json", aceMode: "json" }],
7313
+ [".yaml", { label: "yaml", aceMode: "yaml" }],
7314
+ [".yml", { label: "yaml", aceMode: "yaml" }],
7315
+ [".lua", { label: "lua", aceMode: "lua" }],
7316
+ [".rs", { label: "rust", aceMode: "rust" }],
7317
+ [".py", { label: "python", aceMode: "python" }],
7318
+ [".c", { label: "c", aceMode: "c_cpp" }],
7319
+ [".h", { label: "c/c++", aceMode: "c_cpp" }],
7320
+ [".cpp", { label: "c++", aceMode: "c_cpp" }],
7321
+ [".cc", { label: "c++", aceMode: "c_cpp" }],
7322
+ [".cxx", { label: "c++", aceMode: "c_cpp" }],
7323
+ [".hpp", { label: "c++", aceMode: "c_cpp" }],
7324
+ [".hh", { label: "c++", aceMode: "c_cpp" }],
7325
+ [".hxx", { label: "c++", aceMode: "c_cpp" }],
7326
+ [".toml", { label: "toml", aceMode: "toml" }],
7327
+ [".bzl", BAZEL_LANGUAGE],
7328
+ [".bazel", BAZEL_LANGUAGE],
7329
+ [".js", { label: "javascript", aceMode: "javascript" }],
7330
+ [".mjs", { label: "javascript", aceMode: "javascript" }],
7331
+ [".cjs", { label: "javascript", aceMode: "javascript" }],
7332
+ [".jsx", { label: "jsx", aceMode: "jsx" }],
7333
+ [".ts", { label: "typescript", aceMode: "typescript" }],
7334
+ [".mts", { label: "typescript", aceMode: "typescript" }],
7335
+ [".cts", { label: "typescript", aceMode: "typescript" }],
7336
+ [".tsx", { label: "tsx", aceMode: "tsx" }],
7337
+ [".vue", { label: "vue", aceMode: "vue" }],
7338
+ [".css", { label: "css", aceMode: "css" }],
7339
+ [".scss", { label: "scss", aceMode: "scss" }],
7340
+ [".html", { label: "html", aceMode: "html" }],
7341
+ [".htm", { label: "html", aceMode: "html" }],
7342
+ [".xml", { label: "xml", aceMode: "xml" }],
7343
+ [".sql", { label: "sql", aceMode: "sql" }],
7344
+ [".sh", { label: "shell", aceMode: "sh" }],
7345
+ [".bash", { label: "shell", aceMode: "sh" }],
7346
+ [".zsh", { label: "shell", aceMode: "sh" }],
7347
+ [".ini", { label: "ini", aceMode: "ini" }],
7348
+ [".conf", { label: "ini", aceMode: "ini" }],
7349
+ [".ps1", { label: "powershell", aceMode: "powershell" }],
7350
+ [".bat", TEXT_LANGUAGE],
7351
+ [".cmd", TEXT_LANGUAGE]
7352
+ ]);
7353
+ var MAX_INLINE_PREVIEW_BYTES = 1024 * 1024;
7354
+ var DARK_MODE_STORAGE_KEY = "codex-web-local.dark-mode.v1";
7355
+ var ACE_CDN_BASE = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2";
7356
+ function getLanguageConfigForPath(pathValue) {
7357
+ const fileName = basename4(pathValue).toLowerCase();
7358
+ if (fileName === ".env" || fileName.startsWith(".env.")) {
7359
+ return { language: TEXT_LANGUAGE, recognized: true };
7360
+ }
7361
+ const exactMatch = FILE_LANGUAGE_RULES_BY_NAME.get(fileName);
7362
+ if (exactMatch) {
7363
+ return { language: exactMatch, recognized: true };
7364
+ }
7365
+ const extensionMatch = FILE_LANGUAGE_RULES_BY_EXTENSION.get(extname2(fileName));
7366
+ if (extensionMatch) {
7367
+ return { language: extensionMatch, recognized: true };
7221
7368
  }
7369
+ return { language: TEXT_LANGUAGE, recognized: false };
7222
7370
  }
7223
7371
  function normalizeLocalPath(rawPath) {
7224
7372
  const trimmed = rawPath.trim();
@@ -7241,7 +7389,7 @@ function decodeBrowsePath(rawPath) {
7241
7389
  }
7242
7390
  }
7243
7391
  function isTextEditablePath(pathValue) {
7244
- return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
7392
+ return getLanguageConfigForPath(pathValue).recognized;
7245
7393
  }
7246
7394
  function isHiddenName(value) {
7247
7395
  return value.startsWith(".");
@@ -7275,25 +7423,157 @@ async function isTextEditableFile(localPath) {
7275
7423
  return false;
7276
7424
  }
7277
7425
  }
7426
+ async function getLocalTextFileMetadata(localPath) {
7427
+ const { language, recognized } = getLanguageConfigForPath(localPath);
7428
+ try {
7429
+ const fileStat = await stat5(localPath);
7430
+ if (!fileStat.isFile()) return null;
7431
+ if (recognized) {
7432
+ return {
7433
+ language,
7434
+ sizeBytes: fileStat.size
7435
+ };
7436
+ }
7437
+ const isText = await probeFileIsText(localPath);
7438
+ if (!isText) return null;
7439
+ return {
7440
+ language,
7441
+ sizeBytes: fileStat.size
7442
+ };
7443
+ } catch {
7444
+ return null;
7445
+ }
7446
+ }
7278
7447
  function escapeHtml(value) {
7279
7448
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
7280
7449
  }
7281
7450
  function normalizeNewProjectName(value) {
7282
7451
  return value.trim().replace(/[\\/]+/gu, "").trim();
7283
7452
  }
7284
- function toBrowseHref(pathValue, newProjectName = "") {
7285
- const normalizedName = normalizeNewProjectName(newProjectName);
7286
- const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : "";
7287
- return `/codex-local-browse${encodeURI(pathValue)}${query}`;
7453
+ function normalizeLineTarget(value) {
7454
+ return Number.isInteger(value) && Number(value) > 0 ? Number(value) : null;
7288
7455
  }
7289
- function toEditHref(pathValue, newProjectName = "") {
7290
- const normalizedName = normalizeNewProjectName(newProjectName);
7291
- const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : "";
7292
- return `/codex-local-edit${encodeURI(pathValue)}${query}`;
7456
+ function buildLocationQuery(options) {
7457
+ const query = new URLSearchParams();
7458
+ const normalizedName = normalizeNewProjectName(options?.newProjectName ?? "");
7459
+ const line = normalizeLineTarget(options?.line);
7460
+ const column = line ? normalizeLineTarget(options?.column) : null;
7461
+ if (normalizedName) query.set("newProjectName", normalizedName);
7462
+ if (line) query.set("line", String(line));
7463
+ if (column) query.set("column", String(column));
7464
+ const encoded = query.toString();
7465
+ return encoded ? `?${encoded}` : "";
7466
+ }
7467
+ function toBrowseHref(pathValue, options) {
7468
+ return `/codex-local-browse${encodeURI(pathValue)}${buildLocationQuery(options)}`;
7469
+ }
7470
+ function toEditHref(pathValue, options) {
7471
+ return `/codex-local-edit${encodeURI(pathValue)}${buildLocationQuery(options)}`;
7472
+ }
7473
+ function toRawFileHref(pathValue, options) {
7474
+ const query = new URLSearchParams({ path: pathValue });
7475
+ if (options?.download === true) {
7476
+ query.set("download", "1");
7477
+ }
7478
+ return `/codex-local-file?${query.toString()}`;
7293
7479
  }
7294
7480
  function escapeForInlineScriptString(value) {
7295
7481
  return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
7296
7482
  }
7483
+ function renderStandaloneThemeBootstrapScript() {
7484
+ return [
7485
+ "(function() {",
7486
+ ` const storageKey = ${JSON.stringify(DARK_MODE_STORAGE_KEY)};`,
7487
+ ' const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");',
7488
+ " function readStoredPreference() {",
7489
+ " try {",
7490
+ " const value = window.localStorage.getItem(storageKey);",
7491
+ ' return value === "light" || value === "dark" ? value : "system";',
7492
+ " } catch {",
7493
+ ' return "system";',
7494
+ " }",
7495
+ " }",
7496
+ " function resolveTheme(preference) {",
7497
+ ' if (preference === "light" || preference === "dark") return preference;',
7498
+ ' return mediaQuery.matches ? "dark" : "light";',
7499
+ " }",
7500
+ " function applyTheme() {",
7501
+ " const preference = readStoredPreference();",
7502
+ " const resolvedTheme = resolveTheme(preference);",
7503
+ " document.documentElement.dataset.theme = resolvedTheme;",
7504
+ " document.documentElement.style.colorScheme = resolvedTheme;",
7505
+ " window.__codexLocalBrowseTheme = { preference, resolvedTheme };",
7506
+ ' window.dispatchEvent(new CustomEvent("codex-local-browse-themechange", { detail: window.__codexLocalBrowseTheme }));',
7507
+ " }",
7508
+ " applyTheme();",
7509
+ " window.__codexApplyLocalBrowseTheme = applyTheme;",
7510
+ ' window.addEventListener("storage", function(event) {',
7511
+ " if (event.key !== storageKey) return;",
7512
+ " applyTheme();",
7513
+ " });",
7514
+ ' mediaQuery.addEventListener("change", function() {',
7515
+ ' if (readStoredPreference() !== "system") return;',
7516
+ " applyTheme();",
7517
+ " });",
7518
+ "})();"
7519
+ ].join("\n");
7520
+ }
7521
+ function renderStandaloneThemeCss() {
7522
+ return `
7523
+ :root {
7524
+ color-scheme: light;
7525
+ --lb-bg: #f3f6fb;
7526
+ --lb-surface: #ffffff;
7527
+ --lb-surface-muted: #eef3fb;
7528
+ --lb-surface-strong: #e7edf8;
7529
+ --lb-toolbar-bg: rgba(243, 246, 251, 0.96);
7530
+ --lb-border: #cfd9ea;
7531
+ --lb-border-strong: #b6c4db;
7532
+ --lb-text: #172033;
7533
+ --lb-text-muted: #52607a;
7534
+ --lb-link: #2457b8;
7535
+ --lb-button-text: #172033;
7536
+ --lb-accent-bg: #dfeafe;
7537
+ --lb-accent-border: #9db8ef;
7538
+ --lb-primary-bg: linear-gradient(135deg, #2f67d8 0%, #4b88ff 100%);
7539
+ --lb-primary-border: #2f67d8;
7540
+ --lb-primary-text: #f8fbff;
7541
+ --lb-selection: rgba(77, 124, 255, 0.18);
7542
+ --lb-selection-strong: rgba(77, 124, 255, 0.24);
7543
+ --lb-target-line: rgba(77, 124, 255, 0.14);
7544
+ --lb-target-stripe: #4d7cff;
7545
+ --lb-warning-text: #8a5a00;
7546
+ --lb-warning-bg: rgba(194, 136, 18, 0.12);
7547
+ --lb-shadow: 0 16px 40px rgba(31, 49, 82, 0.12);
7548
+ }
7549
+ :root[data-theme='dark'] {
7550
+ color-scheme: dark;
7551
+ --lb-bg: #09111f;
7552
+ --lb-surface: #0d182b;
7553
+ --lb-surface-muted: #101f3a;
7554
+ --lb-surface-strong: #13233d;
7555
+ --lb-toolbar-bg: rgba(9, 17, 31, 0.96);
7556
+ --lb-border: #20324d;
7557
+ --lb-border-strong: #36557a;
7558
+ --lb-text: #dbe6ff;
7559
+ --lb-text-muted: #8fb8ec;
7560
+ --lb-link: #8cc2ff;
7561
+ --lb-button-text: #dbe6ff;
7562
+ --lb-accent-bg: #162643;
7563
+ --lb-accent-border: #36557a;
7564
+ --lb-primary-bg: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
7565
+ --lb-primary-border: #4f8de0;
7566
+ --lb-primary-text: #eef6ff;
7567
+ --lb-selection: rgba(140, 194, 255, 0.16);
7568
+ --lb-selection-strong: rgba(140, 194, 255, 0.3);
7569
+ --lb-target-line: rgba(140, 194, 255, 0.16);
7570
+ --lb-target-stripe: #8cc2ff;
7571
+ --lb-warning-text: #f0cf78;
7572
+ --lb-warning-bg: rgba(240, 207, 120, 0.08);
7573
+ --lb-shadow: 0 20px 40px rgba(0, 0, 0, 0.24);
7574
+ }
7575
+ `;
7576
+ }
7297
7577
  async function getDirectoryItems(localPath) {
7298
7578
  const entries = await readdir3(localPath, { withFileTypes: true });
7299
7579
  const withMeta = await Promise.all(entries.map(async (entry) => {
@@ -7343,6 +7623,64 @@ function actionButtonsHtml(localPath, newProjectName) {
7343
7623
  const openButton = `<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}" data-label="" data-status="${escapeHtml(openFolderStatusText(normalizedName))}" data-error="${escapeHtml(failureStatusText(normalizedName))}">Open folder in Codex</button>`;
7344
7624
  return `${createButton}${openButton}`;
7345
7625
  }
7626
+ function renderTextPreviewToolbar(localPath, options) {
7627
+ const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
7628
+ const line = normalizeLineTarget(options?.line);
7629
+ const column = line ? normalizeLineTarget(options?.column) : null;
7630
+ const backHref = toBrowseHref(dirname2(localPath), { newProjectName });
7631
+ const rawHref = toRawFileHref(localPath);
7632
+ const downloadHref = toRawFileHref(localPath, { download: true });
7633
+ const lineLocation = line ? { newProjectName, line, column } : { newProjectName };
7634
+ return [
7635
+ `<a href="${escapeHtml(backHref)}">Back</a>`,
7636
+ `<a href="${escapeHtml(rawHref)}" target="_blank" rel="noopener noreferrer">Raw</a>`,
7637
+ `<a href="${escapeHtml(downloadHref)}">Download</a>`,
7638
+ `<a href="${escapeHtml(toEditHref(localPath, lineLocation))}">Edit</a>`
7639
+ ].filter(Boolean).join("");
7640
+ }
7641
+ function formatLineTargetLabel(line, column) {
7642
+ if (!line) return "";
7643
+ if (column) return `line ${String(line)}:${String(column)}`;
7644
+ return `line ${String(line)}`;
7645
+ }
7646
+ function renderPlainPreviewContent(content, targetLine) {
7647
+ if (!targetLine || targetLine < 1) return escapeHtml(content);
7648
+ const trailingNewline = content.endsWith("\n");
7649
+ const lines = content.split("\n");
7650
+ if (trailingNewline) lines.pop();
7651
+ if (targetLine > lines.length) return escapeHtml(content);
7652
+ return lines.map((line, index) => {
7653
+ const escapedLine = escapeHtml(line || " ");
7654
+ if (index + 1 === targetLine) {
7655
+ return `<span id="previewTargetLine" class="preview-target-line">${escapedLine}</span>`;
7656
+ }
7657
+ return escapedLine;
7658
+ }).join("\n") + (trailingNewline ? "\n" : "");
7659
+ }
7660
+ function formatPreviewSize(sizeBytes) {
7661
+ if (sizeBytes >= 1024 * 1024) return `${(sizeBytes / (1024 * 1024)).toFixed(1).replace(/\.0$/u, "")} MiB`;
7662
+ if (sizeBytes >= 1024) return `${Math.round(sizeBytes / 1024)} KiB`;
7663
+ return `${sizeBytes} B`;
7664
+ }
7665
+ function renderAceLineTargetScript() {
7666
+ return [
7667
+ "function selectTargetLine(editorInstance, lineNumber, columnNumber) {",
7668
+ " if (!lineNumber) return;",
7669
+ " const zeroBasedRow = lineNumber - 1;",
7670
+ " const zeroBasedColumn = Math.max(0, (columnNumber ?? 1) - 1);",
7671
+ " editorInstance.gotoLine(lineNumber, zeroBasedColumn, true);",
7672
+ " editorInstance.scrollToLine(lineNumber, true, true, function() {});",
7673
+ " editorInstance.selection.moveCursorToPosition({ row: zeroBasedRow, column: zeroBasedColumn });",
7674
+ " editorInstance.selection.selectLine();",
7675
+ "}",
7676
+ "function currentAceThemeName() {",
7677
+ ' return document.documentElement.dataset.theme === "light" ? "tomorrow" : "tomorrow_night";',
7678
+ "}",
7679
+ "function applyAceTheme(editorInstance) {",
7680
+ ' editorInstance.setTheme("ace/theme/" + currentAceThemeName());',
7681
+ "}"
7682
+ ].join("\n");
7683
+ }
7346
7684
  async function getLocalDirectoryListing(localPath, options = {}) {
7347
7685
  const entries = await readdir3(localPath, { withFileTypes: true });
7348
7686
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
@@ -7366,10 +7704,10 @@ async function createDirectoryListingHtml(localPath, options) {
7366
7704
  const parentPath = dirname2(localPath);
7367
7705
  const rows = items.map((item) => {
7368
7706
  const suffix = item.isDirectory ? "/" : "";
7369
- const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path, newProjectName))}" title="Edit">\u270F\uFE0F</a>` : "";
7370
- return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path, newProjectName))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
7707
+ const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path, { newProjectName }))}" title="Edit">\u270F\uFE0F</a>` : "";
7708
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path, { newProjectName }))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
7371
7709
  }).join("\n");
7372
- const parentLink = localPath !== parentPath ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath, newProjectName))}">..</a>` : "";
7710
+ const parentLink = localPath !== parentPath ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath, { newProjectName }))}">..</a>` : "";
7373
7711
  const pickerSummary = newProjectName ? `<p class="picker-summary">Browse to the parent folder where you want to create <strong>${escapeHtml(newProjectName)}</strong>, or open the current folder directly.</p>` : "";
7374
7712
  const actionButtons = actionButtonsHtml(localPath, newProjectName);
7375
7713
  return `<!doctype html>
@@ -7378,35 +7716,37 @@ async function createDirectoryListingHtml(localPath, options) {
7378
7716
  <meta charset="utf-8" />
7379
7717
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7380
7718
  <title>Index of ${escapeHtml(localPath)}</title>
7719
+ <script>${renderStandaloneThemeBootstrapScript()}</script>
7381
7720
  <style>
7382
- body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
7383
- a { color: #8cc2ff; text-decoration: none; }
7721
+ ${renderStandaloneThemeCss()}
7722
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: var(--lb-bg); color: var(--lb-text); }
7723
+ a { color: var(--lb-link); text-decoration: none; }
7384
7724
  a:hover { text-decoration: underline; }
7385
7725
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
7386
7726
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
7387
- .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
7727
+ .file-link { display: block; padding: 10px 12px; border: 1px solid var(--lb-border); border-radius: 10px; background: var(--lb-surface); color: var(--lb-text); overflow-wrap: anywhere; box-shadow: var(--lb-shadow); }
7388
7728
  .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
7389
- .header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
7729
+ .header-parent-link { color: var(--lb-link); font-size: 14px; padding: 8px 10px; border: 1px solid var(--lb-border); border-radius: 10px; background: var(--lb-surface-muted); }
7390
7730
  .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
7391
7731
  .header-open-btn {
7392
7732
  height: 42px;
7393
7733
  padding: 0 14px;
7394
- border: 1px solid #4f8de0;
7734
+ border: 1px solid var(--lb-primary-border);
7395
7735
  border-radius: 10px;
7396
- background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
7397
- color: #eef6ff;
7736
+ background: var(--lb-primary-bg);
7737
+ color: var(--lb-primary-text);
7398
7738
  font-weight: 700;
7399
7739
  letter-spacing: 0.01em;
7400
7740
  cursor: pointer;
7401
- box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
7741
+ box-shadow: var(--lb-shadow);
7402
7742
  }
7403
7743
  .header-open-btn:hover { filter: brightness(1.08); }
7404
7744
  .header-open-btn:disabled { opacity: 0.6; cursor: default; }
7405
- .picker-summary { margin: 10px 0 0; color: #b8d5ff; max-width: 60rem; line-height: 1.45; }
7745
+ .picker-summary { margin: 10px 0 0; color: var(--lb-text-muted); max-width: 60rem; line-height: 1.45; }
7406
7746
  .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
7407
- .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
7747
+ .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid var(--lb-accent-border); border-radius: 10px; background: var(--lb-accent-bg); color: var(--lb-button-text); text-decoration: none; cursor: pointer; }
7408
7748
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
7409
- .status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
7749
+ .status { margin: 10px 0 0; color: var(--lb-text-muted); min-height: 1.25em; }
7410
7750
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
7411
7751
  @media (max-width: 640px) {
7412
7752
  body { margin: 12px; }
@@ -7467,10 +7807,144 @@ async function createDirectoryListingHtml(localPath, options) {
7467
7807
  </body>
7468
7808
  </html>`;
7469
7809
  }
7470
- async function createTextEditorHtml(localPath) {
7810
+ async function createTextPreviewHtml(localPath, options) {
7811
+ const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
7812
+ const line = normalizeLineTarget(options?.line);
7813
+ const column = line ? normalizeLineTarget(options?.column) : null;
7814
+ const metadata = await getLocalTextFileMetadata(localPath);
7815
+ if (!metadata) {
7816
+ throw new Error("Only text-like files can be previewed inline.");
7817
+ }
7818
+ const toolbar = renderTextPreviewToolbar(localPath, { newProjectName, line, column });
7819
+ const lineTargetLabel = formatLineTargetLabel(line, column);
7820
+ const previewMeta = [localPath, metadata.language.label, formatPreviewSize(metadata.sizeBytes), lineTargetLabel].filter(Boolean).join(" \xB7 ");
7821
+ const previewUnavailable = metadata.sizeBytes > MAX_INLINE_PREVIEW_BYTES;
7822
+ const previewContent = previewUnavailable ? "" : await readFile4(localPath, "utf8");
7823
+ const previewPlainHtml = renderPlainPreviewContent(previewContent, line);
7824
+ const safePreviewLiteral = escapeForInlineScriptString(previewContent);
7825
+ return `<!doctype html>
7826
+ <html lang="en">
7827
+ <head>
7828
+ <meta charset="utf-8" />
7829
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7830
+ <title>${escapeHtml(basename4(localPath))}</title>
7831
+ <script>${renderStandaloneThemeBootstrapScript()}</script>
7832
+ <style>
7833
+ ${renderStandaloneThemeCss()}
7834
+ html, body { margin: 0; width: 100%; min-height: 100%; }
7835
+ body { min-height: 100vh; font-family: ui-monospace, Menlo, Monaco, monospace; background: var(--lb-bg); color: var(--lb-text); display: flex; flex-direction: column; }
7836
+ a { color: inherit; text-decoration: none; }
7837
+ .toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 10px; padding: 12px 16px; background: var(--lb-toolbar-bg); backdrop-filter: blur(8px); border-bottom: 1px solid var(--lb-border); }
7838
+ .toolbar-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
7839
+ .toolbar-row a { display: inline-flex; align-items: center; justify-content: center; min-height: 38px; padding: 0 12px; border: 1px solid var(--lb-accent-border); border-radius: 10px; background: var(--lb-accent-bg); color: var(--lb-button-text); }
7840
+ .toolbar-row a:hover { filter: brightness(1.08); }
7841
+ .meta { color: var(--lb-text-muted); font-size: 12px; overflow-wrap: anywhere; line-height: 1.5; }
7842
+ .preview-shell { flex: 1 1 auto; min-height: 0; display: flex; padding: 18px 16px 24px; }
7843
+ .preview-card { flex: 1 1 auto; min-height: 0; max-width: 100%; border: 1px solid var(--lb-border); border-radius: 14px; background: var(--lb-surface); overflow: hidden; box-shadow: var(--lb-shadow); display: flex; flex-direction: column; }
7844
+ .preview-notice { margin: 0; padding: 12px 16px; border-bottom: 1px solid var(--lb-border); color: var(--lb-warning-text); background: var(--lb-warning-bg); }
7845
+ .preview-unavailable { padding: 26px 20px; }
7846
+ .preview-unavailable-title { margin: 0 0 8px; font-size: 15px; font-weight: 700; }
7847
+ .preview-unavailable-text { margin: 0; color: var(--lb-text-muted); line-height: 1.6; }
7848
+ .preview-plain {
7849
+ flex: 1 1 auto;
7850
+ box-sizing: border-box;
7851
+ margin: 0;
7852
+ padding: 18px 20px 24px;
7853
+ min-height: 0;
7854
+ overflow: auto;
7855
+ white-space: pre-wrap;
7856
+ overflow-wrap: anywhere;
7857
+ tab-size: 2;
7858
+ line-height: 1.55;
7859
+ color: var(--lb-text);
7860
+ background: var(--lb-surface);
7861
+ }
7862
+ .preview-target-line {
7863
+ display: inline-block;
7864
+ min-width: 100%;
7865
+ margin: 0 -20px;
7866
+ padding: 0 20px;
7867
+ background: var(--lb-target-line);
7868
+ box-shadow: inset 3px 0 0 var(--lb-target-stripe);
7869
+ }
7870
+ #previewEditor { flex: 1 1 auto; width: 100%; min-height: 0; }
7871
+ .ace_editor { width: 100% !important; height: 100% !important; }
7872
+ .ace_marker-layer .ace_active-line { background: transparent !important; }
7873
+ .ace_marker-layer .ace_selection { background: var(--lb-selection) !important; }
7874
+ @media (max-width: 640px) {
7875
+ .toolbar { padding: 12px; }
7876
+ .preview-shell { padding: 12px; }
7877
+ }
7878
+ </style>
7879
+ </head>
7880
+ <body>
7881
+ <div class="toolbar">
7882
+ <div class="toolbar-row">${toolbar}</div>
7883
+ <div class="meta">${escapeHtml(previewMeta)}</div>
7884
+ </div>
7885
+ <main class="preview-shell">
7886
+ <section class="preview-card">
7887
+ ${previewUnavailable ? `<div class="preview-unavailable">
7888
+ <p class="preview-unavailable-title">Inline preview disabled</p>
7889
+ <p class="preview-unavailable-text">This file is larger than ${String(Math.floor(MAX_INLINE_PREVIEW_BYTES / 1024))} KiB. Use <strong>Raw</strong> or <strong>Download</strong> instead.</p>
7890
+ </div>` : `<pre id="previewFallback" class="preview-plain">${previewPlainHtml}</pre>
7891
+ <div id="previewEditor" hidden></div>`}
7892
+ </section>
7893
+ </main>
7894
+ ${previewUnavailable ? "" : `<script src="${ACE_CDN_BASE}/ace.min.js"></script>
7895
+ <script>
7896
+ const targetLineNumber = ${line ?? "null"};
7897
+ const targetColumnNumber = ${column ?? "null"};
7898
+ const previewFallback = document.getElementById('previewFallback');
7899
+ const previewTargetLine = document.getElementById('previewTargetLine');
7900
+ const previewEditor = document.getElementById('previewEditor');
7901
+ ${renderAceLineTargetScript()}
7902
+ if (previewTargetLine) {
7903
+ previewTargetLine.scrollIntoView({ block: 'center' });
7904
+ }
7905
+ if (window.ace && previewEditor) {
7906
+ previewEditor.hidden = false;
7907
+ if (previewFallback) previewFallback.hidden = true;
7908
+ ace.config.set('basePath', '${ACE_CDN_BASE}/');
7909
+ const editor = ace.edit('previewEditor');
7910
+ applyAceTheme(editor);
7911
+ editor.session.setMode('ace/mode/${escapeHtml(metadata.language.aceMode)}');
7912
+ editor.session.setUseWorker(false);
7913
+ editor.setValue(${safePreviewLiteral}, -1);
7914
+ editor.setOptions({
7915
+ readOnly: true,
7916
+ highlightActiveLine: !!targetLineNumber,
7917
+ highlightGutterLine: false,
7918
+ showPrintMargin: false,
7919
+ fontSize: '13px',
7920
+ wrap: true,
7921
+ behavioursEnabled: false,
7922
+ displayIndentGuides: true,
7923
+ });
7924
+ editor.renderer.setShowGutter(true);
7925
+ editor.renderer.$cursorLayer.element.style.display = 'none';
7926
+ if (targetLineNumber) {
7927
+ selectTargetLine(editor, targetLineNumber, targetColumnNumber);
7928
+ }
7929
+ window.addEventListener('codex-local-browse-themechange', function() {
7930
+ applyAceTheme(editor);
7931
+ });
7932
+ editor.resize();
7933
+ }
7934
+ </script>`}
7935
+ </body>
7936
+ </html>`;
7937
+ }
7938
+ async function createTextEditorHtml(localPath, options) {
7939
+ const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
7940
+ const line = normalizeLineTarget(options?.line);
7941
+ const column = line ? normalizeLineTarget(options?.column) : null;
7942
+ const metadata = await getLocalTextFileMetadata(localPath);
7943
+ if (!metadata) {
7944
+ throw new Error("Only text-like files are editable.");
7945
+ }
7471
7946
  const content = await readFile4(localPath, "utf8");
7472
7947
  const parentPath = dirname2(localPath);
7473
- const language = languageForPath(localPath);
7474
7948
  const safeContentLiteral = escapeForInlineScriptString(content);
7475
7949
  return `<!doctype html>
7476
7950
  <html lang="en">
@@ -7478,49 +7952,64 @@ async function createTextEditorHtml(localPath) {
7478
7952
  <meta charset="utf-8" />
7479
7953
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7480
7954
  <title>Edit ${escapeHtml(localPath)}</title>
7955
+ <script>${renderStandaloneThemeBootstrapScript()}</script>
7481
7956
  <style>
7957
+ ${renderStandaloneThemeCss()}
7482
7958
  html, body { width: 100%; height: 100%; margin: 0; }
7483
- body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
7484
- .toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: #0b1020; border-bottom: 1px solid #243a5a; }
7959
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; background: var(--lb-bg); color: var(--lb-text); display: flex; flex-direction: column; overflow: hidden; }
7960
+ .toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: var(--lb-bg); border-bottom: 1px solid var(--lb-border); }
7485
7961
  .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
7486
- button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
7962
+ button, a { background: var(--lb-accent-bg); color: var(--lb-button-text); border: 1px solid var(--lb-accent-border); padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
7487
7963
  button:hover, a:hover { filter: brightness(1.08); }
7488
7964
  #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
7489
- #status { margin-left: 8px; color: #8cc2ff; }
7490
- .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
7491
- .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
7492
- .ace_marker-layer .ace_active-line { background: #10213c !important; }
7493
- .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
7494
- .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
7965
+ #status { margin-left: 8px; color: var(--lb-text-muted); }
7966
+ .ace_editor { width: 100% !important; height: 100% !important; }
7967
+ .ace_marker-layer .ace_active-line { background: var(--lb-target-line) !important; }
7968
+ .ace_marker-layer .ace_selection { background: var(--lb-selection-strong) !important; }
7969
+ .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; color: var(--lb-text-muted); }
7495
7970
  </style>
7496
7971
  </head>
7497
7972
  <body>
7498
7973
  <div class="toolbar">
7499
7974
  <div class="row">
7500
- <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
7975
+ <a href="${escapeHtml(toBrowseHref(parentPath, { newProjectName }))}">Back</a>
7976
+ <a href="${escapeHtml(toBrowseHref(localPath, { newProjectName, line, column }))}">Preview</a>
7977
+ <a href="${escapeHtml(toRawFileHref(localPath))}" target="_blank" rel="noopener noreferrer">Raw</a>
7978
+ <a href="${escapeHtml(toRawFileHref(localPath, { download: true }))}">Download</a>
7501
7979
  <button id="saveBtn" type="button">Save</button>
7502
7980
  <span id="status"></span>
7503
7981
  </div>
7504
- <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
7982
+ <div class="meta">${escapeHtml([localPath, metadata.language.label, formatLineTargetLabel(line, column)].filter(Boolean).join(" \xB7 "))}</div>
7505
7983
  </div>
7506
7984
  <div id="editor"></div>
7507
- <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
7508
- <script>
7509
- const saveBtn = document.getElementById('saveBtn');
7510
- const status = document.getElementById('status');
7511
- const editor = ace.edit('editor');
7512
- editor.setTheme('ace/theme/tomorrow_night');
7513
- editor.session.setMode('ace/mode/${escapeHtml(language)}');
7514
- editor.setValue(${safeContentLiteral}, -1);
7985
+ <script src="${ACE_CDN_BASE}/ace.min.js"></script>
7986
+ <script>
7987
+ const targetLineNumber = ${line ?? "null"};
7988
+ const targetColumnNumber = ${column ?? "null"};
7989
+ const saveBtn = document.getElementById('saveBtn');
7990
+ const status = document.getElementById('status');
7991
+ ${renderAceLineTargetScript()}
7992
+ ace.config.set('basePath', '${ACE_CDN_BASE}/');
7993
+ const editor = ace.edit('editor');
7994
+ applyAceTheme(editor);
7995
+ editor.session.setMode('ace/mode/${escapeHtml(metadata.language.aceMode)}');
7996
+ editor.session.setUseWorker(false);
7997
+ editor.setValue(${safeContentLiteral}, -1);
7515
7998
  editor.setOptions({
7516
7999
  fontSize: '13px',
7517
8000
  wrap: true,
7518
8001
  showPrintMargin: false,
7519
8002
  useSoftTabs: true,
7520
8003
  tabSize: 2,
7521
- behavioursEnabled: true,
7522
- });
7523
- editor.resize();
8004
+ behavioursEnabled: true,
8005
+ });
8006
+ if (targetLineNumber) {
8007
+ selectTargetLine(editor, targetLineNumber, targetColumnNumber);
8008
+ }
8009
+ window.addEventListener('codex-local-browse-themechange', function() {
8010
+ applyAceTheme(editor);
8011
+ });
8012
+ editor.resize();
7524
8013
 
7525
8014
  saveBtn.addEventListener('click', async () => {
7526
8015
  status.textContent = 'Saving...';
@@ -7586,6 +8075,34 @@ function readWildcardPathParam(value) {
7586
8075
  if (Array.isArray(value)) return value.join("/");
7587
8076
  return "";
7588
8077
  }
8078
+ function readBooleanQueryFlag(value) {
8079
+ return typeof value === "string" && ["1", "true", "yes", "on"].includes(value.toLowerCase());
8080
+ }
8081
+ function readPositiveIntegerQueryParam(value) {
8082
+ if (typeof value !== "string") return null;
8083
+ const parsed = Number(value);
8084
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
8085
+ }
8086
+ function localFileErrorResponse(error) {
8087
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code ?? "") : "";
8088
+ const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : NaN;
8089
+ if (code === "ENOENT" || statusCode === 404) {
8090
+ return { status: 404, body: { error: "File not found." } };
8091
+ }
8092
+ if (code === "EACCES" || code === "EPERM" || statusCode === 403) {
8093
+ return { status: 403, body: { error: "Access denied." } };
8094
+ }
8095
+ return { status: 500, body: { error: "Failed to read file." } };
8096
+ }
8097
+ function sendLocalFileJsonError(res, error) {
8098
+ if (res.headersSent) return;
8099
+ const response = localFileErrorResponse(error);
8100
+ res.status(response.status).json(response.body);
8101
+ }
8102
+ function attachmentContentDisposition(pathValue) {
8103
+ const fileName = basename5(pathValue).replace(/["\\]/gu, "_");
8104
+ return `attachment; filename="${fileName}"`;
8105
+ }
7589
8106
  function createServer(options = {}) {
7590
8107
  const app = express();
7591
8108
  const bridge = createCodexBridgeMiddleware();
@@ -7613,19 +8130,87 @@ function createServer(options = {}) {
7613
8130
  if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
7614
8131
  });
7615
8132
  });
7616
- app.get("/codex-local-file", (req, res) => {
8133
+ app.post("/codex-api/local-paths/probe", express.json({ limit: "64kb" }), async (req, res) => {
8134
+ const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
8135
+ const rawPaths = Array.isArray(body.paths) ? body.paths : [];
8136
+ const normalizedPaths = [];
8137
+ const seen = /* @__PURE__ */ new Set();
8138
+ for (const candidate of rawPaths.slice(0, 200)) {
8139
+ if (typeof candidate !== "string") continue;
8140
+ const normalized = normalizeLocalPath(candidate);
8141
+ if (!normalized || seen.has(normalized)) continue;
8142
+ seen.add(normalized);
8143
+ normalizedPaths.push(normalized);
8144
+ }
8145
+ const entries = await Promise.all(normalizedPaths.map(async (pathValue) => {
8146
+ if (!isAbsolute3(pathValue)) {
8147
+ return {
8148
+ path: pathValue,
8149
+ exists: false,
8150
+ isDirectory: false
8151
+ };
8152
+ }
8153
+ try {
8154
+ const fileStat = await stat6(pathValue);
8155
+ return {
8156
+ path: pathValue,
8157
+ exists: true,
8158
+ isDirectory: fileStat.isDirectory()
8159
+ };
8160
+ } catch {
8161
+ return {
8162
+ path: pathValue,
8163
+ exists: false,
8164
+ isDirectory: false
8165
+ };
8166
+ }
8167
+ }));
8168
+ res.status(200).json({ data: { entries } });
8169
+ });
8170
+ app.get("/codex-local-file", async (req, res) => {
7617
8171
  const rawPath = typeof req.query.path === "string" ? req.query.path : "";
7618
8172
  const localPath = normalizeLocalPath(rawPath);
8173
+ const wantsDownload = readBooleanQueryFlag(req.query.download);
7619
8174
  if (!localPath || !isAbsolute3(localPath)) {
7620
8175
  res.status(400).json({ error: "Expected absolute local file path." });
7621
8176
  return;
7622
8177
  }
7623
- res.setHeader("Cache-Control", "private, no-store");
7624
- res.setHeader("Content-Disposition", "inline");
7625
- res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
7626
- if (!error) return;
7627
- if (!res.headersSent) res.status(404).json({ error: "File not found." });
7628
- });
8178
+ try {
8179
+ const fileStat = await stat6(localPath);
8180
+ if (!fileStat.isFile()) {
8181
+ res.status(400).json({ error: "Expected file path." });
8182
+ return;
8183
+ }
8184
+ const textMetadata = await getLocalTextFileMetadata(localPath);
8185
+ res.setHeader("Cache-Control", "private, no-store");
8186
+ if (wantsDownload) {
8187
+ res.setHeader("Content-Disposition", attachmentContentDisposition(localPath));
8188
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
8189
+ if (!error) return;
8190
+ sendLocalFileJsonError(res, error);
8191
+ });
8192
+ return;
8193
+ }
8194
+ if (textMetadata) {
8195
+ const stream = createReadStream2(localPath, { encoding: "utf8" });
8196
+ stream.on("open", () => {
8197
+ res.status(200).type("text/plain; charset=utf-8");
8198
+ });
8199
+ stream.on("error", (error) => {
8200
+ stream.destroy();
8201
+ sendLocalFileJsonError(res, error);
8202
+ });
8203
+ stream.pipe(res);
8204
+ return;
8205
+ }
8206
+ res.setHeader("Content-Disposition", "inline");
8207
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
8208
+ if (!error) return;
8209
+ sendLocalFileJsonError(res, error);
8210
+ });
8211
+ } catch (error) {
8212
+ sendLocalFileJsonError(res, error);
8213
+ }
7629
8214
  });
7630
8215
  app.get("/codex-local-directories", async (req, res) => {
7631
8216
  const rawPath = typeof req.query.path === "string" ? req.query.path : "";
@@ -7651,6 +8236,8 @@ function createServer(options = {}) {
7651
8236
  const rawPath = readWildcardPathParam(req.params.path);
7652
8237
  const localPath = decodeBrowsePath(`/${rawPath}`);
7653
8238
  const newProjectName = typeof req.query.newProjectName === "string" ? req.query.newProjectName : "";
8239
+ const line = readPositiveIntegerQueryParam(req.query.line);
8240
+ const column = line ? readPositiveIntegerQueryParam(req.query.column) : null;
7654
8241
  if (!localPath || !isAbsolute3(localPath)) {
7655
8242
  res.status(400).json({ error: "Expected absolute local file path." });
7656
8243
  return;
@@ -7663,6 +8250,13 @@ function createServer(options = {}) {
7663
8250
  res.status(200).type("text/html; charset=utf-8").send(html);
7664
8251
  return;
7665
8252
  }
8253
+ const textMetadata = await getLocalTextFileMetadata(localPath);
8254
+ if (textMetadata) {
8255
+ const html = await createTextPreviewHtml(localPath, { newProjectName, line, column });
8256
+ res.status(200).type("text/html; charset=utf-8").send(html);
8257
+ return;
8258
+ }
8259
+ res.setHeader("Content-Disposition", "attachment");
7666
8260
  res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
7667
8261
  if (!error) return;
7668
8262
  if (!res.headersSent) res.status(404).json({ error: "File not found." });
@@ -7674,6 +8268,9 @@ function createServer(options = {}) {
7674
8268
  app.get("/codex-local-edit/*path", async (req, res) => {
7675
8269
  const rawPath = readWildcardPathParam(req.params.path);
7676
8270
  const localPath = decodeBrowsePath(`/${rawPath}`);
8271
+ const newProjectName = typeof req.query.newProjectName === "string" ? req.query.newProjectName : "";
8272
+ const line = readPositiveIntegerQueryParam(req.query.line);
8273
+ const column = line ? readPositiveIntegerQueryParam(req.query.column) : null;
7677
8274
  if (!localPath || !isAbsolute3(localPath)) {
7678
8275
  res.status(400).json({ error: "Expected absolute local file path." });
7679
8276
  return;
@@ -7684,7 +8281,11 @@ function createServer(options = {}) {
7684
8281
  res.status(400).json({ error: "Expected file path." });
7685
8282
  return;
7686
8283
  }
7687
- const html = await createTextEditorHtml(localPath);
8284
+ if (!await isTextEditableFile(localPath)) {
8285
+ res.status(415).json({ error: "Only text-like files are editable." });
8286
+ return;
8287
+ }
8288
+ const html = await createTextEditorHtml(localPath, { newProjectName, line, column });
7688
8289
  res.status(200).type("text/html; charset=utf-8").send(html);
7689
8290
  } catch {
7690
8291
  res.status(404).json({ error: "File not found." });