ipc

package
v1.52.2 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// Lifecycle
	MsgAttach    = "attach"
	MsgDetach    = "detach"
	MsgShutdown  = "shutdown"
	MsgHeartbeat = "heartbeat"

	// Session control (Client -> Daemon)
	MsgCreatePane   = "create_pane"
	MsgDestroyPane  = "destroy_pane"
	MsgResizePane   = "resize_pane"
	MsgUpdatePane   = "update_pane"
	MsgUpdateLayout = "update_layout"

	// Tab control (Client -> Daemon)
	MsgCreateTab  = "create_tab"
	MsgDestroyTab = "destroy_tab"
	MsgSwitchTab  = "switch_tab"
	MsgUpdateTab  = "update_tab"
	MsgReorderTab = "reorder_tab"

	// Project lifecycle (mirrors the tab message set).
	MsgCreateProject  = "create_project"
	MsgDestroyProject = "destroy_project"
	MsgUpdateProject  = "update_project"
	MsgMergeProjects  = "merge_projects"
	MsgSwitchProject  = "switch_project"
	MsgReorderProject = "reorder_project"

	// MsgLinkLost is synthesised CLIENT-SIDE by the router when a connection
	// fails. It is never written to a socket.
	MsgLinkLost = "link_lost"

	// I/O (bidirectional)
	MsgPaneInput  = "pane_input"
	MsgPaneOutput = "pane_output"

	// State sync (Daemon -> Client)
	MsgWorkspaceState = "workspace_state"
	MsgStateUpdate    = "state_update"

	// Plugin (Daemon -> Client)
	MsgPluginError = "plugin_error"

	// Plugin management (Client -> Daemon)
	MsgReloadPlugins = "reload_plugins"

	// MCP request-response (Client -> Daemon -> Client)
	MsgListPanesReq       = "list_panes_req"
	MsgListPanesResp      = "list_panes_resp"
	MsgReadPaneOutputReq  = "read_pane_output_req"
	MsgReadPaneOutputResp = "read_pane_output_resp"
	MsgPaneStatusReq      = "pane_status_req"
	MsgPaneStatusResp     = "pane_status_resp"
	MsgCreatePaneReq      = "create_pane_req"
	MsgCreatePaneResp     = "create_pane_resp"
	MsgRestartPaneReq     = "restart_pane_req"
	MsgRestartPaneResp    = "restart_pane_resp"
	MsgScreenshotPaneReq  = "screenshot_pane_req"
	MsgScreenshotPaneResp = "screenshot_pane_resp"
	MsgSwitchTabReq       = "switch_tab_req"
	MsgSwitchTabResp      = "switch_tab_resp"
	MsgListTabsReq        = "list_tabs_req"
	MsgListTabsResp       = "list_tabs_resp"
	MsgDestroyPaneReq     = "destroy_pane_req"
	MsgDestroyPaneResp    = "destroy_pane_resp"
	MsgSetActivePane      = "set_active_pane" // broadcast to TUI
	MsgCloseTUI           = "close_tui"       // broadcast to TUI
	MsgHighlightPane      = "highlight_pane"  // broadcast to TUI (MCP interaction indicator)

	// Notification center (M12)
	MsgPaneEvent              = "pane_event"               // broadcast to TUI
	MsgDismissEvent           = "dismiss_event"            // client → daemon
	MsgGetNotificationsReq    = "get_notifications_req"    // MCP request
	MsgGetNotificationsResp   = "get_notifications_resp"   // MCP response
	MsgWatchNotificationsReq  = "watch_notifications_req"  // MCP request (blocking)
	MsgWatchNotificationsResp = "watch_notifications_resp" // MCP response

	// Version negotiation — TUI asks daemon for its version string before
	// attaching so mismatches can be surfaced as a blocking dialog or an
	// auto-restart prompt. A daemon built before this pair existed will
	// silently drop MsgVersionReq; the client handles the timeout.
	MsgVersionReq  = "version_req"  // client → daemon (empty payload)
	MsgVersionResp = "version_resp" // daemon → client (VersionRespPayload)

	// Memory reporting
	MsgMemoryReportReq  = "memory_report_req"
	MsgMemoryReportResp = "memory_report_resp"

	// Pane input history
	MsgPaneHistoryReq       = "pane_history_req"
	MsgPaneHistoryResp      = "pane_history_resp"
	MsgPaneHistoryEntryReq  = "pane_history_entry_req"
	MsgPaneHistoryEntryResp = "pane_history_entry_resp"

	// Pane content search (M11 command palette)
	MsgPaneSearchReq  = "pane_search_req"
	MsgPaneSearchResp = "pane_search_resp"

	// Claude Code session discovery (pane setup dialog "resume" picker)
	MsgClaudeSessionsReq       = "claude_sessions_req"
	MsgClaudeSessionsResp      = "claude_sessions_resp"
	MsgClaudeSessionDetailReq  = "claude_session_detail_req"
	MsgClaudeSessionDetailResp = "claude_session_detail_resp"

	// Directory browsing (pane setup dialog CWD picker). The dialog used to
	// read the machine running the TUI, which in remote mode is the wrong disk.
	MsgBrowseDirReq  = "browse_dir_req"
	MsgBrowseDirResp = "browse_dir_resp"

	// Git repo discovery (Alt+G lazygit overlay, and the setup dialog's
	// discover = "git" pick list). Same reason as the browser: it used to stat
	// the TUI's own disk, so against a remote host it reported "no git repo
	// here" for a directory that is a repo on the machine that matters.
	MsgGitReposReq  = "git_repos_req"
	MsgGitReposResp = "git_repos_resp"

	// Git worktree discovery (pane setup dialog with a repository path).
	// Asks the daemon for the list of worktrees in the repository containing Path.
	MsgWorktreeListReq  = "worktree_list_req"
	MsgWorktreeListResp = "worktree_list_resp"

	// Recent-directory existence check (pane setup dialog's quick pick). The
	// list was filtered with a local os.Stat, so against a remote host every
	// server path failed the test and the pick list rendered silently empty —
	// indistinguishable from a feature that had never been used, because
	// structurally nothing had failed.
	MsgDirsExistReq  = "dirs_exist_req"
	MsgDirsExistResp = "dirs_exist_resp"

	// Auto-update (TUI ⇄ daemon)
	MsgStageUpdateReq  = "stage_update_req"  // TUI → daemon (empty payload)
	MsgStageUpdateResp = "stage_update_resp" // daemon → TUI (unicast)

	// Kube-context discovery (pane setup dialog, discover = "kube"). Same
	// reason as the browser and git discovery: it used to parse the
	// kubeconfig on the machine drawing the UI, so against a remote host it
	// offered the laptop's clusters and launched with a --context the server
	// may not have.
	MsgKubeCtxReq  = "kube_ctx_req"
	MsgKubeCtxResp = "kube_ctx_resp"

	// Plugin availability (Ctrl+N and its consumers: context menu, palette,
	// Alt+G overlay). Availability used to be detected only on the machine
	// drawing the UI, which is the wrong machine whenever the daemon is
	// remote — a tool installed only on the server was greyed out, and one
	// installed only locally was offered and then spawned as a fallback
	// terminal.
	MsgPluginListReq  = "plugin_list_req"
	MsgPluginListResp = "plugin_list_resp"
)

