terminal

package module
v0.7.8 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 51 Imported by: 0

README

deepwork-terminal

English | 简体中文

A mobile-first web terminal for watching and steering your AI coding agents (Claude Code / Codex) — from your phone or a laptop on the go, reach and drive the desktop/laptop back home where the agent actually runs. With auth, Cloudflare tunnel, Web Push / WeChat notifications, and an embedded Vue frontend. Drop it into any Go application via a single HTTP handler.

Why deepwork-terminal

You kick off a long refactor in Claude Code and close your laptop. On the way out it stalls on a Proceed? [Y/n] — and the rest of the run is wasted. Agents are async, but you're mobile, and the terminal is pinned to one machine.

deepwork-terminal moves that terminal — the agent's status, its output files, the [Y/n] prompt — onto your phone's browser, and pings you (Web Push or WeChat) when an agent actually needs you. Tap the notification, land on the exact session, reply, get back to your day.

Four details that actually change the experience:

  1. Remote screenshot pasteCtrl/Cmd+V a screenshot on your PC; it lands in the agent's current working directory and the relative path is injected straight into the command line. Mobile file/photo upload works too — and since the input box is just a normal field, multi-line text and your phone's voice input go straight to the agent.
  2. Watch agents from your phone — install as a PWA for Web Push and tap to deep-link back to the exact session; WeChat (iLink official channel) as a backup; an agent status strip up top; a keyboard-aware viewport that never covers your input.
  3. tmux quick-keyboard bar — one row of buttons for copy / split / switch pane / new session, with your live tmux prefix shown — no shortcuts to memorize. No tmux? You can still run multiple terminals, just without pane splits.
  4. Cross-session continuity — a global upload index, input-history reuse, and a file drawer (image/text preview + fuzzy search) that follow you across sessions.

Who it's for: people who run agents heavily and step away from the desk often. Not for: someone who only codes in one window on one machine and never goes remote — you don't need this.

Features

Watch & steer AI coding agents

  • Live agent status (running / waiting / idle) for Claude Code & Codex, with per-session token & cost overview
  • Notify on Web Push (PWA) and WeChat (iLink official channel) when an agent needs you — tap to deep-link back to the exact session
  • Multi-device session takeover — switch between phone and PC mid-task

Mobile-first terminal

  • Full PTY terminal over WebSocket, with reconnect
  • Quick-keyboard bar for tmux (copy-mode, split / zoom / switch pane, new / list / detach session) — no shortcuts to memorize, dynamic prefix
  • Paste a screenshot with Ctrl/Cmd+V → lands in the active pane's cwd, path injected into the terminal; mobile upload too
  • Resource drawer: cross-session upload index, input-history reuse, image / text preview & fuzzy file search

Deploy anywhere

  • Embedded Vue SPA — zero static file serving; ships as a single binary
  • Optional Cloudflare Tunnel (auto-downloads cloudflared)
  • Hook points for auth, session lifecycle, and shell customization

Install

Fastest path — a prebuilt binary, no Go or Node required:

curl -fsSL https://raw.githubusercontent.com/brightman-ai/deepwork-terminal/main/install.sh | sh

Installs dw-terminal to ~/.local/bin for Linux (amd64/arm64) and macOS (universal). On WSL, this is the right path — it just works. Pin a version or change the directory:

curl -fsSL https://raw.githubusercontent.com/brightman-ai/deepwork-terminal/main/install.sh | sh -s -- --version=v0.5.1 --dir=/usr/local/bin
Homebrew (macOS / Linux)
brew install brightman-ai/tap/dw-terminal
Go (developers, Go ≥ 1.26)
go install github.com/brightman-ai/deepwork-terminal/cmd/dw-terminal@latest

Slow or blocked network? Set a proxy so module + Go-toolchain downloads succeed (./build.sh and install.sh --from-source already default to this):

GOPROXY=https://goproxy.cn,direct go install github.com/brightman-ai/deepwork-terminal/cmd/dw-terminal@latest

No Go installed but want a source build? The installer can bootstrap the latest stable Go:

curl -fsSL https://raw.githubusercontent.com/brightman-ai/deepwork-terminal/main/install.sh | sh -s -- --from-source --install-go
Manual download

Grab a tarball from the Releases page and put dw-terminal on your PATH.

Verify & run
dw-terminal --version
dw-terminal --addr :8022

