Help us shape the future of Go! Take the 2026 Go Developer Survey

session

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Feb 27, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Index

Constants

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 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 UserConfigFileName = "config.toml"

UserConfigFileName is the TOML config file for user preferences

Variables

This section is empty.

Functions

func AddProject

func AddProject(name, baseDir, baseBranch string) error

AddProject adds a new project. Returns error if name already exists.

func CheckClaudeHooksInstalled

func CheckClaudeHooksInstalled(configDir string) bool

CheckClaudeHooksInstalled checks if hangar hooks are present in settings.json.

func CleanStaleEventFiles

func CleanStaleEventFiles()

CleanStaleEventFiles removes event files older than 24 hours.

func ClearMCPCache

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

func ClearProjectMCPs(projectPath string) error

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

func ClearUserConfigCache

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

func ConductorSessionTitle

func ConductorSessionTitle(name string) string

ConductorSessionTitle returns the tmux session title for a conductor (stub).

func ConvertToClaudeDirName

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 CreateExampleConfig

func CreateExampleConfig() error

CreateExampleConfig creates an example config file if none exists

func CreateProfile

func CreateProfile(profile string) error

CreateProfile creates a new empty profile

func DeleteProfile

func DeleteProfile(profile string) error

DeleteProfile deletes a profile and all its data

func DetectBaseBranch

func DetectBaseBranch(repoPath string) string

DetectBaseBranch auto-detects the default branch for a git repo. Tries: git symbolic-ref refs/remotes/origin/HEAD, then falls back to "main" or "master".

func ExpandPath

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 GenerateSessionName

func GenerateSessionName() string

GenerateSessionName returns a random "adjective-noun" name.

func GenerateUniqueSessionName

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 GetAvailableGeminiModels

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 GetAvailableMCPs

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 GetClaudeCommand

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

func GetClaudeConfigDir() string

GetClaudeConfigDir returns the Claude config directory for the active profile. Priority: 1. CLAUDE_CONFIG_DIR env var 2. profile-specific override: [profiles.<profile>.claude].config_dir 3. global setting: [claude].config_dir 4. default: ~/.claude

func GetClaudeSessionID

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 GetConfigPath

func GetConfigPath() (string, error)

GetConfigPath returns the path to the global config file

func GetCustomToolNames

func GetCustomToolNames() []string

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

func GetDBPathForProfile

func GetDBPathForProfile(profile string) (string, error)

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

func GetDefaultTool

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

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

func GetEffectiveProfile(explicit string) string

GetEffectiveProfile returns the profile to use, considering: 1. Explicitly provided profile (from -p flag) 2. Environment variable HANGAR_PROFILE 3. Config default profile 4. Fallback to "default"

func GetEventsDir

func GetEventsDir() string

GetEventsDir returns the path to the events directory.

func GetGeminiConfigDir

func GetGeminiConfigDir() string

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

func GetGeminiMCPNames

func GetGeminiMCPNames() []string

GetGeminiMCPNames returns names of configured MCPs from settings.json

func GetGeminiSessionsDir

func GetGeminiSessionsDir(projectPath string) string

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

func GetGlobalMCPNames

func GetGlobalMCPNames() []string

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

func GetGroupLevel

func GetGroupLevel(path string) int

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

func GetHangarDir

func GetHangarDir() (string, error)

GetHangarDir returns the base hangar directory (~/.hangar)

func GetHooksDir

func GetHooksDir() string

GetHooksDir returns the path to the hooks status directory.

func GetMCPDefaultScope

func GetMCPDefaultScope() string

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

func GetManageMCPJson

func GetManageMCPJson() bool

GetManageMCPJson returns whether hangar should write to .mcp.json files. Defaults to true when unset.

func GetProfileDir

func GetProfileDir(profile string) (string, error)

GetProfileDir returns the path to a specific profile's directory

func GetProfilesDir

func GetProfilesDir() (string, error)

GetProfilesDir returns the path to the profiles directory

func GetProjectMCPNames

func GetProjectMCPNames(projectPath string) []string

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

func GetProjectsFilePath

func GetProjectsFilePath() (string, error)

GetProjectsFilePath returns ~/.hangar/projects.toml

func GetTheme

func GetTheme() string

GetTheme returns the current theme, defaulting to "dark"

func GetToolBusyPatterns

func GetToolBusyPatterns(toolName string) []string

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

func GetToolIcon

func GetToolIcon(toolName string) string

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

func GetUserConfigPath

func GetUserConfigPath() (string, error)

GetUserConfigPath returns the path to the user config file

func GetUserMCPNames

func GetUserMCPNames() []string

