diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 9497d2f3..fe9ea696 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -2653,6 +2653,7 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { .env("OPENCHAMBER_DIST_DIR", dist_dir.clone()) .env("OPENCHAMBER_RUNTIME", "desktop") .env("OPENCHAMBER_DESKTOP_NOTIFY", "true") + .env("OPENCHAMBER_SKIP_API_COMPRESSION", "true") .env("PATH", augmented_path.clone()) .env("NO_PROXY", no_proxy) .env("no_proxy", no_proxy); diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 22185e8d..9dab1bf4 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -768,6 +768,7 @@ const spawnLocalServer = async () => { process.env.OPENCHAMBER_DIST_DIR = resolveWebDistDir(); process.env.OPENCHAMBER_RUNTIME = 'desktop'; process.env.OPENCHAMBER_DESKTOP_NOTIFY = 'true'; + process.env.OPENCHAMBER_SKIP_API_COMPRESSION = process.env.OPENCHAMBER_SKIP_API_COMPRESSION || 'true'; process.env.NO_PROXY = process.env.NO_PROXY || 'localhost,127.0.0.1'; process.env.no_proxy = process.env.no_proxy || 'localhost,127.0.0.1'; diff --git a/packages/web/README.md b/packages/web/README.md index f9b2219d..9e3c8c2f 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -60,6 +60,10 @@ OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber | `OPENCODE_PORT` | Port of external server | | `OPENCODE_SKIP_START` | Skip starting embedded OpenCode server | | `OPENCHAMBER_OPENCODE_HOSTNAME` | Bind hostname for managed OpenCode server (default: `127.0.0.1`, use `0.0.0.0` for LAN/remote access — trusted networks only) | +| `OPENCHAMBER_HOST` | Bind hostname for the OpenChamber web server (default: `127.0.0.1`; use `0.0.0.0` for LAN/remote access — trusted networks only) | +| `OPENCHAMBER_VERBOSE_REQUEST_LOGS` | Set to `true` to log every HTTP request; disabled by default to keep user logs small | +| `OPENCHAMBER_SKIP_API_COMPRESSION` | Set to `true` to disable gzip compression for `/api/*` responses | +| `OPENCHAMBER_COMPRESS_API` | Set to `true` to force `/api/*` compression, or `false` to disable it. Desktop runtime disables API compression by default to reduce local sidecar CPU use | diff --git a/packages/web/server/index.js b/packages/web/server/index.js index b830f25f..945f369e 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -136,6 +136,10 @@ function shouldSkipCompression(req, res) { } const pathname = req.path || req.url || ''; + if ((pathname === '/api' || pathname.startsWith('/api/')) && shouldSkipApiCompression()) { + return true; + } + if (pathname.startsWith('/api/terminal/') && pathname.endsWith('/stream')) { return true; } @@ -168,6 +172,22 @@ const isEnvFlagEnabled = (value) => { return normalized === '1' || normalized === 'true'; }; +const isEnvFlagDisabled = (value) => { + if (value === false || value === 0) return true; + if (typeof value !== 'string') return false; + const normalized = value.trim().toLowerCase(); + return normalized === '0' || normalized === 'false'; +}; + +const shouldSkipApiCompression = () => { + if (isEnvFlagEnabled(process.env.OPENCHAMBER_SKIP_API_COMPRESSION)) return true; + if (isEnvFlagEnabled(process.env.OPENCHAMBER_COMPRESS_API)) return false; + if (isEnvFlagDisabled(process.env.OPENCHAMBER_COMPRESS_API)) return true; + return process.env.OPENCHAMBER_RUNTIME === 'desktop'; +}; + +const OPENCHAMBER_VERBOSE_REQUEST_LOGS = isEnvFlagEnabled(process.env.OPENCHAMBER_VERBOSE_REQUEST_LOGS); + const PLAN_MODE_EXPERIMENT_ENABLED = isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL_PLAN_MODE) || isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL); @@ -1101,6 +1121,7 @@ async function main(options = {}) { planModeExperimentalEnabled: PLAN_MODE_EXPERIMENT_ENABLED, }; }, + verboseRequestLogs: OPENCHAMBER_VERBOSE_REQUEST_LOGS, uiPassword, tunnelAuthController, readSettingsFromDiskMigrated, diff --git a/packages/web/server/lib/git/service.js b/packages/web/server/lib/git/service.js index aa15b56b..6ea0eedf 100644 --- a/packages/web/server/lib/git/service.js +++ b/packages/web/server/lib/git/service.js @@ -1260,45 +1260,55 @@ export async function getStatus(directory, options = {}) { const diffStats = Object.fromEntries(diffStatsMap.entries()); - const newFileStats = lightMode ? [] : await Promise.all( - status.files.map(async (file) => { + const MAX_NEW_FILE_STATS = 200; + const MAX_NEW_FILE_STAT_SIZE = 1024 * 1024; + const newFileStats = []; + + if (!lightMode) { + for (const file of status.files) { + if (newFileStats.length >= MAX_NEW_FILE_STATS) { + break; + } + const working = (file.working_dir || '').trim(); const indexStatus = (file.index || '').trim(); const statusCode = working || indexStatus; if (statusCode !== '?' && statusCode !== 'A') { - return null; + continue; } const existing = diffStats[file.path]; if (existing && existing.insertions > 0) { - return null; + continue; } const absolutePath = path.join(directoryPath, file.path); try { const stat = await fsp.stat(absolutePath); - if (!stat.isFile()) { - return null; + if (!stat.isFile() || stat.size > MAX_NEW_FILE_STAT_SIZE) { + continue; } const buffer = await fsp.readFile(absolutePath); if (buffer.indexOf(0) !== -1) { - return { + newFileStats.push({ path: file.path, insertions: existing?.insertions ?? 0, deletions: existing?.deletions ?? 0, - }; + }); + continue; } const normalized = buffer.toString('utf8').replace(/\r\n/g, '\n'); if (!normalized.length) { - return { + newFileStats.push({ path: file.path, insertions: 0, deletions: 0, - }; + }); + continue; } const segments = normalized.split('\n'); @@ -1307,20 +1317,20 @@ export async function getStatus(directory, options = {}) { } const lineCount = segments.length; - return { + newFileStats.push({ path: file.path, insertions: lineCount, deletions: 0, - }; + }); } catch (error) { - console.warn('Failed to estimate diff stats for new file', file.path, error); - return null; + if (error?.code !== 'ENOENT') { + console.warn('Failed to estimate diff stats for new file', file.path, error); + } } - }) - ); + } + } for (const entry of newFileStats) { - if (!entry) continue; diffStats[entry.path] = { insertions: entry.insertions, deletions: entry.deletions, diff --git a/packages/web/server/lib/opencode/bootstrap-runtime.js b/packages/web/server/lib/opencode/bootstrap-runtime.js index 6bc8a493..ea96bf5b 100644 --- a/packages/web/server/lib/opencode/bootstrap-runtime.js +++ b/packages/web/server/lib/opencode/bootstrap-runtime.js @@ -18,6 +18,7 @@ export const createBootstrapRuntime = (dependencies) => { serverStartedAt, gracefulShutdown, getHealthSnapshot, + verboseRequestLogs, uiPassword, tunnelAuthController, readSettingsFromDiskMigrated, @@ -60,7 +61,7 @@ export const createBootstrapRuntime = (dependencies) => { getHealthSnapshot, }); - registerCommonRequestMiddleware(app, { express }); + registerCommonRequestMiddleware(app, { express, verboseRequestLogs }); const uiAuthController = createUiAuth({ password: uiPassword, diff --git a/packages/web/server/lib/opencode/core-routes.js b/packages/web/server/lib/opencode/core-routes.js index 9c2fba75..53866e88 100644 --- a/packages/web/server/lib/opencode/core-routes.js +++ b/packages/web/server/lib/opencode/core-routes.js @@ -451,7 +451,7 @@ export const registerSettingsUtilityRoutes = (app, dependencies) => { }; export const registerCommonRequestMiddleware = (app, dependencies) => { - const { express } = dependencies; + const { express, verboseRequestLogs = false } = dependencies; app.use((req, res, next) => { if (req.path.startsWith('/api/behavior')) { @@ -492,7 +492,9 @@ export const registerCommonRequestMiddleware = (app, dependencies) => { app.use(express.urlencoded({ extended: true, limit: '50mb' })); app.use((req, _res, next) => { - console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); + if (verboseRequestLogs) { + console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); + } next(); }); };