Platform notes

  • Linux / WSL — one Linux binary covers both. On WSL2 a Windows browser reaches the server at http://localhost:<port>.
  • macOS — release binaries are signed with a Developer ID and notarized, so Gatekeeper allows them. (Homebrew and the install script also clear the quarantine flag as a fallback.)

Screenshots

Standard view — tmux panes + quick-key toolbar


Paste, type, or speak to the agent

Ctrl/Cmd+V a screenshot and the relative path is injected. The input box is also a normal field, so multi-line text and your phone's voice input go straight in.

Multi-line text input

Voice input via the system keyboard


Watch agents from your phone — multi-channel notify

Push "an agent is waiting" to wherever you already look — WeChat / Feishu / WeCom / DingTalk.

Multi-channel notifications

WeChat (iLink) quota: each message you send buys ~10 pushes; reply anything to refresh.

WeChat notify

WeChat notify

A status strip up top shows each session's state plus connection latency.

Health / latency strip


tmux without the shortcuts

Two toolbars — one for tmux (panes / sessions), one general — with your live prefix shown. No tmux? Multiple terminals still work.

Multiple terminals without tmux


Cross-session continuity — uploads, history, file drawer

Past uploads and the prompts you actually typed are reusable from any new session; the file drawer browses the tree and previews files in the browser.


More

Per-session token & cost overview; phone and PC share one session with takeover.

Session cost overview

Multi-device takeover

🔔 Notifications setup

When an agent needs you (waiting on [Y/n], or done), deepwork-terminal pushes to any enabled channel:

Channel Default Credential
WeChat (iLink) on scan-to-login
Browser Web Push (PWA) on grant permission
Feishu / Lark off webhook URL + secret (sign)
DingTalk off webhook URL + secret (sign)
WeCom (企业微信) off webhook URL (key in URL)
Slack off webhook URL

Everything is configured in Settings → Notifications. Each channel has an enable toggle, a 配置 Webhook form (URL + optional secret), and a 测试 (Test) button that fires a real message and echoes the honest backend result. URLs/secrets are encrypted at rest and redacted in the UI (secret is write-only).

To get each webhook URL:

