You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Windows, Opcode's spinner never stops and Claude's response never appears after sending a prompt. This affects native Windows users (without WSL).
Relation to existing issues:
Windows Support for Claudia - Community Fix Available 4.2 FINAL #78 documents a community workaround using WSL as a bridge. That approach works, but requires WSL to be installed. This issue addresses the root cause so that Opcode works natively on Windows without WSL.
When Opcode spawns claude.exe, Claude in turn spawns child processes (Node.js workers, etc.) that inherit the stdout pipe write-handle. When Claude itself exits, those grandchild processes still hold the handle open. The stdout reader in spawn_claude_process never receives EOF — so claude-complete is never emitted and the spinner runs indefinitely.
This is a fundamental behavioral difference between Windows and Unix: on Unix, process groups and signals provide clean separation; on Windows, inherited handles persist until every process holding them exits.
Rather than a platform-specific workaround, we propose a Ports & Adapters pattern (hexagonal architecture) for session output handling. This separates the what (session lifecycle events) from the how (platform I/O mechanics), making the codebase cleanly extensible to other platforms:
SessionOutputAdapter (trait / port)
├── StdoutAdapter → Unix: full real-time streaming (existing behavior, unchanged)
└── FileWatchAdapter → Windows: reads stdout until session init, then defers to child.wait()
FileWatchAdapter (Windows) behaviour:
Reads stdout only until the system:init message arrives (captures the session ID)
Intentionally drops the reader — breaking the deadlock caused by inherited handles
Calls child.wait() to block until Claude actually exits
Emits SessionEvent::Complete — frontend reloads the full conversation from the JSONL file Claude already writes to ~/.claude/projects/.../session.jsonl
Platform selection is compile-time via #[cfg(windows)] / #[cfg(not(windows))] — zero impact on Unix builds.
Why this approach:
The Unix path is completely unchanged — no regression risk for macOS/Linux users
The adapter boundary makes future platform support straightforward to extend
Claude already writes the authoritative conversation to disk — reading from JSONL on completion is reliable and complete
Problem
On Windows, Opcode's spinner never stops and Claude's response never appears after sending a prompt. This affects native Windows users (without WSL).
Relation to existing issues:
Root cause
When Opcode spawns
claude.exe, Claude in turn spawns child processes (Node.js workers, etc.) that inherit the stdout pipe write-handle. When Claude itself exits, those grandchild processes still hold the handle open. The stdout reader inspawn_claude_processnever receives EOF — soclaude-completeis never emitted and the spinner runs indefinitely.This is a fundamental behavioral difference between Windows and Unix: on Unix, process groups and signals provide clean separation; on Windows, inherited handles persist until every process holding them exits.
Proposed solution — Platform-Abstracted Output Adapter
Rather than a platform-specific workaround, we propose a Ports & Adapters pattern (hexagonal architecture) for session output handling. This separates the what (session lifecycle events) from the how (platform I/O mechanics), making the codebase cleanly extensible to other platforms:
FileWatchAdapter(Windows) behaviour:system:initmessage arrives (captures the session ID)child.wait()to block until Claude actually exitsSessionEvent::Complete— frontend reloads the full conversation from the JSONL file Claude already writes to~/.claude/projects/.../session.jsonlPlatform selection is compile-time via
#[cfg(windows)]/#[cfg(not(windows))]— zero impact on Unix builds.Why this approach:
Additional fixes found during investigation
tauri devfails: "cannot determine which binary to run"default-run = "opcode"toCargo.tomlthinkingcontent blocks not visible in new entriesdisplayableMessagesfilter was missingcontent.type === "thinking"loadSessionHistorysilenced during mount; completion guard prevents double-firingEnvironment
Related: #78, #71, #314