codexui-android 0.1.85 → 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-JQMCS7KJ.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
@@ -3778,6 +3779,78 @@ function spawnSyncCommand(command, args = [], options = {}) {
3778
3779
  var PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5e3;
3779
3780
  var THREAD_RESPONSE_TURN_LIMIT = 10;
3780
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
+ }
3781
3854
  function asRecord5(value) {
3782
3855
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3783
3856
  }
@@ -5965,10 +6038,21 @@ async function loadAllThreadsForSearch(appServer) {
5965
6038
  }
5966
6039
  cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
5967
6040
  } while (cursor);
5968
- 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);
5969
6053
  const concurrency = 4;
5970
- for (let offset = 0; offset < threads.length; offset += concurrency) {
5971
- 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);
5972
6056
  const loaded = await Promise.all(batch.map(async (thread) => {
5973
6057
  try {
5974
6058
  const readResponse = await appServer.rpc("thread/read", {
@@ -5977,27 +6061,23 @@ async function loadAllThreadsForSearch(appServer) {
5977
6061
  });
5978
6062
  const messageText = extractThreadMessageText(readResponse);
5979
6063
  const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
5980
- return {
6064
+ return [thread.id, {
5981
6065
  id: thread.id,
5982
6066
  title: thread.title,
5983
6067
  preview: thread.preview,
5984
6068
  messageText,
5985
6069
  searchableText
5986
- };
6070
+ }];
5987
6071
  } catch {
5988
- const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
5989
- return {
5990
- id: thread.id,
5991
- title: thread.title,
5992
- preview: thread.preview,
5993
- messageText: "",
5994
- searchableText
5995
- };
6072
+ return null;
5996
6073
  }
5997
6074
  }));
5998
- docs.push(...loaded);
6075
+ for (const row of loaded) {
6076
+ if (!row) continue;
6077
+ docsById.set(row[0], row[1]);
6078
+ }
5999
6079
  }
6000
- return docs;
6080
+ return Array.from(docsById.values());
6001
6081
  });
6002
6082
  }
6003
6083
  async function buildThreadSearchIndex(appServer) {
@@ -6030,6 +6110,42 @@ function createCodexBridgeMiddleware() {
6030
6110
  }).catch(() => {
6031
6111
  });
6032
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);
6033
6149
  try {
6034
6150
  if (!req.url) {
6035
6151
  next();
@@ -7165,76 +7281,92 @@ function createAuthSession(password) {
7165
7281
  }
7166
7282
 
7167
7283
  // src/server/localBrowseUi.ts
7168
- 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";
7169
7285
  import { open, readFile as readFile4, readdir as readdir3, stat as stat5 } from "fs/promises";
7170
- var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
7171
- ".txt",
7172
- ".md",
7173
- ".json",
7174
- ".js",
7175
- ".ts",
7176
- ".tsx",
7177
- ".jsx",
7178
- ".css",
7179
- ".scss",
7180
- ".html",
7181
- ".htm",
7182
- ".xml",
7183
- ".yml",
7184
- ".yaml",
7185
- ".log",
7186
- ".csv",
7187
- ".env",
7188
- ".py",
7189
- ".sh",
7190
- ".toml",
7191
- ".ini",
7192
- ".conf",
7193
- ".sql",
7194
- ".bat",
7195
- ".cmd",
7196
- ".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]
7304
+ ]);
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]
7197
7352
  ]);
7198
- function languageForPath(pathValue) {
7199
- const extension = extname2(pathValue).toLowerCase();
7200
- switch (extension) {
7201
- case ".js":
7202
- return "javascript";
7203
- case ".ts":
7204
- return "typescript";
7205
- case ".jsx":
7206
- return "javascript";
7207
- case ".tsx":
7208
- return "typescript";
7209
- case ".py":
7210
- return "python";
7211
- case ".sh":
7212
- return "sh";
7213
- case ".css":
7214
- case ".scss":
7215
- return "css";
7216
- case ".html":
7217
- case ".htm":
7218
- return "html";
7219
- case ".json":
7220
- return "json";
7221
- case ".md":
7222
- return "markdown";
7223
- case ".yaml":
7224
- case ".yml":
7225
- return "yaml";
7226
- case ".xml":
7227
- return "xml";
7228
- case ".sql":
7229
- return "sql";
7230
- case ".toml":
7231
- return "ini";
7232
- case ".ini":
7233
- case ".conf":
7234
- return "ini";
7235
- default:
7236
- return "plaintext";
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 };
7237
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 };
7368
+ }
7369
+ return { language: TEXT_LANGUAGE, recognized: false };
7238
7370
  }
7239
7371
  function normalizeLocalPath(rawPath) {
7240
7372
  const trimmed = rawPath.trim();
@@ -7257,7 +7389,7 @@ function decodeBrowsePath(rawPath) {
7257
7389
  }
7258
7390
  }
