fix(windows): native ConPTY + kernel32 metrics + agent loop cap (v0.7.6)
All checks were successful
PR Check / check (pull_request) Successful in 1m0s

Three issues reported on Windows + one user-requested limit bump:

1. Dashboard CPU/RAM/Network all at 0
   handleSystemMetrics read /proc/* exclusively. Replaced with a
   platform-split:
   - metrics_unix.go (!windows): existing /proc reading code.
   - metrics_windows.go: kernel32!GetSystemTimes for CPU
     (delta of idle vs kernel+user FILETIMEs) and
     kernel32!GlobalMemoryStatusEx for memory. Network left at zero
     for now — MIB_IF_ROW2 is too version-sensitive to parse by hand.
   handlers_info.go::handleSystemMetrics reduced to one delegating
   call.

2. Terminal black screen on Windows
   creack/pty/v2 returns "unsupported" on Windows; the v0.7.1 pipe
   fallback works but pipes don't carry TTY signals, so cmd/pwsh/wsl
   go silent. Implemented native ConPTY:
   - terminal_conpty_windows.go: CreatePseudoConsole + STARTUPINFOEX
     + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE wiring via
     windows.NewProcThreadAttributeList. CreateProcessW launches
     child with the PC attached, full ANSI / line discipline /
     resize.
   - canUseConPTY() probes once at startup (Win10 1809+ check).
   - Restructure: terminal_session.go now holds just the interface
     + ptySession + pipeSession structs. terminal_session_unix.go
     wires creack/pty. terminal_session_windows.go tries ConPTY
     first, falls back to pipeSession.

3. Agent stops after 15 tool calls
   MaxToolIterations bumped 15 → 500. Doc comment explains why the
   cap exists at all (infinite-loop safety) and that 500 is well
   above realistic usage.

- internal/version/version.go: 0.7.5 → 0.7.6
- CHANGELOG.md: v0.7.6 entry covers the three fixes
This commit is contained in:
Muyue
2026-04-27 14:04:41 +02:00
parent 79e467c32a
commit d557b8e74c
10 changed files with 596 additions and 119 deletions

View File

@@ -2,19 +2,22 @@ package api
// Cross-platform terminal session abstraction.
//
// On Linux / macOS we have a real PTY via creack/pty: full TTY semantics,
// resize support, interactive apps (vim, top…) work. On Windows the same
// package returns "operating system not supported" at pty.Start time, so we
// fall back to plain pipes (stdin / stdout merged with stderr). Pipes don't
// give a real TTY — interactive TUIs misbehave — but `wsl`, `pwsh`, `cmd`,
// and most CLI tools emit usable line-buffered output, which is what the
// user actually clicks for.
// On Linux / macOS the unix-tagged file (terminal_session_unix.go) wires
// startTermSession to creack/pty for a real PTY: full TTY semantics,
// resize support, interactive apps (vim, top…) work.
//
// On Windows the windows-tagged file (terminal_session_windows.go) tries
// the kernel32 ConPTY API first, with a pipe-based fallback for older
// hosts. pipeSession does NOT carry TTY signals, so most shells go silent
// — it's only kept as a last resort.
//
// Both platforms share the termSession interface, the ptySession type
// (used by unix), and the pipeSession type (used by the Windows fallback).
import (
"io"
"os"
"os/exec"
"runtime"
"sync"
"github.com/creack/pty/v2"
@@ -30,22 +33,7 @@ type termSession interface {
Pid() int
}
// startTermSession tries a real PTY first; on Windows or any pty.Start failure
// it falls back to a pipe-based session.
func startTermSession(cmd *exec.Cmd) (termSession, error) {
if runtime.GOOS != "windows" {
ptmx, err := pty.Start(cmd)
if err == nil {
return &ptySession{ptmx: ptmx, cmd: cmd}, nil
}
// On unix, a pty.Start error is fatal — pipes won't help interactive
// shells without a TTY, and the unix build is the supported path.
return nil, err
}
return startPipeSession(cmd)
}
// ptySession wraps creack/pty's *os.File-backed PTY.
// ptySession wraps creack/pty's *os.File-backed PTY (unix path).
type ptySession struct {
ptmx *os.File
cmd *exec.Cmd
@@ -76,8 +64,10 @@ func (s *ptySession) Pid() int {
return s.cmd.Process.Pid
}
// pipeSession is the Windows fallback: stdin pipe + merged stdout/stderr pipe,
// running concurrently. Resize is a no-op (no TTY to send TIOCSWINSZ to).
// pipeSession is the Windows last-resort fallback when ConPTY is not
// available: stdin pipe + merged stdout/stderr, no TTY signals. Most
// interactive shells go silent in this mode, so it should rarely be hit on
// modern Windows (10 1809+).
type pipeSession struct {
cmd *exec.Cmd
stdin io.WriteCloser
@@ -115,7 +105,7 @@ func startPipeSession(cmd *exec.Cmd) (termSession, error) {
cmd: cmd,
stdin: stdin,
stdout: stdout,
stderr: stderr,
stderr: stderr,
merged: make(chan []byte, 32),
closeCh: make(chan struct{}),
}