Documentation
¶
Overview ¶
Package webui provides React web server with embedded assets
Agent-automate HTTP API.
Exposes workflow discovery, session tracking, and launch controls to the WebUI automate panel. Endpoints mirror the tool layer so the frontend can interact with automate workflows without going through the chat interface.
GET /api/automate/workflows — list available workflows GET /api/automate/sessions — list all automate sessions GET /api/automate/sessions/:id — single session detail POST /api/automate/run — launch a workflow POST /api/automate/sessions/:id/stop — stop a running workflow GET /api/automate/sessions/:id/output — read workflow output
Agent-changes HTTP API.
Exposes the ChangeTracker's session buffer to the WebUI as JSON endpoints. Each endpoint mirrors the LLM-facing tool of the same name and returns the same JSON shape — that way the frontend and the model agree on the data they're reasoning about.
GET /api/changes/session — current manifest (list_changes output) GET /api/changes/diff — unified diff for one file (show_my_change) GET /api/changes/summary — grouped activity-block digest (summarize_my_session) GET /api/changes/timeline — cross-session timeline (my_recent_changes) POST /api/changes/revert — bulk undo with scope (revert_my_changes)
All endpoints resolve the calling client's Agent via the existing client-context pattern so the panel reflects state for THAT browser session's agent (multi-client / multi-workspace safe).
Package webui provides React web server with embedded assets ¶
Package webui: shared helpers + list endpoint (split from chat_sessions_api.go)
Package webui: chat session creation (split from chat_sessions_api.go)
Package webui: chat session deletion (split from chat_sessions_api.go)
Package webui: chat session fork/breakpoints (split from chat_sessions_api.go)
Package webui: chat session rename/pin/unpin (split from chat_sessions_api.go)
Package webui: chat session switch/compact/clear-history (split from chat_sessions_api.go)
Package webui provides React web server with embedded assets ¶
Package webui provides the web-based user interface for sprout, including WebSocket communication, file watching, terminal management, and API endpoints.
File Watcher Shutdown Contract:
The fileWatcher (*fileWatcher) must be stopped via its stop() method when the server shuts down. The ReactWebServer.Shutdown() method calls fileWatcher.stop(), which cancels the internal context and closes the underlying fsnotify.Watcher. This ensures the event loop goroutine exits cleanly. Tests that instantiate a fileWatcher directly should call fw.stop() in a defer or t.Cleanup to prevent goroutine leaks.
Package webui provides the React web server with embedded assets.
UserConnections (SP-118 Phase 1) tracks multiple concurrent WebSocket connections per user for Mode 2 (daemon / sprout service). Mode 1 (sprout agent) does not use this type — it continues to use activeWSByUserID to enforce single-active-session semantics.
Concurrency: UserConnections uses a sync.RWMutex per user, lazily allocated. Read paths (Count, ForEach) take RLock; write paths (Add, Remove) take Lock. The user-index map itself is guarded by a single sync.RWMutex; the per-user slices (when they exist) are guarded by their own sync.RWMutex.
Package webui provides the WebSocket handler for rate-limited events.
The rate_limited event is published by the agent when a provider returns a RateLimitError. It flows through the eventBus → WebSocket subscription channel automatically. This file registers the event type in the outbound allow-list so the message is accepted by the outbound validator.
Package webui provides React web server with embedded assets ¶
Package webui provides React web server with embedded assets ¶
Package webui ... MCP server credential and OAuth management.
Index ¶
- Constants
- Variables
- func CheckPortAvailable(port int) bool
- func DisplayAddr(bindAddr string) string
- func FindAvailablePort(basePort int) (int, error)
- func FindNearestProjectRoot(startDir string) (string, []string)
- func GetHotkeysPath() (string, error)
- func GetMostRecentWorkspace() string
- func IsProjectDirectory(dir string) (bool, []string)
- func RecordWorkspace(path string)
- func RegisterOutboundMessageType(msgType string)
- func RegisterPendingEdit(id, path string, hunks []editHunkInfo, diff string) <-chan editDecisionPayload
- func RemovePendingEdit(id string)
- func SaveHotkeys(config *HotkeyConfig) error
- func UserIDFromContext(ctx context.Context) string
- func ValidateHotkeyConfig(config *HotkeyConfig) error
- func WaitForEditDecision(ch <-chan editDecisionPayload, timeout time.Duration) *editDecisionPayload
- type ActiveSessionRegistry
- func (r *ActiveSessionRegistry) DisconnectDevice(sessionID, deviceID string) bool
- func (r *ActiveSessionRegistry) GetActiveDevice(sessionID string) string
- func (r *ActiveSessionRegistry) RegisterConnection(sessionID, deviceID string) (takeoverPrompt bool, existingDeviceID string)
- func (r *ActiveSessionRegistry) RequestTakeover(sessionID, newDeviceID string) string
- type AskUserResponseData
- type BillingTypeBreakdown
- type ConnectionInfo
- type CostRecord
- type CostStore
- func (cs *CostStore) ForcePersist() error
- func (cs *CostStore) GetCostSummary(start, end time.Time) CostSummary
- func (cs *CostStore) GetDailyCosts(days int) []DailyCost
- func (cs *CostStore) GetSummary(startDate, endDate time.Time) (totalCost float64, byProvider map[string]float64, byModel map[string]float64)
- func (cs *CostStore) RecordCost(provider, model, sessionID, chatID string, promptTokens, outputTokens int, ...)
- func (cs *CostStore) RecordCostWithBilling(provider, model, sessionID, chatID, title, workingDir, billingType string, ...)
- func (cs *CostStore) RecordCostWithSession(provider, model, sessionID, chatID, title, workingDir string, ...)
- type CostSummary
- type DailyCost
- type DuplicateCluster
- type EmbeddingIndexStatus
- type GitCommit
- type GitFile
- type GitStatus
- type HotkeyConfig
- type HotkeyEntry
- type HydrateCompleteData
- type HydrateFileData
- type HydrateManifestData
- type HydrateRequestData
- type ModelChangeData
- type PasswordResponseData
- type PersonaChangeData
- type ProjectInfo
- type ProjectMarker
- type ProviderChangeData
- type ReactWebServer
- func (ws *ReactWebServer) ExtractUserID(r *http.Request) string
- func (ws *ReactWebServer) GetAskUserMgr() *agenttools.AskUserManager
- func (ws *ReactWebServer) GetDaemonRoot() string
- func (ws *ReactWebServer) GetPort() int
- func (ws *ReactWebServer) GetSecurityPromptMgr() *security.ApprovalManager
- func (ws *ReactWebServer) GetWorkspaceRoot() string
- func (ws *ReactWebServer) HandleContainerRecovery(ctx context.Context, clientID string, lastKnownSeq int64) (*SyncReconcileData, error)
- func (ws *ReactWebServer) HandleContainerRecoveryWithSeqs(ctx context.Context, clientID string, browserSeqs map[string]int64) (*SyncReconcileData, error)
- func (ws *ReactWebServer) HasActiveWebUIClients() bool
- func (ws *ReactWebServer) IsRunning() bool
- func (ws *ReactWebServer) IsSharedMode() bool
- func (ws *ReactWebServer) SendSyncReconcile(safeConn *SafeConn, data *SyncReconcileData) error
- func (ws *ReactWebServer) SendSyncReplayComplete(safeConn *SafeConn, clientID string) error
- func (ws *ReactWebServer) SendSyncReplayFile(safeConn *SafeConn, clientID, filePath, content string, seq int64) error
- func (ws *ReactWebServer) SendSyncReplayStart(safeConn *SafeConn, clientID string, fileCount int) error
- func (ws *ReactWebServer) SetAgentEnforceSingleSession(v bool)
- func (ws *ReactWebServer) SetWorkspaceRoot(path string) (string, error)
- func (ws *ReactWebServer) Shutdown() error
- func (ws *ReactWebServer) Start(ctx context.Context) error
- func (ws *ReactWebServer) SyncSharedAgentState(agentInst *agent.Agent) error
- type RecallItem
- type RecallResponse
- type RecentWorkspace
- type ReconciliationAction
- type ReplaceFileChange
- type ReplaceMatch
- type ReplaceRequest
- type ReplaceResponse
- type RuntimeConfig
- type SafeConn
- type SearchMatch
- type SearchResponse
- type SearchResult
- type SecurityApprovalResponseData
- type SecurityPromptResponseData
- type SemanticPreviewContextConfig
- type SemanticPreviewContextResponse
- type SemanticPreviewContextResult
- type SemanticPreviewResponse
- type SemanticSearchResponse
- type SemanticSearchResult
- type SessionCostRow
- type SessionOption
- type ShellInfo
- type SnippetLine
- type SubscribeData
- type SyncReconcileData
- type SyncRecoverData
- type SyncReplayFileData
- type TerminalManager
- func (tm *TerminalManager) AddToHistory(sessionID, command string) error
- func (tm *TerminalManager) AvailableShells() []ShellInfo
- func (tm *TerminalManager) CleanupInactiveSessions(timeout time.Duration, backgroundTimeout ...time.Duration)
- func (tm *TerminalManager) CloseAllSessions() error
- func (tm *TerminalManager) CloseSession(sessionID string) error
- func (tm *TerminalManager) CreateHiddenSession(id, owner, chatID string, opts ...SessionOption) (session *TerminalSession, err error)
- func (tm *TerminalManager) CreateSession(sessionID string, shellOverride ...string) (*TerminalSession, error)
- func (tm *TerminalManager) DetachFromSession(sessionID string) error
- func (tm *TerminalManager) ExecuteCommand(sessionID, command string) error
- func (tm *TerminalManager) ExecuteCommandAndWait(ctx context.Context, session *TerminalSession, command string) (output string, exitCode int, err error)
- func (tm *TerminalManager) ExecuteCommandInBackground(ctx context.Context, chatID, command string) (string, error)
- func (tm *TerminalManager) ExecuteCommandInHidden(ctx context.Context, sessionID, command string) (string, int, error)
- func (tm *TerminalManager) GetBackgroundOutput(sessionID string) (string, error)
- func (tm *TerminalManager) GetHistory(sessionID string) ([]string, error)
- func (tm *TerminalManager) GetOrCreateHiddenSessionForChat(ctx context.Context, chatID string) (string, error)
- func (tm *TerminalManager) GetSession(sessionID string) (*TerminalSession, bool)
- func (tm *TerminalManager) GetSessionCount() int
- func (tm *TerminalManager) GetTerminalSize(sessionID string) (*pty.Winsize, error)
- func (tm *TerminalManager) GetVisibleSessionCount() int
- func (tm *TerminalManager) HasSession(sessionID string) bool
- func (tm *TerminalManager) HasVisibleSession(sessionID string) bool
- func (tm *TerminalManager) IsSessionActive(sessionID string) bool
- func (tm *TerminalManager) ListAllSessions() []string
- func (tm *TerminalManager) ListHiddenSessions() []string
- func (tm *TerminalManager) ListSessions() []string
- func (tm *TerminalManager) NavigateHistory(sessionID string, direction string) (string, error)
- func (tm *TerminalManager) ReattachSession(sessionID string) (string, error)
- func (tm *TerminalManager) ResetHistoryIndex(sessionID string) error
- func (tm *TerminalManager) ResizeTerminal(sessionID string, rows, cols uint16) error
- func (tm *TerminalManager) SessionCount() int
- func (tm *TerminalManager) StartCleanupWorker(ctx context.Context, interval time.Duration, timeout time.Duration, ...)
- func (tm *TerminalManager) StopBackgroundSession(sessionID string) error
- func (tm *TerminalManager) WriteRawInput(sessionID, input string) error
- type TerminalSession
- type UserConnection
- type UserConnections
- func (uc *UserConnections) Add(userID string, c UserConnection)
- func (uc *UserConnections) AllUserIDs() []string
- func (uc *UserConnections) Count(userID string) int
- func (uc *UserConnections) ForEach(userID string, fn func(UserConnection) bool)
- func (uc *UserConnections) Remove(userID string, raw interface{})
- func (uc *UserConnections) Snapshot(userID string) []UserConnection
- type WebSocketMessage
- type WebUIError
- type WorktreeInfo
Constants ¶
const ( // AllowedMessageTypePing is the "ping" message type AllowedMessageTypePing = "ping" // AllowedMessageTypePong is the "pong" message type AllowedMessageTypePong = "pong" // AllowedMessageTypeHeartbeat is the "heartbeat" message type AllowedMessageTypeHeartbeat = "heartbeat" // AllowedMessageTypeSubscribe is the "subscribe" message type AllowedMessageTypeSubscribe = "subscribe" // AllowedMessageTypeRequestStats is the "request_stats" message type AllowedMessageTypeRequestStats = "request_stats" // AllowedMessageTypeProviderChange is the "provider_change" message type AllowedMessageTypeProviderChange = "provider_change" // AllowedMessageTypeModelChange is the "model_change" message type AllowedMessageTypeModelChange = "model_change" // AllowedMessageTypePersonaChange is the "persona_change" message type AllowedMessageTypePersonaChange = "persona_change" // AllowedMessageTypeSecurityApprovalResponse is the "security_approval_response" message type AllowedMessageTypeSecurityApprovalResponse = "security_approval_response" // AllowedMessageTypeSecurityPromptResponse is the "security_prompt_response" message type AllowedMessageTypeSecurityPromptResponse = "security_prompt_response" // AllowedMessageTypeAskUserResponse is the "ask_user_response" message type AllowedMessageTypeAskUserResponse = "ask_user_response" // AllowedMessageTypePasswordResponse is the "password_response" message type AllowedMessageTypePasswordResponse = "password_response" // AllowedMessageTypeSessionTakeover is the "session_takeover" message type (SP-046) AllowedMessageTypeSessionTakeover = "session_takeover" // AllowedMessageTypeHydrateRequest is the "hydrate_request" message type (SP-046) AllowedMessageTypeHydrateRequest = "hydrate_request" // AllowedMessageTypeSyncRecover is the "sync_recover" message type (SP-046) AllowedMessageTypeSyncRecover = "sync_recover" // AllowedMessageTypePause signals the tab is backgrounding but will return — // keep any in-flight query running in the background instead of cancelling // it on heartbeat staleness. AllowedMessageTypePause = "pause" // AllowedMessageTypeResume signals the tab is foregrounded again — clear the // paused state (a reconnect also clears it implicitly). AllowedMessageTypeResume = "resume" // AllowedMessageTypeSessionClose signals the tab is closing/navigating away — // cancel any in-flight query for this client now rather than waiting out the // heartbeat timeout. AllowedMessageTypeSessionClose = "session_close" // Outbound-only hydration message types (SP-046) — server→client AllowedMessageTypeHydrateManifest = "hydrate_manifest" AllowedMessageTypeHydrateFile = "hydrate_file" AllowedMessageTypeHydrateComplete = "hydrate_complete" )
const ( // DaemonPort is the unified fixed port used by all sprout daemons // (both local and SSH-launched remote). All daemons on a given host // share this port — the launcher detects an existing daemon and // reuses it rather than starting a duplicate. DaemonPort = 56000 )
Variables ¶
var ( ErrSessionNotFound = errors.New("session not found") ErrNotBackgroundSession = errors.New("not a background session") )
Sentinel errors returned by GetBackgroundOutput, used for HTTP status code mapping in the agent sessions API handlers.
var ErrNoProviderConfigured = errors.New("no AI provider configured")
ErrNoProviderConfigured is returned by getClientAgent and getChatAgent when no AI provider is configured (e.g., the user skipped onboarding and set LastUsedProvider to "editor").
var ErrOutboundDropped = errors.New("webui: outbound message type not in allowlist; dropped")
ErrOutboundDropped is returned by WriteJSON when the outbound allowlist (websocket_outbound_registry.go) rejected the message. The payload was NOT sent. Callers should check errors.Is(err, ErrOutboundDropped) before logging "successfully sent" — otherwise a missing allowlist entry produces silent drops that masquerade as successful writes (see terminal_websocket.go: pre-fix, every `session_created` and `output` frame was dropped while the handler logged successful sends, leaving the React terminal stuck on "Loading terminal…").
Transport-level failures (connection closed, network error) still surface via the underlying error, distinct from this sentinel.
var ErrSessionExists = errors.New("session already exists")
ErrSessionExists is returned by CreateSession and CreateHiddenSession when a session with the requested ID already exists. Callers can use errors.Is to detect this condition for idempotent get-or-create patterns.
Functions ¶
func CheckPortAvailable ¶
CheckPortAvailable checks if a port is available to bind to
func DisplayAddr ¶
DisplayAddr returns a user-friendly address string for display in logs.
func FindAvailablePort ¶
FindAvailablePort finds an available port starting from a base port
func FindNearestProjectRoot ¶
FindNearestProjectRoot walks up from startDir looking for project markers. Returns (projectRoot, markers) or ("", nil) if none found. Stops at the filesystem root.
func GetHotkeysPath ¶
GetHotkeysPath returns the path to the hotkeys configuration file
func GetMostRecentWorkspace ¶
func GetMostRecentWorkspace() string
GetMostRecentWorkspace returns the most recently used workspace path, or "" if none.
func IsProjectDirectory ¶
IsProjectDirectory checks if a directory appears to be a project root. Returns (isProject, markersFound). A directory is a project if it contains at least one marker with weight >= 50, or two+ markers with weight >= 30.
func RecordWorkspace ¶
func RecordWorkspace(path string)
RecordWorkspace records a workspace as recently used.
func RegisterOutboundMessageType ¶
func RegisterOutboundMessageType(msgType string)
RegisterOutboundMessageType lets tests and dynamic features (e.g. future plugin event types) add to the allow-list at runtime. Safe to call from any goroutine; idempotent. Doesn't take a mutex because the map is written at init+test-setup and read on the hot path — concurrent reads while writes happen would be unsafe, so the test fixture should register BEFORE the WS goroutines start.
func RegisterPendingEdit ¶ added in v0.16.12
func RegisterPendingEdit(id, path string, hunks []editHunkInfo, diff string) <-chan editDecisionPayload
RegisterPendingEdit creates a pending edit entry and returns a channel that blocks until the user submits a decision via the WebUI.
func RemovePendingEdit ¶ added in v0.16.12
func RemovePendingEdit(id string)
RemovePendingEdit cleans up a pending edit entry after it's resolved or timed out.
func SaveHotkeys ¶
func SaveHotkeys(config *HotkeyConfig) error
SaveHotkeys saves the hotkeys configuration to file
func UserIDFromContext ¶
UserIDFromContext retrieves the user ID from a context.
func ValidateHotkeyConfig ¶
func ValidateHotkeyConfig(config *HotkeyConfig) error
ValidateHotkeyConfig validates a hotkeys configuration
func WaitForEditDecision ¶ added in v0.16.12
WaitForEditDecision blocks until a decision is received or timeout elapses. Returns nil on timeout (treat as reject-all for safety).
Types ¶
type ActiveSessionRegistry ¶ added in v0.16.18
type ActiveSessionRegistry struct {
// contains filtered or unexported fields
}
ActiveSessionRegistry tracks which device currently holds the active session for a given user/session. Only one device may be active at a time; connecting from a second device triggers a takeover prompt.
func NewActiveSessionRegistry ¶ added in v0.16.18
func NewActiveSessionRegistry() *ActiveSessionRegistry
NewActiveSessionRegistry creates a new, empty registry.
func (*ActiveSessionRegistry) DisconnectDevice ¶ added in v0.16.18
func (r *ActiveSessionRegistry) DisconnectDevice(sessionID, deviceID string) bool
DisconnectDevice removes a device's session if it matches the currently active device. Returns true if the device was removed.
func (*ActiveSessionRegistry) GetActiveDevice ¶ added in v0.16.18
func (r *ActiveSessionRegistry) GetActiveDevice(sessionID string) string
GetActiveDevice returns the active device ID for a session, or "" if none.
func (*ActiveSessionRegistry) RegisterConnection ¶ added in v0.16.18
func (r *ActiveSessionRegistry) RegisterConnection(sessionID, deviceID string) (takeoverPrompt bool, existingDeviceID string)
RegisterConnection registers (or re-registers) a device for a session.
Returns (takeoverPrompt, existingDeviceID):
- If the session has no active device, the caller is registered and (false, "") is returned.
- If the session already has a different active device, (true, existingDeviceID) is returned so the caller can prompt the user to take over.
- If the same device is re-registering, (false, "") is returned (idempotent).
func (*ActiveSessionRegistry) RequestTakeover ¶ added in v0.16.18
func (r *ActiveSessionRegistry) RequestTakeover(sessionID, newDeviceID string) string
RequestTakeover atomically swaps the active device for a session to newDeviceID. Returns the old device ID that should be disconnected, or "" if no session was active.
type AskUserResponseData ¶
type AskUserResponseData struct {
RequestID string `json:"request_id"`
Response string `json:"response"`
}
AskUserResponseData is the data payload for "ask_user_response" messages.
func (*AskUserResponseData) Validate ¶
func (d *AskUserResponseData) Validate() error
Validate performs field-level validation on AskUserResponseData.
type BillingTypeBreakdown ¶ added in v0.16.19
BillingTypeBreakdown holds aggregated cost and token data for one billing model.
type ConnectionInfo ¶
type ConnectionInfo struct {
SessionID string // Unique session ID for this connection
ClientID string // WebUI client/window identifier
ChatID string // Chat session identifier (optional)
Type string // "webui" or "terminal"
UserID string // User ID extracted from trusted header (service mode)
ConnectedAt time.Time // When the connection was established
Conn *websocket.Conn // Underlying conn for registry lookups (SP-034-3c). Never written to directly; SafeConn owns the write path.
// SafeConn is the serialized write wrapper for this connection. Shared
// across all callers so cross-connection notifications (e.g., terminal
// displacement) use the same mutex as the owning handler goroutine,
// preventing concurrent-write panics. Populated by the handler that
// creates the connection.
SafeConn *SafeConn
// contains filtered or unexported fields
}
ConnectionInfo stores metadata about a WebSocket connection
type CostRecord ¶
type CostRecord struct {
Timestamp time.Time `json:"timestamp"`
Provider string `json:"provider"`
Model string `json:"model"`
PromptTokens int `json:"prompt_tokens"`
OutputTokens int `json:"output_tokens"`
Cost float64 `json:"cost"`
SessionID string `json:"session_id,omitempty"`
ChatID string `json:"chat_id,omitempty"`
// Optional session metadata populated at record time.
Title string `json:"title,omitempty"`
WorkingDir string `json:"working_dir,omitempty"`
LastUpdated string `json:"last_updated,omitempty"` // RFC3339 timestamp
// Billing-model-aware cost tracking (SP-080)
BillingType string `json:"billing_type,omitempty"`
ChargedCost float64 `json:"charged_cost,omitempty"`
TokenCost float64 `json:"token_cost,omitempty"`
}
CostRecord represents a single cost entry for an API request
type CostStore ¶
type CostStore struct {
// contains filtered or unexported fields
}
CostStore handles persisting and querying cost records
func GetCostStore ¶
func GetCostStore() *CostStore
GetCostStore returns the singleton cost store instance
func (*CostStore) ForcePersist ¶
ForcePersist forces immediate persistence (for graceful shutdown)
func (*CostStore) GetCostSummary ¶
func (cs *CostStore) GetCostSummary(start, end time.Time) CostSummary
GetCostSummary returns overall cost summary. When start and end are both zero, all records are included (all-time). When start/end are set, TopSessions is filtered to that range.
func (*CostStore) GetDailyCosts ¶
GetDailyCosts returns daily cost breakdown
func (*CostStore) GetSummary ¶
func (cs *CostStore) GetSummary(startDate, endDate time.Time) (totalCost float64, byProvider map[string]float64, byModel map[string]float64)
GetSummary returns cost summary for a date range
func (*CostStore) RecordCost ¶
func (cs *CostStore) RecordCost(provider, model, sessionID, chatID string, promptTokens, outputTokens int, cost float64)
RecordCost adds a new cost record
func (*CostStore) RecordCostWithBilling ¶ added in v0.16.19
type CostSummary ¶
type CostSummary struct {
TotalCost float64 `json:"total_cost"`
ByProvider map[string]float64 `json:"by_provider"`
ByModel map[string]float64 `json:"by_model"`
ByProviderThisMonth map[string]float64 `json:"by_provider_this_month"`
ByProviderLastMonth map[string]float64 `json:"by_provider_last_month"`
Last30Days float64 `json:"last_30_days"`
Last7Days float64 `json:"last_7_days"`
ThisMonth float64 `json:"this_month"`
LastMonth float64 `json:"last_month"`
TopSessions []SessionCostRow `json:"top_sessions"`
ByBillingType map[string]BillingTypeBreakdown `json:"by_billing_type,omitempty"`
ByProviderBillingType map[string]string `json:"by_provider_billing_type,omitempty"`
ChargedCost float64 `json:"charged_cost,omitempty"`
TokenValue float64 `json:"token_value,omitempty"`
// FirstActivity / LastActivity span all recorded records (not the
// requested time range), so the WebUI can show a "data is older than
// the current period" banner without re-fetching the raw history.
FirstActivity *time.Time `json:"first_activity,omitempty"`
LastActivity *time.Time `json:"last_activity,omitempty"`
}
CostSummary represents aggregated cost data
type DailyCost ¶
type DailyCost struct {
Date string `json:"date"`
TotalCost float64 `json:"total_cost"`
ByProvider map[string]float64 `json:"by_provider,omitempty"`
}
DailyCost represents cost for a single day
type DuplicateCluster ¶
type DuplicateCluster struct {
Files []string `json:"files"`
Similarity float32 `json:"similarity"` // average pairwise similarity
Count int `json:"count"` // number of results in cluster
}
DuplicateCluster represents a group of files that have highly similar code units.
type EmbeddingIndexStatus ¶
type EmbeddingIndexStatus struct {
Available bool `json:"available"` // whether embedding manager exists
Initialized bool `json:"initialized"` // whether embedding provider is initialized
Building bool `json:"building"` // whether an index build is in progress
RecordCount int `json:"record_count"` // number of indexed code units
Workspace string `json:"workspace"` // workspace root path
InitError string `json:"init_error,omitempty"` // error from failed initialization, if any
}
EmbeddingIndexStatus represents the current state of the embedding index.
type GitCommit ¶
type GitCommit struct {
Hash string `json:"hash"`
ShortHash string `json:"short_hash"`
Author string `json:"author"`
Date string `json:"date"`
Message string `json:"message"`
RefNames string `json:"ref_names,omitempty"`
}
GitCommit is a single commit entry in the git log response.
type GitFile ¶
type GitFile struct {
Path string `json:"path"`
Status string `json:"status"`
Staged bool `json:"staged,omitempty"`
}
GitFile represents a file with its git status
type GitStatus ¶
type GitStatus struct {
Branch string `json:"branch"`
Ahead int `json:"ahead"`
Behind int `json:"behind"`
Staged []GitFile `json:"staged"`
Modified []GitFile `json:"modified"`
Untracked []GitFile `json:"untracked"`
Deleted []GitFile `json:"deleted"`
Renamed []GitFile `json:"renamed"`
// Truncated indicates whether any file lists were truncated due to limits
Truncated bool `json:"truncated"`
InGitRepo bool `json:"in_git_repo"`
}
GitStatus represents the git status response
type HotkeyConfig ¶
type HotkeyConfig struct {
Version string `json:"version"`
Hotkeys []HotkeyEntry `json:"hotkeys"`
}
HotkeyConfig represents the complete hotkey configuration
func DefaultHotkeyConfig ¶
func DefaultHotkeyConfig() *HotkeyConfig
DefaultHotkeyConfig returns the default hotkey configuration
func HotkeyPresetConfig ¶
func HotkeyPresetConfig(preset string) *HotkeyConfig
HotkeyPresetConfig returns the hotkey configuration for a named preset. Supported presets: "vscode", "webstorm", "sprout". For unknown presets, falls back to the default config.
func LoadHotkeys ¶
func LoadHotkeys() (*HotkeyConfig, error)
LoadHotkeys loads the hotkeys configuration from file If the file doesn't exist, returns the default configuration
func VsCodeHotkeyConfig ¶
func VsCodeHotkeyConfig() *HotkeyConfig
VsCodeHotkeyConfig returns a hotkey configuration matching VS Code defaults.
func WebStormHotkeyConfig ¶
func WebStormHotkeyConfig() *HotkeyConfig
WebStormHotkeyConfig returns a hotkey configuration matching WebStorm/IntelliJ defaults.
func (*HotkeyConfig) GetHotkeyByCommandID ¶
func (h *HotkeyConfig) GetHotkeyByCommandID(commandID string) *HotkeyEntry
GetHotkeyByCommandID returns the hotkey entry for a specific command_id Returns the first matching entry (typically the non-Mac variant)
func (*HotkeyConfig) GetMacHotkeys ¶
func (h *HotkeyConfig) GetMacHotkeys() []HotkeyEntry
GetMacHotkeys returns only Mac-specific hotkeys (those with Cmd modifier)
func (*HotkeyConfig) GetNonMacHotkeys ¶
func (h *HotkeyConfig) GetNonMacHotkeys() []HotkeyEntry
GetNonMacHotkeys returns only non-Mac hotkeys (those with Ctrl modifier)
type HotkeyEntry ¶
type HotkeyEntry struct {
Key string `json:"key"`
CommandID string `json:"command_id"`
Description string `json:"description,omitempty"`
Global bool `json:"global,omitempty"` // If true, works even when input is focused
}
HotkeyEntry represents a single hotkey binding
type HydrateCompleteData ¶
type HydrateCompleteData struct {
FilesTransferred int64 `json:"files_transferred"`
TotalBytes int64 `json:"total_bytes"`
DurationMs int64 `json:"duration_ms"`
}
HydrateCompleteData is the data payload for "hydrate_complete" messages. Sent after all files have been streamed, summarizing the transfer.
type HydrateFileData ¶
type HydrateFileData struct {
Path string `json:"path"`
ContentBase64 string `json:"content_base64"`
Size int64 `json:"size"`
ModifiedAt string `json:"modified_at"`
ProgressPct float64 `json:"progress_pct"`
}
HydrateFileData is the data payload for "hydrate_file" messages. Carries a single file's base64-encoded content and metadata.
type HydrateManifestData ¶
type HydrateManifestData struct {
TotalFiles int64 `json:"total_files"`
TotalSize int64 `json:"total_size"`
EstimateSeconds int64 `json:"estimate_seconds"`
}
HydrateManifestData is the data payload for "hydrate_manifest" messages. Sent after the workspace scan, before file streaming begins, so the client can display a progress bar with ETA.
type HydrateRequestData ¶
type HydrateRequestData struct{}
HydrateRequestData is the data payload for inbound "hydrate_request" messages. The body is empty — the client just signals it wants hydration.
func (*HydrateRequestData) Validate ¶
func (d *HydrateRequestData) Validate() error
Validate performs field-level validation on HydrateRequestData. Empty body is always valid — the client just requests hydration.
type ModelChangeData ¶
type ModelChangeData struct {
Model string `json:"model"`
Provider string `json:"provider,omitempty"`
}
ModelChangeData is the data payload for "model_change" messages.
func (*ModelChangeData) Validate ¶
func (d *ModelChangeData) Validate() error
Validate performs field-level validation on ModelChangeData.
type PasswordResponseData ¶ added in v0.17.7
type PasswordResponseData struct {
RequestID string `json:"request_id"`
Password string `json:"password"`
}
PasswordResponseData is the data payload for "password_response" messages. The WebUI sends this when the user types a password in the password prompt dialog. The server delivers it to the agent's password broker so the blocked shell command (sudo, ssh, passwd, etc.) can proceed.
CRITICAL: The Password field must NEVER appear in any log output.
func (*PasswordResponseData) Validate ¶ added in v0.17.7
func (d *PasswordResponseData) Validate() error
Validate performs field-level validation on PasswordResponseData.
type PersonaChangeData ¶
type PersonaChangeData struct {
Persona string `json:"persona"`
}
PersonaChangeData is the data payload for "persona_change" messages.
func (*PersonaChangeData) Validate ¶
func (d *PersonaChangeData) Validate() error
Validate performs field-level validation on PersonaChangeData.
type ProjectInfo ¶
type ProjectInfo struct {
Path string `json:"path"`
Name string `json:"name"`
Markers []string `json:"markers,omitempty"`
}
ProjectInfo describes a detected project directory.
func FindProjectsInDirectory ¶
func FindProjectsInDirectory(dir string, maxDepth int) []ProjectInfo
FindProjectsInDirectory scans a directory for subdirectories that look like projects, up to maxDepth levels deep. Returns at most 20 results.
type ProjectMarker ¶
type ProjectMarker struct {
Name string // e.g., ".git", "go.mod"
Weight int // Higher = stronger signal
IsDir bool
}
ProjectMarker represents a file or directory that indicates a project root.
type ProviderChangeData ¶
type ProviderChangeData struct {
Provider string `json:"provider"`
}
ProviderChangeData is the data payload for "provider_change" messages.
func (*ProviderChangeData) Validate ¶
func (d *ProviderChangeData) Validate() error
Validate performs field-level validation on ProviderChangeData.
type ReactWebServer ¶
type ReactWebServer struct {
// contains filtered or unexported fields
}
ReactWebServer provides the React web UI
func NewReactWebServer ¶
func NewReactWebServer(agent *agent.Agent, eventBus *events.EventBus, port int, bindAddr string, socketPath string, authToken string) (*ReactWebServer, error)
NewReactWebServer creates a new React web server
func (*ReactWebServer) ExtractUserID ¶
func (ws *ReactWebServer) ExtractUserID(r *http.Request) string
ExtractUserID reads the trusted user header from the request when running in service mode. In local mode, it always returns an empty string to prevent header spoofing.
func (*ReactWebServer) GetAskUserMgr ¶
func (ws *ReactWebServer) GetAskUserMgr() *agenttools.AskUserManager
GetAskUserMgr returns the ask user manager used by this web server.
func (*ReactWebServer) GetDaemonRoot ¶
func (ws *ReactWebServer) GetDaemonRoot() string
GetDaemonRoot returns the daemon-scoped filesystem root.
func (*ReactWebServer) GetPort ¶
func (ws *ReactWebServer) GetPort() int
GetPort returns the port the web server is running on
func (*ReactWebServer) GetSecurityPromptMgr ¶
func (ws *ReactWebServer) GetSecurityPromptMgr() *security.ApprovalManager
GetSecurityPromptMgr returns the security approval manager used by this web server.
func (*ReactWebServer) GetWorkspaceRoot ¶
func (ws *ReactWebServer) GetWorkspaceRoot() string
GetWorkspaceRoot returns the current workspace root.
func (*ReactWebServer) HandleContainerRecovery ¶
func (ws *ReactWebServer) HandleContainerRecovery(ctx context.Context, clientID string, lastKnownSeq int64) (*SyncReconcileData, error)
HandleContainerRecovery handles the case where a browser reconnects after its container died. It reconciles sequence numbers between the browser's last-known state and the container's current state.
func (*ReactWebServer) HandleContainerRecoveryWithSeqs ¶
func (ws *ReactWebServer) HandleContainerRecoveryWithSeqs(ctx context.Context, clientID string, browserSeqs map[string]int64) (*SyncReconcileData, error)
HandleContainerRecoveryWithSeqs handles full per-file reconciliation after container death, given the browser's per-file sequence numbers.
func (*ReactWebServer) HasActiveWebUIClients ¶
func (ws *ReactWebServer) HasActiveWebUIClients() bool
HasActiveWebUIClients returns true if one or more WebSocket connections of type "webui" are currently connected. The security prompt routing logic uses this to decide whether to route prompts through the WebUI event bus or fall back to CLI-based prompting.
func (*ReactWebServer) IsRunning ¶
func (ws *ReactWebServer) IsRunning() bool
IsRunning returns true if the web server is running
func (*ReactWebServer) IsSharedMode ¶ added in v0.16.17
func (ws *ReactWebServer) IsSharedMode() bool
IsSharedMode reports whether the server is in "shared agent" mode — where a CLI process launched the web server with a live agent. In this mode, the WebUI shares the CLI's agent instance (same conversation, same session) instead of creating its own per-chat agents.
This is the non-daemon interactive case: `sprout` started with a TTY passes its agent to NewReactWebServer, while `sprout daemon` passes nil.
func (*ReactWebServer) SendSyncReconcile ¶
func (ws *ReactWebServer) SendSyncReconcile(safeConn *SafeConn, data *SyncReconcileData) error
SendSyncReconcile sends the reconciliation plan to a client.
func (*ReactWebServer) SendSyncReplayComplete ¶
func (ws *ReactWebServer) SendSyncReplayComplete(safeConn *SafeConn, clientID string) error
SendSyncReplayComplete tells the client the replay is done.
func (*ReactWebServer) SendSyncReplayFile ¶
func (ws *ReactWebServer) SendSyncReplayFile(safeConn *SafeConn, clientID, filePath, content string, seq int64) error
SendSyncReplayFile sends a single file replay patch to a client connection.
func (*ReactWebServer) SendSyncReplayStart ¶
func (ws *ReactWebServer) SendSyncReplayStart(safeConn *SafeConn, clientID string, fileCount int) error
SendSyncReplayStart tells the client a replay is beginning.
func (*ReactWebServer) SetAgentEnforceSingleSession ¶ added in v0.17.3
func (ws *ReactWebServer) SetAgentEnforceSingleSession(v bool)
SetAgentEnforceSingleSession configures whether the WebSocket dispatcher should route connections through the single-active-session (Mode 1) path or the multi-session (Mode 2) path. SP-118 Phase 1.
- true → Mode 1: only one browser window active per user at a time. Conflicts trigger session_conflict and a takeover prompt. This is sprout agent / CWS interactive mode.
- false → Mode 2: N parallel browser windows per user. Currently a stub that accepts connections without enforcement; the full implementation lands in SP-118-2. This is sprout service / daemon.
Cmd should call this immediately after NewReactWebServer returns:
sprout agent path → SetAgentEnforceSingleSession(true) sprout service path → leave false (or explicitly call with false)
Dispatch uses this flag, NOT serviceMode. Tests in pkg/webui flip serviceMode=true to exercise the takeover flow under the Mode 1 path (e.g., TestSessionConflict_Takeover_UserMode); using serviceMode as the dispatch key would break them.
func (*ReactWebServer) SetWorkspaceRoot ¶
func (ws *ReactWebServer) SetWorkspaceRoot(path string) (string, error)
SetWorkspaceRoot updates the active workspace root, changes the process cwd, and resets terminal state.
func (*ReactWebServer) Shutdown ¶
func (ws *ReactWebServer) Shutdown() error
Shutdown gracefully shuts down the web server
func (*ReactWebServer) Start ¶
func (ws *ReactWebServer) Start(ctx context.Context) error
Start starts the web server
func (*ReactWebServer) SyncSharedAgentState ¶ added in v0.16.17
func (ws *ReactWebServer) SyncSharedAgentState(agentInst *agent.Agent) error
SyncSharedAgentState exports the shared agent's state (conversation history, session ID, etc.) into the WebUI's default chat session. Called by the CLI's ProcessQuery wrapper after each CLI query completes, so the browser tab has fresh history when it reconnects or refreshes.
Only meaningful in shared-agent mode (ws.agent != nil). In daemon mode this is a no-op because each chat manages its own agent independently.
type RecallItem ¶ added in v0.16.19
type RecallItem struct {
SessionID string `json:"session_id"`
Workspace string `json:"workspace"`
Summary string `json:"summary"`
Actionable string `json:"actionable"`
Similarity float32 `json:"similarity"`
AgeDays float64 `json:"age_days"`
ContentPreview string `json:"content_preview"`
}
RecallItem is the JSON shape returned by /api/recall (a friendly subset of agent.RecalledItem).
type RecallResponse ¶ added in v0.16.19
type RecallResponse struct {
Query string `json:"query"`
Items []RecallItem `json:"items"`
Count int `json:"count"`
}
RecallResponse is the JSON envelope for the /api/recall endpoint.
type RecentWorkspace ¶
type RecentWorkspace struct {
Path string `json:"path"`
Name string `json:"name"`
LastUsed time.Time `json:"last_used"`
Markers []string `json:"markers,omitempty"`
SessionCount int `json:"session_count"`
}
RecentWorkspace tracks a workspace that was recently used.
func GetRecentWorkspaces ¶
func GetRecentWorkspaces() []RecentWorkspace
GetRecentWorkspaces returns up to 10 recently used workspaces.
type ReconciliationAction ¶
type ReconciliationAction struct {
FilePath string `json:"file_path"`
Action string `json:"action"` // "sync_ok", "container_ahead", "browser_ahead", "diverged"
ContainerSeq int64 `json:"container_seq"`
BrowserSeq int64 `json:"browser_seq"`
ContainerContent string `json:"container_content,omitempty"`
ContainerModTime time.Time `json:"container_mod_time,omitempty"`
}
ReconciliationAction describes what recovery action to take for a single file.
type ReplaceFileChange ¶
type ReplaceFileChange struct {
File string `json:"file"`
Matches []ReplaceMatch `json:"matches"`
ChangedLines int `json:"changed_lines"`
}
ReplaceFileChange represents changes to a single file
type ReplaceMatch ¶
type ReplaceMatch struct {
LineNumber int `json:"line_number"`
OldLine string `json:"old_line"`
NewLine string `json:"new_line"`
ColumnStart int `json:"column_start"`
ColumnEnd int `json:"column_end"`
}
ReplaceMatch represents a match that would be replaced
type ReplaceRequest ¶
type ReplaceRequest struct {
Search string `json:"search"`
Replace string `json:"replace"`
Files []string `json:"files"`
CaseSensitive bool `json:"case_sensitive"`
WholeWord bool `json:"whole_word"`
Regex bool `json:"regex"`
Preview bool `json:"preview"`
}
ReplaceRequest represents a search and replace operation
type ReplaceResponse ¶
type ReplaceResponse struct {
Changes []ReplaceFileChange `json:"changes"`
TotalChanges int `json:"total_changes"`
Preview bool `json:"preview"`
}
ReplaceResponse represents the response from a replace operation
type RuntimeConfig ¶
type RuntimeConfig struct {
// APIBaseURL is the base URL for API requests (e.g., "http://localhost:56000").
APIBaseURL string `json:"apiBaseURL"`
// WSURL is the WebSocket URL for real-time updates.
WSURL string `json:"wsURL"`
// AuthMode controls authentication: "none" (local), "bearer" (cloud/token).
AuthMode string `json:"authMode"`
// AppMode is the application mode: "local" (desktop/self-hosted), "cloud" (managed).
AppMode string `json:"appMode"`
// BuildVersion is the version string embedded at build time.
BuildVersion string `json:"buildVersion"`
// (non-daemon interactive mode). The frontend uses this to hide
// multi-chat UI and show "coupled with terminal" messaging.
SharedMode bool `json:"sharedMode"`
}
RuntimeConfig provides runtime configuration for the web UI. Served via GET /api/bootstrap (unauthenticated) so the frontend can configure itself without hardcoded values.
type SafeConn ¶
type SafeConn struct {
// contains filtered or unexported fields
}
SafeConn wraps a WebSocket connection with write mutex and panic recovery
func NewSafeConn ¶
NewSafeConn creates a new safe connection wrapper
func (*SafeConn) Conn ¶
Conn returns the underlying *websocket.Conn. Useful when callers need to register the connection in a map keyed by pointer identity (e.g. the chatSubscribers registry) without losing the SafeConn write serialization for the actual JSON traffic.
func (*SafeConn) Underlying ¶
Underlying returns the underlying websocket.Conn for read operations (still need to be careful)
func (*SafeConn) WriteJSON ¶
WriteJSON safely writes JSON to the WebSocket connection.
SP-034-5d: outbound payloads carrying a `type` field are validated against the registry in websocket_outbound_registry.go. Unknown types panic in dev (`SPROUT_DEV=1`) so typos surface immediately, and in prod are dropped after logging — WriteJSON returns ErrOutboundDropped so the caller can tell the difference between "actually delivered" and "silently filtered by the registry".
func (*SafeConn) WritePanicError ¶
WritePanicError sends a panic error event to the client. Called only from deferred recover() blocks — never during normal flow. The full panic value is logged server-side but not sent to the client to avoid leaking internal state (stack traces, memory addresses, struct internals).
type SearchMatch ¶
type SearchMatch struct {
LineNumber int `json:"line_number"`
Line string `json:"line"`
ColumnStart int `json:"column_start"`
ColumnEnd int `json:"column_end"`
ContextBefore []string `json:"context_before"`
ContextAfter []string `json:"context_after"`
}
SearchMatch represents a single match within a file
type SearchResponse ¶
type SearchResponse struct {
Results []SearchResult `json:"results"`
TotalMatches int `json:"total_matches"`
TotalFiles int `json:"total_files"`
Truncated bool `json:"truncated"`
Query string `json:"query"`
}
SearchResponse represents the response from a search
type SearchResult ¶
type SearchResult struct {
File string `json:"file"`
Matches []SearchMatch `json:"matches"`
MatchCount int `json:"match_count"`
}
SearchResult represents matches in a single file
type SecurityApprovalResponseData ¶
type SecurityApprovalResponseData struct {
RequestID string `json:"request_id"`
Approved bool `json:"approved"`
Action string `json:"action,omitempty"`
}
SecurityApprovalResponseData is the data payload for "security_approval_response" messages.
Action carries the multi-option dialog choice. Legal values:
- "" → fall back to Approved bool (legacy clients)
- "approve_once" → equivalent to Approved=true
- "approve_always" → shell-only: approve and persist command to allowlist
- "elevate" → shell-only: approve and bump session risk profile to permissive
- "allow_folder_session" → filesystem-only: approve and allowlist the target folder for this session
- "deny" → equivalent to Approved=false
Old WebUI clients that only set Approved continue to work because the server falls back to bool when Action is empty.
func (*SecurityApprovalResponseData) Validate ¶
func (d *SecurityApprovalResponseData) Validate() error
Validate performs field-level validation on SecurityApprovalResponseData.
type SecurityPromptResponseData ¶
type SecurityPromptResponseData struct {
RequestID string `json:"request_id"`
Response bool `json:"response"`
}
SecurityPromptResponseData is the data payload for "security_prompt_response" messages.
func (*SecurityPromptResponseData) Validate ¶
func (d *SecurityPromptResponseData) Validate() error
Validate performs field-level validation on SecurityPromptResponseData.
type SemanticPreviewContextConfig ¶
type SemanticPreviewContextConfig struct {
MinRelevanceScore float64 `json:"min_relevance_score"`
MaxContextualResults int `json:"max_contextual_results"`
MaxContextChars int `json:"max_context_chars"`
WorkspaceScopedRetrieval bool `json:"workspace_scoped_retrieval"`
}
SemanticPreviewContextConfig echoes the resolved PersistentContext params the preview ran against. Useful for the UI to show "this preview used score>=0.50, top 5".
type SemanticPreviewContextResponse ¶
type SemanticPreviewContextResponse struct {
Query string `json:"query"`
Workspace string `json:"workspace"`
Config SemanticPreviewContextConfig `json:"config"`
Results []SemanticPreviewContextResult `json:"results"`
Enabled bool `json:"enabled"`
Note string `json:"note,omitempty"`
}
SemanticPreviewContextResponse mirrors the proactive-context pipeline so a user can see exactly what their Memory settings would inject for a given query before saving them.
type SemanticPreviewContextResult ¶
type SemanticPreviewContextResult struct {
UserMessage string `json:"user_message"` // first-line excerpt of the past turn
Summary string `json:"summary"` // actionable summary if present
Workspace string `json:"workspace"` // working directory the turn was recorded in
Score float64 `json:"score"` // time-decayed cosine similarity
RelativeTime string `json:"relative_time"` // e.g. "3 hours ago"
}
SemanticPreviewContextResult is one entry the proactive-context retriever would inject for the given query, surfaced for the Memory settings panel.
type SemanticPreviewResponse ¶
type SemanticPreviewResponse struct {
File string `json:"file"`
StartLine int `json:"start_line"`
Snippet []SnippetLine `json:"snippet"`
TotalLines int `json:"total_lines"`
}
SemanticPreviewResponse is the JSON response for semantic preview.
type SemanticSearchResponse ¶
type SemanticSearchResponse struct {
Results []SemanticSearchResult `json:"results"`
Query string `json:"query"`
Total int `json:"total"`
Duration string `json:"duration"` // human-readable elapsed time
DuplicateClusters []DuplicateCluster `json:"duplicate_clusters"`
}
SemanticSearchResponse is the JSON response for semantic search.
type SemanticSearchResult ¶
type SemanticSearchResult struct {
File string `json:"file"`
Name string `json:"name"` // function/method name
Signature string `json:"signature"` // full function signature
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
Language string `json:"language"`
Similarity float32 `json:"similarity"`
Type string `json:"type"` // "code_unit" or "file"
Embedding []float32 `json:"-"` // used only for server-side pairwise comparison; not sent to client
ClusterId int `json:"cluster_id,omitempty"` // 0 = not in a cluster, 1+ = cluster group
}
SemanticSearchResult represents a single semantic search match.
type SessionCostRow ¶ added in v0.16.18
type SessionCostRow struct {
SessionID string `json:"session_id"`
Title string `json:"title"`
WorkingDir string `json:"working_dir"`
TotalCost float64 `json:"total_cost"`
LastUpdated string `json:"last_updated"` // RFC3339 timestamp
}
SessionCostRow represents a single session's aggregated cost data
type SessionOption ¶
type SessionOption func(*TerminalSession)
SessionOption is a functional option for configuring a terminal session.
func WithAutoClose ¶
func WithAutoClose(autoClose bool) SessionOption
WithAutoClose sets whether the session should be auto-closed when inactive.
func WithName ¶
func WithName(name string) SessionOption
WithName sets a human-readable name for the session.
type ShellInfo ¶
type ShellInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Default bool `json:"default"`
}
ShellInfo describes an available shell on the system.
type SnippetLine ¶
type SnippetLine struct {
LineNumber int `json:"line_number"`
Content string `json:"content"`
IsContext bool `json:"is_context"` // true for lines before the function start
}
SnippetLine represents a single line in a code snippet preview.
type SubscribeData ¶
type SubscribeData struct {
Events []string `json:"events"`
ChatIDs []string `json:"chat_ids,omitempty"`
Channel string `json:"channel,omitempty"` // Event channel to opt into (e.g., "automate")
}
SubscribeData is the data payload for "subscribe" messages.
Events is the historical event-type filter (kept for backward compat, not currently enforced at the bus level). ChatIDs is the SP-034-3 addition: registers this connection in the server's chatSubscribers list so events targeting any of those chats fan out to this connection even when the originating clientID differs (multi-tab consistency).
func (*SubscribeData) Validate ¶
func (d *SubscribeData) Validate() error
Validate performs field-level validation on SubscribeData.
type SyncReconcileData ¶
type SyncReconcileData struct {
ClientID string `json:"client_id"`
Plan []ReconciliationAction `json:"plan"`
}
SyncReconcileData is the server's response with a reconciliation plan.
type SyncRecoverData ¶
type SyncRecoverData struct {
ClientID string `json:"client_id"`
Seqs map[string]int64 `json:"seqs"` // file_path -> browser_seq
}
SyncRecoverData is the payload the browser sends in a sync_recover message.
type SyncReplayFileData ¶
type SyncReplayFileData struct {
ClientID string `json:"client_id"`
FilePath string `json:"file_path"`
Content string `json:"content"`
Seq int64 `json:"seq"`
Timestamp int64 `json:"timestamp"`
}
SyncReplayFileData is the payload for a single file replay.
type TerminalManager ¶
type TerminalManager struct {
// contains filtered or unexported fields
}
TerminalManager manages terminal sessions.
func NewTerminalManager ¶
func NewTerminalManager(workspaceRoot string) *TerminalManager
NewTerminalManager creates a new terminal manager.
func (*TerminalManager) AddToHistory ¶
func (tm *TerminalManager) AddToHistory(sessionID, command string) error
AddToHistory adds a command to the session history.
func (*TerminalManager) AvailableShells ¶
func (tm *TerminalManager) AvailableShells() []ShellInfo
AvailableShells returns a list of shells found on the system. On Unix, it scans for common shells plus the user's $SHELL. On Windows, it returns cmd.exe and PowerShell if found.
func (*TerminalManager) CleanupInactiveSessions ¶
func (tm *TerminalManager) CleanupInactiveSessions(timeout time.Duration, backgroundTimeout ...time.Duration)
CleanupInactiveSessions removes sessions that have been inactive for too long. Background sessions (IsBackground=true) use a separate timeout (default 2 hours) vs regular hidden sessions (30 minutes).
func (*TerminalManager) CloseAllSessions ¶
func (tm *TerminalManager) CloseAllSessions() error
CloseAllSessions closes all known terminal sessions and returns the first error encountered.
func (*TerminalManager) CloseSession ¶
func (tm *TerminalManager) CloseSession(sessionID string) error
CloseSession terminates the shell process and removes the session from the manager. All active subscribers are notified via channel close before the session is deleted.
The process Wait() runs OUTSIDE tm.mutex so a stuck or zombie shell (NFS hang, D-state process) cannot deadlock the entire TerminalManager. The session is removed from the map under tm.mutex first, so no new callers can observe it after the lock is released; the teardown then proceeds without holding the manager-level lock.
func (*TerminalManager) CreateHiddenSession ¶
func (tm *TerminalManager) CreateHiddenSession(id, owner, chatID string, opts ...SessionOption) (session *TerminalSession, err error)
CreateHiddenSession creates a hidden PTY session for agent use. Hidden sessions are excluded from the default ListSessions() output but still participate in inactive-session cleanup.
NOTE: Session creation runs while holding tm.mutex to prevent the PTY reader goroutine (launched by createUnixSession/createWindowsSession) from being visible to ListSessions() before the Hidden flag is set.
func (*TerminalManager) CreateSession ¶
func (tm *TerminalManager) CreateSession(sessionID string, shellOverride ...string) (*TerminalSession, error)
CreateSession creates a new terminal session with PTY support. The shell process runs for the lifetime of the session and persists across WebSocket disconnections. On reconnect, the ring buffer replays recent output. shellOverride, if non-empty, specifies the preferred shell binary (must be in PATH).
func (*TerminalManager) DetachFromSession ¶
func (tm *TerminalManager) DetachFromSession(sessionID string) error
DetachFromSession signals that the WebSocket has disconnected. The shell process keeps running; the subscriber goroutine (in websocket.go) handles unsubscription via its own defer, so this is a no-op but kept for API compatibility.
func (*TerminalManager) ExecuteCommand ¶
func (tm *TerminalManager) ExecuteCommand(sessionID, command string) error
ExecuteCommand executes a command in the specified session.
func (*TerminalManager) ExecuteCommandAndWait ¶
func (tm *TerminalManager) ExecuteCommandAndWait(ctx context.Context, session *TerminalSession, command string) (output string, exitCode int, err error)
ExecuteCommandAndWait executes a command synchronously on a hidden PTY session, waiting for command completion and returning the output and exit code. This function is designed for agent use on hidden sessions only.
The command is wrapped via /bin/sh -c with a sentinel marker to detect completion:
/bin/sh -c '<command> && echo "__SPROUT_DONE__<marker>:$?" || echo "__SPROUT_DONE__<marker>:$?"'
Using /bin/sh ensures $? works regardless of the session's login shell (e.g., fish uses $status instead of $?).
Parameters:
- ctx: context for cancellation and timeout control
- session: the terminal session to execute the command on (must be hidden)
- command: the command string to execute
Returns:
- output: the command output with ANSI escape sequences stripped
- exitCode: the command's exit code (or -1 if timeout/cancelled)
- err: any error that occurred during execution
func (*TerminalManager) ExecuteCommandInBackground ¶
func (tm *TerminalManager) ExecuteCommandInBackground(ctx context.Context, chatID, command string) (string, error)
ExecuteCommandInBackground creates a new hidden PTY session for a background command, writes the command to it, and returns immediately with the session ID. Unlike foreground hidden sessions (one per chat), each background command gets its own session. Background sessions get a 2-hour cleanup timeout (vs 30-min for regular hidden sessions).
func (*TerminalManager) ExecuteCommandInHidden ¶
func (tm *TerminalManager) ExecuteCommandInHidden(ctx context.Context, sessionID, command string) (string, int, error)
ExecuteCommandInHidden is a convenience wrapper that looks up a hidden session by ID and executes a command synchronously, returning the output and exit code.
This is the primary entry point for agent code that needs to run a command in a hidden PTY session and wait for completion.
func (*TerminalManager) GetBackgroundOutput ¶
func (tm *TerminalManager) GetBackgroundOutput(sessionID string) (string, error)
GetBackgroundOutput returns the accumulated ring buffer output for a background session. The output is stripped of ANSI escape sequences for readability.
func (*TerminalManager) GetHistory ¶
func (tm *TerminalManager) GetHistory(sessionID string) ([]string, error)
GetHistory returns the command history for a session.
func (*TerminalManager) GetOrCreateHiddenSessionForChat ¶
func (tm *TerminalManager) GetOrCreateHiddenSessionForChat(ctx context.Context, chatID string) (string, error)
GetOrCreateHiddenSessionForChat returns the ID of an existing hidden session for the given chat ID, or creates a new one. This enables one-hidden-session-per-chat reuse.
The implementation uses a deterministic session ID ("agent-hidden-<chatID>") and handles the TOCTOU race by catching the "already exists" error from CreateHiddenSession and re-looking up the session that was created by the winning goroutine.
func (*TerminalManager) GetSession ¶
func (tm *TerminalManager) GetSession(sessionID string) (*TerminalSession, bool)
GetSession retrieves any terminal session, including hidden ones. Callers that need user-facing access should check session.Hidden before exposing session data, or use HasVisibleSession() first.
func (*TerminalManager) GetSessionCount ¶
func (tm *TerminalManager) GetSessionCount() int
GetSessionCount returns the number of all active sessions, including hidden ones. For user-facing counts, use GetVisibleSessionCount() instead.
func (*TerminalManager) GetTerminalSize ¶
func (tm *TerminalManager) GetTerminalSize(sessionID string) (*pty.Winsize, error)
GetTerminalSize returns the current terminal size for the session.
func (*TerminalManager) GetVisibleSessionCount ¶
func (tm *TerminalManager) GetVisibleSessionCount() int
GetVisibleSessionCount returns the number of active sessions that are not hidden. Use this for user-facing stats. Use GetSessionCount() for internal/maintenance purposes.
func (*TerminalManager) HasSession ¶
func (tm *TerminalManager) HasSession(sessionID string) bool
HasSession checks if a session exists (for reattach). Returns true for both visible and hidden sessions. Use HasVisibleSession() for user-facing checks.
func (*TerminalManager) HasVisibleSession ¶
func (tm *TerminalManager) HasVisibleSession(sessionID string) bool
HasVisibleSession checks if a non-hidden session exists (for user-facing checks). Holds tm.mutex while reading session.Hidden to maintain consistent lock ordering with ListSessions/ListHiddenSessions/GetSessionCount (tm.mutex → session.mutex).
func (*TerminalManager) IsSessionActive ¶
func (tm *TerminalManager) IsSessionActive(sessionID string) bool
IsSessionActive checks whether a session is still active.
func (*TerminalManager) ListAllSessions ¶
func (tm *TerminalManager) ListAllSessions() []string
ListAllSessions returns ALL session IDs, including hidden (agent-owned) sessions. WARNING: Do NOT expose this to user-facing APIs. Use ListSessions() for user-visible session lists. This is intended for server-side operations like CloseAllSessions().
func (*TerminalManager) ListHiddenSessions ¶
func (tm *TerminalManager) ListHiddenSessions() []string
ListHiddenSessions returns only hidden session IDs.
func (*TerminalManager) ListSessions ¶
func (tm *TerminalManager) ListSessions() []string
ListSessions returns a list of active session IDs, excluding hidden sessions.
func (*TerminalManager) NavigateHistory ¶
func (tm *TerminalManager) NavigateHistory(sessionID string, direction string) (string, error)
NavigateHistory navigates through command history.
func (*TerminalManager) ReattachSession ¶
func (tm *TerminalManager) ReattachSession(sessionID string) (string, error)
ReattachSession returns the scrollback buffer of an existing session so the reconnecting WebSocket can replay it. The shell is already running; the caller subscribes to the session's live output stream immediately after this call.
func (*TerminalManager) ResetHistoryIndex ¶
func (tm *TerminalManager) ResetHistoryIndex(sessionID string) error
ResetHistoryIndex resets the history index to the end (for new input).
func (*TerminalManager) ResizeTerminal ¶
func (tm *TerminalManager) ResizeTerminal(sessionID string, rows, cols uint16) error
ResizeTerminal resizes the PTY for the given session.
func (*TerminalManager) SessionCount ¶
func (tm *TerminalManager) SessionCount() int
SessionCount returns the number of active sessions (alias for GetSessionCount).
func (*TerminalManager) StartCleanupWorker ¶
func (tm *TerminalManager) StartCleanupWorker(ctx context.Context, interval time.Duration, timeout time.Duration, backgroundTimeout ...time.Duration)
StartCleanupWorker starts a background worker to clean up inactive sessions. Background sessions get a separate timeout (default 2 hours) vs regular sessions (timeout). Safe to call multiple times — only one worker goroutine is started per TerminalManager.
func (*TerminalManager) StopBackgroundSession ¶
func (tm *TerminalManager) StopBackgroundSession(sessionID string) error
StopBackgroundSession terminates a background session by sending Ctrl+C to the PTY and then closing the session. Returns an error if the session is not found or is not a background session.
func (*TerminalManager) WriteRawInput ¶
func (tm *TerminalManager) WriteRawInput(sessionID, input string) error
WriteRawInput writes raw terminal input bytes directly to PTY without command history mutation or implicit carriage-return handling.
type TerminalSession ¶
type TerminalSession struct {
ID string
Command *exec.Cmd
Pty *os.File
Cancel context.CancelFunc
Active bool
LastUsed time.Time
StartedAt time.Time // When the session was created (for duration display)
Size *pty.Winsize
// Hidden session metadata — used for agent background PTY sessions.
Hidden bool `json:"-"`
IsBackground bool `json:"-"` // true for background sessions (2-hour timeout vs 30-min for regular hidden)
Owner string `json:"-"` // "agent" or other entity that created this session
ChatID string `json:"-"` // chat session that owns this terminal
Name string `json:"-"` // human-readable name (e.g. command prefix for background tasks)
AutoClose bool `json:"-"` // reserved for Phase B: close automatically when inactive
// NoPTY indicates this session is running in fallback mode without a real
// PTY (e.g. on Alpine Linux or minimal containers where /dev/pts is
// unavailable). Commands are run via exec.Cmd with stdin/stdout pipes
// instead. Terminal resize and raw terminal features are degraded.
NoPTY bool `json:"-"`
// History for shell command navigation.
History []string
HistoryIndex int
// contains filtered or unexported fields
}
TerminalSession represents a persistent terminal session backed by a raw PTY. The shell process keeps running even when no WebSocket is connected; output is buffered in the ring for replay on reconnect.
type UserConnection ¶ added in v0.17.3
type UserConnection struct {
Conn *SafeConn // shared write mutex for cross-conn notifications
Raw interface{} // underlying *websocket.Conn (kept as interface{} to avoid an import cycle in some test setups)
SessionID string // human-readable session id
ClientID string // browser-side client_id (matches ConnectionInfo.ClientID)
UserID string // resolved user id (service mode)
}
UserConnection identifies one connected WebSocket inside a UserConnections registry. The pointer fields are used as identity tokens — Remove matches by pointer equality, not by any string field, so callers can hand out the same pointer to a goroutine that may run after a removal.
type UserConnections ¶ added in v0.17.3
type UserConnections struct {
// contains filtered or unexported fields
}
UserConnections holds a registry of WebSocket connections indexed by user id (or by client id, in local mode). It supports concurrent Add/Remove/Count/ForEach operations.
Zero value is ready to use. No constructor required.
func (*UserConnections) Add ¶ added in v0.17.3
func (uc *UserConnections) Add(userID string, c UserConnection)
Add registers a connection under userID. It is safe to call concurrently from multiple goroutines. The connection is appended to the existing slice (or a new slice if the user has no slot yet).
func (*UserConnections) AllUserIDs ¶ added in v0.17.3
func (uc *UserConnections) AllUserIDs() []string
AllUserIDs returns the set of user ids that currently have at least one registered connection. Order is not specified. The result is a snapshot — callers must not mutate it.
func (*UserConnections) Count ¶ added in v0.17.3
func (uc *UserConnections) Count(userID string) int
Count returns the number of registered connections for userID. O(1) under the read lock.
func (*UserConnections) ForEach ¶ added in v0.17.3
func (uc *UserConnections) ForEach(userID string, fn func(UserConnection) bool)
ForEach invokes fn once per connection registered under userID, in insertion order. If fn returns false the iteration stops. Safe to call concurrently with Add/Remove — fn observes a consistent snapshot at the time of the call.
func (*UserConnections) Remove ¶ added in v0.17.3
func (uc *UserConnections) Remove(userID string, raw interface{})
Remove unregisters a connection by raw pointer identity. It is safe to call concurrently. If the connection is not registered, Remove is a no-op. After removal, the slot is left in place even when its slice is empty — the per-user lock would otherwise become a churn point under high concurrency. Use Count to detect "all gone" if needed.
func (*UserConnections) Snapshot ¶ added in v0.17.3
func (uc *UserConnections) Snapshot(userID string) []UserConnection
Snapshot returns a copy of the connections registered under userID. Used by diagnostics (Phase 5) and by tests that need to assert on the live state without holding the slot lock.
type WebSocketMessage ¶
type WebSocketMessage struct {
Type string `json:"type"`
Data json.RawMessage `json:"data,omitempty"`
}
WebSocketMessage is the envelope for all incoming WebSocket messages. It's used to parse and validate the top-level message structure.
func (*WebSocketMessage) Validate ¶
func (m *WebSocketMessage) Validate() error
Validate performs field-level validation on the message.
type WebUIError ¶
type WebUIError struct {
// Code is a machine-readable error identifier (e.g., "config_conflict",
// "provider_unavailable", "rate_limited").
Code string `json:"code"`
// Message is a human-readable description of the error.
Message string `json:"message"`
// Details contains optional structured data for the frontend to use
// (e.g., current config summary for a conflict error).
Details interface{} `json:"details,omitempty"`
// Retryable indicates whether the client should automatically retry.
Retryable bool `json:"retryable"`
}
WebUIError provides a structured error response for the WebUI API. Handlers should return this type (or wrap it) instead of ad-hoc string errors or bare http.Error calls so that the frontend can match on Code rather than scraping Message text.
func NewWebUIError ¶
func NewWebUIError(code, message string, retryable bool) *WebUIError
NewWebUIError creates a new WebUIError with the given fields.
func NewWebUIErrorWithDetails ¶
func NewWebUIErrorWithDetails(code, message string, retryable bool, details interface{}) *WebUIError
NewWebUIErrorWithDetails creates a new WebUIError with structured details.
func (*WebUIError) Error ¶
func (e *WebUIError) Error() string
Error implements the error interface.
type WorktreeInfo ¶
type WorktreeInfo struct {
Path string `json:"path"`
Branch string `json:"branch"`
IsMain bool `json:"is_main"`
IsCurrent bool `json:"is_current"`
ParentPath string `json:"parent_path,omitempty"`
ParentBranch string `json:"parent_branch,omitempty"`
}
WorktreeInfo contains information about a git worktree
func (WorktreeInfo) IsZero ¶
func (wt WorktreeInfo) IsZero() bool
IsZero checks if WorktreeInfo is zero value (for filtering)
Source Files
¶
- api_agent_sessions.go
- api_bootstrap.go
- api_command.go
- api_diagnostics.go
- api_edits.go
- api_embedding_index.go
- api_file_check_modified.go
- api_files.go
- api_lsp.go
- api_misc.go
- api_password.go
- api_proxy_chat.go
- api_proxy_stats.go
- api_query.go
- api_query_rewind.go
- api_query_shared.go
- api_semantic.go
- api_shells.go
- api_subagent.go
- api_support_bundle.go
- api_sync.go
- api_workspace.go
- api_workspace_symbols.go
- api_ws_metrics.go
- auth_middleware.go
- automations_api.go
- changes_api.go
- chat_run_buffer.go
- chat_run_replay.go
- chat_sessions.go
- chat_sessions_api.go
- chat_sessions_create_api.go
- chat_sessions_delete_api.go
- chat_sessions_fork_api.go
- chat_sessions_modify_api.go
- chat_sessions_switch_api.go
- chat_sessions_worktree_api.go
- chat_subscribers.go
- client_context.go
- client_context_accessors.go
- client_context_cache.go
- client_context_state.go
- cold_hydrate.go
- computer_use_api.go
- config_conflict_envelope.go
- connection_types.go
- cookie_sync.go
- cors_middleware.go
- cost_tracking.go
- cost_tracking_api.go
- errors.go
- export_api.go
- file_access_control.go
- file_browser.go
- file_consent.go
- file_ops.go
- file_read_write.go
- file_stats.go
- file_watcher.go
- git_api_branches.go
- git_api_diff.go
- git_api_log.go
- git_api_pull_request.go
- git_api_refs.go
- git_api_review.go
- git_api_staging.go
- git_api_status.go
- git_api_worktrees.go
- git_helpers.go
- git_status_helpers.go
- handlers.go
- heartbeat_monitor.go
- hotkeys.go
- hotkeys_api.go
- instances_api.go
- log_rate_limit.go
- multi_connection_registry.go
- multi_device_takeover.go
- onboarding_api.go
- project_detect.go
- provider_check.go
- providers_api.go
- rate_limited_handler.go
- recall_api.go
- recent_workspaces.go
- routes.go
- run_buffer_subscriber.go
- safe_conn.go
- search_api.go
- search_semantic_api.go
- security_api.go
- security_headers.go
- server.go
- server_checkorigin.go
- server_lifecycle.go
- server_utils.go
- sessions_api.go
- sessions_search_api.go
- settings_api_credentials.go
- settings_api_general.go
- settings_api_get.go
- settings_api_helpers.go
- settings_api_mcp.go
- settings_api_mcp_oauth.go
- settings_api_mcp_test_handler.go
- settings_api_partial_settings.go
- settings_api_providers.go
- settings_api_put.go
- settings_api_skills.go
- settings_api_subagents.go
- shell_approval_api.go
- skills_api.go
- ssh_binary.go
- ssh_browse.go
- ssh_host_inspect.go
- ssh_launch.go
- ssh_launch_ops.go
- ssh_launch_remote.go
- ssh_launch_stop.go
- ssh_launch_workspace.go
- ssh_proxy.go
- ssh_session_registry.go
- ssh_types.go
- static_loader.go
- stream_coalesce.go
- sync_recovery.go
- terminal_agent_exec.go
- terminal_background.go
- terminal_background_cap.go
- terminal_create.go
- terminal_exec.go
- terminal_history.go
- terminal_lifecycle.go
- terminal_resize.go
- terminal_types.go
- terminal_websocket.go
- wakeup_poller.go
- websocket_handler.go
- websocket_handler_events.go
- websocket_handler_mode.go
- websocket_handler_panic.go
- websocket_handler_takeover.go
- websocket_heartbeat.go
- websocket_message_handlers.go
- websocket_message_types.go
- websocket_message_validator.go
- websocket_outbound_registry.go
- websocket_security_handlers.go
- workspace_sync_handlers.go