GetUserMCPNames returns the names of MCPs in ~/.claude.json (ROOT config)

func GetUserMCPRootPath

func GetUserMCPRootPath() string

GetUserMCPRootPath returns the path to ~/.claude.json (ROOT config, always read by Claude)

func GroupByProject

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

GroupByProject groups sessions by their parent project directory

func HashProjectPath

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 InjectClaudeHooks

func InjectClaudeHooks(configDir string) (bool, error)

InjectClaudeHooks injects hangar 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 InstallBridgeScript

func InstallBridgeScript() error

InstallBridgeScript is a stub; conductor bridge is no longer installed.

func IsClaudeConfigDirExplicit

func IsClaudeConfigDirExplicit() bool

IsClaudeConfigDirExplicit returns true if the Claude config directory is explicitly configured (via CLAUDE_CONFIG_DIR env var, profile override, or global config.toml setting). When false, the user is using the default path and we should NOT override CLAUDE_CONFIG_DIR in commands, allowing the shell's environment to be respected.

This is critical for WSL and other environments where users may have CLAUDE_CONFIG_DIR set in their .bashrc/.zshrc - hangar should not override that with a hardcoded default path.

func ListProfiles

func ListProfiles() ([]string, error)

ListProfiles returns all available profile names

func MarshalToolOptions

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

MarshalToolOptions serializes tool options to JSON

func MergeToolPatterns

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 NeedsMigration

func NeedsMigration() (bool, error)

NeedsMigration checks if migration from old layout is needed

func ProfileExists

func ProfileExists(profile string) (bool, error)

ProfileExists checks if a profile exists

func PruneMCPCache

func PruneMCPCache(maxAge time.Duration)

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

func ReadPrevEventStatus

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 RemoveClaudeHooks

func RemoveClaudeHooks(configDir string) (bool, error)

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

func RemoveProject

func RemoveProject(name string) error

RemoveProject removes a project by name.

func RenameProject

func RenameProject(oldName, newName string) error

RenameProject renames a project from oldName to newName, preserving all other fields (BaseDir, BaseBranch, Order). Matching is case-insensitive. Returns an error if the project is not found.

func ResolveTheme

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

func RestoreFromArchive(baseDir string) error

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

func SaveConfig

func SaveConfig(config *Config) error

SaveConfig saves the global configuration

func SaveProjects

func SaveProjects(projects []*Project) error

SaveProjects writes the projects list atomically.

func SaveUserConfig

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 SendSessionMessageReliable

func SendSessionMessageReliable(profile, sessionRef, message string) error

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

func SetDefaultProfile

func SetDefaultProfile(profile string) error

SetDefaultProfile sets the default profile in the config

func ShouldNotifyTransition

func ShouldNotifyTransition(fromStatus, toStatus string) bool

func StartMaintenanceWorker

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 TierName

func TierName(tier SearchTier) string

TierName returns a human-readable name for the tier

func ToggleLocalMCP

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 UpdateClaudeSessionsWithDedup

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

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 WriteGeminiMCPSettings

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

func WriteGlobalMCP(enabledNames []string) error

WriteGlobalMCP adds or removes MCPs from Claude's global config

func WriteMCPJsonFromConfig

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 hangar (not defined in config.toml)

func WriteStatusEvent

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

func WriteUserMCP(enabledNames []string) error

WriteUserMCP writes MCPs to ~/.claude.json (ROOT config) WARNING: MCPs written here affect ALL Claude sessions regardless of profile!

Types

type AnalyticsDisplaySettings

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

func (a *AnalyticsDisplaySettings) GetShowContextBar() bool

GetShowContextBar returns whether to show context bar, defaulting to true

func (*AnalyticsDisplaySettings) GetShowCost

func (a *AnalyticsDisplaySettings) GetShowCost() bool

GetShowCost returns whether to show cost estimate, defaulting to false

func (*AnalyticsDisplaySettings) GetShowSessionInfo

func (a *AnalyticsDisplaySettings) GetShowSessionInfo() bool

GetShowSessionInfo returns whether to show session info, defaulting to false

func (*AnalyticsDisplaySettings) GetShowTokens

func (a *AnalyticsDisplaySettings) GetShowTokens() bool

GetShowTokens returns whether to show token breakdown, defaulting to false

func (*AnalyticsDisplaySettings) GetShowTools

func (a *AnalyticsDisplaySettings) GetShowTools() bool

GetShowTools returns whether to show tool calls, defaulting to false

type BillingBlock

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

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 ClaudeConfig

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

ClaudeConfig represents the structure of .claude.json

type ClaudeOptions

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

