How to Debug IPC Communication in Orca: A Complete Developer Guide
To debug IPC communication in Orca, instrument the preload bridge in src/preload/index.ts, add logging to ipcMain.handle registrations in src/main/ipc/pty.ts, and use the renderer's DevTools console to inspect window.api calls and event listeners.
Orca is an Electron-based terminal emulator where robust main-process ↔ renderer communication is essential for PTY sessions, filesystem operations, and settings management. Understanding how to trace messages through Orca's typed IPC layers will help you identify where data drops, duplicate listeners form, or environment variables fail to inject. This guide walks through the architectural layers of debugging IPC communication in Orca using the actual source paths and handler signatures from the stablyai/orca repository.
Understanding Orca's IPC Architecture
Orca isolates renderer access through a three-layer IPC stack. Each layer has specific debugging hooks you can instrument without modifying core business logic.
The Preload Bridge (src/preload/index.ts)
The preload bridge exposes a safe window.api object to the renderer, funneling all calls through ipcRenderer.invoke for request-response patterns or ipcRenderer.on for events.
// src/preload/index.ts
contextBridge.exposeInMainWorld('api', {
getIdentity: (): Promise<AppIdentity> => ipcRenderer.invoke('app:getIdentity'),
onTerminalFileDrop: (listener) => {
ipcRenderer.on('terminal:file-drop', listener);
return () => ipcRenderer.removeListener('terminal:file-drop', listener);
},
});
To debug preload exposure, open the renderer DevTools and execute console.log(window.api) to verify which channels are available. You can temporarily expose additional debug namespaces by adding them to this file's exposeInMainWorld call.
Renderer-Side Error Handling (src/renderer/src/lib/ipc-error.ts)
Errors thrown in ipcMain.handle get wrapped by Electron into generic Error objects. The extractIpcErrorMessage helper normalizes these for UI display.
// src/renderer/src/lib/ipc-error.ts
export function extractIpcErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}
When debugging renderer calls, wrap your window.api invocations with try-catch blocks that log the raw error object before it passes through this normalizer. This reveals the full stack trace from the main process.
Main Process Handler Registration (src/main/ipc/register-core-handlers.ts)
All domain-specific handlers (PTY, filesystem, telemetry) register centrally in the registerCoreHandlers function. This function includes a guard against double-registration on macOS re-activation.
// src/main/ipc/register-core-handlers.ts
export function registerCoreHandlers(
store: Store,
runtime: OrcaRuntimeService,
stats: StatsCollector
) {
if (registered) return; // Guard against duplicate registration
registered = true;
registerPtyHandlers(mainWindow, runtime);
registerFilesystemHandlers(mainWindow);
// ... additional domains
}
Insert console.log('[IPC] Registering handlers for:', domain) at the top of each registration function to verify execution order during app startup.
Debugging the PTY Communication Layer
The PTY channel (pty:spawn, pty:write, pty:data) represents the most complex IPC path in Orca, implementing custom batching to reduce round-trip traffic.
Handler Registration and Listener Cleanup
In src/main/ipc/pty.ts, the registerPtyHandlers function must clear previous listeners before attaching new ones to prevent duplicate message handling after window reloads.
// src/main/ipc/pty.ts
export function registerPtyHandlers(mainWindow: BrowserWindow, runtime: OrcaRuntimeService) {
// Critical: Remove old listeners to prevent duplicates
ipcMain.removeAllListeners('pty:write');
ipcMain.handle('pty:spawn', async (event, args) => {
// Debugging hook: log incoming spawn requests
console.log('[debug] PTY spawn:', args);
// ... spawn logic
});
ipcMain.on('pty:write', (event, { id, data }) => {
// Debugging hook: inspect write traffic
console.log(`[debug] PTY write ${id}: ${data.length} bytes`);
});
}
To verify cleanup effectiveness, log ipcMain.listenerCount('pty:write') before and after the removeAllListeners call. If the count exceeds 1 after registration, you have a leak.
Batching and Data Flow Inspection
PTY output is collected in a pendingData map and flushed every 8ms via schedulePendingDataFlush and flushPendingData. To inspect this batching:
- Add
console.log('[debug] Pending data for', id, pendingData.get(id))insideflushPendingData - Observe the timing by logging
Date.now()inschedulePendingDataFlush
This reveals whether lag stems from the batching window or the underlying PTY provider.
Environment Variable Injection
The buildPtyHostEnv function in src/main/ipc/pty.ts (around lines 70-100) injects attribution variables for OpenCode, Pi, Claude, Codex, and GitHub. To verify environment construction:
// Temporary test script
import { buildPtyHostEnv } from './src/main/ipc/pty.ts';
const env = buildPtyHostEnv('test-id', process.env, { isDaemon: false });
console.log('Injected env:', env);
Compare this output against the actual environment seen by the spawned shell to isolate injection failures.
Practical Debugging Techniques
Apply these specific techniques to trace messages end-to-end:
- Inspect raw IPC traffic: In the renderer DevTools, invoke
await window.api.getIdentity()and wrap it inconsole.time('ipc')/console.timeEnd('ipc')to measure latency. - Log main-process handling: Set
process.env.NODE_DEBUG=electron-ipcbefore launching Orca to see Electron's internal IPC debug output. - Verify handler removal on reload: After pressing Ctrl+R in the window, check
require('electron').ipcMain.listenerCount('pty:write')in the main process console to confirm it equals 1. - Force breakpoints: Insert
debugger;insideregisterPtyHandlersor the PTY provider'sspawnmethod, then attach the main process inspector. - Simulate failures: Mock the provider's
spawnto throw insrc/main/ipc/pty.test.tsand observe thenormalizeNodePtySpawnErrorwrapping logic.
Example: Debug Logging for PTY Spawn
Add this temporary instrumentation to src/main/ipc/pty.ts:
ipcMain.handle('pty:spawn', async (event, args) => {
console.log('[debug] PTY spawn request', {
cols: args.cols,
rows: args.rows,
cwd: args.cwd,
connectionId: args.connectionId,
});
// existing logic...
});
Example: Logging All PTY Data in Renderer
Temporarily expose a debug channel in src/preload/index.ts:
contextBridge.exposeInMainWorld('debug', {
ptyData: (listener) => {
ipcRenderer.on('pty:data', listener);
return () => ipcRenderer.removeListener('pty:data', listener);
},
});
Then subscribe in the renderer:
window.debug.ptyData((_event, { id, data }) => {
console.debug(`[debug] PTY ${id}: ${data}`);
});
Summary
- Orca's IPC flows through three layers: the preload bridge (
src/preload/index.ts), renderer error handlers (src/renderer/src/lib/ipc-error.ts), and main-process registrations (src/main/ipc/register-core-handlers.ts). - Always verify listener cleanup in
src/main/ipc/pty.tsusingipcMain.removeAllListenersto prevent duplicate message handling after reloads. - Inspect PTY batching by logging the
pendingDataMap inschedulePendingDataFlushto diagnose latency issues. - Test environment injection by calling
buildPtyHostEnvdirectly to verify that OpenCode, Claude, and other attribution variables are present. - Use the
window.apiobject in renderer DevTools to manually trigger IPC calls and inspect responses without UI interaction.
Frequently Asked Questions
How do I check if my IPC handler is registered twice in Orca?
Log ipcMain.listenerCount('channel-name') immediately after the registration call in src/main/ipc/register-core-handlers.ts. If the count is greater than 1 after a window reload, your cleanup logic in the handler registration function (such as registerPtyHandlers) is missing the removeAllListeners call.
Why am I not seeing PTY output in the Orca renderer?
First verify the preload bridge exposed the data listener in src/preload/index.ts. Then check src/main/ipc/pty.ts for the pendingData flush logic—if the 8ms batching timer fails to fire due to a JavaScript error in the main process, data will accumulate indefinitely without sending to the renderer.
Where does Orca inject environment variables for spawned terminals?
Environment injection happens in the buildPtyHostEnv function within src/main/ipc/pty.ts (lines 70-100). This function is called for both local and daemon-backed PTY spawns, ensuring consistent attribution variables like OPENCODE_AGENT and CLAUDE_CODE_AGENT are present regardless of the connection type.
How can I debug IPC errors that appear generic in the UI?
Errors from ipcRenderer.invoke are wrapped by Electron before reaching the renderer. Import the extractIpcErrorMessage utility from src/renderer/src/lib/ipc-error.ts and modify it to return err.stack instead of err.message during development, or add console.error(err) in the catch block before the normalization occurs to see the full main-process stack trace.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →