-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
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:
- 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.
- Frontend not displaying Claude Code streaming output despite working backend #71 describes the same symptoms (infinite spinner, no output) on macOS due to missing event listeners. The Windows root cause is different and lies deeper in the process I/O layer.
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 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.
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:
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:initmessage 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
- Unlike the WSL bridge in Windows Support for Claudia - Community Fix Available 4.2 FINAL #78, this requires no additional tooling or WSL installation
Additional fixes found during investigation
| Issue | Fix | Status |
|---|---|---|
tauri dev fails: "cannot determine which binary to run" |
Add default-run = "opcode" to Cargo.toml |
Fixed |
thinking content blocks not visible in new entries |
displayableMessages filter was missing content.type === "thinking" |
Fixed |
| Spurious loading spinner cycles at session init | loadSessionHistory silenced during mount; completion guard prevents double-firing |
Partially mitigated — minor cosmetic flicker may still occur, low-priority |
Environment
- Windows 11 Pro, Claude Code 2.1.72
- Opcode 0.2.1, built from source