func NewClaudeOptions(config *UserConfig) *ClaudeOptions

NewClaudeOptions creates ClaudeOptions with defaults from config

func UnmarshalClaudeOptions

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

UnmarshalClaudeOptions deserializes ClaudeOptions from JSON wrapper

func (*ClaudeOptions) ToArgs

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

ToArgs returns command-line arguments based on options

func (*ClaudeOptions) ToArgsForFork

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

func (o *ClaudeOptions) ToolName() string

ToolName returns "claude"

type ClaudeProject

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

ClaudeProject represents a project entry in Claude's config

type ClaudeSettings

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

	// 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, hangar 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"`
}

ClaudeSettings defines Claude Code configuration

func (*ClaudeSettings) GetDangerousMode

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

func (c *ClaudeSettings) GetHooksEnabled() bool

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

type CodexOptions

type CodexOptions struct {
	// 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

func NewCodexOptions(config *UserConfig) *CodexOptions

NewCodexOptions creates CodexOptions with defaults from global config

func UnmarshalCodexOptions

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

UnmarshalCodexOptions deserializes CodexOptions from JSON wrapper

func (*CodexOptions) ToArgs

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

ToArgs returns command-line arguments based on options

func (*CodexOptions) ToolName

func (o *CodexOptions) ToolName() string

ToolName returns "codex"

type CodexSettings

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

CodexSettings defines Codex CLI configuration

type CompletionCycler

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

CompletionCycler manages the state of directory autocomplete.

func (*CompletionCycler) IsActive

func (c *CompletionCycler) IsActive() bool

IsActive returns true if the cycler has active matches.

func (*CompletionCycler) Next

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

func (c *CompletionCycler) Reset()

Reset clears the cycler state.

func (*CompletionCycler) SetMatches

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

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

type ConductorMeta

type ConductorMeta struct {
	Name              string `json:"name"`
	Profile           string `json:"profile"`
	HeartbeatEnabled  bool   `json:"heartbeat_enabled"`
	HeartbeatInterval int    `json:"heartbeat_interval"`
	Description       string `json:"description,omitempty"`
	CreatedAt         string `json:"created_at"`
}

ConductorMeta holds metadata about a conductor instance (stub; conductor is removed).

func ListConductorsForProfile

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

ListConductorsForProfile returns conductors for a profile (stub; always empty).

type ConductorSettings

type ConductorSettings struct {
	Enabled           bool             `toml:"enabled"`
	HeartbeatInterval int              `toml:"heartbeat_interval"`
	Profiles          []string         `toml:"profiles"`
	Telegram          TelegramSettings `toml:"telegram"`
	Slack             SlackSettings    `toml:"slack"`
}

ConductorSettings defines conductor (meta-agent orchestration) settings. Kept as a stub so config.toml files with [conductor] sections parse without errors.

type Config

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 hangar configuration

func LoadConfig

func LoadConfig() (*Config, error)

LoadConfig loads the global configuration

type ContentBuffer

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

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

func (*ContentBuffer) CopyData

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

func (*ContentBuffer) Size

func (b *ContentBuffer) Size() int64

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

func (*ContentBuffer) With

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

type ExperimentsSettings

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

func GetExperimentsSettings() ExperimentsSettings

GetExperimentsSettings returns experiments settings with defaults applied

type FileTracker

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

FileTracker tracks file state for incremental updates

type GeminiMCPConfig

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

type GeminiModelPricing struct {
	Input  float64
	Output float64
}

GeminiModelPricing holds pricing per million tokens

type GeminiSessionAnalytics

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

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

CalculateCost estimates session cost based on token usage and model pricing

func (*GeminiSessionAnalytics) TotalTokens

func (a *GeminiSessionAnalytics) TotalTokens() int

TotalTokens returns the sum of input and output tokens

type GeminiSessionInfo

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

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

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

GeminiSettings defines Gemini CLI configuration

type GlobalSearchIndex

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

GlobalSearchIndex manages the searchable session index

func NewGlobalSearchIndex

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

NewGlobalSearchIndex creates a new search index

func (*GlobalSearchIndex) Close

func (idx *GlobalSearchIndex) Close()

Close shuts down the index and releases all memory

func (*GlobalSearchIndex) EntryCount

func (idx *GlobalSearchIndex) EntryCount() int

EntryCount returns the number of indexed entries

func (*GlobalSearchIndex) FuzzySearch

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

FuzzySearch performs fuzzy matching with typo tolerance

func (*GlobalSearchIndex) GetTier

func (idx *GlobalSearchIndex) GetTier() SearchTier

GetTier returns the current search tier

func (*GlobalSearchIndex) IsLoading

func (idx *GlobalSearchIndex) IsLoading() bool

IsLoading returns true if the index is still loading

func (*GlobalSearchIndex) Search

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

Search performs a simple substring search

type GlobalSearchSettings

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
}