7259
7391
  function isTextEditablePath(pathValue) {
7260
- return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
7392
+ return getLanguageConfigForPath(pathValue).recognized;
7261
7393
  }
7262
7394
  function isHiddenName(value) {
7263
7395
  return value.startsWith(".");
@@ -7291,25 +7423,157 @@ async function isTextEditableFile(localPath) {
7291
7423
  return false;
7292
7424
  }
7293
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
+ }
7294
7447
  function escapeHtml(value) {
7295
7448
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
7296
7449
  }
7297
7450
  function normalizeNewProjectName(value) {
7298
7451
  return value.trim().replace(/[\\/]+/gu, "").trim();
7299
7452
  }
7300
- function toBrowseHref(pathValue, newProjectName = "") {
7301
- const normalizedName = normalizeNewProjectName(newProjectName);
7302
- const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : "";
7303
- return `/codex-local-browse${encodeURI(pathValue)}${query}`;
7453
+ function normalizeLineTarget(value) {
7454
+ return Number.isInteger(value) && Number(value) > 0 ? Number(value) : null;
7304
7455
  }
7305
- function toEditHref(pathValue, newProjectName = "") {
7306
- const normalizedName = normalizeNewProjectName(newProjectName);
7307
- const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : "";
7308
- 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()}`;
7309
7479
  }
7310
7480
  function escapeForInlineScriptString(value) {
7311
7481
  return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
7312
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
+ }
7313
7577
  async function getDirectoryItems(localPath) {
7314
7578
  const entries = await readdir3(localPath, { withFileTypes: true });
7315
7579
  const withMeta = await Promise.all(entries.map(async (entry) => {
@@ -7359,6 +7623,64 @@ function actionButtonsHtml(localPath, newProjectName) {
7359
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>`;
7360
7624
  return `${createButton}${openButton}`;
7361
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
+ }
7362
7684
  async function getLocalDirectoryListing(localPath, options = {}) {
7363
7685
  const entries = await readdir3(localPath, { withFileTypes: true });
7364
7686
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
@@ -7382,10 +7704,10 @@ async function createDirectoryListingHtml(localPath, options) {
7382
7704
  const parentPath = dirname2(localPath);
7383
7705
  const rows = items.map((item) => {
7384
7706
  const suffix = item.isDirectory ? "/" : "";
7385
- 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>` : "";
7386
- 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>`;
7387
7709
  }).join("\n");
7388
- 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>` : "";
7389
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>` : "";
7390
7712
  const actionButtons = actionButtonsHtml(localPath, newProjectName);
7391
7713
  return `<!doctype html>
@@ -7394,35 +7716,37 @@ async function createDirectoryListingHtml(localPath, options) {
7394
7716
  <meta charset="utf-8" />
7395
7717
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7396
7718
  <title>Index of ${escapeHtml(localPath)}</title>
7719
+ <script>${renderStandaloneThemeBootstrapScript()}</script>
7397
7720
  <style>
7398
- body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
7399
- 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; }
7400
7724
  a:hover { text-decoration: underline; }
7401
7725
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
7402
7726
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
7403
- .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); }
7404
7728
  .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
7405
- .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); }
7406
7730
  .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
7407
7731
  .header-open-btn {
7408
7732
  height: 42px;
7409
7733
  padding: 0 14px;
7410
- border: 1px solid #4f8de0;
7734
+ border: 1px solid var(--lb-primary-border);
7411
7735
  border-radius: 10px;
7412
- background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
7413
- color: #eef6ff;
7736
+ background: var(--lb-primary-bg);
7737
+ color: var(--lb-primary-text);
7414
7738
  font-weight: 700;
7415
7739
  letter-spacing: 0.01em;
7416
7740
  cursor: pointer;
7417
- box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
7741
+ box-shadow: var(--lb-shadow);
7418
7742
  }
7419
7743
  .header-open-btn:hover { filter: brightness(1.08); }
7420
7744
  .header-open-btn:disabled { opacity: 0.6; cursor: default; }
7421
- .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; }
7422
7746
  .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
7423
- .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; }
7424
7748
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
7425
- .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; }
7426
7750
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
7427
7751
  @media (max-width: 640px) {
7428
7752
  body { margin: 12px; }
@@ -7483,10 +7807,144 @@ async function createDirectoryListingHtml(localPath, options) {
7483
7807
  </body>
7484
7808
  </html>`;
7485
7809
  }
7486
- 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
+ }
7487
7946
  const content = await readFile4(localPath, "utf8");
7488
7947
  const parentPath = dirname2(localPath);
7489
- const language = languageForPath(localPath);
7490
7948
  const safeContentLiteral = escapeForInlineScriptString(content);