Feishu / Lark
  1. In a group: Settings → Group Bots → Add Bot → Custom Bot.
  2. Security: choose 签名校验 (Signature verification) and copy the secret.
  3. Copy the Webhook URL (https://open.feishu.cn/open-apis/bot/v2/hook/…).
  4. App → 配置 Webhook → paste URL + Secret保存 → enable → 测试.
    • Sign mismatch returns 19021 — recheck the secret. (If you picked "keyword" mode instead, the message must contain that keyword — signature mode is recommended.)
🔔 DingTalk
  1. Group → Settings → Group Assistant → Add Bot → Custom.
  2. Security: choose 加签 (Additional signature) and copy the secret (starts with SEC).
  3. Copy the Webhook URL (https://oapi.dingtalk.com/robot/send?access_token=…).
  4. App → paste URL + Secret (加签)保存 → enable → 测试.
    • Sign error → 310000 sign not match. Avoid "keyword" mode (messages may be rejected).
WeCom (企业微信)
  1. Group → … → Group Bots → Add → Create.
  2. Copy the Webhook URL (https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=…). The key in the URL is the credential — no secret needed.
  3. App → paste URL (leave Secret blank) → 保存 → enable → 测试.
    • 93000 → the URL/key is wrong, or the bot was removed from the group.
Slack
  1. Create an incoming webhook: api.slack.com/apps → Create New App → Incoming Webhooks → Activate → Add New Webhook to Workspace, then pick a channel.
  2. Copy the Webhook URL (https://hooks.slack.com/services/…). The URL is the credential — no secret needed.
  3. App → paste URL (leave Secret blank) → 保存 → enable → 测试.
    • Success returns ok; a malformed request returns invalid_payload. The server must be able to reach hooks.slack.com (may need a proxy in some regions).

Limitations (honest)

A project that only sells itself isn't worth trusting — a few boundaries up front:

  • No native app — it's web-first (PWA), not an App Store application; iOS Web Push has platform caveats.
  • Early-stage — few stars yet; ecosystem, docs, and community are all early.
  • Local-only — it's a remote control for the machine the agent runs on, not a cloud agent-scheduler. You can still reach it over HTTPS from anywhere via the built-in Cloudflare Tunnel.

Quick Start (as a library)

go get github.com/brightman-ai/deepwork-terminal
import terminal "github.com/brightman-ai/deepwork-terminal"

srv := terminal.New(terminal.DefaultConfig())
http.Handle("/terminal/", srv.Handler())
http.ListenAndServe(":8080", nil)

Or run the CLI directly:

go run ./cmd/dw-terminal

See guide/ for full documentation.

Build from source

git clone https://github.com/brightman-ai/deepwork-terminal
cd deepwork-terminal
./build.sh

build.sh handles everything in one step:

  1. Clones (or pulls to latest) the CE App Shell (brightman-ai/deepwork) into a sibling directory
  2. Runs npm install to pick up any new frontend dependencies
  3. Builds the Vue frontend and embeds it into internal/spa/dist/
  4. Downloads any new Go module dependencies (go mod download)
  5. Compiles the Go binary → ./dw-terminal

To update to the latest version after an initial clone:

git pull
./build.sh

Requires: Go 1.26+, Node.js 18+, npm.

Headless servers: npm install browser-download hooks (Playwright/Puppeteer) are suppressed automatically via PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.

Go binary only (no frontend changes)

The frontend is pre-built and committed to internal/spa/dist/. If you only need to recompile the Go binary:

./build.sh --skip-frontend

Or manually:

go build -o dw-terminal ./cmd/dw-terminal/

Contributing

Open source under MIT. If you run Claude Code / Codex heavily and often work away from your desk, give it a try — a ⭐ or an issue is always welcome.

License

MIT

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

View Source
const (
	// MsgInput 键盘输入 / 控制序列 (客户端→服务端)
	MsgInput = "input"
	// MsgResize 窗口大小变化 (客户端→服务端)
	MsgResize = "resize"
	// MsgPing 心跳 (客户端→服务端)
	MsgPing = "ping"
)
View Source
const (
	// MsgOutput PTY 输出 ANSI 透传 (服务端→客户端)
	MsgOutput = "output"
	// MsgClosed 终端关闭通知 (服务端→客户端)
	MsgClosed = "closed"
	// MsgPong 心跳响应 (服务端→客户端)
	MsgPong = "pong"
)
View Source
const (
	ClosedReasonNormal     = "normal"
	ClosedReasonCrashed    = "crashed"
	ClosedReasonStopCalled = "stop_called"
)
View Source
const (
	// AdapterStateSpawning PTY 正在启动
	AdapterStateSpawning = "spawning"
	// AdapterStateRunning PTY 运行中
	AdapterStateRunning = "running"
	// AdapterStateStopping PTY 正在停止
	AdapterStateStopping = "stopping"
	// AdapterStateStopped PTY 已停止(正常)
	AdapterStateStopped = "stopped"
	// AdapterStateFailed PTY 异常退出
	AdapterStateFailed = "failed"
)
View Source
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)
	MsgTypeTmuxNav      = "tmux_nav"     // client → server: navigate tmux windows/sessions
	MsgTypeSessionMeta  = "session_meta" // server → client: pushed once after WS handshake
	MsgTypeAgentState   = "agent_state"  // server → client: agent state push (replaces SSE)
	MsgTypeTmuxState    = "tmux_state"   // server → client: tmux topology/prefix/agent-status push (terminal-owned)
)

Control message type constants.

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

func DefaultPTYFactory(opts PTYStartOptions) (*os.File, *exec.Cmd, error)

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.

The shell string may carry args (config.go documents e.g. "/bin/bash --login", and "tmux attach -t x" is a common case), so we tokenize it shell-words style before exec rather than passing the whole string as a single program path.

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

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

func WaitForPTYReady(t *testing.T, sm *SessionManager, sessionID string, timeout time.Duration)

WaitForPTYReady waits for the session to appear in the SessionManager.

Types

type AgentDetectFunc

type AgentDetectFunc func(ctx context.Context, shellPID int, cwd string) (tool, status string)

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
	Version      string // build version (e.g. "v0.4.0"), surfaced to the UI via GET /version; "dev" for source builds

	// VapidSubscriber is the VAPID JWT "sub" claim — a contact identifying the app
	// server to the push service. Apple APNs (iOS Web Push) REJECTS a token whose sub
	// is not a valid mailto: (real-format domain) or https: URL → 403 BadJwtToken.
	// Pass a BARE email (e.g. "you@your.dev") or an https: URL — webpush-go prepends
	// "mailto:" automatically (a "mailto:" prefix here would double to "mailto:mailto:…").
	// Empty → resolved from DW_VAPID_SUBSCRIBER, then defaultVapidSubscriber.
	VapidSubscriber string
}

Config for the terminal session server.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns sensible defaults.

type CreateOptions

