KAOS/code/internal/server/pty.go
djuka 003650df24 T24: PTY za konzolu i operater chat — real-time streaming
Konzola i operater chat sada koriste pseudo-terminal (PTY) umesto
pipe-a. Claude CLI detektuje terminal i šalje output odmah umesto
da bufferuje. ANSI escape sekvence se uklanjaju pre slanja kroz SSE.

Novi fajl: pty.go (startPTY, readPTY, stripAnsi)
Biblioteka: github.com/creack/pty v1.1.24
5 novih testova za PTY funkcionalnost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:13:13 +00:00

72 lines
1.6 KiB
Go

package server
import (
"io"
"os"
"os/exec"
"regexp"
"github.com/creack/pty"
)
// ansiRegex matches ANSI escape sequences for stripping terminal formatting.
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[[\?0-9;]*[a-zA-Z]`)
// stripAnsi removes ANSI escape codes from a string.
func stripAnsi(s string) string {
return ansiRegex.ReplaceAllString(s, "")
}
// startPTY starts a command in a pseudo-terminal and returns the PTY master fd.
// The caller is responsible for closing the returned *os.File.
func startPTY(cmd *exec.Cmd) (*os.File, error) {
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 40, Cols: 120})
if err != nil {
return nil, err
}
return ptmx, nil
}
// readPTY reads from a PTY master and calls sendLine for each chunk of text.
// It splits on newlines so each SSE event is one line.
func readPTY(ptmx io.Reader, sendLine func(string)) {
buf := make([]byte, 4096)
var partial string
for {
n, err := ptmx.Read(buf)
if n > 0 {
text := partial + stripAnsi(string(buf[:n]))
partial = ""
// Split into lines, keep partial for next read
for {
idx := -1
for i, b := range []byte(text) {
if b == '\n' || b == '\r' {
idx = i
break
}
}
if idx < 0 {
partial = text
break
}
line := text[:idx]
// Skip empty lines from \r\n sequences
if line != "" {
sendLine(line)
}
// Skip past the newline character(s)
text = text[idx+1:]
}
}
if err != nil {
// Send remaining partial text
if partial != "" {
sendLine(partial)
}
break
}
}
}