7491
7949
  return `<!doctype html>
7492
7950
  <html lang="en">
@@ -7494,49 +7952,64 @@ async function createTextEditorHtml(localPath) {
7494
7952
  <meta charset="utf-8" />
7495
7953
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7496
7954
  <title>Edit ${escapeHtml(localPath)}</title>
7955
+ <script>${renderStandaloneThemeBootstrapScript()}</script>
7497
7956
  <style>
7957
+ ${renderStandaloneThemeCss()}
7498
7958
  html, body { width: 100%; height: 100%; margin: 0; }
7499
- body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
7500
- .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); }
7501
7961
  .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
7502
- 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; }
7503
7963
  button:hover, a:hover { filter: brightness(1.08); }
7504
7964
  #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
7505
- #status { margin-left: 8px; color: #8cc2ff; }
7506
- .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
7507
- .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
7508
- .ace_marker-layer .ace_active-line { background: #10213c !important; }
7509
- .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
7510
- .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); }
7511
7970
  </style>
7512
7971
  </head>
7513
7972
  <body>
7514
7973
  <div class="toolbar">
7515
7974
  <div class="row">
7516
- <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>
7517
7979
  <button id="saveBtn" type="button">Save</button>
7518
7980
  <span id="status"></span>
7519
7981
  </div>
7520
- <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>
7521
7983
  </div>
7522
7984
  <div id="editor"></div>
7523
- <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
7524
- <script>
7525
- const saveBtn = document.getElementById('saveBtn');
7526
- const status = document.getElementById('status');
7527
- const editor = ace.edit('editor');
7528
- editor.setTheme('ace/theme/tomorrow_night');
7529
- editor.session.setMode('ace/mode/${escapeHtml(language)}');
7530
- 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);
7531
7998
  editor.setOptions({
7532
7999
  fontSize: '13px',
7533
8000
  wrap: true,
7534
8001
  showPrintMargin: false,
7535
8002
  useSoftTabs: true,
7536
8003
  tabSize: 2,
7537
- behavioursEnabled: true,
7538
- });
7539
- 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();
7540
8013
 
7541
8014
  saveBtn.addEventListener('click', async () => {
7542
8015
  status.textContent = 'Saving...';
@@ -7602,6 +8075,34 @@ function readWildcardPathParam(value) {
7602
8075
  if (Array.isArray(value)) return value.join("/");
7603
8076
  return "";
7604
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
+ }
7605
8106
  function createServer(options = {}) {
7606
8107
  const app = express();
7607
8108
  const bridge = createCodexBridgeMiddleware();
@@ -7629,19 +8130,87 @@ function createServer(options = {}) {
7629
8130
  if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
7630
8131
  });
7631
8132
  });
7632
- 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) => {
7633
8171
  const rawPath = typeof req.query.path === "string" ? req.query.path : "";
7634
8172
  const localPath = normalizeLocalPath(rawPath);
8173
+ const wantsDownload = readBooleanQueryFlag(req.query.download);
7635
8174
  if (!localPath || !isAbsolute3(localPath)) {
7636
8175
  res.status(400).json({ error: "Expected absolute local file path." });
7637
8176
  return;
7638
8177
  }
7639
- res.setHeader("Cache-Control", "private, no-store");
7640
- res.setHeader("Content-Disposition", "inline");
7641
- res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
7642
- if (!error) return;
7643
- if (!res.headersSent) res.status(404).json({ error: "File not found." });
7644
- });
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
+ }
7645
8214
  });
7646
8215
  app.get("/codex-local-directories", async (req, res) => {
7647
8216
  const rawPath = typeof req.query.path === "string" ? req.query.path : "";
@@ -7667,6 +8236,8 @@ function createServer(options = {}) {
7667
8236
  const rawPath = readWildcardPathParam(req.params.path);
7668
8237
  const localPath = decodeBrowsePath(`/${rawPath}`);
7669
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;
7670
8241
  if (!localPath || !isAbsolute3(localPath)) {
7671
8242
  res.status(400).json({ error: "Expected absolute local file path." });
7672
8243
  return;
@@ -7679,6 +8250,13 @@ function createServer(options = {}) {
7679
8250
  res.status(200).type("text/html; charset=utf-8").send(html);
7680
8251
  return;
7681
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");
7682
8260
  res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
7683
8261
  if (!error) return;
7684
8262
  if (!res.headersSent) res.status(404).json({ error: "File not found." });
@@ -7690,6 +8268,9 @@ function createServer(options = {}) {
7690
8268
  app.get("/codex-local-edit/*path", async (req, res) => {
7691
8269
  const rawPath = readWildcardPathParam(req.params.path);
7692
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;
7693
8274
  if (!localPath || !isAbsolute3(localPath)) {
7694
8275
  res.status(400).json({ error: "Expected absolute local file path." });
7695
8276
  return;
@@ -7700,7 +8281,11 @@ function createServer(options = {}) {
7700
8281
  res.status(400).json({ error: "Expected file path." });
7701
8282
  return;
7702
8283
  }
7703
- 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 });
7704
8289
  res.status(200).type("text/html; charset=utf-8").send(html);
7705
8290
  } catch {
7706
8291
  res.status(404).json({ error: "File not found." });