Group represents a group of sessions

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

GroupData represents serializable group data

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 NewGroupTreeFromProjects

func NewGroupTreeFromProjects(instances []*Instance, projects []*Project, storedGroups []*GroupData) *GroupTree

NewGroupTreeFromProjects builds a GroupTree using projects as the authoritative list of top-level groups. Sessions are assigned to groups by matching their GroupPath against each project's slug. Sessions whose GroupPath matches no project are silently ignored (orphans). Stored group data is used only to restore the Expanded state; all structural data comes from projects.

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

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 and all its subgroups. Returns nil if the group doesn't exist or has active sessions (deletion refused). Deletion is refused if the group or any subgroup contains sessions.

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) 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 within its group

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 within its group

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

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

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

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

func (*GroupTree) ShallowCopyForSave

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

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

func (c *HTTPServerConfig) GetStartupTimeout() int

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

type HookStatus

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

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

	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

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

	// Latest user input for context (extracted from session files)
	LatestPrompt string `json:"latest_prompt,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"`

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

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

NewInstanceWithGroupAndTool creates a new session with explicit group and tool

func NewInstanceWithTool

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

NewInstanceWithTool creates a new session with tool-specific initialization

func (*Instance) CanFork

func (i *Instance) CanFork() bool

CanFork returns true if this session can be forked

func (*Instance) CanForkOpenCode

func (i *Instance) CanForkOpenCode() bool

CanForkOpenCode returns true if this OpenCode session can be forked

func (*Instance) CanRestart

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

func (i *Instance) CanRestartGeneric() bool

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

func (*Instance) CaptureLoadedMCPs

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

func (i *Instance) ClearHookStatus()

ClearHookStatus resets the hook-based status, 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

func (inst *Instance) ClearParent()

ClearParent removes the parent session link

func (*Instance) CreateForkedInstance

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

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

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

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

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

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

func (i *Instance) Exists() bool

Exists checks if the tmux session still exists

func (*Instance) ForceNextStatusCheck

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

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

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

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

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

func (i *Instance) GetActualWorkDir() string

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

func (*Instance) GetClaudeOptions

func (i *Instance) GetClaudeOptions() *ClaudeOptions

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

func (*Instance) GetCodexOptions

func (i *Instance) GetCodexOptions() *CodexOptions

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

func (*Instance) GetGenericSessionID

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

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

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

func (*Instance) GetJSONLPath

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

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

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

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. Scan disk for active session ID and retry. 4. Fallback to terminal parsing. 5. If still unavailable, return an empty response (no error).

func (*Instance) GetMCPInfo

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

func (i *Instance) GetOpenCodeOptions() *OpenCodeOptions

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

func (*Instance) GetSessionIDFromTmux

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

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

func (inst *Instance) GetToolThreadSafe() string

GetToolThreadSafe returns the tool name with read-lock protection.

func (*Instance) GetWaitingSince

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

func (inst *Instance) IsSubSession() bool

IsSubSession returns true if this session has a parent

func (*Instance) IsWorktree

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

func (*Instance) MarkAccessed

func (inst *Instance) MarkAccessed()

MarkAccessed updates the LastAccessedAt timestamp to now

func (*Instance) PostStartSync

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

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

func (*Instance) SendText

func (i *Instance) SendText(text string) error

SendText sends text to the session's tmux pane as if the user typed it. Used to deliver initial prompts (e.g. /pr-review) after session start.

func (*Instance) SetAcknowledgedFromShared

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

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

SetClaudeOptions stores Claude-specific options

func (*Instance) SetCodexOptions

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

SetCodexOptions stores Codex-specific options

func (*Instance) SetGeminiModel

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

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

func (*Instance) SetGeminiYoloMode

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

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

SetOpenCodeOptions stores OpenCode-specific options

func (*Instance) SetParent

func (inst *Instance) SetParent(parentID string)

SetParent sets the parent session ID

func (*Instance) SetParentWithPath

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

func (inst *Instance) SetStatusThreadSafe(s Status)

SetStatusThreadSafe sets the session status with write-lock protection.

func (*Instance) SetToolThreadSafe

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

func (*Instance) StartWithMessage

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

func (*Instance) SyncSessionIDsToTmux

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

func (i *Instance) SyncTmuxDisplayName()

SyncTmuxDisplayName updates the tmux status bar to reflect the current title.

