// Package protocol defines the daemon/client control frames shared by // transports. It intentionally contains data shapes only; app behavior stays // in internal/app until the headless daemon split is complete. package protocol import ( "encoding/json" "fmt" "time" ) // FrameType identifies one protocol message kind. type FrameType string const ( FrameHello FrameType = "hello" FrameAuthChallenge FrameType = "auth_challenge" FrameAuthOK FrameType = "auth_ok" FrameAttach FrameType = "attach" FrameDetach FrameType = "detach" FrameProjectList FrameType = "project_list" FrameChrome FrameType = "chrome" FramePaneSnapshot FrameType = "pane_snapshot" FramePaneChunk FrameType = "pane_chunk" FrameLifecycle FrameType = "lifecycle" FrameAttention FrameType = "attention" FrameTrustPrompt FrameType = "trust_prompt" FrameInput FrameType = "input" FrameFocus FrameType = "focus" FrameSwitchProject FrameType = "switch_project" FrameOpenProject FrameType = "open_project" FramePaletteCommand FrameType = "palette_command" FrameTrustResponse FrameType = "trust_response" FrameResize FrameType = "resize" FrameList FrameType = "list" FrameStop FrameType = "stop" FrameError FrameType = "error" ) // Frame is the transport envelope. Payload is deliberately raw JSON so // network transports can frame without knowing every message type; loopback // transports may pass the same bytes without JSON re-encoding. type Frame struct { Type FrameType `json:"type"` RequestID string `json:"request_id,omitempty"` Payload json.RawMessage `json:"payload,omitempty"` } // NewFrame marshals payload into a protocol frame. func NewFrame[T any](typ FrameType, payload T) (Frame, error) { b, err := json.Marshal(payload) if err != nil { return Frame{}, fmt.Errorf("protocol: marshal %s: %w", typ, err) } return Frame{Type: typ, Payload: b}, nil } // Decode unmarshals f.Payload into v. func Decode[T any](f Frame) (T, error) { var v T if len(f.Payload) == 0 { return v, nil } if err := json.Unmarshal(f.Payload, &v); err != nil { return v, fmt.Errorf("protocol: decode %s: %w", f.Type, err) } return v, nil } type Hello struct { Version int `json:"version"` DaemonID string `json:"daemon_id,omitempty"` ClientID string `json:"client_id,omitempty"` ProjectKey string `json:"project_key,omitempty"` } type Attach struct { Token string `json:"token,omitempty"` ProjectKey string `json:"project_key,omitempty"` ProjectPath string `json:"project_path,omitempty"` TermSize Size `json:"term_size"` } type Detach struct { ClientID string `json:"client_id,omitempty"` } type Size struct { Cols uint16 `json:"cols"` Rows uint16 `json:"rows"` } type Project struct { Key string `json:"key"` Path string `json:"path"` Name string `json:"name"` LastActive time.Time `json:"last_active,omitempty"` TabCount int `json:"tab_count"` } type ProjectList struct { Projects []Project `json:"projects"` } type Chrome struct { ProjectKey string `json:"project_key"` Model json.RawMessage `json:"model"` } type PaneSnapshot struct { PaneID string `json:"pane_id"` Bytes []byte `json:"bytes"` Size Size `json:"size,omitempty"` DisplayOwner bool `json:"display_owner,omitempty"` } type PaneChunk struct { PaneID string `json:"pane_id"` Bytes []byte `json:"bytes"` Size Size `json:"size,omitempty"` DisplayOwner bool `json:"display_owner,omitempty"` } type LifecycleKind string const ( LifecycleSpawned LifecycleKind = "spawned" LifecycleExited LifecycleKind = "exited" LifecycleClosed LifecycleKind = "closed" LifecycleStateChanged LifecycleKind = "state_changed" ) type Lifecycle struct { Kind LifecycleKind `json:"kind"` ProjectKey string `json:"project_key,omitempty"` ChildID string `json:"child_id,omitempty"` Child json.RawMessage `json:"child,omitempty"` State string `json:"state,omitempty"` } type Input struct { PaneID string `json:"pane_id"` Bytes []byte `json:"bytes"` } type Focus struct { PaneID string `json:"pane_id,omitempty"` Pad string `json:"pad,omitempty"` } type SwitchProject struct { Key string `json:"key"` } type OpenProject struct { Path string `json:"path"` } type PaletteCommand struct { Kind string `json:"kind"` Data json.RawMessage `json:"data,omitempty"` } type TrustResponse struct { ProcessID string `json:"process_id"` Preset string `json:"preset"` Allow bool `json:"allow"` } type Resize struct { Size Size `json:"size"` } type Error struct { Message string `json:"message"` }