Message type constants

View Source
const ContextTokensCompacting int64 = -1

ContextTokensCompacting is the sentinel value for a pane's context-token count while a Claude compaction is in flight. The true post-compaction size is not knowable at PostCompact time — the compaction summary is written to the transcript as system/user entries with no assistant usage, so a read there would return the (now-stale) pre-compaction count. The daemon stores this sentinel on PostCompact and the TUI renders "<model> · compacting" until the next completed turn's Stop reports the real reduced size. It travels as the context_tokens value in both the hook-event data path and the workspace snapshot; the display convention lives in tui.modelStatusSegment.

Variables

View Source
var ErrConnClosed = errors.New("ipc: conn closed")

ErrConnClosed is returned by SendBlocking when the conn closes (locally, via the overflow path, or by the peer) while waiting for queue space.

View Source
var ErrSendCanceled = errors.New("ipc: blocking send canceled")

ErrSendCanceled is returned by SendBlocking when the caller's cancel channel fires while waiting for queue space.

View Source
var ErrSendOverflow = errors.New("ipc: send buffer overflow (slow client)")

ErrSendOverflow is returned by Conn.Send when the per-conn send buffer is full. The connection has been scheduled for close; future Sends short- circuit with the same error.

Functions

func EncodeFrame added in v1.18.6

func EncodeFrame(msg *Message) ([]byte, error)

EncodeFrame marshals msg into a single length-prefixed wire frame in one allocation. Shared by WriteMessage and the per-conn send queues — replaces the marshal → bytes.Buffer → clone chain that copied every broadcast frame up to four times.

func WriteMessage

func WriteMessage(w io.Writer, msg *Message) error

WriteMessage writes a length-prefixed JSON message to w. Format: [4 bytes uint32 big-endian length][JSON payload]

Types

type AttachPayload

type AttachPayload struct {
	Cols int    `json:"cols"`
	Rows int    `json:"rows"`
	CWD  string `json:"cwd,omitempty"`
}

type BrowseDirReqPayload added in v1.46.0

type BrowseDirReqPayload struct {
	Path  string `json:"path"`
	Child string `json:"child,omitempty"`
}

BrowseDirReqPayload asks the daemon to list one directory. An empty Path means "wherever you would spawn a pane by default".

Child descends: when set, the daemon lists the entry of that name inside Path. The client cannot do this join itself, and that is the point. Path separators are a property of the machine holding the filesystem, not of the one rendering the picker — a Windows TUI attached to a Linux daemon would build `C:\srv\work` shaped paths with filepath.Join and list nothing. The daemon joins with its own separator, so the client never has to know.

Child is a single path element and is rejected if it contains a separator. Only the daemon can safely interpret one, so accepting it here would let a client smuggle traversal through a field documented as a leaf name.

type BrowseDirRespPayload added in v1.46.0

type BrowseDirRespPayload struct {
	Path           string        `json:"path"`
	Child          string        `json:"child,omitempty"`
	Resolved       string        `json:"resolved,omitempty"`
	Parent         string        `json:"parent,omitempty"`
	Entries        []BrowseEntry `json:"entries,omitempty"`
	Roots          []string      `json:"roots,omitempty"`
	Truncated      bool          `json:"truncated,omitempty"`
	RootsTruncated bool          `json:"roots_truncated,omitempty"`
	Error          string        `json:"error,omitempty"`
}

BrowseDirRespPayload carries one directory listing, directories first.

Path and Resolved are separate ON PURPOSE and must not be merged. Path echoes the request VERBATIM and is the client's staleness key — the browser fires a request per keystroke of navigation, so answers routinely arrive after the user has moved on, and the client drops any whose echo does not match where it is now. Resolved is the cleaned absolute path: the usable answer, what the dialog displays and ultimately commits. Collapsing the two would break the echo the first time the daemon cleaned a trailing separator, and the field would hang on its pending state until the timeout fired.

Child echoes the request's Child for the same reason, so the staleness key is the whole request rather than half of it — two descents from one directory differ only in this field.

Parent is the daemon's own answer for "one level up", never computed by the client, for the separator reason described on the request.

