webui

package
v0.17.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 74 Imported by: 0

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

View Source
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"
)
View Source
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

View Source
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.

View Source
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").

View Source
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.

View Source
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

func CheckPortAvailable(port int) bool

CheckPortAvailable checks if a port is available to bind to

func DisplayAddr

func DisplayAddr(bindAddr string) string

DisplayAddr returns a user-friendly address string for display in logs.

func FindAvailablePort

func FindAvailablePort(basePort int) (int, error)

FindAvailablePort finds an available port starting from a base port

func FindNearestProjectRoot

func FindNearestProjectRoot(startDir string) (string, []string)

FindNearestProjectRoot walks up from startDir looking for project markers. Returns (projectRoot, markers) or ("", nil) if none found. Stops at the filesystem root.

func GetHotkeysPath

func GetHotkeysPath() (string, error)

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

func IsProjectDirectory(dir string) (bool, []string)

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

func UserIDFromContext(ctx context.Context) string

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

func WaitForEditDecision(ch <-chan editDecisionPayload, timeout time.Duration) *editDecisionPayload

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

type BillingTypeBreakdown struct {
	Cost   float64 `json:"cost"`
	Tokens int     `json:"tokens"`
}

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

func (cs *CostStore) ForcePersist() error

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

func (cs *CostStore) GetDailyCosts(days int) []DailyCost

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

func (cs *CostStore) RecordCostWithBilling(provider, model, sessionID, chatID, title, workingDir, billingType string, promptTokens, outputTokens int, chargedCost, tokenCost float64)

func (*CostStore) RecordCostWithSession added in v0.16.18

func (cs *CostStore) RecordCostWithSession(provider, model, sessionID, chatID, title, workingDir string, promptTokens, outputTokens int, cost float64)

RecordCostWithSession adds a new cost record with optional session metadata.

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"`

	// SharedMode is true when the server shares the CLI's agent instance
	// (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

func NewSafeConn(conn *websocket.Conn) *SafeConn

NewSafeConn creates a new safe connection wrapper

func (*SafeConn) Close

func (sc *SafeConn) Close() error

Close closes the underlying connection

func (*SafeConn) Conn

func (sc *SafeConn) Conn() *websocket.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

func (sc *SafeConn) Underlying() *websocket.Conn

Underlying returns the underlying websocket.Conn for read operations (still need to be careful)

func (*SafeConn) WriteJSON

func (sc *SafeConn) WriteJSON(v interface{}) error

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

func (sc *SafeConn) WritePanicError(sessionID, location string, r interface{})

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL