ipc

package
v1.47.1 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 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"
	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"

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

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.

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

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

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

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

Jump to

Keyboard shortcuts

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