Documentation
¶
Index ¶
- Constants
- type Daemon
- type Pane
- type PaneEvent
- type Project
- type SessionManager
- func (sm *SessionManager) ActiveProject() string
- func (sm *SessionManager) ActiveTabID() string
- func (sm *SessionManager) CreatePane(tabID string, cwd string) (*Pane, error)
- func (sm *SessionManager) CreateProject(name, rootDir string) *Project
- func (sm *SessionManager) CreateTab(name string) *Tab
- func (sm *SessionManager) CreateTabInProject(projectID, name string) *Tab
- func (sm *SessionManager) DestroyPane(paneID string) error
- func (sm *SessionManager) DestroyProject(id string) []*Pane
- func (sm *SessionManager) DestroyTab(tabID string) error
- func (sm *SessionManager) MergeProjects(into string, absorb []string, name string) bool
- func (sm *SessionManager) NewPane(cwd string) *Pane
- func (sm *SessionManager) Pane(id string) *Pane
- func (sm *SessionManager) PaneSources() []memreport.PaneSource
- func (sm *SessionManager) Panes(tabID string) []*Pane
- func (sm *SessionManager) Projects() []Project
- func (sm *SessionManager) ReorderProject(id string, newIndex int) bool
- func (sm *SessionManager) ReorderTab(tabID string, newIdx int) bool
- func (sm *SessionManager) ReplacePane(oldPaneID string, newPane *Pane) error
- func (sm *SessionManager) RestoreProjects(projects []*Project, activeProject string)
- func (sm *SessionManager) RestoreTab(tab *Tab, panes []*Pane)
- func (sm *SessionManager) SnapshotState() (activeTab string, tabs []*Tab, panesByTab map[string][]*Pane, ...)
- func (sm *SessionManager) SwitchProject(id string) (string, bool)
- func (sm *SessionManager) SwitchTab(tabID string)
- func (sm *SessionManager) Tab(id string) *Tab
- func (sm *SessionManager) Tabs() []*Tab
- func (sm *SessionManager) UpdateProject(id, name, rootDir string, requireBootstrap bool) bool
- type Tab
Constants ¶
const MaxBrowseEntries = 500
MaxBrowseEntries caps one listing.
A response must be incapable of exceeding maxFrameSize BY CONSTRUCTION rather than because directories are usually small: the path is chosen by whoever is driving the dialog, and /nix/store or a node_modules root is an ordinary place to land. At ~256 bytes per entry this bounds a listing to well under the frame.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Pane ¶
type Pane struct {
ID string
TabID string
CWD string
Name string // User-set name (empty = use CWD)
PTY apty.Session
OutputBuf *ringbuf.RingBuffer // Captures PTY output for replay on reconnect
GhostSnap []byte // Pure disk-loaded ghost buffer, cleared after first client replay
HistoryLines int // Ghost-buffer line count, snapshotted at restore (immutable after; broadcast-only restore-checklist hint)
// WorktreeOwned marks a pane created into a linked worktree Quil made for
// it. PERSISTED, and it is the only thing that lets restore tell a missing
// WORKTREE from a missing browsed directory — the snapshot stores just CWD
// otherwise, so the two are indistinguishable and the wrong recovery
// applies to one of them. PluginMu-protected, like Type and CWD beside it.
WorktreeOwned bool
// SpawnError explains why this pane has no process. Runtime-only and
// deliberately NOT persisted: a fresh daemon re-stats and re-derives it,
// while a stored one resurrects a complaint about a worktree the user has
// since restored. Cleared on every successful spawn. PluginMu-protected.
SpawnError string
Type string // Plugin name (default: "terminal")
PluginState map[string]string // Scraped values (e.g., "session_id": "abc123")
// PluginMu protects every mutable field that can be read or written
// concurrently with the daemon's PTY-output goroutine: PluginState,
// GhostSnap, PTY (the pointer itself + Pid lookups), ExitCode, and
// ExitedAt. Type and CWD also join this set: the lazy-restore path
// (spawnRestoredPane via ensurePaneSpawned) rewrites them on its error
// branches (CWD="" when the saved dir is gone, Type="terminal" on spawn
// fallback) WHILE the IPC server is live, racing snapshot() /
// workspaceStateFromSnapshot / buildPaneInfos / handlePaneStatusReq
// readers. Cols/Rows join the set too: handleResizePane rewrites them on
// every genuine resize, from the resizing conn's dispatch goroutine, while
// handleAttach (a different conn), the PTY output goroutine's resizeKick,
// and snapshot() all read them. They were previously described here as
// "immutable once set" and written just outside this lock, which made all
// three readers data races. Only genuinely immutable post-creation fields
// (ID, TabID, OutputBuf pointer) are read without it.
PluginMu sync.Mutex
InstanceName string // Which instance config was used
InstanceArgs []string // Args used to start (for rerun strategy)
ExitCode *int // nil = still running, non-nil = exited
ExitedAt time.Time // When the process exited (zero if running)
Cols int // Last known terminal width (0 = unknown)
Rows int // Last known terminal height (0 = unknown)
LastOutputAt time.Time // Updated on every flushPaneOutput
IdleNotified bool // Prevents re-firing for same idle period
LastIdleEventAt time.Time // Cooldown: last time a idle event was emitted
LastBellEventAt time.Time // Cooldown: last time a bell event was emitted
// LastInputBlockedAt: cooldown for the input_blocked event emitted when
// the input queue overflows (child stopped reading stdin). Under PluginMu.
LastInputBlockedAt time.Time
// Muted suppresses notification events sourced from this pane. Set via
// MsgUpdatePane{Muted: true} from the TUI (default keybinding Alt+M).
// Persisted in the workspace snapshot so mute survives restart. Read
// under PluginMu in emitEvent.
Muted bool
// Eager, when true, makes this pane respawn immediately on daemon restart
// instead of being deferred until first access. Toggled via
// MsgUpdatePane{Eager: true} (default keybinding Alt+Shift+E), persisted in
// the workspace snapshot, and marked on the tab label. Read under PluginMu.
Eager bool
// Overlay marks an ephemeral TUI overlay pane (lazygit toggle view).
// Guarded by PluginMu like Muted (set in handleCreatePane after the
// pane is already published to the session maps; concurrent snapshots
// may read it). Excluded from disk snapshots.
Overlay bool
// MouseModes mirrors the child app's DEC mouse-mode state, scanned from the
// PTY output stream (scanMouseModes) in flushPaneOutput. The daemon is the
// only component that sees the one-time mouse-enable burst on every attach,
// so it is authoritative. Broadcast (not persisted) in the workspace snapshot
// so the TUI can forward wheel events to apps that handle their own
// scrolling. mouseBroadcast / lastMouseBroadcastAt are throttle bookkeeping:
// broadcasts are gated by a cooldown so a hostile PTY stream that alternates
// a mode every flush cannot force a full-snapshot broadcast storm (the
// suppressed change is re-delivered on the next flush past the cooldown, or
// by any other broadcastState caller). All three guarded by PluginMu.
MouseModes mouseModeState
// Pending is true between restore and first spawn for a deferred pane: the
// model + ghost buffer exist but no PTY has been created yet. Runtime-only,
// never persisted. Cleared by ensurePaneSpawned.
Pending bool
// LastHookEventAt is the wall-clock time of the most recent hook event
// the daemon translated into a PaneEvent for this pane. Used by
// checkIdlePanes to skip the legacy idle excerpt heuristic when hook
// events are actively flowing (the AI tool itself is the ground truth
// for what "idle" means once hooks are wired up).
LastHookEventAt time.Time
// HookHealthy flips true the first time a hook event is received for
// this pane. Provides the legacy-idle fallback: panes whose hooks
// never load (plugin throws at module init, settings JSON malformed,
// etc.) remain HookHealthy=false and the idle checker stays active —
// the user always sees SOME notification surface, even if not the
// hook-driven one.
HookHealthy bool
// LastModel / LastContextTokens mirror the model id and context-window
// token count of the most recent completed AI turn, extracted from hook
// event data (claude Stop/PostCompact, opencode session.idle) in
// emitHookEvent. Runtime-only — deliberately NOT persisted (a stale
// token count from a previous daemon run would be wrong until the next
// turn) — and broadcast in the workspace snapshot's runtime block so a
// newly-attached TUI shows them without waiting for the next turn.
// Cleared on restart/respawn like MouseModes. Guarded by PluginMu.
LastModel string
LastContextTokens int64
// contains filtered or unexported fields
}
func (*Pane) EnqueueInput ¶ added in v1.20.3
EnqueueInput hands data to the input writer without ever blocking. Returns false when the queue is full — the child is not reading stdin and the caller decides how to surface the drop.
func (*Pane) EnsureInputWriter ¶ added in v1.20.3
func (p *Pane) EnsureInputWriter()
EnsureInputWriter lazily starts the pane's dedicated PTY input goroutine.
type PaneEvent ¶
type PaneEvent struct {
ID string
PaneID string
TabID string
PaneName string
Type string // "process_exit", "output_match"
Title string
Message string
Severity string // "info", "warning", "error"
Timestamp time.Time
Data map[string]string // e.g., {"exit_code": "1"}
}
PaneEvent represents a notification event from a pane.
type Project ¶ added in v1.47.0
type Project struct {
ID string
Name string
RootDir string
TabIDs []string
ActiveTab string
// Bootstrap marks a project the DAEMON invented rather than one a user
// named: the one createTabLocked makes when a tab needs a home and none
// exists, and the one migrateToDefaultProject wraps a pre-projects
// workspace in. Both are called "Default", but the name cannot be the
// signal — a user is free to name a project Default, and renaming this one
// is exactly what stops it being a bootstrap.
//
// The client uses it to ADOPT: naming a project on a host whose only
// project is this one renames it in place, so the host's existing tabs end
// up under the name the user chose instead of beside it. Persisted, because
// a daemon restart must not turn an un-adopted default into a real project.
Bootstrap bool
}
Project groups tabs under one named piece of work rooted at one directory. Daemon-owned and persisted: a client-side-only grouping would be lost on a fresh client, invisible to a second client, and unusable for MCP scoping.
Project has NO Dest field. The daemon does not know it is remote — Dest is the CLIENT's label for the connection a project arrived on.
type SessionManager ¶
type SessionManager struct {
// contains filtered or unexported fields
}
func NewSessionManager ¶
func NewSessionManager(bufSize int) *SessionManager
func (*SessionManager) ActiveProject ¶ added in v1.47.0
func (sm *SessionManager) ActiveProject() string
func (*SessionManager) ActiveTabID ¶
func (sm *SessionManager) ActiveTabID() string
func (*SessionManager) CreatePane ¶
func (sm *SessionManager) CreatePane(tabID string, cwd string) (*Pane, error)
func (*SessionManager) CreateProject ¶ added in v1.47.0
func (sm *SessionManager) CreateProject(name, rootDir string) *Project
func (*SessionManager) CreateTab ¶
func (sm *SessionManager) CreateTab(name string) *Tab
func (*SessionManager) CreateTabInProject ¶ added in v1.47.0
func (sm *SessionManager) CreateTabInProject(projectID, name string) *Tab
func (*SessionManager) DestroyPane ¶
func (sm *SessionManager) DestroyPane(paneID string) error
func (*SessionManager) DestroyProject ¶ added in v1.47.0
func (sm *SessionManager) DestroyProject(id string) []*Pane
DestroyProject removes a project with every tab and pane under it and returns the detached panes. Callers MUST hand them to releasePanes OFF-LOCK: PTY.Close() blocks until the child is reaped, and doing that under sm.mu starves every reader behind the RWMutex writer.
func (*SessionManager) DestroyTab ¶
func (sm *SessionManager) DestroyTab(tabID string) error
func (*SessionManager) MergeProjects ¶ added in v1.49.0
func (sm *SessionManager) MergeProjects(into string, absorb []string, name string) bool
MergeProjects folds every project named in absorb into the one named by into: each absorbed project's tabs are REASSIGNED, never closed, and the emptied project record is dropped. into is then renamed to name and rooted at rootDir.
It exists because "one remote host holds one project" shipped as a CREATE-TIME GUARD. That can refuse to make a host worse; it cannot repair one already carrying duplicates from before the rule, and every remedy the client had was 1:1 on a project — rename, destroy — while DestroyProject takes the tabs and panes with it. A user consolidating by hand therefore had to lose the work that made them care which project survived.
The rename is part of the SAME operation rather than a MsgUpdateProject behind it: two messages leave the host observably holding one project under the name the fold was replacing, and cost two snapshots.
absorb is EXPLICIT rather than "every other project". The local daemon is deliberately exempt from the one-project rule, so a payload meaning "fold everything" would let one mis-aimed local send collapse the projects on the machine the user is sitting at. Unknown IDs and into itself are skipped: a client acting on a stale snapshot folds what it can see and leaves the rest for the next create to offer again, rather than failing whole. The survivor's RootDir is deliberately NOT a parameter — see MergeProjectsPayload. A fold renames and absorbs; relocating is UpdateProject.
func (*SessionManager) NewPane ¶
func (sm *SessionManager) NewPane(cwd string) *Pane
NewPane creates a Pane object with a unique ID and ring buffer, but does NOT add it to any tab. Use with ReplacePane for atomic swaps.
func (*SessionManager) Pane ¶
func (sm *SessionManager) Pane(id string) *Pane
func (*SessionManager) PaneSources ¶ added in v1.9.0
func (sm *SessionManager) PaneSources() []memreport.PaneSource
PaneSources returns an adapter per live pane. Implements memreport.PaneLister. Callers must not retain the returned slice beyond a single collection cycle.
func (*SessionManager) Panes ¶
func (sm *SessionManager) Panes(tabID string) []*Pane
func (*SessionManager) Projects ¶ added in v1.47.0
func (sm *SessionManager) Projects() []Project
Projects returns COPIES. Returning live pointers would let a caller holding the slice past the unlock race UpdateProject mutating Name/RootDir.
func (*SessionManager) ReorderProject ¶ added in v1.47.0
func (sm *SessionManager) ReorderProject(id string, newIndex int) bool
func (*SessionManager) ReorderTab ¶ added in v1.15.0
func (sm *SessionManager) ReorderTab(tabID string, newIdx int) bool
ReorderTab moves the tab with tabID to ordinal newIdx within its OWN project. The tab bar renders one project's tabs and nothing else, so a drag can only ever reorder within a project and newIdx is a PROJECT-relative ordinal. newIdx is clamped to that project's bounds; out-of-range values silently snap to the nearest valid slot rather than erroring, so a stale TUI doesn't have to race the daemon for an authoritative tab count.
Project.TabIDs is the half that must move. The client rebuilds each project's tab list from it, so a slide that touched only sm.tabOrder was undone by the very broadcast it triggered — the drag snapped back, and nothing was persisted either, since the snapshot writes tab_ids too.
sm.tabOrder is RE-ANCHORED rather than slid by the same index: it is the global list, in which a project-relative ordinal means nothing. Reinserting the tab beside its new project neighbour leaves every other project's tabs exactly where they were and keeps daemon-side iteration (Tabs(), list_tabs, the snapshot's tabs array) agreeing with what the user sees.
A tab whose project is unknown — or whose project does not list it — falls back to the pre-project behaviour and slides the global order directly.
Returns true when the order actually changed (caller decides whether to snapshot/broadcast).
func (*SessionManager) ReplacePane ¶
func (sm *SessionManager) ReplacePane(oldPaneID string, newPane *Pane) error
ReplacePane atomically swaps an old pane for a new one at the same position in the tab's pane list. The old pane's PTY is closed.
func (*SessionManager) RestoreProjects ¶ added in v1.47.0
func (sm *SessionManager) RestoreProjects(projects []*Project, activeProject string)
RestoreProjects installs a pre-built project set loaded from disk (or synthesized by migrateToDefaultProject), mirroring RestoreTab. Order in projects becomes projectOrder. activeProject is adopted only when non-empty, so a caller that has nothing to restore (fresh workspace) cannot clobber a project already created earlier in startup.
func (*SessionManager) RestoreTab ¶
func (sm *SessionManager) RestoreTab(tab *Tab, panes []*Pane)
RestoreTab inserts a pre-built tab and its panes into the session. Used during workspace restore from disk. All insertions happen under a single lock hold to prevent orphaned panes.
func (*SessionManager) SnapshotState ¶
func (sm *SessionManager) SnapshotState() (activeTab string, tabs []*Tab, panesByTab map[string][]*Pane, projects []Project, activeProject string)
SnapshotState returns a consistent view of the entire session state under a single RLock hold. This prevents torn reads when tabs/panes are created or destroyed concurrently.
projects/activeProject ride this SAME lock hold rather than a second call to Projects()/ActiveProject() — a nested RLock on this goroutine could deadlock behind a writer parked between the two acquisitions (the oscillation hazard noted at daemon.go's snapshot()).
func (*SessionManager) SwitchProject ¶ added in v1.47.0
func (sm *SessionManager) SwitchProject(id string) (string, bool)
SwitchProject makes id the active project and moves the global active tab onto that project's OWN remembered tab. It returns that tab's ID (empty for a project with no tabs) and whether the project existed.
Moving sm.activeTab is not bookkeeping. It is what respawnPanes eagerly restores on the next daemon start, so leaving it on the OUTGOING project's tab makes the following restore warm up the wrong project.
The tab ID is returned rather than kept private because the caller has to spawn it: after a lazy restore every tab but sm.activeTab's is Pending, and nothing else on the project-switch path reaches ensureTabSpawned — so the incoming project's panes would sit on the restore indicator forever, with no process behind them and no resize able to rescue them.
func (*SessionManager) SwitchTab ¶
func (sm *SessionManager) SwitchTab(tabID string)
func (*SessionManager) Tab ¶
func (sm *SessionManager) Tab(id string) *Tab
func (*SessionManager) Tabs ¶
func (sm *SessionManager) Tabs() []*Tab
func (*SessionManager) UpdateProject ¶ added in v1.47.0
func (sm *SessionManager) UpdateProject(id, name, rootDir string, requireBootstrap bool) bool
requireBootstrap makes the update conditional on the project still being one the daemon invented — the compare-and-swap half of the client's adopt path. See UpdateProjectPayload.AdoptBootstrap.