func (*Instance) UpdateClaudeSession

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

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

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

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

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

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

	// Worktree support
	WorktreePath     string `json:"worktree_path,omitempty"`
	WorktreeRepoRoot string `json:"worktree_repo_root,omitempty"`
	WorktreeBranch   string `json:"worktree_branch,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"`

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

InstanceData represents the serializable session data

type InstanceSettings

type InstanceSettings struct {
	// AllowMultiple allows running multiple hangar 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"`
}

InstanceSettings configures multiple hangar instance behavior

func GetInstanceSettings

func GetInstanceSettings() InstanceSettings

GetInstanceSettings returns instance behavior settings

func (*InstanceSettings) GetAllowMultiple

func (i *InstanceSettings) GetAllowMultiple() bool

GetAllowMultiple returns whether multiple instances are allowed, defaulting to true

type Item

type Item struct {
	Type                ItemType
	Group               *Group
	Session             *Instance
	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)
}

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
)

type LocalMCP

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

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

func GetLogSettings() LogSettings

GetLogSettings returns log management settings with defaults applied

type MCPDef

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, hangar 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

func GetMCPDef(name string) *MCPDef

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

func (*MCPDef) GetTransport

func (m *MCPDef) GetTransport() string

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

func (*MCPDef) HasAutoStartServer

func (m *MCPDef) HasAutoStartServer() bool

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

func (*MCPDef) IsHTTP

func (m *MCPDef) IsHTTP() bool

IsHTTP returns true if this MCP uses HTTP or SSE transport

type MCPInfo

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

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

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

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

func (m *MCPInfo) HasAny() bool

HasAny returns true if any MCPs are configured

func (*MCPInfo) Local

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

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

func (*MCPInfo) Total

func (m *MCPInfo) Total() int

Total returns total number of MCPs across all sources

type MCPMode

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

func GetMCPMode(projectPath string) MCPMode

GetMCPMode determines the MCP configuration mode for a project

type MCPServer

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

MCPServer represents an MCP with its enabled state

func GetLocalMCPState

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

GetLocalMCPState returns Local MCPs with their enabled state

type MCPServerConfig

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

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

MaintenanceResult holds the outcome of a maintenance run.

func RunMaintenance

func RunMaintenance() MaintenanceResult

RunMaintenance executes all maintenance tasks and returns the result.

type MaintenanceSettings

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

func GetMaintenanceSettings() MaintenanceSettings

GetMaintenanceSettings returns maintenance settings from config

type MatchRange

type MatchRange struct {
	Start int
	End   int
}

MatchRange represents a match position in content

type MigrationResult

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

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:

~/.hangar/sessions.json
~/.hangar/sessions.json.bak
~/.hangar/sessions.json.bak.1
~/.hangar/sessions.json.bak.2

New layout:

~/.hangar/config.json
~/.hangar/profiles/default/sessions.json
~/.hangar/profiles/default/sessions.json.bak
~/.hangar/profiles/default/sessions.json.bak.1
~/.hangar/profiles/default/sessions.json.bak.2
~/.hangar/logs/ (unchanged)

type ModelPricing

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

ModelPricing holds pricing per million tokens for a model

type NotificationEntry

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

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

NotificationManager tracks waiting sessions for the notification bar

func NewNotificationManager

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

NewNotificationManager creates a new notification manager

func (*NotificationManager) Add

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

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

func (*NotificationManager) Clear

func (nm *NotificationManager) Clear()

Clear removes all notifications

func (*NotificationManager) Count

func (nm *NotificationManager) Count() int

Count returns number of notifications

func (*NotificationManager) FormatBar

func (nm *NotificationManager) FormatBar() string

FormatBar returns the formatted status bar text

func (*NotificationManager) GetEntries

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

GetEntries returns a copy of current entries (newest first)

func (*NotificationManager) GetSessionByKey

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

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

func (*NotificationManager) Has

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

Has checks if a session is in notifications

func (*NotificationManager) IsMinimal

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

func (nm *NotificationManager) Remove(sessionID string)

Remove removes a session from notifications

func (*NotificationManager) SyncFromInstances

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

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

NotificationsConfig configures the waiting session notification bar

func GetNotificationsSettings

func GetNotificationsSettings() NotificationsConfig

GetNotificationsSettings returns notification bar settings with defaults applied

type OpenCodeOptions

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

func NewOpenCodeOptions(config *UserConfig) *OpenCodeOptions

NewOpenCodeOptions creates OpenCodeOptions with defaults from config

func UnmarshalOpenCodeOptions

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

UnmarshalOpenCodeOptions deserializes OpenCodeOptions from JSON wrapper

