1package client
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "log/slog"
12 "net/http"
13 "time"
14
15 "github.com/charmbracelet/crush/internal/app"
16 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/history"
18 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/proto"
20 "github.com/charmbracelet/crush/internal/pubsub"
21 "github.com/charmbracelet/crush/internal/session"
22 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
23)
24
25// CreateWorkspace creates a new workspace on the server.
26func (c *Client) CreateWorkspace(ctx context.Context, ws proto.Workspace) (*proto.Workspace, error) {
27 rsp, err := c.post(ctx, "/workspaces", nil, jsonBody(ws), http.Header{"Content-Type": []string{"application/json"}})
28 if err != nil {
29 return nil, fmt.Errorf("failed to create workspace: %w", err)
30 }
31 defer rsp.Body.Close()
32 if rsp.StatusCode != http.StatusOK {
33 return nil, fmt.Errorf("failed to create workspace: status code %d", rsp.StatusCode)
34 }
35 var created proto.Workspace
36 if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
37 return nil, fmt.Errorf("failed to decode workspace: %w", err)
38 }
39 return &created, nil
40}
41
42// DeleteWorkspace deletes a workspace on the server.
43func (c *Client) DeleteWorkspace(ctx context.Context, id string) error {
44 rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
45 if err != nil {
46 return fmt.Errorf("failed to delete workspace: %w", err)
47 }
48 defer rsp.Body.Close()
49 if rsp.StatusCode != http.StatusOK {
50 return fmt.Errorf("failed to delete workspace: status code %d", rsp.StatusCode)
51 }
52 return nil
53}
54
55// SubscribeEvents subscribes to server-sent events for a workspace.
56func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, error) {
57 events := make(chan any, 100)
58 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/events", id), nil, http.Header{
59 "Accept": []string{"text/event-stream"},
60 "Cache-Control": []string{"no-cache"},
61 "Connection": []string{"keep-alive"},
62 })
63 if err != nil {
64 return nil, fmt.Errorf("failed to subscribe to events: %w", err)
65 }
66
67 if rsp.StatusCode != http.StatusOK {
68 return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
69 }
70
71 go func() {
72 defer rsp.Body.Close()
73
74 scr := bufio.NewReader(rsp.Body)
75 for {
76 line, err := scr.ReadBytes('\n')
77 if errors.Is(err, io.EOF) {
78 break
79 }
80 if err != nil {
81 slog.Error("Reading from events stream", "error", err)
82 time.Sleep(time.Second * 2)
83 continue
84 }
85 line = bytes.TrimSpace(line)
86 if len(line) == 0 {
87 continue
88 }
89
90 data, ok := bytes.CutPrefix(line, []byte("data:"))
91 if !ok {
92 slog.Warn("Invalid event format", "line", string(line))
93 continue
94 }
95
96 data = bytes.TrimSpace(data)
97
98 var event pubsub.Event[any]
99 if err := json.Unmarshal(data, &event); err != nil {
100 slog.Error("Unmarshaling event", "error", err)
101 continue
102 }
103
104 type alias pubsub.Event[any]
105 aux := &struct {
106 Payload json.RawMessage `json:"payload"`
107 *alias
108 }{
109 alias: (*alias)(&event),
110 }
111
112 if err := json.Unmarshal(data, &aux); err != nil {
113 slog.Error("Unmarshaling event payload", "error", err)
114 continue
115 }
116
117 var p pubsub.Payload
118 if err := json.Unmarshal(aux.Payload, &p); err != nil {
119 slog.Error("Unmarshaling event payload", "error", err)
120 continue
121 }
122
123 switch p.Type {
124 case pubsub.PayloadTypeLSPEvent:
125 var e pubsub.Event[proto.LSPEvent]
126 _ = json.Unmarshal(data, &e)
127 sendEvent(ctx, events, e)
128 case pubsub.PayloadTypeMCPEvent:
129 var e pubsub.Event[proto.MCPEvent]
130 _ = json.Unmarshal(data, &e)
131 sendEvent(ctx, events, e)
132 case pubsub.PayloadTypePermissionRequest:
133 var e pubsub.Event[proto.PermissionRequest]
134 _ = json.Unmarshal(data, &e)
135 sendEvent(ctx, events, e)
136 case pubsub.PayloadTypePermissionNotification:
137 var e pubsub.Event[proto.PermissionNotification]
138 _ = json.Unmarshal(data, &e)
139 sendEvent(ctx, events, e)
140 case pubsub.PayloadTypeMessage:
141 var e pubsub.Event[proto.Message]
142 _ = json.Unmarshal(data, &e)
143 sendEvent(ctx, events, e)
144 case pubsub.PayloadTypeSession:
145 var e pubsub.Event[proto.Session]
146 _ = json.Unmarshal(data, &e)
147 sendEvent(ctx, events, e)
148 case pubsub.PayloadTypeFile:
149 var e pubsub.Event[proto.File]
150 _ = json.Unmarshal(data, &e)
151 sendEvent(ctx, events, e)
152 case pubsub.PayloadTypeAgentEvent:
153 var e pubsub.Event[proto.AgentEvent]
154 _ = json.Unmarshal(data, &e)
155 sendEvent(ctx, events, e)
156 default:
157 slog.Warn("Unknown event type", "type", p.Type)
158 continue
159 }
160 }
161 }()
162
163 return events, nil
164}
165
166func sendEvent(ctx context.Context, evc chan any, ev any) {
167 slog.Info("Event received", "event", fmt.Sprintf("%T %+v", ev, ev))
168 select {
169 case evc <- ev:
170 case <-ctx.Done():
171 close(evc)
172 return
173 }
174}
175
176// GetLSPDiagnostics retrieves LSP diagnostics for a specific LSP client.
177func (c *Client) GetLSPDiagnostics(ctx context.Context, id string, lspName string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
178 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps/%s/diagnostics", id, lspName), nil, nil)
179 if err != nil {
180 return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
181 }
182 defer rsp.Body.Close()
183 if rsp.StatusCode != http.StatusOK {
184 return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
185 }
186 var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
187 if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
188 return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
189 }
190 return diagnostics, nil
191}
192
193// GetLSPs retrieves the LSP client states for a workspace.
194func (c *Client) GetLSPs(ctx context.Context, id string) (map[string]app.LSPClientInfo, error) {
195 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps", id), nil, nil)
196 if err != nil {
197 return nil, fmt.Errorf("failed to get LSPs: %w", err)
198 }
199 defer rsp.Body.Close()
200 if rsp.StatusCode != http.StatusOK {
201 return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
202 }
203 var lsps map[string]app.LSPClientInfo
204 if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
205 return nil, fmt.Errorf("failed to decode LSPs: %w", err)
206 }
207 return lsps, nil
208}
209
210// GetAgentSessionQueuedPrompts retrieves the number of queued prompts for a
211// session.
212func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) (int, error) {
213 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/queued", id, sessionID), nil, nil)
214 if err != nil {
215 return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
216 }
217 defer rsp.Body.Close()
218 if rsp.StatusCode != http.StatusOK {
219 return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
220 }
221 var count int
222 if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
223 return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
224 }
225 return count, nil
226}
227
228// ClearAgentSessionQueuedPrompts clears the queued prompts for a session.
229func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) error {
230 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/clear", id, sessionID), nil, nil, nil)
231 if err != nil {
232 return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
233 }
234 defer rsp.Body.Close()
235 if rsp.StatusCode != http.StatusOK {
236 return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
237 }
238 return nil
239}
240
241// GetAgentInfo retrieves the agent status for a workspace.
242func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) {
243 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, nil)
244 if err != nil {
245 return nil, fmt.Errorf("failed to get agent status: %w", err)
246 }
247 defer rsp.Body.Close()
248 if rsp.StatusCode != http.StatusOK {
249 return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
250 }
251 var info proto.AgentInfo
252 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
253 return nil, fmt.Errorf("failed to decode agent status: %w", err)
254 }
255 return &info, nil
256}
257
258// UpdateAgent triggers an agent model update on the server.
259func (c *Client) UpdateAgent(ctx context.Context, id string) error {
260 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/update", id), nil, nil, nil)
261 if err != nil {
262 return fmt.Errorf("failed to update agent: %w", err)
263 }
264 defer rsp.Body.Close()
265 if rsp.StatusCode != http.StatusOK {
266 return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
267 }
268 return nil
269}
270
271// SendMessage sends a message to the agent for a workspace.
272func (c *Client) SendMessage(ctx context.Context, id string, sessionID, prompt string, attachments ...message.Attachment) error {
273 protoAttachments := make([]proto.Attachment, len(attachments))
274 for i, a := range attachments {
275 protoAttachments[i] = proto.Attachment{
276 FilePath: a.FilePath,
277 FileName: a.FileName,
278 MimeType: a.MimeType,
279 Content: a.Content,
280 }
281 }
282 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, jsonBody(proto.AgentMessage{
283 SessionID: sessionID,
284 Prompt: prompt,
285 Attachments: protoAttachments,
286 }), http.Header{"Content-Type": []string{"application/json"}})
287 if err != nil {
288 return fmt.Errorf("failed to send message to agent: %w", err)
289 }
290 defer rsp.Body.Close()
291 if rsp.StatusCode != http.StatusOK {
292 return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
293 }
294 return nil
295}
296
297// GetAgentSessionInfo retrieves the agent session info for a workspace.
298func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) {
299 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s", id, sessionID), nil, nil)
300 if err != nil {
301 return nil, fmt.Errorf("failed to get session agent info: %w", err)
302 }
303 defer rsp.Body.Close()
304 if rsp.StatusCode != http.StatusOK {
305 return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
306 }
307 var info proto.AgentSession
308 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
309 return nil, fmt.Errorf("failed to decode session agent info: %w", err)
310 }
311 return &info, nil
312}
313
314// AgentSummarizeSession requests a session summarization.
315func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
316 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
317 if err != nil {
318 return fmt.Errorf("failed to summarize session: %w", err)
319 }
320 defer rsp.Body.Close()
321 if rsp.StatusCode != http.StatusOK {
322 return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
323 }
324 return nil
325}
326
327// InitiateAgentProcessing triggers agent initialization on the server.
328func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error {
329 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/init", id), nil, nil, nil)
330 if err != nil {
331 return fmt.Errorf("failed to initiate session agent processing: %w", err)
332 }
333 defer rsp.Body.Close()
334 if rsp.StatusCode != http.StatusOK {
335 return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
336 }
337 return nil
338}
339
340// ListMessages retrieves all messages for a session.
341func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]message.Message, error) {
342 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
343 if err != nil {
344 return nil, fmt.Errorf("failed to get messages: %w", err)
345 }
346 defer rsp.Body.Close()
347 if rsp.StatusCode != http.StatusOK {
348 return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
349 }
350 var messages []message.Message
351 if err := json.NewDecoder(rsp.Body).Decode(&messages); err != nil {
352 return nil, fmt.Errorf("failed to decode messages: %w", err)
353 }
354 return messages, nil
355}
356
357// GetSession retrieves a specific session.
358func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*session.Session, error) {
359 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
360 if err != nil {
361 return nil, fmt.Errorf("failed to get session: %w", err)
362 }
363 defer rsp.Body.Close()
364 if rsp.StatusCode != http.StatusOK {
365 return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
366 }
367 var sess session.Session
368 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
369 return nil, fmt.Errorf("failed to decode session: %w", err)
370 }
371 return &sess, nil
372}
373
374// ListSessionHistoryFiles retrieves history files for a session.
375func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]history.File, error) {
376 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/history", id, sessionID), nil, nil)
377 if err != nil {
378 return nil, fmt.Errorf("failed to get session history files: %w", err)
379 }
380 defer rsp.Body.Close()
381 if rsp.StatusCode != http.StatusOK {
382 return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
383 }
384 var files []history.File
385 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
386 return nil, fmt.Errorf("failed to decode session history files: %w", err)
387 }
388 return files, nil
389}
390
391// CreateSession creates a new session in a workspace.
392func (c *Client) CreateSession(ctx context.Context, id string, title string) (*session.Session, error) {
393 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, jsonBody(session.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}})
394 if err != nil {
395 return nil, fmt.Errorf("failed to create session: %w", err)
396 }
397 defer rsp.Body.Close()
398 if rsp.StatusCode != http.StatusOK {
399 return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
400 }
401 var sess session.Session
402 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
403 return nil, fmt.Errorf("failed to decode session: %w", err)
404 }
405 return &sess, nil
406}
407
408// ListSessions lists all sessions in a workspace.
409func (c *Client) ListSessions(ctx context.Context, id string) ([]session.Session, error) {
410 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, nil)
411 if err != nil {
412 return nil, fmt.Errorf("failed to get sessions: %w", err)
413 }
414 defer rsp.Body.Close()
415 if rsp.StatusCode != http.StatusOK {
416 return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
417 }
418 var sessions []session.Session
419 if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
420 return nil, fmt.Errorf("failed to decode sessions: %w", err)
421 }
422 return sessions, nil
423}
424
425// GrantPermission grants a permission on a workspace.
426func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) error {
427 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
428 if err != nil {
429 return fmt.Errorf("failed to grant permission: %w", err)
430 }
431 defer rsp.Body.Close()
432 if rsp.StatusCode != http.StatusOK {
433 return fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
434 }
435 return nil
436}
437
438// SetPermissionsSkipRequests sets the skip-requests flag for a workspace.
439func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error {
440 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, jsonBody(proto.PermissionSkipRequest{Skip: skip}), http.Header{"Content-Type": []string{"application/json"}})
441 if err != nil {
442 return fmt.Errorf("failed to set permissions skip requests: %w", err)
443 }
444 defer rsp.Body.Close()
445 if rsp.StatusCode != http.StatusOK {
446 return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
447 }
448 return nil
449}
450
451// GetPermissionsSkipRequests retrieves the skip-requests flag for a workspace.
452func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) {
453 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, nil)
454 if err != nil {
455 return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
456 }
457 defer rsp.Body.Close()
458 if rsp.StatusCode != http.StatusOK {
459 return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
460 }
461 var skip proto.PermissionSkipRequest
462 if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
463 return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
464 }
465 return skip.Skip, nil
466}
467
468// GetConfig retrieves the workspace-specific configuration.
469func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) {
470 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/config", id), nil, nil)
471 if err != nil {
472 return nil, fmt.Errorf("failed to get config: %w", err)
473 }
474 defer rsp.Body.Close()
475 if rsp.StatusCode != http.StatusOK {
476 return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
477 }
478 var cfg config.Config
479 if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
480 return nil, fmt.Errorf("failed to decode config: %w", err)
481 }
482 return &cfg, nil
483}
484
485func jsonBody(v any) *bytes.Buffer {
486 b := new(bytes.Buffer)
487 m, _ := json.Marshal(v)
488 b.Write(m)
489 return b
490}