type CreateOptions struct {
	Name   string
	Title  string
	Engine string
	Shell  string
	CWD    string
}

CreateOptions describes the product-level terminal session metadata and runtime options supplied by the WebUI.

type ErrorPayload

type ErrorPayload struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

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

Create implements TerminalSessionService.

func (*InProcessService) Delete

func (s *InProcessService) Delete(_ context.Context, id string) error

Delete implements TerminalSessionService.

func (*InProcessService) Get

Get implements TerminalSessionService.

func (*InProcessService) Input

func (s *InProcessService) Input(_ context.Context, id string, data []byte) error

Input implements TerminalSessionService. Writes raw bytes directly to the session's PTY file descriptor.

func (*InProcessService) List

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) Resize

func (s *InProcessService) Resize(_ context.Context, id string, cols, rows int) error

Resize implements TerminalSessionService.

func (*InProcessService) TailOutput

func (s *InProcessService) TailOutput(_ context.Context, id string, lines int) ([]string, error)

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 WithAddr

func WithAddr(addr string) Option

WithAddr sets the listen address.

func WithAuthCode

func WithAuthCode(code string) Option

WithAuthCode sets the auth code. When left empty, NewServer generates one.

func WithConfig

func WithConfig(c Config) Option

WithConfig replaces the entire config.

func WithHooks

func WithHooks(h Hooks) Option

WithHooks sets integration hooks.

func WithTmuxProvider added in v0.2.0

func WithTmuxProvider(p TmuxStateProvider) Option

WithTmuxProvider overrides the default in-process tmux provider. Hosts use this to supply a richer snapshot; standalone needs nothing.

type PTYFactory

type PTYFactory func(opts PTYStartOptions) (master *os.File, cmd *exec.Cmd, err error)

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

type PTYStartOptions struct {
	Shell string
	CWD   string
}

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

type ResizePayload struct {
	Cols int `json:"cols"`
	Rows int `json:"rows"`
}

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.

func (*RingBuffer) Reset

func (rb *RingBuffer) Reset()

Reset clears the buffer.

func (*RingBuffer) Write

func (rb *RingBuffer) Write(p []byte) (int, error)

Write appends data to the ring buffer, overwriting oldest data when full (FIFO). Returns the number of bytes written (always len(p)).

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.

func (*SafeWriteEnds) Get

func (s *SafeWriteEnds) Get(index int) *os.File

Get returns the write end at the given index (thread-safe).

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 NewServer

func NewServer(opts ...Option) (*Server, error)

NewServer creates a terminal session server.

func (*Server) Close

func (s *Server) Close() error

Close shuts down all sessions and stops the background push notifier.

func (*Server) Handler

func (s *Server) Handler() http.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

func (s *Server) ListenAndServe(ctx context.Context) error

ListenAndServe starts the standalone server (API + SPA). The SPA is the embedded Vue frontend (built by build.sh).

func (*Server) Port

func (s *Server) Port() int

Port returns the actual listening port (useful when Addr is ":0").

func (*Server) Service added in v0.2.0

func (s *Server) Service() TerminalSessionService

Service returns the in-process terminal session service owned by this server. Hosts should depend on this interface for product integrations such as agent state enrichment instead of reaching into SessionManager internals.

func (*Server) SetAuthCode added in v0.6.0

func (s *Server) SetAuthCode(code string)

SetAuthCode replaces the current auth code under lock. Exported so an embedding host (deepwork-pro) can keep this embedded terminal's gate in sync when it rotates its own auth code — the code pro injects via withTerminalHostAuth must match what this terminal compares against, or every forwarded request 401s.

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

func (s *Session) GetExitCode() int

GetExitCode returns the shell exit code (thread-safe).

func (*Session) GetLastActive

func (s *Session) GetLastActive() time.Time

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

func (s *Session) GetTmuxDetected() bool

GetTmuxDetected returns whether tmux was detected (thread-safe).

func (*Session) ShellPID

func (s *Session) ShellPID() int

ShellPID returns the PID of the shell process running in the PTY.

func (*Session) TailOutput

func (s *Session) TailOutput(n int) []string

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

func (s *Session) WorkingDir() string

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) List

func (m *SessionManager) List() []*Session

List returns all sessions.

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.

func (*SessionManager) Subscribe

func (m *SessionManager) Subscribe(sess *Session, subID string) (<-chan []byte, func())

Subscribe adds a subscriber channel for receiving PTY output. Returns a channel and an unsubscribe function.

type SessionMeta