func (*OpenCodeOptions) ToArgs

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

ToArgs returns command-line arguments based on options

func (*OpenCodeOptions) ToArgsForFork

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

func (o *OpenCodeOptions) ToolName() string

ToolName returns "opencode"

type OpenCodeSettings

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

OpenCodeSettings defines OpenCode CLI configuration

type PreviewSettings

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

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

PreviewSettings defines preview pane configuration

func GetPreviewSettings

func GetPreviewSettings() PreviewSettings

GetPreviewSettings returns preview settings with defaults applied

func (*PreviewSettings) GetAnalyticsSettings

func (p *PreviewSettings) GetAnalyticsSettings() AnalyticsDisplaySettings

GetAnalyticsSettings returns the analytics display settings with defaults applied

func (*PreviewSettings) GetShowAnalytics

func (p *PreviewSettings) GetShowAnalytics() bool

GetShowAnalytics returns whether to show analytics, defaulting to false

func (*PreviewSettings) GetShowOutput

func (p *PreviewSettings) GetShowOutput() bool

GetShowOutput returns whether to show terminal output, defaulting to true

type ProfileClaudeSettings

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

ProfileClaudeSettings defines profile-specific Claude overrides.

type ProfileSettings

type ProfileSettings struct {
	// Claude defines Claude Code overrides for a specific profile.
	Claude ProfileClaudeSettings `toml:"claude"`
}

ProfileSettings defines per-profile configuration overrides.

type Project

type Project struct {
	Name       string `toml:"name"`
	BaseDir    string `toml:"base_dir"`
	BaseBranch string `toml:"base_branch"`
	Order      int    `toml:"order,omitempty"`
}

Project represents a git repository that hangar manages sessions for.

func GetProject

func GetProject(name string) (*Project, error)

GetProject returns a project by name.

func ListProjects

func ListProjects() ([]*Project, error)

ListProjects returns projects sorted by Order then Name.

func LoadProjects

func LoadProjects() ([]*Project, error)

LoadProjects reads ~/.hangar/projects.toml Returns empty list if file doesn't exist.

type ProjectMCPSettings

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 ProjectsManager

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

ProjectsManager manages the projects list.

type ResponseOutput

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 SearchEntry

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

func (e *SearchEntry) ContentPreview(max int) string

func (*SearchEntry) ContentString

func (e *SearchEntry) ContentString() string

func (*SearchEntry) GetSnippet

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

func (e *SearchEntry) Match(query string) []MatchRange

Match searches for query in entry content (case-insensitive) Returns match positions for highlighting

func (*SearchEntry) MatchCount

func (e *SearchEntry) MatchCount(query string) int

type SearchResult

type SearchResult struct {
	Entry   *SearchEntry
	Matches []MatchRange
	Score   int
	Snippet string
}

SearchResult represents a search result with match info

type SearchTier

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

func DetectTier(totalSize int64) SearchTier

DetectTier determines the appropriate search tier based on data size

type SessionAnalytics

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

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

func ParseSessionJSONL(path string) (*SessionAnalytics, error)

ParseSessionJSONL parses a Claude session JSONL file and returns analytics

func (*SessionAnalytics) CalculateCost

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

CalculateCost estimates session cost based on token usage and model pricing

func (*SessionAnalytics) ContextPercent

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 (defaults to 200000 for Claude)

func (*SessionAnalytics) TotalTokens

func (a *SessionAnalytics) TotalTokens() int

TotalTokens returns the sum of all token types

type ShellSettings

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., "~/.hangar/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

func (s *ShellSettings) GetIgnoreMissingEnvFiles() bool

GetIgnoreMissingEnvFiles returns whether to ignore missing env files, defaulting to true

type SlackSettings

type SlackSettings struct {
	BotToken string `toml:"bot_token"`
	AppToken string `toml:"app_token"`
}

SlackSettings defines Slack bot configuration (stub; conductor is removed).

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

type StatusEvent

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 ~/.hangar/events/ by both the hook handler (Claude) and the TUI's background status poller (all tools).

type StatusEventWatcher

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

StatusEventWatcher watches ~/.hangar/events/ for new status events using fsnotify. Delivers parsed StatusEvent structs via a channel.

func NewStatusEventWatcher

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

func (w *StatusEventWatcher) EventCh() <-chan StatusEvent

EventCh returns the channel that delivers parsed status events.

func (*StatusEventWatcher) Start

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

func (w *StatusEventWatcher) Stop()

Stop shuts down the watcher and closes the event channel.

func (*StatusEventWatcher) WaitForStatus

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

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

