proto.go

  1package proto
  2
  3import (
  4	"encoding/json"
  5	"errors"
  6	"time"
  7
  8	"charm.land/catwalk/pkg/catwalk"
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/lsp"
 11)
 12
 13// Workspace represents a running app.App workspace with its associated
 14// resources and state.
 15type Workspace struct {
 16	ID       string         `json:"id"`
 17	Path     string         `json:"path"`
 18	YOLO     bool           `json:"yolo,omitempty"`
 19	Debug    bool           `json:"debug,omitempty"`
 20	DataDir  string         `json:"data_dir,omitempty"`
 21	Version  string         `json:"version,omitempty"`
 22	ClientID string         `json:"client_id,omitempty"`
 23	Config   *config.Config `json:"config,omitempty"`
 24	Env      []string       `json:"env,omitempty"`
 25	// Skills carries the snapshot of skill discovery state at workspace
 26	// creation time. Subsequent updates flow through the SSE event
 27	// stream.
 28	Skills []SkillState `json:"skills,omitempty"`
 29}
 30
 31// Error represents an error response.
 32type Error struct {
 33	Message string `json:"message"`
 34}
 35
 36// ConfigChanged is published whenever the workspace's configuration is
 37// mutated by a backend operation. Clients react by re-fetching the
 38// workspace snapshot so cached config stays in sync across subscribers.
 39type ConfigChanged struct {
 40	WorkspaceID string `json:"workspace_id"`
 41}
 42
 43// CurrentSession is the request body for the per-client
 44// current-session endpoint. An empty SessionID clears the entry.
 45type CurrentSession struct {
 46	SessionID string `json:"session_id"`
 47}
 48
 49// RunComplete is the authoritative end-of-run signal for a session,
 50// emitted exactly once per top-level agent turn after all message
 51// updates for the turn have flushed. Clients that need a reliable
 52// completion contract (notably `crush run` in client/server mode)
 53// should listen for this event filtered by RunID (preferred) — or
 54// by SessionID when no RunID was supplied — and use Text and
 55// MessageID to reconcile any output they have already streamed from
 56// earlier message events. Error is non-empty when the run terminated
 57// with an error; Cancelled is true when terminated due to context
 58// cancellation.
 59//
 60// RunID echoes the value the caller set on AgentMessage.RunID. It is
 61// the only safe correlator when the caller's prompt was queued
 62// behind a busy session: another turn's RunComplete for the same
 63// SessionID may arrive first, and filtering by SessionID alone
 64// would terminate the caller before its own turn ran.
 65type RunComplete struct {
 66	SessionID string `json:"session_id"`
 67	RunID     string `json:"run_id,omitempty"`
 68	MessageID string `json:"message_id"`
 69	Text      string `json:"text,omitempty"`
 70	Error     string `json:"error,omitempty"`
 71	Cancelled bool   `json:"cancelled,omitempty"`
 72}
 73
 74// SkillInfo describes a visible skill exposed to a frontend.
 75type SkillInfo struct {
 76	ID          string `json:"id"`
 77	Name        string `json:"name"`
 78	Description string `json:"description"`
 79	Label       string `json:"label"`
 80	Source      string `json:"source"`
 81}
 82
 83// ReadSkillRequest is the request body for reading a skill's content.
 84type ReadSkillRequest struct {
 85	SkillID string `json:"skill_id"`
 86}
 87
 88// ReadSkillResponse is the response for reading a skill's content.
 89type ReadSkillResponse struct {
 90	Content []byte          `json:"content"`
 91	Result  SkillReadResult `json:"result"`
 92}
 93
 94// SkillReadResult holds metadata about a skill returned alongside its
 95// content.
 96type SkillReadResult struct {
 97	Name        string `json:"name"`
 98	Description string `json:"description"`
 99	Source      string `json:"source"`
100	Builtin     bool   `json:"builtin"`
101}
102
103// AgentInfo represents information about the agent.
104type AgentInfo struct {
105	IsBusy   bool                 `json:"is_busy"`
106	IsReady  bool                 `json:"is_ready"`
107	Model    catwalk.Model        `json:"model"`
108	ModelCfg config.SelectedModel `json:"model_cfg"`
109}
110
111// IsZero checks if the AgentInfo is zero-valued.
112func (a AgentInfo) IsZero() bool {
113	return !a.IsBusy && !a.IsReady && a.Model.ID == ""
114}
115
116// AgentMessage represents a message sent to the agent.
117//
118// RunID, when non-empty, is echoed back on the [RunComplete] event
119// emitted for the resulting turn. Callers that need to correlate a
120// specific SendMessage with its terminal event (notably
121// `crush run`, which may attach to a busy session whose currently
122// running turn finishes first) should set it to a fresh unique
123// value before the request. Server-side propagation flows through
124// agent.WithRunID on the request context into the
125// SessionAgentCall; it is preserved across the busy-session queue.
126// When empty the resulting RunComplete carries an empty RunID and
127// callers must fall back to SessionID-only filtering, which
128// remains correct only when no other turns are in flight for the
129// same session.
130type AgentMessage struct {
131	SessionID   string       `json:"session_id"`
132	RunID       string       `json:"run_id,omitempty"`
133	Prompt      string       `json:"prompt"`
134	Attachments []Attachment `json:"attachments,omitempty"`
135}
136
137// AgentSession represents a session with its busy status.
138type AgentSession struct {
139	Session
140	IsBusy bool `json:"is_busy"`
141}
142
143// IsZero checks if the AgentSession is zero-valued.
144func (a AgentSession) IsZero() bool {
145	return a.ID == "" && !a.IsBusy
146}
147
148// PermissionAction represents an action taken on a permission request.
149type PermissionAction string
150
151const (
152	PermissionAllow           PermissionAction = "allow"
153	PermissionAllowForSession PermissionAction = "allow_session"
154	PermissionDeny            PermissionAction = "deny"
155)
156
157// MarshalText implements the [encoding.TextMarshaler] interface.
158func (p PermissionAction) MarshalText() ([]byte, error) {
159	return []byte(p), nil
160}
161
162// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
163func (p *PermissionAction) UnmarshalText(text []byte) error {
164	*p = PermissionAction(text)
165	return nil
166}
167
168// PermissionGrant represents a permission grant request.
169type PermissionGrant struct {
170	Permission PermissionRequest `json:"permission"`
171	Action     PermissionAction  `json:"action"`
172}
173
174// PermissionGrantResponse is the server's response to a permission
175// grant call. Resolved is true when this call resolved the pending
176// request, and false when the request had already been resolved by a
177// previous caller (e.g., another client in a multi-subscriber UI). A
178// false value is not an error.
179type PermissionGrantResponse struct {
180	Resolved bool `json:"resolved"`
181}
182
183// PermissionSkipRequest represents a request to skip permission prompts.
184type PermissionSkipRequest struct {
185	Skip bool `json:"skip"`
186}
187
188// LSPEventType represents the type of LSP event.
189type LSPEventType string
190
191const (
192	LSPEventStateChanged       LSPEventType = "state_changed"
193	LSPEventDiagnosticsChanged LSPEventType = "diagnostics_changed"
194)
195
196// MarshalText implements the [encoding.TextMarshaler] interface.
197func (e LSPEventType) MarshalText() ([]byte, error) {
198	return []byte(e), nil
199}
200
201// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
202func (e *LSPEventType) UnmarshalText(data []byte) error {
203	*e = LSPEventType(data)
204	return nil
205}
206
207// LSPEvent represents an event in the LSP system.
208type LSPEvent struct {
209	Type            LSPEventType    `json:"type"`
210	Name            string          `json:"name"`
211	State           lsp.ServerState `json:"state"`
212	Error           error           `json:"error,omitempty"`
213	DiagnosticCount int             `json:"diagnostic_count,omitempty"`
214}
215
216// MarshalJSON implements the [json.Marshaler] interface.
217func (e LSPEvent) MarshalJSON() ([]byte, error) {
218	type Alias LSPEvent
219	return json.Marshal(&struct {
220		Error string `json:"error,omitempty"`
221		Alias
222	}{
223		Error: func() string {
224			if e.Error != nil {
225				return e.Error.Error()
226			}
227			return ""
228		}(),
229		Alias: Alias(e),
230	})
231}
232
233// UnmarshalJSON implements the [json.Unmarshaler] interface.
234func (e *LSPEvent) UnmarshalJSON(data []byte) error {
235	type Alias LSPEvent
236	aux := &struct {
237		Error string `json:"error,omitempty"`
238		Alias
239	}{
240		Alias: Alias(*e),
241	}
242	if err := json.Unmarshal(data, &aux); err != nil {
243		return err
244	}
245	*e = LSPEvent(aux.Alias)
246	if aux.Error != "" {
247		e.Error = errors.New(aux.Error)
248	}
249	return nil
250}
251
252// LSPClientInfo holds information about an LSP client's state.
253type LSPClientInfo struct {
254	Name            string          `json:"name"`
255	State           lsp.ServerState `json:"state"`
256	Error           error           `json:"error,omitempty"`
257	DiagnosticCount int             `json:"diagnostic_count,omitempty"`
258	ConnectedAt     time.Time       `json:"connected_at"`
259}
260
261// MarshalJSON implements the [json.Marshaler] interface.
262func (i LSPClientInfo) MarshalJSON() ([]byte, error) {
263	type Alias LSPClientInfo
264	return json.Marshal(&struct {
265		Error string `json:"error,omitempty"`
266		Alias
267	}{
268		Error: func() string {
269			if i.Error != nil {
270				return i.Error.Error()
271			}
272			return ""
273		}(),
274		Alias: Alias(i),
275	})
276}
277
278// UnmarshalJSON implements the [json.Unmarshaler] interface.
279func (i *LSPClientInfo) UnmarshalJSON(data []byte) error {
280	type Alias LSPClientInfo
281	aux := &struct {
282		Error string `json:"error,omitempty"`
283		Alias
284	}{
285		Alias: Alias(*i),
286	}
287	if err := json.Unmarshal(data, &aux); err != nil {
288		return err
289	}
290	*i = LSPClientInfo(aux.Alias)
291	if aux.Error != "" {
292		i.Error = errors.New(aux.Error)
293	}
294	return nil
295}