wraith/internal/ai/terminal_buffer.go
Vantz Stockwell 7ee5321d69
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
feat: AI copilot backend — OAuth PKCE, Claude API streaming, 16 tools, conversations
- OAuth PKCE flow for Max subscription auth (no API key needed)
- Claude API client with SSE streaming (Messages API v1)
- 16 tool definitions: terminal, SFTP, RDP, session management
- Tool dispatch router mapping to existing Wraith services
- Conversation manager with SQLite persistence
- Terminal output ring buffer for AI context
- RDP screenshot encoder (RGBA → JPEG with downscaling)
- Wired into Wails app as AIService

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:09:23 -04:00

102 lines
2.3 KiB
Go

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)
}