tmux

package
v1.9.2 Latest Latest
Warning

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

Go to latest
Published: May 13, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Index

Constants

View Source
const SessionPrefix = "agentdeck_"

Variables

View Source
var ErrCaptureTimeout = errors.New("capture-pane timed out")

ErrCaptureTimeout is returned when CapturePane exceeds its timeout. Callers should preserve previous state rather than transitioning to error/inactive.

Functions

func BindMouseStatusRightDetach added in v0.26.4

func BindMouseStatusRightDetach() error

BindMouseStatusRightDetach binds a mouse click on the status-right area to detach. Only fires inside agentdeck sessions (guards against detaching the user's outer tmux).

func BindSwitchKey added in v0.8.43

func BindSwitchKey(key, targetSession string) error

BindSwitchKey binds a number key to switch to target session. Uses prefix table (default) so Ctrl+b N works. The key should be a single character like "1", "2", etc. Deprecated: Use BindSwitchKeyWithAck for notification bar integration.

func BindSwitchKeyWithAck added in v0.8.52

func BindSwitchKeyWithAck(key, targetSession, sessionID string) error

BindSwitchKeyWithAck binds a number key to switch to target session AND writes a signal file so agent-deck can acknowledge the session was selected. This enables proper acknowledgment when user presses Ctrl+b 1-6 shortcuts.

func CleanupOrphanedLogs added in v0.5.3

func CleanupOrphanedLogs() (removed int, freedBytes int64, err error)

CleanupOrphanedLogs removes log files for sessions that no longer exist A log is considered orphaned if: 1. No tmux session with matching name exists 2. The log file is older than 1 hour (to avoid race conditions during session creation)

func ClearStatusLeft added in v0.8.43

func ClearStatusLeft(sessionName string) error

ClearStatusLeft resets status-left to default for a session. Called when notifications are cleared or acknowledged.

func ClearStatusLeftGlobal added in v0.8.65

func ClearStatusLeftGlobal() error

ClearStatusLeftGlobal restores the original global status-left value. If the original value was captured, it is restored so the user's theme/plugin (e.g., tmux-oasis) is preserved. Falls back to unsetting the option only if no original value was captured.

func DefaultSocketName added in v1.7.50

func DefaultSocketName() string

DefaultSocketName returns the process-wide default socket name, or "" when the user has not configured isolation. Safe for concurrent use.

func DetectTerminal added in v0.3.0

func DetectTerminal() string

DetectTerminal identifies the current terminal emulator from environment variables Returns terminal name: "warp", "iterm2", "kitty", "alacritty", "vscode", "windows-terminal", or "unknown"

func EmitITermBadgeViaTty added in v1.7.72

func EmitITermBadgeViaTty(title string, configEnabled bool)

EmitITermBadgeViaTty writes the SetBadgeFormat OSC to /dev/tty wrapped in a tmux DCS passthrough envelope. Used by Claude rename-hook subprocesses that run inside a tmux pane: their stdout is owned by the hook protocol (writing OSC there would corrupt the response), but their controlling tty is the tmux pane's pty, so a wrapped OSC routes through tmux out to iTerm2 — this is what closes the gap when Claude renames the session mid-attach.

Silent no-op when:

  • not iTerm2 (or env / config disabled)
  • the process has no controlling tty (e.g. detached daemon)
  • the user isn't attached to this pane in iTerm2 (tmux buffers the pane output but iTerm2 never sees it — fine, the next attach emit in pty.go will catch up)

Debug: set AGENTDECK_ITERM_BADGE_DEBUG=1 to log decisions to /tmp/agent-deck-iterm-badge.log (timestamp, gate values, /dev/tty open result). Useful for diagnosing the multi-process chain Claude → hook subprocess → /dev/tty → tmux → iTerm2.

func EnsurePIDsDead added in v1.7.68

func EnsurePIDsDead(pids []int, timeout time.Duration)

EnsurePIDsDead blocks until every pid in `pids` is dead (signal-0 probe fails) or `timeout` elapses. Escalates SIGTERM → SIGKILL with a 500ms pause between stages. A zero-length slice is a no-op.

Callers in CLI processes should use this instead of scheduling ensureProcessesDead on a goroutine — see issue #59.

The PID-reuse guard in isOurProcess is preserved: if a captured PID has been recycled into an unrelated process, it's skipped.

func Exec added in v1.7.50

func Exec(socketName string, args ...string) *exec.Cmd

Exec is the public package counterpart to tmuxExec. Call sites outside internal/tmux (the session package, CLI helpers, web terminal bridge) use this when they have a socket name — typically Instance.TmuxSocketName — and need to spawn a one-off tmux subprocess. Pass "" for the user's default server.

This keeps the `-L <name>` plumbing centralised: there is exactly one place in the codebase that knows how to assemble a tmux argv, so a future socket-selection change (phase 2/3 — per-conductor sockets, env var fallback) only needs to be made here.

func ExecContext added in v1.7.50

func ExecContext(ctx context.Context, socketName string, args ...string) *exec.Cmd

ExecContext is the context-aware variant of Exec.

func ExpirePaneInfoCacheForTest added in v1.8.3

func ExpirePaneInfoCacheForTest(t testing.TB)

ExpirePaneInfoCacheForTest leaves the cache contents intact but rewinds the timestamp past the freshness threshold so GetCachedPaneInfo treats it as stale. Used to model the case where backgroundStatusUpdate hasn't run for a while (e.g. navigation hot-window) and the snapshot rebuild path must not blow away previously-known pane titles. t.Cleanup restores the timestamp so calling Expire alone (without a prior Seed that owns its own cleanup) is also safe.

func GetAckSignalPath added in v0.8.52

func GetAckSignalPath() (string, error)

GetAckSignalPath returns the path to the acknowledgment signal file

func GetActiveSession added in v0.8.43

func GetActiveSession() (string, error)

GetActiveSession returns the session name the user is currently attached to. Returns empty string and error if not attached to any session.

func GetAttachedSessions added in v0.8.65

func GetAttachedSessions() ([]string, error)

GetAttachedSessions returns the names of tmux sessions that have real clients attached. Used to detect which session the user is currently viewing. Filters out control mode clients (from PipeManager) which are not real user sessions.

func HasSession added in v1.7.8

func HasSession(name string) bool

HasSession is a lightweight public probe for session presence on the user's default tmux server. Exported so packages outside internal/tmux (e.g., the reviver) can answer "does this tmux session exist right now?" without reaching into unexported helpers. Runs a direct `tmux has-session -t <name>` — skips the cache on purpose because the reviver's purpose is to detect a mismatch between our cached view and ground truth.

Use HasSessionOnSocket when the caller knows the session's stored TmuxSocketName — critical for the reviver, which must not ask the default server about sessions that live on an isolated socket.

func HasSessionOnSocket added in v1.7.50

func HasSessionOnSocket(socketName, name string) bool

HasSessionOnSocket probes for a session on an explicit tmux server. Pass Instance.TmuxSocketName (or Session.SocketName) verbatim; empty means the user's default server.

func IndexCtrlQ added in v0.26.4

func IndexCtrlQ(data []byte) int

IndexCtrlQ returns the index of a Ctrl+Q sequence in data, or -1 if not found. This is a convenience wrapper around IndexDetachKey with the default Ctrl+Q byte.

func IndexDetachKey added in v0.26.4

func IndexDetachKey(data []byte, detachByte byte) int

IndexDetachKey returns the index of a control-key sequence in data, or -1 if not found. detachByte is the raw ASCII byte (e.g. 0x11 for Ctrl+Q). Handles three encodings:

  • Raw byte
  • xterm modifyOtherKeys: ESC[27;5;{keyCode}~
  • CSI u (kitty keyboard protocol): ESC[{keyCode};5u

func InitializeStatusBarOptions added in v0.8.67

func InitializeStatusBarOptions() error

InitializeStatusBarOptions sets optimal status bar options for agent-deck. Fixes truncation by setting adequate status-left-length globally. Should be called once during startup.

func IsServerAlive added in v1.3.1

func IsServerAlive() bool

IsServerAlive returns whether the tmux server was recently reachable. Result is cached for 5 seconds to avoid redundant checks.

func IsTmuxAvailable

func IsTmuxAvailable() error

IsTmuxAvailable checks if tmux is installed and accessible Returns nil if tmux is available, otherwise returns an error with details

func KillSessionsWithEnvValue added in v1.7.2

func KillSessionsWithEnvValue(envKey, envValue, excludeName string)

KillSessionsWithEnvValue kills agentdeck tmux sessions that have the given environment variable set to the given value, excluding the session named `excludeName`. This prevents duplicate tmux sessions running the same Claude conversation (#596).

func ListAgentDeckSessions added in v0.8.62

func ListAgentDeckSessions() ([]string, error)

ListAgentDeckSessions returns the names of all agentdeck tmux sessions. This is used to update notification bars across ALL sessions, not just those in the current profile. This ensures consistent notification bars when users switch between sessions.

func LogDir added in v0.3.0

func LogDir() string

LogDir returns the directory containing all session logs

func ReadAndClearAckSignal added in v0.8.52

func ReadAndClearAckSignal() string

ReadAndClearAckSignal reads the session ID from the signal file and deletes it. Returns empty string if no signal file exists or on error.

func RefreshExistingSessions added in v0.6.0

func RefreshExistingSessions()

RefreshExistingSessions is an alias for RefreshSessionCache for backwards compatibility

func RefreshPaneInfoCache added in v0.14.0

func RefreshPaneInfoCache()

RefreshPaneInfoCache updates the cache of pane titles and commands for all sessions. Call this ONCE per tick (from backgroundStatusUpdate), then use GetCachedPaneInfo() to read cached values. Tries PipeManager first, falls back to subprocess.

func RefreshSessionCache added in v0.6.0

func RefreshSessionCache()

RefreshSessionCache updates the cache of existing tmux sessions and their activity Call this ONCE per tick, then use Session.Exists() and Session.GetWindowActivity() which read from cache. This reduces 30+ subprocess spawns to just 1 per tick cycle.

Tries PipeManager first (zero subprocess), falls back to subprocess.

NOTE: We use window_activity (not session_activity) because window_activity updates when there's actual terminal output, while session_activity only updates on session-level events. This is critical for detecting when Claude is actively working.

func RefreshStatusBarImmediate added in v0.8.67

func RefreshStatusBarImmediate() error

RefreshStatusBarImmediate forces an immediate status bar redraw for ALL connected clients. This bypasses the status-interval timer (default 15s) for instant visual feedback. Uses -S flag which only refreshes the status line (lightweight operation ~1-2ms per client). Filters out control mode clients (from PipeManager) which don't have a visible status bar.

func ResetVersionWarningOnceForTest added in v1.7.70

func ResetVersionWarningOnceForTest()

ResetVersionWarningOnceForTest clears the sync.Once so tests can exercise the repeat-call path. Not for production use.

func RunLogMaintenance added in v0.5.3

func RunLogMaintenance(maxSizeMB int, maxLines int, removeOrphans bool)

RunLogMaintenance performs all log maintenance tasks based on settings This should be called once at startup and optionally periodically

func SeedPaneInfoCacheForTest added in v1.8.3

func SeedPaneInfoCacheForTest(t testing.TB, info map[string]PaneInfo)

SeedPaneInfoCacheForTest replaces the package's pane info cache with the supplied data and marks it fresh. Test cleanup wipes the cache back to its pristine zero state so concurrent or follow-on tests do not see seeded data.

Production callers must use RefreshPaneInfoCache; this exists so packages outside internal/tmux (notably internal/ui) can drive snapshot/render tests without standing up a real tmux server.

func SetDefaultSocketName added in v1.7.50

func SetDefaultSocketName(name string)

SetDefaultSocketName seeds the process-wide socket used by package-level tmux calls. Called once from main.go after config load and CLI flag parsing. Whitespace is trimmed; a blank or whitespace-only input clears the default (falls back to pre-v1.7.50 behavior).

func SetPipeManager added in v0.11.0

func SetPipeManager(pm *PipeManager)

SetPipeManager sets the global PipeManager instance (called once at startup).

func SetStatusLeft added in v0.8.43

func SetStatusLeft(sessionName, text string) error

SetStatusLeft sets the left side of tmux status bar for a session. Used by NotificationManager to display waiting session notifications.

func SetStatusLeftGlobal added in v0.8.65

func SetStatusLeftGlobal(text string) error

SetStatusLeftGlobal sets the left side of tmux status bar globally. This is a MAJOR performance optimization: ONE tmux call instead of 100+. All agentdeck sessions inherit this global setting. On first call, captures the existing status-left so ClearStatusLeftGlobal can restore it.

func SpinnerRuneSet added in v0.9.2

func SpinnerRuneSet() []rune

spinnerRuneSet returns the full set of spinner runes for content normalization. Includes both the "active-only" chars (used for busy detection) and the additional chars (·, ✻) that appear in done/other states but still need stripping for stable hashing.

func StopServiceUnit added in v1.7.21

func StopServiceUnit(sessionName string) error

StopServiceUnit best-effort stops + resets-failed the transient user-level service for the given session name. Called by agent-deck remove on service-mode sessions to guarantee the unit does not Restart=on-failure its way back into existence after removal. Errors are returned but callers typically log-and-continue.

Returns nil on non-systemd hosts (no-op), on already-stopped units, and on hosts where systemctl is missing — removal must not block on systemd availability.

The unit name derivation mirrors startCommandSpec's service branch: "agentdeck-tmux-" + sanitized(sessionName) + ".service".

func StripANSI

func StripANSI(content string) string

StripANSI removes ANSI escape codes from content using O(n) single-pass algorithm. This is important because terminal output contains color codes.

PERFORMANCE: Uses strings.Builder with pre-allocation for O(n) time complexity. Previous implementation used string concatenation in loops which was O(n²) and caused 2-11 second UI freezes on large terminal output (Issue #39).

NOTE: We intentionally avoid regex here because complex ANSI regex patterns can cause catastrophic backtracking on malformed escape sequences.

func StripSpinnerRunes added in v0.12.3

func StripSpinnerRunes(s string) string

StripSpinnerRunes removes all spinner characters in a single O(n) pass using strings.Map, replacing 16 sequential strings.ReplaceAll calls.

func SupportsHyperlinks() bool

SupportsHyperlinks returns true if the current terminal supports OSC 8 hyperlinks

func TruncateLargeLogFiles added in v0.5.3

func TruncateLargeLogFiles(maxSizeMB int, maxLines int) (truncated int, err error)

TruncateLargeLogFiles checks all log files and truncates any that exceed maxSizeMB

func TruncateLogFile added in v0.5.3

func TruncateLogFile(logPath string, maxLines int) error

TruncateLogFile truncates a log file to keep only the last maxLines lines This is called when a log file exceeds maxSizeBytes

func UnbindKey added in v0.8.43

func UnbindKey(key string) error

UnbindKey removes a key binding and restores default behavior. After unbinding, attempts to restore the default behavior where number keys select windows. The restore is best-effort since it may fail in environments without windows (e.g., CI) and agent-deck rebinds keys every 2s anyway.

func UnbindMouseStatusClicks added in v0.26.4

func UnbindMouseStatusClicks()

UnbindMouseStatusClicks removes mouse click bindings from the status bar.

func WarnIfVulnerableTmux added in v1.7.70

func WarnIfVulnerableTmux()

WarnIfVulnerableTmux prints a one-time stderr warning when the host tmux is known-vulnerable to the CONTROL_SHOULD_NOTIFY_CLIENT NULL-deref (tmux #4980, stability row S14, issue #737). No-op on non-macOS, no-op when AGENTDECK_SUPPRESS_TMUX_WARNING=1/true. Safe to call from main() unconditionally; gated by sync.Once so repeat invocations are free.

Types

type ControlPipe added in v0.11.0

type ControlPipe struct {
	// contains filtered or unexported fields
}

ControlPipe wraps a persistent `tmux -C attach-session -t <name>` process. It provides event-driven output detection via %output events and zero-subprocess command execution through the stdin/stdout pipe.

func NewControlPipe added in v0.11.0

func NewControlPipe(sessionName, socketName string) (*ControlPipe, error)

NewControlPipe starts a tmux control mode pipe attached to the given session on the given socket. socketName is the tmux `-L <name>` selector captured at session-creation time (Instance.TmuxSocketName / Session.SocketName); pass "" to target the user's default tmux server. Blocks until the initial handshake completes (or a short timeout), so the pipe is ready for SendCommand immediately after return. Retries a few times to smooth over transient tmux/control-mode startup failures.

func (*ControlPipe) CapturePaneVia added in v0.11.0

func (cp *ControlPipe) CapturePaneVia() (string, error)

CapturePaneVia sends capture-pane through the control mode pipe. Returns the pane content without spawning any subprocess.

func (*ControlPipe) Close added in v0.11.0

func (cp *ControlPipe) Close()

Close shuts down the control mode pipe.

Teardown is staged: (1) close stdin so the `tmux -C attach-session` child sees EOF and orderly-detaches via the control protocol's %exit path; (2) wait up to controlPipeEOFExitGrace (200ms) for that to complete — the vast majority of cases settle in 1-4ms; (3) only on timeout, escalate to softKillProcessGroup (SIGTERM+grace, SIGKILL fallback) for stuck or wedged clients.

The previous implementation went straight from stdin.Close() to softKillProcessGroup with no wait. Even the SIGTERM-with-grace form races tmux's server-side control_notify_client_detached walk (tmux/tmux#4980, present in macOS Homebrew tmux 3.6a) — the bug is server-side, so any signal-driven detach can trigger it. Letting the child self-exit on EOF goes through the protocol's orderly-detach codepath instead, which empirically does not trigger the crash. See ~/.claude/scratchpad/agent-deck/tmux-issues/PLAN.md "Empirical validation" for the measurements.

func (*ControlPipe) Done added in v0.11.0

func (cp *ControlPipe) Done() <-chan struct{}

Done returns a channel that closes when the pipe exits.

func (*ControlPipe) IsAlive added in v0.11.0

func (cp *ControlPipe) IsAlive() bool

IsAlive returns true if the control mode process is still running.

func (*ControlPipe) LastOutputTime added in v0.11.0

func (cp *ControlPipe) LastOutputTime() time.Time

LastOutputTime returns the time of the most recent %output event.

func (*ControlPipe) OutputEvents added in v0.11.0

func (cp *ControlPipe) OutputEvents() <-chan struct{}

OutputEvents returns a channel that fires when the session produces output. Multiple rapid outputs may be coalesced into fewer channel sends.

func (*ControlPipe) SendCommand added in v0.11.0

func (cp *ControlPipe) SendCommand(command string) (string, error)

SendCommand sends a command through the control mode pipe and waits for the response. Commands are serialized via cmdMu. Returns the response text or an error. Timeout is slightly relaxed to reduce false negatives when tmux is busy.

func (*ControlPipe) WindowEvents added in v0.21.0

func (cp *ControlPipe) WindowEvents() <-chan struct{}

WindowEvents returns a channel that fires when a window is added or closed.

type PaneInfo added in v0.14.0

type PaneInfo struct {
	Title          string
	CurrentCommand string
	Dead           bool
}

PaneInfo holds pane title and current command for a tmux session.

func GetCachedPaneInfo added in v0.14.0

func GetCachedPaneInfo(sessionName string) (PaneInfo, bool)

GetCachedPaneInfo returns cached pane info for a session. Returns (info, true) if found and cache is fresh, (zero, false) otherwise.

type PipeManager added in v0.11.0

type PipeManager struct {
	// contains filtered or unexported fields
}

PipeManager manages ControlPipes for all active tmux sessions. It provides zero-subprocess CapturePane and event-driven output detection. Falls back to subprocess execution when pipes are unavailable.

func GetPipeManager added in v0.11.0

func GetPipeManager() *PipeManager

GetPipeManager returns the global PipeManager instance. Returns nil if not initialized (control pipes disabled or not yet started).

func NewPipeManager added in v0.11.0

func NewPipeManager(ctx context.Context, onOutput func(sessionName string)) *PipeManager

NewPipeManager creates a new PipeManager. The onOutput callback is invoked whenever a connected session produces terminal output (via %output events).

func (*PipeManager) CapturePane added in v0.11.0

func (pm *PipeManager) CapturePane(sessionName string) (string, error)

CapturePane routes capture-pane through the control mode pipe if available. Falls back to subprocess execution if the pipe is nil, dead, or errors.

func (*PipeManager) Close added in v0.11.0

func (pm *PipeManager) Close()

Close shuts down all pipes and cancels the context.

func (*PipeManager) Connect added in v0.11.0

func (pm *PipeManager) Connect(sessionName, socketName string) error

Connect creates a control mode pipe for the given tmux session. If a pipe already exists and is alive, this is a no-op. Uses reconnecting map to prevent concurrent pipe creation for the same session. Connect opens a control-mode pipe to sessionName on the tmux server selected by socketName (Session.SocketName). Pass "" to target the user's default server. Safe to call repeatedly; a live pipe short-circuits and returns nil.

func (*PipeManager) ConnectedCount added in v0.11.0

func (pm *PipeManager) ConnectedCount() int

ConnectedCount returns the number of alive pipes.

func (*PipeManager) Disconnect added in v0.11.0

func (pm *PipeManager) Disconnect(sessionName string)

Disconnect closes and removes the pipe for the given session.

func (*PipeManager) GetPipe added in v0.11.0

func (pm *PipeManager) GetPipe(sessionName string) *ControlPipe

GetPipe returns the ControlPipe for a session, or nil if not connected.

func (*PipeManager) GetWindowActivity added in v0.11.0

func (pm *PipeManager) GetWindowActivity(sessionName string) (int64, error)

GetWindowActivity sends a display-message command through the pipe to get the window_activity timestamp. Falls back to error if pipe unavailable.

func (*PipeManager) IsConnected added in v0.11.0

func (pm *PipeManager) IsConnected(sessionName string) bool

IsConnected returns true if a session has an alive pipe.

func (*PipeManager) LastOutputTime added in v0.11.0

func (pm *PipeManager) LastOutputTime(sessionName string) time.Time

LastOutputTime returns the last output time for a session from its pipe. Returns zero time if no pipe or no output recorded.

func (*PipeManager) RefreshAllActivities added in v0.11.0

func (pm *PipeManager) RefreshAllActivities() (map[string]int64, map[string][]WindowInfo, error)

RefreshAllActivities sends a single list-windows command through any available pipe to get activity timestamps for ALL sessions. This replaces the subprocess call in RefreshSessionCache.

func (*PipeManager) RefreshAllPaneInfo added in v0.14.0

func (pm *PipeManager) RefreshAllPaneInfo() (map[string]PaneInfo, map[string]map[int]string, error)

RefreshAllPaneInfo sends a single list-panes command through any available pipe to get pane titles and current commands for ALL sessions. This provides the data needed for title-based state detection without subprocess spawns. Also returns per-window tool detection data for enriching the window cache.

func (*PipeManager) SetWindowChangeCallback added in v0.21.0

func (pm *PipeManager) SetWindowChangeCallback(cb func())

SetWindowChangeCallback sets the callback for window add/close events. Must be called before Connect to ensure all pipes forward events.

type PromptDetector

type PromptDetector struct {
	// contains filtered or unexported fields
}

PromptDetector checks for tool-specific prompts in terminal content Based on Claude Squad's exact implementation: https://github.com/smtg-ai/claude-squad/blob/main/session/tmux/tmux.go

func NewPromptDetector

func NewPromptDetector(tool string) *PromptDetector

NewPromptDetector creates a detector for the specified tool

func (*PromptDetector) HasPrompt

func (d *PromptDetector) HasPrompt(content string) bool

HasPrompt checks if the terminal content contains a prompt waiting for input These patterns are derived from Claude Squad + additional research for edge cases

type RawPatterns added in v0.9.2

type RawPatterns struct {
	BusyPatterns   []string // plain strings + "re:" prefixed regex
	PromptPatterns []string
	SpinnerChars   []string
	WhimsicalWords []string
}

RawPatterns holds string-form patterns before compilation. Patterns prefixed with "re:" are compiled as regex; everything else uses strings.Contains.

func DefaultRawPatterns added in v0.9.2

func DefaultRawPatterns(toolName string) *RawPatterns

DefaultRawPatterns returns the built-in detection patterns for a known tool. Returns nil for unknown tools (they have no defaults).

func MergeRawPatterns added in v0.9.2

func MergeRawPatterns(defaults, overrides, extras *RawPatterns) *RawPatterns

MergeRawPatterns merges defaults with overrides and extras.

  • If overrides has a field set (non-nil slice, even if empty), it replaces the default.
  • extras fields are appended to the result (after defaults or overrides).
  • If defaults is nil, only overrides/extras are used.

type ResolvedPatterns added in v0.9.2

type ResolvedPatterns struct {
	BusyStrings   []string
	BusyRegexps   []*regexp.Regexp
	PromptStrings []string
	PromptRegexps []*regexp.Regexp
	SpinnerChars  []string

	// Pre-built combo patterns (from WhimsicalWords + SpinnerChars)
	ThinkingPattern         *regexp.Regexp
	ThinkingPatternEllipsis *regexp.Regexp
	SpinnerActivePattern    *regexp.Regexp
}

ResolvedPatterns holds the compiled, ready-to-use patterns for status detection.

func CompilePatterns added in v0.9.2

func CompilePatterns(raw *RawPatterns) (*ResolvedPatterns, error)

CompilePatterns compiles raw string patterns into ready-to-use ResolvedPatterns. Patterns prefixed with "re:" are compiled as regex. Invalid regex patterns are logged as warnings and skipped (never crash).

type Session

type Session struct {
	Name        string
	DisplayName string
	WorkDir     string
	Command     string
	Created     time.Time
	InstanceID  string // Agent-deck instance ID for hook callbacks

	// SocketName is the tmux `-L <name>` socket selector for this session.
	// When empty (pre-v1.7.50 default), every tmux call targets the user's
	// default server at $TMUX_TMPDIR/tmux-<uid>/default, preserving the
	// historical behavior exactly. When non-empty, every tmux subprocess
	// spawned by methods on this Session carries `-L <SocketName>` so the
	// agent-deck tmux server is fully isolated from the user's interactive
	// tmux.
	//
	// SocketName is populated at session-creation time from (in precedence
	// order) the CLI flag `--tmux-socket`, then `[tmux].socket_name` in
	// config.toml, then empty. It is persisted per-instance in SQLite so
	// subsequent restarts/revives reach the correct server even if the
	// installation-wide config later changes. See RFC socket-isolation
	// phase 1 and Instance.TmuxSocketName. Never mutate after Start().
	SocketName string

	// OptionOverrides are user-specified tmux set-option overrides from config.
	// Applied AFTER all defaults in Start(), so they take precedence.
	// Keys are tmux option names, values are their settings.
	// Example: {"allow-passthrough": "all", "history-limit": "50000"}
	OptionOverrides map[string]string

	// RunCommandAsInitialProcess launches Start(command) as the pane's initial
	// process instead of sending it via SendKeysAndEnter after session creation.
	// Sandbox sessions enable this so pane-dead detection can restart exited tools.
	RunCommandAsInitialProcess bool

	// LaunchInUserScope starts the tmux server through systemd-run --user --scope
	// so the server is owned by the user's systemd manager instead of the current
	// login session scope.
	LaunchInUserScope bool

	// LaunchAs overrides the spawn form (v1.7.21+). Valid values:
	// "scope", "service", "direct", "auto", or "" (defer to
	// LaunchInUserScope). "service" uses systemd-run --user --unit
	// <NAME>.service with Type=forking + Restart=on-failure so tmux
	// auto-restarts on OOM / SIGKILL / unexpected death. Unknown values
	// fall through to LaunchInUserScope behavior — populated by callers
	// from TmuxSettings.GetLaunchAs which already canonicalises.
	LaunchAs string
	// contains filtered or unexported fields
}

Session represents a tmux session NOTE: All mutable fields are protected by mu. The Bubble Tea event loop is single-threaded, but we use mutex protection for defensive programming and future-proofing.

func DiscoverAllTmuxSessions

func DiscoverAllTmuxSessions() ([]*Session, error)

DiscoverAllTmuxSessions returns all tmux sessions (including non-Agent Deck ones)

func ListAllSessions

func ListAllSessions() ([]*Session, error)

ListAllSessions returns all Agent Deck tmux sessions

func NewSession

func NewSession(name, workDir string) *Session

NewSession creates a new Session instance with a unique name

func ReconnectSession

func ReconnectSession(tmuxName, displayName, workDir, command string) *Session

ReconnectSession creates a Session object for an existing tmux session This is used when loading sessions from storage - it properly initializes all fields needed for status detection to work correctly

Note: This runs immediate configuration (ConfigureStatusBar). For lazy loading during TUI startup, use ReconnectSessionLazy instead.

func ReconnectSessionLazy added in v0.8.95

func ReconnectSessionLazy(tmuxName, displayName, workDir, command string, previousStatus string) *Session

ReconnectSessionLazy creates a Session object without running any tmux configuration. PERFORMANCE: This is used during TUI startup to avoid subprocess overhead. Non-essential configuration (EnableMouseMode, ConfigureStatusBar) is deferred until first user interaction via EnsureConfigured().

Use this for bulk session loading where immediate configuration is not needed. For sessions that need immediate configuration, use ReconnectSession or ReconnectSessionWithStatus.

func ReconnectSessionWithStatus

func ReconnectSessionWithStatus(tmuxName, displayName, workDir, command string, previousStatus string) *Session

ReconnectSessionWithStatus creates a Session with pre-initialized state based on previous status This restores the exact status state across app restarts:

  • "idle" (gray): acknowledged=true, cooldown expired
  • "waiting" (yellow): acknowledged=false, cooldown expired
  • "active" (green): will be recalculated based on actual content changes

func (*Session) Acknowledge

func (s *Session) Acknowledge()

Acknowledge marks the session as "seen" by the user Call this when user attaches to the session

func (*Session) AcknowledgeWithSnapshot

func (s *Session) AcknowledgeWithSnapshot()

AcknowledgeWithSnapshot marks the session as seen and baselines the current content hash. Called when user detaches from session.

func (*Session) ApplySharedAcknowledged added in v0.14.0

func (s *Session) ApplySharedAcknowledged(ack bool)

ApplySharedAcknowledged applies acknowledgment state replicated from SQLite. Unlike Acknowledge/ResetAcknowledged, this only synchronizes the ack flag and does not force an immediate status transition. GetStatus() will naturally map to waiting/idle on the next poll based on busy/prompt conditions.

func (*Session) ApplyThemeOptions added in v0.26.2

func (s *Session) ApplyThemeOptions() error

func (*Session) Attach

func (s *Session) Attach(ctx context.Context, detachByte ...byte) error

Attach attaches to the tmux session with full PTY support. The configured detach key (default Ctrl+Q) will detach and return to the caller. Pass an optional detachByte to override the default (0x11 / Ctrl+Q).

func (*Session) AttachReadOnly

func (s *Session) AttachReadOnly(ctx context.Context) error

AttachReadOnly attaches to the session in read-only mode

func (*Session) AttachWindow added in v0.21.0

func (s *Session) AttachWindow(ctx context.Context, windowIndex int, detachByte ...byte) error

AttachWindow attaches to a specific window within this tmux session. Selects the target window first, then uses the standard Attach flow.

func (*Session) CaptureFullHistory

func (s *Session) CaptureFullHistory() (string, error)

CaptureFullHistory captures the scrollback history (limited to last 2000 lines for performance)

func (*Session) CapturePane

func (s *Session) CapturePane() (string, error)

CapturePane captures the visible pane content. Tries control mode pipe first (zero subprocess), falls back to subprocess. Uses singleflight to deduplicate concurrent calls.

func (*Session) CapturePaneFresh added in v0.19.11

func (s *Session) CapturePaneFresh() (string, error)

CapturePaneFresh captures pane content via a direct tmux subprocess call. Unlike CapturePane(), this bypasses the control-mode pipe and short-lived cache to provide a fresh snapshot. Use this for send verification where stale pane content can hide unsent composer input.

func (*Session) CaptureWindowFullHistory added in v0.21.0

func (s *Session) CaptureWindowFullHistory(windowIndex int) (string, error)

CaptureWindowFullHistory captures the scrollback history of a specific window (last 2000 lines).

func (*Session) ConfigureStatusBar added in v0.3.0

func (s *Session) ConfigureStatusBar()

ConfigureStatusBar sets up the tmux status bar with session info. Shows: notification bar on left (managed by NotificationManager), session info on right. NOTE: status-left is reserved for the notification bar showing waiting sessions. Options defined in tmux options are respected — agent-deck skips those keys.

func (*Session) ConfigureTerminalTitle added in v1.7.21

func (s *Session) ConfigureTerminalTitle()

ConfigureTerminalTitle sets tmux options that drive the outer terminal tab or window title for this session.

func (*Session) DetectTool

func (s *Session) DetectTool() string

DetectTool detects which AI coding tool is running in the session Uses caching to avoid re-detection on every call

func (*Session) EnableMouseMode

func (s *Session) EnableMouseMode() error

EnableMouseMode enables mouse scrolling, clipboard integration, and optimal settings Safe to call multiple times - just sets the options again

Enables: - mouse on: Mouse wheel scrolling, text selection, pane resizing - set-clipboard on: OSC 52 clipboard integration (works with modern terminals) - allow-passthrough on: OSC 8 hyperlinks, advanced escape sequences (tmux 3.2+) - escape-time 10: Fast Vim/editor responsiveness (default 500ms is too slow)

Terminal compatibility: - Warp, iTerm2, kitty, Alacritty, WezTerm: Full support (hyperlinks, clipboard, true color) - Windows Terminal, VS Code: Full support - Apple Terminal.app: Limited (no hyperlinks or clipboard)

Note: With mouse mode on, hold Shift while selecting to use native terminal selection instead of tmux's selection (useful for copying to system clipboard in some terminals)

func (*Session) EnsureConfigured added in v0.8.95

func (s *Session) EnsureConfigured()

EnsureConfigured runs deferred tmux configuration if not already done. PERFORMANCE: This should be called before attaching to a session or when the session needs full functionality (e.g., status bar, mouse mode).

Safe to call multiple times - does nothing if already configured or session doesn't exist. Thread-safe via mutex protection.

func (*Session) Exists

func (s *Session) Exists() bool

Exists checks if the tmux session exists Uses cached session list when available (refreshed by RefreshExistingSessions) Falls back to direct tmux call if cache is stale

func (*Session) ForceDetectTool

func (s *Session) ForceDetectTool() string

ForceDetectTool forces a re-detection of the tool, ignoring cache

func (*Session) GetCachedWindowActivity added in v0.10.6

func (s *Session) GetCachedWindowActivity() int64

GetCachedWindowActivity returns the cached window_activity timestamp without spawning a subprocess. Returns 0 if the cache is stale or session not found. This is used for cheap idle-session activity gating in tiered polling.

func (*Session) GetEnvironment added in v0.5.0

func (s *Session) GetEnvironment(key string) (string, error)

GetEnvironment gets an environment variable from this tmux session. Uses a 30-second cache to avoid spawning tmux show-environment subprocesses on every poll cycle. Call InvalidateEnvCache() after SetEnvironment to clear.

func (*Session) GetLastActivityTime added in v0.5.6

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

GetLastActivityTime returns when the session content last changed Returns zero time if no activity has been tracked

func (*Session) GetMouse added in v1.7.68

func (s *Session) GetMouse() bool

GetMouse reports whether tmux mouse mode is currently enabled for this session. Used by tests and by the Start / EnableMouseMode code paths to decide whether to set `mouse on`.

func (*Session) GetStatus

func (s *Session) GetStatus() (string, error)

func (*Session) GetWaitingSince added in v0.8.51

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

GetWaitingSince returns when the session transitioned to waiting status Returns zero time if session has never been waiting

func (*Session) GetWindowActivity added in v0.3.0

func (s *Session) GetWindowActivity() (int64, error)

GetWindowActivity returns Unix timestamp of last tmux window activity Uses cached data when available (refreshed by RefreshSessionCache) Falls back to direct tmux call if cache is stale

func (*Session) GetWorkDir

func (s *Session) GetWorkDir() string

GetWorkDir returns the current working directory of the tmux pane This is the live directory from the pane, not the initial WorkDir

func (*Session) HasUpdated

func (s *Session) HasUpdated() (bool, error)

HasUpdated checks if the pane content has changed since last check

func (*Session) InvalidateEnvCache added in v0.9.2

func (s *Session) InvalidateEnvCache()

InvalidateEnvCache clears the environment variable cache for this session. Should be called after SetEnvironment to ensure fresh reads.

func (*Session) IsAcknowledged added in v0.16.0

func (s *Session) IsAcknowledged() bool

IsAcknowledged returns whether the session has been acknowledged by the user. Used by the hook fast path to distinguish waiting (orange) from idle (gray).

func (*Session) IsClaudeRunning added in v0.5.3

func (s *Session) IsClaudeRunning() bool

IsClaudeRunning checks if Claude appears to be running in the session Returns true if Claude indicators are found

func (*Session) IsConfigured added in v0.8.95

func (s *Session) IsConfigured() bool

IsConfigured returns whether the session has been fully configured. Used for debugging and testing.

func (*Session) IsPaneDead added in v0.19.17

func (s *Session) IsPaneDead() bool

IsPaneDead returns true if the session's pane process has exited. Uses the cached pane info (refreshed once per tick) for zero-cost lookups. Falls back to a direct tmux query targeting pane 0.0 (the primary pane) to avoid false positives in multi-pane layouts.

func (*Session) Kill

func (s *Session) Kill() error

Kill terminates the tmux session. Like RespawnPane, this captures the process tree first and ensures all processes actually die. tmux kill-session sends SIGHUP which some CLI tools (e.g. Claude Code 2.1.27+) ignore, leaving orphan processes.

func (*Session) KillAndWait added in v1.7.68

func (s *Session) KillAndWait() error

KillAndWait is the synchronous variant of Session.Kill. When it returns, tmux kill-session has been run AND every pane process we captured before the kill has been verified dead (or reaped via SIGTERM/SIGKILL). Intended for short-lived CLI processes where the goroutine scheduled by Kill would be aborted on exit.

See issue #59 and the package-level docs above.

func (*Session) LogFile added in v0.3.0

func (s *Session) LogFile() string

LogFile returns the path to this session's log file Logs are stored in ~/.agent-deck/logs/<session-name>.log

func (*Session) ResetAcknowledged

func (s *Session) ResetAcknowledged()

ResetAcknowledged marks the session as needing attention Call this when a hook event indicates the agent finished (Stop, AfterAgent) This ensures the session shows yellow (waiting) instead of gray (idle)

func (*Session) Resize

func (s *Session) Resize(cols, rows int) error

Resize changes the terminal size of the tmux session

func (*Session) RespawnPane added in v0.5.4

func (s *Session) RespawnPane(command string) error

RespawnPane kills the current process in the pane and starts a new command. This is more reliable than sending Ctrl+C and waiting for shell prompt. The -k flag kills the current process before respawning.

IMPORTANT: After respawn, this function verifies that old processes actually died. Some CLI tools (notably Claude Code 2.1.27+) ignore SIGHUP sent by tmux respawn-pane, leaving orphan processes that consume CPU indefinitely. If old processes survive, we escalate through SIGTERM → SIGKILL.

func (*Session) SendCommand added in v0.5.1

func (s *Session) SendCommand(command string) error

SendCommand sends a command to the tmux session and presses Enter

func (*Session) SendCtrlC added in v0.5.1

func (s *Session) SendCtrlC() error

SendCtrlC sends Ctrl+C (interrupt signal) to the tmux session

func (*Session) SendCtrlU added in v0.5.3

func (s *Session) SendCtrlU() error

SendCtrlU sends Ctrl+U (clear line) to the tmux session

func (*Session) SendEnter

func (s *Session) SendEnter() error

SendEnter sends an Enter key to the tmux session

func (*Session) SendKeys

func (s *Session) SendKeys(keys string) error

SendKeys sends keys to the tmux session Uses -l flag to treat keys as literal text, preventing tmux special key interpretation

func (*Session) SendKeysAndEnter added in v0.12.2

func (s *Session) SendKeysAndEnter(keys string) error

SendKeysAndEnter sends literal text followed by Enter as two separate tmux calls with a short delay between them. The delay is necessary because tmux 3.2+ wraps send-keys -l in bracketed paste sequences (\e[200~...\e[201~). Without the delay, Enter arrives in the same PTY buffer as the paste-end marker and gets swallowed by async TUI frameworks (Ink/Node.js, curses).

func (*Session) SendKeysChunked added in v0.8.80

func (s *Session) SendKeysChunked(content string) error

SendKeysChunked sends large content to the tmux session in chunks to avoid tmux/OS buffer limits. Content ≤4KB is sent directly via SendKeys. Larger content is split at newline boundaries with a short delay between chunks.

func (*Session) SetClearOnRestart added in v1.4.1

func (s *Session) SetClearOnRestart(clear bool)

SetClearOnRestart controls whether RespawnPane clears the scrollback buffer. When false (default), previous output is preserved on restart.

func (*Session) SetCustomPatterns added in v0.8.27

func (s *Session) SetCustomPatterns(toolName string, busyPatterns, promptPatterns, detectPatterns []string)

SetCustomPatterns sets custom patterns for generic tool support These patterns enable custom tools defined in config.toml to have proper status detection

func (*Session) SetDetectPatterns added in v0.9.2

func (s *Session) SetDetectPatterns(toolName string, detectPatterns []string)

SetDetectPatterns sets tool auto-detection patterns (separate from busy/prompt patterns).

func (*Session) SetEnvironment added in v0.5.0

func (s *Session) SetEnvironment(key, value string) error

SetEnvironment sets an environment variable for this tmux session

func (*Session) SetInjectStatusLine added in v0.15.0

func (s *Session) SetInjectStatusLine(inject bool)

SetInjectStatusLine controls whether ConfigureStatusBar modifies tmux settings. When set to false, the status bar is left unchanged, preserving user's tmux config.

func (*Session) SetMouse added in v1.7.68

func (s *Session) SetMouse(enabled bool)

SetMouse controls whether tmux mouse mode is enabled for this session. When false, the inline `mouse on` set-option during Start is skipped AND EnableMouseMode becomes a no-op — required for VS Code Linux integrated terminal click-drag selection (issue #730).

func (*Session) SetPatterns added in v0.9.2

func (s *Session) SetPatterns(p *ResolvedPatterns)

SetPatterns sets the compiled ResolvedPatterns for configurable status detection. When set, hasBusyIndicator and normalizeContent use these instead of hardcoded values.

func (*Session) SetTerminalChromeEnabled added in v1.7.72

func (s *Session) SetTerminalChromeEnabled(enabled bool)

SetTerminalChromeEnabled controls whether Attach emits outer-terminal chrome (currently the iTerm2 badge) on attach/detach. Mirrors the SetInjectStatusLine plumbing pattern: callers in internal/session read `[terminal].iterm_badge` from user config and forward it here. AGENTDECK_ITERM_BADGE overrides this at runtime; see chrome.go.

func (*Session) Start

func (s *Session) Start(command string) error

Start creates and starts a tmux session. By default, command is sent after session creation (legacy behavior). When RunCommandAsInitialProcess is true, command is passed directly to tmux new-session and becomes the pane's initial process.

func (*Session) StreamOutput

func (s *Session) StreamOutput(ctx context.Context, w io.Writer) error

StreamOutput streams the session output to the provided writer

func (*Session) WaitForReady added in v0.7.0

func (s *Session) WaitForReady(timeout time.Duration) bool

WaitForReady polls the terminal until the agent is ready for input Ready state = NO busy indicator AND prompt visible This works for Claude ("> "), Gemini, and other agents

func (*Session) WaitForShellPrompt added in v0.5.3

func (s *Session) WaitForShellPrompt(timeout time.Duration) bool

WaitForShellPrompt polls the terminal until a shell prompt is detected Returns true if shell prompt found, false if timeout Shell prompts: $, #, %, ❯, ➜, or bare > at end of line

type SessionState

type SessionState string

SessionState represents the detected state of a session

const (
	StateIdle    SessionState = "idle"    // No activity, waiting for user
	StateBusy    SessionState = "busy"    // Actively working (output changing)
	StateWaiting SessionState = "waiting" // Showing a prompt, needs input
)

type SpinnerActivityTracker added in v0.14.0

type SpinnerActivityTracker struct {
	// contains filtered or unexported fields
}

SpinnerActivityTracker tracks when the spinner was last detected on screen. Used for the grace period between tool calls where the spinner briefly disappears.

This is intentionally simple: spinner PRESENCE from the curated char set (which excludes ✻ done marker and · non-spinner) is the reliable signal. No movement tracking needed because the char set itself distinguishes active vs done.

func NewSpinnerActivityTracker added in v0.14.0

func NewSpinnerActivityTracker() *SpinnerActivityTracker

NewSpinnerActivityTracker creates a tracker with default grace period.

func (*SpinnerActivityTracker) InGracePeriod added in v0.14.0

func (sat *SpinnerActivityTracker) InGracePeriod() bool

InGracePeriod returns true if an active spinner was visible recently. This covers the brief gap between tool calls where the spinner disappears before the next tool starts.

func (*SpinnerActivityTracker) MarkBusy added in v0.14.0

func (sat *SpinnerActivityTracker) MarkBusy()

MarkBusy records that an active spinner char is currently visible on screen.

type StateTracker

type StateTracker struct {
	// contains filtered or unexported fields
}

StateTracker tracks content changes for notification-style status detection

StateTracker implements a simple 3-state model:

GREEN (active)   = Content changed within 2 seconds
YELLOW (waiting) = Content stable, user hasn't seen it
GRAY (idle)      = Content stable, user has seen it

type TerminalInfo added in v0.3.0

type TerminalInfo struct {
	Name              string // Terminal name (warp, iterm2, kitty, alacritty, etc.)
	SupportsOSC8      bool   // Supports OSC 8 hyperlinks
	SupportsOSC52     bool   // Supports OSC 52 clipboard
	SupportsTrueColor bool   // Supports 24-bit color
}

TerminalInfo contains detected terminal information

func GetTerminalInfo added in v0.3.0

func GetTerminalInfo() TerminalInfo

GetTerminalInfo returns detailed terminal capabilities

type TitleState added in v0.14.0

type TitleState int

TitleState represents the state inferred from the tmux pane title. Claude Code sets pane titles via OSC escape sequences:

  • Braille spinner chars (U+2800-28FF) while actively working
  • Done markers (✳✻✽✶✢) when a task completes
const (
	TitleStateUnknown TitleState = iota // No recognizable pattern (non-Claude tools)
	TitleStateWorking                   // Braille spinner detected = actively working
	TitleStateDone                      // Done marker detected, fall through to prompt detection
)

func AnalyzePaneTitle added in v0.14.0

func AnalyzePaneTitle(title, _ string) TitleState

AnalyzePaneTitle determines session state from the pane title. Priority: Braille spinner > Done marker > Unknown.

NOTE: We intentionally do NOT use pane_current_command to detect "exited" state. Claude Code frequently spawns bash subprocesses for tool execution, and tmux reports that child process as pane_current_command. This means a waiting Claude session often shows "bash" as the command, making it indistinguishable from "Claude exited and shell is showing". The existing Exists() check handles truly dead sessions reliably.

type VersionProbe added in v1.7.70

type VersionProbe func() (string, error)

VersionProbe returns the raw output of `tmux -V`, e.g. "tmux 3.6a".

type WindowInfo added in v0.21.0

type WindowInfo struct {
	Index    int
	Name     string
	Activity int64
	Tool     string // Detected tool (claude, gemini, etc.) or empty
}

WindowInfo holds basic info about a tmux window within a session.

func GetCachedWindows added in v0.21.0

func GetCachedWindows(sessionName string) []WindowInfo

GetCachedWindows returns cached window info for a session with tool data merged in. Returns a copy — callers cannot mutate the cache. Returns nil if not found or cache is stale.

Jump to

Keyboard shortcuts

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