session

package
v1.9.30 Latest Latest
Warning

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

Go to latest
Published: May 22, 2026 License: MIT Imports: 49 Imported by: 0

Documentation

Overview

Issue #1143: idle-timeout JSON helpers.

These thin wrappers merge / extract the idle_timeout_secs field on the tool_data blob without changing the positional MarshalToolData / UnmarshalToolData signatures. The MergeToolDataExtras layer in statedb preserves keys outside the typed schema across INSERT OR REPLACE, so a row written by an old binary survives a round-trip through a new binary (and vice versa).

Issue #1143: central poll watcher that auto-stops sessions whose tmux pane content hasn't changed for `IdleTimeoutSecs` seconds.

Design (per RFC in PR body):

  • One watcher per agent-deck process (TUI or daemon). It walks every instance on each Tick — no per-session goroutine.
  • The "idle" signal is the FNV-1a hash of the tmux capture-pane content. Anything cheaper (status transitions) doesn't catch the dormant-worker case the self-improvement report flagged on 2026-05-21.
  • On trigger: Stop callback fires + a single JSONL row is appended to ~/.agent-deck/logs/session-lifecycle.jsonl with action "idle-timeout-expired".
  • Idempotent: once a session is stopped, its lastSeen entry is dropped so a Restart() re-enters the watcher fresh.

Index

Constants

View Source
const (
	ConductorAgentClaude = "claude"
	ConductorAgentCodex  = "codex"

	ConductorSessionTitlePrefix     = "conductor-"
	ConductorHeartbeatMessagePrefix = "Heartbeat:"
	ConductorBridgeHeartbeatPrefix  = "[HEARTBEAT]"
)
View Source
const (
	// DefaultProfile is the name of the default profile
	DefaultProfile = "default"

	// ProfilesDirName is the directory containing all profiles
	ProfilesDirName = "profiles"

	// ConfigFileName is the global config file name
	ConfigFileName = "config.json"
)
View Source
const (
	FieldTitle              = "title"
	FieldPath               = "path"
	FieldCommand            = "command"
	FieldTool               = "tool"
	FieldWrapper            = "wrapper"
	FieldChannels           = "channels"
	FieldPlugins            = "plugins"
	FieldExtraArgs          = "extra-args"
	FieldColor              = "color"
	FieldNotes              = "notes"
	FieldClaudeSessionID    = "claude-session-id"
	FieldGeminiSessionID    = "gemini-session-id"
	FieldTitleLocked        = "title-locked"
	FieldNoTransitionNotify = "no-transition-notify"
	FieldSkipPermissions    = "skip-permissions"
	FieldAutoMode           = "auto-mode"
	FieldAccount            = "account"      // #924 per-session named account slot
	FieldIdleTimeout        = "idle-timeout" // #1143 auto-stop dormant sessions
)

Field names accepted by SetField. Kept as raw strings to match the `agent-deck session set <field>` CLI surface verbatim.

View Source
const (
	MinPreviewPct = 10
	MaxPreviewPct = 90
)

MinPreviewPct and MaxPreviewPct bound the preview width to keep both panes usable.

View Source
const (
	ITermOpenAsTab     = "tab"
	ITermOpenAsWindow  = "window"
	DefaultITermOpenAs = ITermOpenAsTab
)

iTerm "open as" modes for Shift+Enter dispatch.

View Source
const DefaultGroupName = "My Sessions"

DefaultGroupName is the display name for the default group where ungrouped sessions go

View Source
const DefaultGroupPath = "my-sessions"

DefaultGroupPath is the normalized path for the default group (used for lookups and protection)

View Source
const DefaultPreviewPct = 65

DefaultPreviewPct is the default preview-pane width percentage. Matches the historical hardcoded 0.35 sessions / 0.65 preview split.

View Source
const DefaultWorktreeSetupTimeout = 60 * time.Second

DefaultWorktreeSetupTimeout is the fallback used when no explicit value is configured. Kept small and visible so the git package can share it.

View Source
const LaunchdPlistName = "com.agentdeck.conductor-bridge"

LaunchdPlistName is the launchd label for the conductor bridge daemon

View Source
const ReasonIdleTimeoutExpired = "idle-timeout-expired"
View Source
const StreamSchemaVersion = "1"

StreamSchemaVersion identifies the event schema. Bump on any breaking change to field names or event shapes; consumers can branch on it.

View Source
const TierThresholdBalanced = 500 * 1024 * 1024

TierThresholdBalanced is the max size for balanced tier (500MB)

View Source
const TierThresholdInstant = 100 * 1024 * 1024

TierThresholdInstant is the max size for instant tier (100MB)

View Source
const TransitionNotifierLaunchdPlistName = "com.agentdeck.transition-notifier"

TransitionNotifierLaunchdPlistName is the launchd label for the transition notifier daemon.

View Source
const UnlimitedWorktreeSetupTimeout time.Duration = 0

UnlimitedWorktreeSetupTimeout is the sentinel returned by SetupTimeout() when the user has configured `setup_timeout_seconds = 0`. The git layer interprets this as "no deadline" (context.Background() instead of context.WithTimeout). Value chosen as 0 so the config value flows straight through to the git layer unchanged.

View Source
const UserConfigFileName = "config.toml"

UserConfigFileName is the TOML config file for user preferences

Variables

View Source
var (
	ErrSkillSourceExists    = errors.New("skill source already exists")
	ErrSkillSourceNotFound  = errors.New("skill source not found")
	ErrSkillNotFound        = errors.New("skill not found")
	ErrSkillAmbiguous       = errors.New("skill reference is ambiguous")
	ErrSkillUnsupportedKind = errors.New("skill is not a Claude-compatible directory skill")
	ErrSkillAlreadyAttached = errors.New("skill already attached")
	ErrSkillNotAttached     = errors.New("skill not attached")
	ErrSkillTargetConflict  = errors.New("skill target path conflict")
)
View Source
var ErrInsertNotPersistent = errors.New("insert not persistent: row dropped by concurrent writer")

ErrInsertNotPersistent is returned by InsertSessionAndVerify when, after retries, the row is still missing from the database. The most likely cause is a concurrent SaveInstances rewrite from another agent-deck process that loaded the instances slice before this INSERT landed and then DELETE'd the row via the `DELETE FROM instances WHERE id NOT IN (...)` step inside SaveInstances.

Surfacing this as a real error (rather than silently returning success) is the user-facing half of the issue #1031 fix.

View Source
var ErrProfileMissing = errors.New("target profile does not exist")

ErrProfileMissing is returned when the destination profile has no state.db (i.e., it has never been used). Profiles are otherwise lazily created.

View Source
var ErrRemovalNotPersistent = errors.New("removal not persistent: row resurrected by concurrent writer")

ErrRemovalNotPersistent is returned by RemoveSessionAndVerify when, after retries, the row is still observed in the database. The most likely cause is a concurrent SaveInstances rewrite from another agent-deck process that loaded the instances slice before this DELETE landed and re-inserted the row via INSERT OR REPLACE.

Surfacing this as a real error (rather than silently printing "✓ Removed") is the user-facing half of the issue #909 fix.

View Source
var ErrSameProfile = errors.New("source and target profile are the same")

ErrSameProfile is returned when source == target.

View Source
var ErrSessionRunning = errors.New("session is running; stop it first or pass --force")

ErrSessionRunning is returned when a session is in "running" status and Force is false. CLI handlers translate this to a non-zero exit with a hint.

View Source
var ErrStreamTimeout = errors.New("stream idle timeout")

ErrStreamTimeout is returned when IdleTimeout elapses with no progress.

View Source
var ValidDefaultFilters = map[string]bool{
	"":        true,
	"active":  true,
	"running": true,
	"waiting": true,
	"idle":    true,
	"error":   true,
}

ValidDefaultFilters lists acceptable values for DefaultFilter.

Functions

func AddSkillSource added in v0.19.1

func AddSkillSource(name, path, description string) error

AddSkillSource adds a new named local source path.

func ApplyProjectSkills added in v0.19.1

func ApplyProjectSkills(projectPath, tool string, desired []SkillCandidate) error

ApplyProjectSkills makes project attachments exactly match desired candidates. This is useful for TUI apply flows where users move items between columns.

func BridgeDaemonHint added in v0.16.0

func BridgeDaemonHint() string

BridgeDaemonHint returns a platform-appropriate hint for starting the bridge daemon.

func CheckClaudeHooksInstalled added in v0.16.0

func CheckClaudeHooksInstalled(configDir string) bool

CheckClaudeHooksInstalled checks if agent-deck hooks are present in settings.json.

func CheckGeminiHooksInstalled added in v0.25.0

func CheckGeminiHooksInstalled(configDir string) bool

CheckGeminiHooksInstalled checks whether required agent-deck Gemini hooks are installed.

func CleanStaleEventFiles added in v0.18.0

func CleanStaleEventFiles()

CleanStaleEventFiles removes event files older than 24 hours.

func ClearHookSessionAnchor added in v0.25.0

func ClearHookSessionAnchor(instanceID string)

ClearHookSessionAnchor removes the persisted hook session ID sidecar.

func ClearMCPCache added in v0.5.3

func ClearMCPCache(projectPath string)

ClearMCPCache invalidates the MCP cache for a project path and all parent directories This is important because getMCPInfoUncached walks up parent directories to find .mcp.json

func ClearProjectMCPs added in v0.5.3

func ClearProjectMCPs(projectPath string) error

ClearProjectMCPs removes all MCPs from projects[path].mcpServers in Claude's config

func ClearUserConfigCache added in v0.8.12

func ClearUserConfigCache()

ClearUserConfigCache clears the cached user config, allowing tests to reset state. This does NOT reload - the next LoadUserConfig() call will read fresh from disk. Resets both the cache pointer AND the tracked mtime so the invalidation state machine starts clean.

func ConductorDir added in v0.12.0

func ConductorDir() (string, error)

ConductorDir returns the base conductor directory (~/.agent-deck/conductor)

func ConductorNameDir added in v0.12.0

func ConductorNameDir(name string) (string, error)

ConductorNameDir returns the directory for a named conductor (~/.agent-deck/conductor/<name>)

func ConductorProfileDir added in v0.12.0

func ConductorProfileDir(profile string) (string, error)

ConductorProfileDir returns the per-profile conductor directory. Deprecated: Use ConductorNameDir instead. Kept for backward compatibility.

func ConductorSessionTitle added in v0.12.0

func ConductorSessionTitle(name string) string

ConductorSessionTitle returns the session title for a named conductor

func ConvertToClaudeDirName added in v0.8.49

func ConvertToClaudeDirName(path string) string

ConvertToClaudeDirName converts a filesystem path to Claude's directory naming format. Claude Code replaces all non-alphanumeric characters (except hyphens) with hyphens. Example: /Users/master/Code cloud/!Project → -Users-master-Code-cloud--Project

func CountRunningInGroup added in v1.9.1

func CountRunningInGroup(instances []*Instance, groupPath string) int

CountRunningInGroup returns the number of instances in the given group whose status is StatusRunning. Queued, stopped, and other-group instances are not counted.

func CreateExampleConfig added in v0.3.0

func CreateExampleConfig() error

CreateExampleConfig creates an example config file if none exists

func CreateProfile added in v0.3.0

func CreateProfile(profile string) error

CreateProfile creates a new empty profile

func DeduplicateDirnames added in v0.26.2

func DeduplicateDirnames(paths []string) []string

DeduplicateDirnames returns unique directory names for the given paths. When multiple paths share the same basename, a numeric suffix is appended (e.g., "src-1").

func DeleteProfile added in v0.3.0

func DeleteProfile(profile string) error

DeleteProfile deletes a profile and all its data

func EmitTelegramChannelDriftWarning added in v1.9.27

func EmitTelegramChannelDriftWarning(title, instanceID, configDir string, channels []string, result TelegramChannelEnablementResult)

EmitTelegramChannelDriftWarning logs a WARN-level structured entry when VerifyTelegramChannelEnabled returns OK=false for a session that owns a telegram channel. The code field is stable (`telegram_channel_plugin_drift`) so log-monitoring rules can trigger on it without parsing free-form text.

Intentionally a no-op when result.OK is true — callers can invoke it unconditionally on the prepare path.

func EventFingerprint added in v1.7.74

func EventFingerprint(e TransitionNotificationEvent) string

EventFingerprint returns a stable identifier for a transition event, keyed on its intrinsic identity rather than the moment of any particular emit. Two attempts to persist the "same" logical transition (same child, same status flip, same observed timestamp) collapse to the same fingerprint and are deduplicated by the inbox writer and the notifier-missed log.

Keying on Timestamp.UnixNano() is load-bearing: scheduleBusyRetry and the deferred-queue drain both fire from the same TransitionNotificationEvent, so the timestamp set by the daemon when it first observed the flip is stable across retry attempts. time.Now() at write-emit time would NOT be stable and would silently break dedup.

The fingerprint is a hex SHA-256 so it can safely be embedded in a JSON string field without escaping concerns and is cheap to grep for.

func ExpandPath added in v0.19.6

func ExpandPath(path string) string

ExpandPath expands environment variables and ~ prefix in a path. Use resolvePath when relative paths also need to be resolved against a working directory.

func FindAgentDeck added in v1.5.1

func FindAgentDeck() string

FindAgentDeck looks for agent-deck in common locations

func GenerateHeartbeatPlist added in v0.13.0

func GenerateHeartbeatPlist(name string, intervalMinutes int) (string, error)

GenerateHeartbeatPlist returns a launchd plist for a conductor's heartbeat timer

func GenerateID added in v1.3.1

func GenerateID() string

generateID generates a unique session ID GenerateID creates a unique session identifier.

func GenerateLaunchdPlist added in v0.12.0

func GenerateLaunchdPlist() (string, error)

GenerateLaunchdPlist returns a launchd plist with paths substituted

func GenerateSessionName added in v0.13.0

func GenerateSessionName() string

GenerateSessionName returns a random "adjective-noun" name.

func GenerateSystemdBridgeService added in v0.16.0

func GenerateSystemdBridgeService() (string, error)

GenerateSystemdBridgeService returns a systemd unit for the bridge daemon

func GenerateSystemdHeartbeatService added in v0.16.0

func GenerateSystemdHeartbeatService(name string) (string, error)

GenerateSystemdHeartbeatService returns a systemd service unit for a conductor heartbeat

func GenerateSystemdHeartbeatTimer added in v0.16.0

func GenerateSystemdHeartbeatTimer(name string, intervalMinutes int) string

GenerateSystemdHeartbeatTimer returns a systemd timer unit for a conductor heartbeat

func GenerateSystemdTransitionNotifierService added in v0.19.13

func GenerateSystemdTransitionNotifierService() (string, error)

GenerateSystemdTransitionNotifierService returns the systemd unit content for transition notifier.

func GenerateTransitionNotifierLaunchdPlist added in v0.19.13

func GenerateTransitionNotifierLaunchdPlist() (string, error)

GenerateTransitionNotifierLaunchdPlist returns a launchd plist for the transition notifier daemon.

func GenerateUniqueSessionName added in v0.13.0

func GenerateUniqueSessionName(instances []*Instance, groupPath string) string

GenerateUniqueSessionName generates a name that doesn't collide with existing session titles in the given group. Retries up to 10 times, then falls back to appending a timestamp.

func GetAgentDeckDir added in v0.3.0

func GetAgentDeckDir() (string, error)

GetAgentDeckDir returns the base agent-deck directory (~/.agent-deck)

func GetAvailableGeminiModels added in v0.8.79

func GetAvailableGeminiModels() ([]string, error)

GetAvailableGeminiModels returns a sorted list of Gemini models that support generateContent. Priority: 1) GEMINI_MODELS_OVERRIDE env var, 2) cached API result, 3) live API call, 4) fallback list.

func GetAvailableMCPNames added in v0.5.3

func GetAvailableMCPNames() []string

GetAvailableMCPNames returns sorted list of MCP names from config.toml

func GetAvailableMCPs added in v0.5.3

func GetAvailableMCPs() map[string]MCPDef

GetAvailableMCPs returns MCPs from config.toml as a map This replaces the old catalog-based approach with explicit user configuration

func GetAvailablePluginNames added in v1.9.2

func GetAvailablePluginNames() []string

GetAvailablePluginNames returns sorted catalog keys of plugins. Refused entries are excluded (consistent with GetAvailablePlugins).

func GetAvailablePlugins added in v1.9.2

func GetAvailablePlugins() map[string]PluginDef

GetAvailablePlugins returns the plugin catalog from config.toml, never nil. Filters out:

  • entries refused by IsTelegramOfficialRefusal (RFC §6)
  • entries failing validatePluginDef (RFC charset filter — security defense against path traversal, argv injection, lock-path escape)

Invalid entries are logged once per LoadUserConfig cycle and silently dropped — callers never see them, so unsafe values cannot reach exec, filesystem ops, or settings.json mutations.

func GetClaudeCommand added in v0.8.30

func GetClaudeCommand() string

GetClaudeCommand returns the configured Claude command/alias Priority: 1) UserConfig setting, 2) Default "claude" This allows users to configure an alias like "cdw" or "cdp" that sets CLAUDE_CONFIG_DIR automatically, avoiding the need for config_dir setting

func GetClaudeConfigDir added in v0.4.1

func GetClaudeConfigDir() string

GetClaudeConfigDir returns the Claude config directory for the active profile.

func GetClaudeConfigDirForGroup added in v1.5.4

func GetClaudeConfigDirForGroup(groupPath string) string

GetClaudeConfigDirForGroup returns the Claude config directory, walking the group chain: env > group > profile > global > default.

func GetClaudeConfigDirForInstance added in v1.5.4

func GetClaudeConfigDirForInstance(inst *Instance) string

GetClaudeConfigDirForInstance returns the Claude config directory for this Instance. Per-instance TOML overrides (conductor, group) beat the shell-wide CLAUDE_CONFIG_DIR env var; less-specific config (profile, global) falls to env and below.