type SessionMeta struct {
	Shell string
	CWD   string
}

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

type SessionSnapshot struct {
	ID     string
	Name   string
	Shell  string
	CWD    string
	Status string
}

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

type SessionSummary struct {
	ID     string
	Name   string
	Status string
}

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]

func (*SessionV2) GetState

func (cs *SessionV2) GetState() string

GetState 返回当前状态(线程安全)

func (*SessionV2) Lock

func (cs *SessionV2) Lock()

Lock 加锁保护 WSConn / State 访问

func (*SessionV2) SetState

func (cs *SessionV2) SetState(s string)

SetState 设置状态(线程安全)

func (*SessionV2) SetWSConn

func (cs *SessionV2) SetWSConn(conn *websocket.Conn) bool

SetWSConn 替换 WebSocket 连接(关闭旧连接),[T5 §6.1] 返回是否关闭了旧连接

func (*SessionV2) Unlock

func (cs *SessionV2) Unlock()

Unlock 释放锁

func (*SessionV2) WriteWS

func (cs *SessionV2) WriteWS(data []byte)

WriteWS 向当前 WebSocket 发送消息(线程安全) 若无连接则静默忽略

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 TmuxCopyMotioner added in v0.2.0

type TmuxCopyMotioner interface {
	CopyMotion(ctx context.Context, session, motion string) error
}

TmuxCopyMotioner is an OPTIONAL provider capability: drive a copy-mode scroll motion directly against the tmux server (bypassing the PTY keystroke stream, which silently no-ops for these motions). The default provider implements it; a host-injected provider may omit it, in which case the endpoint 501s gracefully.

type TmuxNavPayload

type TmuxNavPayload struct {
	Action string `json:"action"` // "window_next"|"window_prev"|"session_next"|"session_prev"
}

TmuxNavPayload is the payload for a "tmux_nav" control message. The backend silently ignores the action when TmuxDetected=false.

type TmuxOverviewToggler added in v0.7.0

type TmuxOverviewToggler interface {
	SetOverviewActive(v bool)
}

TmuxOverviewToggler is an OPTIONAL provider capability: gate per-window live-tail capture on whether a client has the Agent Overview open — so tail costs nothing when nobody's viewing it.

type TmuxRefresher added in v0.6.0

type TmuxRefresher interface {
	RefreshClient(ctx context.Context, shellPID int) error
}

TmuxRefresher is an OPTIONAL provider capability: force a full server-side redraw to the caller's tmux client. Used by the UI to resync xterm.js when its buffer has diverged from tmux's model (ghosting under fullscreen TUIs). Default provider implements it.

type TmuxSessionMaker added in v0.2.0

type TmuxSessionMaker interface {
	NewSession(ctx context.Context, shellPID int) (string, error)
}

TmuxSessionMaker is an OPTIONAL provider capability: create a new tmux session and switch the requesting client onto it (server-side, since keystroke-driven new-session is unreliable and refuses to nest inside a client). Default provider implements it.

type TmuxStateProvider added in v0.2.0

type TmuxStateProvider interface {
	TmuxState(ctx context.Context, shellPID int) (json.RawMessage, error)
}

TmuxStateProvider yields a JSON-encoded tmux topology snapshot for the host.

The terminal ships a default, in-process provider (defaultTmuxProvider) so the standalone build gets tmux topology/prefix/agent-status without any host wiring. A host (e.g. the pro repo) may inject a richer provider via WithTmuxProvider; this is purely additive and never required.

shellPID is the calling session's shell PID (0 when unknown); it lets the provider compute whether that specific shell is attached inside tmux.

type TmuxWindowSelector added in v0.7.6

type TmuxWindowSelector interface {
	SelectWindow(ctx context.Context, shellPID, index int) error
}

TmuxWindowSelector is an OPTIONAL provider capability: switch the requesting client onto a window by index, server-side. The keystroke route leaks a `select-window -t N` burst into the focused pane for index ≥10 (tmux binds prefix+digit only up to 9), so this drives it against the socket instead. The default provider implements it; a host-injected provider may omit it, in which case the endpoint 501s gracefully.

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.

Directories

Path Synopsis
cmd
dw-terminal command
internal
spa
Package notify is the host-agnostic notification provider abstraction shared by deepwork-terminal and deepwork-pro (imported like agentintel).
Package notify is the host-agnostic notification provider abstraction shared by deepwork-terminal and deepwork-pro (imported like agentintel).

Jump to

Keyboard shortcuts

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