Skip to content

feat: add Windows backend for dashboard PTY bridge - #21982

Closed
bgmbgm94 wants to merge 5 commits into
NousResearch:mainfrom
bgmbgm94:feat/windows-dashboard-winpty-backend
Closed

feat: add Windows backend for dashboard PTY bridge#21982
bgmbgm94 wants to merge 5 commits into
NousResearch:mainfrom
bgmbgm94:feat/windows-dashboard-winpty-backend

Conversation

@bgmbgm94

@bgmbgm94 bgmbgm94 commented May 8, 2026

Copy link
Copy Markdown

Summary

Draft stacked PR for the dashboard PTY Windows backend experiment.

Native Windows is now documented as an early-beta runtime, with the dashboard /chat embedded terminal pane called out as the major remaining native limitation. This PR targets that specific gap by wiring the existing pywinpty dependency from the pty extra into the existing dashboard api/pty bridge. It does not claim installer support, broad Windows support-policy changes, or production-ready browser chat support.

Stacking note: this branch is based on #21770, which keeps the dashboard/web_server importable when Unix PTY modules are missing. If #21770 changes or merges, this branch should be rebased before final review.

What changed

  • Refactor PtyBridge into a stable byte-oriented facade plus backend implementations.
  • Preserve the POSIX backend behavior behind _PosixPtyBackend.
  • Add _WindowsPtyBackend using winpty.ptyprocess.PtyProcess from pywinpty.
  • Keep the public PtyBridge contract used by api/pty:
    • read returns bytes, empty bytes, or None
    • write accepts bytes
    • resize accepts cols and rows
    • pid, is_alive, and close remain available
  • Add a unit test with a fake winpty process to verify Windows backend mapping:
    • spawn argv/cwd/env/dimensions
    • str read normalized to UTF-8 bytes
    • bytes write decoded for winpty
    • resize maps to setwinsize(rows, cols)
    • close is idempotent

Validation

Linux/WSL automated checks:

  • .venv/bin/python -m pytest tests/hermes_cli/test_pty_bridge.py tests/hermes_cli/test_web_server.py -q -o addopts=
    • 151 passed
  • .venv/bin/ruff check hermes_cli/pty_bridge.py tests/hermes_cli/test_pty_bridge.py tests/hermes_cli/test_web_server.py
    • All checks passed
  • git diff --check
    • passed

