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
This commit is contained in:
Mohamed Boudra
2026-04-09 18:54:25 +07:00
parent 7a9348a5fd
commit 65b3779da5
4 changed files with 82 additions and 29 deletions
+31
View File
@@ -371,10 +371,41 @@ function AppContainer({
const toggleBothSidebars = usePanelStore((state) => state.toggleBothSidebars); const toggleBothSidebars = usePanelStore((state) => state.toggleBothSidebars);
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode); const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
const isCompactLayout = isCompactFormFactor(); const isCompactLayout = isCompactFormFactor();
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0; 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({ useKeyboardShortcuts({
enabled: chromeEnabled, enabled: chromeEnabled,
isMobile: isCompactLayout, isMobile: isCompactLayout,
+12
View File
@@ -37,6 +37,18 @@ const APP_SCHEME = "paseo";
const OPEN_PROJECT_EVENT = "paseo:event:open-project"; const OPEN_PROJECT_EVENT = "paseo:event:open-project";
app.setName("Paseo"); 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({ let pendingOpenProjectPath = parseOpenProjectPathFromArgv({
argv: process.argv, argv: process.argv,
isDefaultApp: process.defaultApp, isDefaultApp: process.defaultApp,
+32 -23
View File
@@ -133,21 +133,28 @@ import { resolveVoiceMcpBridgeFromRuntime } from "./voice-mcp-bridge-command.js"
type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>; type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>;
function resolveVoiceMcpBridgeCommand(logger: Logger): { command: string; baseArgs: string[] } { function resolveVoiceMcpBridgeCommand(
const decision = resolveVoiceMcpBridgeFromRuntime({ logger: Logger,
bootstrapModuleUrl: import.meta.url, ): { command: string; baseArgs: string[] } | null {
execPath: process.execPath, try {
explicitScriptPath: process.env.PASEO_MCP_STDIO_SOCKET_BRIDGE_SCRIPT, const decision = resolveVoiceMcpBridgeFromRuntime({
}); bootstrapModuleUrl: import.meta.url,
logger.info( execPath: process.execPath,
{ explicitScriptPath: process.env.PASEO_MCP_STDIO_SOCKET_BRIDGE_SCRIPT,
source: decision.source, });
command: decision.resolved.command, logger.info(
baseArgs: decision.resolved.baseArgs, {
}, source: decision.source,
"Resolved voice MCP bridge command", command: decision.resolved.command,
); baseArgs: decision.resolved.baseArgs,
return decision.resolved; },
"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; export type PaseoOpenAIConfig = OpenAiSpeechProviderConfig;
@@ -669,14 +676,16 @@ export async function createPaseoDaemon(
speechService, speechService,
terminalManager, terminalManager,
{ {
voiceAgentMcpStdio: { voiceAgentMcpStdio: voiceMcpBridgeCommand
command: voiceMcpBridgeCommand.command, ? {
baseArgs: [...voiceMcpBridgeCommand.baseArgs], command: voiceMcpBridgeCommand.command,
env: { baseArgs: [...voiceMcpBridgeCommand.baseArgs],
ELECTRON_RUN_AS_NODE: "1", env: {
PASEO_HOME: config.paseoHome, ELECTRON_RUN_AS_NODE: "1",
}, PASEO_HOME: config.paseoHome,
}, },
}
: null,
ensureVoiceMcpSocketForAgent: (agentId) => ensureVoiceMcpSocketForAgent: (agentId) =>
voiceMcpBridgeManager?.ensureBridgeForCaller(agentId) ?? voiceMcpBridgeManager?.ensureBridgeForCaller(agentId) ??
Promise.reject(new Error("Voice MCP bridge manager is not initialized")), Promise.reject(new Error("Voice MCP bridge manager is not initialized")),
+7 -6
View File
@@ -143,6 +143,7 @@ async function main() {
logger.error({ pid: err.existingLock?.pid }, err.message); logger.error({ pid: err.existingLock?.pid }, err.message);
process.exit(1); process.exit(1);
} }
logger.fatal({ err }, "Daemon bootstrap failed");
throw err; throw err;
} }
@@ -153,6 +154,7 @@ async function main() {
logger.error({ pid: err.existingLock?.pid }, err.message); logger.error({ pid: err.existingLock?.pid }, err.message);
process.exit(1); process.exit(1);
} }
logger.fatal({ err }, "Daemon failed to start listening");
throw err; throw err;
} }
@@ -171,10 +173,9 @@ async function main() {
} }
main().catch((err) => { main().catch((err) => {
if (process.env.PASEO_DEBUG === "1") { process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
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
} else { // before the process exits. Without this, async file streams may lose the
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); // last few entries that explain why the daemon crashed.
} setTimeout(() => process.exit(1), 200);
process.exit(1);
}); });