StatusFileWatcher watches ~/.hangar/hooks/ for status file changes and updates instance hook status in real time.

func NewStatusFileWatcher

func NewStatusFileWatcher(onChange func()) (*StatusFileWatcher, error)

NewStatusFileWatcher creates a new watcher for the hooks directory. Call Start() to begin watching.

func (*StatusFileWatcher) GetHookStatus

func (w *StatusFileWatcher) GetHookStatus(instanceID string) *HookStatus

GetHookStatus returns the hook status for an instance, or nil if not available.

func (*StatusFileWatcher) Start

func (w *StatusFileWatcher) Start()

Start begins watching the hooks directory. Must be called in a goroutine.

func (*StatusFileWatcher) Stop

func (w *StatusFileWatcher) Stop()

Stop shuts down the watcher.

type StatusSettings

type StatusSettings struct {
}

func GetStatusSettings

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

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

func (s *Storage) Close() error

Close closes the underlying database connection.

func (*Storage) DeleteInstance

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

func (s *Storage) DeleteTodo(id string) error

DeleteTodo removes a todo by ID.

func (*Storage) DeleteTodosForSession

func (s *Storage) DeleteTodosForSession(sessionID string) error

DeleteTodosForSession removes the todo linked to the given session. The todo-session relationship is 1:1 by design; at most one todo will be affected.

func (*Storage) FindTodoBySessionID

func (s *Storage) FindTodoBySessionID(sessionID string) (*Todo, error)

FindTodoBySessionID returns the todo linked to the given session, or nil.

func (*Storage) GetDB

func (s *Storage) GetDB() *statedb.StateDB

GetDB returns the underlying StateDB for direct access (status writes, heartbeat, etc.)

func (*Storage) GetFileMtime

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

func (s *Storage) GetUpdatedAt() (time.Time, error)

GetUpdatedAt returns the last modification timestamp from SQLite metadata.

func (*Storage) Load

func (s *Storage) Load() ([]*Instance, error)

Load reads instances from SQLite

func (*Storage) LoadLite

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

func (s *Storage) LoadTodos(projectPath string) ([]*Todo, error)

LoadTodos returns all todos for the given project path.

func (*Storage) LoadWithGroups

func (s *Storage) LoadWithGroups() ([]*Instance, []*GroupData, error)

LoadWithGroups reads instances and groups from SQLite, reconnects tmux sessions.

func (*Storage) OrphanTodosForSession

func (s *Storage) OrphanTodosForSession(sessionID string) error

OrphanTodosForSession sets status to "orphaned" for the todo linked to the given session. The todo-session relationship is 1:1 by design; at most one todo will be affected.

func (*Storage) Path

func (s *Storage) Path() string

Path returns the database path this storage is using

func (*Storage) Profile

func (s *Storage) Profile() string

Profile returns the profile name this storage is using

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

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

func (s *Storage) SaveTodo(todo *Todo) error

SaveTodo inserts or updates a todo. Updates todo.UpdatedAt to the current time before saving.

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.

func (*Storage) UpdateTodoStatus

func (s *Storage) UpdateTodoStatus(id string, status TodoStatus, sessionID string) error

UpdateTodoStatus updates a todo's status and linked session ID.

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 SubagentInfo

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 TelegramSettings

type TelegramSettings struct {
	Token  string `toml:"token"`
	UserID int64  `toml:"user_id"`
}

TelegramSettings defines Telegram bot configuration (stub; conductor is removed).

type TmuxSettings

type TmuxSettings struct {
	// InjectStatusLine controls whether hangar 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.
	// Default: true (nil = use default true)
	InjectStatusLine *bool `toml:"inject_status_line"`

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

	// MouseMode controls whether tmux mouse mode is enabled when attaching sessions.
	// When true, clicking the tmux status bar switches windows and scroll works in panes.
	// Default: false (opt-in). Enable with mouse_mode = true in [tmux] config section.
	MouseMode *bool `toml:"mouse_mode"`
}

TmuxSettings allows users to override tmux options applied to every session. Options are applied AFTER hangar's defaults, so they take precedence.

Example config.toml:

[tmux]
inject_status_line = false
mouse_mode = true
options = { "allow-passthrough" = "all", "history-limit" = "50000" }

func GetTmuxSettings

func GetTmuxSettings() TmuxSettings

GetTmuxSettings returns tmux option overrides from config

func (TmuxSettings) GetInjectStatusLine

func (t TmuxSettings) GetInjectStatusLine() bool

GetInjectStatusLine returns whether to inject status line, defaulting to true

func (TmuxSettings) GetMouseMode

func (t TmuxSettings) GetMouseMode() bool