Native Windows smoke checks from WSL using the Windows Hermes venv:

  • Loaded branch hermes_cli.pty_bridge.py by UNC file path under native Windows Python.
  • winpty module available.
  • PtyBridge.is_available() returned true on native Windows.
  • api/pty in-process WebSocket smoke using branch hermes_cli.web_server.py and branch hermes_cli.pty_bridge.py:
    • cmd.exe /c echo API_PTY_WIN_OK spawned through the Windows backend.
    • WebSocket received API_PTY_WIN_OK as binary frames.
  • api/pty Node TTY probe smoke:
    • Node ran behind api/pty through winpty.
    • After sending minimal terminal responses for ESC[1t and ESC[c, frames contained:
      • stdin_isTTY=true
      • stdout_isTTY=true
      • platform=win32
    • no hermes-tui: no TTY output observed.
  • Cleanup check:
    • no leftover node.exe from smoke runs remained; only the pre-existing OpenClaw gateway node process was present.

Browser smoke evidence

Native Windows real-browser smoke now passes against the #21982 backend copy without modifying the live main install.

  • Runtime under test:
    • copied this PR branch into a disposable Windows temp directory
    • served dashboard on http://127.0.0.1:9120/chat with HERMES_WEB_DIST pointing at packaged dashboard assets
    • verified hermes_cli.pty_bridge and hermes_cli.web_server imported from the temp branch copy, not the live install
    • verified PtyBridge.is_available() returned true on win32
  • Real browser smoke:
    • Chrome / Playwright loaded /chat with HTTP 200
    • xterm rendered: .xterm count 1, helper textarea count 1, canvas count 3
    • /api/ws opened and session.create returned a session id
    • /api/pty opened, sent 1 frame, received 1 binary frame, and closed cleanly
    • no WSL/POSIX/no-TTY unavailable text was observed
  • Evidence artifacts were saved locally for review:
    • %LOCALAPPDATA%\Temp\hermes-pr21982-smoke-evidence\chat-smoke-result-rerun.json
    • %LOCALAPPDATA%\Temp\hermes-pr21982-smoke-evidence\chat-smoke-rerun.png

This removes the earlier browser-smoke caveat that only the narrower API PTY probe had passed. The PR should still remain draft until #21770 lands and this branch is rebased, because it is stacked on that import/fallback PR.

Limitations / follow-up

  • This is a backend bridge MVP for the documented native-Windows /chat gap, not a broad Windows support-policy or production-readiness claim.
  • pywinpty read is blocking and does not provide the same clean timeout behavior as the POSIX fd/select implementation. api/pty runs reads in an executor and close should unblock the child, but this deserves more manual browser/dashboard validation.
  • A full real ui-tui/dist/entry.js TestClient smoke hung in the harness and was manually cleaned up. The narrower api/pty Node TTY probe passed. Full browser xterm.js validation should be a follow-up before marking this ready.
  • The isolated WSL clone does not have ui-tui/dist or node_modules built; real TUI smoke used the native Windows install tree for evidence.

Related

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental native-Windows backend to the dashboard /api/pty bridge by refactoring PtyBridge into a byte-oriented facade over platform-specific backend implementations (POSIX via ptyprocess, Windows via pywinpty).

Changes:

  • Refactors hermes_cli.pty_bridge.PtyBridge into a backend-based design (_PosixPtyBackend / _WindowsPtyBackend) while preserving the public API.
  • Introduces a Windows backend using winpty.ptyprocess.PtyProcess and updates PTY availability checks accordingly.
  • Adds/updates tests for import fallback behavior and Windows backend adapter behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
hermes_cli/pty_bridge.py Refactors PTY bridge into backend seam and adds _WindowsPtyBackend implementation.
tests/hermes_cli/test_pty_bridge.py Adds a unit test for the Windows backend adapter mapping.
tests/hermes_cli/test_web_server.py Adds/extends tests around dashboard import fallback and PTY WebSocket close behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/pty_bridge.py Outdated
Comment on lines +11 to +13
platform-specific PTY details live behind a backend seam. The POSIX
backend is the only enabled backend today; Windows support can be added by
implementing the same byte-oriented contract.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — updated the module docstring in follow-up commit 18c75cb so it describes the current POSIX + native-Windows backend shape instead of the older POSIX-only future-work wording.

Comment thread hermes_cli/pty_bridge.py Outdated
Comment on lines +257 to +274
def read(self, timeout: float = 0.2) -> Optional[bytes]:
if self._closed:
return None
try:
data = self._proc.read()
except EOFError:
return None
except OSError as exc:
if exc.errno in (errno.EIO, errno.EBADF, errno.EPIPE):
return None
raise
if data is None:
return None
if data == "" or data == b"":
return b""
if isinstance(data, bytes):
return data
return str(data).encode("utf-8")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — addressed in follow-up commit 18c75cb. The Windows backend now isolates pywinpty's blocking read() in a per-bridge daemon reader thread and has PtyBridge.read(timeout) poll an internal queue, so the WebSocket executor path no longer calls the blocking winpty read directly.

Comment thread hermes_cli/pty_bridge.py Outdated
Comment on lines +398 to +401
* empty bytes (b empty) — no data available within timeout
* None — child has exited and the master fd is at EOF

Never blocks longer than timeout seconds. Safe to call after close;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — updated in follow-up commit 18c75cb. The read() docstring now uses the correct b"" wording and documents the bounded facade behavior with backend-specific blocking reads kept out of the caller path.

Comment on lines +182 to +187
class TestWindowsPtyBackendAdapter:
def test_spawn_and_io_normalize_winpty_text_api(self, monkeypatch):
import types

import hermes_cli.pty_bridge as pty_bridge

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in follow-up commit 18c75cb. The module-level pytest.importorskip('ptyprocess') gate is gone; POSIX tests are conditionally skipped, while the Windows adapter tests can run without ptyprocess.

Comment thread tests/hermes_cli/test_web_server.py Outdated
Comment on lines +1969 to +1974
ws = importlib.import_module("hermes_cli.web_server")

assert ws.app is not None
assert ws.PtyBridge.is_available() is False
with pytest.raises(ws.PtyUnavailableError):
ws.PtyBridge.spawn(["hermes"])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in follow-up commit 18c75cb. The import-fallback test now asserts dashboard importability first, and only expects PtyUnavailableError when no backend is available; native Windows with pywinpty may legitimately remain available.

@bgmbgm94
bgmbgm94 force-pushed the feat/windows-dashboard-winpty-backend branch from eab9ce0 to 985bbad Compare May 8, 2026 23:51
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard labels May 9, 2026
@teknium1

teknium1 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Hi @bgmbgm94 — thanks for kicking this off back in May. The ecosystem has moved on since: pywinpty is now a declared win32 dep in pyproject.toml (pywinpty>=2.0.0,<3; sys_platform == 'win32'), tools/process_registry.py already exercises the same ConPTY API in production, and the bridge can be much smaller (~180 LOC) by mirroring pty_bridge.py's public surface 1:1. Going with that approach in #42251 — live-tested end-to-end on Windows 11. Closing this as superseded; thanks for being the first to flag the gap.

Copy link
Copy Markdown
Author

Thanks for the explanation and for preserving the direction in #42251. Agreed that the smaller WinPtyBridge/ConPTY approach is the right path now, so I’m closing this draft as superseded.

@bgmbgm94 bgmbgm94 closed this Jun 11, 2026
@bgmbgm94

Copy link
Copy Markdown
Author

Thanks for the explanation and for preserving the direction in #42251. Agreed that the smaller WinPtyBridge/ConPTY approach is the right path now, so I’m closing this draft as superseded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants