Skip to main content
Glama
pty_manager.go10.9 kB
package aicoder import ( "fmt" "strings" "sync" "time" "github.com/google/uuid" ) // PTYManager manages multiple PTY sessions for AI coders type PTYManager struct { sessions map[string]*PTYSession activeSessions []string // Ordered list of session IDs currentSession string // Currently focused session mu sync.RWMutex // Brummer integration dataProvider BrummerDataProvider eventBus EventBus } // NewPTYManager creates a new PTY session manager func NewPTYManager(dataProvider BrummerDataProvider, eventBus EventBus) *PTYManager { return &PTYManager{ sessions: make(map[string]*PTYSession), dataProvider: dataProvider, eventBus: eventBus, } } // CreateSession creates a new PTY session func (pm *PTYManager) CreateSession(name, command string, args []string) (*PTYSession, error) { return pm.CreateSessionWithEnv(name, command, args, nil) } // CreateSessionWithEnv creates a new PTY session with additional environment variables func (pm *PTYManager) CreateSessionWithEnv(name, command string, args []string, extraEnv map[string]string) (*PTYSession, error) { pm.mu.Lock() defer pm.mu.Unlock() sessionID := uuid.New().String() session, err := NewPTYSessionWithEnv(sessionID, name, command, args, extraEnv) if err != nil { return nil, fmt.Errorf("failed to create PTY session: %w", err) } pm.sessions[sessionID] = session pm.activeSessions = append(pm.activeSessions, sessionID) // Set as current session if it's the first one if pm.currentSession == "" { pm.currentSession = sessionID } // Start monitoring the session go pm.monitorSession(session) // Emit session created event if pm.eventBus != nil { pm.eventBus.Emit("pty_session_created", map[string]interface{}{ "session_id": sessionID, "name": name, "command": command, }) } return session, nil } // GetSession retrieves a PTY session by ID func (pm *PTYManager) GetSession(sessionID string) (*PTYSession, bool) { pm.mu.RLock() defer pm.mu.RUnlock() session, exists := pm.sessions[sessionID] return session, exists } // GetCurrentSession returns the currently focused session func (pm *PTYManager) GetCurrentSession() (*PTYSession, bool) { pm.mu.RLock() defer pm.mu.RUnlock() if pm.currentSession == "" { return nil, false } session, exists := pm.sessions[pm.currentSession] return session, exists } // SetCurrentSession sets the currently focused session func (pm *PTYManager) SetCurrentSession(sessionID string) error { pm.mu.Lock() defer pm.mu.Unlock() if _, exists := pm.sessions[sessionID]; !exists { return fmt.Errorf("session %s does not exist", sessionID) } pm.currentSession = sessionID // Emit session focus event if pm.eventBus != nil { pm.eventBus.Emit("pty_session_focused", map[string]interface{}{ "session_id": sessionID, }) } return nil } // NextSession switches to the next session in the list func (pm *PTYManager) NextSession() (*PTYSession, error) { pm.mu.Lock() defer pm.mu.Unlock() if len(pm.activeSessions) == 0 { return nil, fmt.Errorf("no active sessions") } // Find current session index currentIndex := -1 for i, sessionID := range pm.activeSessions { if sessionID == pm.currentSession { currentIndex = i break } } // Move to next session (wrap around) nextIndex := (currentIndex + 1) % len(pm.activeSessions) pm.currentSession = pm.activeSessions[nextIndex] session := pm.sessions[pm.currentSession] // Emit session switch event if pm.eventBus != nil { pm.eventBus.Emit("pty_session_switched", map[string]interface{}{ "session_id": pm.currentSession, "direction": "next", }) } return session, nil } // PreviousSession switches to the previous session in the list func (pm *PTYManager) PreviousSession() (*PTYSession, error) { pm.mu.Lock() defer pm.mu.Unlock() if len(pm.activeSessions) == 0 { return nil, fmt.Errorf("no active sessions") } // Find current session index currentIndex := -1 for i, sessionID := range pm.activeSessions { if sessionID == pm.currentSession { currentIndex = i break } } // Move to previous session (wrap around) prevIndex := currentIndex - 1 if prevIndex < 0 { prevIndex = len(pm.activeSessions) - 1 } pm.currentSession = pm.activeSessions[prevIndex] session := pm.sessions[pm.currentSession] // Emit session switch event if pm.eventBus != nil { pm.eventBus.Emit("pty_session_switched", map[string]interface{}{ "session_id": pm.currentSession, "direction": "previous", }) } return session, nil } // CloseSession closes and removes a PTY session func (pm *PTYManager) CloseSession(sessionID string) error { pm.mu.Lock() defer pm.mu.Unlock() session, exists := pm.sessions[sessionID] if !exists { return fmt.Errorf("session %s does not exist", sessionID) } // Close the session if err := session.Close(); err != nil { return fmt.Errorf("failed to close session: %w", err) } // Remove from maps and lists delete(pm.sessions, sessionID) // Remove from active sessions list for i, id := range pm.activeSessions { if id == sessionID { pm.activeSessions = append(pm.activeSessions[:i], pm.activeSessions[i+1:]...) break } } // If this was the current session, switch to another one if pm.currentSession == sessionID { if len(pm.activeSessions) > 0 { pm.currentSession = pm.activeSessions[0] } else { pm.currentSession = "" } } // Emit session closed event if pm.eventBus != nil { pm.eventBus.Emit("pty_session_closed", map[string]interface{}{ "session_id": sessionID, }) } return nil } // ListSessions returns all active sessions func (pm *PTYManager) ListSessions() []*PTYSession { pm.mu.RLock() defer pm.mu.RUnlock() sessions := make([]*PTYSession, 0, len(pm.activeSessions)) for _, sessionID := range pm.activeSessions { if session, exists := pm.sessions[sessionID]; exists { sessions = append(sessions, session) } } return sessions } // InjectDataToCurrent injects data into the current session func (pm *PTYManager) InjectDataToCurrent(dataType DataInjectionType) error { session, exists := pm.GetCurrentSession() if !exists { return fmt.Errorf("no current session") } data, err := pm.getDataForInjection(dataType) if err != nil { return fmt.Errorf("failed to get data for injection: %w", err) } return session.InjectData(dataType, data) } // InjectDataToSession injects data into a specific session func (pm *PTYManager) InjectDataToSession(sessionID string, dataType DataInjectionType) error { session, exists := pm.GetSession(sessionID) if !exists { return fmt.Errorf("session %s does not exist", sessionID) } data, err := pm.getDataForInjection(dataType) if err != nil { return fmt.Errorf("failed to get data for injection: %w", err) } return session.InjectData(dataType, data) } // getDataForInjection retrieves data from Brummer based on injection type func (pm *PTYManager) getDataForInjection(dataType DataInjectionType) (interface{}, error) { if pm.dataProvider == nil { return nil, fmt.Errorf("no data provider available") } switch dataType { case DataInjectError, DataInjectLastError: return pm.dataProvider.GetLastError(), nil case DataInjectLogs: return pm.dataProvider.GetRecentLogs(10), nil case DataInjectTestFailure: return pm.dataProvider.GetTestFailures(), nil case DataInjectBuildOutput: return pm.dataProvider.GetBuildOutput(), nil case DataInjectProcessInfo: return pm.dataProvider.GetProcessInfo(), nil case DataInjectURLs: return pm.dataProvider.GetDetectedURLs(), nil case DataInjectProxyReq: return pm.dataProvider.GetRecentProxyRequests(5), nil default: return nil, fmt.Errorf("unsupported data type: %s", dataType) } } // monitorSession monitors a PTY session for events func (pm *PTYManager) monitorSession(session *PTYSession) { for event := range session.EventChan { // Handle session events switch event.Type { case PTYEventClose: // Auto-cleanup closed sessions pm.CloseSession(session.ID) case PTYEventOutput: // In debug mode, analyze output for triggers if session.IsDebugModeEnabled() { pm.analyzeOutputForAutoInjection(session, event.Data) } } // Forward events to main event bus if pm.eventBus != nil { pm.eventBus.Emit(string(event.Type), event) } } } // analyzeOutputForAutoInjection analyzes output for automatic data injection triggers func (pm *PTYManager) analyzeOutputForAutoInjection(session *PTYSession, data interface{}) { if outputBytes, ok := data.([]byte); ok { output := string(outputBytes) // Look for error patterns and automatically inject relevant data if containsErrorPattern(output) { // Auto-inject last error go func() { time.Sleep(100 * time.Millisecond) // Small delay to avoid race conditions session.InjectData(DataInjectLastError, pm.dataProvider.GetLastError()) }() } // Look for test failure patterns if containsTestFailurePattern(output) { go func() { time.Sleep(100 * time.Millisecond) session.InjectData(DataInjectTestFailure, pm.dataProvider.GetTestFailures()) }() } // Look for build failure patterns if containsBuildFailurePattern(output) { go func() { time.Sleep(100 * time.Millisecond) session.InjectData(DataInjectBuildOutput, pm.dataProvider.GetBuildOutput()) }() } } } // Helper functions for pattern matching func containsErrorPattern(output string) bool { errorPatterns := []string{ "error:", "Error:", "ERROR:", "failed", "Failed", "FAILED", "exception", "Exception", } for _, pattern := range errorPatterns { if strings.Contains(output, pattern) { return true } } return false } func containsTestFailurePattern(output string) bool { testPatterns := []string{ "test failed", "Test failed", "TEST FAILED", "FAIL:", "✗", "❌", } for _, pattern := range testPatterns { if strings.Contains(output, pattern) { return true } } return false } func containsBuildFailurePattern(output string) bool { buildPatterns := []string{ "build failed", "Build failed", "BUILD FAILED", "compilation error", "Compilation error", "compile error", } for _, pattern := range buildPatterns { if strings.Contains(output, pattern) { return true } } return false } // GetSessionCount returns the number of active sessions func (pm *PTYManager) GetSessionCount() int { pm.mu.RLock() defer pm.mu.RUnlock() return len(pm.activeSessions) } // CloseAllSessions closes all active sessions func (pm *PTYManager) CloseAllSessions() error { pm.mu.Lock() sessionIDs := make([]string, len(pm.activeSessions)) copy(sessionIDs, pm.activeSessions) pm.mu.Unlock() var lastError error for _, sessionID := range sessionIDs { if err := pm.CloseSession(sessionID); err != nil { lastError = err } } return lastError }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server