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}