Roots lists the filesystem roots, and is populated only when Resolved IS a root. On Unix a root has nothing above it, so it stays empty; on Windows it carries the available drive letters, which is what "up" from `C:\` offers.

It exists because the client cannot enumerate them: the old browser walked A:\ to Z:\ with os.Stat under a runtime.GOOS check, and both halves describe the machine DRAWING the picker rather than the one holding the disk. Against a Linux daemon there are no drives at all, and against a Windows daemon the letters are the server's.

Truncated reports that the directory held more than the listing cap.

RootsTruncated is the same statement about Roots, and is deliberately a SECOND flag rather than a reuse of Truncated. The two are independent: the drive sweep can give up on unresponsive mappings while the directory read that follows it succeeds completely, and the client shows the roots AS the listing once the user navigates up — so one flag would either claim the file list was capped when it was not, or let a short drive list pass for a complete one.

type BrowseEntry added in v1.46.0

type BrowseEntry struct {
	Name  string `json:"name"`
	IsDir bool   `json:"is_dir"`
}

BrowseEntry is one child of a listed directory. Only the leaf name travels — the client already knows the parent it asked about, and repeating the full path on every entry would multiply the frame for nothing.

type ClaudeSessionDetailReqPayload added in v1.42.0

type ClaudeSessionDetailReqPayload struct {
	CWD       string `json:"cwd"`
	SessionID string `json:"session_id"`
}

ClaudeSessionDetailReqPayload asks for the deep read of ONE session — the listing head-reads every transcript in a directory, so this is issued per user request (the picker's info key), never per listing.

type ClaudeSessionDetailRespPayload added in v1.42.0

type ClaudeSessionDetailRespPayload struct {
	CWD         string `json:"cwd"`
	SessionID   string `json:"session_id"`
	FirstPrompt string `json:"first_prompt,omitempty"`
	LastPrompt  string `json:"last_prompt,omitempty"`
	UserPrompts int    `json:"user_prompts,omitempty"`
	StartedMs   int64  `json:"started_ms,omitempty"`
	ModifiedMs  int64  `json:"modified_ms,omitempty"`
	SizeBytes   int64  `json:"size_bytes,omitempty"`
	Error       string `json:"error,omitempty"`
}

ClaudeSessionDetailRespPayload answers with one session's summary. CWD and SessionID echo the request VERBATIM for the same staleness contract ClaudeSessionsRespPayload documents — here the pair is what identifies which highlighted row the answer belongs to, since the user can keep moving the cursor while the read is in flight.

StartedMs is 0 when no opening entry carried a timestamp. Prompts are multi-line: they render as paragraphs, not rows. UserPrompts counts only what the user typed — see claudesessions.Detail for why no assistant-side count is reported.

type ClaudeSessionInfo added in v1.42.0

type ClaudeSessionInfo struct {
	ID          string `json:"id"`
	Title       string `json:"title"`
	ModifiedMs  int64  `json:"modified_ms"`
	InUsePaneID string `json:"in_use_pane_id,omitempty"`
}

ClaudeSessionInfo is one resumable session. InUsePaneID identifies the live pane already attached to this session (empty when free) — two claude processes on one transcript would fight over it, so the TUI renders those rows blocked. Like PaneSearchHit, only the id travels: the TUI already holds tab/pane metadata and resolves the display label itself.

type ClaudeSessionsReqPayload added in v1.42.0

type ClaudeSessionsReqPayload struct {
	CWD string `json:"cwd"`
}

ClaudeSessionsReqPayload asks the daemon to enumerate the Claude Code sessions recorded for CWD. CWD is the directory currently highlighted in the pane setup dialog — not yet committed, which is why the response echoes it back for staleness comparison.

type ClaudeSessionsRespPayload added in v1.42.0

type ClaudeSessionsRespPayload struct {
	CWD       string              `json:"cwd"`
	Sessions  []ClaudeSessionInfo `json:"sessions"`
	Truncated bool                `json:"truncated,omitempty"`
	Error     string              `json:"error,omitempty"`
}

ClaudeSessionsRespPayload carries one directory's sessions, newest first. CWD echoes the request VERBATIM (never cleaned or resolved — the TUI compares it against its own value to drop responses that arrived after the user moved to a different directory; any daemon-side normalization would make a legitimate request look permanently stale). Truncated is set when the directory held more sessions than the discovery cap returns.

type Client

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

Client connects to the daemon over a Unix socket.

func NewClient

func NewClient(socketPath string) (*Client, error)

func NewClientWithDialer added in v1.43.0

func NewClientWithDialer(ctx context.Context, dial DialFunc) (*Client, error)

NewClientWithDialer builds a Client over whatever connection dial returns. NewClient remains the Unix-socket convenience wrapper used by every local call site.

func (*Client) Close

func (c *Client) Close() error

func (*Client) Flush added in v1.47.2

func (c *Client) Flush(timeout time.Duration) bool

Flush waits for queued must-deliver frames to reach the socket. See (*Conn).Flush — Send is non-blocking, so closing straight after it discards frames the caller was told were accepted.

func (*Client) Receive

func (c *Client) Receive() (*Message, error)

func (*Client) Send

func (c *Client) Send(msg *Message) error

func (*Client) SetReadDeadline added in v1.8.0

func (c *Client) SetReadDeadline(t time.Time) error

SetReadDeadline installs a read deadline on the underlying socket. Pass the zero time to disable it. Used by the pre-attach version handshake to cap how long we wait for MsgVersionResp from daemons that may predate the version-negotiation protocol.

type Conn

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

Conn wraps a net.Conn with message framing.

Sends are non-blocking: each Conn owns TWO 64-slot queues and a dedicated goroutine that drains them into the underlying socket. The critical queue carries must-deliver frames (state, responses, ghost replay, lifecycle); the droppable queue carries live PTY output broadcasts. The send loop drains critical first (priority) so an output flood can never starve state. A slow or wedged peer drains its own queues; if the CRITICAL queue overflows the offending conn is closed in the background and Send returns ErrSendOverflow. If the DROPPABLE queue overflows the frame is dropped (cosmetic — the next output frame supersedes it) and the conn survives. Other connections are never affected by one client's slowness — closing the wedge-incident class where a single stuck TUI or MCP bridge stalled the daemon's broadcast for every other client, AND the busy-but-alive class where an output storm force-closed a TUI mid-restore.

func (*Conn) Close

func (c *Conn) Close() error

func (*Conn) Dropped added in v1.17.0

func (c *Conn) Dropped() uint64

Dropped returns the number of droppable (live-output) frames discarded because this conn's output queue was full. Test/metrics observability.

func (*Conn) Flush added in v1.47.2

func (c *Conn) Flush(timeout time.Duration) bool

Close shuts down the conn. Idempotent — safe to call concurrently from any goroutine. Any frames still queued in critCh or outCh at close time are intentionally discarded: by the time Close is called we are either tearing down an overflowed (already broken) peer or shutting down the server entirely, and in both cases delivery guarantees no longer apply. Flush waits until every must-deliver frame accepted by Send has reached the socket, or until timeout. Call it before Close when the frames still queued matter: Close signals done, and sendLoop returns on done without writing what is left, so closing straight after Send discards frames the caller was told were accepted. The TUI exit path pairs the two — final keystrokes are exactly this case.

Droppable output frames are deliberately not counted; they are droppable by design and a busy pane could keep the count above zero indefinitely.

Returns true if the queue drained. A closed connection returns immediately: sendLoop is gone, so nothing further will ever be written and waiting could only burn the whole timeout.

func (*Conn) Receive

func (c *Conn) Receive() (*Message, error)

Receive reads the next message from the connection. Callers must ensure a single reader at a time per conn — daemon: handleConn's goroutine; client: the version handshake, then the receive loop, sequentially — so br needs no locking.

func (*Conn) Send

func (c *Conn) Send(msg *Message) error

Send marshals msg into the wire frame and queues it for transmission. Returns ErrSendOverflow when the per-conn buffer is full — the conn has been scheduled for async close at that point.

The closed/overflow short-circuit here is the fast path: it skips the JSON marshal entirely for a known-dead conn. The actual race-safe check happens inside sendFrame next to the channel send — do not remove either one.

func (*Conn) SendBlocking added in v1.18.1

func (c *Conn) SendBlocking(msg *Message, cancel <-chan struct{}) error

SendBlocking queues a must-deliver frame, waiting for the critical queue to drain below sendHeadroom instead of tripping the slow-client overflow close. For unicast bulk transfers (ghost replay, event replay during attach) that run on the sender's own goroutine: backpressure slows only this client's replay, while a genuinely wedged peer is still bounded by sendLoop's writeDeadline (deadline trips → conn closes → done fires → this returns). cancel (typically the daemon shutdown channel) may be nil.

Without this, a freshly attached TUI busy applying workspace state was force-closed whenever replay volume exceeded sendBufSize frames — two full 256 KB ghost buffers were enough — locking the client out on every attach.

type CreatePanePayload

type CreatePanePayload struct {
	TabID         string   `json:"tab_id"`
	CWD           string   `json:"cwd"`
	Type          string   `json:"type,omitempty"`
	InstanceName  string   `json:"instance_name,omitempty"`
	InstanceArgs  []string `json:"instance_args,omitempty"`
	ReplacePaneID string   `json:"replace_pane_id,omitempty"`
	// Overlay marks the pane as a TUI overlay (lazygit toggle view): it
	// never enters the layout tree, is muted at creation, and is excluded
	// from disk snapshots (ephemeral — gone on daemon restart).
	// Trust: any IPC client can set this field; the daemon honors it under
	// the same socket trust model as every other field (the MCP bridge
	// deliberately does not expose it).
	Overlay bool `json:"overlay,omitempty"`
	// ResumeSessionID resumes an existing Claude Code session instead of
	// starting a fresh one: the daemon spawns `claude --resume <id>` in place
	// of the preassign_id strategy's `--session-id <new-uuid>`. Empty (the
	// default) preserves the fresh-session behavior.
	//
	// Trust: like Overlay, any IPC client can set this. The daemon validates
	// it against the canonical UUID shape before it reaches argv, and also
	// refuses a session a live pane already holds — two claude processes on
	// one transcript overwrite each other's history. Either rejection falls
	// back to a fresh session rather than failing the spawn. The MCP bridge
	// deliberately does not expose this field.
	ResumeSessionID string `json:"resume_session_id,omitempty"`
	// Worktree asks the daemon to CREATE a linked worktree and spawn the pane
	// inside it, ignoring CWD. Nil (the default) is the ordinary synchronous
	// create every existing client makes.
	//
	// A POINTER rather than a value: nil is what keeps every other create —
	// MCP create_pane, the plugin dialog, restore — on the unchanged path with
	// no branch anywhere in the daemon, and it says "this create is different"
	// structurally rather than by a zero-value convention someone can forget.
	//
	// Trust: like Overlay and ResumeSessionID, any IPC client can set this.
	// The daemon validates the branch name against both the ref and the path
	// grammar before it reaches argv, and never passes --force. The MCP bridge
	// deliberately does not expose this field.
	Worktree *WorktreeSpec `json:"worktree,omitempty"`
}

type CreatePaneReqPayload

type CreatePaneReqPayload struct {
	TabID        string   `json:"tab_id,omitempty"`
	CWD          string   `json:"cwd,omitempty"`
	Type         string   `json:"type,omitempty"`
	InstanceName string   `json:"instance_name,omitempty"`
	InstanceArgs []string `json:"instance_args,omitempty"`
}

type CreatePaneRespPayload

type CreatePaneRespPayload struct {
	PaneID string `json:"pane_id"`
	TabID  string `json:"tab_id"`
	// Error explains a create that produced NO pane. Only a create carrying a
	// WorktreeSpec can fail this way — an ordinary create is synchronous and
	// its result arrives in the next workspace broadcast, as it always has.
	//
	// It carries git's own stderr where there is any: "already used by
	// worktree '/x/feat-y'" names the pane to go look at, and no message Quil
	// could invent would.
	Error string `json:"error,omitempty"`
	// Swapped reports whether a REPLACE actually removed the pane named by
	// ReplacePaneID. It is a statement about what happened, unlike Worktree
	// below, and it exists because the two are not implied by Error.
	//
	// A worktree-backed replace creates the worktree BEFORE the swap, so an add
	// that fails leaves the pane alive and the client must put it back. But the
	// swap itself happens before the new pane's PTY is spawned — so a spawn
	// failure reports an error with the old pane already destroyed, and a
	// client that inferred "error means untouched" would restore a pane the
	// daemon no longer has. Keystrokes then route to a pane id that does not
	// exist until the next broadcast prunes the leaf.
	//
	// omitempty: absent means false, which is the correct reading for every
	// non-replace response and for an older daemon that does not send it.
	Swapped bool `json:"swapped,omitempty"`
	// Worktree echoes the request's spec VERBATIM on every path, including the
	// error one. It is the client's staleness key, not a statement about what
	// was created — the client armed a layout placeholder before the send and
	// nothing else will unwind it.
	Worktree *WorktreeSpec `json:"worktree,omitempty"`
}

type CreateProjectPayload added in v1.47.0

type CreateProjectPayload struct {
	Name    string `json:"name"`
	RootDir string `json:"root_dir"`
}

type CreateTabPayload

type CreateTabPayload struct {
	Name string `json:"name"`
}

type DestroyPanePayload

type DestroyPanePayload struct {
	PaneID string `json:"pane_id"`
}

type DestroyPaneReqPayload

type DestroyPaneReqPayload struct {
	PaneID string `json:"pane_id"`
}

type DestroyPaneRespPayload

type DestroyPaneRespPayload struct {
	Success bool `json:"success"`
}

type DestroyProjectPayload added in v1.47.0

type DestroyProjectPayload struct {
	ProjectID string `json:"project_id"`
}

type DestroyTabPayload

type DestroyTabPayload struct {
	TabID string `json:"tab_id"`
}

type DialFunc added in v1.43.0

type DialFunc func(ctx context.Context) (net.Conn, error)

DialFunc establishes one transport-level connection to a daemon. It is the seam that lets a Client run over something other than a Unix socket (an SSH channel today, a TLS connection later) without the protocol layer knowing.

CONTRACT: ctx bounds the dial only. Once a DialFunc returns a net.Conn, that conn owns any underlying process or socket and releases it on Close — cancelling ctx afterwards must not disturb a live connection. Reconnect loops depend on this: they dial under a per-attempt timeout with a deferred cancel, and would otherwise destroy each session as they created it.

type DirsExistReqPayload added in v1.46.0

type DirsExistReqPayload struct {
	Paths []string `json:"paths"`
}

DirsExistReqPayload asks the daemon which of Paths still resolve to directories on ITS filesystem.

type DirsExistRespPayload added in v1.46.0

type DirsExistRespPayload struct {
	Paths []string `json:"paths,omitempty"`
	Error string   `json:"error,omitempty"`
}

DirsExistRespPayload carries the surviving directories.

Paths is the subset of the request that resolved to a directory, in the request's order. It is deliberately NOT an echo of the request, so it cannot serve as a staleness key the way BrowseDirRespPayload.Path does — correlation is by the per-request generation in Message.ID instead, because a path LIST is a poor key: two requests differing only in order would compare equal under any cheap comparison, and comparing them properly costs more than the generation.

An empty Paths with an empty Error is a real answer — "none of these exist any more" — and must stay distinguishable from a failure, because only one of the two justifies telling the user their remembered directories are gone.

type DismissEventPayload

type DismissEventPayload struct {
	EventID string `json:"event_id"` // empty = dismiss all
}

type GetNotificationsRespPayload

type GetNotificationsRespPayload struct {
	Events []PaneEventPayload `json:"events"`
}

type GitReposReqPayload added in v1.46.0

type GitReposReqPayload struct {
	CWD string `json:"cwd"`
}

GitReposReqPayload asks the daemon which git repositories are near CWD — the enclosing repo plus one level of sub-repos. An empty CWD means the daemon's default.

type GitReposRespPayload added in v1.46.0

type GitReposRespPayload struct {
	CWD   string   `json:"cwd"`
	Repos []string `json:"repos,omitempty"`
	Error string   `json:"error,omitempty"`
}

GitReposRespPayload carries the discovered repositories, enclosing repo first.

CWD echoes the request VERBATIM, the same staleness contract the browse and session listings use: the answer is only meaningful for the directory that was asked about, and the user may have moved on by the time it lands.

An empty Repos with an empty Error is a real answer — "there is no repo here" — and is deliberately distinguishable from a failure, because the two produce different UI: the first flashes a finding, the second must not claim one.

type HighlightPanePayload

type HighlightPanePayload struct {
	PaneID string `json:"pane_id"`
}

type HistoryEntryMeta added in v1.30.0

type HistoryEntryMeta struct {
	TsMs    int64  `json:"ts_ms"`
	Preview string `json:"preview"`
}

HistoryEntryMeta is one list row: a stable id (TsMs) and a single-line preview. The list renders exactly one row per entry, so the preview is flattened daemon-side (panehistory.PreviewLine) rather than shipped as the prompt's separate lines — the wire carries what is displayed, nothing more.

type KubeContextInfo added in v1.46.0

type KubeContextInfo struct {
	Name      string `json:"name"`
	Namespace string `json:"namespace,omitempty"`
	Current   bool   `json:"current,omitempty"`
}

KubeContextInfo is one context enumerated from the daemon's kubeconfig. Current is carried per entry rather than as a top-level name, matching kubediscover.Context — the setup dialog draws ● from this field directly.

type KubeCtxReqPayload added in v1.46.0

type KubeCtxReqPayload struct{}

KubeCtxReqPayload is deliberately empty: kube-context discovery is CWD-independent, so there is no content key that could go stale. The per-request generation in Message.ID is the whole correlator.

type KubeCtxRespPayload added in v1.46.0

type KubeCtxRespPayload struct {
	Contexts  []KubeContextInfo `json:"contexts,omitempty"`
	Truncated bool              `json:"truncated,omitempty"`
	Error     string            `json:"error,omitempty"`
}

KubeCtxRespPayload carries the discovered kube contexts.

An empty Contexts with an empty Error is a real answer — "no kubeconfig here" — deliberately distinguishable from a failure: only one of the two justifies telling the user there are no contexts. Truncated is set when the daemon capped the list at maxKubeContexts.

type ListPanesRespPayload

type ListPanesRespPayload struct {
	Panes []PaneInfo `json:"panes"`
}

type ListTabsRespPayload

type ListTabsRespPayload struct {
	Tabs []TabInfo `json:"tabs"`
}

type MemoryReportReqPayload added in v1.9.0

type MemoryReportReqPayload struct{}

type MemoryReportRespPayload added in v1.9.0

type MemoryReportRespPayload struct {
	SnapshotAt int64         `json:"snapshot_at"` // Unix nanoseconds
	Panes      []PaneMemInfo `json:"panes"`
	Total      uint64        `json:"total"`
	// Tabs is the same view that MsgListTabsResp would return at the moment
	// the daemon assembled this response. Embedded here so MCP
	// `get_memory_report` does not need a second round-trip to enrich tab
	// IDs with names. Note: the per-pane memory numbers come from the
	// memreport collector's last tick (up to 5 s old), while Tabs is taken
	// fresh — the two halves are captured close-in-time on the daemon side
	// but are not guaranteed to be drawn from the exact same instant.
	Tabs []TabInfo `json:"tabs,omitempty"`
}

type MergeProjectsPayload added in v1.49.0

type MergeProjectsPayload struct {
	ProjectID string   `json:"project_id"`
	Absorb    []string `json:"absorb"`
	Name      string   `json:"name"`
}

MergeProjectsPayload folds the Absorb projects' tabs into ProjectID and drops the emptied records, then renames the survivor to Name. Tabs and panes are never destroyed — that is the whole difference from DestroyProject, and the reason a user could not consolidate a host by hand.

Absorb is an explicit list rather than "every other project on that daemon": the one-project-per-host rule is the CLIENT's (Project has no Dest field), so a daemon-side "fold everything" would be wrong on the local machine, where several projects are expected.

There is deliberately NO RootDir. A fold renames and absorbs; it does not relocate. The survivor already has a root somebody chose, while the form field that would supply one holds — in the ordinary case — whatever the dialog's own opening browse resolved, since that request carries an empty path and the daemon answers with its default CWD. Carrying it would overwrite a deliberate value with an artifact on nearly every fold. Changing a project's root is what MsgUpdateProject is for, from a dialog seeded with the project's own.

type Message

type Message struct {
	Type    string          `json:"type"`
	ID      string          `json:"id,omitempty"` // request-response correlation (MCP bridge)
	Payload json.RawMessage `json:"payload,omitempty"`
	// Origin names the daemon a message came from (set by the router on receive)
	// or is destined for (set by the Model on send). Client-side routing state
	// only: `json:"-"` keeps it off the wire, so adding it needs no protocol
	// version bump. Empty on receive means the local daemon; empty on send means
	// "resolve it" — see router.Send.
	Origin string `json:"-"`
}

Message is the wire format for IPC communication.

func NewMessage

func NewMessage(typ string, payload any) (*Message, error)

NewMessage creates a Message with a typed payload.

func ReadMessage

func ReadMessage(r io.Reader) (*Message, error)

ReadMessage reads a length-prefixed JSON message from r.

func (*Message) DecodePayload

func (m *Message) DecodePayload(target any) error

DecodePayload unmarshals the message payload into the given target.

type MessageHandler

type MessageHandler func(conn *Conn, msg *Message)

MessageHandler is called for each incoming message on a connection.

type PaneEventPayload

type PaneEventPayload struct {
	ID        string            `json:"id"`
	PaneID    string            `json:"pane_id"`
	TabID     string            `json:"tab_id"`
	PaneName  string            `json:"pane_name"`
	Type      string            `json:"type"`
	Title     string            `json:"title"`
	Message   string            `json:"message,omitempty"`
	Severity  string            `json:"severity"`
	Timestamp int64             `json:"timestamp"`
	Data      map[string]string `json:"data,omitempty"`
}

type PaneHistoryEntryReqPayload added in v1.30.0

type PaneHistoryEntryReqPayload struct {
	PaneID string `json:"pane_id"`
	TsMs   int64  `json:"ts_ms"`
}

PaneHistoryEntryReqPayload requests one entry's full text by its TsMs id.

type PaneHistoryEntryRespPayload added in v1.30.0

type PaneHistoryEntryRespPayload struct {
	PaneID string `json:"pane_id"`
	TsMs   int64  `json:"ts_ms"`
	Text   string `json:"text"`
	Found  bool   `json:"found"`
}

PaneHistoryEntryRespPayload carries one entry's full text (Found=false if the id no longer exists, e.g. compacted away between list and fetch).

type PaneHistoryReqPayload added in v1.30.0

type PaneHistoryReqPayload struct {
	PaneID string `json:"pane_id"`
}

PaneHistoryReqPayload requests the input-history preview list for one pane.

type PaneHistoryRespPayload added in v1.30.0

type PaneHistoryRespPayload struct {
	PaneID  string             `json:"pane_id"`
	Entries []HistoryEntryMeta `json:"entries"`
}

PaneHistoryRespPayload carries the preview list, newest first.

type PaneInfo

type PaneInfo struct {
	ID           string `json:"id"`
	TabID        string `json:"tab_id"`
	TabName      string `json:"tab_name"`
	Name         string `json:"name"`
	Type         string `json:"type"`
	CWD          string `json:"cwd"`
	Running      bool   `json:"running"`
	Pending      bool   `json:"pending,omitempty"`
	InstanceName string `json:"instance_name,omitempty"`
}

type PaneInputPayload

type PaneInputPayload struct {
	PaneID string `json:"pane_id"`
	Data   []byte `json:"data"`
}

type PaneMemInfo added in v1.9.0

type PaneMemInfo struct {
	PaneID      string `json:"pane_id"`
	TabID       string `json:"tab_id"`
	GoHeapBytes uint64 `json:"go_heap_bytes"`
	PTYRSSBytes uint64 `json:"pty_rss_bytes"`
	TotalBytes  uint64 `json:"total_bytes"`
}

PaneMemInfo is the wire form of a single pane's daemon-side memory. TUI-local memory is not part of the wire format — the TUI merges its own values at render time.

type PaneOutputPayload

type PaneOutputPayload struct {
	PaneID string `json:"pane_id"`
	Data   []byte `json:"data"`
	Ghost  bool   `json:"ghost,omitempty"`
}

type PaneSearchHit added in v1.40.0

type PaneSearchHit struct {
	PaneID    string `json:"pane_id"`
	Matches   int    `json:"matches"`
	Excerpt   string `json:"excerpt"`
	Truncated bool   `json:"truncated,omitempty"`
}

PaneSearchHit is one matching pane. The TUI resolves the display label itself from PaneID (it already holds tab/pane metadata), so the daemon returns only the id, the total match count, a single preview line, and whether THIS pane's count was capped (the per-hit flag is what the "capped" label renders from — the payload-level Truncated is only a "some pane was capped" summary).

type PaneSearchReqPayload added in v1.40.0

type PaneSearchReqPayload struct {
	Query string `json:"query"`
}

PaneSearchReqPayload asks the daemon to scan every pane's scrollback for a literal, case-insensitive substring. Query is the palette query verbatim — content search runs inline with the command filter, so there is no sigil to strip; the daemon trims it only for matching and echoes it back unchanged.

type PaneSearchRespPayload added in v1.40.0

type PaneSearchRespPayload struct {
	Query     string          `json:"query"`
	Hits      []PaneSearchHit `json:"hits"`
	Truncated bool            `json:"truncated,omitempty"`
}

PaneSearchRespPayload carries the hits for one search. Query echoes the request term VERBATIM (never trimmed — the TUI compares it against its own untrimmed term to drop responses that arrived after the user typed more). Truncated is set when any pane hit the per-pane match cap.

type PaneStatusReqPayload

type PaneStatusReqPayload struct {
	PaneID string `json:"pane_id"`
}

type PaneStatusRespPayload

type PaneStatusRespPayload struct {
	PaneID   string `json:"pane_id"`
	Running  bool   `json:"running"`
	Pending  bool   `json:"pending,omitempty"`
	ExitCode *int   `json:"exit_code,omitempty"`
	Type     string `json:"type"`
	CWD      string `json:"cwd"`
	Name     string `json:"name"`
}

type PluginErrorPayload

type PluginErrorPayload struct {
	PaneID  string `json:"pane_id"`
	Title   string `json:"title"`
	Message string `json:"message"`
}

type PluginInfo added in v1.46.0

type PluginInfo struct {
	Name      string `json:"name"`
	Available bool   `json:"available"`
}

PluginInfo is one plugin's availability as the daemon sees it.

No Homepage field: a greyed row already links out via the LOCAL plugin definition's own Homepage, which points at the same URL either machine would give. The field would only matter for a plugin the TUI does not define, which it cannot render at all — so it is dropped rather than carried unused.

type PluginListReqPayload added in v1.46.0

type PluginListReqPayload struct{}

PluginListReqPayload is deliberately empty: the answer is "the daemon's whole registry", not scoped to any request-supplied key.

type PluginListRespPayload added in v1.46.0

type PluginListRespPayload struct {
	Plugins []PluginInfo `json:"plugins,omitempty"`
}

PluginListRespPayload carries the daemon's own registry. Deliberately no generation field: every response describes the same daemon and applying it is idempotent, so a late answer says exactly what a fresh one would.

type ReadPaneOutputReqPayload

type ReadPaneOutputReqPayload struct {
	PaneID    string `json:"pane_id"`
	LastLines int    `json:"last_lines"`
}

type ReadPaneOutputRespPayload

type ReadPaneOutputRespPayload struct {
	PaneID string `json:"pane_id"`
	Text   string `json:"text"`
	Lines  int    `json:"lines"`
}

type ReorderProjectPayload added in v1.47.0

type ReorderProjectPayload struct {
	ProjectID string `json:"project_id"`
	NewIndex  int    `json:"new_index"`
}

type ReorderTabPayload added in v1.15.0

type ReorderTabPayload struct {
	TabID    string `json:"tab_id"`
	NewIndex int    `json:"new_index"`
}

ReorderTabPayload moves an existing tab to a new ordinal position. NewIndex is clamped to the daemon-side tab list bounds, so a stale TUI does not have to track creation/destruction races to send a safe value.

type ResizePanePayload

type ResizePanePayload struct {
	PaneID string `json:"pane_id"`
	Rows   uint16 `json:"rows"`
	Cols   uint16 `json:"cols"`
}

type RestartPaneReqPayload

type RestartPaneReqPayload struct {
	PaneID string `json:"pane_id"`
}

type RestartPaneRespPayload

type RestartPaneRespPayload struct {
	PaneID  string `json:"pane_id"`
	Success bool   `json:"success"`
}

type ScreenshotPaneReqPayload

type ScreenshotPaneReqPayload struct {
	PaneID string `json:"pane_id"`
	Width  int    `json:"width,omitempty"`
	Height int    `json:"height,omitempty"`
}

type ScreenshotPaneRespPayload

type ScreenshotPaneRespPayload struct {
	PaneID  string `json:"pane_id"`
	Text    string `json:"text"`
	CursorX int    `json:"cursor_x"`
	CursorY int    `json:"cursor_y"`
}

type Server

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

Server listens for client connections over a Unix socket.

func NewServer

func NewServer(socketPath string, handler MessageHandler, onDisconnect func(*Conn)) *Server

func (*Server) Broadcast

func (s *Server) Broadcast(msg *Message)

Broadcast sends a message to all connected clients without blocking on any individual conn. Marshals the wire frame once and shares the bytes across all per-conn send queues. Live PTY output (MsgPaneOutput) is enqueued as droppable — a slow conn sheds it without being closed. All other message types are critical: a slow or wedged conn that overflows its critical queue is dropped from the fan-out (logged once, per CAS-guarded enqueue) without affecting the others.

func (*Server) ConnCount added in v1.16.0

func (s *Server) ConnCount() int

ConnCount returns the number of currently-connected clients. Test-friendly alternative to the existing log-line scraping pattern; used to wait for connect/disconnect events without time-based sleeps.

func (*Server) Start

func (s *Server) Start() error

func (*Server) Stop

func (s *Server) Stop() error

Stop closes the listener and all active connections. Frames queued in any conn's send buffer at the moment of Stop are discarded — Daemon.Stop's shutdown sequence does not rely on a final IPC broadcast reaching clients (the final-snapshot durability lives in the on-disk workspace.json path, not in the wire).

type SetActivePanePayload

type SetActivePanePayload struct {
	PaneID string `json:"pane_id"`
}

type StageUpdateRespPayload added in v1.37.0

type StageUpdateRespPayload struct {
	Success bool   `json:"success"`
	Version string `json:"version,omitempty"`
	Error   string `json:"error,omitempty"`
}

StageUpdateRespPayload answers MsgStageUpdateReq (About → Update now with nothing staged yet).

type SwitchProjectPayload added in v1.47.0

type SwitchProjectPayload struct {
	ProjectID string `json:"project_id"`
}

type SwitchTabPayload

type SwitchTabPayload struct {
	TabID string `json:"tab_id"`
}

type SwitchTabReqPayload

type SwitchTabReqPayload struct {
	TabID string `json:"tab_id"`
}

type SwitchTabRespPayload

type SwitchTabRespPayload struct {
	TabID string `json:"tab_id"`
}

type TabInfo

type TabInfo struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Color     string `json:"color,omitempty"`
	PaneCount int    `json:"pane_count"`
	Active    bool   `json:"active"`
}

type UpdateInfo added in v1.37.0

type UpdateInfo struct {
	LatestVersion   string `json:"latest_version"`
	ReleaseURL      string `json:"release_url,omitempty"`
	StagedVersion   string `json:"staged_version,omitempty"` // set once fully staged
	InstallWritable bool   `json:"install_writable"`
}

UpdateInfo rides the workspace_state broadcast under the "update" key when a newer release than the running daemon's version is known. Omitted entirely when up to date; old clients ignore the extra key.

type UpdateLayoutPayload

type UpdateLayoutPayload struct {
	TabID  string          `json:"tab_id"`
	Layout json.RawMessage `json:"layout"`
}

type UpdatePanePayload

type UpdatePanePayload struct {
	PaneID string `json:"pane_id"`
	Name   string `json:"name,omitempty"`
	CWD    string `json:"cwd,omitempty"`
	// Muted is a pointer so an unset field (nil) is distinguishable from an
	// explicit false. Callers updating only Name or CWD pass nil and the
	// daemon leaves the pane's mute state untouched.
	Muted *bool `json:"muted,omitempty"`
	// Eager is a pointer for the same nil-vs-false tri-state reason as Muted.
	Eager *bool `json:"eager,omitempty"`
}

type UpdateProjectPayload added in v1.47.0

type UpdateProjectPayload struct {
	ProjectID string `json:"project_id"`
	Name      string `json:"name"`
	RootDir   string `json:"root_dir"`
	// AdoptBootstrap makes the update conditional: apply it only while the
	// project is still one the daemon invented. Set by the client's adopt path,
	// where naming a project on a host RENAMES the host's unnamed one — two
	// clients adopting the same host would otherwise each rename the other's
	// freshly named project, since the Bootstrap check lives in each client's
	// own snapshot. Omitted (false) means an ordinary rename, which always
	// applies. omitempty so an older daemon sees the same wire shape it did.
	AdoptBootstrap bool `json:"adopt_bootstrap,omitempty"`
}

type UpdateTabPayload

type UpdateTabPayload struct {
	TabID string `json:"tab_id"`
	Name  string `json:"name,omitempty"`
	Color string `json:"color,omitempty"`
	// ClearColor disambiguates an empty Color: "" alone means "no change"
	// (e.g. a rename of an uncolored tab), ClearColor=true means "reset to
	// the default color" (the tab-color cycle wrapping past the last color).
	ClearColor bool `json:"clear_color,omitempty"`
}

type VersionRespPayload added in v1.8.0

type VersionRespPayload struct {
	Version string `json:"version"`
}

VersionRespPayload carries the daemon's version string. MsgVersionReq has no payload — the request is just "what version are you running?".

type WatchNotificationsReqPayload

type WatchNotificationsReqPayload struct {
	PaneIDs   []string `json:"pane_ids,omitempty"`
	TimeoutMs int      `json:"timeout_ms"`
	// SinceTimestamp closes the race between "kick off a task" and "start
	// watching" — events fired during that window would otherwise be lost.
	// When set (Unix ms), the daemon first scans the existing event queue
	// for any matching event whose timestamp is strictly greater, returning
	// the oldest such event immediately. Only if the queue holds no
	// qualifying event does it register a blocking watcher. Agents should
	// pass the timestamp of the last event they handled.
	SinceTimestamp int64 `json:"since_timestamp,omitempty"`
}

type WatchNotificationsRespPayload

type WatchNotificationsRespPayload struct {
	Event   *PaneEventPayload `json:"event,omitempty"`
	Timeout bool              `json:"timeout"`
}

type WorktreeInfo added in v1.51.0

type WorktreeInfo struct {
	Path     string `json:"path"`
	Branch   string `json:"branch,omitempty"`
	Detached bool   `json:"detached,omitempty"`
	Main     bool   `json:"main,omitempty"`
	Locked   bool   `json:"locked,omitempty"`
	Prunable bool   `json:"prunable,omitempty"`
	Bare     bool   `json:"bare,omitempty"`
}

WorktreeInfo is one entry of the repository's worktree list, as the daemon sees it. A mirror of gitworktree.Worktree rather than a reuse of it: this is a wire type, and the internal one is free to change shape.

type WorktreeListReqPayload added in v1.51.0

type WorktreeListReqPayload struct {
	Path string `json:"path"`
}

WorktreeListReqPayload asks which git worktrees belong to the repository containing Path. An empty Path means the daemon's default directory.

type WorktreeListRespPayload added in v1.51.0

type WorktreeListRespPayload struct {
	Path         string         `json:"path"`
	Repo         bool           `json:"repo,omitempty"`
	Root         string         `json:"root,omitempty"`
	WorktreeRoot string         `json:"worktree_root,omitempty"`
	Worktrees    []WorktreeInfo `json:"worktrees,omitempty"`
	Error        string         `json:"error,omitempty"`
}

WorktreeListRespPayload carries the repository's worktrees, main checkout first.

CONTRACT: Path echoes the request VERBATIM on every path, including the error and single-flight-rejection ones. It is the client's staleness key, not a statement about what was read — normalising it daemon-side would make a live request look permanently stale.

Repo false with an empty Error is a real answer ("this is not a repository") and must stay distinguishable from a failure: only one of the two justifies telling the user there is no repository here.

WorktreeRoot is the directory NEW worktrees would go in, already joined by the daemon with the daemon's own separators. The client must never compute it: doing so means running filepath.Dir/Join with the CLIENT's separators over a path that lives on the daemon's machine. Unused by stage A beyond display, and present now so the contract does not change under stage B.

type WorktreeSpec added in v1.52.0

type WorktreeSpec struct {
	// RepoRoot is the repository the worktree branches from, as the DAEMON's
	// filesystem spells it. The client sends back the directory the daemon's
	// own browse answered with, so no path built on the client is involved.
	RepoRoot string `json:"repo_root"`
	// Branch is the NEW branch, off the repository's current HEAD.
	//
	// Existing branches are deliberately not offered: one already checked out
	// in another worktree fails at the git level and needs its own error path,
	// and attaching to that worktree — which stage A ships — covers the real
	// case anyway.
	Branch string `json:"branch"`
}

WorktreeSpec asks the daemon to create a linked worktree for a new pane. Create-time only — an instruction, not stored pane state; what persists is the resulting CWD, plus a flag saying the pane owns a worktree.

Jump to

Keyboard shortcuts

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