152 lines
7.0 KiB
Go
152 lines
7.0 KiB
Go
// Package process provides background process execution and output streaming for agentctl.
|
|
//
|
|
// InteractiveRunner extends the pattern from ProcessRunner to support interactive
|
|
// CLI passthrough sessions where users interact directly with agent CLIs through
|
|
// a PTY-backed terminal.
|
|
|
|
package process
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/agentctl/types"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// InteractiveStartRequest contains parameters for starting an interactive passthrough process.
|
|
type InteractiveStartRequest struct {
|
|
SessionID string `json:"session_id"` // Required: Agent session owning this process
|
|
Command []string `json:"command"` // Required: Command and args to execute
|
|
WorkingDir string `json:"working_dir"` // Working directory
|
|
Env map[string]string `json:"env,omitempty"` // Additional environment variables
|
|
PromptPattern string `json:"prompt_pattern,omitempty"` // Regex pattern to detect agent prompt for turn completion
|
|
IdleTimeout time.Duration `json:"idle_timeout,omitempty"` // Idle timeout for turn detection
|
|
BufferMaxBytes int64 `json:"buffer_max_bytes,omitempty"` // Max output buffer size
|
|
StatusDetector string `json:"status_detector,omitempty"` // Status detector type: "claude_code", "codex", ""
|
|
CheckInterval time.Duration `json:"check_interval,omitempty"` // How often to check state (default 100ms)
|
|
StabilityWindow time.Duration `json:"stability_window,omitempty"` // State stability window (default 0)
|
|
ImmediateStart bool `json:"immediate_start,omitempty"` // Start immediately with default dimensions (don't wait for resize)
|
|
DefaultCols int `json:"default_cols,omitempty"` // Default columns if ImmediateStart (default 120)
|
|
DefaultRows int `json:"default_rows,omitempty"` // Default rows if ImmediateStart (default 40)
|
|
InitialCommand string `json:"initial_command,omitempty"` // Command to write to stdin after shell starts (for script terminals)
|
|
DisableTurnDetection bool `json:"disable_turn_detection,omitempty"` // Disable idle timer and turn detection (for user shell terminals)
|
|
IsUserShell bool `json:"is_user_shell,omitempty"` // Mark as user shell process (excluded from session-level lookups)
|
|
}
|
|
|
|
// InteractiveProcessInfo represents the state of an interactive process.
|
|
type InteractiveProcessInfo struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
Command []string `json:"command"`
|
|
WorkingDir string `json:"working_dir"`
|
|
Status types.ProcessStatus `json:"status"`
|
|
ExitCode *int `json:"exit_code,omitempty"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Output []ProcessOutputChunk `json:"output,omitempty"`
|
|
}
|
|
|
|
// DirectOutputWriter is a writer that receives raw PTY output.
|
|
// When set, output bypasses the event bus and goes directly to this writer.
|
|
type DirectOutputWriter interface {
|
|
io.Writer
|
|
io.Closer
|
|
}
|
|
|
|
// TurnCompleteCallback is called when turn detection determines the agent is waiting for input.
|
|
type TurnCompleteCallback func(sessionID string)
|
|
|
|
// OutputCallback is called when process output is received.
|
|
// Used when running without a WorkspaceTracker (e.g., standalone passthrough mode).
|
|
type OutputCallback func(output *types.ProcessOutput)
|
|
|
|
// StatusCallback is called when process status changes.
|
|
// Used when running without a WorkspaceTracker (e.g., standalone passthrough mode).
|
|
type StatusCallback func(status *types.ProcessStatusUpdate)
|
|
|
|
// AgentStateCallback is called when agent TUI state changes (working, waiting, etc.).
|
|
type AgentStateCallback func(sessionID string, state AgentState)
|
|
|
|
// sessionWebSocket tracks a WebSocket connection at the session level.
|
|
// This allows the WebSocket to survive process restarts.
|
|
type sessionWebSocket struct {
|
|
writer DirectOutputWriter
|
|
lastCols uint16
|
|
lastRows uint16
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// userShellEntry tracks a user shell with its metadata.
|
|
type userShellEntry struct {
|
|
ProcessID string
|
|
Label string // Display name (e.g., "Terminal" or "Terminal 2")
|
|
InitialCommand string // Command that was run when shell started (empty for plain shells)
|
|
Closable bool // Whether the terminal can be closed (first terminal is not closable)
|
|
CreatedAt time.Time // When the shell was created (for stable ordering)
|
|
}
|
|
|
|
// InteractiveRunner manages interactive PTY-based processes with stdin support.
|
|
type InteractiveRunner struct {
|
|
logger *logger.Logger
|
|
workspaceTracker *WorkspaceTracker
|
|
bufferMaxBytes int64
|
|
turnCompleteCallback TurnCompleteCallback
|
|
outputCallback OutputCallback
|
|
statusCallback StatusCallback
|
|
stateCallback AgentStateCallback
|
|
|
|
mu sync.RWMutex
|
|
processes map[string]*interactiveProcess
|
|
|
|
// Session-level WebSocket tracking - survives process restarts
|
|
sessionWsMu sync.RWMutex
|
|
sessionWs map[string]*sessionWebSocket
|
|
|
|
// User shell processes - key: "sessionId:terminalId"
|
|
userShellsMu sync.RWMutex
|
|
userShells map[string]*userShellEntry
|
|
}
|
|
|
|
// NewInteractiveRunner creates a new interactive process runner.
|
|
func NewInteractiveRunner(workspaceTracker *WorkspaceTracker, log *logger.Logger, bufferMaxBytes int64) *InteractiveRunner {
|
|
return &InteractiveRunner{
|
|
logger: log.WithFields(zap.String("component", "interactive-runner")),
|
|
workspaceTracker: workspaceTracker,
|
|
bufferMaxBytes: bufferMaxBytes,
|
|
processes: make(map[string]*interactiveProcess),
|
|
sessionWs: make(map[string]*sessionWebSocket),
|
|
userShells: make(map[string]*userShellEntry),
|
|
}
|
|
}
|
|
|
|
// SetTurnCompleteCallback sets the callback to invoke when turn detection fires.
|
|
func (r *InteractiveRunner) SetTurnCompleteCallback(cb TurnCompleteCallback) {
|
|
r.turnCompleteCallback = cb
|
|
}
|
|
|
|
// SetOutputCallback sets the callback to invoke when process output is received.
|
|
// This is used when running without a WorkspaceTracker.
|
|
func (r *InteractiveRunner) SetOutputCallback(cb OutputCallback) {
|
|
r.outputCallback = cb
|
|
}
|
|
|
|
// SetStatusCallback sets the callback to invoke when process status changes.
|
|
// This is used when running without a WorkspaceTracker.
|
|
func (r *InteractiveRunner) SetStatusCallback(cb StatusCallback) {
|
|
r.statusCallback = cb
|
|
}
|
|
|
|
// SetStateCallback sets the callback to invoke when agent TUI state changes.
|
|
func (r *InteractiveRunner) SetStateCallback(cb AgentStateCallback) {
|
|
r.stateCallback = cb
|
|
}
|
|
|
|
// createStatusDetector creates a status detector for TUI state tracking.
|
|
// Currently always returns an idle detector that relies on the idle timer mechanism.
|
|
func createStatusDetector() StatusDetector {
|
|
return NewIdleDetector()
|
|
}
|