GetMouseMode returns whether tmux mouse mode should be enabled on attach. Defaults to false if not set.

type Todo

type Todo struct {
	ID          string
	ProjectPath string
	Title       string
	Description string
	Prompt      string // optional; sent to session on creation
	Status      TodoStatus
	SessionID   string // empty = unlinked
	Order       int
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

Todo represents a work item tied to a project.

func NewTodo

func NewTodo(title, description, prompt, projectPath string) *Todo

NewTodo creates a new Todo for the given project with todo status.

type TodoStatus

type TodoStatus string

TodoStatus represents the lifecycle state of a todo item.

const (
	TodoStatusTodo       TodoStatus = "todo"
	TodoStatusInProgress TodoStatus = "in_progress"
	TodoStatusInReview   TodoStatus = "in_review"
	TodoStatusDone       TodoStatus = "done"
	TodoStatusOrphaned   TodoStatus = "orphaned"
)

type ToolCall

type ToolCall struct {
	Name  string `json:"name"`
	Count int    `json:"count"`
}

ToolCall represents a tool and its usage count

type ToolDef

type ToolDef struct {
	// Command is the shell command to run
	Command string `toml:"command"`

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

func GetToolDef(toolName string) *ToolDef

GetToolDef returns a tool definition from user config Returns nil if tool is not defined

type ToolOptions

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

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

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

func NewTransitionDaemon

func NewTransitionDaemon() *TransitionDaemon

func (*TransitionDaemon) Run

func (d *TransitionDaemon) Run(ctx context.Context) error

func (*TransitionDaemon) SyncOnce

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

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

	TargetSessionID string `json:"target_session_id,omitempty"`
	TargetKind      string `json:"target_kind,omitempty"` // parent | conductor
	DeliveryResult  string `json:"delivery_result,omitempty"`
}

type TransitionNotifier

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

func NewTransitionNotifier

func NewTransitionNotifier() *TransitionNotifier

func (*TransitionNotifier) NotifyTransition

type UpdateSettings

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

func GetUpdateSettings() UpdateSettings

GetUpdateSettings returns update settings with defaults applied

type UserConfig

type UserConfig struct {
	// DefaultTool is the pre-selected AI tool when creating new sessions
	// Valid values: "claude", "gemini", "opencode", "codex", or any custom tool name
	// If empty or invalid, defaults to "shell" (no pre-selection)
	DefaultTool string `toml:"default_tool"`

	// 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 hangar writes to .mcp.json in project directories.
	// Set to false to prevent hangar 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"`

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

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

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

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

	// RemoteLabels maps git remote hostnames to short display labels shown in
	// the preview pane. github.com → "github" is always built-in.
	// Example: "ghe.mycompany.com" = "github"
	RemoteLabels map[string]string `toml:"remote_labels"`

	// Tmux defines tmux option overrides applied to every session
	Tmux TmuxSettings `toml:"tmux"`
}

UserConfig represents user-facing configuration in TOML format

func LoadUserConfig

func LoadUserConfig() (*UserConfig, error)

LoadUserConfig loads the user configuration from TOML file Returns cached config after first load

func ReloadUserConfig

func ReloadUserConfig() (*UserConfig, error)

ReloadUserConfig forces a reload of the user config

func (*UserConfig) GetProfileClaudeConfigDir

func (c *UserConfig) GetProfileClaudeConfigDir(profile string) string

GetProfileClaudeConfigDir returns the profile-specific Claude config directory, if configured.

func (*UserConfig) GetShowAnalytics

func (c *UserConfig) GetShowAnalytics() bool

GetShowAnalytics returns whether to show analytics panel, defaulting to false

func (*UserConfig) GetShowOutput

func (c *UserConfig) GetShowOutput() bool

GetShowOutput returns whether to show terminal output in preview

type WorktreeSettings

type WorktreeSettings struct {
	// AutoCleanup: remove worktree when session is deleted
	AutoCleanup bool `toml:"auto_cleanup"`

	// 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}, {branch}, {session-id}
	// Unknown variables like {foo} are left as-is in the path.
	// If set, overrides DefaultLocation.
	PathTemplate *string `toml:"path_template"`

	// AutoUpdateBase: when true, fast-forward pull the base branch before creating
	// a new worktree so sessions start from the latest upstream code.
	// Default: true
	AutoUpdateBase bool `toml:"auto_update_base"`
}

WorktreeSettings contains git worktree preferences.

func GetWorktreeSettings

func GetWorktreeSettings() WorktreeSettings

GetWorktreeSettings returns worktree settings with defaults applied

func (*WorktreeSettings) Template

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