Priority (most-specific → least-specific):

  1. Instance.Account (#924) → [profiles.<account>.claude].config_dir
  2. [conductors.<name>.claude].config_dir — consulted only when Instance.Title starts with "conductor-"
  3. [groups."<group>".claude].config_dir
  4. CLAUDE_CONFIG_DIR env var
  5. [profiles.<profile>.claude].config_dir
  6. [claude].config_dir
  7. ~/.claude

Why conductor/group beat env (fix-config-dir-priority, 2026-04-17): developer shells commonly export CLAUDE_CONFIG_DIR via aliases (cdp, cdw) to select a profile. When the user then writes an explicit [conductors.foo.claude] or [groups.bar.claude] block in config.toml, that TOML block is scoped to exactly that conductor/group and is semantically MORE specific than a shell-wide default. The old env-first order silently shadowed every TOML override. Profile/global remain beaten by env because they're shell-wide too (less specific than env in intent).

func GetClaudeConfigDirSourceForGroup added in v1.5.4

func GetClaudeConfigDirSourceForGroup(groupPath string) (path, source string)

GetClaudeConfigDirSourceForGroup returns (path, source) for the group chain. Sources: "env", "group", "profile", "global", "default".

func GetClaudeConfigDirSourceForInstance added in v1.5.4

func GetClaudeConfigDirSourceForInstance(inst *Instance) (path, source string)

GetClaudeConfigDirSourceForInstance returns (path, source) for the instance chain. Source labels: "account" (issue #924), "conductor", "group", "env", "profile", "global", "default".

func GetClaudeSessionID added in v0.4.1

func GetClaudeSessionID(projectPath string) (string, error)

GetClaudeSessionID returns the ACTIVE session ID for a project path It first tries to find the currently running session by checking recently modified .jsonl files, then falls back to lastSessionId from config

func GetCodexCommand added in v1.9.2

func GetCodexCommand() string

GetCodexCommand returns the configured Codex command/alias.

func GetConductorLastActivity added in v1.9.20

func GetConductorLastActivity(name, profile string) (time.Time, error)

GetConductorLastActivity returns the most recent persistent agent activity across sessions watched by the conductor. Conductors watch sessions in their profile, including sessions that are not parented under the conductor, so the activity scope includes every non-conductor session in the profile plus any conductor descendants. The conductor's own session is intentionally excluded so that heartbeat responses written to it do not reset the idle timer.

Returns zero time (and no error) when the conductor has no managed sessions; callers decide whether zero means "no data" or "idle".

func GetConfigPath added in v0.3.0

func GetConfigPath() (string, error)

GetConfigPath returns the path to the global config file

func GetCrushCommand added in v1.9.14

func GetCrushCommand() string

GetCrushCommand returns the configured crush command/alias. Mirrors GetCodexCommand on main: prefer the user config override, fall back to the bare binary name.

func GetCustomToolNames added in v0.8.87

func GetCustomToolNames() []string

GetCustomToolNames returns sorted custom tool names from config.toml, excluding names that shadow built-in tools (claude, gemini, opencode, codex, pi, shell, cursor, aider). Returns nil if no custom tools are configured.

func GetDBPathForProfile added in v0.11.0

func GetDBPathForProfile(profile string) (string, error)

GetDBPathForProfile returns the path to the state.db file for a specific profile.

func GetDefaultTool added in v0.4.3

func GetDefaultTool() string

GetDefaultTool returns the user's preferred default tool for new sessions Returns empty string if not configured (defaults to shell)

func GetDirectoryCompletions added in v0.8.86

func GetDirectoryCompletions(input string) ([]string, error)

GetDirectoryCompletions returns a list of directories that match the input prefix. Supports absolute, relative, and tilde-prefixed (~) paths.

func GetEffectiveProfile added in v0.3.0

func GetEffectiveProfile(explicit string) string

GetEffectiveProfile returns the profile to use, considering: 1. Explicitly provided profile (from -p flag) 2. Environment variable AGENTDECK_PROFILE 3. Inferred from CLAUDE_CONFIG_DIR (e.g. ~/.claude-work -> "work") 4. Config default profile 5. Fallback to "default"

Priority 3 was added to fix issue #881: prior to this, the TUI's profile.DetectCurrentProfile honored CLAUDE_CONFIG_DIR while the web / storage / push paths did not, so the same user on the same machine could see different sessions in TUI vs web. Both call sites now route through this function to guarantee a single source of truth.

func GetEventsDir added in v0.18.0

func GetEventsDir() string

GetEventsDir returns the path to the events directory.

func GetGeminiConfigDir added in v0.8.0

func GetGeminiConfigDir() string

GetGeminiConfigDir returns ~/.gemini Unlike Claude, Gemini has no GEMINI_CONFIG_DIR env var override

func GetGeminiMCPNames added in v0.8.1

func GetGeminiMCPNames() []string

GetGeminiMCPNames returns names of configured MCPs from settings.json

func GetGeminiSessionsDir added in v0.8.0

func GetGeminiSessionsDir(projectPath string) string

GetGeminiSessionsDir returns the chats directory for a project Format: ~/.gemini/tmp/<project_hash>/chats/

func GetGlobalHTTPPool added in v0.8.96

func GetGlobalHTTPPool() *mcppool.HTTPPool

GetGlobalHTTPPool returns the global HTTP pool instance (may be nil)

func GetGlobalMCPNames added in v0.5.3

func GetGlobalMCPNames() []string

GetGlobalMCPNames returns the names of MCPs currently in Claude's global config

func GetGlobalPool added in v0.6.2

func GetGlobalPool() *mcppool.Pool

GetGlobalPool returns the global socket pool instance (may be nil if disabled)

func GetGlobalPoolRunningCount added in v0.8.70

func GetGlobalPoolRunningCount() int

GetGlobalPoolRunningCount returns the number of running MCPs in the global pool

func GetGroupLevel

func GetGroupLevel(path string) int

GetGroupLevel returns the nesting level of a group (0 for root, 1 for child, etc.)

func GetHTTPServerStatus added in v0.8.96

func GetHTTPServerStatus(name string) string

GetHTTPServerStatus returns the status of an HTTP MCP server

func GetHooksDir added in v0.16.0

func GetHooksDir() string

GetHooksDir returns the path to the hooks status directory.

func GetHotkeyOverrides added in v0.21.0

func GetHotkeyOverrides() map[string]string

GetHotkeyOverrides returns user-configured hotkey overrides from config.toml.

Merge order (issue #434):

  1. Start from the `[hotkeys]` table.
  2. If `[tmux].detach_key` is set AND the caller has not already set `[hotkeys].detach`, layer tmux.detach_key into the hotkeys map as the "detach" action. Explicit `[hotkeys].detach` always wins so there is exactly one authoritative source of truth when both are present.

Returns nil only when nothing is configured in either table.

func GetMCPDefaultScope added in v0.10.11

func GetMCPDefaultScope() string

GetMCPDefaultScope returns the configured default MCP scope. Returns "local", "global", or "user". Defaults to "local" if unset or invalid.

func GetManageMCPJson added in v0.19.6

func GetManageMCPJson() bool

GetManageMCPJson returns whether agent-deck should write to .mcp.json files. Defaults to true when unset.

func GetProfileDir added in v0.3.0

func GetProfileDir(profile string) (string, error)

GetProfileDir returns the path to a specific profile's directory

func GetProfilesDir added in v0.3.0

func GetProfilesDir() (string, error)

GetProfilesDir returns the path to the profiles directory

func GetProjectMCPNames added in v0.5.3

func GetProjectMCPNames(projectPath string) []string

GetProjectMCPNames returns MCPs from projects[path].mcpServers in Claude's config

func GetProjectSkillsDir added in v1.7.32

func GetProjectSkillsDir(tool string) (string, bool)

GetProjectSkillsDir returns the runtime-managed project skill directory.

func GetProjectSkillsManifestPath added in v0.19.1

func GetProjectSkillsManifestPath(projectPath string) string

GetProjectSkillsManifestPath returns <project>/.agent-deck/skills.toml.

func GetProjectSkillsPath added in v1.7.32

func GetProjectSkillsPath(projectPath, tool string) string

GetProjectSkillsPath returns the runtime-specific project skills path.

func GetSessionIDLifecycleLogPath added in v0.28.0

func GetSessionIDLifecycleLogPath() string

GetSessionIDLifecycleLogPath returns ~/.agent-deck/logs/session-id-lifecycle.jsonl.

func GetSessionLifecycleLogPath added in v1.9.29

func GetSessionLifecycleLogPath() string

GetSessionLifecycleLogPath returns ~/.agent-deck/logs/session-lifecycle.jsonl. Distinct from session-id-lifecycle.jsonl (which logs bind/rebind), this covers process-level lifecycle decisions like idle-timeout-expired.

func GetSkillPoolPath added in v0.19.1

func GetSkillPoolPath() (string, error)

GetSkillPoolPath returns ~/.agent-deck/skills/pool.

func GetSkillSourcesPath added in v0.19.1

func GetSkillSourcesPath() (string, error)

GetSkillSourcesPath returns ~/.agent-deck/skills/sources.toml.

func GetSkillsRootPath added in v0.19.1

func GetSkillsRootPath() (string, error)

GetSkillsRootPath returns ~/.agent-deck/skills.

func GetTheme added in v0.8.16

func GetTheme() string

GetTheme returns the current theme, defaulting to "dark"

func GetToolBusyPatterns added in v0.3.0

func GetToolBusyPatterns(toolName string) []string

GetToolBusyPatterns returns busy patterns for a tool (custom + built-in)

func GetToolCommand added in v1.9.19

func GetToolCommand(toolName string) string

GetToolCommand returns the configured command override for a builtin tool, falling back to the bare tool name if no override is set.

func GetToolIcon added in v0.3.0

func GetToolIcon(toolName string) string

GetToolIcon returns the icon for a tool (custom or built-in)

func GetUserConfigPath added in v0.3.0

func GetUserConfigPath() (string, error)

GetUserConfigPath returns the path to the user config file

func GetUserMCPNames added in v0.8.69

func GetUserMCPNames() []string

GetUserMCPNames returns the names of MCPs in ~/.claude.json (ROOT config) These MCPs are loaded by ALL Claude sessions regardless of CLAUDE_CONFIG_DIR. This is different from GetGlobalMCPNames which reads from $CLAUDE_CONFIG_DIR/.claude.json

func GetUserMCPRootPath added in v0.8.69

func GetUserMCPRootPath() string

GetUserMCPRootPath returns the path to ~/.claude.json (ROOT config, always read by Claude) This is the ROOT config that Claude ALWAYS reads, regardless of CLAUDE_CONFIG_DIR setting. MCPs defined here apply to ALL Claude sessions globally.

func GetWebMutationsEnabled added in v1.7.75

func GetWebMutationsEnabled() bool

GetWebMutationsEnabled returns whether `agent-deck web` should accept mutating HTTP requests (POST/PATCH/DELETE). Defaults to true when the `[web].mutations_enabled` key is omitted from config.toml.

func GroupByProject

func GroupByProject(instances []*Instance) map[string][]*Instance

GroupByProject groups sessions by their parent project directory

func GroupMaxConcurrent added in v1.9.1

func GroupMaxConcurrent(tree *GroupTree, groupPath string) int

GroupMaxConcurrent returns the effective max_concurrent cap for groupPath by looking it up in the group tree. Returns 0 (unlimited) when the group is not found, preserving legacy behavior for groups without persisted metadata.

func GroupPathForProject added in v1.9.8

func GroupPathForProject(projectPath string) string

GroupPathForProject is the exported wrapper around extractGroupPath. It gives CLI callers (issue #972) a single source of truth for "what group does this project path imply" — matching what NewInstance assigns by default — so launch/add can prefer cwd-derived groups over inherited parent groups without duplicating the heuristic.

func HashProjectPath added in v0.8.0

func HashProjectPath(projectPath string) string

HashProjectPath generates SHA256 hash of absolute project path This matches Gemini CLI's project hash algorithm for session storage VERIFIED: echo -n "/Users/ashesh" | shasum -a 256 NOTE: Must resolve symlinks (e.g., /tmp -> /private/tmp on macOS)

func HeartbeatPlistLabel added in v0.13.0

func HeartbeatPlistLabel(name string) string

HeartbeatPlistLabel returns the launchd label for a conductor's heartbeat

func HeartbeatPlistPath added in v0.13.0

func HeartbeatPlistPath(name string) (string, error)

HeartbeatPlistPath returns the path where a conductor's heartbeat plist should be installed

func HookSessionAnchorPath added in v0.25.0

func HookSessionAnchorPath(instanceID string) string

HookSessionAnchorPath returns the sidecar file path used to persist the last known non-empty hook session ID for one instance.

func InboxDir added in v1.7.73

func InboxDir() string

InboxDir returns the directory that holds per-parent inbox files.

func InboxPathFor added in v1.7.73

func InboxPathFor(parentSessionID string) string

InboxPathFor returns the absolute inbox path for a given parent session id. The parent id is treated as a filename and must not contain path separators or shell metacharacters; agent-deck session ids are URL-safe by convention, so this is enforced by sanitizing rather than escaping.

func InboxTTL added in v1.9.10

func InboxTTL() time.Duration

InboxTTL returns the configured age past which persisted inbox events are swept. Honors AGENT_DECK_INBOX_TTL (parsed by time.ParseDuration) and falls back to defaultInboxTTL when unset or unparseable.

func InitializeGlobalPool added in v0.6.2

func InitializeGlobalPool(ctx context.Context, config *UserConfig, sessions []*Instance) (*mcppool.Pool, error)

InitializeGlobalPool creates and starts the global MCP pool

func InjectClaudeHooks added in v0.16.0

func InjectClaudeHooks(configDir string) (bool, error)

InjectClaudeHooks injects agent-deck hook entries into Claude Code's settings.json. Uses read-preserve-modify-write pattern to preserve all existing settings and user hooks. Returns true if hooks were newly installed, false if already present.

func InjectGeminiHooks added in v0.25.0

func InjectGeminiHooks(configDir string) (bool, error)

InjectGeminiHooks injects agent-deck hook entries into Gemini CLI settings.json. Uses read-preserve-modify-write pattern to preserve all existing settings and user hooks. Returns true if hooks were newly installed, false if already present.

Known limitation: this path does not currently use file locking, so concurrent writers to the same settings.json can race (last-writer-wins).

func InstallBridgeDaemon added in v0.16.0

func InstallBridgeDaemon() (string, error)

InstallBridgeDaemon installs and starts the bridge daemon. macOS: launchd plist; Linux: systemd user service. Returns the unit/plist file path on success.

func InstallBridgeScript added in v0.12.0

func InstallBridgeScript() error

InstallBridgeScript copies bridge.py to the conductor base directory. It writes from the embedded const.

func InstallHeartbeatDaemon added in v0.16.0

func InstallHeartbeatDaemon(name, profile string, intervalMinutes int) error

InstallHeartbeatDaemon installs and starts the heartbeat timer for a conductor. macOS: launchd plist; Linux: systemd timer/service pair.

func InstallHeartbeatScript added in v0.13.0

func InstallHeartbeatScript(name, profile string) error

InstallHeartbeatScript writes the heartbeat.sh script for a conductor. This is a standalone heartbeat that works without Telegram.

func InstallLearningsMD added in v0.19.11

func InstallLearningsMD() error

InstallLearningsMD writes the default LEARNINGS.md to the conductor base directory. This is the shared (Tier 1) learnings file for generic patterns across all conductors.

func InstallPolicyMD added in v0.19.6

func InstallPolicyMD(customPath string) error

InstallPolicyMD writes the default POLICY.md to the conductor base directory, or creates a symlink if customPath is provided. This contains agent behavior rules (auto-response policy, escalation guidelines).

func InstallSharedClaudeMD added in v0.12.0

func InstallSharedClaudeMD(customPath string) error

InstallSharedClaudeMD writes the shared CLAUDE.md to the conductor base directory. Deprecated: use InstallSharedConductorInstructions with the Claude agent.

func InstallSharedConductorInstructions added in v0.26.4

func InstallSharedConductorInstructions(agent, customPath string) error

InstallSharedConductorInstructions writes the shared instructions file for the given conductor agent, or creates a symlink if customPath is provided.

func InstallTransitionNotifierDaemon added in v0.19.13

func InstallTransitionNotifierDaemon() (string, error)

InstallTransitionNotifierDaemon installs and starts the transition notifier daemon.

func IsAtCap added in v1.9.1

func IsAtCap(running, max int) bool

IsAtCap reports whether the running count has reached the cap for a group. max <= 0 is treated as unlimited (never at cap).

func IsBridgeDaemonRunning added in v0.16.0

func IsBridgeDaemonRunning() bool

IsBridgeDaemonRunning checks if the bridge daemon is currently running.

func IsClaudeCompatible added in v0.21.0

func IsClaudeCompatible(toolName string) bool

IsClaudeCompatible returns true if the tool is "claude" or a custom tool whose underlying command is "claude". Use this for capability gates (session tracking, MCP, skills, hooks, etc.) where custom tools wrapping Claude should get full Claude functionality.

func IsClaudeConfigDirExplicit added in v0.8.12

func IsClaudeConfigDirExplicit() bool

IsClaudeConfigDirExplicit returns true when any priority level (env, profile, global) sets the dir.

func IsClaudeConfigDirExplicitForGroup added in v1.5.4

func IsClaudeConfigDirExplicitForGroup(groupPath string) bool

IsClaudeConfigDirExplicitForGroup returns true when any priority level sets the dir.

func IsClaudeConfigDirExplicitForInstance added in v1.5.4

func IsClaudeConfigDirExplicitForInstance(inst *Instance) bool

IsClaudeConfigDirExplicitForInstance returns true if ANY priority level sets a config dir for this Instance.

func IsCodexCompatible added in v1.7.35

func IsCodexCompatible(toolName string) bool

IsCodexCompatible returns true if the tool is "codex" or a custom tool whose underlying command is "codex". Use this for capability gates where custom tools wrapping Codex should get full Codex functionality without losing their configured tool identity.

func IsConductorHeartbeatMessage added in v1.9.20

func IsConductorHeartbeatMessage(message string) bool

func IsConductorSetup added in v0.12.0

func IsConductorSetup(name string) bool

IsConductorSetup checks if a named conductor is set up by verifying meta.json exists

func IsHTTPServerRunning added in v0.8.96

func IsHTTPServerRunning(name string) bool

IsHTTPServerRunning checks if an HTTP MCP server is running

func IsTelegramOfficialRefusal added in v1.9.2

func IsTelegramOfficialRefusal(name, source string) bool

IsTelegramOfficialRefusal reports whether (name, source) pair is the exact "telegram@claude-plugins-official" id refused in v1. The check is case-sensitive — the upstream catalog uses these literal strings.

func IsTransitionNotifierDaemonRunning added in v0.19.13

func IsTransitionNotifierDaemonRunning() bool

IsTransitionNotifierDaemonRunning checks if transition notifier daemon is running.

func IsValidSessionColor added in v1.7.70

func IsValidSessionColor(v string) bool

IsValidSessionColor validates a per-session color tint (issue #391). Accepts "", "#RRGGBB" hex, or ANSI 256-palette decimal "0".."255".

func LaunchdPlistPath added in v0.12.0

func LaunchdPlistPath() (string, error)

LaunchdPlistPath returns the path where the plist should be installed

func ListProfiles added in v0.3.0

func ListProfiles() ([]string, error)

ListProfiles returns all available profile names

func LoadSkillSources added in v0.19.1

func LoadSkillSources() (map[string]SkillSourceDef, error)

LoadSkillSources loads the global source registry. If no registry exists yet, defaults are returned.

func LogCgroupIsolationDecision added in v1.5.4

func LogCgroupIsolationDecision()

LogCgroupIsolationDecision emits exactly one structured log line per process describing the cgroup isolation decision the runtime made. The emitted message is one of these four exact strings (pinned by TestLogCgroupIsolationDecision_*):

  • "tmux cgroup isolation: enabled (systemd-run detected)"
  • "tmux cgroup isolation: disabled (systemd-run not available)"
  • "tmux cgroup isolation: enabled (config override)"
  • "tmux cgroup isolation: disabled (config override)"

Decision logic mirrors GetLaunchInUserScope: an explicit (non-nil) LaunchInUserScope wins, otherwise systemdAvailableForLog() decides. The sync.Once guarantees one-line-per-process even when called from multiple goroutines.

Satisfies OBS-01. Intended to be called once from the application bootstrap (cmd/agent-deck/main.go) immediately after logging.Init so the line lands in ~/.agent-deck/debug.log via lumberjack.

func MarshalToolOptions added in v0.8.39

func MarshalToolOptions(opts ToolOptions) (json.RawMessage, error)

MarshalToolOptions serializes tool options to JSON

func MergeToolPatterns added in v0.9.2

func MergeToolPatterns(toolName string) *tmux.RawPatterns

MergeToolPatterns returns merged RawPatterns for a tool, combining built-in defaults with any user overrides/extras from config.toml. Works for ALL tools: built-in (claude, gemini, etc.) and custom. Returns nil only if there are no defaults AND no config entry.

func MigrateClaudeProjectDir added in v1.7.28

func MigrateClaudeProjectDir(home, oldProjectPath, newProjectPath string, copy bool) error

MigrateClaudeProjectDir moves ~/.claude/projects/<oldSlug>/ to ~/.claude/projects/<newSlug>/ so `claude --resume` in the new project location picks up the prior conversation history.

  • No-op when the source dir doesn't exist (fresh sessions have nothing).
  • If copy=true, the source is preserved and the destination gets a recursive copy — useful when other sessions still reference oldPath.
  • If copy=false (default), rename is attempted; falls back to copy+remove across filesystems.
  • Errors when the destination already exists to avoid silent overwrite.

func MigrateConductorHeartbeatScripts added in v0.19.14

func MigrateConductorHeartbeatScripts() ([]string, error)

MigrateConductorHeartbeatScripts refreshes managed heartbeat scripts to the current template without touching custom user-authored scripts.

func MigrateConductorLearnings added in v0.19.11

func MigrateConductorLearnings() ([]string, error)

MigrateConductorLearnings backfills LEARNINGS.md files for existing conductors and updates per-conductor CLAUDE.md startup checklists to include the LEARNINGS.md reading step. It only rewrites non-symlink CLAUDE.md files that exactly match the pre-learnings generated template. Returns the names of conductors that were updated.

func MigrateConductorPolicySplit added in v0.19.6

func MigrateConductorPolicySplit() ([]string, error)

MigrateConductorPolicySplit updates legacy generated per-conductor CLAUDE.md templates to include POLICY.md instructions. It only rewrites non-symlink CLAUDE.md files that exactly match the legacy generated template.

func MigrateLegacyConductors added in v0.12.0

func MigrateLegacyConductors() ([]string, error)

MigrateLegacyConductors scans for conductor dirs that have CLAUDE.md but no meta.json, and creates meta.json for them. Returns the names of migrated conductors.

func NeedsMigration added in v0.3.0

func NeedsMigration() (bool, error)

NeedsMigration checks if migration from old layout is needed

func ParseIdleTimeoutFlag added in v1.9.29

func ParseIdleTimeoutFlag(value string) (int64, error)

ParseIdleTimeoutFlag parses a --idle-timeout flag value: a Go-style duration like "30m", "1h", "24h". Empty string and "0" mean disabled (returns 0). Negative durations are rejected so a typo like "-1s" surfaces loudly instead of silently disabling. Returns the value in seconds (int64).

func ProfileExists added in v0.3.0

func ProfileExists(profile string) (bool, error)

ProfileExists checks if a profile exists

func PruneMCPCache added in v0.10.6

func PruneMCPCache(maxAge time.Duration)

PruneMCPCache removes cache entries older than maxAge to prevent unbounded growth. Called periodically from the TUI tick handler.

func ReadHookSessionAnchor added in v0.25.0

func ReadHookSessionAnchor(instanceID string) string

ReadHookSessionAnchor reads the persisted hook session ID sidecar.

func ReadIdleTimeoutSecsFromToolData added in v1.9.29

func ReadIdleTimeoutSecsFromToolData(td json.RawMessage) int64

ReadIdleTimeoutSecsFromToolData extracts idle_timeout_secs from the blob. Returns 0 (disabled) for missing/malformed/legacy rows.

func ReadPrevEventStatus added in v0.18.0

func ReadPrevEventStatus(instanceID string) string

ReadPrevEventStatus reads the previous status from an existing event file. Returns empty string if no event file exists or can't be read.

func RefreshInstancesForCLIStatus added in v1.7.9

func RefreshInstancesForCLIStatus(instances []*Instance)

RefreshInstancesForCLIStatus is the CLI analogue of SessionDataService.refreshStatuses (internal/web) and Home.backgroundStatusUpdate (internal/ui). CLI JSON emitters must call it before iterating inst.UpdateStatus() so the tmux pane-title cache is warm and on-disk hook statuses are loaded — without this step the title fast-path in tmux.GetStatus cannot fire and long-running Claude sessions get reported as idle/waiting instead of running (issue #610).

Symmetry with the other two surfaces:

  • internal/ui/home.go:backgroundStatusUpdate
  • internal/web/session_data_service.go:refreshStatuses

func RefreshStalePluginPins added in v1.9.9

func RefreshStalePluginPins(mcpFile string, profileDirs []string) (int, error)

RefreshStalePluginPins scans mcpFile for mcpServers entries whose `command` or `args` strings reference <profileDir>/plugins/cache/<source>/<name>/<version>/... where <version> no longer exists on disk. Each stale reference is rewritten to point at the most recently installed version directory under the same <source>/<name>. Returns the number of entries that were modified.

If mcpFile does not exist, returns (0, nil). If no profileDirs are supplied or no pins are stale, returns (0, nil) without touching the file.

func RemoveClaudeHooks added in v0.16.0

func RemoveClaudeHooks(configDir string) (bool, error)

RemoveClaudeHooks removes agent-deck hook entries from Claude Code's settings.json. Returns true if hooks were removed, false if none found.

func RemoveGeminiHooks added in v0.25.0

func RemoveGeminiHooks(configDir string) (bool, error)

RemoveGeminiHooks removes agent-deck hook entries from Gemini CLI settings.json. Returns true if hooks were removed, false if none found.

func RemoveHeartbeatPlist added in v0.13.0

func RemoveHeartbeatPlist(name string) error

RemoveHeartbeatPlist removes the launchd plist for a conductor's heartbeat

func RemoveNotifyStateRecord added in v1.9.1

func RemoveNotifyStateRecord(childSessionID string) (bool, error)

RemoveNotifyStateRecord drops records[childSessionID] from runtime/transition-notify-state.json. Returns whether a record was actually present.

Idempotent: a second call on the same id returns (false, nil). A missing state file is treated as "no record present".

Issue #910 — the dedup ledger must release the removed child's slot, otherwise the next genuinely-delivered transition for a reused id would be (incorrectly) dedup-suppressed against the stale row.

func RemoveSkillSource added in v0.19.1

func RemoveSkillSource(name string) error

RemoveSkillSource removes a named source.

func ResetInboxFingerprintCacheForTest added in v1.7.74

func ResetInboxFingerprintCacheForTest()

ResetInboxFingerprintCacheForTest clears the process-local dedup cache. Tests use it to simulate a fresh process so the on-disk recovery path is exercised. Production code does not call this.

func ResolveCostLineTemplate added in v1.7.75

func ResolveCostLineTemplate(cfg *UserConfig, profile string) (template string, hideWhenZero bool)

ResolveCostLineTemplate returns the active status-bar cost-line template and hide-when-zero flag, applying the resolution chain:

profile.costs > [costs] > hardcoded "{cost_today} today" (template, default true for hide)

Pointer semantics:

  • nil at any level falls through to the next level
  • explicit empty string for template disables the segment (returned as "")
  • explicit bool for hide_when_zero is honored at that level

Safe to call with cfg == nil; returns the hardcoded default + true.

func ResolveTheme added in v0.15.0

func ResolveTheme() string

ResolveTheme resolves the configured theme to "dark" or "light". If theme is "system", detects the OS dark mode setting. Falls back to "dark" on detection failure.

func RestoreFromArchive added in v0.8.79

func RestoreFromArchive(baseDir string) error

RestoreFromArchive moves all files from archive/ subdirectories back to their parent directories under baseDir/profiles/*/.

func SaveConductorMeta added in v0.12.0

func SaveConductorMeta(meta *ConductorMeta) error

SaveConductorMeta writes meta.json for a conductor

func SaveConfig added in v0.3.0

func SaveConfig(config *Config) error

SaveConfig saves the global configuration

func SaveProjectSkillsManifest added in v0.19.1

func SaveProjectSkillsManifest(projectPath string, manifest *ProjectSkillsManifest) error

SaveProjectSkillsManifest writes project attachment state atomically.

func SaveSkillSources added in v0.19.1

func SaveSkillSources(sources map[string]SkillSourceDef) error

SaveSkillSources writes the source registry atomically.

func SaveUserConfig added in v0.8.15

func SaveUserConfig(config *UserConfig) error

SaveUserConfig writes the config to config.toml using atomic write pattern This clears the cache so next LoadUserConfig() reads fresh values

func SaveWatcherMeta added in v1.5.1

func SaveWatcherMeta(meta *WatcherMeta) error

SaveWatcherMeta writes meta.json for a watcher. Creates the watcher directory if it does not exist.

Uses a write-temp-rename atomic pattern: data is first written to meta.json.tmp, then renamed into place via os.Rename (POSIX atomic on the same filesystem). A mid-write crash therefore leaves either the old meta.json intact or the new meta.json complete — never a partial write. Any stale .tmp from a previous crash is overwritten on the next Save and removed on rename failure.

func ScrubProcessEnvForChildLaunch added in v1.9.9

func ScrubProcessEnvForChildLaunch(inst *Instance)

ScrubProcessEnvForChildLaunch removes TELEGRAM_* vars from the CURRENT process environment when the given instance represents a non-channel-owning claude child. Issue #955: `agent-deck launch` invoked from a conductor session inherits the conductor's TELEGRAM_STATE_DIR; without this strip the var propagates into the tmux server (which inherits the launching process env on first `new-session`) and from there into every subprocess in the new pane — Bash-tool spawns, fork claudes, restart respawn — even when the S8 exec-layer protects the immediate claude binary. Any of those descendants can load the Claude Code telegram plugin and start a second `bun telegram` poller against the conductor's bot, racing the conductor for the Bot API lock (HTTP 409) and silently dropping inbound messages.

#1133 broadens the scrub: TELEGRAM_STATE_DIR alone left a hole — conductors that also exported TELEGRAM_BOT_TOKEN (the plugin re-derives state from the token) still leaked the duplicate poller. We now unset every var named in telegramEnvVarsToStrip *and* any other TELEGRAM_-prefixed var present in os.Environ. The prefix sweep means a future plugin env addition (TELEGRAM_API_HASH etc.) is covered without code change here.

Reuses telegramStateDirStripExpr as the single source of truth for the strip predicate so this layer can never disagree with the shell-level and exec-level layers about which sessions own the telegram bot. No-op for conductors, explicit telegram channel owners, --inherit-telegram-env opt-ins, and non-claude tools.

func SendSessionMessageReliable added in v0.19.13

func SendSessionMessageReliable(profile, sessionRef, message string) error

SendSessionMessageReliable sends a message using the same queued/reliable semantics as `agent-deck session send` (default mode, no --no-wait). It invokes the CLI command to keep behavior identical across callers.

func SetDefaultProfile added in v0.3.0

func SetDefaultProfile(profile string) error

SetDefaultProfile sets the default profile in the config

func SetField added in v1.7.70

func SetField(inst *Instance, field, value string, extraArgsTokens []string) (oldValue string, postCommit func(), err error)

SetField is the single source of truth for session metadata edits — both `agent-deck session set` and the TUI EditSessionDialog call it.

postCommit is non-nil for fields that need a slow tmux subprocess (claude/gemini session-id env propagation). TUI callers must drop instancesMu before invoking it so the subprocess doesn't stall background readers; CLI callers run it inline.

extraArgsTokens supplies pre-tokenized argv for FieldExtraArgs (CLI path); when nil, FieldExtraArgs falls back to strings.Fields(value) (TUI path).

Persistence is the caller's responsibility.

func SetSSHRunnerRunFnForTest added in v1.9.24

func SetSSHRunnerRunFnForTest(r *SSHRunner, fn func(args ...string) ([]byte, error))

SetSSHRunnerRunFnForTest assigns the unexported runFn field on an SSHRunner. Tests in other packages (notably internal/ui) need this to stub out SSH command execution without spawning a real ssh subprocess.

Do not call from non-test code; runFn is meant for test injection only.

func SetupConductor added in v0.12.0

func SetupConductor(name, profile string, heartbeatEnabled bool, clearOnCompact bool, description string, customClaudeMD string, customPolicyMD string, customHeartbeatRulesMD string, env map[string]string, envFile string) error

SetupConductor creates a Claude conductor for backward compatibility. New callers should prefer SetupConductorWithAgent.

func SetupConductorProfile added in v0.12.0

func SetupConductorProfile(profile string) error

SetupConductorProfile creates a default Claude conductor for a profile. Deprecated: Use SetupConductor instead. Kept for backward compatibility.

func SetupConductorWithAgent added in v0.26.4

func SetupConductorWithAgent(name, profile, agent string, heartbeatEnabled bool, clearOnCompact bool, description string, customInstructionsMD string, customPolicyMD string, customHeartbeatRulesMD string, env map[string]string, envFile string, heartbeatIdleMinutes ...int) error

SetupConductorWithAgent creates the conductor directory, agent-specific instructions file, and meta.json. If customInstructionsMD is provided, creates a symlink instead of writing the template. If customPolicyMD is provided, creates a per-conductor POLICY.md symlink (overrides the shared POLICY.md). If customHeartbeatRulesMD is provided, creates a per-conductor HEARTBEAT_RULES.md symlink (overrides the shared HEARTBEAT_RULES.md and any per-profile override). It does NOT register the session (that's done by the CLI handler which has access to storage).

func ShouldNotifyTransition added in v0.19.13

func ShouldNotifyTransition(fromStatus, toStatus string) bool

func ShouldQueue added in v1.9.1

func ShouldQueue(instances []*Instance, groupPath string, maxConcurrent int) bool

ShouldQueue reports whether a new launch into groupPath must be queued because the group is at its MaxConcurrent cap.

func ShouldRestartProjectSkills added in v1.7.32

func ShouldRestartProjectSkills(tool string) bool

ShouldRestartProjectSkills reports whether agent-deck should auto-restart the session after project skill changes for this runtime.

func ShouldSkipRestart added in v1.7.39

func ShouldSkipRestart(inst *Instance, now time.Time, force bool) (bool, string)

ShouldSkipRestart decides whether `agent-deck session restart` should short-circuit instead of calling Instance.Restart().

Returns skip=true (with a human-readable reason) when the session is in a healthy state AND was started within restartFreshnessWindow. The force parameter (wired to the CLI --force flag) bypasses the guard.

A zero LastStartedAt is treated as "unknown" — we always permit the restart so users can recover legacy records.

func ShutdownGlobalPool added in v0.6.2

func ShutdownGlobalPool(shouldShutdown bool) error

ShutdownGlobalPool stops the global pools if shouldShutdown is true. If shouldShutdown is false, it disconnects from the pools but leaves processes running.

func SlugifyClaudeProjectPath added in v1.7.28

func SlugifyClaudeProjectPath(projectPath string) string

SlugifyClaudeProjectPath converts a project path to Claude Code's encoded directory name format. Claude stores conversation history in ~/.claude/projects/<slug>/ where <slug> is the project path with `/` and `.` replaced by `-`.

Example: /home/user/foo.v1 → -home-user-foo-v1

Kept in the session package so both the costs sync path and the CLI `session move` command share one implementation (issue #414).

func SortInstancesByActionable added in v1.9.7

func SortInstancesByActionable(insts []*Instance)

SortInstancesByActionable sorts the given slice in place so the most recently actionable sessions surface first within a group (issue #857). Key precedence:

  1. actionablePriority(Status) asc — error/waiting/running first
  2. LastAccessedAt desc — recent attention first
  3. Order asc — preserves user-customized position as a stable tie-breaker (TestSessionOrderPersistence, TestSessionOrderMigration)

func StartHTTPServer added in v0.8.96

func StartHTTPServer(name string, def *MCPDef) error

StartHTTPServer starts an HTTP MCP server on demand This is called when an HTTP MCP with server config is attached to a session

func StartMaintenanceWorker added in v0.8.79

func StartMaintenanceWorker(ctx context.Context, onComplete func(MaintenanceResult))

StartMaintenanceWorker launches a background goroutine that runs maintenance on a 15-minute ticker with an immediate first run. It checks GetMaintenanceSettings().Enabled before each run.

func StreamTranscript added in v1.7.48

func StreamTranscript(ctx context.Context, path, sessionID string, sentAt time.Time, w io.Writer, cfg StreamConfig) error

StreamTranscript tails path (a Claude session JSONL file) and writes structured StreamEvents as JSONL to w. sentAt gates out records whose timestamp is strictly before (sentAt - 250ms skew) so we don't replay history.

Returns nil on natural end_turn stop, ctx.Err on cancel, or ErrStreamTimeout on idle timeout (with an error event already written).

func StripResumeFields added in v1.3.2

func StripResumeFields(raw json.RawMessage) json.RawMessage

StripResumeFields removes session-specific fields (resume_session_id, session_mode) from serialized ToolOptionsJSON so that a new session inheriting another session's settings starts fresh instead of resuming the source conversation. Other options (skip_permissions, etc.) are preserved. Returns the input unchanged when it is nil/empty or when unmarshalling fails.

func SupportsLaunchModel added in v1.9.14

func SupportsLaunchModel(tool string) bool

SupportsLaunchModel reports whether a newly-created session can receive an explicit model override through Agent Deck's generic session creation path.

func SupportsProjectSkills added in v1.7.32

func SupportsProjectSkills(tool string) bool

SupportsProjectSkills reports whether the runtime supports project skill materialization.

func SweepInboxByTTL added in v1.9.10

func SweepInboxByTTL(maxAge time.Duration) (int, error)

SweepInboxByTTL walks every inbox file and drops entries older than maxAge (computed against TransitionNotificationEvent.Timestamp). Returns the total entries dropped across all inbox files.

Issue #962 variant: defense-in-depth alongside SweepInboxByTuple. The tuple sweep relies on a future successful transition for the same (child, from, to) to clear stale entries. Children that complete and never transition again would otherwise leave their last deferred_target_busy entry in the inbox forever. The TTL puts a hard ceiling on inbox growth.

func SweepInboxByTuple added in v1.9.10

func SweepInboxByTuple(parentSessionID, childSessionID, fromStatus, toStatus string) (int, error)

SweepInboxByTuple drops every entry in the parent's inbox file whose (child_session_id, from_status, to_status) matches the given tuple. Returns the count of dropped entries.

Issue #962 variant: when a transition for (child, from, to) is later delivered successfully, any earlier persisted entry for the same tuple represents an event the operator no longer needs to see — the state it described has already been signaled to the conductor by the live send. Without this sweep, the inbox JSONL grows by one entry every time the target is busy at first-attempt time.

Idempotent and best-effort: missing files are not an error. The rewrite is atomic via temp file + rename, mirroring SweepInboxesForChildSession.

func SweepInboxesForChildSession added in v1.9.1

func SweepInboxesForChildSession(childSessionID string) (int, error)

SweepInboxesForChildSession rewrites every inbox JSONL file under the agent-deck inboxes directory, dropping lines whose child_session_id equals the given id. Returns the total number of lines dropped across all inbox files.

Each rewrite is atomic (temp file + rename). A no-op for absent files, empty inboxes, or inboxes that contain no matching lines.

Issue #910 — without this sweep, the conductor replays deferred_target_busy events for children that have been removed.

func SyncPluginChannels added in v1.9.2

func SyncPluginChannels(inst *Instance)

SyncPluginChannels is the cmd/-side entry point (mirrors syncPluginChannels).

func SystemdBridgeServicePath added in v0.16.0

func SystemdBridgeServicePath() (string, error)

SystemdBridgeServicePath returns the full path to the bridge systemd service file

func SystemdHeartbeatServiceName added in v0.16.0

func SystemdHeartbeatServiceName(name string) string

SystemdHeartbeatServiceName returns the systemd service name for a conductor heartbeat

func SystemdHeartbeatServicePath added in v0.16.0

func SystemdHeartbeatServicePath(name string) (string, error)

SystemdHeartbeatServicePath returns the full path to a heartbeat systemd service

func SystemdHeartbeatTimerName added in v0.16.0

func SystemdHeartbeatTimerName(name string) string

SystemdHeartbeatTimerName returns the systemd timer name for a conductor heartbeat

func SystemdHeartbeatTimerPath added in v0.16.0

func SystemdHeartbeatTimerPath(name string) (string, error)

SystemdHeartbeatTimerPath returns the full path to a heartbeat systemd timer

func SystemdTransitionNotifierServicePath added in v0.19.13

func SystemdTransitionNotifierServicePath() (string, error)

SystemdTransitionNotifierServicePath returns the full path to the transition notifier service file.

func SystemdUserDir added in v0.16.0

func SystemdUserDir() (string, error)

SystemdUserDir returns the systemd user unit directory (~/.config/systemd/user/)

func TeardownConductor added in v0.12.0

func TeardownConductor(name string) error

TeardownConductor removes the conductor directory for a named conductor. It does NOT remove the session from storage (that's done by the CLI handler).

func TeardownConductorProfile added in v0.12.0

func TeardownConductorProfile(profile string) error

TeardownConductorProfile removes the conductor directory for a profile. Deprecated: Use TeardownConductor instead. Kept for backward compatibility.

func ThemeColorFGBG added in v0.26.0

func ThemeColorFGBG() string

ThemeColorFGBG returns the COLORFGBG value for the current resolved theme. Used by tmux session setup to persist the value via set-environment.

func TierName added in v0.5.3

func TierName(tier SearchTier) string

TierName returns a human-readable name for the tier

func ToggleLocalMCP added in v0.5.3

func ToggleLocalMCP(projectPath, mcpName string) error

ToggleLocalMCP toggles a Local MCP on/off It respects the existing mode (whitelist vs blacklist) or initializes with blacklist

func TransitionNotifierDaemonHint added in v0.19.13

func TransitionNotifierDaemonHint() string

TransitionNotifierDaemonHint returns how to start transition notifier daemon.

func TransitionNotifierLaunchdPlistPath added in v0.19.13

func TransitionNotifierLaunchdPlistPath() (string, error)

TransitionNotifierLaunchdPlistPath returns the launchd plist path for transition notifier.

func UninstallBridgeDaemon added in v0.16.0

func UninstallBridgeDaemon() error

UninstallBridgeDaemon stops and removes the bridge daemon.

func UninstallHeartbeatDaemon added in v0.16.0

func UninstallHeartbeatDaemon(name string) error

UninstallHeartbeatDaemon stops and removes the heartbeat timer for a conductor.

func UninstallTransitionNotifierDaemon added in v0.19.13

func UninstallTransitionNotifierDaemon() error

UninstallTransitionNotifierDaemon stops and removes the transition notifier daemon.

func UpdateClaudeSessionsWithDedup added in v0.4.1

func UpdateClaudeSessionsWithDedup(instances []*Instance)

UpdateClaudeSessionsWithDedup clears duplicate Claude session IDs across instances. The oldest session (by CreatedAt) keeps its ID, newer duplicates are cleared. With tmux env being authoritative, duplicates shouldn't occur in normal use, but we handle them defensively for loaded/migrated sessions.

func UpdateGeminiAnalyticsFromDisk added in v0.8.35

func UpdateGeminiAnalyticsFromDisk(projectPath, sessionID string, analytics *GeminiSessionAnalytics) error

UpdateGeminiAnalyticsFromDisk updates the analytics struct from the session file on disk. Uses mtime caching to skip re-parsing unchanged files (important for 40MB+ session files).

func ValidateConductorName added in v0.12.0

func ValidateConductorName(name string) error

ValidateConductorName checks that a conductor name is valid

func WatcherDir added in v1.5.1

func WatcherDir() (string, error)

WatcherDir returns the base directory for all watchers (~/.agent-deck/watcher).

func WatcherNameDir added in v1.5.1

func WatcherNameDir(name string) (string, error)

WatcherNameDir returns the directory for a named watcher (~/.agent-deck/watcher/<name>).

func WriteGeminiMCPSettings added in v0.8.1

func WriteGeminiMCPSettings(enabledNames []string) error

WriteGeminiMCPSettings writes MCPs to ~/.gemini/settings.json Preserves existing config fields (security, theme, etc.) Uses atomic write with .tmp file for safety

func WriteGlobalMCP added in v0.5.3

func WriteGlobalMCP(enabledNames []string) error

WriteGlobalMCP adds or removes MCPs from Claude's global config This modifies ~/.claude-work/.claude.json → mcpServers

func WriteHookSessionAnchor added in v0.25.0

func WriteHookSessionAnchor(instanceID, sessionID string)

WriteHookSessionAnchor persists the latest non-empty hook session ID.

func WriteIdleTimeoutSecsToToolData added in v1.9.29

func WriteIdleTimeoutSecsToToolData(td json.RawMessage, secs int64) json.RawMessage

WriteIdleTimeoutSecsToToolData merges idle_timeout_secs into the given tool_data JSON blob. Passing secs == 0 removes the key (keeps the blob shape identical to a pre-#1143 row, so downgrades stay clean).

func WriteInboxEvent added in v1.7.73

func WriteInboxEvent(parentSessionID string, event TransitionNotificationEvent) error

WriteInboxEvent appends one event to the parent's inbox as a JSONL line. Safe for concurrent callers within a single process.

Fingerprint dedup: events that share an EventFingerprint with one already persisted in the file are silently skipped. This is the producer-side guard for issue #824 (scheduleBusyRetry firing the same exhaustion path for the same logical event multiple times). Consumers still get at-most-once delivery via ReadAndTruncateInbox.

func WriteMCPJsonFromConfig added in v0.5.3

func WriteMCPJsonFromConfig(projectPath string, enabledNames []string) error

WriteMCPJsonFromConfig writes enabled MCPs from config.toml to project's .mcp.json It preserves any existing entries not managed by agent-deck (not defined in config.toml)

func WriteSessionIDLifecycleEvent added in v0.28.0

func WriteSessionIDLifecycleEvent(event SessionIDLifecycleEvent) error

WriteSessionIDLifecycleEvent appends a single JSONL event.

func WriteSessionLifecycleEvent added in v1.9.29

func WriteSessionLifecycleEvent(ev SessionLifecycleEvent) error

WriteSessionLifecycleEvent appends a single JSONL row.

func WriteStatusEvent added in v0.18.0

func WriteStatusEvent(event StatusEvent) error

WriteStatusEvent atomically writes a status event to the events directory. Uses tmp file + rename to avoid partial reads by watchers.

func WriteUserMCP added in v0.8.69

func WriteUserMCP(enabledNames []string) error

WriteUserMCP writes MCPs to ~/.claude.json (ROOT config) Uses socket proxies if pool is running, otherwise falls back to stdio WARNING: MCPs written here affect ALL Claude sessions regardless of profile!

func ZoxideAvailable added in v1.7.52

func ZoxideAvailable() bool

ZoxideAvailable reports whether the zoxide binary is reachable on PATH.

func ZoxideQuery added in v1.7.52

func ZoxideQuery(ctx context.Context, query string) ([]string, error)

ZoxideQuery returns matching directory paths from zoxide's database, ordered by frecency. An empty query returns the full list. Returns an empty slice (not an error) when zoxide has no matches for the query.

Types

type AnalyticsDisplaySettings added in v0.8.53

type AnalyticsDisplaySettings struct {
	// ShowContextBar shows the context window usage bar (default: true)
	ShowContextBar *bool `toml:"show_context_bar"`

	// ShowTokens shows the token breakdown (In/Out/Cache/Total) (default: false)
	ShowTokens *bool `toml:"show_tokens"`

	// ShowSessionInfo shows duration, turns, start time (default: false)
	ShowSessionInfo *bool `toml:"show_session_info"`

	// ShowTools shows the top tool calls (default: true)
	ShowTools *bool `toml:"show_tools"`

	// ShowCost shows the estimated cost (default: false)
	ShowCost *bool `toml:"show_cost"`
}

AnalyticsDisplaySettings configures which analytics sections to display All settings use pointers to distinguish "not set" from "explicitly false"

func (*AnalyticsDisplaySettings) GetShowContextBar added in v0.8.53

func (a *AnalyticsDisplaySettings) GetShowContextBar() bool

GetShowContextBar returns whether to show context bar, defaulting to true

func (*AnalyticsDisplaySettings) GetShowCost added in v0.8.53

func (a *AnalyticsDisplaySettings) GetShowCost() bool

GetShowCost returns whether to show cost estimate, defaulting to false

func (*AnalyticsDisplaySettings) GetShowSessionInfo added in v0.8.53

func (a *AnalyticsDisplaySettings) GetShowSessionInfo() bool

GetShowSessionInfo returns whether to show session info, defaulting to false

func (*AnalyticsDisplaySettings) GetShowTokens added in v0.8.53

func (a *AnalyticsDisplaySettings) GetShowTokens() bool

GetShowTokens returns whether to show token breakdown, defaulting to false

func (*AnalyticsDisplaySettings) GetShowTools added in v0.8.53

func (a *AnalyticsDisplaySettings) GetShowTools() bool

GetShowTools returns whether to show tool calls, defaulting to false

type BillingBlock added in v0.8.27

type BillingBlock struct {
	StartTime  time.Time `json:"start_time"`
	EndTime    time.Time `json:"end_time"`
	TokensUsed int       `json:"tokens_used"`
	IsActive   bool      `json:"is_active"`
}

BillingBlock represents a 5-hour billing window

func CalculateBillingBlocks added in v0.8.27

func CalculateBillingBlocks(timestamps []time.Time, windowSize time.Duration) []BillingBlock

CalculateBillingBlocks groups timestamps into billing windows. Claude Code API bills in 5-hour windows. Each block represents a billing period. Timestamps are sorted chronologically and grouped - a new block starts when a timestamp exceeds the windowSize from the current block's start time. The last block is marked as "active" if it's still within the window from now.

type BudgetSettings added in v0.26.4

type BudgetSettings struct {
	DailyLimit   float64                  `toml:"daily_limit"`
	WeeklyLimit  float64                  `toml:"weekly_limit"`
	MonthlyLimit float64                  `toml:"monthly_limit"`
	Groups       map[string]GroupBudget   `toml:"groups"`
	Sessions     map[string]SessionBudget `toml:"sessions"`
}

type ClaudeConfig added in v0.4.1

type ClaudeConfig struct {
	Projects map[string]ClaudeProject `json:"projects"`
}

ClaudeConfig represents the structure of .claude.json

type ClaudeOptions added in v0.8.39

type ClaudeOptions struct {
	// SessionMode: "new" (default), "continue" (-c), or "resume" (-r)
	SessionMode string `json:"session_mode,omitempty"`
	// ResumeSessionID is the session ID for -r flag (only when SessionMode="resume")
	ResumeSessionID string `json:"resume_session_id,omitempty"`
	// Model overrides the Claude model for this session. Aliases like "sonnet",
	// "opus", and "haiku" let Claude Code resolve the latest version; full
	// model IDs pin a specific version.
	Model string `json:"model,omitempty"`
	// SkipPermissions adds --dangerously-skip-permissions flag
	SkipPermissions bool `json:"skip_permissions,omitempty"`
	// AllowSkipPermissions adds --allow-dangerously-skip-permissions flag
	// Only used when SkipPermissions is false (SkipPermissions takes precedence)
	AllowSkipPermissions bool `json:"allow_skip_permissions,omitempty"`
	// AutoMode adds --permission-mode auto flag
	// Uses a classifier model to auto-approve safe operations while blocking risky ones.
	// Only used when SkipPermissions is false (SkipPermissions takes precedence).
	AutoMode bool `json:"auto_mode,omitempty"`
	// UseChrome adds --chrome flag
	UseChrome bool `json:"use_chrome,omitempty"`
	// UseTeammateMode adds --teammate-mode tmux flag
	UseTeammateMode bool `json:"use_teammate_mode,omitempty"`

	// Transient fields for worktree fork (not persisted)
	WorkDir          string `json:"-"`
	WorktreePath     string `json:"-"`
	WorktreeRepoRoot string `json:"-"`
	WorktreeBranch   string `json:"-"`
}

ClaudeOptions holds launch options for Claude Code sessions

func NewClaudeOptions added in v0.8.39

func NewClaudeOptions(config *UserConfig) *ClaudeOptions

NewClaudeOptions creates ClaudeOptions with defaults from config

func UnmarshalClaudeOptions added in v0.8.39

func UnmarshalClaudeOptions(data json.RawMessage) (*ClaudeOptions, error)

UnmarshalClaudeOptions deserializes ClaudeOptions from JSON wrapper

func (*ClaudeOptions) ToArgs added in v0.8.39

func (o *ClaudeOptions) ToArgs() []string

ToArgs returns command-line arguments based on options

func (*ClaudeOptions) ToArgsForFork added in v0.8.39

func (o *ClaudeOptions) ToArgsForFork() []string

ToArgsForFork returns arguments suitable for fork resume command Fork always uses --resume internally, so session mode flags are not included

func (*ClaudeOptions) ToolName added in v0.8.39

func (o *ClaudeOptions) ToolName() string

ToolName returns "claude"

type ClaudeProject added in v0.4.1

type ClaudeProject struct {
	LastSessionId string `json:"lastSessionId"`
}

ClaudeProject represents a project entry in Claude's config

type ClaudeSettings added in v0.4.1

type ClaudeSettings struct {
	// Command is the Claude CLI command or alias to use (e.g., "claude", "cdw", "cdp")
	// Default: "claude"
	// This allows using shell aliases that set CLAUDE_CONFIG_DIR automatically
	Command string `toml:"command"`

	// ConfigDir is the path to Claude's config directory
	// Default: ~/.claude (or CLAUDE_CONFIG_DIR env var)
	ConfigDir string `toml:"config_dir"`

	// DangerousMode enables --dangerously-skip-permissions flag for Claude sessions
	// Default: true (nil = use default true, explicitly set false to disable)
	// Power users typically want this enabled for faster iteration
	DangerousMode *bool `toml:"dangerous_mode"`

	// AllowDangerousMode enables --allow-dangerously-skip-permissions flag
	// This unlocks bypass as an option without activating it by default.
	// Ignored when dangerous_mode is true (the stronger flag takes precedence).
	// Default: false
	AllowDangerousMode bool `toml:"allow_dangerous_mode"`

	// AutoMode enables --permission-mode auto flag for Claude sessions
	// A classifier model reviews commands before they run, blocking scope escalation
	// and hostile-content-driven actions while letting routine work proceed without prompts.
	// Ignored when dangerous_mode is true (the stronger flag takes precedence).
	// Default: false
	AutoMode bool `toml:"auto_mode"`

	// ExtraArgs are user-supplied Claude CLI flags used as the New Session
	// dialog default. They are persisted as discrete TOML array entries and
	// copied to Instance.ExtraArgs when a Claude session is created.
	ExtraArgs []string `toml:"extra_args"`

	// UseChrome enables --chrome by default for Claude sessions.
	UseChrome bool `toml:"use_chrome"`

	// UseTeammateMode enables --teammate-mode tmux by default for Claude sessions.
	UseTeammateMode bool `toml:"use_teammate_mode"`

	// EnvFile is a .env file specific to Claude sessions
	// Sourced AFTER global [shell].env_files
	// Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
	EnvFile string `toml:"env_file"`

	// HooksEnabled enables Claude Code hooks for real-time status detection.
	// When enabled, agent-deck uses lifecycle hooks (SessionStart, Stop, etc.)
	// for instant, deterministic status updates instead of polling tmux content.
	// Default: true (nil = use default true, set false to disable)
	HooksEnabled *bool `toml:"hooks_enabled"`

	// AutoResumeSummary auto-presses Enter on Claude's "Resume from summary"
	// picker that appears after `claude --resume` on long-running sessions
	// (>~250k tokens). Critical for unattended conductors which would
	// otherwise sit frozen on the picker forever (closes #67).
	// Default: true (nil = use default true, set false to disable).
	AutoResumeSummary *bool `toml:"auto_resume_summary"`
}

ClaudeSettings defines Claude Code configuration

func (*ClaudeSettings) GetAutoResumeSummary added in v1.9.19

func (c *ClaudeSettings) GetAutoResumeSummary() bool

GetAutoResumeSummary returns whether the "Resume from summary" picker is auto-confirmed on session restart, defaulting to true. Conductors and any other unattended session runner depend on this — without it, a single claude --resume on a >250k-token session leaves the session frozen on the picker screen forever.

func (*ClaudeSettings) GetDangerousMode added in v0.8.76

func (c *ClaudeSettings) GetDangerousMode() bool

GetDangerousMode returns whether dangerous mode is enabled, defaulting to true Power users (the primary audience) typically want this enabled for faster iteration

func (*ClaudeSettings) GetHooksEnabled added in v0.16.0

func (c *ClaudeSettings) GetHooksEnabled() bool

GetHooksEnabled returns whether Claude Code hooks are enabled, defaulting to true

type CodexOptions added in v0.10.18

type CodexOptions struct {
	// Model overrides the Codex model for this session (for example, "gpt-5").
	Model string `json:"model,omitempty"`
	// YoloMode enables --yolo flag (bypass approvals and sandbox)
	// nil = inherit from global config, true/false = explicit override
	YoloMode *bool `json:"yolo_mode,omitempty"`
}

CodexOptions holds launch options for Codex CLI sessions

func NewCodexOptions added in v0.10.18

func NewCodexOptions(config *UserConfig) *CodexOptions

NewCodexOptions creates CodexOptions with defaults from global config

func UnmarshalCodexOptions added in v0.10.18

func UnmarshalCodexOptions(data json.RawMessage) (*CodexOptions, error)

UnmarshalCodexOptions deserializes CodexOptions from JSON wrapper

func (*CodexOptions) ToArgs added in v0.10.18

func (o *CodexOptions) ToArgs() []string

ToArgs returns command-line arguments based on options

func (*CodexOptions) ToolName added in v0.10.18

func (o *CodexOptions) ToolName() string

ToolName returns "codex"

type CodexSettings added in v0.10.18

type CodexSettings struct {
	// Command is the Codex CLI command or alias to use (e.g., "codex", "codex-v2")
	// Default: "codex"
	Command string `toml:"command"`

	// ConfigDir is the path to Codex home directory.
	// Default: ~/.codex (or CODEX_HOME env var)
	ConfigDir string `toml:"config_dir"`

	// YoloMode enables --yolo flag for Codex sessions (bypass approvals and sandbox)
	// Default: false
	YoloMode bool `toml:"yolo_mode"`

	// EnvFile is a .env file specific to Codex sessions
	// Sourced AFTER global [shell].env_files
	// Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
	EnvFile string `toml:"env_file"`
}

CodexSettings defines Codex CLI configuration

type CompletionCycler added in v0.8.86

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

CompletionCycler manages the state of directory autocomplete.

func (*CompletionCycler) IsActive added in v0.8.86

func (c *CompletionCycler) IsActive() bool

IsActive returns true if the cycler has active matches.

func (*CompletionCycler) Next added in v0.8.86

func (c *CompletionCycler) Next() string

Next returns the next match in the cycle. Wraps around to the beginning if the end is reached.

func (*CompletionCycler) Reset added in v0.8.86

func (c *CompletionCycler) Reset()

Reset clears the cycler state.

func (*CompletionCycler) SetMatches added in v0.8.86

func (c *CompletionCycler) SetMatches(matches []string)

SetMatches sets the matches for the cycler and resets the index.

type ConductorAgentSpec added in v0.26.4

type ConductorAgentSpec struct {
	Agent                  string
	DisplayName            string
	DefaultCommand         string
	InstructionsFileName   string
	SupportsClearOnCompact bool
}

ConductorAgentSpec describes conductor-specific behavior for an agent runtime.

func GetConductorAgentSpec added in v0.26.4

func GetConductorAgentSpec(agent string) (ConductorAgentSpec, error)

GetConductorAgentSpec returns the normalized spec for a supported conductor agent.

type ConductorClaudeSettings added in v1.5.4

type ConductorClaudeSettings struct {
	// ConfigDir overrides [claude].config_dir for this conductor only.
	ConfigDir string `toml:"config_dir"`

	// EnvFile is sourced before claude exec for this conductor.
	// Matches CFG-03 semantics — missing file logs a warning, does not block.
	EnvFile string `toml:"env_file"`
}

ConductorClaudeSettings defines conductor-specific Claude overrides. Semantics mirror GroupClaudeSettings — ExpandPath is applied on read via GetConductorClaudeConfigDir; env_file resolution is deferred to the spawn builder (resolvePath handles path expansion at use).

type ConductorMeta added in v0.12.0

type ConductorMeta struct {
	Name              string `json:"name"`
	Agent             string `json:"agent,omitempty"`
	Profile           string `json:"profile"`
	HeartbeatEnabled  bool   `json:"heartbeat_enabled"`
	HeartbeatInterval int    `json:"heartbeat_interval"` // 0 = use global default
	Description       string `json:"description,omitempty"`
	CreatedAt         string `json:"created_at"`

	// ClearOnCompact blocks Claude's auto-compaction and sends /clear instead.
	// When context fills up (~95%), Claude normally summarizes prior conversation (lossy).
	// With this enabled, agent-deck blocks compaction and clears context entirely,
	// relying on CLAUDE.md and conductor state for continuity.
	// Default: true (nil = use default true via GetClearOnCompact)
	ClearOnCompact *bool `json:"clear_on_compact,omitempty"`

	// Env holds inline environment variables for the conductor session.
	// These are exported before the conductor command launches.
	Env map[string]string `json:"env,omitempty"`

	// EnvFile is a path to a .env file to source before the conductor command.
	// Supports ~ and $VAR expansion.
	EnvFile string `json:"env_file,omitempty"`

	// HeartbeatIdleMinutes is the minutes of inactivity before pausing heartbeats.
	// 0 or negative = disabled (never pause). Positive = number of minutes.
	HeartbeatIdleMinutes int `json:"heartbeat_idle_minutes"`
}

ConductorMeta holds metadata for a named conductor instance

func ListConductors added in v0.12.0

func ListConductors() ([]ConductorMeta, error)

ListConductors scans all conductor directories that have meta.json

func ListConductorsForProfile added in v0.12.0

func ListConductorsForProfile(profile string) ([]ConductorMeta, error)

ListConductorsForProfile returns conductors belonging to a specific profile

func LoadConductorMeta added in v0.12.0

func LoadConductorMeta(name string) (*ConductorMeta, error)

LoadConductorMeta reads meta.json for a named conductor

func (*ConductorMeta) GetAgent added in v0.26.4

func (m *ConductorMeta) GetAgent() string

GetAgent returns the normalized conductor agent, defaulting to Claude.

func (*ConductorMeta) GetClearOnCompact added in v0.19.15

func (m *ConductorMeta) GetClearOnCompact() bool

GetClearOnCompact returns whether to block compaction and send /clear instead, defaulting to true

func (*ConductorMeta) GetHeartbeatIdleMinutes added in v1.9.20

func (m *ConductorMeta) GetHeartbeatIdleMinutes() int

GetHeartbeatIdleMinutes returns the heartbeat idle threshold in minutes. Returns 0 when disabled (value is 0 or negative). Returns the configured value when positive.

type ConductorOverrides added in v1.5.4

type ConductorOverrides struct {
	// Claude defines Claude Code overrides for a specific conductor.
	Claude ConductorClaudeSettings `toml:"claude"`
}

ConductorOverrides defines per-conductor configuration overrides. Mirrors GroupSettings — conductors are first-class entities keyed by conductor name (derived from Instance.Title via strings.TrimPrefix at the call site, same pattern as env.go getConductorEnv).

Named ConductorOverrides (not ConductorSettings) to avoid collision with the pre-existing global [conductor] meta-agent orchestration block declared in conductor.go:49 (heartbeat, telegram, slack, discord). Closes issue #602.

type ConductorSettings added in v0.12.0

type ConductorSettings struct {
	// Enabled activates the conductor system
	Enabled bool `toml:"enabled"`

	// HeartbeatInterval is the interval in minutes between heartbeat checks
	// Default: 15
	HeartbeatInterval int `toml:"heartbeat_interval"`

	// Profiles is the list of agent-deck profiles to manage
	// Kept for backward compat but ignored after migration to meta.json-based discovery
	Profiles []string `toml:"profiles"`

	// Telegram defines Telegram bot integration settings
	Telegram TelegramSettings `toml:"telegram"`

	// Slack defines Slack bot integration settings
	Slack SlackSettings `toml:"slack"`

	// Discord defines Discord bot integration settings
	Discord DiscordSettings `toml:"discord"`
}

ConductorSettings defines conductor (meta-agent orchestration) configuration

func GetConductorSettings added in v0.12.0

func GetConductorSettings() ConductorSettings

GetConductorSettings loads and returns conductor settings from config

func (*ConductorSettings) GetHeartbeatInterval added in v0.12.0

func (c *ConductorSettings) GetHeartbeatInterval() int

GetHeartbeatInterval returns the heartbeat interval in minutes. Returns 0 when HeartbeatInterval is 0 (explicitly disabled). Returns 15 (default) when HeartbeatInterval is negative. Returns the configured value when HeartbeatInterval is positive.

func (*ConductorSettings) GetProfiles added in v0.12.0

func (c *ConductorSettings) GetProfiles() []string

GetProfiles returns the configured profiles, defaulting to ["default"]

type Config added in v0.3.0

type Config struct {
	// DefaultProfile is the profile to use when none is specified
	DefaultProfile string `json:"default_profile"`

	// LastUsed is the most recently used profile (for future use)
	LastUsed string `json:"last_used,omitempty"`

	// Version tracks config format for future migrations
	Version int `json:"version"`
}

Config represents the global agent-deck configuration

func LoadConfig added in v0.3.0

func LoadConfig() (*Config, error)

LoadConfig loads the global configuration

type ContentBuffer added in v0.10.0

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

ContentBuffer stores full content and a lowercased copy for fast search. It is safe for concurrent reads and append-only writes.

func (*ContentBuffer) Append added in v0.10.0

func (b *ContentBuffer) Append(data []byte)

func (*ContentBuffer) CopyData added in v0.10.0

func (b *ContentBuffer) CopyData() []byte

func (*ContentBuffer) Size added in v0.10.5

func (b *ContentBuffer) Size() int64

Size returns the total memory used by this buffer (data + lowercased copy)

func (*ContentBuffer) With added in v0.10.0

func (b *ContentBuffer) With(fn func(data, lower []byte))

type CopilotOptions added in v1.7.26

type CopilotOptions struct {
	// SessionMode: "new" (default) or "resume" (--resume).
	// When "resume" and ResumeSessionID is empty, Copilot CLI shows its
	// session picker; when set, agent-deck passes the ID through as
	// --resume <id>.
	SessionMode string `json:"session_mode,omitempty"`
	// ResumeSessionID is the Copilot session ID for --resume (only used
	// when SessionMode == "resume").
	ResumeSessionID string `json:"resume_session_id,omitempty"`
	// Model overrides the default Copilot model (e.g., "claude-opus-4.6",
	// "gpt-5.2"). Passed as --model <value>.
	Model string `json:"model,omitempty"`
	// AllowAll enables --allow-all (equivalent to --allow-all-tools
	// --allow-all-paths --allow-all-urls). Required for non-interactive
	// scripting scenarios.
	AllowAll bool `json:"allow_all,omitempty"`
}

CopilotOptions holds launch options for GitHub Copilot CLI sessions (the standalone `copilot` binary from @github/copilot, not the older `gh copilot` extension).

func NewCopilotOptions added in v1.7.26

func NewCopilotOptions(config *UserConfig) *CopilotOptions

NewCopilotOptions creates CopilotOptions with defaults from config

func UnmarshalCopilotOptions added in v1.7.26

func UnmarshalCopilotOptions(data json.RawMessage) (*CopilotOptions, error)

UnmarshalCopilotOptions deserializes CopilotOptions from JSON wrapper

func (*CopilotOptions) ToArgs added in v1.7.26

func (o *CopilotOptions) ToArgs() []string

ToArgs returns command-line arguments based on options

func (*CopilotOptions) ToolName added in v1.7.26

func (o *CopilotOptions) ToolName() string

ToolName returns "copilot"

type CopilotSettings added in v1.7.26

type CopilotSettings struct {
	// EnvFile is a .env file specific to Copilot sessions (sourced before
	// the `copilot` command runs, like [gemini].env_file). Optional.
	EnvFile string `toml:"env_file"`

	// Command overrides the default binary/invocation for Copilot sessions.
	// Supports flags (e.g., "copilot --custom-flag"). Default: "copilot"
	Command string `toml:"command"`

	// DefaultModel sets the Copilot model for new sessions (e.g., "claude-opus-4.6",
	// "gpt-5.2"). Passed as --model <value>. Can be overridden per-session.
	DefaultModel string `toml:"default_model"`

	// AllowAll enables --allow-all by default for new sessions (equivalent to
	// --allow-all-tools --allow-all-paths --allow-all-urls). Can be overridden
	// per-session.
	AllowAll bool `toml:"allow_all"`
}

CopilotSettings defines GitHub Copilot CLI configuration (Issue #556). Binary: `copilot` from @github/copilot (GA 2026-02-25). Doc: https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli

type CostsSettings added in v0.26.4

type CostsSettings struct {
	Currency      string `toml:"currency"`
	Timezone      string `toml:"timezone"`
	RetentionDays int    `toml:"retention_days"`
	// CostLineTemplate overrides the home status-bar cost segment.
	// Three-state pointer: nil falls through to the next layer
	// (profile -> global -> hardcoded); explicit empty string disables.
	CostLineTemplate *string `toml:"cost_line_template"`
	// CostLineHideWhenZero hides the segment when every recognized variable
	// in the active template renders to $0.00. Three-state pointer; default
	// is true (preserves the legacy "no events, no segment" behavior).
	CostLineHideWhenZero *bool           `toml:"cost_line_hide_when_zero"`
	Budgets              BudgetSettings  `toml:"budgets"`
	Pricing              PricingSettings `toml:"pricing"`
}

CostsSettings configures cost tracking, budgets, and pricing overrides.

func (CostsSettings) GetRetentionDays added in v0.26.4

func (c CostsSettings) GetRetentionDays() int

func (CostsSettings) GetTimezone added in v0.26.4

func (c CostsSettings) GetTimezone() string

type CrushOptions added in v1.9.14

type CrushOptions struct {
	// YoloMode enables --yolo flag (auto-accept all permission prompts).
	// nil = inherit from config, true/false = explicit override.
	YoloMode *bool `json:"yolo_mode,omitempty"`
	// ResumeSessionID is the crush session ID for --session/-s flag.
	// Empty means start a fresh session.
	ResumeSessionID string `json:"resume_session_id,omitempty"`
	// ContinueLast maps to --continue/-C (resume the most recent session).
	// Mutually exclusive with ResumeSessionID at the crush CLI level; if
	// both are set, ResumeSessionID wins (it is the more specific signal).
	ContinueLast bool `json:"continue_last,omitempty"`
}

CrushOptions holds launch options for charmbracelet/crush CLI sessions (Issue #940). Binary: `crush` from github.com/charmbracelet/crush.

func NewCrushOptions added in v1.9.14

func NewCrushOptions(config *UserConfig) *CrushOptions

NewCrushOptions creates CrushOptions with defaults from global config.

func UnmarshalCrushOptions added in v1.9.14

func UnmarshalCrushOptions(data json.RawMessage) (*CrushOptions, error)

UnmarshalCrushOptions deserializes CrushOptions from JSON wrapper.

func (*CrushOptions) ToArgs added in v1.9.14

func (o *CrushOptions) ToArgs() []string

ToArgs returns command-line arguments based on options. Ordering matches the crush CLI flag groups (session first, then yolo).

func (*CrushOptions) ToolName added in v1.9.14

func (o *CrushOptions) ToolName() string

ToolName returns "crush"

type CrushSettings added in v1.9.14

type CrushSettings struct {
	// Command overrides the default binary/invocation for Crush sessions.
	// Supports flags (e.g., "crush --debug"). Default: "crush"
	Command string `toml:"command"`

	// EnvFile is a .env file specific to Crush sessions (sourced before
	// the `crush` command runs, like [gemini].env_file). Optional.
	EnvFile string `toml:"env_file"`

	// YoloMode enables --yolo flag for Crush sessions (auto-accept all
	// permission prompts). Default: false
	YoloMode bool `toml:"yolo_mode"`
}

CrushSettings defines charmbracelet/crush CLI configuration (Issue #940). Binary: `crush` from github.com/charmbracelet/crush. Interactive TUI. Key flags: --yolo, --session/-s <id>, --continue/-C, --cwd, --debug.

type DiscordSettings added in v0.20.1

type DiscordSettings struct {
	// BotToken is the Discord bot token from the Developer Portal
	BotToken string `toml:"bot_token"`

	// GuildID is the Discord server (guild) where the bot operates
	GuildID int64 `toml:"guild_id"`

	// ChannelID is the Discord channel where the bot listens and posts
	ChannelID int64 `toml:"channel_id"`

	// UserID is the authorized Discord user ID
	UserID int64 `toml:"user_id"`

	// ListenMode controls when the bot responds: "mentions" (only @mentions) or "all" (all channel messages)
	// Default: "all"
	ListenMode string `toml:"listen_mode"`

	// IgnoreRepliesToOthers skips forwarding replies unless they reply to the bot itself.
	// Default: false
	IgnoreRepliesToOthers bool `toml:"ignore_replies_to_others"`
}

DiscordSettings defines Discord bot configuration for the conductor bridge

type DisplaySettings added in v0.26.0

type DisplaySettings struct {
	// FullRepaint forces a full screen clear on every render cycle instead of
	// incremental redraws. Enable this if you see vertical drift or rendering
	// artifacts in terminals that use unicode grapheme-cluster widths (e.g.
	// Ghostty 1.3+ with grapheme-width-method=unicode).
	// Can also be enabled via AGENTDECK_REPAINT=full env var.
	// Default: false
	FullRepaint bool `toml:"full_repaint"`

	// DefaultFilter sets the initial status filter when the TUI opens.
	// Valid values: "" (all, default), "active" (hides error/stopped),
	// "running", "waiting", "idle", "error".
	// If set to "active" and no non-error sessions exist, falls back to showing all.
	DefaultFilter string `toml:"default_filter"`

	// ActiveFilterLabel sets the label shown on the filter pill when the active
	// filter is engaged. Default: "Open". Examples: "Active", "Live", "Open".
	ActiveFilterLabel string `toml:"active_filter_label"`

	// ActiveFilterExcludes is the list of session statuses that the % "Open"
	// filter hides. Default: ["error", "stopped"] — matches the original
	// upstream behavior. Set to ["error"] to keep stopped/closed sessions
	// visible while still hiding errors, or extend with "idle" for an
	// aggressive "show only running/waiting" definition. Unknown statuses
	// are dropped silently; if all entries are unknown the default applies.
	// Valid statuses: "running", "waiting", "idle", "error", "starting",
	// "stopped".
	ActiveFilterExcludes []string `toml:"active_filter_excludes"`
}

DisplaySettings controls TUI rendering behavior.

func (DisplaySettings) GetActiveFilterExcludes added in v1.8.1

func (d DisplaySettings) GetActiveFilterExcludes() map[Status]bool

GetActiveFilterExcludes returns the resolved set of statuses the % filter should hide. Default {error, stopped} matches the original upstream hardcoded behavior; opt into ["error"] to keep stopped sessions visible. Unknown values are dropped; an empty resolved set falls back to the default.

func (DisplaySettings) GetDefaultFilter added in v1.3.3

func (d DisplaySettings) GetDefaultFilter() string

GetDefaultFilter returns the validated default_filter value, falling back to "" on invalid input.

func (DisplaySettings) GetFullRepaint added in v0.26.0

func (d DisplaySettings) GetFullRepaint() bool

GetFullRepaint returns whether full-repaint mode is active, checking the env var AGENTDECK_REPAINT=full as an override.

type DockerSettings added in v0.19.17

type DockerSettings struct {
	// DefaultImage is the sandbox image to use when not specified per-session.
	DefaultImage string `toml:"default_image"`

	// DefaultEnabled enables sandbox by default for new sessions.
	DefaultEnabled bool `toml:"default_enabled"`

	// CPULimit is the default CPU limit for sandboxed containers (e.g. "2.0").
	CPULimit string `toml:"cpu_limit"`

	// MemoryLimit is the default memory limit for sandboxed containers (e.g. "4g").
	MemoryLimit string `toml:"memory_limit"`

	// VolumeIgnores is a list of directories to exclude from the project mount.
	VolumeIgnores []string `toml:"volume_ignores"`

	// Environment lists host environment variable names whose values are forwarded to the
	// container at runtime via docker exec -e. The actual values are read from the host
	// on each command invocation, so changes take effect without recreating the container.
	Environment []string `toml:"environment"`

	// ExtraVolumes maps host paths to container paths for additional bind mounts.
	ExtraVolumes map[string]string `toml:"extra_volumes"`

	// EnvironmentValues are static key=value pairs baked into the container at creation
	// time via docker create -e. Unlike Environment (which forwards by name at runtime),
	// these are fixed when the container is created.
	EnvironmentValues map[string]string `toml:"environment_values"`

	// MountSSH mounts ~/.ssh read-only inside the container.
	MountSSH bool `toml:"mount_ssh"`

	// AutoCleanup removes sandbox containers on session kill (default: true).
	AutoCleanup *bool `toml:"auto_cleanup"`
}

DockerSettings defines Docker sandbox configuration.

func GetDockerSettings added in v0.19.17

func GetDockerSettings() DockerSettings

GetDockerSettings returns docker sandbox settings with defaults applied.

func (DockerSettings) GetAutoCleanup added in v0.19.17

func (d DockerSettings) GetAutoCleanup() bool

GetAutoCleanup returns whether to auto-remove sandbox containers, defaulting to true.

type ExperimentsSettings added in v0.8.43

type ExperimentsSettings struct {
	// Directory is the base directory for experiments
	// Default: ~/src/tries
	Directory string `toml:"directory"`

	// DatePrefix adds YYYY-MM-DD- prefix to new experiment folders
	// Default: true
	DatePrefix bool `toml:"date_prefix"`

	// DefaultTool is the AI tool to use for experiment sessions
	// Default: "claude"
	DefaultTool string `toml:"default_tool"`
}

ExperimentsSettings defines experiment folder configuration

func GetExperimentsSettings added in v0.8.43

func GetExperimentsSettings() ExperimentsSettings

GetExperimentsSettings returns experiments settings with defaults applied

type FeedbackSettings added in v1.7.38

type FeedbackSettings struct {
	// Disabled suppresses all passive feedback prompts when true.
	// Defaults to false. Set by RecordOptOut paths; cleared on re-enable.
	Disabled bool `toml:"disabled"`
}

FeedbackSettings controls the in-product feedback prompts. When Disabled is true, neither the auto-prompt (TUI) nor the post-launch auto-trigger (CLI, if any) will fire. Explicit `agent-deck feedback` invocations still run but show a re-enable prompt first. v1.7.38+.

type FieldRestartPolicy added in v1.7.70

type FieldRestartPolicy int
const (
	FieldLive FieldRestartPolicy = iota
	FieldRestartRequired
)

func RestartPolicyFor added in v1.7.70

func RestartPolicyFor(field string) FieldRestartPolicy

type FileTracker added in v0.5.3

type FileTracker struct {
	Path       string
	LastOffset int64
	LastSize   int64
	LastMod    time.Time
}

FileTracker tracks file state for incremental updates

type FlickerDetector added in v1.9.0

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

FlickerDetector observes status transitions per session and emits a synthetic "flicker_detected" WARN log when a session oscillates more than flickerThreshold times within flickerWindow.

Motivation: today's six-flicker oscillation incident was logged at Debug only and provided no aggregate signal. Operators had to grep status_changed lines manually. A single WARN per burst per session is enough to trigger an alert without spamming logs during a sustained flicker storm (rate-limited via flickerCooldown).

func GlobalFlickerDetector added in v1.9.0

func GlobalFlickerDetector() *FlickerDetector

GlobalFlickerDetector returns the process-wide detector used by the TUI's status_changed call site.

func NewFlickerDetector added in v1.9.0

func NewFlickerDetector() *FlickerDetector

NewFlickerDetector creates a fresh detector. Tests construct their own; production uses GlobalFlickerDetector.

func (*FlickerDetector) Observe added in v1.9.0

func (d *FlickerDetector) Observe(sessionID, status string)

Observe records a status transition for sessionID at the current time. Callers should only invoke this when the new status differs from the previous status — Observe does not de-duplicate.

type GeminiMCPConfig added in v0.8.1

type GeminiMCPConfig struct {
	MCPServers map[string]MCPServerConfig `json:"mcpServers"`
}

GeminiMCPConfig represents settings.json structure VERIFIED: Actual settings.json does NOT have mcp.allowed/excluded (Simplified structure compared to research docs)

type GeminiModelPricing added in v0.8.35

type GeminiModelPricing struct {
	Input  float64
	Output float64
}

GeminiModelPricing holds pricing per million tokens

type GeminiSessionAnalytics added in v0.8.35

type GeminiSessionAnalytics struct {
	// Token usage
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`

	// Current context size (last turn's input + cache read tokens)
	CurrentContextTokens int `json:"current_context_tokens"`

	// Session metrics
	TotalTurns int           `json:"total_turns"`
	Duration   time.Duration `json:"duration"`
	StartTime  time.Time     `json:"start_time"`
	LastActive time.Time     `json:"last_active"`

	// Cost estimation
	EstimatedCost float64 `json:"estimated_cost"`

	// Model detected from session file messages
	Model string `json:"model,omitempty"`

	// In-memory cache: last file modification time (skip re-parse if unchanged)
	LastFileModTime time.Time `json:"-"`
}

GeminiSessionAnalytics holds metrics for a Gemini session

func (*GeminiSessionAnalytics) CalculateCost added in v0.8.35

func (a *GeminiSessionAnalytics) CalculateCost(model string) float64

CalculateCost estimates session cost based on token usage and model pricing

func (*GeminiSessionAnalytics) TotalTokens added in v0.8.35

func (a *GeminiSessionAnalytics) TotalTokens() int

TotalTokens returns the sum of input and output tokens

type GeminiSessionInfo added in v0.8.1

type GeminiSessionInfo struct {
	SessionID    string // Full UUID
	Filename     string // session-2025-12-26T15-09-4d8fcb4d.json
	StartTime    time.Time
	LastUpdated  time.Time
	MessageCount int
}

GeminiSessionInfo holds parsed session metadata

func ListGeminiSessions added in v0.8.1

func ListGeminiSessions(projectPath string) ([]GeminiSessionInfo, error)

ListGeminiSessions returns all sessions for a project path Scans ~/.gemini/tmp/<hash>/chats/ and parses session files Sorted by LastUpdated (most recent first)

type GeminiSettings added in v0.8.35

type GeminiSettings struct {
	// YoloMode enables --yolo flag for Gemini sessions (auto-approve all actions)
	// Default: false
	YoloMode bool `toml:"yolo_mode"`

	// DefaultModel is the model to use for new Gemini sessions (e.g., "gemini-2.5-flash")
	// If empty, Gemini CLI uses its own default
	DefaultModel string `toml:"default_model"`

	// EnvFile is a .env file specific to Gemini sessions
	// Sourced AFTER global [shell].env_files
	// Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
	EnvFile string `toml:"env_file"`

	// Command overrides the default binary/invocation for Gemini sessions.
	// Supports flags (e.g., "gemini --custom-flag"). Default: "gemini"
	Command string `toml:"command"`
}

GeminiSettings defines Gemini CLI configuration

type GlobalSearchIndex added in v0.5.3

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

GlobalSearchIndex manages the searchable session index

func NewGlobalSearchIndex added in v0.5.3

func NewGlobalSearchIndex(claudeDir string, config GlobalSearchSettings) (*GlobalSearchIndex, error)

NewGlobalSearchIndex creates a new search index

func (*GlobalSearchIndex) Close added in v0.5.3

func (idx *GlobalSearchIndex) Close()

Close shuts down the index and releases all memory

func (*GlobalSearchIndex) EntryCount added in v0.5.3

func (idx *GlobalSearchIndex) EntryCount() int

EntryCount returns the number of indexed entries

func (*GlobalSearchIndex) FuzzySearch added in v0.5.3

func (idx *GlobalSearchIndex) FuzzySearch(query string) []*SearchResult

FuzzySearch performs fuzzy matching with typo tolerance

func (*GlobalSearchIndex) GetTier added in v0.5.3

func (idx *GlobalSearchIndex) GetTier() SearchTier

GetTier returns the current search tier

func (*GlobalSearchIndex) IsLoading added in v0.5.3

func (idx *GlobalSearchIndex) IsLoading() bool

IsLoading returns true if the index is still loading

func (*GlobalSearchIndex) Search added in v0.5.3

func (idx *GlobalSearchIndex) Search(query string) []*SearchResult

Search performs a simple substring search

type GlobalSearchSettings added in v0.5.3

type GlobalSearchSettings struct {
	// Enabled enables/disables global search feature (default: true when loaded via LoadUserConfig)
	Enabled bool `toml:"enabled"`

	// Tier controls search strategy: "auto", "instant", "balanced", "disabled"
	// auto: Auto-detect based on data size (recommended)
	// instant: Force full in-memory (fast, uses more RAM)
	// balanced: Force LRU cache mode (slower, capped RAM)
	// disabled: Disable global search entirely
	Tier string `toml:"tier"`

	// MemoryLimitMB caps memory usage for search index (default: 100)
	// Only applies to balanced tier
	MemoryLimitMB int `toml:"memory_limit_mb"`

	// RecentDays limits search to sessions from last N days (0 = all)
	// Reduces index size for users with long history (default: 90)
	RecentDays int `toml:"recent_days"`

	// IndexRateLimit limits files indexed per second during background indexing
	// Lower = less CPU impact (default: 20)
	IndexRateLimit int `toml:"index_rate_limit"`
}

GlobalSearchSettings defines global conversation search configuration

type Group

type Group struct {
	Name        string
	Path        string // Full path like "projects" or "projects/devops"
	Expanded    bool
	Sessions    []*Instance
	Order       int
	DefaultPath string // Explicit default path for new sessions in this group
	// MaxConcurrent caps simultaneous running sessions in this group (v1.9.1).
	// 0 = unlimited (legacy default for groups predating this field); 1 = serial
	// (default for newly-created groups); N>=2 = bounded parallelism. Negative
	// values are treated as unlimited (explicit opt-out).
	MaxConcurrent int
}

Group represents a group of sessions

type GroupBudget added in v0.26.4

type GroupBudget struct {
	DailyLimit float64 `toml:"daily_limit"`
}

type GroupClaudeSettings added in v1.5.4

type GroupClaudeSettings struct {
	// ConfigDir overrides [claude].config_dir for sessions in this group.
	ConfigDir string `toml:"config_dir"`

	// EnvFile overrides [claude].env_file for sessions in this group.
	EnvFile string `toml:"env_file"`
}

GroupClaudeSettings defines group-specific Claude overrides.

type GroupData

type GroupData struct {
	Name        string `json:"name"`
	Path        string `json:"path"`
	Expanded    bool   `json:"expanded"`
	Order       int    `json:"order"`
	DefaultPath string `json:"default_path,omitempty"`
	// MaxConcurrent caps simultaneous running sessions in this group (v1.9.1).
	// 0 = unlimited (legacy default for groups predating this field); 1 = serial
	// (default for newly-created groups); N>=2 = bounded parallelism.
	MaxConcurrent int `json:"max_concurrent,omitempty"`
}

GroupData represents serializable group data

type GroupSettings added in v1.5.4

type GroupSettings struct {
	// Claude defines Claude Code overrides for a specific group.
	Claude GroupClaudeSettings `toml:"claude"`
}

GroupSettings defines per-group configuration overrides.

type GroupTree

type GroupTree struct {
	Groups    map[string]*Group // path -> group
	GroupList []*Group          // Ordered list of groups
	Expanded  map[string]bool   // Collapsed state persistence
}

GroupTree manages hierarchical session organization

func NewGroupTree

func NewGroupTree(instances []*Instance) *GroupTree

NewGroupTree creates a new group tree from instances

func NewGroupTreeWithGroups

func NewGroupTreeWithGroups(instances []*Instance, storedGroups []*GroupData) *GroupTree

NewGroupTreeWithGroups creates a group tree from instances and stored group data

func (*GroupTree) AddSession

func (t *GroupTree) AddSession(inst *Instance)

AddSession adds a session to the appropriate group

func (*GroupTree) CollapseGroup

func (t *GroupTree) CollapseGroup(path string)

CollapseGroup collapses a group

func (*GroupTree) CreateGroup

func (t *GroupTree) CreateGroup(name string) *Group

CreateGroup creates a new empty group

func (*GroupTree) CreateSubgroup

func (t *GroupTree) CreateSubgroup(parentPath, name string) *Group

CreateSubgroup creates a new empty group under a parent group

func (*GroupTree) DefaultPathForGroup added in v0.19.1

func (t *GroupTree) DefaultPathForGroup(groupPath string) string

DefaultPathForGroup returns the effective default path for creating new sessions in the group: explicit configured default_path first, then most recent session path.

func (*GroupTree) DeleteGroup

func (t *GroupTree) DeleteGroup(path string) []*Instance

DeleteGroup deletes a group, all its subgroups, and moves all sessions to default

func (*GroupTree) DemoteSession added in v1.9.2

func (t *GroupTree) DemoteSession(inst *Instance)

DemoteSession converts a top-level session into a sub-session of the previous top-level peer in the same group, inserting it as that peer's last child. No-op if there is no previous peer (group's first top-level), if the session is already a sub-session, or if it has its own children — single-level nesting only, mirroring the validation in `session set-parent`.

func (*GroupTree) ExpandGroup

func (t *GroupTree) ExpandGroup(path string)

ExpandGroup expands a group

func (*GroupTree) ExpandGroupWithParents

func (t *GroupTree) ExpandGroupWithParents(path string)

ExpandGroupWithParents expands a group and all its parent groups This ensures the group and its contents are visible in the flattened view

func (*GroupTree) Flatten

func (t *GroupTree) Flatten() []Item

Flatten returns a flat list of items for cursor navigation

func (*GroupTree) GetAllInstances

func (t *GroupTree) GetAllInstances() []*Instance

GetAllInstances returns all instances in order

func (*GroupTree) GetGroupNames

func (t *GroupTree) GetGroupNames() []string

GetGroupNames returns all group names for selection

func (*GroupTree) GetGroupPaths

func (t *GroupTree) GetGroupPaths() []string

GetGroupPaths returns all group paths for selection

func (*GroupTree) GroupCount

func (t *GroupTree) GroupCount() int

GroupCount returns total group count

func (*GroupTree) MoveGroupDown

func (t *GroupTree) MoveGroupDown(path string)

MoveGroupDown moves a group down in the order (only within siblings at same level)

func (*GroupTree) MoveGroupTo added in v1.7.29

func (t *GroupTree) MoveGroupTo(sourcePath, destParentPath string) error

MoveGroupTo reparents a group (and its entire subtree) under destParentPath. An empty destParentPath promotes the group to root level. Returns an error for: unknown source, source == DefaultGroupPath, unknown destParent, destParent == source or its descendant (circular), or a collision at the target path. Same-parent call is a no-op.

This is the engine behind the #447 "group change" CLI / TUI.

func (*GroupTree) MoveGroupUp

func (t *GroupTree) MoveGroupUp(path string)

MoveGroupUp moves a group up in the order (only within siblings at same level)

func (*GroupTree) MoveSessionDown

func (t *GroupTree) MoveSessionDown(inst *Instance)

MoveSessionDown moves a session down among its visual siblings. See MoveSessionUp for the sibling-aware semantics; this is the symmetric case that swaps with the next same-parent session in the slice.

When a sub-session has no following same-parent sibling (it is at the bottom of its parent's children block), it is promoted to top-level at its current slice position so the renderer shows it as a top-level peer immediately after the parent's block. At the group's bottom boundary (no following top-level peer) the call is a no-op.

func (*GroupTree) MoveSessionToGroup

func (t *GroupTree) MoveSessionToGroup(inst *Instance, newGroupPath string)

MoveSessionToGroup moves a session to a different group

func (*GroupTree) MoveSessionUp

func (t *GroupTree) MoveSessionUp(inst *Instance)

MoveSessionUp moves a session up among its visual siblings: top-level sessions (empty ParentSessionID) reorder among other top-level sessions in the same group; sub-sessions reorder among other sub-sessions of the same parent. Non-siblings interleaved in the flat slice are skipped.

When a sub-session has no previous same-parent sibling (it is at the top of its parent's children block), it is promoted to top-level and inserted in the slice immediately before the parent. At the group's top boundary (no previous top-level peer) the call is a no-op; cross-group moves remain on the M shortcut.

func (*GroupTree) PromoteSession added in v1.9.2

func (t *GroupTree) PromoteSession(inst *Instance)

PromoteSession converts a sub-session into a top-level peer in the same group. Slice position is preserved so the renderer places the session as a top-level peer immediately after its former parent's children block. Top-level sessions are unchanged.

func (*GroupTree) RemoveSession

func (t *GroupTree) RemoveSession(inst *Instance)

RemoveSession removes a session from its group

func (*GroupTree) RenameGroup

func (t *GroupTree) RenameGroup(oldPath, newName string)

RenameGroup renames a group and updates all subgroups

func (*GroupTree) SessionCount

func (t *GroupTree) SessionCount() int

SessionCount returns total session count

func (*GroupTree) SessionCountForGroup added in v0.8.37

func (t *GroupTree) SessionCountForGroup(groupPath string) int

SessionCountForGroup returns session count for a group INCLUDING all its subgroups This enables hierarchical counts like "Project (5)" where 5 includes all nested sessions

func (*GroupTree) SetDefaultPathForGroup added in v0.19.1

func (t *GroupTree) SetDefaultPathForGroup(groupPath, defaultPath string) bool

SetDefaultPathForGroup sets (or clears) an explicit default path for a group.

func (*GroupTree) ShallowCopyForSave added in v0.7.0

func (t *GroupTree) ShallowCopyForSave() *GroupTree

ShallowCopyForSave creates a copy of the GroupTree that's safe to use from a goroutine for saving purposes. It deep copies the Group structs to prevent data races when the main thread modifies group fields (Name, Path, Expanded, Order) while a background goroutine reads them.

func (*GroupTree) SyncWithInstances

func (t *GroupTree) SyncWithInstances(instances []*Instance)

SyncWithInstances updates the tree with a new set of instances while preserving existing group structure (including empty groups)

func (*GroupTree) ToggleGroup

func (t *GroupTree) ToggleGroup(path string)

ToggleGroup toggles the expanded state of a group

type HTTPServerConfig added in v0.8.96

type HTTPServerConfig struct {
	// Command is the executable to run (e.g., "uvx", "python", "node")
	Command string `toml:"command"`

	// Args are command-line arguments for the server
	Args []string `toml:"args"`

	// Env is environment variables for the server process
	Env map[string]string `toml:"env"`

	// StartupTimeout is milliseconds to wait for server to become ready (default: 5000)
	StartupTimeout int `toml:"startup_timeout"`

	// HealthCheck is an optional health endpoint URL to poll (e.g., "http://localhost:30000/health")
	// If not set, the main URL is used for health checking
	HealthCheck string `toml:"health_check"`
}

HTTPServerConfig defines how to auto-start an HTTP MCP server

func (*HTTPServerConfig) GetStartupTimeout added in v0.8.96

func (c *HTTPServerConfig) GetStartupTimeout() int

GetStartupTimeout returns the startup timeout in milliseconds, defaulting to 5000ms

type HermesOptions added in v1.9.19

type HermesOptions struct {
	// YoloMode enables --yolo flag (auto-approve all tool calls).
	// nil = inherit from config, true/false = explicit override.
	YoloMode *bool `json:"yolo_mode,omitempty"`
}

HermesOptions holds launch options for Hermes Agent CLI sessions. Binary: `hermes` from github.com/NousResearch/hermes-agent (MIT, v0.13.0+). Status detection: process-alive/dead only (content-sniffing deferred). NOTE: CLI --yolo override (via applyCLIYoloOverride) is deferred until HermesOptions is wired into the launch command builder.

func NewHermesOptions added in v1.9.19

func NewHermesOptions(config *UserConfig) *HermesOptions

NewHermesOptions creates HermesOptions with defaults from config.

func UnmarshalHermesOptions added in v1.9.19

func UnmarshalHermesOptions(data json.RawMessage) (*HermesOptions, error)

UnmarshalHermesOptions deserializes HermesOptions from JSON wrapper.

func (*HermesOptions) ToArgs added in v1.9.19

func (o *HermesOptions) ToArgs() []string

ToArgs returns command-line arguments based on options.

func (*HermesOptions) ToolName added in v1.9.19

func (o *HermesOptions) ToolName() string

ToolName returns "hermes"

type HermesSettings added in v1.9.19

type HermesSettings struct {
	// Command is the Hermes CLI command or invocation to use.
	// Supports flags (e.g., "hermes --model gpt-5.5-pro --provider openai").
	// Default: "hermes"
	Command string `toml:"command"`
	// EnvFile is a .env file specific to Hermes sessions (sourced before
	// the `hermes` command runs). Optional.
	EnvFile string `toml:"env_file"`
	// YoloMode enables --yolo flag for Hermes sessions (auto-approve all tool calls).
	// Default: false
	YoloMode bool `toml:"yolo_mode"`
}

HermesSettings defines Hermes Agent CLI configuration. Binary: `hermes` from github.com/NousResearch/hermes-agent (MIT, v0.13.0+). Status detection: process-alive/dead only (content-sniffing deferred).

type HookStatus added in v0.16.0

type HookStatus struct {
	Status    string    // running, idle, waiting, dead
	SessionID string    // Claude session ID
	Event     string    // Hook event name
	UpdatedAt time.Time // When this status was received
}

HookStatus holds the decoded status from a hook status file.

type IdleTimeoutWatcher added in v1.9.29

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

IdleTimeoutWatcher polls running sessions, tracks per-instance pane-content hashes, and triggers Stop when content stays unchanged longer than IdleTimeoutSecs.

Safe for use from one goroutine. Tick is the unit of work; production code drives Tick from an existing 2-second ticker (e.g. statusWorker in the TUI) and ticks roughly once per minute via Start.

func NewIdleTimeoutWatcher added in v1.9.29

func NewIdleTimeoutWatcher(cfg IdleTimeoutWatcherConfig) *IdleTimeoutWatcher

NewIdleTimeoutWatcher constructs a watcher with production defaults filled in for any nil config callback.

func (*IdleTimeoutWatcher) Start added in v1.9.29

func (w *IdleTimeoutWatcher) Start(ctx context.Context, interval time.Duration, snapshot func() []*Instance)

Start runs Tick at the given interval until ctx is cancelled. instances is re-fetched each tick via the snapshot func; pass the parent TUI/daemon's instance accessor so the watcher always sees fresh state.

func (*IdleTimeoutWatcher) Tick added in v1.9.29

func (w *IdleTimeoutWatcher) Tick(instances []*Instance)

Tick scans the given instances. Sessions with IdleTimeoutSecs <= 0 are skipped (and their tracking state is cleared so toggling off via SetField works on the next tick). Non-running sessions are skipped too.

type IdleTimeoutWatcherConfig added in v1.9.29

type IdleTimeoutWatcherConfig struct {
	// Now is the clock source. Defaults to time.Now.
	Now func() time.Time
	// Capture returns the current tmux pane content for the instance.
	// Defaults to instance.tmuxSession.CapturePane().
	Capture func(*Instance) (string, error)
	// Stop is invoked once when an instance has been idle for >= its
	// IdleTimeoutSecs. Defaults to inst.Kill().
	Stop func(*Instance) error
	// LogEvent persists a "session lifecycle" row. Defaults to
	// WriteSessionLifecycleEvent.
	LogEvent func(SessionLifecycleEvent) error
}

IdleTimeoutWatcherConfig wires the watcher to its environment. All fields are optional and default to production behavior; tests inject Now / Capture / Stop so they don't touch real tmux or real clocks.

type Instance

type Instance struct {
	ID                 string `json:"id"`
	Title              string `json:"title"`
	ProjectPath        string `json:"project_path"`
	GroupPath          string `json:"group_path"`                     // e.g., "projects/devops"
	Order              int    `json:"order"`                          // Position within group (for reorder persistence)
	ParentSessionID    string `json:"parent_session_id,omitempty"`    // Links to parent session (makes this a sub-session)
	ParentProjectPath  string `json:"parent_project_path,omitempty"`  // Parent's project path (for --add-dir access)
	IsConductor        bool   `json:"is_conductor,omitempty"`         // True if this session is a conductor orchestrator
	NoTransitionNotify bool   `json:"no_transition_notify,omitempty"` // Suppress transition event dispatch for this session

	// TitleLocked, when true, blocks Claude's session name from syncing into
	// the agent-deck Title (issue #697). Conductors launch workers with a
	// semantic title (e.g. "SCRUM-351") that Claude would otherwise overwrite
	// with its auto-generated summary on the next hook event. Set via
	// `--title-lock` on add/launch or `session set-title-lock`.
	TitleLocked bool `json:"title_locked,omitempty"`

	// Git worktree support
	WorktreePath     string `json:"worktree_path,omitempty"`      // Path to worktree (if session is in worktree)
	WorktreeRepoRoot string `json:"worktree_repo_root,omitempty"` // Original repo root
	WorktreeBranch   string `json:"worktree_branch,omitempty"`    // Branch name in worktree
	WorktreeType     string `json:"worktree_type,omitempty"`      // "git", "jujutsu", or "" (legacy = git)

	// Account is the per-session named account slot (issue #924). Maps to
	// `[profiles.<account>.claude].config_dir` in ~/.agent-deck/config.toml
	// at spawn time and becomes the most-specific level in the
	// CLAUDE_CONFIG_DIR resolution chain — beating conductor / group / env.
	// Switching the value requires a session restart (the Option 1 MVP
	// tradeoff): the in-flight Claude conversation is lost since the new
	// account's settings.json and history live elsewhere. Empty means
	// "fall through to conductor/group/env/profile/global/default" so
	// pre-#924 sessions keep their existing behavior unchanged.
	Account string `json:"account,omitempty"`

	// Multi-repo support
	MultiRepoEnabled   bool                `json:"multi_repo_enabled,omitempty"`
	AdditionalPaths    []string            `json:"additional_paths,omitempty"`    // Paths beyond ProjectPath
	MultiRepoTempDir   string              `json:"multi_repo_temp_dir,omitempty"` // Temp cwd for multi-repo sessions
	MultiRepoWorktrees []MultiRepoWorktree `json:"multi_repo_worktrees,omitempty"`

	Command        string    `json:"command"`
	Wrapper        string    `json:"wrapper,omitempty"` // Optional wrapper command with {command} placeholder
	Tool           string    `json:"tool"`
	Status         Status    `json:"status"`
	CreatedAt      time.Time `json:"created_at"`
	LastAccessedAt time.Time `json:"last_accessed_at,omitempty"` // When user last attached

	// LastStartedAt is the wall-clock time of the most recent successful
	// Start() / StartWithMessage() / Restart() call. Persisted so short-lived
	// CLI invocations can see it across processes (issue #30): a restart
	// queued seconds after a start must detect the session is already fresh
	// and skip the teardown. Zero value means "unknown" (old record or
	// never started) and callers MUST NOT treat zero as "just now".
	LastStartedAt time.Time `json:"last_started_at,omitempty"`

	// Claude Code integration
	ClaudeSessionID  string    `json:"claude_session_id,omitempty"`
	ClaudeDetectedAt time.Time `json:"claude_detected_at,omitempty"`

	// Gemini CLI integration
	GeminiSessionID  string                  `json:"gemini_session_id,omitempty"`
	GeminiDetectedAt time.Time               `json:"gemini_detected_at,omitempty"`
	GeminiYoloMode   *bool                   `json:"gemini_yolo_mode,omitempty"` // Per-session override (nil = use global config)
	GeminiModel      string                  `json:"gemini_model,omitempty"`     // Active model for this session
	GeminiAnalytics  *GeminiSessionAnalytics `json:"gemini_analytics,omitempty"` // Per-session analytics

	// OpenCode CLI integration
	OpenCodeSessionID  string    `json:"opencode_session_id,omitempty"`
	OpenCodeDetectedAt time.Time `json:"opencode_detected_at,omitempty"`
	OpenCodeStartedAt  int64     `json:"-"` // Unix millis when we started OpenCode (for session matching, not persisted)

	// Codex CLI integration
	CodexSessionID  string    `json:"codex_session_id,omitempty"`
	CodexDetectedAt time.Time `json:"codex_detected_at,omitempty"`
	CodexStartedAt  int64     `json:"-"` // Unix millis when we started Codex (for session matching, not persisted)

	// GitHub Copilot CLI integration
	CopilotSessionID  string    `json:"copilot_session_id,omitempty"`
	CopilotDetectedAt time.Time `json:"copilot_detected_at,omitempty"`
	CopilotStartedAt  int64     `json:"-"`                           // Unix millis when we started Copilot (for session matching, not persisted)
	CopilotModel      string    `json:"copilot_model,omitempty"`     // Active model for this session
	CopilotAllowAll   bool      `json:"copilot_allow_all,omitempty"` // Per-session --allow-all override

	// Latest user input for context (extracted from session files)
	LatestPrompt string `json:"latest_prompt,omitempty"`
	Notes        string `json:"notes,omitempty"`

	// Color is an optional user-chosen tint for this session's TUI row (issue #391).
	// Accepts a lipgloss-compatible color spec:
	//   - "#RRGGBB"      - truecolor hex
	//   - "0".."255"     - ANSI 256-palette index as a decimal string
	//   - ""             - default (no tint, current rendering unchanged)
	// Validation happens at CLI/API boundary in cmd/agent-deck/session_cmd.go.
	// Empty string is the default so the field is fully opt-in and never
	// changes rendering for users who don't set it.
	Color string `json:"color,omitempty"`

	// Docker sandbox support.
	Sandbox          *SandboxConfig `json:"sandbox,omitempty"`
	SandboxContainer string         `json:"sandbox_container,omitempty"` // Container name when running in sandbox.

	// SSH remote support
	SSHHost       string `json:"ssh_host,omitempty"`
	SSHRemotePath string `json:"ssh_remote_path,omitempty"`

	// TmuxSocketName is the tmux `-L <name>` socket selector captured when
	// this instance was created (v1.7.50+, issue #687). Empty string keeps
	// the pre-v1.7.50 behavior of targeting the user's default tmux server
	// — zero change for existing installations.
	//
	// Precedence at creation time: the `--tmux-socket` CLI flag on
	// `agent-deck add` / `agent-deck launch` wins, else
	// `[tmux].socket_name` from config.toml, else empty. Once persisted,
	// this value is IMMUTABLE — lifecycle operations (start/stop/restart/
	// revive) MUST target this same socket even if the installation-wide
	// config is later edited. Mixing sockets would leave the session
	// orphaned on an unreachable tmux server.
	TmuxSocketName string `json:"tmux_socket_name,omitempty"`

	// MCP tracking - which MCPs were loaded when session started/restarted
	// Used to detect pending MCPs (added after session start) and stale MCPs (removed but still running)
	LoadedMCPNames []string `json:"loaded_mcp_names,omitempty"`

	// TrackedMCPPIDs holds the OS PIDs of stdio MCP children spawned for
	// this session (issue #965). Session stop must SIGTERM (then SIGKILL
	// after a grace period) each PID so children aren't reparented to
	// PID 1 and leaked. Mutated only via RegisterMCPChild /
	// UnregisterMCPChild to keep concurrent access safe.
	TrackedMCPPIDs []int `json:"tracked_mcp_pids,omitempty"`

	// Channels are Claude Code plugin-channel ids (e.g. "plugin:telegram@user/repo").
	// When non-empty on a claude session, buildClaudeExtraFlags emits
	// `--channels <csv>` so the session subscribes to inbound plugin messages.
	// Without this flag the channel plugin runs as a plain MCP (tools only,
	// no inbound delivery) which silently drops Telegram/Discord/Slack
	// messages on conductor restart.
	Channels []string `json:"channels,omitempty"`

	// Plugins is the catalog-key list of Claude Code plugins enabled for
	// this session via `agent-deck add --plugin <name>` /
	// `session set <id> plugins <csv>`. Names are short catalog keys (NOT
	// fully-qualified `<name>@<source>` ids) and resolve through the
	// [plugins.<name>] table in ~/.agent-deck/config.toml at spawn time.
	// When non-empty on a claude session, EnsureWorkerScratchConfigDir
	// writes enabledPlugins[<id>] = true into the scratch settings.json so
	// the plugin loads only for this session, not globally.
	// RFC: docs/rfc/PLUGIN_ATTACH.md.
	Plugins []string `json:"plugins,omitempty"`

	// InheritTelegramEnv is the explicit opt-in for #1133: when true, a
	// non-channel-owning claude child KEEPS the conductor's TELEGRAM_*
	// env vars (TELEGRAM_STATE_DIR, TELEGRAM_BOT_TOKEN, etc.). Default
	// false strips them so a child can't spawn a duplicate `bun telegram`
	// poller that races the conductor for getUpdates (Telegram 409
	// Conflict + dropped inbound messages). CLI flag:
	// `--inherit-telegram-env` on `agent-deck launch`. Rare use case;
	// existing behavior is preserved when the flag is absent.
	InheritTelegramEnv bool `json:"inherit_telegram_env,omitempty"`

	// PluginChannelLinkDisabled opts the session out of the catalog-driven
	// auto-link between Plugins and Channels (RFC §4.7). When true, an
	// `--plugin foo` whose catalog entry has EmitsChannel=true does NOT
	// auto-add `plugin:foo@source` to Channels. Useful for tools-only
	// usage of channel-emitting plugins. CLI flag: `--no-channel-link`.
	PluginChannelLinkDisabled bool `json:"plugin_channel_link_disabled,omitempty"`

	// AutoLinkedChannels is the persisted set of channel ids that
	// syncPluginChannels last added via the auto-link mechanism. Lets
	// reconciliation distinguish "channel I owned" from "channel the
	// user added manually" — without it, a plugin removed from the
	// catalog or an opt-out toggle would leave stale autolinks behind
	// (G4 / C2). Updated on every Plugins mutation; never written
	// directly by users.
	AutoLinkedChannels []string `json:"auto_linked_channels,omitempty"`

	// WorkerScratchConfigDir is the ephemeral CLAUDE_CONFIG_DIR prepared
	// for a non-conductor claude worker (issue #59, v1.7.68). The
	// scratch dir copies the ambient profile's settings.json with the
	// telegram plugin explicitly disabled, symlinks the rest of the
	// profile, and is cleaned up on session stop/remove. Empty for
	// conductor sessions, explicit telegram channel owners, and
	// non-claude tools — they use the ambient profile as-is.
	WorkerScratchConfigDir string `json:"worker_scratch_config_dir,omitempty"`

	// IdleTimeoutSecs is the auto-stop threshold (#1143). When > 0, a central
	// watcher poll triggers Kill() if the tmux pane content stays unchanged
	// for this many seconds. 0 = disabled (current behavior). Default is 0
	// so existing sessions are unaffected on upgrade.
	IdleTimeoutSecs int64 `json:"idle_timeout_secs,omitempty"`

	// IsForkAwaitingStart signals that this instance was produced by
	// CreateForkedInstanceWithOptions and holds a pre-built fork command
	// in Command that must be run verbatim on the first Start() (#745).
	// Without this flag, Start()'s claude-compatible dispatch sees the
	// pre-populated ClaudeSessionID (the new fork UUID), routes to
	// buildClaudeResumeCommand, which fails to find a JSONL for a
	// brand-new UUID and falls back to a plain --session-id fresh
	// command — stripping --resume <parent-id> / --fork-session and
	// dropping all conversation history from the parent. Transient
	// (json:"-"): persisting this would cause a restart of the forked
	// session to re-emit --fork-session and double-count the parent
	// transcript.
	IsForkAwaitingStart bool `json:"-"`

	// ExtraArgs are user-supplied claude CLI tokens appended verbatim to every
	// start/resume/fork command (e.g. ["--agent","reviewer","--model","opus"]).
	// Each token is shellescape-quoted on emission so values with spaces
	// survive the bash -c wrapper.
	ExtraArgs []string `json:"extra_args,omitempty"`

	// StartupQuery is the claude-code positional "startup query" (#725,
	// v1.7.67). Set from the new-session dialog's "Start query" field and
	// emitted as a single shell-quoted positional arg on the claude
	// new-session command line only.
	//
	// Per-session, NEVER persisted — the `json:"-"` tag is load-bearing.
	// On Restart/Resume the field is empty, so the query does NOT replay.
	// This is the whole point of having a dedicated field instead of
	// overloading ExtraArgs (which persists and space-splits).
	StartupQuery string `json:"-"`

	// ToolOptions stores tool-specific launch options (Claude, Codex, Gemini, etc.)
	// JSON structure: {"tool": "claude", "options": {...}}
	ToolOptionsJSON json.RawMessage `json:"tool_options,omitempty"`

	// SkipMCPRegenerate skips .mcp.json regeneration on next Restart()
	// Set by MCP dialog Apply() to avoid race condition where Apply writes
	// config then Restart immediately overwrites it with different pool state
	SkipMCPRegenerate bool `json:"-"` // Don't persist, transient flag
	// contains filtered or unexported fields
}

Instance represents a single agent/shell session

func DiscoverExistingTmuxSessions

func DiscoverExistingTmuxSessions(existingInstances []*Instance) ([]*Instance, error)

DiscoverExistingTmuxSessions finds all tmux sessions and converts them to instances

func FilterByQuery

func FilterByQuery(instances []*Instance, query string) []*Instance

FilterByQuery filters sessions by title, project path, tool, or status Supports status filters: "waiting", "running", "idle", "error"

func FindNextQueued added in v1.9.1

func FindNextQueued(instances []*Instance, groupPath string) *Instance

FindNextQueued returns the oldest queued instance in the given group, or nil if none are queued. "Oldest" is by CreatedAt (FIFO drain order).

func NewInstance

func NewInstance(title, projectPath string) *Instance

NewInstance creates a new session instance

func NewInstanceWithGroup

func NewInstanceWithGroup(title, projectPath, groupPath string) *Instance

NewInstanceWithGroup creates a new session instance with explicit group

func NewInstanceWithGroupAndTool added in v0.4.1

func NewInstanceWithGroupAndTool(title, projectPath, groupPath, tool string) *Instance

NewInstanceWithGroupAndTool creates a new session with explicit group and tool

func NewInstanceWithTool added in v0.4.1

func NewInstanceWithTool(title, projectPath, tool string) *Instance

NewInstanceWithTool creates a new session with tool-specific initialization

func (*Instance) AllProjectPaths added in v0.26.2

func (inst *Instance) AllProjectPaths() []string

AllProjectPaths returns all project paths: [ProjectPath] + AdditionalPaths.

func (*Instance) ApplyLaunchModel added in v1.9.14

func (i *Instance) ApplyLaunchModel(model string) error

ApplyLaunchModel stores a per-session model override in the tool-specific field that the relevant command builder already reads on start/restart.

func (*Instance) CanFork added in v0.4.1

func (i *Instance) CanFork() bool

CanFork returns true if this session can be forked

func (*Instance) CanForkOpenCode added in v0.8.99

func (i *Instance) CanForkOpenCode() bool

CanForkOpenCode returns true if this OpenCode session can be forked

func (*Instance) CanRestart added in v0.4.1

func (i *Instance) CanRestart() bool

CanRestart returns true if the session can be restarted For Claude sessions with known ID: can always restart (interrupt and resume) For Gemini sessions with known ID: can always restart (interrupt and resume) For OpenCode sessions with known ID: can always restart (interrupt and resume) For Codex sessions with known ID: can always restart (interrupt and resume) For custom tools with session resume config: can restart if session ID available For other sessions: only if dead/error state

func (*Instance) CanRestartFresh added in v1.7.21

func (i *Instance) CanRestartFresh() bool

CanRestartFresh returns true when the session has a known tool session binding that can be intentionally discarded to start with a new session ID.

func (*Instance) CanRestartGeneric added in v0.8.27

func (i *Instance) CanRestartGeneric() bool

CanRestartGeneric returns true if a custom tool can be restarted with session resume

func (*Instance) CaptureLoadedMCPs added in v0.5.3

func (i *Instance) CaptureLoadedMCPs()

CaptureLoadedMCPs captures the current MCP names as the "loaded" state This should be called when a session starts or restarts, so we can track which MCPs are actually loaded in the running Claude session vs just configured

func (*Instance) CleanupMultiRepoTempDir added in v0.26.2

func (inst *Instance) CleanupMultiRepoTempDir() error

CleanupMultiRepoTempDir removes the multi-repo temporary directory.

func (*Instance) CleanupWorkerScratchConfigDir added in v1.7.68

func (i *Instance) CleanupWorkerScratchConfigDir()

CleanupWorkerScratchConfigDir removes the scratch dir. Best-effort.

func (*Instance) ClearHookStatus added in v0.16.0

func (i *Instance) ClearHookStatus()

ClearHookStatus resets the hook-based status and removes the persisted hook record, forcing the next UpdateStatus() to fall through to polling. Used when the user manually overrides status (e.g., pressing 'u' to unacknowledge after an Escape interrupt where the Stop hook didn't fire).

func (*Instance) ClearParent added in v0.7.0

func (inst *Instance) ClearParent()

ClearParent removes the parent session link

func (*Instance) ConductorClearOnCompact added in v0.19.15

func (i *Instance) ConductorClearOnCompact() bool

ConductorClearOnCompact checks if this conductor instance has clear_on_compact enabled. Extracts the conductor name from the session title ("conductor-{NAME}"), loads meta.json, and returns the setting (defaults to true). Returns false if the title doesn't match conductor format, since the caller should not enable clear-on-compact for non-conductor sessions.

func (*Instance) ConsumeCodexRestartWarning added in v0.21.0

func (i *Instance) ConsumeCodexRestartWarning() string

ConsumeCodexRestartWarning returns and clears any pending Codex restart warning.

func (*Instance) CreateForkedInstance added in v0.4.1

func (i *Instance) CreateForkedInstance(newTitle, newGroupPath string) (*Instance, string, error)

CreateForkedInstance creates a new Instance configured for forking Deprecated: Use CreateForkedInstanceWithOptions instead

func (*Instance) CreateForkedInstanceWithOptions added in v0.8.39

func (i *Instance) CreateForkedInstanceWithOptions(
	newTitle, newGroupPath string,
	opts *ClaudeOptions,
) (*Instance, string, error)

CreateForkedInstanceWithOptions creates a new Instance configured for forking with custom options

func (*Instance) CreateForkedOpenCodeInstance added in v0.8.99

func (i *Instance) CreateForkedOpenCodeInstance(newTitle, newGroupPath string) (*Instance, string, error)

CreateForkedOpenCodeInstance creates a new Instance configured for forking an OpenCode session Deprecated: Use CreateForkedOpenCodeInstanceWithOptions instead.

func (*Instance) CreateForkedOpenCodeInstanceWithOptions added in v0.12.1

func (i *Instance) CreateForkedOpenCodeInstanceWithOptions(
	newTitle, newGroupPath string,
	opts *OpenCodeOptions,
) (*Instance, string, error)

CreateForkedOpenCodeInstanceWithOptions creates a new Instance configured for forking with custom options

func (*Instance) DetectCodexSession added in v0.8.76

func (i *Instance) DetectCodexSession()

DetectCodexSession is the public wrapper for async Codex session detection Call this for restored sessions that don't have a session ID yet

func (*Instance) DetectOpenCodeSession added in v0.8.72

func (i *Instance) DetectOpenCodeSession()

DetectOpenCodeSession is the public wrapper for async OpenCode session detection Call this for restored sessions that don't have a session ID yet

func (*Instance) EffectiveWorkingDir added in v0.26.2

func (inst *Instance) EffectiveWorkingDir() string

EffectiveWorkingDir returns the working directory for this session. For multi-repo sessions, this is the temp dir; otherwise the ProjectPath.

func (*Instance) EnsurePluginsInstalled added in v1.9.2

func (i *Instance) EnsurePluginsInstalled(sourceProfileDir string) error

EnsurePluginsInstalled shells out to `claude plugin install` for every catalog plugin in i.Plugins whose code isn't already on disk under sourceProfileDir. Best-effort: always returns nil. Concurrent installs across sessions serialize via a per-(profile, plugin) lock under `~/.agent-deck/locks/`. The catalog `auto_install` flag is NOT consulted — explicit attach is itself the consent signal.

func (*Instance) EnsureWorkerScratchConfigDir added in v1.7.68

func (i *Instance) EnsureWorkerScratchConfigDir(sourceProfileDir string) (string, error)

EnsureWorkerScratchConfigDir idempotently prepares the scratch CLAUDE_CONFIG_DIR. Returns "" (no error) when no scratch is needed — callers treat that as "use the ambient profile". The scratch mirrors sourceProfileDir via symlinks and rewrites settings.json with the deny+allow overlay. Source absence is fine — we emit a minimal settings.json.

func (*Instance) Exists

func (i *Instance) Exists() bool

Exists checks if the tmux session still exists

func (*Instance) ForceNextStatusCheck added in v0.16.0

func (i *Instance) ForceNextStatusCheck()

ForceNextStatusCheck clears the idle polling optimization so the next UpdateStatus() performs a full check instead of short-circuiting. Call this before UpdateStatus() when a status-affecting change was made externally (e.g. the u key toggling acknowledged state).

func (*Instance) Fork added in v0.4.1

func (i *Instance) Fork(newTitle, newGroupPath string) (string, error)

Fork returns the command to create a forked Claude session Uses capture-resume pattern: starts fork in print mode to get new session ID, stores in tmux environment, then resumes interactively Deprecated: Use ForkWithOptions instead

func (*Instance) ForkOpenCode added in v0.8.99

func (i *Instance) ForkOpenCode(newTitle, newGroupPath string) (string, error)

ForkOpenCode returns the command to create a forked OpenCode session. Uses export/import to clone the session with a new ID, then launches the forked session with opencode -s <new-id>. Deprecated: Use ForkOpenCodeWithOptions instead.

func (*Instance) ForkOpenCodeWithOptions added in v0.12.1

func (i *Instance) ForkOpenCodeWithOptions(newTitle, newGroupPath string, opts *OpenCodeOptions) (string, error)

ForkOpenCodeWithOptions returns the command to create a forked OpenCode session with custom options. Uses export/import to clone the session with a new ID, then launches the forked session with opencode -s <new-id> plus any model/agent flags.

func (*Instance) ForkWithOptions added in v0.8.39

func (i *Instance) ForkWithOptions(newTitle, newGroupPath string, opts *ClaudeOptions) (string, error)

ForkWithOptions returns the command to create a forked Claude session with custom options Uses capture-resume pattern: starts fork in print mode to get new session ID, stores in tmux environment, then resumes interactively

func (*Instance) GetActualWorkDir added in v0.4.1

func (i *Instance) GetActualWorkDir() string

GetActualWorkDir returns the actual working directory from tmux, or falls back to ProjectPath

func (*Instance) GetClaudeOptions added in v0.8.39

func (i *Instance) GetClaudeOptions() *ClaudeOptions

GetClaudeOptions returns Claude-specific options, or nil if not set

func (*Instance) GetCodexOptions added in v0.10.18

func (i *Instance) GetCodexOptions() *CodexOptions

GetCodexOptions returns Codex-specific options, or nil if not set

func (*Instance) GetCopilotOptions added in v1.9.20

func (i *Instance) GetCopilotOptions() *CopilotOptions

GetCopilotOptions returns CopilotOptions from the instance's tool options.

func (*Instance) GetGenericSessionID added in v0.8.27

func (i *Instance) GetGenericSessionID() string

GetGenericSessionID gets session ID from tmux environment for a custom tool Uses the session_id_env field from tool config

func (*Instance) GetHookStatus added in v0.16.0

func (i *Instance) GetHookStatus() (string, bool)

GetHookStatus returns the current hook-based status and its freshness. Freshness window is tool-specific.

func (*Instance) GetJSONLPath added in v0.8.27

func (i *Instance) GetJSONLPath() string

GetJSONLPath returns the path to the Claude session JSONL file for analytics Returns empty string if this is not a Claude session or no session ID is available

func (*Instance) GetLastActivityTime added in v0.5.6

func (inst *Instance) GetLastActivityTime() time.Time

GetLastActivityTime returns when the session was last active (content changed) Returns CreatedAt if no activity has been tracked yet

func (*Instance) GetLastResponse added in v0.7.0

func (i *Instance) GetLastResponse() (*ResponseOutput, error)

GetLastResponse returns the last assistant response from the session For Claude: Parses the JSONL file for the last assistant message For Gemini: Parses the JSON session file for the last assistant message For Codex/Others: Attempts to parse terminal output

func (*Instance) GetLastResponseBestEffort added in v0.19.10

func (i *Instance) GetLastResponseBestEffort() (*ResponseOutput, error)

GetLastResponseBestEffort returns the last assistant response with fallback logic intended for CLI read paths (like `session output`) where we prefer useful output over hard errors.

Behavior for Claude: 1. Try structured JSONL read via stored ClaudeSessionID. 2. Refresh ID from tmux env and retry. 3. Fallback to terminal parsing. 4. If still unavailable, return an empty response (no error).

Behavior for Gemini (mirrors Claude): 1. Try structured JSON read via stored GeminiSessionID. 2. Refresh ID from tmux env and retry. 3. Scan disk for latest session and retry. 4. Fallback to terminal parsing. 5. If still unavailable, return an empty response (no error).

func (*Instance) GetMCPInfo added in v0.5.3

func (i *Instance) GetMCPInfo() *MCPInfo

GetMCPInfo returns MCP server information for this session Returns nil if not a Claude or Gemini session

func (*Instance) GetOpenCodeOptions added in v0.12.1

func (i *Instance) GetOpenCodeOptions() *OpenCodeOptions

GetOpenCodeOptions returns OpenCode-specific options, or nil if not set

func (*Instance) GetSessionIDFromTmux added in v0.5.0

func (i *Instance) GetSessionIDFromTmux() string

GetSessionIDFromTmux reads Claude session ID from tmux environment This is the primary method for sessions started with the capture-resume pattern

func (*Instance) GetStatusThreadSafe added in v0.10.6

func (inst *Instance) GetStatusThreadSafe() Status

GetStatusThreadSafe returns the session status with read-lock protection. Use this when reading Status from a goroutine concurrent with backgroundStatusUpdate.

func (*Instance) GetTmuxSession

func (i *Instance) GetTmuxSession() *tmux.Session

GetTmuxSession returns the tmux session object

func (*Instance) GetToolThreadSafe added in v0.10.6

func (inst *Instance) GetToolThreadSafe() string

GetToolThreadSafe returns the tool name with read-lock protection.

func (*Instance) GetWaitingSince added in v0.8.50

func (inst *Instance) GetWaitingSince() time.Time

GetWaitingSince returns when the session transitioned to waiting status Used for sorting notification bar (newest waiting sessions first)

func (*Instance) HasUpdated

func (i *Instance) HasUpdated() bool

HasUpdated checks if there's new output since last check

func (*Instance) IsMultiRepo added in v0.26.2

func (inst *Instance) IsMultiRepo() bool

IsMultiRepo returns true if this session has multi-repo mode enabled.

func (*Instance) IsSSH added in v0.20.0

func (inst *Instance) IsSSH() bool

IsSSH returns true if this instance runs on a remote host via SSH.

func (*Instance) IsSandboxed added in v0.19.17

func (inst *Instance) IsSandboxed() bool

IsSandboxed returns true if this instance is configured to run in a Docker sandbox.

func (*Instance) IsSubSession added in v0.7.0

func (inst *Instance) IsSubSession() bool

IsSubSession returns true if this session has a parent

func (*Instance) IsWorktree added in v0.8.22

func (inst *Instance) IsWorktree() bool

IsWorktree returns true if this session is running in a git worktree

func (*Instance) Kill

func (i *Instance) Kill() error

Kill terminates the tmux session and cleans up sandbox container if present.

func (*Instance) KillAndWait added in v1.7.68

func (i *Instance) KillAndWait() error

KillAndWait is the synchronous companion to Kill. It performs the same teardown AND blocks until the pane process tree has been verified dead (SIGTERM → SIGKILL escalation inline, not in a background goroutine). Callers in short-lived CLI processes (`agent-deck remove`, `agent-deck session remove`) MUST use this variant — see issue #59 (v1.7.68). The TUI and web callers can keep using Kill for the non-blocking path.

func (*Instance) LaunchModelID added in v1.9.14

func (i *Instance) LaunchModelID() string

LaunchModelID returns the exact per-session model override that will be passed to the underlying tool on start/restart. Empty means tool default.

func (*Instance) LaunchModelInfo added in v1.9.14

func (i *Instance) LaunchModelInfo() ModelInfo

LaunchModelInfo returns normalized status fields for the session's model override. Empty ModelID means the tool's configured default is in effect.

func (*Instance) MarkAccessed added in v0.5.3

func (inst *Instance) MarkAccessed()

MarkAccessed updates the LastAccessedAt timestamp to now

func (*Instance) NeedsWorkerScratchConfigDir added in v1.7.68

func (i *Instance) NeedsWorkerScratchConfigDir() bool

NeedsWorkerScratchConfigDir is true when a scratch CLAUDE_CONFIG_DIR must be prepared at spawn. Reasons:

  1. Telegram poller defense (v1.7.68 / #759) — pin telegramPluginID off.
  2. Per-session plugin enablement (RFC docs/rfc/PLUGIN_ATTACH.md) — write enabledPlugins[<id>] = true without contaminating the ambient profile or peer sessions.
  3. GLOBAL_ANTIPATTERN guard (issue #941) — channel-owning conductor with `enabledPlugins.telegram=true` already in the ambient settings.json. The TelegramValidator surfaces this as DOUBLE_LOAD but warnings don't prevent the spawn; the scratch pins telegram off so --channels is the only activation source and exactly one bun poller runs.

When multiple reasons fire, EnsureWorkerScratchConfigDir combines the deny+allow lists.

func (*Instance) OpenContainerShell added in v0.19.17

func (i *Instance) OpenContainerShell() (string, error)

OpenContainerShell creates a tmux session running an interactive shell inside the sandbox container. Returns the tmux session name for attaching. Uses /bin/sh for portability (not all images have bash).

func (*Instance) PostStartSync added in v0.8.97

func (i *Instance) PostStartSync(maxWait time.Duration)

PostStartSync captures session IDs from tmux environment after Start() or Restart(). Designed for CLI commands that exit after starting. The TUI doesn't need this because its background worker handles detection.

For Claude: polls tmux env for CLAUDE_SESSION_ID (set by bash uuidgen before exec). For Gemini: reads session ID from filesystem. For OpenCode/Codex: no-op (async goroutine detection, too slow for sync CLI).

func (*Instance) Preview

func (i *Instance) Preview() (string, error)

Preview returns the last 3 lines of terminal output

func (*Instance) PreviewFull

func (i *Instance) PreviewFull() (string, error)

PreviewFull returns all terminal output

func (*Instance) PreviewWindowFull added in v0.21.0

func (i *Instance) PreviewWindowFull(windowIndex int) (string, error)

PreviewWindowFull returns the full scrollback of a specific tmux window.

func (*Instance) RefreshLiveSessionIDs added in v1.7.13

func (i *Instance) RefreshLiveSessionIDs()

RefreshLiveSessionIDs re-reads tool-specific session identifiers from the live tmux environment and updates the instance's stored IDs when a newer non-empty value is found. Safe no-op when tmuxSession is nil or the tool has no live-env handle.

Call this before reads that must reflect the CURRENT conversation (e.g. TUI cross-session send-output, issue #598). Reads that tolerate stale data (status polling) don't need it.

func (*Instance) RegisterMCPChild added in v1.9.9

func (i *Instance) RegisterMCPChild(pid int)

RegisterMCPChild records the OS PID of a stdio MCP child spawned for this session. Session stop iterates these PIDs and signals each (SIGTERM → SIGKILL) to prevent the issue-#965 orphan accumulation where MCP children get reparented to PID 1.

Safe to call concurrently. Passing pid <= 0 is a no-op.

func (*Instance) Restart added in v0.4.1

func (i *Instance) Restart() error

Restart restarts the Claude session For Claude sessions with known ID: sends Ctrl+C twice and resume command to existing session For dead sessions or unknown ID: recreates the tmux session

Issue #1040: gated by acquireInstanceSpawnLock plus a "spawned-while- we-waited" stamp so concurrent callers (TUI poller + RC-exit handler in-process; multiple `agent-deck session start` CLI invocations cross-process) cannot each race to recreate a tmux session for the same instance. A legitimate manual restart still proceeds because the stamp from any prior spawn pre-dates the new caller's beforeLock.

func (*Instance) RestartFresh added in v1.7.21

func (i *Instance) RestartFresh() error

RestartFresh restarts the current tool without resuming the existing tool session. This recreates the tmux session and clears the stored tool session binding first, so the next start gets a brand-new tool session ID.

func (*Instance) SetAcknowledgedFromShared added in v0.11.0

func (i *Instance) SetAcknowledgedFromShared(ack bool)

SetAcknowledgedFromShared applies an acknowledgment from another TUI instance (read from SQLite). This transitions a YELLOW (waiting) session to GRAY (idle) without requiring the user to interact with this specific TUI instance.

func (*Instance) SetClaudeOptions added in v0.8.39

func (i *Instance) SetClaudeOptions(opts *ClaudeOptions) error

SetClaudeOptions stores Claude-specific options

func (*Instance) SetCodexOptions added in v0.10.18

func (i *Instance) SetCodexOptions(opts *CodexOptions) error

SetCodexOptions stores Codex-specific options

func (*Instance) SetGeminiModel added in v0.8.79

func (i *Instance) SetGeminiModel(model string) error

SetGeminiModel sets the Gemini model for this session and triggers a restart if running.

func (*Instance) SetGeminiYoloMode added in v0.8.50

func (i *Instance) SetGeminiYoloMode(enabled bool)

SetGeminiYoloMode sets the YOLO mode for Gemini and syncs it to the tmux environment. This ensures the background status worker sees the correct state during restarts.

func (*Instance) SetOpenCodeOptions added in v0.12.1

func (i *Instance) SetOpenCodeOptions(opts *OpenCodeOptions) error

SetOpenCodeOptions stores OpenCode-specific options

func (*Instance) SetParent added in v0.7.0

func (inst *Instance) SetParent(parentID string)

SetParent sets the parent session ID

func (*Instance) SetParentWithPath added in v0.8.34

func (inst *Instance) SetParentWithPath(parentID, parentProjectPath string)

SetParentWithPath sets both parent session ID and parent's project path The project path is used to grant subagent access via --add-dir

func (*Instance) SetStatusThreadSafe added in v0.10.6

func (inst *Instance) SetStatusThreadSafe(s Status)

SetStatusThreadSafe sets the session status with write-lock protection.

func (*Instance) SetTmuxSessionForTest added in v1.8.3

func (i *Instance) SetTmuxSessionForTest(s *tmux.Session)

SetTmuxSessionForTest assigns the unexported tmuxSession field. Tests in other packages (notably internal/ui) need this to construct an Instance with a *tmux.Session attached without going through storage hydration or a real tmux server.

Do not call from non-test code; production paths populate tmuxSession via Start (instance.go) or storage.LoadWithGroups (storage.go).

func (*Instance) SetToolThreadSafe added in v0.10.6

func (inst *Instance) SetToolThreadSafe(t string)

SetToolThreadSafe sets the tool name with write-lock protection.

func (*Instance) Start

func (i *Instance) Start() error

Start starts the session in tmux.

Issue #1040: gated by acquireInstanceSpawnLock plus a "spawned-while- we-waited" stamp so concurrent `agent-deck session start <id>` invocations after a Claude exit don't each fall through the "tmux session does not exist" gate and spawn parallel sessions. The lock and gate are inlined here (rather than wrapping the whole body in a SpawnAttempt helper) to preserve the structural-grep contract that checks Start()'s body for the #745 IsForkAwaitingStart guard.

func (*Instance) StartWithMessage added in v0.6.2

func (i *Instance) StartWithMessage(message string) error

StartWithMessage starts the session and sends an initial message when ready The message is sent synchronously after detecting the agent's prompt This approach is more reliable than embedding send logic in the tmux command Works for Claude, Gemini, OpenCode, and other agents

Issue #1040: same per-instance spawn lock as Start() — a concurrent `launch -m "..."` racing with a poller-triggered Start() must not produce two parallel tmux sessions.

func (*Instance) StopServiceUnit added in v1.7.21

func (i *Instance) StopServiceUnit() error

StopServiceUnit best-effort stops + resets-failed the transient systemd-user service unit associated with this instance's tmux server (if LaunchAs=service was used). Intended for the remove/delete code path ONLY — NOT for restart, which needs the unit to persist so it can re-spawn tmux.

No-ops on non-systemd hosts. Returns nil when the unit doesn't exist or was never started (best-effort semantics per v1.7.21 spec).

func (*Instance) SyncSessionIDsFromTmux added in v0.26.0

func (i *Instance) SyncSessionIDsFromTmux()

SyncSessionIDsFromTmux reads tool session IDs from the tmux environment into the Instance struct. This is the reverse of SyncSessionIDsToTmux. Used in the stop path to capture IDs that may not have been saved during start (e.g., if PostStartSync timed out but the tool started late). Only updates fields where the tmux env has a non-empty value; does not blank existing IDs if the tmux env is missing the variable.

func (*Instance) SyncSessionIDsToTmux added in v0.8.95

func (i *Instance) SyncSessionIDsToTmux()

SyncSessionIDsToTmux syncs session IDs from Instance to tmux environment. PERFORMANCE: This is called on-demand (e.g., first attach) rather than at load time to reduce subprocess overhead during TUI startup.

Session IDs are needed in tmux environment for restart/resume operations that spawn new processes. Without this sync, R key wouldn't resume the correct session.

func (*Instance) SyncTmuxDisplayName added in v0.8.80

func (i *Instance) SyncTmuxDisplayName()

SyncTmuxDisplayName updates tmux-rendered UI that reflects the current title.

func (*Instance) UnregisterMCPChild added in v1.9.9

func (i *Instance) UnregisterMCPChild(pid int)

UnregisterMCPChild removes a previously registered MCP child PID, e.g. when the child has been observed exiting cleanly.

func (*Instance) UpdateClaudeSession added in v0.4.1

func (i *Instance) UpdateClaudeSession(excludeIDs map[string]bool)

UpdateClaudeSession updates the Claude session ID from tmux environment. The capture-resume pattern (used in Start/Fork/Restart) sets CLAUDE_SESSION_ID in the tmux environment, making this the single authoritative source.

No file scanning fallback - we rely on the consistent capture-resume pattern.

func (*Instance) UpdateCodexSession added in v0.8.76

func (i *Instance) UpdateCodexSession(excludeIDs map[string]bool)

UpdateCodexSession updates the Codex session ID. Primary source: tmux environment. Fallback: project-aware filesystem scan.

func (*Instance) UpdateGeminiSession added in v0.8.1

func (i *Instance) UpdateGeminiSession(excludeIDs map[string]bool)

UpdateGeminiSession updates the Gemini session ID, YOLO mode, analytics, and latest prompt. Delegates to focused helpers for each concern.

func (*Instance) UpdateHookStatus added in v0.16.0

func (i *Instance) UpdateHookStatus(status *HookStatus)

UpdateHookStatus updates the instance's hook-based status fields. Called by StatusFileWatcher when a hook status file changes.

func (*Instance) UpdateOpenCodeSession added in v1.5.1

func (i *Instance) UpdateOpenCodeSession()

UpdateOpenCodeSession refreshes the OpenCode session ID from OpenCode CLI state without stealing a different tab's session from the same project.

func (*Instance) UpdateStatus

func (i *Instance) UpdateStatus() error

UpdateStatus updates the session status by checking tmux. Thread-safe: acquires write lock to protect Status, Tool, and internal cache fields.

func (*Instance) WaitForClaudeSession added in v0.4.1

func (i *Instance) WaitForClaudeSession(maxWait time.Duration) string

WaitForClaudeSession waits for the tmux environment variable to be set. The capture-resume pattern sets CLAUDE_SESSION_ID in tmux env, so we poll for that. Returns the detected session ID or empty string after timeout.

func (*Instance) WaitForClaudeSessionWithExclude added in v0.5.0

func (i *Instance) WaitForClaudeSessionWithExclude(maxWait time.Duration, excludeIDs map[string]bool) string

WaitForClaudeSessionWithExclude waits for the tmux environment variable to be set. The excludeIDs parameter is kept for API compatibility but not used since tmux env is authoritative and won't return duplicate IDs.

type InstanceData

type InstanceData struct {
	ID                 string    `json:"id"`
	Title              string    `json:"title"`
	ProjectPath        string    `json:"project_path"`
	GroupPath          string    `json:"group_path"`
	Order              int       `json:"order"`
	ParentSessionID    string    `json:"parent_session_id,omitempty"`    // Links to parent session (sub-session support)
	IsConductor        bool      `json:"is_conductor,omitempty"`         // True if this session is a conductor orchestrator
	NoTransitionNotify bool      `json:"no_transition_notify,omitempty"` // Suppress transition event dispatch
	TitleLocked        bool      `json:"title_locked,omitempty"`         // #697: block Claude session-name sync into Title
	Command            string    `json:"command"`
	Wrapper            string    `json:"wrapper,omitempty"`
	Tool               string    `json:"tool"`
	Status             Status    `json:"status"`
	CreatedAt          time.Time `json:"created_at"`
	LastAccessedAt     time.Time `json:"last_accessed_at,omitempty"`
	TmuxSession        string    `json:"tmux_session"`
	// TmuxSocketName is the tmux -L selector captured at Instance creation
	// (issue #687, v1.7.50). Empty for pre-v1.7.50 rows — those keep hitting
	// the default server after upgrade.
	TmuxSocketName string `json:"tmux_socket_name,omitempty"`

	// Worktree support
	WorktreePath     string `json:"worktree_path,omitempty"`
	WorktreeRepoRoot string `json:"worktree_repo_root,omitempty"`
	WorktreeBranch   string `json:"worktree_branch,omitempty"`

	// Account is the per-session named account (issue #924). See
	// Instance.Account for full semantics.
	Account string `json:"account,omitempty"`

	// Claude session (persisted for resume after app restart)
	ClaudeSessionID  string    `json:"claude_session_id,omitempty"`
	ClaudeDetectedAt time.Time `json:"claude_detected_at,omitempty"`

	// Gemini session (persisted for resume after app restart)
	GeminiSessionID  string    `json:"gemini_session_id,omitempty"`
	GeminiDetectedAt time.Time `json:"gemini_detected_at,omitempty"`
	GeminiYoloMode   *bool     `json:"gemini_yolo_mode,omitempty"`
	GeminiModel      string    `json:"gemini_model,omitempty"`

	// OpenCode session (persisted for resume after app restart)
	OpenCodeSessionID  string    `json:"opencode_session_id,omitempty"`
	OpenCodeDetectedAt time.Time `json:"opencode_detected_at,omitempty"`

	// Codex session (persisted for resume after app restart)
	CodexSessionID  string    `json:"codex_session_id,omitempty"`
	CodexDetectedAt time.Time `json:"codex_detected_at,omitempty"`

	// Latest user input for context
	LatestPrompt string `json:"latest_prompt,omitempty"`
	Notes        string `json:"notes,omitempty"`

	// Tool-specific launch options (generic for all tools: claude, codex, etc.)
	ToolOptionsJSON json.RawMessage `json:"tool_options,omitempty"`

	// MCP tracking (persisted for sync status display)
	LoadedMCPNames []string `json:"loaded_mcp_names,omitempty"`

	// Plugin channels (persisted for --channels CLI flag on Claude restart)
	Channels []string `json:"channels,omitempty"`

	// Plugins is the catalog-key list of Claude Code plugins enabled for
	// this session (RFC docs/rfc/PLUGIN_ATTACH.md). Resolved through
	// [plugins.<name>] in ~/.agent-deck/config.toml at spawn time and
	// emitted as enabledPlugins[<id>] = true in the per-session scratch
	// settings.json by EnsureWorkerScratchConfigDir.
	Plugins []string `json:"plugins,omitempty"`

	// PluginChannelLinkDisabled mirrors Instance.PluginChannelLinkDisabled
	// (RFC §4.7) for state.db round-trip.
	PluginChannelLinkDisabled bool `json:"plugin_channel_link_disabled,omitempty"`

	// AutoLinkedChannels mirrors Instance.AutoLinkedChannels (RFC §4.7,
	// fixes G4/C2). Persisted so reconciliation can clean up channels
	// auto-added in a previous session even after the user toggles
	// PluginChannelLinkDisabled or removes the plugin from the catalog.
	AutoLinkedChannels []string `json:"auto_linked_channels,omitempty"`

	// User-supplied claude CLI tokens, appended to every start/resume/fork
	// command. Persisted so restarts preserve custom flags like --agent/--model.
	ExtraArgs []string `json:"extra_args,omitempty"`

	// Color is an optional per-session TUI row tint (issue #391). Empty = no tint.
	Color string `json:"color,omitempty"`

	// Sandbox support
	Sandbox          *SandboxConfig `json:"sandbox,omitempty"`
	SandboxContainer string         `json:"sandbox_container,omitempty"`

	// SSH remote support
	SSHHost       string `json:"ssh_host,omitempty"`
	SSHRemotePath string `json:"ssh_remote_path,omitempty"`

	// Multi-repo support
	MultiRepoEnabled   bool                            `json:"multi_repo_enabled,omitempty"`
	AdditionalPaths    []string                        `json:"additional_paths,omitempty"`
	MultiRepoTempDir   string                          `json:"multi_repo_temp_dir,omitempty"`
	MultiRepoWorktrees []statedb.MultiRepoWorktreeData `json:"multi_repo_worktrees,omitempty"`

	// IdleTimeoutSecs mirrors Instance.IdleTimeoutSecs (#1143). 0 = disabled.
	IdleTimeoutSecs int64 `json:"idle_timeout_secs,omitempty"`
}

InstanceData represents the serializable session data

type InstanceSettings added in v0.8.74

type InstanceSettings struct {
	// AllowMultiple allows running multiple agent-deck TUI instances for the same profile
	// When true (default), multiple instances can run, but only the first (primary) manages the notification bar
	// When false, only one instance can run per profile
	AllowMultiple *bool `toml:"allow_multiple"`

	// FollowCwdOnAttach updates the session's ProjectPath from tmux pane_current_path
	// after returning from attach, and persists the new path.
	// Default: false
	FollowCwdOnAttach *bool `toml:"follow_cwd_on_attach"`
}

InstanceSettings configures multiple agent-deck instance behavior

func GetInstanceSettings added in v0.8.74

func GetInstanceSettings() InstanceSettings

GetInstanceSettings returns instance behavior settings

func (*InstanceSettings) GetAllowMultiple added in v0.8.97

func (i *InstanceSettings) GetAllowMultiple() bool

GetAllowMultiple returns whether multiple instances are allowed, defaulting to true

func (*InstanceSettings) GetFollowCwdOnAttach added in v0.21.0

func (i *InstanceSettings) GetFollowCwdOnAttach() bool

GetFollowCwdOnAttach returns whether attach-return CWD follow is enabled.

type Item

type Item struct {
	Type                ItemType
	Group               *Group
	Session             *Instance
	RemoteSession       *RemoteSessionInfo // Set for ItemTypeRemoteSession/ItemTypeRemoteGroup
	RemoteName          string             // Remote name for remote items
	Level               int                // Indentation level (0 for root groups, 1 for sessions)
	Path                string             // Group path for this item
	IsLastInGroup       bool               // True if this is the last session in its group (for tree rendering)
	RootGroupNum        int                // Pre-computed root group number for hotkey display (1-9, 0 if not a root group)
	IsSubSession        bool               // True if this session has a parent session
	IsLastSubSession    bool               // True if this is the last sub-session of its parent (for tree rendering)
	ParentIsLastInGroup bool               // True if parent session is last top-level item (for tree line rendering)
	IsWindow            bool               // True for ItemTypeWindow items
	IsLastWindow        bool               // True if last window of parent session
	WindowIndex         int                // Tmux window index (for ItemTypeWindow)
	WindowName          string             // Tmux window name (for ItemTypeWindow)
	WindowSessionID     string             // Parent session ID (for ItemTypeWindow)
	WindowTool          string             // Detected tool in this window (claude, gemini, etc.)
	CreatingID          string             // Non-empty for placeholder items (worktree creation in progress)
	CreatingTitle       string             // Display title for creating placeholder
	CreatingTool        string             // Tool for creating placeholder
}

Item represents a single item in the flattened group tree view

type ItemType

type ItemType int

ItemType represents the type of item in the flattened list

const (
	ItemTypeGroup ItemType = iota
	ItemTypeSession
	ItemTypeRemoteGroup
	ItemTypeRemoteSession
	ItemTypeWindow
)

type LocalMCP added in v0.6.1

type LocalMCP struct {
	Name       string // MCP name
	SourcePath string // Directory containing the .mcp.json file
}

LocalMCP represents an MCP defined in a local .mcp.json file

type LogSettings added in v0.5.3

type LogSettings struct {
	// MaxSizeMB is the maximum size in MB before a log file is truncated
	// When a log exceeds this size, it keeps only the last MaxLines lines
	// Default: 10 (10MB)
	MaxSizeMB int `toml:"max_size_mb"`

	// MaxLines is the number of lines to keep when truncating
	// Default: 10000
	MaxLines int `toml:"max_lines"`

	// RemoveOrphans removes log files for sessions that no longer exist
	// Default: true
	RemoveOrphans bool `toml:"remove_orphans"`

	// DebugLevel sets the minimum log level: "debug", "info", "warn", "error"
	// Default: "info"
	DebugLevel string `toml:"debug_level"`

	// DebugFormat sets the log format: "json" (default) or "text"
	DebugFormat string `toml:"debug_format"`

	// DebugMaxMB is the max size in MB for debug.log before rotation
	// Default: 10
	DebugMaxMB int `toml:"debug_max_mb"`

	// DebugBackups is the number of rotated debug.log files to keep
	// Default: 5
	DebugBackups int `toml:"debug_backups"`

	// DebugRetentionDays is the number of days to keep rotated debug logs
	// Default: 10
	DebugRetentionDays int `toml:"debug_retention_days"`

	// DebugCompress enables gzip compression for rotated debug logs
	// Default: true
	DebugCompress bool `toml:"debug_compress"`

	// RingBufferMB is the in-memory ring buffer size in MB for crash dumps
	// Default: 10
	RingBufferMB int `toml:"ring_buffer_mb"`

	// PprofEnabled starts a pprof server on localhost:6060 when debug mode is active
	// Default: false
	PprofEnabled bool `toml:"pprof_enabled"`

	// AggregateIntervalS is the event aggregation flush interval in seconds
	// Default: 30
	AggregateIntervalS int `toml:"aggregate_interval_secs"`
}

LogSettings defines log file management configuration

func GetLogSettings added in v0.5.3

func GetLogSettings() LogSettings

GetLogSettings returns log management settings with defaults applied

type MCPDef added in v0.5.3

type MCPDef struct {
	// Command is the executable to run (e.g., "npx", "docker", "node")
	// Required for stdio MCPs, optional for HTTP/SSE MCPs
	Command string `toml:"command"`

	// Args are command-line arguments
	Args []string `toml:"args"`

	// Env is optional environment variables
	Env map[string]string `toml:"env"`

	// Description is optional help text shown in the MCP Manager
	Description string `toml:"description"`

	// URL is the endpoint for HTTP/SSE MCPs (e.g., "http://localhost:8000/mcp")
	// If set, this MCP uses HTTP or SSE transport instead of stdio
	URL string `toml:"url"`

	// Transport specifies the MCP transport type: "stdio" (default), "http", or "sse"
	// Only needed when URL is set; defaults to "http" if URL is present
	Transport string `toml:"transport"`

	// Headers is optional HTTP headers for HTTP/SSE MCPs (e.g., for authentication)
	// Example: { Authorization = "Bearer token123" }
	Headers map[string]string `toml:"headers"`

	// Server defines how to auto-start an HTTP MCP server process
	// When set, agent-deck will start the server before connecting via HTTP
	// This is optional - you can also connect to externally managed servers
	Server *HTTPServerConfig `toml:"server"`
}

MCPDef defines an MCP server configuration for the MCP Manager

func GetMCPDef added in v0.5.3

func GetMCPDef(name string) *MCPDef

GetMCPDef returns a specific MCP definition by name Returns nil if not found

func (*MCPDef) GetTransport added in v0.8.96

func (m *MCPDef) GetTransport() string

GetTransport returns the transport type, defaulting to "http" if URL is set

func (*MCPDef) HasAutoStartServer added in v0.8.96

func (m *MCPDef) HasAutoStartServer() bool

HasAutoStartServer returns true if this HTTP MCP has server auto-start configured

func (*MCPDef) IsHTTP added in v0.8.96

func (m *MCPDef) IsHTTP() bool

IsHTTP returns true if this MCP uses HTTP or SSE transport

type MCPInfo added in v0.5.3

type MCPInfo struct {
	Global    []string   // From CLAUDE_CONFIG_DIR/.claude.json mcpServers
	Project   []string   // From CLAUDE_CONFIG_DIR/.claude.json projects[path].mcpServers
	LocalMCPs []LocalMCP // From .mcp.json files (walks up parent directories)
}

MCPInfo contains MCP server information for a session

func GetGeminiMCPInfo added in v0.8.1

func GetGeminiMCPInfo(projectPath string) *MCPInfo

GetGeminiMCPInfo reads MCP configuration from settings.json Returns MCPInfo with Global MCPs only (Gemini has no project-level MCPs) VERIFIED: settings.json structure is simple {"mcpServers": {...}}

func GetMCPInfo added in v0.5.3

func GetMCPInfo(projectPath string) *MCPInfo

GetMCPInfo retrieves MCP server information for a project path (cached) It reads from three sources: 1. Global MCPs: CLAUDE_CONFIG_DIR/.claude.json → mcpServers 2. Project MCPs: CLAUDE_CONFIG_DIR/.claude.json → projects[projectPath].mcpServers 3. Local MCPs: {projectPath}/.mcp.json → mcpServers

func (*MCPInfo) AllNames added in v0.5.3

func (m *MCPInfo) AllNames() []string

AllNames returns a deduplicated, sorted list of all MCP names across all sources Used for capturing loaded MCPs at session start for sync tracking

func (*MCPInfo) HasAny added in v0.5.3

func (m *MCPInfo) HasAny() bool

HasAny returns true if any MCPs are configured

func (*MCPInfo) Local added in v0.5.3

func (m *MCPInfo) Local() []string

Local returns MCP names for backward compatibility Use LocalMCPs directly if you need source path information

func (*MCPInfo) Total added in v0.5.3

func (m *MCPInfo) Total() int

Total returns total number of MCPs across all sources

type MCPMode added in v0.5.3

type MCPMode int

MCPMode indicates how MCP enabling/disabling is configured

const (
	MCPModeDefault   MCPMode = iota // No explicit config, all enabled
	MCPModeWhitelist                // enabledMcpjsonServers is set
	MCPModeBlacklist                // disabledMcpjsonServers is set
)

func GetMCPMode added in v0.5.3

func GetMCPMode(projectPath string) MCPMode

GetMCPMode determines the MCP configuration mode for a project

type MCPPoolSettings added in v0.6.2

type MCPPoolSettings struct {
	// Enabled enables HTTP pool mode (default: false)
	Enabled bool `toml:"enabled"`

	// AutoStart starts pool when agent-deck launches (default: true)
	AutoStart bool `toml:"auto_start"`

	// PortStart is the first port in the pool range (default: 8001)
	PortStart int `toml:"port_start"`

	// PortEnd is the last port in the pool range (default: 8050)
	PortEnd int `toml:"port_end"`

	// StartOnDemand starts MCPs lazily on first attach (default: false)
	StartOnDemand bool `toml:"start_on_demand"`

	// ShutdownOnExit stops HTTP servers when agent-deck quits (default: true)
	ShutdownOnExit bool `toml:"shutdown_on_exit"`

	// PoolMCPs is the list of MCPs to run in pool mode
	// Empty = auto-detect common MCPs (memory, exa, firecrawl, etc.)
	PoolMCPs []string `toml:"pool_mcps"`

	// FallbackStdio uses stdio for MCPs without socket support (default: true)
	FallbackStdio bool `toml:"fallback_to_stdio"`

	// ShowStatus shows pool status in TUI (default: true)
	ShowStatus bool `toml:"show_pool_status"`

	// PoolAll pools all MCPs by default (default: false)
	PoolAll bool `toml:"pool_all"`

	// ExcludeMCPs excludes specific MCPs from pool when pool_all = true
	ExcludeMCPs []string `toml:"exclude_mcps"`

	// SocketWaitTimeout is seconds to wait for socket to become ready (default: 5)
	SocketWaitTimeout int `toml:"socket_wait_timeout"`
}

MCPPoolSettings defines HTTP MCP pool configuration

type MCPServer added in v0.5.3

type MCPServer struct {
	Name    string
	Source  string // "local", "global", "project"
	Enabled bool
}

MCPServer represents an MCP with its enabled state

func GetLocalMCPState added in v0.5.3

func GetLocalMCPState(projectPath string) ([]MCPServer, error)

GetLocalMCPState returns Local MCPs with their enabled state

type MCPServerConfig added in v0.5.3

type MCPServerConfig struct {
	Type    string            `json:"type,omitempty"`
	Command string            `json:"command,omitempty"`
	Args    []string          `json:"args,omitempty"`
	Env     map[string]string `json:"env,omitempty"`
	URL     string            `json:"url,omitempty"`     // For HTTP transport
	Headers map[string]string `json:"headers,omitempty"` // For HTTP transport (e.g., Authorization)
}

MCPServerConfig represents an MCP server configuration (Claude's format)

type MaintenanceResult added in v0.8.79

type MaintenanceResult struct {
	PrunedLogs       int
	PrunedBackups    int
	ArchivedSessions int
	OrphanContainers int
	Duration         time.Duration
}

MaintenanceResult holds the outcome of a maintenance run.

func RunMaintenance added in v0.8.79

func RunMaintenance(ctx context.Context) MaintenanceResult

RunMaintenance executes all maintenance tasks and returns the result.

type MaintenanceSettings added in v0.8.79

type MaintenanceSettings struct {
	// Enabled enables the maintenance worker (default: false)
	// Prunes Gemini logs, cleans old backups, archives bloated sessions
	Enabled bool `toml:"enabled"`
}

MaintenanceSettings controls the automatic maintenance worker

func GetMaintenanceSettings added in v0.8.79

func GetMaintenanceSettings() MaintenanceSettings

GetMaintenanceSettings returns maintenance settings from config

type MatchRange added in v0.5.3

type MatchRange struct {
	Start int
	End   int
}

MatchRange represents a match position in content

type MaterializedProjectSkill added in v1.7.32

type MaterializedProjectSkill struct {
	EntryName  string `json:"entry_name"`
	TargetPath string `json:"target_path"`
}

MaterializedProjectSkill is one on-disk skill entry under a managed project root.

func ListMaterializedProjectSkills added in v0.19.1

func ListMaterializedProjectSkills(projectPath string) ([]MaterializedProjectSkill, error)

ListMaterializedProjectSkills returns all entries currently present in known managed project roots.

type MigrationResult added in v0.3.0

type MigrationResult struct {
	Migrated     bool   // True if migration was performed
	ProfilePath  string // Path to the migrated profile
	SessionCount int    // Number of sessions migrated
	Message      string // Human-readable message
}

MigrationResult contains information about the migration outcome

func MigrateToProfiles added in v0.3.0

func MigrateToProfiles() (*MigrationResult, error)

MigrateToProfiles migrates from the old single-file layout to the profiles layout. This is safe to call multiple times - it will only migrate once.

Old layout:

~/.agent-deck/sessions.json
~/.agent-deck/sessions.json.bak
~/.agent-deck/sessions.json.bak.1
~/.agent-deck/sessions.json.bak.2

New layout:

~/.agent-deck/config.json
~/.agent-deck/profiles/default/sessions.json
~/.agent-deck/profiles/default/sessions.json.bak
~/.agent-deck/profiles/default/sessions.json.bak.1
~/.agent-deck/profiles/default/sessions.json.bak.2
~/.agent-deck/logs/ (unchanged)

type ModelInfo added in v1.9.14

type ModelInfo struct {
	ModelID string
	Model   string
	Version string
}

ModelInfo is the normalized status view of a per-session model override. ModelID is the exact string passed to the underlying tool; Model and Version are best-effort display fields derived from common provider ID formats.

func ParseModelID added in v1.9.14

func ParseModelID(modelID string) ModelInfo

ParseModelID derives display-friendly model and version fields from common provider model IDs while preserving the exact ID for command execution.

func (ModelInfo) Display added in v1.9.14

func (m ModelInfo) Display() string

Display returns a compact label for human-readable status output.

type ModelPricing added in v0.8.27

type ModelPricing struct {
	Input      float64
	Output     float64
	CacheRead  float64
	CacheWrite float64
}

ModelPricing holds pricing per million tokens for a model

type MultiRepoWorktree added in v0.26.2

type MultiRepoWorktree struct {
	OriginalPath string `json:"original_path"`
	WorktreePath string `json:"worktree_path"`
	RepoRoot     string `json:"repo_root"`
	Branch       string `json:"branch"`
}

MultiRepoWorktree tracks a worktree created for one repo in a multi-repo session.

type MultiRepoWorktreeResult added in v1.9.25

type MultiRepoWorktreeResult struct {
	MappedPaths []string
	Worktrees   []MultiRepoWorktree
	Warnings    []string
}

func CreateMultiRepoWorktrees added in v1.9.25

func CreateMultiRepoWorktrees(allPaths []string, parentDir string, branch string, setupTimeout time.Duration) MultiRepoWorktreeResult

type MutationError added in v1.7.70

type MutationError struct {
	Field string
	Msg   string
}

func (*MutationError) Error added in v1.7.70

func (e *MutationError) Error() string

type NotificationEntry added in v0.8.43

type NotificationEntry struct {
	SessionID    string
	TmuxName     string
	Title        string
	AssignedKey  string
	WaitingSince time.Time
	Status       Status // For icon rendering when show_all enabled
}

NotificationEntry represents a waiting session in the notification bar

type NotificationManager added in v0.8.43

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

NotificationManager tracks waiting sessions for the notification bar

func NewNotificationManager added in v0.8.43

func NewNotificationManager(maxShown int, showAll, minimal bool) *NotificationManager

NewNotificationManager creates a new notification manager

func (*NotificationManager) Add added in v0.8.43

func (nm *NotificationManager) Add(inst *Instance) error

Add registers a session as waiting (newest goes to position [0])

func (*NotificationManager) Clear added in v0.8.43

func (nm *NotificationManager) Clear()

Clear removes all notifications

func (*NotificationManager) Count added in v0.8.43

func (nm *NotificationManager) Count() int

Count returns number of notifications

func (*NotificationManager) FormatBar added in v0.8.43

func (nm *NotificationManager) FormatBar() string

FormatBar returns the formatted status bar text

func (*NotificationManager) GetEntries added in v0.8.43

func (nm *NotificationManager) GetEntries() []*NotificationEntry

GetEntries returns a copy of current entries (newest first)

func (*NotificationManager) GetSessionByKey added in v0.8.43

func (nm *NotificationManager) GetSessionByKey(key string) *NotificationEntry

GetSessionByKey returns the entry for a given key (1-6)

func (*NotificationManager) Has added in v0.8.43

func (nm *NotificationManager) Has(sessionID string) bool

Has checks if a session is in notifications

func (*NotificationManager) IsMinimal added in v0.19.15

func (nm *NotificationManager) IsMinimal() bool

IsMinimal reports whether this manager is in minimal (icon+count) mode. home.go uses this to skip key binding updates when minimal=true.

func (*NotificationManager) Remove added in v0.8.43

func (nm *NotificationManager) Remove(sessionID string)

Remove removes a session from notifications

func (*NotificationManager) SyncFromInstances added in v0.8.43

func (nm *NotificationManager) SyncFromInstances(instances []*Instance, currentSessionID string) (added, removed []string)

SyncFromInstances updates notifications based on current instance states Call this periodically to sync with actual session statuses

type NotificationsConfig added in v0.8.43

type NotificationsConfig struct {
	// Enabled shows notification bar in tmux status (default: true)
	Enabled bool `toml:"enabled"`

	// MaxShown is the maximum number of sessions shown in the bar (default: 6)
	MaxShown int `toml:"max_shown"`

	// ShowAll displays all sessions (with status icons) instead of only waiting sessions (default: false)
	ShowAll bool `toml:"show_all"`

	// Minimal shows a compact icon+count summary instead of session names: ● 2 │ ◐ 3 │ ○ 1
	// When true, key bindings (Ctrl+b 1-6) are disabled. ShowAll is ignored. (default: false)
	Minimal bool `toml:"minimal"`

	// TransitionEvents controls whether the transition daemon sends tmux messages
	// to parent sessions when a child transitions (e.g., running → waiting).
	// Default: true (nil = true). Set to false to suppress dispatch globally.
	// Per-session override: Instance.NoTransitionNotify
	TransitionEvents *bool `toml:"transition_events"`
}

NotificationsConfig configures the waiting session notification bar

func GetNotificationsSettings added in v0.8.43

func GetNotificationsSettings() NotificationsConfig

GetNotificationsSettings returns notification bar settings with defaults applied

func (NotificationsConfig) GetTransitionEventsEnabled added in v1.7.35

func (n NotificationsConfig) GetTransitionEventsEnabled() bool

GetTransitionEventsEnabled returns whether transition event dispatch is enabled. Defaults to true when unset (nil).

type OpenClawSettings added in v0.21.0

type OpenClawSettings struct {
	// GatewayURL is the WebSocket URL of the OpenClaw gateway (default: "ws://127.0.0.1:31337")
	GatewayURL string `toml:"gateway_url"`

	// Password is the gateway authentication password.
	// Supports env var references (e.g. "$OPENCLAW_PASSWORD" or "${OPENCLAW_PASSWORD}").
	// Falls back to OPENCLAW_PASSWORD env var if not set.
	Password string `toml:"password"`

	// AutoSync syncs OpenClaw agents as agent-deck sessions on TUI startup
	AutoSync bool `toml:"auto_sync"`

	// GroupName is the agent-deck group name for OpenClaw sessions (default: "openclaw")
	GroupName string `toml:"group_name"`
}

OpenClawSettings configures the OpenClaw gateway connection.

type OpenCodeOptions added in v0.12.1

type OpenCodeOptions struct {
	// SessionMode: "new" (default), "continue" (-c), or "resume" (-s)
	SessionMode string `json:"session_mode,omitempty"`
	// ResumeSessionID is the session ID for -s flag (only when SessionMode="resume")
	ResumeSessionID string `json:"resume_session_id,omitempty"`
	// Model overrides the model (e.g., "anthropic/claude-sonnet-4-5-20250929")
	Model string `json:"model,omitempty"`
	// Agent overrides the agent to use
	Agent string `json:"agent,omitempty"`
}

OpenCodeOptions holds launch options for OpenCode CLI sessions

func NewOpenCodeOptions added in v0.12.1

func NewOpenCodeOptions(config *UserConfig) *OpenCodeOptions

NewOpenCodeOptions creates OpenCodeOptions with defaults from config

func UnmarshalOpenCodeOptions added in v0.12.1

func UnmarshalOpenCodeOptions(data json.RawMessage) (*OpenCodeOptions, error)

UnmarshalOpenCodeOptions deserializes OpenCodeOptions from JSON wrapper

func (*OpenCodeOptions) ToArgs added in v0.12.1

func (o *OpenCodeOptions) ToArgs() []string

ToArgs returns command-line arguments based on options

func (*OpenCodeOptions) ToArgsForFork added in v0.12.1

func (o *OpenCodeOptions) ToArgsForFork() []string

ToArgsForFork returns arguments suitable for fork resume command. Fork uses -s internally, so session mode flags are excluded.

func (*OpenCodeOptions) ToolName added in v0.12.1

func (o *OpenCodeOptions) ToolName() string

ToolName returns "opencode"

type OpenCodeSettings added in v0.12.1

type OpenCodeSettings struct {
	// DefaultModel is the model to use for new OpenCode sessions
	// Format: "provider/model" (e.g., "anthropic/claude-sonnet-4-5-20250929")
	// If empty, OpenCode uses its own default
	DefaultModel string `toml:"default_model"`

	// DefaultAgent is the agent to use for new OpenCode sessions
	// If empty, OpenCode uses its own default
	DefaultAgent string `toml:"default_agent"`

	// EnvFile is a .env file specific to OpenCode sessions
	// Sourced AFTER global [shell].env_files
	// Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
	EnvFile string `toml:"env_file"`

	// Command overrides the default binary/invocation for OpenCode sessions.
	// Supports flags (e.g., "opencode --custom-flag"). Default: "opencode"
	Command string `toml:"command"`
}

OpenCodeSettings defines OpenCode CLI configuration

type PluginDef added in v1.9.2

type PluginDef struct {
	// Name is the short plugin name as exposed by the upstream marketplace's
	// plugin.json (e.g. "telegram", "octopus"). Required.
	Name string `toml:"name"`

	// Source is the marketplace identifier the plugin lives in. Either a
	// curated marketplace name (e.g. "claude-plugins-official") or a github
	// "owner/repo" pair (e.g. "nyldn/claude-octopus"). Required.
	Source string `toml:"source"`

	// EmitsChannel hints that this plugin participates in the inbound
	// `notifications/claude/channel` protocol — when true, attaching the
	// plugin via --plugin auto-populates Instance.Channels with
	// "plugin:<Name>@<Source>" so the harness registers the inbound handler.
	// Catalog hint only; agent-deck does not introspect the plugin source.
	EmitsChannel bool `toml:"emits_channel"`

	// AutoInstall enables shell-out to `claude plugin install <Name>@<Source>`
	// at session spawn when the plugin code is not yet present under the
	// source profile's plugins/ directory. Best-effort: install failure is
	// logged but does not block session start.
	AutoInstall bool `toml:"auto_install"`

	// Description is optional help text shown in the Edit Session dialog
	// pill list.
	Description string `toml:"description"`
}

PluginDef defines a Claude Code plugin entry exposed via `agent-deck add --plugin <name>` and `agent-deck session set <id> plugins <csv>`.

Plugin id at runtime is constructed as "<Name>@<Source>" and written to the per-session scratch settings.json under enabledPlugins (see internal/session/worker_scratch.go). v1 is catalog-only: only short names listed in [plugins.<name>] tables in ~/.agent-deck/config.toml are valid values for the --plugin flag.

RFC: docs/rfc/PLUGIN_ATTACH.md.

func GetPluginDef added in v1.9.2

func GetPluginDef(name string) *PluginDef

GetPluginDef returns a specific plugin definition by catalog key. Returns nil if not found OR if the entry matches the v1 refusal policy.

func (*PluginDef) ChannelID added in v1.9.2

func (p *PluginDef) ChannelID() string

ChannelID returns the channel id produced by the auto-link path when EmitsChannel is true. Format: "plugin:<Name>@<Source>".

func (*PluginDef) ID added in v1.9.2

func (p *PluginDef) ID() string

ID returns the fully-qualified plugin identifier "<Name>@<Source>" used both as the enabledPlugins key in settings.json and as the channel id "plugin:<ID>" when EmitsChannel is true.

type PreviewSettings added in v0.8.27

type PreviewSettings struct {
	// ShowOutput shows terminal output in preview pane (including launch animation)
	// Default: true (pointer to distinguish "not set" from "explicitly false")
	ShowOutput *bool `toml:"show_output"`

	// ShowAnalytics shows session analytics panel for Claude sessions
	// Default: false (pointer to distinguish "not set" from "explicitly false")
	ShowAnalytics *bool `toml:"show_analytics"`

	// ShowNotes shows session notes section in preview pane
	// Default: false (pointer to distinguish "not set" from "explicitly true")
	ShowNotes *bool `toml:"show_notes"`

	// Analytics configures which sections to show in the analytics panel
	Analytics AnalyticsDisplaySettings `toml:"analytics"`

	// NotesOutputSplit controls vertical space allocation between notes and output
	// in the preview pane when output is visible.
	// Range: 0.1 - 0.9 (fraction reserved for notes). Default: 0.33
	NotesOutputSplit float64 `toml:"notes_output_split"`
}

PreviewSettings defines preview pane configuration

func GetPreviewSettings added in v0.8.27

func GetPreviewSettings() PreviewSettings

GetPreviewSettings returns preview settings with defaults applied

func (*PreviewSettings) GetAnalyticsSettings added in v0.8.53

func (p *PreviewSettings) GetAnalyticsSettings() AnalyticsDisplaySettings

GetAnalyticsSettings returns the analytics display settings with defaults applied

func (*PreviewSettings) GetNotesOutputSplit added in v0.21.0

func (p *PreviewSettings) GetNotesOutputSplit() float64

GetNotesOutputSplit returns notes/output split ratio, clamped to sane bounds.

func (*PreviewSettings) GetShowAnalytics added in v0.8.27

func (p *PreviewSettings) GetShowAnalytics() bool

GetShowAnalytics returns whether to show analytics, defaulting to false

func (*PreviewSettings) GetShowNotes added in v0.25.0

func (p *PreviewSettings) GetShowNotes() bool

GetShowNotes returns whether to show notes section, defaulting to false

func (*PreviewSettings) GetShowOutput added in v0.8.33

func (p *PreviewSettings) GetShowOutput() bool

GetShowOutput returns whether to show terminal output, defaulting to true

type PricingOverride added in v0.26.4

type PricingOverride struct {
	InputPerMtok      float64 `toml:"input_per_mtok"`
	OutputPerMtok     float64 `toml:"output_per_mtok"`
	CacheReadPerMtok  float64 `toml:"cache_read_per_mtok"`
	CacheWritePerMtok float64 `toml:"cache_write_per_mtok"`
}

type PricingSettings added in v0.26.4

type PricingSettings struct {
	Overrides map[string]PricingOverride `toml:"overrides"`
}

type ProfileClaudeSettings added in v0.19.1

type ProfileClaudeSettings struct {
	// ConfigDir overrides [claude].config_dir for this profile only.
	ConfigDir string `toml:"config_dir"`
}

ProfileClaudeSettings defines profile-specific Claude overrides.

type ProfileCodexSettings added in v1.9.18

type ProfileCodexSettings struct {
	// ConfigDir overrides [codex].config_dir for this profile only.
	ConfigDir string `toml:"config_dir"`
}

ProfileCodexSettings defines profile-specific Codex overrides.

type ProfileCosts added in v1.7.75

type ProfileCosts struct {
	CostLineTemplate     *string `toml:"cost_line_template"`
	CostLineHideWhenZero *bool   `toml:"cost_line_hide_when_zero"`
}

ProfileCosts holds per-profile overrides for cost-related settings. Pointer fields use the same fall-through semantics as CostsSettings.

type ProfileMigrateOptions added in v1.9.2

type ProfileMigrateOptions struct {
	// Force bypasses the "session is running" safety check. Without this, a
	// running session is refused with a hint to stop it first.
	Force bool
}

ProfileMigrateOptions controls a cross-profile migration (issue #928).

type ProfileMigrateResult added in v1.9.2

type ProfileMigrateResult struct {
	// MovedSessionIDs are the IDs that ended up in the destination DB. This
	// includes IDs that were already there (idempotent re-run).
	MovedSessionIDs []string
	// SkippedIdempotent lists session IDs that were already in the destination
	// (the migration only needed to clean up the source side, if any).
	SkippedIdempotent []string
	// MovedCostEvents is the count of cost_events rows inserted at dst.
	MovedCostEvents int
	// MovedWatcherEvents is the count of watcher_events rows inserted at dst.
	MovedWatcherEvents int
	// CreatedGroups lists group_path values that did not exist in dst and were
	// created by the migration.
	CreatedGroups []string
	// MetaUpdated indicates the conductor meta.json was rewritten with the new
	// profile (conductor migrations only).
	MetaUpdated bool
}

ProfileMigrateResult summarizes what a migration moved. All counts include rows that were already present at the destination (skipped idempotently).

func MigrateConductorToProfile added in v1.9.2

func MigrateConductorToProfile(name, sourceProfile, targetProfile string, opts ProfileMigrateOptions) (*ProfileMigrateResult, error)

MigrateConductorToProfile moves a conductor session AND every child session (where parent_session_id == conductor.ID) from src to dst, then atomically rewrites ~/.agent-deck/conductor/<name>/meta.json with the new profile.

Per CLAUDE.md and issue #928: workers MUST travel with their conductor — a split would orphan the children's parent_session_id reference.

func MigrateGroupToProfile added in v1.9.2

func MigrateGroupToProfile(groupPath, sourceProfile, targetProfile string, opts ProfileMigrateOptions) (*ProfileMigrateResult, error)

MigrateGroupToProfile moves every session whose group_path matches groupPath from src to dst. The group row itself (preserving expanded/order/default_path) is also created in dst. Empty groups are refused.

func MigrateSessionsToProfile added in v1.9.2

func MigrateSessionsToProfile(sourceProfile, targetProfile string, sessionIDs []string, opts ProfileMigrateOptions) (*ProfileMigrateResult, error)

MigrateSessionsToProfile moves the listed session rows from sourceProfile to targetProfile. All associated rows (cost_events, watcher_events linked via session_id or triage_session_id) are moved alongside. The session's group row is created in the target if missing.

The algorithm is target-write-then-source-delete, with a best-effort rollback on source-delete failure. Two SQLite databases cannot share a transaction, so we accept a tiny window where a crash between phases would leave the row in both DBs — re-running the command cleans this up.

Refuses running sessions unless opts.Force is set. Idempotent: a session already in dst is treated as already-migrated and only the source side is reconciled.

type ProfileSettings added in v0.19.1

type ProfileSettings struct {
	// Claude defines Claude Code overrides for a specific profile.
	Claude ProfileClaudeSettings `toml:"claude"`
	// Codex defines Codex CLI overrides for a specific profile.
	Codex ProfileCodexSettings `toml:"codex"`
	// Costs defines profile-specific cost-tracking overrides.
	// Nil pointer means "no [profiles.<name>.costs] block in TOML"; the
	// resolver falls through to global [costs] settings.
	Costs *ProfileCosts `toml:"costs"`
}

ProfileSettings defines per-profile configuration overrides.

type ProjectMCPSettings added in v0.5.3

type ProjectMCPSettings struct {
	EnableAllProjectMcpServers bool     `json:"enableAllProjectMcpServers,omitempty"`
	EnabledMcpjsonServers      []string `json:"enabledMcpjsonServers,omitempty"`
	DisabledMcpjsonServers     []string `json:"disabledMcpjsonServers,omitempty"`
}

ProjectMCPSettings represents .claude/settings.local.json

type ProjectSkillAttachment added in v0.19.1

type ProjectSkillAttachment struct {
	ID         string `toml:"id"`
	Name       string `toml:"name"`
	Source     string `toml:"source"`
	SourcePath string `toml:"source_path"`
	EntryName  string `toml:"entry_name"`
	TargetPath string `toml:"target_path"` // relative to project path
	Mode       string `toml:"mode,omitempty"`
	AttachedAt string `toml:"attached_at,omitempty"`
}

ProjectSkillAttachment is persisted in .agent-deck/skills.toml.

func AttachSkillToProject added in v0.19.1

func AttachSkillToProject(projectPath, tool, skillRef, sourceName string) (*ProjectSkillAttachment, error)

AttachSkillToProject resolves and attaches one skill into the runtime-specific project skills dir.

func DetachSkillFromProject added in v0.19.1

func DetachSkillFromProject(projectPath, skillRef, sourceName string) (*ProjectSkillAttachment, error)

DetachSkillFromProject detaches one managed skill and removes its manifest entry.

func GetAttachedProjectSkills added in v0.19.1

func GetAttachedProjectSkills(projectPath string) ([]ProjectSkillAttachment, error)

GetAttachedProjectSkills returns manifest-backed attached skills.

type ProjectSkillsManifest added in v0.19.1

type ProjectSkillsManifest struct {
	Skills []ProjectSkillAttachment `toml:"skills"`
}

ProjectSkillsManifest is the project-local attachment state.

func LoadProjectSkillsManifest added in v0.19.1

func LoadProjectSkillsManifest(projectPath string) (*ProjectSkillsManifest, error)

LoadProjectSkillsManifest reads project attachment state.

type RemoteConfig added in v0.20.0

type RemoteConfig struct {
	// Host is the SSH destination (e.g., "user@host" or "user@host:port")
	Host string `toml:"host"`

	// AgentDeckPath is the path to agent-deck binary on the remote (default: "agent-deck")
	AgentDeckPath string `toml:"agent_deck_path"`

	// Profile is the remote profile to use (default: "default")
	Profile string `toml:"profile"`
}

RemoteConfig defines a remote agent-deck instance accessible via SSH.

func (RemoteConfig) GetAgentDeckPath added in v0.20.0

func (rc RemoteConfig) GetAgentDeckPath() string

GetAgentDeckPath returns the agent-deck binary path, defaulting to "agent-deck".

func (RemoteConfig) GetProfile added in v0.20.0

func (rc RemoteConfig) GetProfile() string

GetProfile returns the remote profile, defaulting to "default".

type RemoteKeySender added in v1.9.24

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

RemoteKeySender dispatches insert-mode keystrokes to a session living on a remote agent-deck instance over the existing SSHRunner RPC (#1102 bug 2: previously, insert mode silently no-op'd for remote sessions because dispatch only saw `*Instance`, not `RemoteSessionInfo`). Each Send shells to the remote `agent-deck session send-keys <id>` subcommand; ControlMaster keeps the SSH multiplex channel warm so calls don't pay the full TCP+SSH handshake every time.

Unlike the local path, this does NOT open a long-running subprocess — `ssh ... agent-deck session send-keys <id> -- <text>` is fork+exec per call. Acceptable because:

  • Remote latency is dominated by network RTT, not local fork+exec.
  • SSH ControlMaster (already set up in SSHRunner) amortizes the connection cost across all calls during an insert-mode session.
  • Keeping a persistent `ssh ... -t` interactive session would require a custom on-remote shell protocol — out of scope for the bugfix.

The remote side's `session send-keys` handler uses the existing tmux SendKeys / SendNamedKey / SendEnter on the remote Instance, so the behavior reaching the pane is bit-identical to a local insert-mode send.

func NewRemoteKeySender added in v1.9.24

func NewRemoteKeySender(runner *SSHRunner, sessionID string, ctx context.Context) *RemoteKeySender

NewRemoteKeySender wires up a sender bound to a single remote session. The context governs every Send call's deadline; pass context.Background() to inherit SSHRunner.Run's default 10s timeout, or a derived context for the lifetime of the insert-mode session.

func (*RemoteKeySender) Close added in v1.9.24

func (r *RemoteKeySender) Close() error

Close marks the sender as closed. Subsequent Send calls fail with "closed". Idempotent so panic-recovery flows can safely double-close. Does NOT tear down the SSH ControlMaster — that's shared across the process and persists per ssh -o ControlPersist=600.

func (*RemoteKeySender) SendEnter added in v1.9.24

func (r *RemoteKeySender) SendEnter() error

SendEnter forwards a single Enter keystroke. Modeled as a discrete flag so the remote handler can preserve the existing tmux SendEnter semantics (with bracketed-paste flush delay; see internal/tmux/tmux.go:3984).

func (*RemoteKeySender) SendKeys added in v1.9.24

func (r *RemoteKeySender) SendKeys(text string) error

SendKeys forwards `text` as literal keystrokes to the remote session. Empty text is a no-op so the caller doesn't have to guard before flushing an empty rune buffer.

func (*RemoteKeySender) SendNamedKey added in v1.9.24

func (r *RemoteKeySender) SendNamedKey(key string) error

SendNamedKey forwards a tmux named key (BSpace/Up/Down/Left/Right/Tab/ BTab/C-c/C-d — the same set #1094 added to the local path).

type RemoteLatency added in v1.9.24

type RemoteLatency struct {
	// MS is the round-trip time in milliseconds. Meaningful only when
	// Offline is false.
	MS int
	// Offline is true when the most recent measurement attempt failed
	// (network error, SSH dead, remote agent-deck binary missing, etc).
	Offline bool
	// MeasuredAt is when the sample was taken; zero value means never measured.
	MeasuredAt time.Time
}

RemoteLatency is a live round-trip-time sample for a configured remote. Tracked per remote host (not per session) — multiple sessions on the same host share the same connection, so latency is a host-level metric. See issue #1103.

type RemoteSessionInfo added in v0.20.0

type RemoteSessionInfo struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Path      string `json:"path"`
	Group     string `json:"group"`
	Tool      string `json:"tool"`
	Status    string `json:"status"`
	CreatedAt string `json:"created_at"`

	// Set locally, not from JSON
	RemoteName string `json:"-"`
}

RemoteSessionInfo represents a session from a remote agent-deck instance.

type ResponseOutput added in v0.7.0

type ResponseOutput struct {
	Tool      string `json:"tool"`                 // Tool type (claude, gemini, etc.)
	Role      string `json:"role"`                 // Always "assistant" for now
	Content   string `json:"content"`              // The actual response text
	Timestamp string `json:"timestamp,omitempty"`  // When the response was generated (Claude only)
	SessionID string `json:"session_id,omitempty"` // Claude session ID (if available)
}

ResponseOutput represents a parsed response from an agent session

type RevivalClass added in v1.7.8

type RevivalClass int

RevivalClass categorizes an Instance's recoverability at scan time.

  • ClassAlive — tmux session exists AND our control pipe is alive. The session is healthy; no action needed.
  • ClassErrored — tmux session exists but the control pipe is dead (or Status == StatusError). Likely cause: SSH logout killed our inherited pipe but the tmux server survived under its own user scope. Revivable.
  • ClassDead — tmux session does not exist. Server gone (OOM, reboot, explicit tmux kill-session, or a systemd scope reap that took the whole server). NOT auto-revived; user must explicit `session restart` because we cannot distinguish intentional kill from crash.
const (
	ClassAlive RevivalClass = iota
	ClassErrored
	ClassDead
)

func (RevivalClass) String added in v1.7.8

func (c RevivalClass) String() string

type ReviveOutcome added in v1.7.8

type ReviveOutcome struct {
	InstanceID string
	Title      string
	Class      RevivalClass
	Revived    bool
	Err        error
}

ReviveOutcome describes one instance's revive attempt.

type Reviver added in v1.7.8

type Reviver struct {
	// TmuxExists receives the session's stored socket name so the probe
	// targets the right tmux server. Sessions created under an isolated
	// socket (Instance.TmuxSocketName != "") would otherwise appear "dead"
	// on the default server and the reviver would wrongly mark them gone.
	TmuxExists   func(name, socketName string) bool
	PipeAlive    func(name string) bool
	ReviveAction func(*Instance) error
	Stagger      time.Duration
	Log          *slog.Logger
}

Reviver walks storage and re-establishes dead control pipes for instances whose underlying tmux server is still alive. See REPORT-D-auto-revive.md and .planning/v178-ssh-reviver/PLAN.md for design rationale.

Fields are injectable so tests can stub out tmux and pipe checks without spawning real processes. Production code should use NewReviver() to get sensible defaults.

func NewReviver added in v1.7.8

func NewReviver() *Reviver

NewReviver returns a Reviver wired to real tmux + PipeManager primitives. Defaults: 500ms stagger between revives to avoid thundering herd on Claude cold-start rate limits when many sessions are errored simultaneously.

func (*Reviver) Classify added in v1.7.8

func (r *Reviver) Classify(inst *Instance) RevivalClass

Classify decides which bucket an instance falls into at scan time.

func (*Reviver) ReviveAll added in v1.7.8

func (r *Reviver) ReviveAll(instances []*Instance) []ReviveOutcome

ReviveAll walks instances, classifies each, and triggers ReviveAction for those in ClassErrored. Calls are staggered by r.Stagger. Alive/dead entries do NOT consume a stagger slot — total wall clock scales with errored count, not total count.

func (*Reviver) ReviveOne added in v1.7.8

func (r *Reviver) ReviveOne(inst *Instance) ReviveOutcome

ReviveOne runs a single-instance revive cycle. Used by the CLI --name flag.

type SSHRunner added in v0.20.0

type SSHRunner struct {
	Host          string // SSH destination (e.g., "user@host")
	AgentDeckPath string // Remote agent-deck binary path
	Profile       string // Remote profile name
	// contains filtered or unexported fields
}

SSHRunner executes commands on a remote host via SSH.

func NewSSHRunner added in v0.20.0

func NewSSHRunner(name string, rc RemoteConfig) *SSHRunner

NewSSHRunner creates an SSHRunner from a RemoteConfig.

func (*SSHRunner) Attach added in v0.20.0

func (r *SSHRunner) Attach(sessionID string) error

Attach connects interactively to a remote agent-deck session. Uses a local PTY so that SSH can detect the terminal dimensions and propagate them to the remote side. Handles SIGWINCH to keep the remote PTY in sync when the local terminal is resized, and sends SIGWINCH to self on detach so Bubble Tea re-queries the terminal size.

func (*SSHRunner) CheckBinary added in v0.21.0

func (r *SSHRunner) CheckBinary(ctx context.Context) (version string, found bool)

CheckBinary checks if agent-deck exists at the configured path on the remote. Returns the version string if found, or empty string if not found.

func (*SSHRunner) CreateSession added in v0.21.0

func (r *SSHRunner) CreateSession(ctx context.Context) (string, error)

CreateSession creates and starts a new session on the remote, returning its ID. It runs "add --quick --json" to create the session, then "session start" to launch the tmux process, so the session is ready to attach.

func (*SSHRunner) DeleteSession added in v0.25.0

func (r *SSHRunner) DeleteSession(ctx context.Context, sessionID string) error

DeleteSession removes a session on the remote host.

func (*SSHRunner) DeployBinary added in v0.21.0

func (r *SSHRunner) DeployBinary(ctx context.Context, binaryData []byte) error

DeployBinary uploads a binary to the remote at the configured agent-deck path.

func (*SSHRunner) DetectPlatform added in v0.21.0

func (r *SSHRunner) DetectPlatform(ctx context.Context) (goos, goarch string, err error)

DetectPlatform returns the remote host's OS and architecture (e.g., "linux", "amd64").

func (*SSHRunner) FetchCostSummary added in v1.9.24

func (r *SSHRunner) FetchCostSummary(ctx context.Context) (*costs.RemoteCostSummary, error)

FetchCostSummary retrieves the remote agent-deck's cost summary as JSON. #1101: the local TUI's status-line cost segment used to show only events written to the local cost_events table — remote sessions' Stop hooks write to the remote DB, so their spend never surfaced locally. The TUI calls this per configured remote and folds the totals into the displayed figures.

Returns nil with no error when the remote returns empty output (older agent-deck builds that predate `costs summary --json`). Callers should treat a nil summary as "remote not available; render local-only totals".

func (*SSHRunner) FetchSessionOutput added in v1.7.31

func (r *SSHRunner) FetchSessionOutput(ctx context.Context, sessionID string) (string, error)

FetchSessionOutput retrieves the last response content for a remote session.

func (*SSHRunner) FetchSessionPane added in v1.9.24

func (r *SSHRunner) FetchSessionPane(ctx context.Context, sessionID string) (string, error)

FetchSessionPane retrieves the tmux capture-pane content for a remote session. #1101: Local TUI previews render capture-pane content (ANSI + tool UI chrome). Remote previews used to fetch only the parsed transcript text via FetchSessionOutput, which is why claude-formatted output never showed for SSH sessions. FetchSessionPane closes that gap by asking the remote for the raw pane content via `session output --pane --json`.

func (*SSHRunner) FetchSessions added in v0.20.0

func (r *SSHRunner) FetchSessions(ctx context.Context) ([]RemoteSessionInfo, error)

FetchSessions retrieves the session list from the remote agent-deck instance.

func (*SSHRunner) MeasureLatency added in v1.9.24

func (r *SSHRunner) MeasureLatency(ctx context.Context) (time.Duration, error)

MeasureLatency measures the round-trip time of a lightweight noop call to the remote agent-deck binary. Returns the elapsed duration on success.

Implementation note: we run `agent-deck --version` because it is the cheapest possible call (no DB read, no tmux probe, no network back to services). The ControlMaster socket is persisted across calls so we measure mostly network RTT after the first hit, which is exactly what the user wants to see in the header per #1103.

func (*SSHRunner) OpenStream added in v1.9.25

func (r *SSHRunner) OpenStream(ctx context.Context, args ...string) (io.WriteCloser, func() error, error)

OpenStream spawns a single long-running remote `agent-deck <args...>` subprocess over SSH and returns its stdin pipe + a close function that terminates the subprocess. Used by #1112 bug 2's persistent insert-mode stream so 100 keystrokes amortize to one ssh fork+exec instead of 100.

The returned WriteCloser is goroutine-safe at the OS pipe layer; the caller is responsible for serializing if it needs message-level ordering (RemoteKeySender does this with its own mutex).

func (*SSHRunner) RestartSession added in v0.25.0

func (r *SSHRunner) RestartSession(ctx context.Context, sessionID string) error

RestartSession restarts a session on the remote host.

func (*SSHRunner) Run added in v0.20.0

func (r *SSHRunner) Run(ctx context.Context, args ...string) ([]byte, error)

Run executes an agent-deck command on the remote host and returns stdout.

func (*SSHRunner) RunCommand added in v0.20.0

func (r *SSHRunner) RunCommand(ctx context.Context, args ...string) ([]byte, error)

RunCommand executes an arbitrary agent-deck command on the remote.

func (*SSHRunner) StopSession added in v0.25.0

func (r *SSHRunner) StopSession(ctx context.Context, sessionID string) error

StopSession stops a session process on the remote host without removing metadata.

type SandboxConfig added in v0.19.17

type SandboxConfig struct {
	// Enabled indicates the session runs inside a container.
	Enabled bool `json:"enabled"`

	// Image is the Docker image name (e.g. "ghcr.io/asheshgoplani/agent-deck-sandbox:latest").
	Image string `json:"image"`

	// CPULimit is the optional CPU quota for the container (e.g. "2.0").
	CPULimit *string `json:"cpu_limit,omitempty"`

	// MemoryLimit is the optional memory cap for the container (e.g. "4g").
	MemoryLimit *string `json:"memory_limit,omitempty"`

	// ExtraVolumes maps host paths to container paths for additional bind mounts.
	ExtraVolumes map[string]string `json:"extra_volumes,omitempty"`
}

SandboxConfig holds per-session Docker sandbox settings.

func NewSandboxConfig added in v0.19.17

func NewSandboxConfig(imageOverride string) *SandboxConfig

NewSandboxConfig builds a SandboxConfig from CLI flags and user settings. imageOverride takes precedence; when empty the global default image is used. CPU and memory limits are applied from DockerSettings when configured.

type SearchEntry added in v0.5.3

type SearchEntry struct {
	SessionID string    // Claude session UUID
	FilePath  string    // Path to .jsonl file
	CWD       string    // Project working directory
	Summary   string    // First user message or summary
	ModTime   time.Time // File modification time
	FileSize  int64     // File size in bytes
	// contains filtered or unexported fields
}

SearchEntry represents a searchable Claude session

func (*SearchEntry) ContentPreview added in v0.10.0

func (e *SearchEntry) ContentPreview(max int) string

func (*SearchEntry) ContentString added in v0.10.0

func (e *SearchEntry) ContentString() string

func (*SearchEntry) GetSnippet added in v0.5.3

func (e *SearchEntry) GetSnippet(query string, windowSize int) string

GetSnippet extracts a context window around the first match Uses rune-based indexing to safely handle UTF-8 content Optimized: Single rune conversion instead of triple conversion

func (*SearchEntry) Match added in v0.5.3

func (e *SearchEntry) Match(query string) []MatchRange

Match searches for query in entry content (case-insensitive) Returns match positions for highlighting

func (*SearchEntry) MatchCount added in v0.10.0

func (e *SearchEntry) MatchCount(query string) int

type SearchResult added in v0.5.3

type SearchResult struct {
	Entry   *SearchEntry
	Matches []MatchRange
	Score   int
	Snippet string
}

SearchResult represents a search result with match info

type SearchTier added in v0.5.3

type SearchTier int

SearchTier represents the search strategy tier

const (
	TierInstant  SearchTier = iota // < 100MB, full in-memory
	TierBalanced                   // 100MB-500MB, on-demand scan to cap memory
)

func DetectTier added in v0.5.3

func DetectTier(totalSize int64) SearchTier

DetectTier determines the appropriate search tier based on data size

type SessionAnalytics added in v0.8.27

type SessionAnalytics struct {
	// Token usage (cumulative across all turns)
	InputTokens      int `json:"input_tokens"`
	OutputTokens     int `json:"output_tokens"`
	CacheReadTokens  int `json:"cache_read_input_tokens"`
	CacheWriteTokens int `json:"cache_creation_input_tokens"`

	// Current context size (last turn's input + cache read tokens)
	// This represents the actual context window usage, not cumulative totals
	CurrentContextTokens int `json:"current_context_tokens"`

	// Session metrics
	TotalTurns int           `json:"total_turns"`
	Duration   time.Duration `json:"duration"`
	StartTime  time.Time     `json:"start_time"`
	LastActive time.Time     `json:"last_active"`

	// Tool usage
	ToolCalls []ToolCall `json:"tool_calls"`

	// Model ID from the last assistant message (e.g. "claude-opus-4-6")
	Model string `json:"model,omitempty"`

	// Subagents
	Subagents []SubagentInfo `json:"subagents"`

	// Cost estimation
	EstimatedCost float64 `json:"estimated_cost"`

	// 5-hour billing blocks
	BillingBlocks []BillingBlock `json:"billing_blocks"`
}

SessionAnalytics holds parsed session metrics from Claude JSONL files

func ParseSessionJSONL added in v0.8.27

func ParseSessionJSONL(path string) (*SessionAnalytics, error)

ParseSessionJSONL parses a Claude session JSONL file and returns analytics

func (*SessionAnalytics) CalculateCost added in v0.8.27

func (a *SessionAnalytics) CalculateCost(model string) float64

CalculateCost estimates session cost based on token usage and model pricing

func (*SessionAnalytics) ContextPercent added in v0.8.27

func (a *SessionAnalytics) ContextPercent(modelLimit int) float64

ContextPercent returns the percentage of context window used Uses CurrentContextTokens (last turn's input + cache) for accurate context usage modelLimit is the model's context window size; if 0, it is inferred from the Model field

func (*SessionAnalytics) TotalTokens added in v0.8.27

func (a *SessionAnalytics) TotalTokens() int

TotalTokens returns the sum of all token types

type SessionBudget added in v0.26.4

type SessionBudget struct {
	TotalLimit float64 `toml:"total_limit"`
}

type SessionIDLifecycleEvent added in v0.28.0

type SessionIDLifecycleEvent struct {
	InstanceID string `json:"instance_id"`
	Tool       string `json:"tool"`
	Action     string `json:"action"` // bind | rebind | reject | scan_disabled
	Source     string `json:"source"` // tmux_env | hook_payload | hook_anchor | disk_scan
	OldID      string `json:"old_id,omitempty"`
	NewID      string `json:"new_id,omitempty"`
	Candidate  string `json:"candidate,omitempty"`
	Reason     string `json:"reason,omitempty"`
	HookEvent  string `json:"hook_event,omitempty"`
	Timestamp  int64  `json:"ts"`
}

SessionIDLifecycleEvent captures every session-ID bind/rebind/reject decision. Events are appended as JSONL for postmortem debugging.

type SessionLifecycleEvent added in v1.9.29

type SessionLifecycleEvent struct {
	InstanceID string `json:"instance_id"`
	Action     string `json:"action"` // currently only "idle-timeout-expired"
	Reason     string `json:"reason,omitempty"`
	Timestamp  int64  `json:"ts"`
}

SessionLifecycleEvent is a single row in session-lifecycle.jsonl.

type ShellSettings added in v0.8.77

type ShellSettings struct {
	// EnvFiles is a list of .env files to source for ALL sessions
	// Paths can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
	// Files are sourced in order; later files override earlier ones
	EnvFiles []string `toml:"env_files"`

	// InitScript is an optional shell script or command to run before each session
	// Useful for direnv, nvm, pyenv, etc.
	// Can be a file path (e.g., "~/.agent-deck/init.sh") or inline command
	// (e.g., 'eval "$(direnv hook bash)"')
	InitScript string `toml:"init_script"`

	// IgnoreMissingEnvFiles silently ignores missing .env files (default: true)
	// When false, sessions will error if an env_file doesn't exist
	IgnoreMissingEnvFiles *bool `toml:"ignore_missing_env_files"`
}

ShellSettings defines shell environment configuration for sessions

func (*ShellSettings) GetIgnoreMissingEnvFiles added in v0.8.77

func (s *ShellSettings) GetIgnoreMissingEnvFiles() bool

GetIgnoreMissingEnvFiles returns whether to ignore missing env files, defaulting to true

type SkillCandidate added in v0.19.1

type SkillCandidate struct {
	ID          string `json:"id"` // source/name
	Name        string `json:"name"`
	Source      string `json:"source"`
	SourcePath  string `json:"source_path"`
	EntryName   string `json:"entry_name"` // directory/file name in source
	Description string `json:"description,omitempty"`
	Kind        string `json:"kind"` // "dir" or "file"
}

SkillCandidate is a discovered skill from one source.

func ListAvailableSkills added in v0.19.1

func ListAvailableSkills() ([]SkillCandidate, error)

ListAvailableSkills returns all discovered skills across enabled sources.

func ResolveSkillCandidate added in v0.19.1

func ResolveSkillCandidate(skillRef, sourceName string) (*SkillCandidate, error)

ResolveSkillCandidate resolves one skill from discovery by name or source/name.

type SkillSource added in v0.19.1

type SkillSource struct {
	Name        string `json:"name"`
	Path        string `json:"path"`
	Description string `json:"description,omitempty"`
	Enabled     bool   `json:"enabled"`
}

SkillSource is a resolved source used for display and discovery.

func ListSkillSources added in v0.19.1

func ListSkillSources() ([]SkillSource, error)

ListSkillSources returns sorted source definitions for display.

type SkillSourceDef added in v0.19.1

type SkillSourceDef struct {
	Path        string `toml:"path"`
	Description string `toml:"description,omitempty"`
	Enabled     *bool  `toml:"enabled,omitempty"`
}

SkillSourceDef defines a named source path for discovering skills.

func (SkillSourceDef) IsEnabled added in v0.19.1

func (s SkillSourceDef) IsEnabled() bool

IsEnabled returns true when the source should be considered during discovery.

type SkillSourcesConfig added in v0.19.1

type SkillSourcesConfig struct {
	Sources map[string]SkillSourceDef `toml:"sources"`
}

SkillSourcesConfig is persisted in ~/.agent-deck/skills/sources.toml.

type SlackSettings added in v0.16.0

type SlackSettings struct {
	// BotToken is the Slack bot token (xoxb-...)
	BotToken string `toml:"bot_token"`

	// AppToken is the Slack app-level token for Socket Mode (xapp-...)
	AppToken string `toml:"app_token"`

	// ChannelID is the Slack channel where the bot listens and posts (C01234...)
	ChannelID string `toml:"channel_id"`

	// ListenMode controls when the bot responds: "mentions" (only @mentions) or "all" (all channel messages)
	// Default: "mentions"
	ListenMode string `toml:"listen_mode"`

	// AllowedUserIDs is a list of Slack user IDs authorized to use the bot.
	// If empty, all users are allowed (backward compatible).
	// Get user ID from Slack: Right-click user → View profile → More → Copy member ID
	AllowedUserIDs []string `toml:"allowed_user_ids"`
}

SlackSettings defines Slack bot configuration for the conductor bridge

type SpawnAttempt added in v1.9.19

type SpawnAttempt struct {
	// InstanceID is the lock-key partition. Different instances do not
	// serialize against each other.
	InstanceID string

	// AlreadyAlive is an optional supplementary gate. When non-nil and
	// it returns true, Run() skips Spawn even if no stamp exists. Used
	// by callers that already know the spawn is unnecessary (e.g. CLI
	// `session start` pre-checks `Exists()`); leave nil to let the
	// stamp logic decide.
	AlreadyAlive func() bool

	// Spawn is the protected critical section. Run() invokes it exactly
	// once across concurrent callers in a storm window, and never if a
	// sibling already completed while we were waiting on the lock.
	Spawn func() error
}

SpawnAttempt is the single-flight wrapper used by Restart(), Start(), and StartWithMessage(). The lock acquisition + sibling-detect window is implicit — call Run().

The storm-discriminator is *time*, not "is a session alive". A legitimate manual restart of a long-running session must proceed even though tmux holds a live AGENTDECK_INSTANCE_ID-matching session; only the *storm* shape — multiple spawns racing while one is in-flight — should be suppressed. Run() captures a timestamp before acquiring the lock and consults the per-instance spawn-stamp after acquisition. If a sibling completed during our wait (stamp mtime > our pre-lock time), we skip; otherwise we run Spawn and stamp on success.

func (SpawnAttempt) Run added in v1.9.19

func (a SpawnAttempt) Run() error

Run acquires the per-instance spawn lock, gates on a "spawned-while- we-waited" check, and invokes Spawn. Returns the first non-nil error from any step.

type Status

type Status string

Status represents the current state of a session

const (
	StatusRunning  Status = "running"
	StatusWaiting  Status = "waiting"
	StatusIdle     Status = "idle"
	StatusError    Status = "error"
	StatusStarting Status = "starting" // Session is being created (tmux initializing)
	StatusStopped  Status = "stopped"  // Session intentionally stopped by user (not crashed)
	// StatusQueued: session is waiting for group capacity. v1.9.1 introduces
	// group max_concurrent caps; a launch into a group at cap stores the
	// instance with this status and starts it once a running session ends.
	StatusQueued Status = "queued"
)

type StatusEvent added in v0.18.0

type StatusEvent struct {
	InstanceID string `json:"instance_id"`
	Title      string `json:"title"`
	Tool       string `json:"tool"`
	Status     string `json:"status"`
	PrevStatus string `json:"prev_status"`
	Timestamp  int64  `json:"ts"`
}

StatusEvent represents a session status change event. Written atomically to ~/.agent-deck/events/ by both the hook handler (Claude) and the TUI's background status poller (all tools).

type StatusEventWatcher added in v0.18.0

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

StatusEventWatcher watches ~/.agent-deck/events/ for new status events using fsnotify. Delivers parsed StatusEvent structs via a channel.

func NewStatusEventWatcher added in v0.18.0

func NewStatusEventWatcher(filterInstanceID string) (*StatusEventWatcher, error)

NewStatusEventWatcher creates a watcher for the events directory. If filterInstanceID is non-empty, only events for that instance are delivered. Call Start() in a goroutine, then read from EventCh().

func (*StatusEventWatcher) EventCh added in v0.18.0

func (w *StatusEventWatcher) EventCh() <-chan StatusEvent

EventCh returns the channel that delivers parsed status events.

func (*StatusEventWatcher) Start added in v0.18.0

func (w *StatusEventWatcher) Start()

Start begins watching the events directory. Must be called in a goroutine. Blocks until Stop() is called or the context is cancelled.

func (*StatusEventWatcher) Stop added in v0.18.0

func (w *StatusEventWatcher) Stop()

Stop shuts down the watcher and closes the event channel.

func (*StatusEventWatcher) WaitForStatus added in v0.18.0

func (w *StatusEventWatcher) WaitForStatus(statuses []string, timeout time.Duration) (StatusEvent, error)

WaitForStatus blocks until an event with one of the given statuses is received, or the timeout expires.

type StatusFileWatcher added in v0.16.0

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

StatusFileWatcher watches ~/.agent-deck/hooks/ for status file changes and updates instance hook status in real time.

func NewStatusFileWatcher added in v0.16.0

func NewStatusFileWatcher(onChange func()) (*StatusFileWatcher, error)

NewStatusFileWatcher creates a new watcher for the hooks directory. Call Start() to begin watching.

func (*StatusFileWatcher) ClearHookStatus added in v1.9.2

func (w *StatusFileWatcher) ClearHookStatus(instanceID string)

ClearHookStatus removes the cached hook status for an instance.

func (*StatusFileWatcher) GetHookStatus added in v0.16.0

func (w *StatusFileWatcher) GetHookStatus(instanceID string) *HookStatus

GetHookStatus returns the hook status for an instance, or nil if not available.

func (*StatusFileWatcher) Start added in v0.16.0

func (w *StatusFileWatcher) Start()

Start begins watching the hooks directory. Must be called in a goroutine.

func (*StatusFileWatcher) Stop added in v0.16.0

func (w *StatusFileWatcher) Stop()

Stop shuts down the watcher.

type StatusSettings added in v0.11.0

type StatusSettings struct {
}

func GetStatusSettings added in v0.11.0

func GetStatusSettings() StatusSettings

GetStatusSettings returns status detection settings with defaults applied.

type Storage

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

Storage handles persistence of session data via SQLite. Thread-safe with mutex protection for concurrent access within a single process. Multiple processes share data via SQLite WAL mode.

func NewStorageWithProfile added in v0.3.0

func NewStorageWithProfile(profile string) (*Storage, error)

NewStorageWithProfile creates a storage instance for a specific profile. If profile is empty, uses the effective profile (from env var or config). Automatically runs migration from old layout if needed, then opens SQLite. If sessions.json exists and state.db is empty, auto-migrates data.

func (*Storage) Close added in v0.11.0

func (s *Storage) Close() error

Close closes the underlying database connection.

func (*Storage) DeleteInstance added in v0.11.3

func (s *Storage) DeleteInstance(id string) error

DeleteInstance removes a single instance from the database by ID. This ensures the row is immediately removed, preventing resurrection on reload.

func (*Storage) GetDB added in v0.11.0

func (s *Storage) GetDB() *statedb.StateDB

GetDB returns the underlying StateDB for direct access (status writes, heartbeat, etc.)

func (*Storage) GetFileMtime added in v0.10.15

func (s *Storage) GetFileMtime() (time.Time, error)

GetFileMtime returns the filesystem modification time of the database file. This is useful for detecting external changes when polling.

func (*Storage) GetUpdatedAt added in v0.6.1

func (s *Storage) GetUpdatedAt() (time.Time, error)

GetUpdatedAt returns the last modification timestamp from SQLite metadata.

func (*Storage) InsertSessionAndVerify added in v1.9.15

func (s *Storage) InsertSessionAndVerify(newInstance *Instance, groupTree *GroupTree) error

InsertSessionAndVerify performs a durable single-row session insert.

Flow (v1.9.x issue #1031 fix, parallel to #909's RemoveSessionAndVerify):

  1. SaveInstance(row) — targeted INSERT OR REPLACE on the single new row only, NOT a full-table rewrite. This sidesteps the load-modify-write race where a sibling launch's `DELETE FROM instances WHERE id NOT IN (...)` inside SaveInstances would silently delete this row.
  2. SaveGroupsOnly(groupTree) — persist any group structure changes WITHOUT rewriting the instances table. Rewriting (SaveWithGroups) is the load-modify-write pattern that lets a concurrent launch drop this row; skipping it eliminates the structural race for our own write.
  3. Verify InstanceExists(id) is true. If not (some other process issued a SaveInstances rewrite that excluded this row because it loaded the instances slice pre-INSERT), re-issue the targeted INSERT and loop with linear backoff.
  4. After exhausting attempts, return ErrInsertNotPersistent so the caller can fail loudly instead of returning success on a row that's not actually there.

instances is the post-insert session list, used only to compute group sort_order / membership for SaveGroupsOnly. groupTree may be nil if the caller doesn't care to persist groups.

func (*Storage) InstanceExists added in v1.9.1

func (s *Storage) InstanceExists(id string) (bool, error)

InstanceExists returns true iff a row with the given id is currently persisted. Used by RemoveSessionAndVerify to confirm a DELETE actually landed (issue #909).

func (*Storage) Load

func (s *Storage) Load() ([]*Instance, error)

Load reads instances from SQLite

func (*Storage) LoadLite added in v0.8.95

func (s *Storage) LoadLite() ([]*InstanceData, []*GroupData, error)

LoadLite reads session data from SQLite without tmux reconnection. This is a fast path for operations that only need to read session metadata (e.g., finding current session by tmux name) without initializing full Instance objects. Returns raw InstanceData and GroupData without any subprocess calls.

func (*Storage) LoadRecentSessions added in v0.20.0

func (s *Storage) LoadRecentSessions() ([]*statedb.RecentSessionRow, error)

LoadRecentSessions returns recently deleted session configs for the picker.

func (*Storage) LoadWithGroups

func (s *Storage) LoadWithGroups() ([]*Instance, []*GroupData, error)

LoadWithGroups reads instances and groups from SQLite, reconnects tmux sessions.

func (*Storage) Path added in v0.8.1

func (s *Storage) Path() string

Path returns the database path this storage is using

func (*Storage) Profile added in v0.3.0

func (s *Storage) Profile() string

Profile returns the profile name this storage is using

func (*Storage) RemoveSessionAndVerify added in v1.9.1

func (s *Storage) RemoveSessionAndVerify(id string, remainingInstances []*Instance, groupTree *GroupTree) error

RemoveSessionAndVerify performs a durable session removal.

Flow (v1.9.1 issue #909 fix):

  1. DeleteInstance(id) — targeted DELETE, busy-retry inside statedb.
  2. SaveGroupsOnly(groupTree) — persist any group structure changes WITHOUT rewriting the instances table. Rewriting (SaveWithGroups) is the load-modify-write pattern that lets a concurrent rm resurrect this row via INSERT OR REPLACE; skipping it eliminates the structural race for our own write.
  3. Verify InstanceExists(id) is false. If still present (because some other process did a SaveInstances rewrite that included the row), re-issue the targeted DELETE and loop with linear backoff.
  4. After exhausting attempts, return ErrRemovalNotPersistent so the caller can fail loudly instead of printing "✓ Removed" on a row that's still there.

remainingInstances is the post-removal session list, used only to compute group sort_order / membership for SaveGroupsOnly. groupTree may be nil if the caller doesn't care to persist groups.

func (*Storage) Save

func (s *Storage) Save(instances []*Instance) error

Save persists instances to SQLite DEPRECATED: Use SaveWithGroups to ensure groups are not lost

func (*Storage) SaveGroupsOnly added in v0.11.3

func (s *Storage) SaveGroupsOnly(groupTree *GroupTree) error

SaveGroupsOnly persists only the groups table to SQLite. This is a lightweight save for visual state like group expanded/collapsed. It does NOT call Touch() to avoid triggering StorageWatcher reloads on other instances.

func (*Storage) SaveRecentSession added in v0.20.0

func (s *Storage) SaveRecentSession(inst *Instance) error

SaveRecentSession captures a deleted session's config for quick re-creation.

func (*Storage) SaveWithGroups

func (s *Storage) SaveWithGroups(instances []*Instance, groupTree *GroupTree) error

SaveWithGroups persists instances and groups to SQLite. Converts Instance objects to database rows, then batch-inserts in a transaction.

type StorageData

type StorageData struct {
	Instances []*InstanceData `json:"instances"`
	Groups    []*GroupData    `json:"groups,omitempty"` // Persist empty groups
	UpdatedAt time.Time       `json:"updated_at"`
}

StorageData represents the JSON structure for persistence (kept for migration/compat)

type StreamConfig added in v1.7.48

type StreamConfig struct {
	// IdleTimeout is the max time without any new record before the
	// streamer emits an error event and returns. Default: 10s.
	IdleTimeout time.Duration

	// CharBudget is the max chars buffered in the text accumulator before
	// a flush. Default: 4000.
	CharBudget int

	// ToolBudget is the max queued tool events (tool_use + tool_result)
	// before buffered text is flushed. Default: 3.
	ToolBudget int

	// PollInterval is how often the tail loop checks the file for new
	// bytes. Default: 100ms.
	PollInterval time.Duration
}

StreamConfig controls streamer behavior. Zero values resolve to the approved defaults via WithDefaults(): 10s idle, 4000 chars, 3 tools.

func (StreamConfig) WithDefaults added in v1.7.48

func (c StreamConfig) WithDefaults() StreamConfig

WithDefaults returns a StreamConfig with zero-valued fields replaced by the approved defaults. Negative values are also treated as zero.

type StreamEvent added in v1.7.48

type StreamEvent struct {
	Type          string          `json:"type"`                     // start | text | tool_use | tool_result | stop | error
	SchemaVersion string          `json:"schema_version,omitempty"` // only set on "start"
	SessionID     string          `json:"session_id,omitempty"`     // set on "start"
	Timestamp     string          `json:"ts,omitempty"`             // RFC3339Nano when available
	MessageID     string          `json:"message_id,omitempty"`     // assistant message id (text / tool_use)
	Delta         string          `json:"delta,omitempty"`          // flushed text block
	ToolUseID     string          `json:"tool_use_id,omitempty"`
	Name          string          `json:"name,omitempty"`  // tool name on tool_use
	Input         json.RawMessage `json:"input,omitempty"` // tool input on tool_use
	Content       json.RawMessage `json:"content,omitempty"`
	Reason        string          `json:"reason,omitempty"`  // stop reason
	Message       string          `json:"message,omitempty"` // error payload
}

StreamEvent is one structured event emitted by the streamer, serialized as one JSONL line on the output writer.

type StreamingRemoteKeySender added in v1.9.25

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

StreamingRemoteKeySender amortizes the per-keystroke ssh fork+exec cost (#1112 bug 2) across an entire insert-mode session by holding one long-running `ssh ... agent-deck session send-keys <id> --stream` subprocess open. Each Send writes one line to that subprocess's stdin — sub-millisecond regardless of network RTT, because the SSH transport is already negotiated.

Wire protocol (matches cmd/agent-deck/session_send_keys_cmd.go's runSendKeysStream):

T <hex-text>\n   — SendKeys(decode(<hex-text>))
N <name>\n       — SendNamedKey(<name>)
E\n              — SendEnter()

Hex encoding for text means newlines/NULL/non-printables round-trip safely without escape-sequence gymnastics.

func OpenStreamingRemoteKeySender added in v1.9.25

func OpenStreamingRemoteKeySender(runner *SSHRunner, sessionID string, ctx context.Context) (*StreamingRemoteKeySender, error)

OpenStreamingRemoteKeySender spawns the persistent SSH stream and returns a sender ready to dispatch. Falls back to a per-call RemoteKeySender if the stream can't open (caller can check via errors.Is — the streaming type implements the same insertKeySender interface as RemoteKeySender).

func (*StreamingRemoteKeySender) Close added in v1.9.25

func (s *StreamingRemoteKeySender) Close() error

Close terminates the persistent stream. Idempotent.

func (*StreamingRemoteKeySender) SendEnter added in v1.9.25

func (s *StreamingRemoteKeySender) SendEnter() error

SendEnter writes "E\n" to the stream.

func (*StreamingRemoteKeySender) SendKeys added in v1.9.25

func (s *StreamingRemoteKeySender) SendKeys(text string) error

SendKeys writes a "T <hex>\n" line to the persistent stream. Empty text is a no-op (matches RemoteKeySender).

func (*StreamingRemoteKeySender) SendNamedKey added in v1.9.25

func (s *StreamingRemoteKeySender) SendNamedKey(key string) error

SendNamedKey writes "N <key>\n" to the stream. Empty key fails fast.

type SubagentInfo added in v0.8.27

type SubagentInfo struct {
	ID        string    `json:"id"`
	StartTime time.Time `json:"start_time"`
	Turns     int       `json:"turns"`
}

SubagentInfo holds metadata about a subagent spawned during a session

type SystemStatsSettings added in v0.28.1

type SystemStatsSettings struct {
	// Enabled controls whether system stats are collected and displayed (default: true)
	Enabled *bool `toml:"enabled"`

	// RefreshSeconds sets the collection interval in seconds (default: 5, min: 2)
	RefreshSeconds int `toml:"refresh_seconds"`

	// Format controls display density: "compact" (icons), "full" (labels), "minimal" (values only)
	Format string `toml:"format"`

	// Show lists which stats to display: "cpu", "ram", "disk", "load", "gpu", "network"
	Show []string `toml:"show"`
}

SystemStatsSettings configures the system stats display in the status bar.

func (SystemStatsSettings) GetEnabled added in v0.28.1

func (s SystemStatsSettings) GetEnabled() bool

GetEnabled returns whether system stats display is enabled (default: true).

func (SystemStatsSettings) GetFormat added in v0.28.1

func (s SystemStatsSettings) GetFormat() string

GetFormat returns the display format (default: "compact").

func (SystemStatsSettings) GetRefreshSeconds added in v0.28.1

func (s SystemStatsSettings) GetRefreshSeconds() int

GetRefreshSeconds returns the collection interval, clamped to [2, 300].

func (SystemStatsSettings) GetShow added in v0.28.1

func (s SystemStatsSettings) GetShow() []string

GetShow returns the list of stats to display. Defaults to cpu, ram, disk, network.

type TelegramChannelEnablementResult added in v1.9.27

type TelegramChannelEnablementResult struct {
	// OK is true when the session has no telegram channel OR the
	// effective settings.json has telegram=true. Either case means
	// `--channels` will not silently land on a disabled plugin.
	OK bool

	// Reason describes the failure when OK=false. Empty when OK=true.
	// Stable English phrasing — operators grep for substrings.
	Reason string

	// EffectiveValue is the value found in settings.json
	// (true/false/absent). Carried so the warning can name the exact
	// drift variant the operator is observing.
	EffectiveValue string
}

TelegramChannelEnablementResult is the structured outcome of VerifyTelegramChannelEnabled. OK is the only consumer-relevant bit; Reason carries the human-readable diagnostic for log lines.

func VerifyTelegramChannelEnabled added in v1.9.27

func VerifyTelegramChannelEnabled(configDir string, channels []string) TelegramChannelEnablementResult

VerifyTelegramChannelEnabled inspects the given config dir's settings.json and reports whether a session whose `--channels` references `plugin:telegram@…` will find a live MCP transport.

The check is conservative: a missing or unreadable settings.json is treated as "not enabled" (Reason fills in). This matches Claude Code's own behavior — absence equals disabled for channel plugins.

Pure / read-only; safe to call from prepare path and from a runtime monitor (telegram-doctor CLI).

type TelegramSettings added in v0.12.0

type TelegramSettings struct {
	// Token is the Telegram bot token from @BotFather
	Token string `toml:"token"`

	// UserID is the authorized Telegram user ID from @userinfobot
	UserID int64 `toml:"user_id"`
}

TelegramSettings defines Telegram bot configuration for the conductor bridge

type TelegramValidatorInput added in v1.7.22

type TelegramValidatorInput struct {
	// GlobalEnabled is the value of
	// enabledPlugins."telegram@claude-plugins-official" in the relevant
	// profile's settings.json, or false if that file or key is absent.
	GlobalEnabled bool

	// SessionChannels is the list of channels the session is launched with
	// (Instance.Channels). Empty for ordinary child sessions.
	SessionChannels []string

	// SessionWrapper is the wrapper template for the session (may be empty).
	SessionWrapper string
}

TelegramValidatorInput captures the three signals the validator inspects.

type TelegramWarning added in v1.7.22

type TelegramWarning struct {
	Code    string // GLOBAL_ANTIPATTERN | DOUBLE_LOAD | WRAPPER_DEPRECATED
	Message string
}

TelegramWarning is one emission from the validator.

func ValidateTelegramTopology added in v1.7.22

func ValidateTelegramTopology(in TelegramValidatorInput) []TelegramWarning

ValidateTelegramTopology returns zero or more warnings for the given session configuration. Ordering is stable: GLOBAL_ANTIPATTERN, DOUBLE_LOAD, WRAPPER_DEPRECATED.

type TerminalSettings added in v1.7.72

type TerminalSettings struct {
	// ITermBadge controls whether agent-deck sets the iTerm2 badge to the
	// attached session's title for the duration of the attach, and refreshes
	// it when Claude renames the session mid-attach. No-op outside iTerm2.
	//
	// AGENTDECK_ITERM_BADGE env var overrides this in either direction
	// (=1/true/yes/on force on, =0/false/no/off force off; unset defers to
	// this config). Caveat: env reliably reaches the attach/detach path
	// (agent-deck reads its own env directly) but the rename-while-attached
	// path runs in a hook subprocess spawned through agent-deck → tmux →
	// Claude → hook, and Claude may filter custom env vars. For consistent
	// behavior on both paths, prefer this config setting — every process
	// re-reads it from disk, so propagation is independent of the spawn
	// chain.
	//
	// Default: false (opt-in). Most users have their own iTerm2 badge scheme
	// (e.g. host/cwd via shell PROMPT_COMMAND), so silently overwriting it on
	// every attach is too presumptuous a default. Users who want the
	// per-session badge set this to true explicitly.
	ITermBadge *bool `toml:"iterm_badge"`
}

TerminalSettings controls outer-terminal chrome agent-deck writes directly to the host terminal (bypassing tmux). These settings affect what the terminal emulator displays — currently only iTerm2's badge.

Example config.toml:

[terminal]
iterm_badge = true

func GetTerminalSettings added in v1.7.72

func GetTerminalSettings() TerminalSettings

GetTerminalSettings returns terminal-chrome settings from config.

func (TerminalSettings) GetITermBadge added in v1.7.72

func (t TerminalSettings) GetITermBadge() bool

GetITermBadge returns whether the iTerm2 badge integration is enabled, defaulting to false (opt-in). Mirrors the GetInjectStatusLine pattern but with the inverse default — see ITermBadge field doc for rationale.

type TmuxSettings added in v0.12.1

type TmuxSettings struct {
	// InjectStatusLine controls whether agent-deck injects a custom status line
	// into new tmux sessions. When false, the tmux status bar is not modified,
	// allowing users to use their own tmux status line configuration. This also
	// disables Agent Deck's global tmux notification bar and key bindings so the
	// runtime stops mutating global tmux options.
	// Default: true (nil = use default true)
	InjectStatusLine *bool `toml:"inject_status_line"`

	// Mouse controls whether agent-deck enables tmux mouse mode on new
	// sessions. When false, tmux `mouse on` is never set, so the terminal
	// emulator keeps raw control of mouse events — required by the VS Code
	// Linux integrated terminal to let users click-drag to select text
	// (issue #730). Affects both the inline set-option during session
	// creation and the separate EnableMouseMode() path used on reconnect.
	// Default: true (nil = use default true, preserves pre-#730 behavior)
	Mouse *bool `toml:"mouse"`

	// LaunchInUserScope starts new tmux servers via `systemd-run --user --scope`
	// so the tmux server lives under the user's systemd manager instead of the
	// current login session scope. This keeps tmux alive when an SSH session
	// scope is torn down.
	//
	// Default (when nil / field absent): true on Linux hosts where
	// `systemd-run --user --version` succeeds, false otherwise. Explicit
	// `launch_in_user_scope = true` or `launch_in_user_scope = false` in
	// config.toml is always honored. Pointer type is required to distinguish
	// "field absent" from "explicit false".
	LaunchInUserScope *bool `toml:"launch_in_user_scope"`

	// LaunchAs selects the spawn form for new tmux servers (v1.7.21+).
	// Valid values (case-insensitive, whitespace-trimmed):
	//   "scope"   — systemd-run --user --scope (PR #467 legacy behavior)
	//   "service" — systemd-run --user --unit <NAME>.service with
	//               Type=forking + Restart=on-failure. Adds auto-restart
	//               if the tmux daemon dies unexpectedly (OOM, SIGKILL,
	//               kernel signal). Opt-in defense-in-depth.
	//   "direct"  — plain `tmux new-session` (no systemd isolation).
	//   "auto"    — service where systemd-user manager is available,
	//               else direct.
	//   ""        — unset (default): defer to LaunchInUserScope.
	//
	// LaunchAs, when non-empty and valid, takes precedence over
	// LaunchInUserScope. Unknown values are ignored (fall through to
	// LaunchInUserScope) so a config typo doesn't silently opt the user
	// onto an unintended spawn path.
	//
	// This is additive — v1.7.20 users get zero behavior change until
	// they explicitly set launch_as.
	LaunchAs *string `toml:"launch_as"`

	// WindowStyleOverride sets the tmux window-style (and window-active-style) for
	// all sessions, overriding the theme default. Use "default" to let your terminal
	// emulator's background show through instead of agent-deck's theme color.
	// Empty string (default) means use the theme's built-in value.
	// Takes precedence over the same keys in Options if both are set.
	// Example: window_style_override = "default"
	WindowStyleOverride string `toml:"window_style_override"`

	// ClearOnRestart clears the tmux scrollback buffer when a session is
	// restarted (respawn-pane). When false (default), the previous session's
	// output is preserved in scrollback. When true, scrollback is wiped so
	// the new session starts with a clean buffer.
	ClearOnRestart bool `toml:"clear_on_restart"`

	// DetachKey overrides the PTY-attach detach key (issue #434). Accepts
	// the same lowercase "ctrl+<letter>" form as `[hotkeys].detach` (e.g.
	// "ctrl+d"). When set to a non-empty string, it becomes an alias for
	// `[hotkeys].detach`. Precedence: explicit `[hotkeys].detach` always
	// wins; `[tmux].detach_key` is used only when `[hotkeys].detach` is
	// absent. Empty string (default) preserves the built-in Ctrl+Q.
	//
	// Why the alias exists: #434 reporters asked for a `[tmux]` section
	// entry because they think of the detach as a tmux-attach concern.
	// Keeping `[hotkeys].detach` authoritative avoids two sources of truth.
	DetachKey string `toml:"detach_key"`

	// Options is a map of tmux option names to values.
	// These are passed to `tmux set-option -t <session>` after defaults.
	Options map[string]string `toml:"options"`

	// SocketName is the tmux `-L <name>` socket selector for every
	// agent-deck tmux spawn (v1.7.50+, issue #687). Empty string — the
	// default — keeps pre-v1.7.50 behavior byte-for-byte: agent-deck shares
	// the user's default tmux server at $TMUX_TMPDIR/tmux-<uid>/default.
	//
	// Set this to isolate agent-deck onto its own tmux server so:
	//   - `[tmux].inject_status_line`, bind-key, and global set-option
	//     mutations stay on the agent-deck server and never touch the
	//     user's interactive tmux config (the original #276 complaint);
	//   - a `tmux kill-server` in the user's shell can't take agent-deck's
	//     managed sessions down with it;
	//   - `tmux -L <name> ls` from the shell shows exactly agent-deck's
	//     sessions — no mixing with the user's own work sessions.
	//
	// Each Instance captures this value at creation time into
	// Instance.TmuxSocketName; changing socket_name later does NOT migrate
	// existing sessions (they remain reachable on their original socket
	// until explicitly re-created). See docs/SOCKET_ISOLATION.md for the
	// migration procedure.
	//
	// Precedence at Instance creation: CLI flag `--tmux-socket <name>`
	// wins, else this config value, else empty.
	SocketName string `toml:"socket_name"`
}

TmuxSettings allows users to override tmux options applied to every session. Options are applied AFTER agent-deck's defaults, so they take precedence.

Example config.toml:

[tmux]
inject_status_line = false
options = { "allow-passthrough" = "all", "history-limit" = "50000" }

func GetTmuxSettings added in v0.12.1

func GetTmuxSettings() TmuxSettings

GetTmuxSettings returns tmux option overrides from config

func (TmuxSettings) GetInjectStatusLine added in v0.15.0

func (t TmuxSettings) GetInjectStatusLine() bool

GetInjectStatusLine returns whether to inject status line, defaulting to true.

func (TmuxSettings) GetLaunchAs added in v1.7.21

func (t TmuxSettings) GetLaunchAs() string

GetLaunchAs returns the canonicalised launch mode string parsed from config.toml's tmux.launch_as key. Returns "" if the field is unset or contains an unknown value (in which case downstream callers fall back to LaunchInUserScope). v1.7.21+.

func (TmuxSettings) GetLaunchInUserScope added in v1.3.1

func (t TmuxSettings) GetLaunchInUserScope() bool

GetLaunchInUserScope returns whether new tmux servers should be launched under the user's systemd manager. If LaunchInUserScope is non-nil (explicit override in config.toml), its value is returned. Otherwise the default is determined by isSystemdUserScopeAvailable(): true on Linux+systemd hosts, false elsewhere. PERSIST-01..PERSIST-03.

func (TmuxSettings) GetMouse added in v1.7.68

func (t TmuxSettings) GetMouse() bool

GetMouse returns whether tmux mouse mode should be enabled, defaulting to true. Issue #730: users on VS Code's Linux integrated terminal need mouse OFF so the terminal can handle click-drag selection natively.

func (TmuxSettings) GetSocketName added in v1.7.50

func (t TmuxSettings) GetSocketName() string

GetSocketName returns the trimmed `[tmux].socket_name` value, or "" when unset, whitespace-only, or absent. Centralising the trim here means every caller — tmux.SetDefaultSocketName at startup, CLI flag merging, Instance creation — sees the same sanitised value.

type ToolCall added in v0.8.27

type ToolCall struct {
	Name  string `json:"name"`
	Count int    `json:"count"`
}

ToolCall represents a tool and its usage count

type ToolDef added in v0.3.0

type ToolDef struct {
	// Command is the shell command to run
	Command string `toml:"command"`

	// CompatibleWith opts this tool into compatibility behavior for a built-in
	// tool even when the configured command is a wrapper script rather than the
	// literal executable name. Supported values currently include "claude" and
	// "codex".
	CompatibleWith string `toml:"compatible_with"`

	// Wrapper is an optional command that wraps the tool command.
	// Use {command} placeholder to include the tool command, or omit it to replace the command.
	// Example: wrapper = "nvim +'terminal {command}' +'startinsert'"
	Wrapper string `toml:"wrapper"`

	// Icon is the emoji/symbol to display
	Icon string `toml:"icon"`

	// BusyPatterns are strings that indicate the tool is busy
	BusyPatterns []string `toml:"busy_patterns"`

	// PromptPatterns are strings that indicate the tool is waiting for input
	PromptPatterns []string `toml:"prompt_patterns"`

	// DetectPatterns are regex patterns to auto-detect this tool from terminal content
	DetectPatterns []string `toml:"detect_patterns"`

	// ResumeFlag is the CLI flag to resume a session (e.g., "--resume")
	ResumeFlag string `toml:"resume_flag"`

	// SessionIDEnv is the tmux environment variable name storing the session ID
	SessionIDEnv string `toml:"session_id_env"`

	// DangerousMode enables dangerous mode flag for this tool
	DangerousMode bool `toml:"dangerous_mode"`

	// DangerousFlag is the CLI flag for dangerous mode (e.g., "--dangerously-skip-permissions")
	DangerousFlag string `toml:"dangerous_flag"`

	// OutputFormatFlag is the CLI flag for JSON output format (e.g., "--output-format json")
	OutputFormatFlag string `toml:"output_format_flag"`

	// SessionIDJsonPath is the jq path to extract session ID from JSON output
	SessionIDJsonPath string `toml:"session_id_json_path"`

	// EnvFile is a .env file specific to this tool
	// Sourced AFTER global [shell].env_files
	// Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
	EnvFile string `toml:"env_file"`

	// Env is inline environment variables for this tool
	// These are exported AFTER env_file (highest priority)
	// Example: env = { ANTHROPIC_BASE_URL = "https://...", API_KEY = "token" }
	Env map[string]string `toml:"env"`

	// BusyPatternsExtra appends additional busy patterns to the built-in defaults
	BusyPatternsExtra []string `toml:"busy_patterns_extra"`

	// PromptPatternsExtra appends additional prompt patterns to the built-in defaults
	PromptPatternsExtra []string `toml:"prompt_patterns_extra"`

	// SpinnerChars replaces the default spinner characters entirely (use with caution)
	SpinnerChars []string `toml:"spinner_chars"`

	// SpinnerCharsExtra appends additional spinner characters to the built-in defaults
	SpinnerCharsExtra []string `toml:"spinner_chars_extra"`
}

ToolDef defines a custom AI tool

func GetToolDef added in v0.3.0

func GetToolDef(toolName string) *ToolDef

GetToolDef returns a tool definition from user config Returns nil if tool is not defined

type ToolOptions added in v0.8.39

type ToolOptions interface {
	// ToolName returns the name of the tool (e.g., "claude", "codex")
	ToolName() string
	// ToArgs returns command-line arguments for the tool
	ToArgs() []string
}

ToolOptions is the interface for tool-specific launch options Each AI tool (claude, codex, gemini, etc.) can have its own options struct that implements this interface

type ToolOptionsWrapper added in v0.8.39

type ToolOptionsWrapper struct {
	Tool    string          `json:"tool"`
	Options json.RawMessage `json:"options"`
}

ToolOptionsWrapper wraps tool options for JSON serialization JSON structure: {"tool": "claude", "options": {...}}

type TransitionDaemon added in v0.19.13

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

func NewTransitionDaemon added in v0.19.13

func NewTransitionDaemon() *TransitionDaemon

func (*TransitionDaemon) Flush added in v1.7.45

func (d *TransitionDaemon) Flush()

Flush exposes the notifier's in-flight-dispatch wait for callers of SyncOnce that need deterministic log output before returning (e.g., the `agent-deck notify-daemon --once` CLI path).

func (*TransitionDaemon) Run added in v0.19.13

func (d *TransitionDaemon) Run(ctx context.Context) error

func (*TransitionDaemon) SyncOnce added in v0.19.13

func (d *TransitionDaemon) SyncOnce(_ context.Context) time.Duration

SyncOnce performs one full monitoring pass and returns the recommended delay until the next pass.

type TransitionNotificationEvent added in v0.19.13

type TransitionNotificationEvent struct {
	ChildSessionID string    `json:"child_session_id"`
	ChildTitle     string    `json:"child_title"`
	Profile        string    `json:"profile"`
	FromStatus     string    `json:"from_status"`
	ToStatus       string    `json:"to_status"`
	Timestamp      time.Time `json:"timestamp"`

	// LastOutputHash is a cheap stable signal (e.g. SHA-1 of the last N
	// bytes of the child's tmux pane at transition time) used by the
	// notifier's #1142 dedup to suppress repeated [EVENT] notifications
	// for a dormant child whose pane content hasn't changed. Optional —
	// empty string disables hash-based dedup and falls back to the legacy
	// 90s short window.
	LastOutputHash string `json:"last_output_hash,omitempty"`

	TargetSessionID string `json:"target_session_id,omitempty"`
	TargetKind      string `json:"target_kind,omitempty"` // parent | conductor
	DeliveryResult  string `json:"delivery_result,omitempty"`
}

func ReadAndTruncateInbox added in v1.7.73

func ReadAndTruncateInbox(parentSessionID string) ([]TransitionNotificationEvent, error)

ReadAndTruncateInbox reads all events from the parent's inbox and removes the file. Returns an empty slice (not an error) when the inbox doesn't exist or holds no parseable lines.

The read+truncate pair is not atomic against a concurrent writer: a write that lands between os.Open and os.Remove is lost. This is acceptable for the conductor's expected drain cadence (seconds) but documented so callers don't expect at-least-once semantics across producer/consumer races. When strict atomicity matters, callers should externally serialize.

type TransitionNotifier added in v0.19.13

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

func NewTransitionNotifier added in v0.19.13

func NewTransitionNotifier() *TransitionNotifier

func (*TransitionNotifier) Close added in v1.7.73

func (n *TransitionNotifier) Close()

Close cancels any pending in-process busy retries. Production callers do not need it because the daemon process owns the notifier for its lifetime; tests use it to stop scheduleBusyRetry goroutines from writing to inbox files after t.TempDir cleanup. Idempotent.

func (*TransitionNotifier) DrainRetryQueue added in v1.7.45

func (n *TransitionNotifier) DrainRetryQueue(profile string)

DrainRetryQueue is the production entry point used by the daemon's poll loop. It resolves target availability by reading the live session state.

func (*TransitionNotifier) DrainRetryQueueWithResolver added in v1.7.45

func (n *TransitionNotifier) DrainRetryQueueWithResolver(profile string, isAvailable targetAvailabilityResolver)

DrainRetryQueueWithResolver is the test seam. It walks the queue, dispatching entries whose target is now available and expiring entries older than defaultQueueMaxAge or past defaultQueueMaxAttempts.

func (*TransitionNotifier) EnqueueDeferred added in v1.7.45

func (n *TransitionNotifier) EnqueueDeferred(event TransitionNotificationEvent)

EnqueueDeferred persists a deferred event so the next DrainRetryQueue pass can try delivery again once the target is free. Events keyed by (child, from, to) de-duplicate: a repeat defer for the same transition refreshes the event but keeps FirstDeferredAt so the age-out timer is honest.

func (*TransitionNotifier) Flush added in v1.7.45

func (n *TransitionNotifier) Flush()

Flush waits for every pending async dispatch to resolve (sent, failed, or timed out) so that callers with a bounded lifetime — the `notify-daemon --once` CLI entry point, the graceful-shutdown path of Run, and any test that needs deterministic log contents — can observe the real delivery outcome before exiting. Bounded by sendTimeout for watchers plus any outstanding sender goroutines that finish within the same window.

func (*TransitionNotifier) NotifyTransition added in v0.19.13

NotifyTransition validates the event, resolves the delivery target, and schedules an async send. Synchronous returns: dropped / deferred. Async returns: dispatching (final sent/failed/timeout is written to logs from the send goroutine). Deferred events are persisted to the retry queue so the next daemon poll can try again when the target is free — this is the v1.7.45 fix for the silent-loss bug where the daemon's lastStatus update permanently masked deferred transitions.

type UISettings added in v1.9.23

type UISettings struct {
	// PreviewPct is the percentage of horizontal width allocated to the
	// preview pane (sessions list gets the remainder). Valid range: 10-90.
	// Default: 65 (current behavior — sessions 35 / preview 65).
	// Adjustable at runtime via < and > keybindings (5% step).
	PreviewPct int `toml:"preview_pct"`

	// ITermOpenAs controls whether Shift+Enter pops the focused session
	// into a new iTerm2 *tab* or a new iTerm2 *window* on macOS. Valid
	// values: "tab", "window". Empty defaults to "tab" (iTerm's natural
	// UX). Issue #1100, follow-up to #1098 — credit @ddorman-dn.
	ITermOpenAs string `toml:"iterm_open_as"`
	// RemoteLatencyRefreshSecs sets how often the TUI re-measures the
	// round-trip latency to each configured remote (issue #1103). Valid
	// range: 2-300. Default: matches [system_stats].refresh_seconds (5s)
	// so the latency marker ticks alongside CPU/RAM/load.
	RemoteLatencyRefreshSecs int `toml:"remote_latency_refresh_secs"`
}

UISettings controls TUI layout proportions. See issue #1092.

func (UISettings) GetITermOpenAs added in v1.9.24

func (u UISettings) GetITermOpenAs() string

GetITermOpenAs returns the configured iTerm open mode. Unknown or empty values fall through to the default ("tab"). Matching is case-insensitive so users can write "Tab" or "WINDOW" in TOML.

func (UISettings) GetPreviewPct added in v1.9.23

func (u UISettings) GetPreviewPct() int

GetPreviewPct returns the configured preview percentage, clamped to [MinPreviewPct, MaxPreviewPct]. Falls back to DefaultPreviewPct when unset or out of range.

func (UISettings) GetRemoteLatencyRefreshSecs added in v1.9.24

func (u UISettings) GetRemoteLatencyRefreshSecs(fallbackSecs int) int

GetRemoteLatencyRefreshSecs returns the remote latency refresh interval in seconds, clamped to [2, 300]. When the user has not set this value it falls back to fallbackSecs (typically the system_stats refresh interval, so the latency marker ticks at the same cadence as CPU/RAM per #1103). fallbackSecs <= 0 maps to 5.

type UpdateSettings added in v0.8.2

type UpdateSettings struct {
	// AutoUpdate automatically installs updates without prompting
	// Default: false
	AutoUpdate bool `toml:"auto_update"`

	// CheckEnabled enables automatic update checks on startup
	// Default: true
	CheckEnabled bool `toml:"check_enabled"`

	// CheckIntervalHours is how often to check for updates (in hours)
	// Default: 24
	CheckIntervalHours int `toml:"check_interval_hours"`

	// NotifyInCLI shows update notification in CLI commands (not just TUI)
	// Default: true
	NotifyInCLI bool `toml:"notify_in_cli"`
}

UpdateSettings defines auto-update configuration

func GetUpdateSettings added in v0.8.2

func GetUpdateSettings() UpdateSettings

GetUpdateSettings returns update settings with defaults applied

type UserConfig added in v0.3.0

type UserConfig struct {
	// DefaultTool is the pre-selected AI tool when creating new sessions
	// Valid values: "claude", "gemini", "opencode", "codex", "pi", or any custom tool name
	// If empty or invalid, defaults to "shell" (no pre-selection)
	DefaultTool string `toml:"default_tool"`

	// Hotkeys overrides default keyboard shortcuts in the TUI.
	// Keys are action names, values are key bindings (e.g., "delete" = "backspace").
	// Set an action to "" to explicitly unbind it.
	Hotkeys map[string]string `toml:"hotkeys"`

	// Theme sets the color scheme: "dark" (default), "light", or "system"
	Theme string `toml:"theme"`

	// Tools defines custom AI tool configurations
	Tools map[string]ToolDef `toml:"tools"`

	// MCPDefaultScope sets the default scope for MCP operations
	// Valid values: "local" (default), "global", "user"
	MCPDefaultScope string `toml:"mcp_default_scope"`

	// ManageMCPJson controls whether agent-deck writes to .mcp.json in project directories.
	// Set to false to prevent agent-deck from touching any .mcp.json files, which is useful
	// when you manage that file manually or via another tool.
	// Default: true (nil = true)
	ManageMCPJson *bool `toml:"manage_mcp_json"`

	// MCPs defines available MCP servers for the MCP Manager
	// These can be attached/detached per-project via the MCP Manager (M key)
	MCPs map[string]MCPDef `toml:"mcps"`

	// Plugins defines available Claude Code plugins for per-session attach
	// (RFC docs/rfc/PLUGIN_ATTACH.md). Catalog-only in v1: every name passed
	// via `--plugin <name>` must resolve to an entry here. Each entry maps a
	// short catalog name (e.g. "octopus") to a Claude Code plugin id
	// (`<name>@<source>`) plus per-plugin policy (auto-install, channel link).
	Plugins map[string]PluginDef `toml:"plugins"`

	// Claude defines Claude Code integration settings
	Claude ClaudeSettings `toml:"claude"`

	// Profiles defines optional per-profile overrides.
	// Example:
	// [profiles.work.claude]
	// config_dir = "~/.claude-work"
	Profiles map[string]ProfileSettings `toml:"profiles"`

	// Groups defines optional per-group overrides.
	// Example:
	// [groups."my-group".claude]
	// config_dir = "~/.claude-my-group"
	Groups map[string]GroupSettings `toml:"groups"`

	// Conductors defines optional per-conductor overrides.
	// Keyed by conductor name (matches Instance.Title minus "conductor-" prefix).
	// Mirrors Groups — see ConductorOverrides for the sub-table shape.
	// Closes issue #602.
	// Example:
	// [conductors.gsd-v154.claude]
	// config_dir = "~/.claude-work"
	// env_file   = "~/git/work/.envrc"
	Conductors map[string]ConductorOverrides `toml:"conductors"`

	// Gemini defines Gemini CLI integration settings
	Gemini GeminiSettings `toml:"gemini"`

	// OpenCode defines OpenCode CLI integration settings
	OpenCode OpenCodeSettings `toml:"opencode"`

	// Codex defines Codex CLI integration settings
	Codex CodexSettings `toml:"codex"`

	// Copilot defines GitHub Copilot CLI integration settings (Issue #556)
	Copilot CopilotSettings `toml:"copilot"`

	// Crush defines charmbracelet/crush CLI integration settings (Issue #940)
	Crush CrushSettings `toml:"crush"`

	// Hermes defines Hermes Agent CLI integration settings
	Hermes HermesSettings `toml:"hermes"`

	// Worktree defines git worktree preferences
	Worktree WorktreeSettings `toml:"worktree"`

	// GlobalSearch defines global conversation search settings
	GlobalSearch GlobalSearchSettings `toml:"global_search"`

	// Logs defines session log management settings
	Logs LogSettings `toml:"logs"`

	// MCPPool defines HTTP MCP pool settings for shared MCP servers
	MCPPool MCPPoolSettings `toml:"mcp_pool"`

	// Updates defines auto-update settings
	Updates UpdateSettings `toml:"updates"`

	// Preview defines preview pane display settings
	Preview PreviewSettings `toml:"preview"`

	// Experiments defines experiment folder settings for 'try' command
	Experiments ExperimentsSettings `toml:"experiments"`

	// Notifications defines waiting session notification bar settings
	Notifications NotificationsConfig `toml:"notifications"`

	// Instances defines multiple instance behavior settings
	Instances InstanceSettings `toml:"instances"`

	// Shell defines global shell environment settings for sessions
	Shell ShellSettings `toml:"shell"`

	// Maintenance defines automatic maintenance worker settings
	Maintenance MaintenanceSettings `toml:"maintenance"`

	// Status defines session status detection settings
	Status StatusSettings `toml:"status"`

	// Conductor defines conductor (meta-agent orchestration) settings
	Conductor ConductorSettings `toml:"conductor"`

	// Tmux defines tmux option overrides applied to every session
	Tmux TmuxSettings `toml:"tmux"`

	// Docker defines Docker sandbox settings for containerized sessions
	Docker DockerSettings `toml:"docker"`

	// Remotes defines named SSH remote agent-deck instances
	Remotes map[string]RemoteConfig `toml:"remotes"`

	// OpenClaw defines OpenClaw gateway integration settings
	OpenClaw OpenClawSettings `toml:"openclaw"`

	// Display defines rendering and display settings
	Display DisplaySettings `toml:"display"`

	// Costs defines cost tracking and budget settings
	Costs CostsSettings `toml:"costs"`

	// SystemStats defines system stats display settings (CPU, RAM, etc.)
	SystemStats SystemStatsSettings `toml:"system_stats"`

	// Watcher defines event watcher settings
	Watcher WatcherSettings `toml:"watcher"`

	// Feedback defines in-product feedback prompt settings (v1.7.38+).
	// Mirrors the opt-out in ~/.agent-deck/feedback-state.json so it is visible
	// to the user and editable without running `agent-deck feedback`.
	Feedback FeedbackSettings `toml:"feedback"`

	// Terminal defines outer-terminal chrome settings — sequences agent-deck
	// writes directly to the host terminal (iTerm2 badge, etc), distinct
	// from anything tmux draws. Empty/absent uses defaults; see TerminalSettings.
	Terminal TerminalSettings `toml:"terminal"`

	// Web defines `agent-deck web` HTTP server settings.
	Web WebSettings `toml:"web"`

	// UI defines TUI layout settings (split ratios, etc).
	UI UISettings `toml:"ui"`
}

UserConfig represents user-facing configuration in TOML format

func LoadUserConfig added in v0.3.0

func LoadUserConfig() (*UserConfig, error)

LoadUserConfig loads the user configuration from TOML file. After the first load the result is cached; the cache is invalidated when config.toml's mtime advances, so external edits to the file (e.g. the user editing ~/.agent-deck/config.toml by hand while the TUI is running) are picked up on the next call without a manual ClearUserConfigCache.

func MergePanelConfigOntoDisk added in v1.9.21

func MergePanelConfigOntoDisk(panel *UserConfig) (*UserConfig, error)

MergePanelConfigOntoDisk loads the on-disk UserConfig and overlays the subset of fields that the TUI settings panel and setup wizard manage. Every other top-level field — Remotes, Hotkeys, Plugins, Conductors, Groups, Notifications, OpenClaw, Costs, Watcher, Shell, etc. — is preserved verbatim from disk.

Issue #1067 fix. Previously, SettingsPanel.GetConfig and SetupWizard.GetConfig built fresh UserConfig values and home.go saved them directly. Any top-level field the panel did not explicitly copy from originalConfig was silently wiped — the reporter saw this as "remotes disappeared after Ctrl+C exit" because the most common path that triggers the save is opening Settings during a session.

The fix inverts the data flow: instead of "construct fresh + manually preserve a few fields", we "start from disk + overlay panel-managed fields". New top-level UserConfig fields are now safe-by-default — they survive panel saves unless explicitly listed here.

Callers (home.go settings + setup wizard paths) should replace

if err := SaveUserConfig(panel.GetConfig()); err != nil { ... }

with

merged, err := session.MergePanelConfigOntoDisk(panel.GetConfig())
if err == nil { _ = session.SaveUserConfig(merged) }

to inherit the preservation guarantee.

func ReloadUserConfig added in v0.3.0

func ReloadUserConfig() (*UserConfig, error)

ReloadUserConfig forces a reload of the user config

func (*UserConfig) GetConductorClaudeConfigDir added in v1.5.4

func (c *UserConfig) GetConductorClaudeConfigDir(name string) string

GetConductorClaudeConfigDir returns the conductor-specific Claude config directory, if configured. Keyed by conductor name (Instance.Title minus "conductor-" prefix — single source of truth is conductorNameFromInstance in claude.go). Path expansion matches GetGroupClaudeConfigDir. Returns "" when the conductor has no block or no config_dir — callers fall through to the group/profile/global chain.

func (*UserConfig) GetConductorClaudeEnvFile added in v1.5.4

func (c *UserConfig) GetConductorClaudeEnvFile(name string) string

GetConductorClaudeEnvFile returns the conductor-specific Claude env_file, if configured. Mirrors GetGroupClaudeEnvFile — no expansion here; resolvePath handles it at the spawn-command build site (env.go).

func (*UserConfig) GetGroupClaudeConfigDir added in v1.5.4

func (c *UserConfig) GetGroupClaudeConfigDir(groupPath string) string

GetGroupClaudeConfigDir returns the group-specific Claude config directory, walking ancestor groups when the exact path has no override. A child group like "personal/foo" inherits the [groups."personal".claude].config_dir setting from its parent so per-group account isolation propagates through nested groups.

func (*UserConfig) GetGroupClaudeEnvFile added in v1.5.4

func (c *UserConfig) GetGroupClaudeEnvFile(groupPath string) string

GetGroupClaudeEnvFile returns the group-specific Claude env file, walking ancestor groups when the exact path has no override. Mirrors GetGroupClaudeConfigDir's inheritance semantics so nested groups don't silently drop the parent's env_file.

func (*UserConfig) GetProfileClaudeConfigDir added in v0.19.1

func (c *UserConfig) GetProfileClaudeConfigDir(profile string) string

GetProfileClaudeConfigDir returns the profile-specific Claude config directory, if configured.

func (*UserConfig) GetProfileCodexConfigDir added in v1.9.18

func (c *UserConfig) GetProfileCodexConfigDir(profile string) string

GetProfileCodexConfigDir returns the profile-specific Codex config directory, if configured.

func (*UserConfig) GetShowAnalytics added in v0.8.27

func (c *UserConfig) GetShowAnalytics() bool

GetShowAnalytics returns whether to show analytics panel, defaulting to false

func (*UserConfig) GetShowNotes added in v0.25.0

func (c *UserConfig) GetShowNotes() bool

GetShowNotes returns whether to show notes section, defaulting to false

func (*UserConfig) GetShowOutput added in v0.8.27

func (c *UserConfig) GetShowOutput() bool

GetShowOutput returns whether to show terminal output in preview

type WatcherAlertsSettings added in v1.6.0

type WatcherAlertsSettings struct {
	// Enabled turns the bridge on. Default: false (no alerts emitted).
	Enabled bool `toml:"enabled"`

	// Channels lists notification channel names the bridge's notifier should fan out to
	// (e.g. "telegram", "slack", "discord"). Semantics are owned by the Notifier
	// implementation; the bridge only passes the list to the notifier.
	Channels []string `toml:"channels"`

	// DebounceMinutes is the per-(watcher x trigger) debounce window. Default: 15.
	DebounceMinutes int `toml:"debounce_minutes"`
}

WatcherAlertsSettings configures the health alerts bridge (REQ-WF-3). Opt-in via [watcher.alerts] in config.toml.

func (WatcherAlertsSettings) GetDebounceMinutes added in v1.6.0

func (a WatcherAlertsSettings) GetDebounceMinutes() int

GetDebounceMinutes returns the debounce window in minutes (default: 15).

type WatcherMeta added in v1.5.1

type WatcherMeta struct {
	Name           string `json:"name"`
	Type           string `json:"type"`                       // adapter type: "webhook", "ntfy", "github", "slack", "gmail"
	CreatedAt      string `json:"created_at"`                 // RFC3339 timestamp
	WatchExpiry    string `json:"watch_expiry,omitempty"`     // RFC3339 UTC (gmail only) — Gmail watch() expiration
	WatchHistoryID string `json:"watch_history_id,omitempty"` // uint64 as string (gmail only) — last processed Gmail history ID
}

WatcherMeta holds metadata for a named watcher instance. Persisted as meta.json in ~/.agent-deck/watcher/<name>/.

func LoadWatcherMeta added in v1.5.1

func LoadWatcherMeta(name string) (*WatcherMeta, error)

LoadWatcherMeta reads meta.json for a named watcher.

type WatcherSettings added in v1.5.1

type WatcherSettings struct {
	// MaxEventsPerWatcher is the maximum number of events to retain per watcher (default: 500)
	MaxEventsPerWatcher int `toml:"max_events_per_watcher"`

	// MaxSilenceMinutes triggers a health warning when no events received (default: 60)
	MaxSilenceMinutes int `toml:"max_silence_minutes"`

	// HealthCheckIntervalSeconds is the interval between health checks in seconds (default: 30)
	HealthCheckIntervalSeconds int `toml:"health_check_interval_seconds"`

	// Alerts configures the health alerts bridge (opt-in). See WatcherAlertsSettings.
	Alerts WatcherAlertsSettings `toml:"alerts"`
}

WatcherSettings configures the event watcher system.

func (WatcherSettings) GetHealthCheckIntervalSeconds added in v1.5.1

func (w WatcherSettings) GetHealthCheckIntervalSeconds() int

GetHealthCheckIntervalSeconds returns the health check interval in seconds (default: 30).

func (WatcherSettings) GetMaxEventsPerWatcher added in v1.5.1

func (w WatcherSettings) GetMaxEventsPerWatcher() int

GetMaxEventsPerWatcher returns the max events per watcher (default: 500).

func (WatcherSettings) GetMaxSilenceMinutes added in v1.5.1

func (w WatcherSettings) GetMaxSilenceMinutes() int

GetMaxSilenceMinutes returns the silence threshold in minutes (default: 60).

type WebSettings added in v1.7.75

type WebSettings struct {
	// MutationsEnabled controls whether POST/PATCH/DELETE endpoints accept
	// requests. nil (omitted) defaults to true. Forced off by --read-only.
	MutationsEnabled *bool `toml:"mutations_enabled"`
}

WebSettings configures the `agent-deck web` HTTP server.

type WorktreeSettings added in v0.8.22

type WorktreeSettings struct {
	// AutoCleanup: remove worktree when session is deleted
	AutoCleanup bool `toml:"auto_cleanup"`

	// DefaultEnabled controls whether worktree creation is pre-selected in
	// new-session and fork dialogs by default.
	// Default: false
	DefaultEnabled bool `toml:"default_enabled"`

	// DefaultLocation: "sibling" (next to repo), "subdirectory" (inside .worktrees/),
	// or a custom path (e.g., "~/worktrees") creating <path>/<repo_name>/<branch>
	DefaultLocation string `toml:"default_location"`

	// PathTemplate: custom path template for worktree location.
	// Variables:
	//   {repo-name}, {repo-root}, {session-id}
	//   {branch}         -> sanitized (human-friendly, may collide)
	//   {branch-escaped} -> URL-escaped (collision-resistant, reversible)
	// Unknown variables like {foo} are left as-is in the path.
	// If set, overrides DefaultLocation.
	PathTemplate *string `toml:"path_template"`

	// BranchPrefix is the prefix for auto-generated branch names when creating
	// worktree sessions. For example, "feature/" produces "feature/my-session".
	// Set to "" to disable auto-prefixing (just the session name).
	// Default: "feature/" when not set.
	BranchPrefix *string `toml:"branch_prefix"`

	// SetupTimeoutSeconds caps how long .agent-deck/worktree-setup.sh may run.
	// Pointer (not plain int) so the loader can distinguish three cases:
	//   nil         → field unset → 60s default (backward compat, GH #724)
	//   *0          → explicit unlimited (no deadline) — #727 follow-up
	//   *N (N > 0)  → N seconds
	//   *N (N < 0)  → treated as unset (60s default)
	// The `*0 = unlimited` convention matches standard CLI tooling (curl,
	// systemd, docker). Reporter @Clindbergh flagged the v1.7.65 behaviour
	// (`0 = default`) as counter-convention in the PR review for #727.
	SetupTimeoutSeconds *int `toml:"setup_timeout_seconds"`
}

WorktreeSettings contains git worktree preferences.

func GetWorktreeSettings added in v0.8.22

func GetWorktreeSettings() WorktreeSettings

GetWorktreeSettings returns worktree settings with defaults applied

func (*WorktreeSettings) ApplyBranchPrefix added in v1.7.2

func (w *WorktreeSettings) ApplyBranchPrefix(branch string) string

ApplyBranchPrefix prepends the configured prefix to a branch name. If the branch name already starts with the expanded prefix, it is returned unchanged.

func (*WorktreeSettings) Prefix added in v0.21.0

func (w *WorktreeSettings) Prefix() string

Prefix returns the branch prefix if set, or "feature/" if nil. Environment variables (e.g., $USER) in the prefix are expanded.

func (WorktreeSettings) SetupTimeout added in v1.7.65

func (w WorktreeSettings) SetupTimeout() time.Duration

SetupTimeout returns the configured worktree-setup-script timeout. Semantics (post-#727 follow-up):

  • field unset (nil) or negative → DefaultWorktreeSetupTimeout (60s)
  • explicit 0 → UnlimitedWorktreeSetupTimeout (no deadline)
  • positive N → N seconds

func (*WorktreeSettings) Template added in v0.10.10

func (w *WorktreeSettings) Template() string

Template returns the path template if set, or empty string if nil.

Jump to

Keyboard shortcuts

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