From 65b3779da506ad781ca3f3bb24f931369aeca5bf Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 8 Apr 2026 18:55:56 +0700 Subject: [PATCH] fix: log bootstrap errors to daemon.log and add layout debug logging - Log fatal errors during daemon bootstrap and startup to pino (daemon.log) instead of only writing to stderr, which is invisible on Windows desktop - Delay process.exit to let pino async streams flush - Make voice MCP bridge script resolution non-fatal (warn + disable instead of crashing the daemon) - Add PASEO_ELECTRON_FLAGS env var for passing Chromium flags on Linux - Add layout debug logging to AppContainer for diagnosing sidebar issues --- packages/app/src/app/_layout.tsx | 31 ++++++++++++++ packages/desktop/src/main.ts | 12 ++++++ packages/server/src/server/bootstrap.ts | 55 ++++++++++++++----------- packages/server/src/server/index.ts | 13 +++--- 4 files changed, 82 insertions(+), 29 deletions(-) diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 0540dfb2e..097b59da0 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -371,10 +371,41 @@ function AppContainer({ const toggleBothSidebars = usePanelStore((state) => state.toggleBothSidebars); const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); + const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen); + const sidebarWidth = usePanelStore((state) => state.sidebarWidth); const isCompactLayout = isCompactFormFactor(); const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0; + useEffect(() => { + const bp = UnistylesRuntime.breakpoint; + const screenW = UnistylesRuntime.screen.width; + const screenH = UnistylesRuntime.screen.height; + const isElectron = getIsElectronRuntime(); + const windowW = Platform.OS === "web" ? window.innerWidth : undefined; + const windowH = Platform.OS === "web" ? window.innerHeight : undefined; + const dpr = Platform.OS === "web" ? window.devicePixelRatio : undefined; + const ua = Platform.OS === "web" ? navigator.userAgent : undefined; + + console.log( + "[layout-debug]", + JSON.stringify({ + breakpoint: bp, + isCompactLayout, + isElectron, + chromeEnabled, + isFocusModeEnabled, + agentListOpen, + sidebarWidth, + sidebarRenderedInRow: !isCompactLayout && chromeEnabled && !isFocusModeEnabled, + unistylesScreen: { w: screenW, h: screenH }, + window: { w: windowW, h: windowH }, + devicePixelRatio: dpr, + userAgent: ua, + }), + ); + }, [isCompactLayout, chromeEnabled, isFocusModeEnabled, agentListOpen, sidebarWidth]); + useKeyboardShortcuts({ enabled: chromeEnabled, isMobile: isCompactLayout, diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index d5e4106c5..ff4c3d655 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -37,6 +37,18 @@ const APP_SCHEME = "paseo"; const OPEN_PROJECT_EVENT = "paseo:event:open-project"; app.setName("Paseo"); +// Allow users to pass Chromium flags via PASEO_ELECTRON_FLAGS for debugging +// rendering issues (e.g. "--disable-gpu --ozone-platform=x11"). +// Must run before app.whenReady(). +const electronFlags = process.env.PASEO_ELECTRON_FLAGS?.trim(); +if (electronFlags) { + for (const token of electronFlags.split(/\s+/)) { + const [key, ...rest] = token.replace(/^--/, "").split("="); + app.commandLine.appendSwitch(key, rest.join("=") || undefined); + } + log.info("[electron-flags]", electronFlags); +} + let pendingOpenProjectPath = parseOpenProjectPathFromArgv({ argv: process.argv, isDefaultApp: process.defaultApp, diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index d411050e8..deb87f3cc 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -133,21 +133,28 @@ import { resolveVoiceMcpBridgeFromRuntime } from "./voice-mcp-bridge-command.js" type AgentMcpTransportMap = Map; -function resolveVoiceMcpBridgeCommand(logger: Logger): { command: string; baseArgs: string[] } { - const decision = resolveVoiceMcpBridgeFromRuntime({ - bootstrapModuleUrl: import.meta.url, - execPath: process.execPath, - explicitScriptPath: process.env.PASEO_MCP_STDIO_SOCKET_BRIDGE_SCRIPT, - }); - logger.info( - { - source: decision.source, - command: decision.resolved.command, - baseArgs: decision.resolved.baseArgs, - }, - "Resolved voice MCP bridge command", - ); - return decision.resolved; +function resolveVoiceMcpBridgeCommand( + logger: Logger, +): { command: string; baseArgs: string[] } | null { + try { + const decision = resolveVoiceMcpBridgeFromRuntime({ + bootstrapModuleUrl: import.meta.url, + execPath: process.execPath, + explicitScriptPath: process.env.PASEO_MCP_STDIO_SOCKET_BRIDGE_SCRIPT, + }); + logger.info( + { + source: decision.source, + command: decision.resolved.command, + baseArgs: decision.resolved.baseArgs, + }, + "Resolved voice MCP bridge command", + ); + return decision.resolved; + } catch (err) { + logger.warn({ err }, "Voice MCP bridge script not available — voice MCP via stdio disabled"); + return null; + } } export type PaseoOpenAIConfig = OpenAiSpeechProviderConfig; @@ -669,14 +676,16 @@ export async function createPaseoDaemon( speechService, terminalManager, { - voiceAgentMcpStdio: { - command: voiceMcpBridgeCommand.command, - baseArgs: [...voiceMcpBridgeCommand.baseArgs], - env: { - ELECTRON_RUN_AS_NODE: "1", - PASEO_HOME: config.paseoHome, - }, - }, + voiceAgentMcpStdio: voiceMcpBridgeCommand + ? { + command: voiceMcpBridgeCommand.command, + baseArgs: [...voiceMcpBridgeCommand.baseArgs], + env: { + ELECTRON_RUN_AS_NODE: "1", + PASEO_HOME: config.paseoHome, + }, + } + : null, ensureVoiceMcpSocketForAgent: (agentId) => voiceMcpBridgeManager?.ensureBridgeForCaller(agentId) ?? Promise.reject(new Error("Voice MCP bridge manager is not initialized")), diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index a71628887..87f104963 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -143,6 +143,7 @@ async function main() { logger.error({ pid: err.existingLock?.pid }, err.message); process.exit(1); } + logger.fatal({ err }, "Daemon bootstrap failed"); throw err; } @@ -153,6 +154,7 @@ async function main() { logger.error({ pid: err.existingLock?.pid }, err.message); process.exit(1); } + logger.fatal({ err }, "Daemon failed to start listening"); throw err; } @@ -171,10 +173,9 @@ async function main() { } main().catch((err) => { - if (process.env.PASEO_DEBUG === "1") { - process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`); - } else { - process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); - } - process.exit(1); + process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`); + // Give pino streams a moment to flush the fatal log entry to daemon.log + // before the process exits. Without this, async file streams may lose the + // last few entries that explain why the daemon crashed. + setTimeout(() => process.exit(1), 200); });