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	UserInvocable bool   `json:"user_invocable"`
 82}
 83
 84// ReadSkillRequest is the request body for reading a skill's content.
 85type ReadSkillRequest struct {
 86	SkillID string `json:"skill_id"`
 87}
 88
 89// ReadSkillResponse is the response for reading a skill's content.
 90type ReadSkillResponse struct {
 91	Content []byte          `json:"content"`
 92	Result  SkillReadResult `json:"result"`
 93}
 94
 95// SkillReadResult holds metadata about a skill returned alongside its
 96// content.
 97type SkillReadResult struct {
 98	Name        string `json:"name"`
 99	Description string `json:"description"`
100	Source      string `json:"source"`
101	Builtin     bool   `json:"builtin"`
102}
103
104// AgentInfo represents information about the agent.
105type AgentInfo struct {
106	IsBusy   bool                 `json:"is_busy"`
107	IsReady  bool                 `json:"is_ready"`
108	Model    catwalk.Model        `json:"model"`
109	ModelCfg config.SelectedModel `json:"model_cfg"`
110}
111
112// IsZero checks if the AgentInfo is zero-valued.
113func (a AgentInfo) IsZero() bool {
114	return !a.IsBusy && !a.IsReady && a.Model.ID == ""
115}
116
117// AgentMessage represents a message sent to the agent.
118//
119// RunID, when non-empty, is echoed back on the [RunComplete] event
120// emitted for the resulting turn. Callers that need to correlate a
121// specific SendMessage with its terminal event (notably
122// `crush run`, which may attach to a busy session whose currently
123// running turn finishes first) should set it to a fresh unique
124// value before the request. Server-side propagation flows through
125// agent.WithRunID on the request context into the
126// SessionAgentCall; it is preserved across the busy-session queue.
127// When empty the resulting RunComplete carries an empty RunID and
128// callers must fall back to SessionID-only filtering, which
129// remains correct only when no other turns are in flight for the
130// same session.
131type AgentMessage struct {
132	SessionID   string       `json:"session_id"`
133	RunID       string       `json:"run_id,omitempty"`
134	Prompt      string       `json:"prompt"`
135	Attachments []Attachment `json:"attachments,omitempty"`
136}
137
138// AgentSession represents a session with its busy status.
139type AgentSession struct {
140	Session
141	IsBusy bool `json:"is_busy"`
142}
143
144// IsZero checks if the AgentSession is zero-valued.
145func (a AgentSession) IsZero() bool {
146	return a.ID == "" && !a.IsBusy
147}
148
149// PermissionAction represents an action taken on a permission request.
150type PermissionAction string
151
152const (
153	PermissionAllow           PermissionAction = "allow"
154	PermissionAllowForSession PermissionAction = "allow_session"
155	PermissionDeny            PermissionAction = "deny"
156)
157
158// MarshalText implements the [encoding.TextMarshaler] interface.
159func (p PermissionAction) MarshalText() ([]byte, error) {
160	return []byte(p), nil
161}
162
163// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
164func (p *PermissionAction) UnmarshalText(text []byte) error {
165	*p = PermissionAction(text)
166	return nil
167}
168
169// PermissionGrant represents a permission grant request.
170type PermissionGrant struct {
171	Permission PermissionRequest `json:"permission"`
172	Action     PermissionAction  `json:"action"`
173}
174
175// PermissionGrantResponse is the server's response to a permission
176// grant call. Resolved is true when this call resolved the pending
177// request, and false when the request had already been resolved by a
178// previous caller (e.g., another client in a multi-subscriber UI). A
179// false value is not an error.
180type PermissionGrantResponse struct {
181	Resolved bool `json:"resolved"`
182}
183
184// PermissionSkipRequest represents a request to skip permission prompts.
185type PermissionSkipRequest struct {
186	Skip bool `json:"skip"`
187}
188
189// LSPEventType represents the type of LSP event.
190type LSPEventType string
191
192const (
193	LSPEventStateChanged       LSPEventType = "state_changed"
194	LSPEventDiagnosticsChanged LSPEventType = "diagnostics_changed"
195)
196
197// MarshalText implements the [encoding.TextMarshaler] interface.
198func (e LSPEventType) MarshalText() ([]byte, error) {
199	return []byte(e), nil
200}
201
202// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
203func (e *LSPEventType) UnmarshalText(data []byte) error {
204	*e = LSPEventType(data)
205	return nil
206}
207
208// LSPEvent represents an event in the LSP system.
209type LSPEvent struct {
210	Type            LSPEventType    `json:"type"`
211	Name            string          `json:"name"`
212	State           lsp.ServerState `json:"state"`
213	Error           error           `json:"error,omitempty"`
214	DiagnosticCount int             `json:"diagnostic_count,omitempty"`
215}
216
217// MarshalJSON implements the [json.Marshaler] interface.
218func (e LSPEvent) MarshalJSON() ([]byte, error) {
219	type Alias LSPEvent
220	return json.Marshal(&struct {
221		Error string `json:"error,omitempty"`
222		Alias
223	}{
224		Error: func() string {
225			if e.Error != nil {
226				return e.Error.Error()
227			}
228			return ""
229		}(),
230		Alias: Alias(e),
231	})
232}
233
234// UnmarshalJSON implements the [json.Unmarshaler] interface.
235func (e *LSPEvent) UnmarshalJSON(data []byte) error {
236	type Alias LSPEvent
237	aux := &struct {
238		Error string `json:"error,omitempty"`
239		Alias
240	}{
241		Alias: Alias(*e),
242	}
243	if err := json.Unmarshal(data, &aux); err != nil {
244		return err
245	}
246	*e = LSPEvent(aux.Alias)
247	if aux.Error != "" {
248		e.Error = errors.New(aux.Error)
249	}
250	return nil
251}
252
253// LSPClientInfo holds information about an LSP client's state.
254type LSPClientInfo struct {
255	Name            string          `json:"name"`
256	State           lsp.ServerState `json:"state"`
257	Error           error           `json:"error,omitempty"`
258	DiagnosticCount int             `json:"diagnostic_count,omitempty"`
259	ConnectedAt     time.Time       `json:"connected_at"`
260}
261
262// MarshalJSON implements the [json.Marshaler] interface.
263func (i LSPClientInfo) MarshalJSON() ([]byte, error) {
264	type Alias LSPClientInfo
265	return json.Marshal(&struct {
266		Error string `json:"error,omitempty"`
267		Alias
268	}{
269		Error: func() string {
270			if i.Error != nil {
271				return i.Error.Error()
272			}
273			return ""
274		}(),
275		Alias: Alias(i),
276	})
277}
278
279// UnmarshalJSON implements the [json.Unmarshaler] interface.
280func (i *LSPClientInfo) UnmarshalJSON(data []byte) error {
281	type Alias LSPClientInfo
282	aux := &struct {
283		Error string `json:"error,omitempty"`
284		Alias
285	}{
286		Alias: Alias(*i),
287	}
288	if err := json.Unmarshal(data, &aux); err != nil {
289		return err
290	}
291	*i = LSPClientInfo(aux.Alias)
292	if aux.Error != "" {
293		i.Error = errors.New(aux.Error)
294	}
295	return nil
296}