feat: setup and cleanup script (#80)
fix setup error messages feat: codex error handling
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -58,6 +59,9 @@ type PermissionNotification struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// defaultStderrBufferSize is the number of recent stderr lines to keep for error context
|
||||
const defaultStderrBufferSize = 50
|
||||
|
||||
// Manager manages the agent subprocess
|
||||
type Manager struct {
|
||||
cfg *config.InstanceConfig
|
||||
@@ -72,6 +76,10 @@ type Manager struct {
|
||||
exitCode atomic.Int32
|
||||
exitErr atomic.Value // error
|
||||
|
||||
// Stderr buffering for error context
|
||||
stderrBuffer []string
|
||||
stderrMu sync.RWMutex
|
||||
|
||||
// Workspace tracker for git status and file changes
|
||||
workspaceTracker *WorkspaceTracker
|
||||
|
||||
@@ -386,7 +394,10 @@ func (m *Manager) createAdapter() error {
|
||||
case agent.ProtocolACP:
|
||||
m.adapter = adapter.NewACPAdapter(m.adapterCfg, m.logger)
|
||||
case agent.ProtocolCodex:
|
||||
m.adapter = adapter.NewCodexAdapter(m.adapterCfg, m.logger)
|
||||
codexAdapter := adapter.NewCodexAdapter(m.adapterCfg, m.logger)
|
||||
// Set stderr provider so errors can include recent stderr output
|
||||
codexAdapter.SetStderrProvider(m)
|
||||
m.adapter = codexAdapter
|
||||
default:
|
||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
||||
}
|
||||
@@ -528,6 +539,9 @@ func (m *Manager) readStderr() {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
m.logger.Debug("agent stderr", zap.String("line", line))
|
||||
|
||||
// Buffer the line for error context
|
||||
m.appendStderr(line)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -535,6 +549,46 @@ func (m *Manager) readStderr() {
|
||||
}
|
||||
}
|
||||
|
||||
// ansiEscapeRegex matches ANSI escape sequences
|
||||
var ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
|
||||
|
||||
// stripANSI removes ANSI escape codes from a string
|
||||
func stripANSI(s string) string {
|
||||
return ansiEscapeRegex.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
// appendStderr adds a line to the stderr ring buffer
|
||||
func (m *Manager) appendStderr(line string) {
|
||||
m.stderrMu.Lock()
|
||||
defer m.stderrMu.Unlock()
|
||||
|
||||
// Strip ANSI escape codes for cleaner display
|
||||
cleanLine := stripANSI(line)
|
||||
|
||||
if len(m.stderrBuffer) >= defaultStderrBufferSize {
|
||||
// Ring buffer: drop oldest line
|
||||
m.stderrBuffer = m.stderrBuffer[1:]
|
||||
}
|
||||
m.stderrBuffer = append(m.stderrBuffer, cleanLine)
|
||||
}
|
||||
|
||||
// GetRecentStderr returns a copy of the recent stderr lines
|
||||
func (m *Manager) GetRecentStderr() []string {
|
||||
m.stderrMu.RLock()
|
||||
defer m.stderrMu.RUnlock()
|
||||
|
||||
result := make([]string, len(m.stderrBuffer))
|
||||
copy(result, m.stderrBuffer)
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearStderrBuffer clears the stderr buffer (e.g., after successful operation)
|
||||
func (m *Manager) ClearStderrBuffer() {
|
||||
m.stderrMu.Lock()
|
||||
defer m.stderrMu.Unlock()
|
||||
m.stderrBuffer = nil
|
||||
}
|
||||
|
||||
// waitForExit waits for the process to exit
|
||||
func (m *Manager) waitForExit() {
|
||||
defer m.wg.Done()
|
||||
|
||||
@@ -76,3 +76,76 @@ func TestProcessRunnerCapturesOutput(t *testing.T) {
|
||||
}
|
||||
t.Fatal("process output not captured in time")
|
||||
}
|
||||
|
||||
func TestStripANSI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no ANSI codes",
|
||||
input: "hello world",
|
||||
expected: "hello world",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "simple color code",
|
||||
input: "\x1b[31mred text\x1b[0m",
|
||||
expected: "red text",
|
||||
},
|
||||
{
|
||||
name: "bold text",
|
||||
input: "\x1b[1mbold\x1b[0m",
|
||||
expected: "bold",
|
||||
},
|
||||
{
|
||||
name: "multiple colors",
|
||||
input: "\x1b[31mred\x1b[32mgreen\x1b[34mblue\x1b[0m",
|
||||
expected: "redgreenblue",
|
||||
},
|
||||
{
|
||||
name: "256 color code",
|
||||
input: "\x1b[38;5;196mcolored\x1b[0m",
|
||||
expected: "colored",
|
||||
},
|
||||
{
|
||||
name: "RGB color code",
|
||||
input: "\x1b[38;2;255;0;0mred\x1b[0m",
|
||||
expected: "red",
|
||||
},
|
||||
{
|
||||
name: "cursor movement",
|
||||
input: "\x1b[2Amove up\x1b[3Bmove down",
|
||||
expected: "move upmove down",
|
||||
},
|
||||
{
|
||||
name: "clear line",
|
||||
input: "text\x1b[2Kcleared",
|
||||
expected: "textcleared",
|
||||
},
|
||||
{
|
||||
name: "real world npm output",
|
||||
input: "\x1b[32m✓\x1b[39m \x1b[90mCompiled successfully\x1b[39m",
|
||||
expected: "✓ Compiled successfully",
|
||||
},
|
||||
{
|
||||
name: "mixed with newlines",
|
||||
input: "\x1b[31mError:\x1b[0m\nSomething failed\n\x1b[33mWarning:\x1b[0m check logs",
|
||||
expected: "Error:\nSomething failed\nWarning: check logs",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := stripANSI(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("stripANSI(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user