• IPC traffic viewer for monitoring renderer ↔ main process messages. Displays, filters, and inspects IPC logs with direction, channel, and search filters.

    Returns ReactElement

    JSX element rendering the IPC viewer panel

    export function IpcViewer(): React.ReactElement {
    const { ipcEvents, maxIpcEntries, isLogRedactionEnabled } = useDebugState();
    const { clearIpcEvents } = useDebugActions();

    const [searchTerm, setSearchTerm] = useState("");
    const [channelFilter, setChannelFilter] = useState("");
    const [directionFilters, setDirectionFilters] = useState<
    Record<DirectionFilter, boolean>
    >(DEFAULT_DIRECTION_FILTER);
    const [isClipboardFallbackOpen, setIsClipboardFallbackOpen] = useState(false);
    const [clipboardFallbackText, setClipboardFallbackText] = useState("");
    const DEFAULT_WINDOW_SIZE = 100;
    const [visibleCount, setVisibleCount] = useState<number>(DEFAULT_WINDOW_SIZE);

    // Listen for custom events from PerformanceMonitor to set filter
    useEffect(() => {
    const handleSetFilter = (event: Event) => {
    const customEvent = event as CustomEvent<{ channel?: string }>;
    const detail = customEvent.detail;
    if (detail?.channel) {
    // Set channel filter when custom event is triggered
    setChannelFilter(detail.channel);
    setVisibleCount(DEFAULT_WINDOW_SIZE);
    }
    };

    globalThis.addEventListener(IPC_SET_FILTER_EVENT, handleSetFilter);

    return () => {
    globalThis.removeEventListener(IPC_SET_FILTER_EVENT, handleSetFilter);
    };
    }, []);

    const filteredEntries = useMemo(() => {
    // Step 1: Apply direction and search filters
    const directionAndSearchFiltered = ipcEvents.filter(
    (entry) =>
    matchesDirection(entry, directionFilters) &&
    includesQuery(entry, searchTerm),
    );

    // Step 2: Apply channel filtering - first try exact/substring match, then fallback to fuzzy
    if (!channelFilter.trim()) {
    return directionAndSearchFiltered;
    }

    const filterLower = channelFilter.toLowerCase();
    const exactMatches = directionAndSearchFiltered.filter((entry) =>
    entry.channel.toLowerCase().includes(filterLower),
    );

    // If we found matches with substring search, return them
    if (exactMatches.length > 0) {
    return exactMatches;
    }

    // Fallback to fuzzy search if no substring matches found
    const fuse = buildFuse(directionAndSearchFiltered, ["channel"], {
    ...FUSE_PRESET_LOOSE,
    });

    const results = fuse.search(channelFilter);
    return results.map((result: { item: IpcLogEntry }) => result.item);
    }, [ipcEvents, directionFilters, searchTerm, channelFilter]);

    const availableChannels = useMemo(() => {
    const set = new Set<string>();
    for (const entry of ipcEvents) {
    set.add(entry.channel);
    }
    return Array.from(set).sort((a, b) => a.localeCompare(b));
    }, [ipcEvents]);

    const totalEntries = ipcEvents.length;

    const toggleDirection = (direction: DirectionFilter) => {
    setDirectionFilters((prev) => ({
    ...prev,
    [direction]: !prev[direction],
    }));
    };

    const handleClear = () => {
    if (!totalEntries) return;
    clearIpcEvents();
    toast.success("IPC log cleared");
    };

    const handleCopy = async (entry: IpcLogEntry) => {
    try {
    const sanitized = isLogRedactionEnabled
    ? sanitizeForDebug(entry, { redactSensitive: true })
    : entry;
    const text = JSON.stringify(sanitized, null, 2);
    await navigator.clipboard.writeText(text);
    toast.success("Entry copied to clipboard");
    } catch (error) {
    // Fallback 1: use hidden textarea if clipboard API fails
    try {
    const textarea = document.createElement("textarea");
    const sanitized = isLogRedactionEnabled
    ? sanitizeForDebug(entry, { redactSensitive: true })
    : entry;
    textarea.value = JSON.stringify(sanitized, null, 2);
    textarea.style.position = "fixed";
    textarea.style.opacity = "0";
    document.body.appendChild(textarea);
    textarea.select();
    const success = (
    document as { execCommand(command: string): boolean }
    ).execCommand("copy");
    textarea.remove();

    if (success) {
    toast.success("Entry copied to clipboard");
    } else {
    throw new Error("execCommand failed");
    }
    } catch (fallbackError) {
    // Fallback 2: Show modal with textarea for manual selection
    console.error("Failed to copy IPC log entry", error, fallbackError);
    try {
    const sanitized = isLogRedactionEnabled
    ? sanitizeForDebug(entry, { redactSensitive: true })
    : entry;
    setClipboardFallbackText(JSON.stringify(sanitized, null, 2));
    setIsClipboardFallbackOpen(true);
    } catch (modalError) {
    console.error("Failed to set clipboard fallback modal", modalError);
    toast.error(
    "Unable to copy: clipboard access denied. Try enabling clipboard permissions or use HTTPS.",
    );
    }
    }
    }
    };

    const onCopy = useCallback((entry: IpcLogEntry) => {
    void handleCopy(entry);
    }, []);

    const reversedFiltered = useMemo(() => {
    // show newest entries first
    return [...filteredEntries].reverse();
    }, [filteredEntries]);

    const visibleEntries = useMemo(() => {
    return reversedFiltered.slice(0, visibleCount);
    }, [reversedFiltered, visibleCount]);

    const loadMore = () => setVisibleCount((v) => v + DEFAULT_WINDOW_SIZE);
    const showAll = () => setVisibleCount(reversedFiltered.length || 0);
    const resetWindow = () => setVisibleCount(DEFAULT_WINDOW_SIZE);

    return (
    <div className="flex h-full flex-col gap-4">
    <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
    <div className="space-y-2 lg:max-w-2xl">
    <h2 className="text-lg font-semibold">IPC traffic monitor</h2>
    <p className="text-muted-foreground text-sm">
    Observe renderer and main process interactions. Use filters to
    narrow down channels or payload content. The most recent{" "}
    {maxIpcEntries} entries are retained.
    </p>
    <div className="flex flex-wrap items-center gap-2 text-xs">
    <Badge variant="outline" className="border-border/60 bg-muted/40">
    {filteredEntries.length} showing
    </Badge>
    <Badge variant="outline" className="border-border/60 bg-muted/40">
    {totalEntries} captured
    </Badge>
    <Badge variant="outline" className="border-border/60 bg-muted/40">
    {availableChannels.length} channel
    {availableChannels.length === 1 ? "" : "s"}
    </Badge>
    </div>
    <div className="mt-2 flex flex-row flex-wrap items-center gap-2">
    {DIRECTIONS.map((direction) => {
    const active = directionFilters[direction];
    return (
    <Button
    key={direction}
    type="button"
    variant={active ? "default" : "outline"}
    className="rounded-full px-3 py-1 text-xs"
    onClick={() => toggleDirection(direction)}
    >
    <span
    className={cn(
    "flex items-center gap-1",
    DIRECTION_META[direction].tone,
    )}
    >
    {DIRECTION_META[direction].label}
    </span>
    </Button>
    );
    })}
    <Button
    variant="outline"
    onClick={handleClear}
    disabled={!totalEntries}
    >
    <Trash2 className="mr-2 h-4 w-4" />
    Clear
    </Button>
    <div className="flex min-w-[220px] max-w-xl flex-1 flex-row gap-2">
    <div className="relative min-w-0 flex-1">
    <Search className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
    <Input
    value={searchTerm}
    onChange={(event) => setSearchTerm(event.target.value)}
    placeholder="Search payload, channel, status"
    className="pl-9"
    />
    </div>
    <div className="relative min-w-0 flex-1">
    <Filter className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
    <Input
    value={channelFilter}
    onChange={(event) => setChannelFilter(event.target.value)}
    placeholder="Filter by channel"
    className="pl-9"
    list="debug-ipc-channels"
    />
    {availableChannels.length > 0 && (
    <datalist id="debug-ipc-channels">
    {availableChannels.map((channel) => (
    <option key={channel} value={channel} />
    ))}
    </datalist>
    )}
    </div>
    </div>
    </div>
    </div>
    </div>

    <Separator />

    <ScrollArea type="always" className="h-full max-h-[35vh] w-full">
    <div className="space-y-3 pr-4">
    {filteredEntries.length === 0 ? (
    <div className="border-border/60 bg-muted/20 flex h-40 flex-col items-center justify-center gap-3 rounded-xl border border-dashed text-center">
    <p className="font-medium">No IPC entries to display</p>
    <p className="text-muted-foreground text-sm">
    Adjust filters or wait for new IPC messages to be captured.
    </p>
    </div>
    ) : (
    <>
    <div className="flex items-center justify-end gap-2">
    <div className="text-muted-foreground mr-2 text-xs">
    Showing {Math.min(visibleCount, reversedFiltered.length)} of{" "}
    {reversedFiltered.length}
    </div>
    {reversedFiltered.length > visibleCount && (
    <Button size="sm" variant="outline" onClick={loadMore}>
    Load more
    </Button>
    )}
    {reversedFiltered.length > 0 &&
    reversedFiltered.length !== visibleCount && (
    <Button size="sm" variant="ghost" onClick={showAll}>
    Show all
    </Button>
    )}
    {visibleCount !== DEFAULT_WINDOW_SIZE && (
    <Button size="sm" variant="outline" onClick={resetWindow}>
    Reset
    </Button>
    )}
    </div>

    {visibleEntries.map((entry) => (
    <IpcEntry
    key={entry.id}
    entry={entry}
    onCopy={onCopy}
    searchTerm={searchTerm}
    isLogRedactionEnabled={isLogRedactionEnabled}
    />
    ))}
    </>
    )}
    </div>
    </ScrollArea>

    {/* Clipboard fallback modal */}
    <Dialog
    open={isClipboardFallbackOpen}
    onOpenChange={setIsClipboardFallbackOpen}
    >
    <DialogContent className="max-h-96 max-w-2xl">
    <DialogHeader>
    <DialogTitle>Copy IPC Entry (Clipboard Unavailable)</DialogTitle>
    <DialogDescription>
    Your system clipboard is not accessible. Select all text below and
    copy it manually, or click &quot;Select All&quot; to prepare for
    copying.
    </DialogDescription>
    </DialogHeader>
    <Textarea
    readOnly
    value={clipboardFallbackText}
    className="max-h-64 min-h-48 font-mono text-xs"
    onFocus={(event) => event.currentTarget.select()}
    />
    <DialogFooter className="flex gap-2">
    <Button
    variant="outline"
    onClick={() => {
    const textarea = document.querySelector(
    "textarea[readonly]",
    ) as HTMLTextAreaElement;
    if (textarea) {
    textarea.select();
    textarea.focus();
    }
    }}
    >
    <Check className="mr-2 h-4 w-4" />
    Select All
    </Button>
    <Button
    variant="default"
    onClick={() => setIsClipboardFallbackOpen(false)}
    >
    Close
    </Button>
    </DialogFooter>
    </DialogContent>
    </Dialog>
    </div>
    );
    }