Documentation
¶
Overview ¶
Package terminal — observability declarations (BS-08 Terminal).
Package terminal — protocol.go: T5 §5 WebSocket 终端消息协议定义 (r2) BS-08 v2 消息类型:input/output/resize/ping/pong/closed
Package terminal — pty_manager.go: PTY spawn/stop/watch goroutines [T5 §4]
Package terminal — session.go: SessionV2 内存对象 [T5 §2.1] BS-08 v2: Int64 SessionID, WorkspaceID, PTY, Ring, WS 连接
Package terminal — shell_cmd.go: 跨平台 shell 命令构造辅助
Package terminal implements the BS-08 Terminal subsystem. It provides PTY management, WebSocket-based terminal I/O, and session lifecycle for browser-based terminal access. All state is held in memory (IR-03).
Index ¶
- Constants
- func DefaultPTYFactory(opts PTYStartOptions) (*os.File, *exec.Cmd, error)
- func DialTestWS(t *testing.T, server *httptest.Server, sessionID string, token string) *websocket.Conn
- func NewTestCLIServer(t *testing.T) (*httptest.Server, *SessionManager, *SafeWriteEnds)
- func NewTestCLIServerWithAuth(t *testing.T, authCode string) (*httptest.Server, *SessionManager, *SafeWriteEnds)
- func WaitForBinaryContaining(t *testing.T, ws *websocket.Conn, substr string, timeout time.Duration) []byte
- func WaitForPTYReady(t *testing.T, sm *SessionManager, sessionID string, timeout time.Duration)
- type AgentDetectFunc
- type AgentStatePushFunc
- type Config
- type CreateOptions
- type ErrorPayload
- type Hooks
- type HudLogRequest
- type InProcessService
- func (s *InProcessService) Close() error
- func (s *InProcessService) Create(_ context.Context, opts CreateOptions) (*SessionInfo, error)
- func (s *InProcessService) Delete(_ context.Context, id string) error
- func (s *InProcessService) Get(_ context.Context, id string) (*SessionInfo, error)
- func (s *InProcessService) Input(_ context.Context, id string, data []byte) error
- func (s *InProcessService) List(_ context.Context) ([]SessionInfo, error)
- func (s *InProcessService) PasteUpload(_ context.Context, id string, filename string, content io.Reader, ...) (string, error)
- func (s *InProcessService) Resize(_ context.Context, id string, cols, rows int) error
- func (s *InProcessService) TailOutput(_ context.Context, id string, lines int) ([]string, error)
- type InputPayload
- type Option
- type PTYFactory
- type PTYStartOptions
- type PreemptedPayload
- type ResizePayload
- type RingBuffer
- type SafeWriteEnds
- type Server
- type Session
- func (s *Session) Done() <-chan struct{}
- func (s *Session) GetExitCode() int
- func (s *Session) GetLastActive() time.Time
- func (s *Session) GetStatus() SessionStatus
- func (s *Session) GetTmuxDetected() bool
- func (s *Session) ShellPID() int
- func (s *Session) TailOutput(n int) []string
- func (s *Session) WorkingDir() string
- type SessionInfo
- type SessionManager
- func (m *SessionManager) ClearActiveConn(sessionID string, conn *websocket.Conn)
- func (m *SessionManager) CloseAll() error
- func (m *SessionManager) Create(name string) (*Session, error)
- func (m *SessionManager) CreateWithOptions(opts CreateOptions) (*Session, error)
- func (m *SessionManager) Destroy(id string) error
- func (m *SessionManager) DestroyAll()
- func (m *SessionManager) Get(id string) (*Session, error)
- func (m *SessionManager) List() []*Session
- func (m *SessionManager) SetActiveConn(sessionID string, conn *websocket.Conn, cancel context.CancelFunc)
- func (m *SessionManager) Subscribe(sess *Session, subID string) (<-chan []byte, func())
- type SessionMeta
- type SessionMetaPayload
- type SessionSnapshot
- type SessionStatus
- type SessionStore
- type SessionSummary
- type SessionV2
- type ShellExitPayload
- type TermMsg
- type TerminalSessionService
- type TmuxNavPayload
- type Tunnel
- type WSControlMessage
Constants ¶
const ( // MsgInput 键盘输入 / 控制序列 (客户端→服务端) MsgInput = "input" // MsgResize 窗口大小变化 (客户端→服务端) MsgResize = "resize" // MsgPing 心跳 (客户端→服务端) MsgPing = "ping" )
const ( // MsgOutput PTY 输出 ANSI 透传 (服务端→客户端) MsgOutput = "output" // MsgClosed 终端关闭通知 (服务端→客户端) MsgClosed = "closed" // MsgPong 心跳响应 (服务端→客户端) MsgPong = "pong" )
const ( ClosedReasonNormal = "normal" ClosedReasonCrashed = "crashed" ClosedReasonStopCalled = "stop_called" )
const ( // AdapterStateSpawning PTY 正在启动 AdapterStateSpawning = "spawning" // AdapterStateRunning PTY 运行中 AdapterStateRunning = "running" // AdapterStateStopping PTY 正在停止 AdapterStateStopping = "stopping" // AdapterStateStopped PTY 已停止(正常) AdapterStateStopped = "stopped" // AdapterStateFailed PTY 异常退出 AdapterStateFailed = "failed" )
const ( MsgTypeResize = "resize" MsgTypeHeartbeat = "heartbeat" MsgTypeHeartbeatAck = "heartbeat_ack" MsgTypePing = "ping" MsgTypePong = "pong" MsgTypeAuthRefresh = "auth_refresh" MsgTypeShellExit = "shell_exit" MsgTypeError = "error" MsgTypePreempted = "preempted" MsgTypeInput = "input" // client → server: terminal input as text frame (WKWebView binary frame fix) MsgTypeSessionMeta = "session_meta" // server → client: pushed once after WS handshake MsgTypeAgentState = "agent_state" // server → client: agent state push (replaces SSE) )
Control message type constants.
const DefaultBufferCapacity = 1024 * 1024
DefaultBufferCapacity is the default size for the RingBuffer (1 MB). [Ref: T5-B3, CAP-terminal-io S4, DDC-03]
Variables ¶
This section is empty.
Functions ¶
func DefaultPTYFactory ¶
DefaultPTYFactory creates a real PTY. Tries independent process group (Setpgid) for DDC-01 SIGHUP isolation; falls back gracefully in restricted environments (containers, seccomp) where Setpgid is denied.
func DialTestWS ¶
func DialTestWS(t *testing.T, server *httptest.Server, sessionID string, token string) *websocket.Conn
DialTestWS opens a WebSocket connection to the test server for the given session.
func NewTestCLIServer ¶
func NewTestCLIServer(t *testing.T) (*httptest.Server, *SessionManager, *SafeWriteEnds)
NewTestCLIServer creates a test HTTP server with terminal routes (no auth). Returns the server, SessionManager, and write-end pipes for data injection.
func NewTestCLIServerWithAuth ¶
func NewTestCLIServerWithAuth(t *testing.T, authCode string) (*httptest.Server, *SessionManager, *SafeWriteEnds)
NewTestCLIServerWithAuth creates a test server with a specific auth code. Note: in tests, requests go through httptest which connects from 127.0.0.1, so auth is bypassed automatically for localhost connections.
func WaitForBinaryContaining ¶
func WaitForBinaryContaining(t *testing.T, ws *websocket.Conn, substr string, timeout time.Duration) []byte
WaitForBinaryContaining reads binary WS messages until one contains substr.
func WaitForPTYReady ¶
WaitForPTYReady waits for the session to appear in the SessionManager.
Types ¶
type AgentDetectFunc ¶
AgentDetectFunc detects an AI agent running under the given shell PID. Returns (tool, status) strings; empty tool means no agent detected.
type AgentStatePushFunc ¶
type AgentStatePushFunc func(ctx context.Context, sessionID string) (<-chan json.RawMessage, func(), error)
AgentStatePushFunc subscribes to agent state changes for a session. Returns a channel of JSON-encoded AgentIntelResponse and a cleanup function. Injected by the webui layer to avoid terminal → agent_intel import cycle.
type Config ¶
type Config struct {
Addr string // listen address, e.g. ":8022" (always 0.0.0.0)
DefaultShell string // e.g. "/bin/bash --login"
BufferSize int // ring buffer size in bytes, default 1MB
MaxSessions int // max concurrent sessions, default 100
AuthCode string // auto-generated auth code, printed to console on start
DataDir string // data directory for persistence
}
Config for the terminal session server.
type CreateOptions ¶
CreateOptions describes the product-level terminal session metadata and runtime options supplied by the WebUI.
type ErrorPayload ¶
ErrorPayload is the payload for an "error" control message.
type Hooks ¶
type Hooks struct {
OnSessionStart func(ctx context.Context, id string, meta SessionMeta) error
OnSessionEnd func(ctx context.Context, id string, exitCode int) error
OnOutput func(ctx context.Context, id string, data []byte) error
OnCommand func(ctx context.Context, id string, cmd string) (handled bool, err error)
Store SessionStore
// AgentDetect optionally enriches session list with agent tool/status.
// Injected by the host to avoid import cycles.
AgentDetect AgentDetectFunc
// AgentStatePush optionally subscribes to agent state changes for WS push.
// Injected by the host to avoid import cycles.
AgentStatePush AgentStatePushFunc
}
Hooks defines optional integration points for a host application. All fields are optional — nil means standalone behavior.
type HudLogRequest ¶
type HudLogRequest struct {
SessionID string `json:"sessionId"`
Timestamp string `json:"timestamp"`
UserAgent string `json:"userAgent"`
Screen json.RawMessage `json:"screen"`
Events json.RawMessage `json:"events"`
Snapshot json.RawMessage `json:"snapshot"`
}
HudLogRequest is the request body for POST /api/cli/debug/logs. [Ref: CAP-hud-diagnostics S4]
type InProcessService ¶
type InProcessService struct {
// contains filtered or unexported fields
}
InProcessService wraps SessionManager and implements TerminalSessionService. All calls are in-process; no network hop occurs. SessionManager is already thread-safe via sync.Map, so InProcessService inherits that guarantee.
func NewInProcessService ¶
func NewInProcessService(manager *SessionManager) *InProcessService
NewInProcessService creates an InProcessService backed by the given manager.
func (*InProcessService) Close ¶
func (s *InProcessService) Close() error
Close implements TerminalSessionService.
func (*InProcessService) Create ¶
func (s *InProcessService) Create(_ context.Context, opts CreateOptions) (*SessionInfo, error)
Create implements TerminalSessionService.
func (*InProcessService) Delete ¶
func (s *InProcessService) Delete(_ context.Context, id string) error
Delete implements TerminalSessionService.
func (*InProcessService) Get ¶
func (s *InProcessService) Get(_ context.Context, id string) (*SessionInfo, error)
Get implements TerminalSessionService.
func (*InProcessService) Input ¶
Input implements TerminalSessionService. Writes raw bytes directly to the session's PTY file descriptor.
func (*InProcessService) List ¶
func (s *InProcessService) List(_ context.Context) ([]SessionInfo, error)
List implements TerminalSessionService.
func (*InProcessService) PasteUpload ¶
func (s *InProcessService) PasteUpload(_ context.Context, id string, filename string, content io.Reader, sessionCWD string) (string, error)
PasteUpload implements TerminalSessionService. Saves content to a temp directory under the session's CWD (or sessionCWD if non-empty) and returns the absolute path of the saved file. The logic mirrors clipboard_paste.go but operates on io.Reader rather than gin.Context so that it can be called without an HTTP layer.
func (*InProcessService) TailOutput ¶
TailOutput implements TerminalSessionService.
type InputPayload ¶
type InputPayload struct {
Data []byte `json:"data"` // raw terminal bytes (JSON base64-encoded)
}
InputPayload carries terminal input bytes as a JSON text frame. [TH-0501-m9j] WKWebView drops rapid binary WS frames; text frames are reliable.
type Option ¶
type Option func(*Server)
Option configures a Server.
func WithAuthCode ¶
WithAuthCode sets the auth code (empty = no auth).
type PTYFactory ¶
PTYFactory creates a PTY-backed process. Returns the master side file descriptor, the command (may be nil for mock implementations), and an error. This abstraction allows testing without fork/exec.
type PTYStartOptions ¶
PTYStartOptions describes how a PTY-backed process should be started.
type PreemptedPayload ¶
type PreemptedPayload struct {
Message string `json:"message"`
}
PreemptedPayload is the payload for a "preempted" control message.
type ResizePayload ¶
ResizePayload is the payload for a "resize" control message. [Ref: T5-B3]
type RingBuffer ¶
type RingBuffer struct {
// contains filtered or unexported fields
}
RingBuffer is a fixed-capacity circular byte buffer used to store recent terminal output for replay on WebSocket reconnection. It is safe for concurrent use. [Ref: T5-B3, CAP-terminal-io S4, DDC-03] Testability: Len() int + IsFull() bool [Ref: T6-A4.1]
func NewRingBuffer ¶
func NewRingBuffer(capacity int) *RingBuffer
NewRingBuffer creates a RingBuffer with the given capacity. If capacity <= 0, DefaultBufferCapacity is used.
func (*RingBuffer) IsFull ¶
func (rb *RingBuffer) IsFull() bool
IsFull returns true if the buffer has been completely filled at least once. [Ref: T6-A4.1 testability requirement]
func (*RingBuffer) Len ¶
func (rb *RingBuffer) Len() int
Len returns the number of valid bytes currently stored. [Ref: T6-A4.1 testability requirement]
func (*RingBuffer) Read ¶
func (rb *RingBuffer) Read() []byte
Read returns all buffered data in chronological order. If the buffer has never wrapped, returns data from the start to writePos. If wrapped, returns data from writePos to end + start to writePos.
func (*RingBuffer) ReadTail ¶
func (rb *RingBuffer) ReadTail(n int) []byte
ReadTail returns the last n bytes from the buffer without copying the entire buffer. This minimizes mutex hold time — critical for avoiding PTY input backpressure.
type SafeWriteEnds ¶
type SafeWriteEnds struct {
// contains filtered or unexported fields
}
SafeWriteEnds provides thread-safe access to pipe write ends.
func (*SafeWriteEnds) CloseAt ¶
func (s *SafeWriteEnds) CloseAt(index int)
CloseAt closes the write end at the given index.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is a complete terminal session HTTP service. Standalone: ListenAndServe() runs API + SPA. Embedded: Handler() returns API routes for a host to mount.
func (*Server) Handler ¶
Handler returns the API routes (no SPA) for embedding into a host server. Routes are relative: GET /sessions, POST /sessions, GET /sessions/{id}/ws, etc. The host uses http.StripPrefix to mount at any path prefix.
func (*Server) ListenAndServe ¶
ListenAndServe starts the standalone server (API + SPA). The SPA is the embedded Vue frontend (built by build.sh).
type Session ¶
type Session struct {
ID string `json:"id"`
Name string `json:"name"`
Title string `json:"title"`
Engine string `json:"engine"`
CWD string `json:"cwd"`
ShellPath string `json:"-"`
PTY *os.File `json:"-"`
Cmd *exec.Cmd `json:"-"`
Buffer *RingBuffer `json:"-"`
Status SessionStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
LastActive time.Time `json:"lastActive"`
// TmuxDetected indicates whether the shell is running inside tmux.
// Set after session creation by checking /proc/{pid}/environ.
// [Ref: BUG-6, DDC-13]
TmuxDetected bool `json:"tmuxDetected"`
// contains filtered or unexported fields
}
Session represents a single terminal session backed by a PTY. [Ref: T5-B3]
func (*Session) Done ¶
func (s *Session) Done() <-chan struct{}
Done returns a channel that is closed when the PTY read loop exits.
func (*Session) GetExitCode ¶
GetExitCode returns the shell exit code (thread-safe).
func (*Session) GetLastActive ¶
GetLastActive returns when the session last received PTY output (thread-safe).
func (*Session) GetStatus ¶
func (s *Session) GetStatus() SessionStatus
GetStatus returns the session status (thread-safe).
func (*Session) GetTmuxDetected ¶
GetTmuxDetected returns whether tmux was detected (thread-safe).
func (*Session) TailOutput ¶
TailOutput returns the last n lines of terminal output from the RingBuffer. Used by agent intel for output analysis in direct (non-tmux) mode.
func (*Session) WorkingDir ¶
WorkingDir returns the working directory of the session.
type SessionInfo ¶
type SessionInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Title string `json:"title,omitempty"`
Engine string `json:"engine"`
CWD string `json:"cwd"`
Status SessionStatus `json:"status"`
CreatedAt string `json:"created_at"`
LastActive string `json:"last_active"`
ShellPID int `json:"shell_pid,omitempty"`
TmuxDetected bool `json:"tmux_detected,omitempty"`
ExitCode int `json:"exit_code,omitempty"`
}
SessionInfo is the service-layer representation of a terminal session. It avoids exposing the internal Session struct (with PTY, Cmd, etc.) to callers outside the package.
type SessionManager ¶
type SessionManager struct {
// contains filtered or unexported fields
}
SessionManager manages terminal sessions with PTY processes. All state is held in memory (IR-03: no DB, no persistence). [Ref: T5-B3, CAP-session-lifecycle S2, DDC-11]
func NewSessionManager ¶
func NewSessionManager(bufferSize int, defaultShell string) *SessionManager
NewSessionManager creates a new SessionManager.
func NewSessionManagerWithFactory ¶
func NewSessionManagerWithFactory(bufferSize int, defaultShell string, factory PTYFactory) *SessionManager
NewSessionManagerWithFactory creates a SessionManager with a custom PTY factory (for testing).
func (*SessionManager) ClearActiveConn ¶
func (m *SessionManager) ClearActiveConn(sessionID string, conn *websocket.Conn)
ClearActiveConn removes the active connection entry for a session if it matches the given conn.
func (*SessionManager) CloseAll ¶
func (m *SessionManager) CloseAll() error
CloseAll terminates all sessions. Called during server shutdown.
func (*SessionManager) Create ¶
func (m *SessionManager) Create(name string) (*Session, error)
Create creates a new terminal session with a PTY process. [Ref: T5-B3, T5-B4.M1, CAP-session-lifecycle S2]
func (*SessionManager) CreateWithOptions ¶
func (m *SessionManager) CreateWithOptions(opts CreateOptions) (*Session, error)
CreateWithOptions creates a new terminal session with product metadata.
func (*SessionManager) Destroy ¶
func (m *SessionManager) Destroy(id string) error
Destroy terminates a session's PTY process and removes it from the manager. [Ref: CAP-session-lifecycle S2]
func (*SessionManager) DestroyAll ¶
func (m *SessionManager) DestroyAll()
DestroyAll terminates all sessions. Kept for compatibility.
func (*SessionManager) Get ¶
func (m *SessionManager) Get(id string) (*Session, error)
Get returns a session by ID or an error if not found.
func (*SessionManager) SetActiveConn ¶
func (m *SessionManager) SetActiveConn(sessionID string, conn *websocket.Conn, cancel context.CancelFunc)
SetActiveConn registers a new active WS connection for a session, preempting any existing one. BUG-3: Only one WS connection per session is allowed at a time.
type SessionMeta ¶
SessionMeta is passed to OnSessionStart.
type SessionMetaPayload ¶
type SessionMetaPayload struct {
TmuxDetected bool `json:"tmux_detected"`
}
SessionMetaPayload is pushed to the client once after the WS replay buffer is sent. The client uses TmuxDetected to decide whether to show tmux gesture hints.
type SessionSnapshot ¶
SessionSnapshot is a point-in-time session state for persistence.
type SessionStatus ¶
type SessionStatus string
SessionStatus represents the lifecycle state of a terminal session. [Ref: T5-B3, CAP-session-lifecycle S2]
const ( StatusRunning SessionStatus = "running" StatusExited SessionStatus = "exited" )
type SessionStore ¶
type SessionStore interface {
SaveSession(ctx context.Context, s *SessionSnapshot) error
ListSessions(ctx context.Context) ([]SessionSummary, error)
}
SessionStore is optional persistence. nil = in-memory only (data lost on restart).
type SessionSummary ¶
SessionSummary is a brief session descriptor.
type SessionV2 ¶
type SessionV2 struct {
SessionID int64 // 关联 BS-03 Session.id (Int64)
WorkspaceID int64 // 关联 BS-10 Workspace.id (Int64)
RootDir string // Workspace.root_dir (PTY cwd)
Pty *os.File // PTY master fd
Cmd *exec.Cmd // Shell 进程
Ring *RingBuffer // 环形缓冲 (一屏内容 ≈ 4KB)
WSConn *websocket.Conn // 当前 WebSocket 连接 (可为 nil)
State string // spawning | running | stopping | stopped | failed
// contains filtered or unexported fields
}
SessionV2 是 BS-08 v2 维护的运行时状态(内存,不持久化)。 Session 持久化由 BS-03 的 conversationDB 负责。[T5 §2.1]
type ShellExitPayload ¶
type ShellExitPayload struct {
ExitCode int `json:"exitCode"`
}
ShellExitPayload is the payload for a "shell_exit" control message.
type TermMsg ¶
type TermMsg struct {
Type string `json:"type"`
Data string `json:"data,omitempty"` // input / output
Cols int `json:"cols,omitempty"` // resize
Rows int `json:"rows,omitempty"` // resize
Reason string `json:"reason,omitempty"` // closed
ExitCode int `json:"exit_code,omitempty"` // closed
}
TermMsg 是 WebSocket 文本帧的统一 JSON 格式 [T5 §5.2 §5.3]
type TerminalSessionService ¶
type TerminalSessionService interface {
// List returns all active sessions.
List(ctx context.Context) ([]SessionInfo, error)
// Create starts a new PTY-backed session with the given options.
Create(ctx context.Context, opts CreateOptions) (*SessionInfo, error)
// Get returns the session info for the given ID.
// Returns an error wrapping "not found" when the session does not exist.
Get(ctx context.Context, id string) (*SessionInfo, error)
// Delete destroys the session identified by id, killing the PTY process.
Delete(ctx context.Context, id string) error
// Resize sets the terminal window size for the session.
Resize(ctx context.Context, id string, cols, rows int) error
// Input writes raw terminal bytes to the session PTY.
// Used by the HTTP fallback path (WKWebView POST /sessions/:id/input).
Input(ctx context.Context, id string, data []byte) error
// PasteUpload saves clipboard content to a temp file under the session CWD
// and returns the absolute path. filename is the original filename hint;
// content is the raw file body; sessionCWD is the working directory to use
// (pass empty string to use the session's own CWD).
PasteUpload(ctx context.Context, id string, filename string, content io.Reader, sessionCWD string) (string, error)
// TailOutput returns the last n lines of terminal output for agent intel.
TailOutput(ctx context.Context, id string, lines int) ([]string, error)
// Close shuts down the service and destroys all sessions.
Close() error
}
TerminalSessionService is the service-layer interface for terminal session management. Implementations: InProcessService (delegates to SessionManager directly) or a future RemoteService (proxies to a TerminalMuxHost over HTTP). The webui layer and tests depend only on this interface, enabling the underlying transport to be swapped without touching callers.
type TmuxNavPayload ¶
type TmuxNavPayload struct {
}
TmuxNavPayload is the payload for a "tmux_nav" control message. The backend silently ignores the action when TmuxDetected=false.
type Tunnel ¶
type Tunnel struct {
// contains filtered or unexported fields
}
Tunnel manages a cloudflared quick-tunnel process.
type WSControlMessage ¶
type WSControlMessage struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload,omitempty"`
}
WSControlMessage represents a JSON control message on the WebSocket. Binary frames carry raw terminal I/O; Text/JSON frames carry control messages. [Ref: T5-B3, CAP-terminal-io S3, DDC-02]
func WaitForControlMessage ¶
func WaitForControlMessage(t *testing.T, ws *websocket.Conn, msgType string, timeout time.Duration) WSControlMessage
WaitForControlMessage reads WS messages until a control message of the given type is found.