package ai import ( "strings" "sync" ) // TerminalBuffer is a thread-safe ring buffer that captures terminal output lines. // It is written to by SSH read loops and read by the AI tool dispatch for terminal_read. type TerminalBuffer struct { lines []string mu sync.RWMutex max int partial string // accumulates data that doesn't end with \n } // NewTerminalBuffer creates a buffer that retains at most maxLines lines. func NewTerminalBuffer(maxLines int) *TerminalBuffer { if maxLines <= 0 { maxLines = 200 } return &TerminalBuffer{ lines: make([]string, 0, maxLines), max: maxLines, } } // Write ingests raw terminal output, splitting on newlines and appending complete lines. // Partial lines (data without a trailing newline) are accumulated until the next Write. func (b *TerminalBuffer) Write(data []byte) { b.mu.Lock() defer b.mu.Unlock() text := b.partial + string(data) b.partial = "" parts := strings.Split(text, "\n") // The last element of Split is always either: // - empty string if text ends with \n (discard it) // - a partial line if text doesn't end with \n (save as partial) last := parts[len(parts)-1] parts = parts[:len(parts)-1] if last != "" { b.partial = last } for _, line := range parts { b.lines = append(b.lines, line) } // Trim to max if len(b.lines) > b.max { excess := len(b.lines) - b.max b.lines = b.lines[excess:] } } // ReadLast returns the last n lines from the buffer. // If fewer than n lines are available, all lines are returned. func (b *TerminalBuffer) ReadLast(n int) []string { b.mu.RLock() defer b.mu.RUnlock() total := len(b.lines) if n > total { n = total } if n <= 0 { return []string{} } result := make([]string, n) copy(result, b.lines[total-n:]) return result } // ReadAll returns all lines currently in the buffer. func (b *TerminalBuffer) ReadAll() []string { b.mu.RLock() defer b.mu.RUnlock() result := make([]string, len(b.lines)) copy(result, b.lines) return result } // Clear removes all lines from the buffer. func (b *TerminalBuffer) Clear() { b.mu.Lock() defer b.mu.Unlock() b.lines = b.lines[:0] b.partial = "" } // Len returns the number of complete lines in the buffer. func (b *TerminalBuffer) Len() int { b.mu.RLock() defer b.mu.RUnlock() return len(b.lines) }