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 "net/url"
14 "time"
15
16 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/proto"
19 "github.com/charmbracelet/crush/internal/pubsub"
20 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
21)
22
23// ListWorkspaces retrieves all workspaces from the server.
24func (c *Client) ListWorkspaces(ctx context.Context) ([]proto.Workspace, error) {
25 rsp, err := c.get(ctx, "/workspaces", nil, nil)
26 if err != nil {
27 return nil, fmt.Errorf("failed to list workspaces: %w", err)
28 }
29 defer rsp.Body.Close()
30 if rsp.StatusCode != http.StatusOK {
31 return nil, fmt.Errorf("failed to list workspaces: status code %d", rsp.StatusCode)
32 }
33 var workspaces []proto.Workspace
34 if err := json.NewDecoder(rsp.Body).Decode(&workspaces); err != nil {
35 return nil, fmt.Errorf("failed to decode workspaces: %w", err)
36 }
37 return workspaces, nil
38}
39
40// CreateWorkspace creates a new workspace on the server.
41func (c *Client) CreateWorkspace(ctx context.Context, ws proto.Workspace) (*proto.Workspace, error) {
42 rsp, err := c.post(ctx, "/workspaces", nil, jsonBody(ws), http.Header{"Content-Type": []string{"application/json"}})
43 if err != nil {
44 return nil, fmt.Errorf("failed to create workspace: %w", err)
45 }
46 defer rsp.Body.Close()
47 if rsp.StatusCode != http.StatusOK {
48 return nil, fmt.Errorf("failed to create workspace: status code %d", rsp.StatusCode)
49 }
50 var created proto.Workspace
51 if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
52 return nil, fmt.Errorf("failed to decode workspace: %w", err)
53 }
54 return &created, nil
55}
56
57// GetWorkspace retrieves a workspace from the server.
58func (c *Client) GetWorkspace(ctx context.Context, id string) (*proto.Workspace, error) {
59 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
60 if err != nil {
61 return nil, fmt.Errorf("failed to get workspace: %w", err)
62 }
63 defer rsp.Body.Close()
64 if rsp.StatusCode != http.StatusOK {
65 return nil, fmt.Errorf("failed to get workspace: status code %d", rsp.StatusCode)
66 }
67 var ws proto.Workspace
68 if err := json.NewDecoder(rsp.Body).Decode(&ws); err != nil {
69 return nil, fmt.Errorf("failed to decode workspace: %w", err)
70 }
71 return &ws, nil
72}
73
74// DeleteWorkspace deletes a workspace on the server.
75func (c *Client) DeleteWorkspace(ctx context.Context, id string) error {
76 rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
77 if err != nil {
78 return fmt.Errorf("failed to delete workspace: %w", err)
79 }
80 defer rsp.Body.Close()
81 if rsp.StatusCode != http.StatusOK {
82 return fmt.Errorf("failed to delete workspace: status code %d", rsp.StatusCode)
83 }
84 return nil
85}
86
87// SubscribeEvents subscribes to server-sent events for a workspace.
88func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, error) {
89 events := make(chan any, 100)
90 //nolint:bodyclose
91 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/events", id), nil, http.Header{
92 "Accept": []string{"text/event-stream"},
93 "Cache-Control": []string{"no-cache"},
94 "Connection": []string{"keep-alive"},
95 })
96 if err != nil {
97 return nil, fmt.Errorf("failed to subscribe to events: %w", err)
98 }
99
100 if rsp.StatusCode != http.StatusOK {
101 rsp.Body.Close()
102 return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
103 }
104
105 go func() {
106 defer rsp.Body.Close()
107
108 scr := bufio.NewReader(rsp.Body)
109 for {
110 line, err := scr.ReadBytes('\n')
111 if errors.Is(err, io.EOF) {
112 break
113 }
114 if err != nil {
115 slog.Error("Reading from events stream", "error", err)
116 time.Sleep(time.Second * 2)
117 continue
118 }
119 line = bytes.TrimSpace(line)
120 if len(line) == 0 {
121 continue
122 }
123
124 data, ok := bytes.CutPrefix(line, []byte("data:"))
125 if !ok {
126 slog.Warn("Invalid event format", "line", string(line))
127 continue
128 }
129
130 data = bytes.TrimSpace(data)
131
132 var p pubsub.Payload
133 if err := json.Unmarshal(data, &p); err != nil {
134 slog.Error("Unmarshaling event envelope", "error", err)
135 continue
136 }
137
138 switch p.Type {
139 case pubsub.PayloadTypeLSPEvent:
140 var e pubsub.Event[proto.LSPEvent]
141 _ = json.Unmarshal(p.Payload, &e)
142 sendEvent(ctx, events, e)
143 case pubsub.PayloadTypeMCPEvent:
144 var e pubsub.Event[proto.MCPEvent]
145 _ = json.Unmarshal(p.Payload, &e)
146 sendEvent(ctx, events, e)
147 case pubsub.PayloadTypePermissionRequest:
148 var e pubsub.Event[proto.PermissionRequest]
149 _ = json.Unmarshal(p.Payload, &e)
150 sendEvent(ctx, events, e)
151 case pubsub.PayloadTypePermissionNotification:
152 var e pubsub.Event[proto.PermissionNotification]
153 _ = json.Unmarshal(p.Payload, &e)
154 sendEvent(ctx, events, e)
155 case pubsub.PayloadTypeMessage:
156 var e pubsub.Event[proto.Message]
157 _ = json.Unmarshal(p.Payload, &e)
158 sendEvent(ctx, events, e)
159 case pubsub.PayloadTypeSession:
160 var e pubsub.Event[proto.Session]
161 _ = json.Unmarshal(p.Payload, &e)
162 sendEvent(ctx, events, e)
163 case pubsub.PayloadTypeFile:
164 var e pubsub.Event[proto.File]
165 _ = json.Unmarshal(p.Payload, &e)
166 sendEvent(ctx, events, e)
167 case pubsub.PayloadTypeAgentEvent:
168 var e pubsub.Event[proto.AgentEvent]
169 _ = json.Unmarshal(p.Payload, &e)
170 sendEvent(ctx, events, e)
171 default:
172 slog.Warn("Unknown event type", "type", p.Type)
173 continue
174 }
175 }
176 }()
177
178 return events, nil
179}
180
181func sendEvent(ctx context.Context, evc chan any, ev any) {
182 slog.Info("Event received", "event", fmt.Sprintf("%T %+v", ev, ev))
183 select {
184 case evc <- ev:
185 case <-ctx.Done():
186 close(evc)
187 return
188 }
189}
190
191// GetLSPDiagnostics retrieves LSP diagnostics for a specific LSP client.
192func (c *Client) GetLSPDiagnostics(ctx context.Context, id string, lspName string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
193 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps/%s/diagnostics", id, lspName), nil, nil)
194 if err != nil {
195 return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
196 }
197 defer rsp.Body.Close()
198 if rsp.StatusCode != http.StatusOK {
199 return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
200 }
201 var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
202 if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
203 return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
204 }
205 return diagnostics, nil
206}
207
208// GetLSPs retrieves the LSP client states for a workspace.
209func (c *Client) GetLSPs(ctx context.Context, id string) (map[string]proto.LSPClientInfo, error) {
210 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps", id), nil, nil)
211 if err != nil {
212 return nil, fmt.Errorf("failed to get LSPs: %w", err)
213 }
214 defer rsp.Body.Close()
215 if rsp.StatusCode != http.StatusOK {
216 return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
217 }
218 var lsps map[string]proto.LSPClientInfo
219 if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
220 return nil, fmt.Errorf("failed to decode LSPs: %w", err)
221 }
222 return lsps, nil
223}
224
225// MCPGetStates retrieves the MCP client states for a workspace.
226func (c *Client) MCPGetStates(ctx context.Context, id string) (map[string]proto.MCPClientInfo, error) {
227 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/mcp/states", id), nil, nil)
228 if err != nil {
229 return nil, fmt.Errorf("failed to get MCP states: %w", err)
230 }
231 defer rsp.Body.Close()
232 if rsp.StatusCode != http.StatusOK {
233 return nil, fmt.Errorf("failed to get MCP states: status code %d", rsp.StatusCode)
234 }
235 var states map[string]proto.MCPClientInfo
236 if err := json.NewDecoder(rsp.Body).Decode(&states); err != nil {
237 return nil, fmt.Errorf("failed to decode MCP states: %w", err)
238 }
239 return states, nil
240}
241
242// MCPRefreshPrompts refreshes prompts for a named MCP client.
243func (c *Client) MCPRefreshPrompts(ctx context.Context, id, name string) error {
244 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-prompts", id), nil,
245 jsonBody(struct {
246 Name string `json:"name"`
247 }{Name: name}),
248 http.Header{"Content-Type": []string{"application/json"}})
249 if err != nil {
250 return fmt.Errorf("failed to refresh MCP prompts: %w", err)
251 }
252 defer rsp.Body.Close()
253 if rsp.StatusCode != http.StatusOK {
254 return fmt.Errorf("failed to refresh MCP prompts: status code %d", rsp.StatusCode)
255 }
256 return nil
257}
258
259// MCPRefreshResources refreshes resources for a named MCP client.
260func (c *Client) MCPRefreshResources(ctx context.Context, id, name string) error {
261 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-resources", id), nil,
262 jsonBody(struct {
263 Name string `json:"name"`
264 }{Name: name}),
265 http.Header{"Content-Type": []string{"application/json"}})
266 if err != nil {
267 return fmt.Errorf("failed to refresh MCP resources: %w", err)
268 }
269 defer rsp.Body.Close()
270 if rsp.StatusCode != http.StatusOK {
271 return fmt.Errorf("failed to refresh MCP resources: status code %d", rsp.StatusCode)
272 }
273 return nil
274}
275
276// GetAgentSessionQueuedPrompts retrieves the number of queued prompts for a
277// session.
278func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) (int, error) {
279 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/queued", id, sessionID), nil, nil)
280 if err != nil {
281 return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
282 }
283 defer rsp.Body.Close()
284 if rsp.StatusCode != http.StatusOK {
285 return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
286 }
287 var count int
288 if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
289 return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
290 }
291 return count, nil
292}
293
294// ClearAgentSessionQueuedPrompts clears the queued prompts for a session.
295func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) error {
296 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/clear", id, sessionID), nil, nil, nil)
297 if err != nil {
298 return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
299 }
300 defer rsp.Body.Close()
301 if rsp.StatusCode != http.StatusOK {
302 return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
303 }
304 return nil
305}
306
307// GetAgentInfo retrieves the agent status for a workspace.
308func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) {
309 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, nil)
310 if err != nil {
311 return nil, fmt.Errorf("failed to get agent status: %w", err)
312 }
313 defer rsp.Body.Close()
314 if rsp.StatusCode != http.StatusOK {
315 return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
316 }
317 var info proto.AgentInfo
318 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
319 return nil, fmt.Errorf("failed to decode agent status: %w", err)
320 }
321 return &info, nil
322}
323
324// UpdateAgent triggers an agent model update on the server.
325func (c *Client) UpdateAgent(ctx context.Context, id string) error {
326 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/update", id), nil, nil, nil)
327 if err != nil {
328 return fmt.Errorf("failed to update agent: %w", err)
329 }
330 defer rsp.Body.Close()
331 if rsp.StatusCode != http.StatusOK {
332 return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
333 }
334 return nil
335}
336
337// SendMessage sends a message to the agent for a workspace.
338func (c *Client) SendMessage(ctx context.Context, id string, sessionID, prompt string, attachments ...message.Attachment) error {
339 protoAttachments := make([]proto.Attachment, len(attachments))
340 for i, a := range attachments {
341 protoAttachments[i] = proto.Attachment{
342 FilePath: a.FilePath,
343 FileName: a.FileName,
344 MimeType: a.MimeType,
345 Content: a.Content,
346 }
347 }
348 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, jsonBody(proto.AgentMessage{
349 SessionID: sessionID,
350 Prompt: prompt,
351 Attachments: protoAttachments,
352 }), http.Header{"Content-Type": []string{"application/json"}})
353 if err != nil {
354 return fmt.Errorf("failed to send message to agent: %w", err)
355 }
356 defer rsp.Body.Close()
357 if rsp.StatusCode != http.StatusOK {
358 return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
359 }
360 return nil
361}
362
363// GetAgentSessionInfo retrieves the agent session info for a workspace.
364func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) {
365 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s", id, sessionID), nil, nil)
366 if err != nil {
367 return nil, fmt.Errorf("failed to get session agent info: %w", err)
368 }
369 defer rsp.Body.Close()
370 if rsp.StatusCode != http.StatusOK {
371 return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
372 }
373 var info proto.AgentSession
374 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
375 return nil, fmt.Errorf("failed to decode session agent info: %w", err)
376 }
377 return &info, nil
378}
379
380// AgentSummarizeSession requests a session summarization.
381func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
382 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
383 if err != nil {
384 return fmt.Errorf("failed to summarize session: %w", err)
385 }
386 defer rsp.Body.Close()
387 if rsp.StatusCode != http.StatusOK {
388 return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
389 }
390 return nil
391}
392
393// InitiateAgentProcessing triggers agent initialization on the server.
394func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error {
395 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/init", id), nil, nil, nil)
396 if err != nil {
397 return fmt.Errorf("failed to initiate session agent processing: %w", err)
398 }
399 defer rsp.Body.Close()
400 if rsp.StatusCode != http.StatusOK {
401 return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
402 }
403 return nil
404}
405
406// ListMessages retrieves all messages for a session as proto types.
407func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
408 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
409 if err != nil {
410 return nil, fmt.Errorf("failed to get messages: %w", err)
411 }
412 defer rsp.Body.Close()
413 if rsp.StatusCode != http.StatusOK {
414 return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
415 }
416 var msgs []proto.Message
417 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
418 return nil, fmt.Errorf("failed to decode messages: %w", err)
419 }
420 return msgs, nil
421}
422
423// GetSession retrieves a specific session as a proto type.
424func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*proto.Session, error) {
425 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
426 if err != nil {
427 return nil, fmt.Errorf("failed to get session: %w", err)
428 }
429 defer rsp.Body.Close()
430 if rsp.StatusCode != http.StatusOK {
431 return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
432 }
433 var sess proto.Session
434 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
435 return nil, fmt.Errorf("failed to decode session: %w", err)
436 }
437 return &sess, nil
438}
439
440// ListSessionHistoryFiles retrieves history files for a session as proto types.
441func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]proto.File, error) {
442 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/history", id, sessionID), nil, nil)
443 if err != nil {
444 return nil, fmt.Errorf("failed to get session history files: %w", err)
445 }
446 defer rsp.Body.Close()
447 if rsp.StatusCode != http.StatusOK {
448 return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
449 }
450 var files []proto.File
451 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
452 return nil, fmt.Errorf("failed to decode session history files: %w", err)
453 }
454 return files, nil
455}
456
457// CreateSession creates a new session in a workspace as a proto type.
458func (c *Client) CreateSession(ctx context.Context, id string, title string) (*proto.Session, error) {
459 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, jsonBody(proto.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}})
460 if err != nil {
461 return nil, fmt.Errorf("failed to create session: %w", err)
462 }
463 defer rsp.Body.Close()
464 if rsp.StatusCode != http.StatusOK {
465 return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
466 }
467 var sess proto.Session
468 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
469 return nil, fmt.Errorf("failed to decode session: %w", err)
470 }
471 return &sess, nil
472}
473
474// ListSessions lists all sessions in a workspace as proto types.
475func (c *Client) ListSessions(ctx context.Context, id string) ([]proto.Session, error) {
476 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, nil)
477 if err != nil {
478 return nil, fmt.Errorf("failed to get sessions: %w", err)
479 }
480 defer rsp.Body.Close()
481 if rsp.StatusCode != http.StatusOK {
482 return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
483 }
484 var sessions []proto.Session
485 if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
486 return nil, fmt.Errorf("failed to decode sessions: %w", err)
487 }
488 return sessions, nil
489}
490
491// GrantPermission grants a permission on a workspace.
492func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) error {
493 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
494 if err != nil {
495 return fmt.Errorf("failed to grant permission: %w", err)
496 }
497 defer rsp.Body.Close()
498 if rsp.StatusCode != http.StatusOK {
499 return fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
500 }
501 return nil
502}
503
504// SetPermissionsSkipRequests sets the skip-requests flag for a workspace.
505func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error {
506 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"}})
507 if err != nil {
508 return fmt.Errorf("failed to set permissions skip requests: %w", err)
509 }
510 defer rsp.Body.Close()
511 if rsp.StatusCode != http.StatusOK {
512 return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
513 }
514 return nil
515}
516
517// GetPermissionsSkipRequests retrieves the skip-requests flag for a workspace.
518func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) {
519 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, nil)
520 if err != nil {
521 return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
522 }
523 defer rsp.Body.Close()
524 if rsp.StatusCode != http.StatusOK {
525 return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
526 }
527 var skip proto.PermissionSkipRequest
528 if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
529 return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
530 }
531 return skip.Skip, nil
532}
533
534// GetConfig retrieves the workspace-specific configuration.
535func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) {
536 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/config", id), nil, nil)
537 if err != nil {
538 return nil, fmt.Errorf("failed to get config: %w", err)
539 }
540 defer rsp.Body.Close()
541 if rsp.StatusCode != http.StatusOK {
542 return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
543 }
544 var cfg config.Config
545 if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
546 return nil, fmt.Errorf("failed to decode config: %w", err)
547 }
548 return &cfg, nil
549}
550
551func jsonBody(v any) *bytes.Buffer {
552 b := new(bytes.Buffer)
553 m, _ := json.Marshal(v)
554 b.Write(m)
555 return b
556}
557
558// SaveSession updates a session in a workspace, returning a proto type.
559func (c *Client) SaveSession(ctx context.Context, id string, sess proto.Session) (*proto.Session, error) {
560 rsp, err := c.put(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sess.ID), nil, jsonBody(sess), http.Header{"Content-Type": []string{"application/json"}})
561 if err != nil {
562 return nil, fmt.Errorf("failed to save session: %w", err)
563 }
564 defer rsp.Body.Close()
565 if rsp.StatusCode != http.StatusOK {
566 return nil, fmt.Errorf("failed to save session: status code %d", rsp.StatusCode)
567 }
568 var saved proto.Session
569 if err := json.NewDecoder(rsp.Body).Decode(&saved); err != nil {
570 return nil, fmt.Errorf("failed to decode session: %w", err)
571 }
572 return &saved, nil
573}
574
575// DeleteSession deletes a session from a workspace.
576func (c *Client) DeleteSession(ctx context.Context, id string, sessionID string) error {
577 rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
578 if err != nil {
579 return fmt.Errorf("failed to delete session: %w", err)
580 }
581 defer rsp.Body.Close()
582 if rsp.StatusCode != http.StatusOK {
583 return fmt.Errorf("failed to delete session: status code %d", rsp.StatusCode)
584 }
585 return nil
586}
587
588// ListUserMessages retrieves user-role messages for a session as proto types.
589func (c *Client) ListUserMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
590 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages/user", id, sessionID), nil, nil)
591 if err != nil {
592 return nil, fmt.Errorf("failed to get user messages: %w", err)
593 }
594 defer rsp.Body.Close()
595 if rsp.StatusCode != http.StatusOK {
596 return nil, fmt.Errorf("failed to get user messages: status code %d", rsp.StatusCode)
597 }
598 var msgs []proto.Message
599 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
600 return nil, fmt.Errorf("failed to decode user messages: %w", err)
601 }
602 return msgs, nil
603}
604
605// ListAllUserMessages retrieves all user-role messages across sessions as proto types.
606func (c *Client) ListAllUserMessages(ctx context.Context, id string) ([]proto.Message, error) {
607 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/messages/user", id), nil, nil)
608 if err != nil {
609 return nil, fmt.Errorf("failed to get all user messages: %w", err)
610 }
611 defer rsp.Body.Close()
612 if rsp.StatusCode != http.StatusOK {
613 return nil, fmt.Errorf("failed to get all user messages: status code %d", rsp.StatusCode)
614 }
615 var msgs []proto.Message
616 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
617 return nil, fmt.Errorf("failed to decode all user messages: %w", err)
618 }
619 return msgs, nil
620}
621
622// CancelAgentSession cancels an ongoing agent operation for a session.
623func (c *Client) CancelAgentSession(ctx context.Context, id string, sessionID string) error {
624 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/cancel", id, sessionID), nil, nil, nil)
625 if err != nil {
626 return fmt.Errorf("failed to cancel agent session: %w", err)
627 }
628 defer rsp.Body.Close()
629 if rsp.StatusCode != http.StatusOK {
630 return fmt.Errorf("failed to cancel agent session: status code %d", rsp.StatusCode)
631 }
632 return nil
633}
634
635// GetAgentSessionQueuedPromptsList retrieves the list of queued prompt
636// strings for a session.
637func (c *Client) GetAgentSessionQueuedPromptsList(ctx context.Context, id string, sessionID string) ([]string, error) {
638 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/list", id, sessionID), nil, nil)
639 if err != nil {
640 return nil, fmt.Errorf("failed to get queued prompts list: %w", err)
641 }
642 defer rsp.Body.Close()
643 if rsp.StatusCode != http.StatusOK {
644 return nil, fmt.Errorf("failed to get queued prompts list: status code %d", rsp.StatusCode)
645 }
646 var prompts []string
647 if err := json.NewDecoder(rsp.Body).Decode(&prompts); err != nil {
648 return nil, fmt.Errorf("failed to decode queued prompts list: %w", err)
649 }
650 return prompts, nil
651}
652
653// GetDefaultSmallModel retrieves the default small model for a provider.
654func (c *Client) GetDefaultSmallModel(ctx context.Context, id string, providerID string) (*config.SelectedModel, error) {
655 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/default-small-model", id), url.Values{"provider_id": []string{providerID}}, nil)
656 if err != nil {
657 return nil, fmt.Errorf("failed to get default small model: %w", err)
658 }
659 defer rsp.Body.Close()
660 if rsp.StatusCode != http.StatusOK {
661 return nil, fmt.Errorf("failed to get default small model: status code %d", rsp.StatusCode)
662 }
663 var model config.SelectedModel
664 if err := json.NewDecoder(rsp.Body).Decode(&model); err != nil {
665 return nil, fmt.Errorf("failed to decode default small model: %w", err)
666 }
667 return &model, nil
668}
669
670// FileTrackerRecordRead records a file read for a session.
671func (c *Client) FileTrackerRecordRead(ctx context.Context, id string, sessionID, path string) error {
672 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/filetracker/read", id), nil, jsonBody(struct {
673 SessionID string `json:"session_id"`
674 Path string `json:"path"`
675 }{SessionID: sessionID, Path: path}), http.Header{"Content-Type": []string{"application/json"}})
676 if err != nil {
677 return fmt.Errorf("failed to record file read: %w", err)
678 }
679 defer rsp.Body.Close()
680 if rsp.StatusCode != http.StatusOK {
681 return fmt.Errorf("failed to record file read: status code %d", rsp.StatusCode)
682 }
683 return nil
684}
685
686// FileTrackerLastReadTime returns the last read time for a file in a
687// session.
688func (c *Client) FileTrackerLastReadTime(ctx context.Context, id string, sessionID, path string) (time.Time, error) {
689 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/filetracker/lastread", id), url.Values{
690 "session_id": []string{sessionID},
691 "path": []string{path},
692 }, nil)
693 if err != nil {
694 return time.Time{}, fmt.Errorf("failed to get last read time: %w", err)
695 }
696 defer rsp.Body.Close()
697 if rsp.StatusCode != http.StatusOK {
698 return time.Time{}, fmt.Errorf("failed to get last read time: status code %d", rsp.StatusCode)
699 }
700 var t time.Time
701 if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil {
702 return time.Time{}, fmt.Errorf("failed to decode last read time: %w", err)
703 }
704 return t, nil
705}
706
707// FileTrackerListReadFiles returns the list of read files for a session.
708func (c *Client) FileTrackerListReadFiles(ctx context.Context, id string, sessionID string) ([]string, error) {
709 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/filetracker/files", id, sessionID), nil, nil)
710 if err != nil {
711 return nil, fmt.Errorf("failed to get read files: %w", err)
712 }
713 defer rsp.Body.Close()
714 if rsp.StatusCode != http.StatusOK {
715 return nil, fmt.Errorf("failed to get read files: status code %d", rsp.StatusCode)
716 }
717 var files []string
718 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
719 return nil, fmt.Errorf("failed to decode read files: %w", err)
720 }
721 return files, nil
722}
723
724// LSPStart starts an LSP server for a path.
725func (c *Client) LSPStart(ctx context.Context, id string, path string) error {
726 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/start", id), nil, jsonBody(struct {
727 Path string `json:"path"`
728 }{Path: path}), http.Header{"Content-Type": []string{"application/json"}})
729 if err != nil {
730 return fmt.Errorf("failed to start LSP: %w", err)
731 }
732 defer rsp.Body.Close()
733 if rsp.StatusCode != http.StatusOK {
734 return fmt.Errorf("failed to start LSP: status code %d", rsp.StatusCode)
735 }
736 return nil
737}
738
739// LSPStopAll stops all LSP servers for a workspace.
740func (c *Client) LSPStopAll(ctx context.Context, id string) error {
741 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/stop", id), nil, nil, nil)
742 if err != nil {
743 return fmt.Errorf("failed to stop LSPs: %w", err)
744 }
745 defer rsp.Body.Close()
746 if rsp.StatusCode != http.StatusOK {
747 return fmt.Errorf("failed to stop LSPs: status code %d", rsp.StatusCode)
748 }
749 return nil
750}