• Sets up IPC debugging instrumentation in the preload/renderer context. Wraps ipcRenderer methods to capture and log IPC events. Exposes electronDebug object with IPC viewer and memory stats.

    Returns void

    export function setupIpcDebugging(): void {
    if (installed) return;
    installed = true;

    // Check if IPC debugging should be enabled based on saved preferences
    // This runs in preload context where localStorage is available
    try {
    const debugModeEnabled = localStorage.getItem("debug-mode-enabled");
    const featureToggles = localStorage.getItem("debug-feature-toggles");

    if (debugModeEnabled === "true" && featureToggles) {
    const toggles = JSON.parse(featureToggles);
    if (toggles.ipcViewer === true) {
    enabled = true;
    }
    }
    } catch {
    // Default to disabled if there's any error reading settings
    enabled = false;
    }

    const originalInvoke = ipcRenderer.invoke.bind(ipcRenderer);
    const originalSend = ipcRenderer.send.bind(ipcRenderer);
    const originalPostMessage =
    typeof ipcRenderer.postMessage === "function"
    ? ipcRenderer.postMessage.bind(ipcRenderer)
    : undefined;
    const originalOn = ipcRenderer.on.bind(ipcRenderer);
    const originalOnce = ipcRenderer.once.bind(ipcRenderer);
    const originalAddListener = ipcRenderer.addListener.bind(ipcRenderer);
    const originalRemoveListener = ipcRenderer.removeListener.bind(ipcRenderer);
    const originalOff =
    typeof ipcRenderer.off === "function"
    ? ipcRenderer.off.bind(ipcRenderer)
    : undefined;

    ipcRenderer.invoke = async (channel: string, ...args: unknown[]) => {
    const correlationId = generateId();
    const startedAt = nowMs();

    appendEvent({
    correlationId,
    channel,
    direction: "sent",
    transport: "invoke",
    status: "pending",
    timestamp: new Date().toISOString(),
    payload: createPayload(args),
    });

    try {
    const result = await originalInvoke(channel, ...args);
    const durationMs = Math.max(0, nowMs() - startedAt);
    appendEvent({
    correlationId,
    channel,
    direction: "received",
    transport: "invoke-response",
    status: "fulfilled",
    timestamp: new Date().toISOString(),
    durationMs,
    payload: createPayload(result),
    });
    return result;
    } catch (error) {
    const durationMs = Math.max(0, nowMs() - startedAt);
    appendEvent({
    correlationId,
    channel,
    direction: "received",
    transport: "invoke-response",
    status: "rejected",
    timestamp: new Date().toISOString(),
    durationMs,
    payload: createPayload(
    error instanceof Error
    ? { name: error.name, message: error.message }
    : error,
    ),
    error:
    error instanceof Error
    ? `${error.name}: ${error.message}`
    : String(error),
    });
    throw error;
    }
    };

    ipcRenderer.send = (channel: string, ...args: unknown[]) => {
    appendEvent({
    channel,
    direction: "sent",
    transport: "send",
    timestamp: new Date().toISOString(),
    payload: createPayload(args),
    });
    return originalSend(channel, ...args);
    };

    if (originalPostMessage) {
    ipcRenderer.postMessage = (
    channel: string,
    message: unknown,
    transfer?: MessagePort[],
    ) => {
    appendEvent({
    channel,
    direction: "sent",
    transport: "message",
    timestamp: new Date().toISOString(),
    payload: createPayload({
    message,
    transferDescriptors: transfer?.length ?? 0,
    }),
    });
    return originalPostMessage(channel, message, transfer);
    };
    }

    const assignListener =
    (
    register: (
    channel: string,
    listener: RendererListener,
    ) => typeof ipcRenderer,
    mode: "on" | "once",
    ) =>
    (channel: string, listener: RendererListener) =>
    register(channel, wrapListener(channel, listener, mode));

    ipcRenderer.on = assignListener(originalOn, "on");
    ipcRenderer.addListener = assignListener(originalAddListener, "on");
    ipcRenderer.once = assignListener(originalOnce, "once");

    ipcRenderer.removeListener = (
    channel: string,
    listener: RendererListener,
    ) => {
    const wrapped = listenerMap.get(listener);
    if (wrapped) {
    listenerMap.delete(listener);
    return originalRemoveListener(channel, wrapped);
    }
    return originalRemoveListener(channel, listener);
    };

    if (originalOff) {
    ipcRenderer.off = (channel: string, listener: RendererListener) => {
    const wrapped = listenerMap.get(listener);
    if (wrapped) {
    listenerMap.delete(listener);
    return originalOff(channel, wrapped);
    }
    return originalOff(channel, listener);
    };
    }

    contextBridge.exposeInMainWorld("electronDebug", {
    ipc: {
    maxEntries: MAX_IPC_LOG_ENTRIES,
    getEvents: (): IpcLogEntry[] => collector.getEntries(),
    subscribe: (callback: (entries: IpcLogEntry[]) => void) =>
    collector.subscribe(callback),
    clear: () => collector.clear(),
    setEnabled: (value: boolean) => {
    enabled = value;
    if (!value) {
    collector.clear();
    }
    },
    isEnabled: () => enabled,
    },
    getMemoryStats: () => ipcRenderer.invoke("debug:get-memory-stats"),
    });
    }