agent-orchestrator/backend/internal/adapters/runtime/conpty/ring.go

84 lines
2.3 KiB
Go

package conpty
import (
"strings"
"sync"
)
// MaxOutputLines is the rolling line-buffer cap, matching MAX_OUTPUT_LINES in pty-host.ts.
const MaxOutputLines = 1000
// Ring is a bounded rolling buffer of terminal output lines, ANSI codes preserved.
// It mirrors the appendOutput state machine from pty-host.ts.
// Concurrent Append and Snapshot/Tail calls are safe.
type Ring struct {
mu sync.Mutex
lines []string // each entry is "line\n" (or bare text on FlushPartial)
partialLine string
}
// NewRing returns an empty Ring.
func NewRing() *Ring {
return &Ring{}
}
// Append mirrors appendOutput from pty-host.ts: prepend the current partialLine,
// split on newlines, store completed lines with "\n" re-appended, keep the last
// element as the new partialLine, then trim to MaxOutputLines.
func (r *Ring) Append(raw []byte) {
r.mu.Lock()
defer r.mu.Unlock()
text := r.partialLine + string(raw)
parts := strings.Split(text, "\n")
// The last element is either "" (text ended with \n) or an incomplete line.
r.partialLine = parts[len(parts)-1]
for _, line := range parts[:len(parts)-1] {
r.lines = append(r.lines, line+"\n")
}
if len(r.lines) > MaxOutputLines {
// ponytail: slice off the head; ceiling: O(n) copy on every trim cycle.
// Upgrade path: circular buffer if trim rate is very high.
r.lines = r.lines[len(r.lines)-MaxOutputLines:]
}
}
// FlushPartial pushes any in-progress partial line as a final entry.
// Called on PTY exit to mirror the pty-host.ts onExit handler.
func (r *Ring) FlushPartial() {
r.mu.Lock()
defer r.mu.Unlock()
if r.partialLine == "" {
return
}
r.lines = append(r.lines, r.partialLine)
r.partialLine = ""
}
// Snapshot returns all stored lines concatenated as raw bytes for scrollback replay.
// The in-progress partialLine is NOT included (matches TS outputBuffer.join("")).
func (r *Ring) Snapshot() []byte {
r.mu.Lock()
defer r.mu.Unlock()
return []byte(strings.Join(r.lines, ""))
}
// Tail returns the last n stored lines joined as a string.
// Mirrors the MSG_GET_OUTPUT_REQ handler: start = max(0, len-lines).
// n <= 0 returns "".
func (r *Ring) Tail(n int) string {
r.mu.Lock()
defer r.mu.Unlock()
if n <= 0 {
return ""
}
start := len(r.lines) - n
if start < 0 {
start = 0
}
return strings.Join(r.lines[start:], "")
}