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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions