Skip to content

[Windows Native] Infinite spinner after Claude response — stdout pipe handle inheritance prevents EOF #447

@johanbrughmans

Description

@johanbrughmans

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 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:

  1. Reads stdout only until the system:init message arrives (captures the session ID)
  2. Intentionally drops the reader — breaking the deadlock caused by inherited handles
  3. Calls child.wait() to block until Claude actually exits
  4. 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

Related: #78, #71, #314

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions