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 select {
183 case evc <- ev:
184 case <-ctx.Done():
185 close(evc)
186 return
187 }
188}
189
190// GetLSPDiagnostics retrieves LSP diagnostics for a specific LSP client.
191func (c *Client) GetLSPDiagnostics(ctx context.Context, id string, lspName string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
192 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps/%s/diagnostics", id, lspName), nil, nil)
193 if err != nil {
194 return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
195 }
196 defer rsp.Body.Close()
197 if rsp.StatusCode != http.StatusOK {
198 return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
199 }
200 var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
201 if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
202 return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
203 }
204 return diagnostics, nil
205}
206
207// GetLSPs retrieves the LSP client states for a workspace.
208func (c *Client) GetLSPs(ctx context.Context, id string) (map[string]proto.LSPClientInfo, error) {
209 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps", id), nil, nil)
210 if err != nil {
211 return nil, fmt.Errorf("failed to get LSPs: %w", err)
212 }
213 defer rsp.Body.Close()
214 if rsp.StatusCode != http.StatusOK {
215 return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
216 }
217 var lsps map[string]proto.LSPClientInfo
218 if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
219 return nil, fmt.Errorf("failed to decode LSPs: %w", err)
220 }
221 return lsps, nil
222}
223
224// MCPGetStates retrieves the MCP client states for a workspace.
225func (c *Client) MCPGetStates(ctx context.Context, id string) (map[string]proto.MCPClientInfo, error) {
226 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/mcp/states", id), nil, nil)
227 if err != nil {
228 return nil, fmt.Errorf("failed to get MCP states: %w", err)
229 }
230 defer rsp.Body.Close()
231 if rsp.StatusCode != http.StatusOK {
232 return nil, fmt.Errorf("failed to get MCP states: status code %d", rsp.StatusCode)
233 }
234 var states map[string]proto.MCPClientInfo
235 if err := json.NewDecoder(rsp.Body).Decode(&states); err != nil {
236 return nil, fmt.Errorf("failed to decode MCP states: %w", err)
237 }
238 return states, nil
239}
240
241// MCPRefreshPrompts refreshes prompts for a named MCP client.
242func (c *Client) MCPRefreshPrompts(ctx context.Context, id, name string) error {
243 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-prompts", id), nil,
244 jsonBody(struct {
245 Name string `json:"name"`
246 }{Name: name}),
247 http.Header{"Content-Type": []string{"application/json"}})
248 if err != nil {
249 return fmt.Errorf("failed to refresh MCP prompts: %w", err)
250 }
251 defer rsp.Body.Close()
252 if rsp.StatusCode != http.StatusOK {
253 return fmt.Errorf("failed to refresh MCP prompts: status code %d", rsp.StatusCode)
254 }
255 return nil
256}
257
258// MCPRefreshResources refreshes resources for a named MCP client.
259func (c *Client) MCPRefreshResources(ctx context.Context, id, name string) error {
260 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-resources", id), nil,
261 jsonBody(struct {
262 Name string `json:"name"`
263 }{Name: name}),
264 http.Header{"Content-Type": []string{"application/json"}})
265 if err != nil {
266 return fmt.Errorf("failed to refresh MCP resources: %w", err)
267 }
268 defer rsp.Body.Close()
269 if rsp.StatusCode != http.StatusOK {
270 return fmt.Errorf("failed to refresh MCP resources: status code %d", rsp.StatusCode)
271 }
272 return nil
273}
274
275// GetAgentSessionQueuedPrompts retrieves the number of queued prompts for a
276// session.
277func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) (int, error) {
278 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/queued", id, sessionID), nil, nil)
279 if err != nil {
280 return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
281 }
282 defer rsp.Body.Close()
283 if rsp.StatusCode != http.StatusOK {
284 return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
285 }
286 var count int
287 if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
288 return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
289 }
290 return count, nil
291}
292
293// ClearAgentSessionQueuedPrompts clears the queued prompts for a session.
294func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) error {
295 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/clear", id, sessionID), nil, nil, nil)
296 if err != nil {
297 return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
298 }
299 defer rsp.Body.Close()
300 if rsp.StatusCode != http.StatusOK {
301 return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
302 }
303 return nil
304}
305
306// GetAgentInfo retrieves the agent status for a workspace.
307func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) {
308 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, nil)
309 if err != nil {
310 return nil, fmt.Errorf("failed to get agent status: %w", err)
311 }
312 defer rsp.Body.Close()
313 if rsp.StatusCode != http.StatusOK {
314 return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
315 }
316 var info proto.AgentInfo
317 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
318 return nil, fmt.Errorf("failed to decode agent status: %w", err)
319 }
320 return &info, nil
321}
322
323// UpdateAgent triggers an agent model update on the server.
324func (c *Client) UpdateAgent(ctx context.Context, id string) error {
325 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/update", id), nil, nil, nil)
326 if err != nil {
327 return fmt.Errorf("failed to update agent: %w", err)
328 }
329 defer rsp.Body.Close()
330 if rsp.StatusCode != http.StatusOK {
331 return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
332 }
333 return nil
334}
335
336// SendMessage sends a message to the agent for a workspace.
337func (c *Client) SendMessage(ctx context.Context, id string, sessionID, prompt string, attachments ...message.Attachment) error {
338 protoAttachments := make([]proto.Attachment, len(attachments))
339 for i, a := range attachments {
340 protoAttachments[i] = proto.Attachment{
341 FilePath: a.FilePath,
342 FileName: a.FileName,
343 MimeType: a.MimeType,
344 Content: a.Content,
345 }
346 }
347 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, jsonBody(proto.AgentMessage{
348 SessionID: sessionID,
349 Prompt: prompt,
350 Attachments: protoAttachments,
351 }), http.Header{"Content-Type": []string{"application/json"}})
352 if err != nil {
353 return fmt.Errorf("failed to send message to agent: %w", err)
354 }
355 defer rsp.Body.Close()
356 if rsp.StatusCode != http.StatusOK {
357 return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
358 }
359 return nil
360}
361
362// GetAgentSessionInfo retrieves the agent session info for a workspace.
363func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) {
364 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s", id, sessionID), nil, nil)
365 if err != nil {
366 return nil, fmt.Errorf("failed to get session agent info: %w", err)
367 }
368 defer rsp.Body.Close()
369 if rsp.StatusCode != http.StatusOK {
370 return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
371 }
372 var info proto.AgentSession
373 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
374 return nil, fmt.Errorf("failed to decode session agent info: %w", err)
375 }
376 return &info, nil
377}
378
379// AgentSummarizeSession requests a session summarization.
380func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
381 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
382 if err != nil {
383 return fmt.Errorf("failed to summarize session: %w", err)
384 }
385 defer rsp.Body.Close()
386 if rsp.StatusCode != http.StatusOK {
387 return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
388 }
389 return nil
390}
391
392// InitiateAgentProcessing triggers agent initialization on the server.
393func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error {
394 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/init", id), nil, nil, nil)
395 if err != nil {
396 return fmt.Errorf("failed to initiate session agent processing: %w", err)
397 }
398 defer rsp.Body.Close()
399 if rsp.StatusCode != http.StatusOK {
400 return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
401 }
402 return nil
403}
404
405// ListMessages retrieves all messages for a session as proto types.
406func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
407 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
408 if err != nil {
409 return nil, fmt.Errorf("failed to get messages: %w", err)
410 }
411 defer rsp.Body.Close()
412 if rsp.StatusCode != http.StatusOK {
413 return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
414 }
415 var msgs []proto.Message
416 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
417 return nil, fmt.Errorf("failed to decode messages: %w", err)
418 }
419 return msgs, nil
420}
421
422// GetSession retrieves a specific session as a proto type.
423func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*proto.Session, error) {
424 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
425 if err != nil {
426 return nil, fmt.Errorf("failed to get session: %w", err)
427 }
428 defer rsp.Body.Close()
429 if rsp.StatusCode != http.StatusOK {
430 return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
431 }
432 var sess proto.Session
433 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
434 return nil, fmt.Errorf("failed to decode session: %w", err)
435 }
436 return &sess, nil
437}
438
439// ListSessionHistoryFiles retrieves history files for a session as proto types.
440func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]proto.File, error) {
441 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/history", id, sessionID), nil, nil)
442 if err != nil {
443 return nil, fmt.Errorf("failed to get session history files: %w", err)
444 }
445 defer rsp.Body.Close()
446 if rsp.StatusCode != http.StatusOK {
447 return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
448 }
449 var files []proto.File
450 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
451 return nil, fmt.Errorf("failed to decode session history files: %w", err)
452 }
453 return files, nil
454}
455
456// CreateSession creates a new session in a workspace as a proto type.
457func (c *Client) CreateSession(ctx context.Context, id string, title string) (*proto.Session, error) {
458 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, jsonBody(proto.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}})
459 if err != nil {
460 return nil, fmt.Errorf("failed to create session: %w", err)
461 }
462 defer rsp.Body.Close()
463 if rsp.StatusCode != http.StatusOK {
464 return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
465 }
466 var sess proto.Session
467 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
468 return nil, fmt.Errorf("failed to decode session: %w", err)
469 }
470 return &sess, nil
471}
472
473// ListSessions lists all sessions in a workspace as proto types.
474func (c *Client) ListSessions(ctx context.Context, id string) ([]proto.Session, error) {
475 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, nil)
476 if err != nil {
477 return nil, fmt.Errorf("failed to get sessions: %w", err)
478 }
479 defer rsp.Body.Close()
480 if rsp.StatusCode != http.StatusOK {
481 return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
482 }
483 var sessions []proto.Session
484 if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
485 return nil, fmt.Errorf("failed to decode sessions: %w", err)
486 }
487 return sessions, nil
488}
489
490// GrantPermission grants a permission on a workspace.
491func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) error {
492 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
493 if err != nil {
494 return fmt.Errorf("failed to grant permission: %w", err)
495 }
496 defer rsp.Body.Close()
497 if rsp.StatusCode != http.StatusOK {
498 return fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
499 }
500 return nil
501}
502
503// SetPermissionsSkipRequests sets the skip-requests flag for a workspace.
504func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error {
505 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"}})
506 if err != nil {
507 return fmt.Errorf("failed to set permissions skip requests: %w", err)
508 }
509 defer rsp.Body.Close()
510 if rsp.StatusCode != http.StatusOK {
511 return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
512 }
513 return nil
514}
515
516// GetPermissionsSkipRequests retrieves the skip-requests flag for a workspace.
517func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) {
518 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, nil)
519 if err != nil {
520 return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
521 }
522 defer rsp.Body.Close()
523 if rsp.StatusCode != http.StatusOK {
524 return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
525 }
526 var skip proto.PermissionSkipRequest
527 if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
528 return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
529 }
530 return skip.Skip, nil
531}
532
533// GetConfig retrieves the workspace-specific configuration.
534func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) {
535 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/config", id), nil, nil)
536 if err != nil {
537 return nil, fmt.Errorf("failed to get config: %w", err)
538 }
539 defer rsp.Body.Close()
540 if rsp.StatusCode != http.StatusOK {
541 return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
542 }
543 var cfg config.Config
544 if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
545 return nil, fmt.Errorf("failed to decode config: %w", err)
546 }
547 return &cfg, nil
548}
549
550func jsonBody(v any) *bytes.Buffer {
551 b := new(bytes.Buffer)
552 m, _ := json.Marshal(v)
553 b.Write(m)
554 return b
555}
556
557// SaveSession updates a session in a workspace, returning a proto type.
558func (c *Client) SaveSession(ctx context.Context, id string, sess proto.Session) (*proto.Session, error) {
559 rsp, err := c.put(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sess.ID), nil, jsonBody(sess), http.Header{"Content-Type": []string{"application/json"}})
560 if err != nil {
561 return nil, fmt.Errorf("failed to save session: %w", err)
562 }
563 defer rsp.Body.Close()
564 if rsp.StatusCode != http.StatusOK {
565 return nil, fmt.Errorf("failed to save session: status code %d", rsp.StatusCode)
566 }
567 var saved proto.Session
568 if err := json.NewDecoder(rsp.Body).Decode(&saved); err != nil {
569 return nil, fmt.Errorf("failed to decode session: %w", err)
570 }
571 return &saved, nil
572}
573
574// DeleteSession deletes a session from a workspace.
575func (c *Client) DeleteSession(ctx context.Context, id string, sessionID string) error {
576 rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
577 if err != nil {
578 return fmt.Errorf("failed to delete session: %w", err)
579 }
580 defer rsp.Body.Close()
581 if rsp.StatusCode != http.StatusOK {
582 return fmt.Errorf("failed to delete session: status code %d", rsp.StatusCode)
583 }
584 return nil
585}
586
587// ListUserMessages retrieves user-role messages for a session as proto types.
588func (c *Client) ListUserMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
589 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages/user", id, sessionID), nil, nil)
590 if err != nil {
591 return nil, fmt.Errorf("failed to get user messages: %w", err)
592 }
593 defer rsp.Body.Close()
594 if rsp.StatusCode != http.StatusOK {
595 return nil, fmt.Errorf("failed to get user messages: status code %d", rsp.StatusCode)
596 }
597 var msgs []proto.Message
598 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
599 return nil, fmt.Errorf("failed to decode user messages: %w", err)
600 }
601 return msgs, nil
602}
603
604// ListAllUserMessages retrieves all user-role messages across sessions as proto types.
605func (c *Client) ListAllUserMessages(ctx context.Context, id string) ([]proto.Message, error) {
606 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/messages/user", id), nil, nil)
607 if err != nil {
608 return nil, fmt.Errorf("failed to get all user messages: %w", err)
609 }
610 defer rsp.Body.Close()
611 if rsp.StatusCode != http.StatusOK {
612 return nil, fmt.Errorf("failed to get all user messages: status code %d", rsp.StatusCode)
613 }
614 var msgs []proto.Message
615 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
616 return nil, fmt.Errorf("failed to decode all user messages: %w", err)
617 }
618 return msgs, nil
619}
620
621// CancelAgentSession cancels an ongoing agent operation for a session.
622func (c *Client) CancelAgentSession(ctx context.Context, id string, sessionID string) error {
623 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/cancel", id, sessionID), nil, nil, nil)
624 if err != nil {
625 return fmt.Errorf("failed to cancel agent session: %w", err)
626 }
627 defer rsp.Body.Close()
628 if rsp.StatusCode != http.StatusOK {
629 return fmt.Errorf("failed to cancel agent session: status code %d", rsp.StatusCode)
630 }
631 return nil
632}
633
634// GetAgentSessionQueuedPromptsList retrieves the list of queued prompt
635// strings for a session.
636func (c *Client) GetAgentSessionQueuedPromptsList(ctx context.Context, id string, sessionID string) ([]string, error) {
637 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/list", id, sessionID), nil, nil)
638 if err != nil {
639 return nil, fmt.Errorf("failed to get queued prompts list: %w", err)
640 }
641 defer rsp.Body.Close()
642 if rsp.StatusCode != http.StatusOK {
643 return nil, fmt.Errorf("failed to get queued prompts list: status code %d", rsp.StatusCode)
644 }
645 var prompts []string
646 if err := json.NewDecoder(rsp.Body).Decode(&prompts); err != nil {
647 return nil, fmt.Errorf("failed to decode queued prompts list: %w", err)
648 }
649 return prompts, nil
650}
651
652// GetDefaultSmallModel retrieves the default small model for a provider.
653func (c *Client) GetDefaultSmallModel(ctx context.Context, id string, providerID string) (*config.SelectedModel, error) {
654 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/default-small-model", id), url.Values{"provider_id": []string{providerID}}, nil)
655 if err != nil {
656 return nil, fmt.Errorf("failed to get default small model: %w", err)
657 }
658 defer rsp.Body.Close()
659 if rsp.StatusCode != http.StatusOK {
660 return nil, fmt.Errorf("failed to get default small model: status code %d", rsp.StatusCode)
661 }
662 var model config.SelectedModel
663 if err := json.NewDecoder(rsp.Body).Decode(&model); err != nil {
664 return nil, fmt.Errorf("failed to decode default small model: %w", err)
665 }
666 return &model, nil
667}
668
669// FileTrackerRecordRead records a file read for a session.
670func (c *Client) FileTrackerRecordRead(ctx context.Context, id string, sessionID, path string) error {
671 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/filetracker/read", id), nil, jsonBody(struct {
672 SessionID string `json:"session_id"`
673 Path string `json:"path"`
674 }{SessionID: sessionID, Path: path}), http.Header{"Content-Type": []string{"application/json"}})
675 if err != nil {
676 return fmt.Errorf("failed to record file read: %w", err)
677 }
678 defer rsp.Body.Close()
679 if rsp.StatusCode != http.StatusOK {
680 return fmt.Errorf("failed to record file read: status code %d", rsp.StatusCode)
681 }
682 return nil
683}
684
685// FileTrackerLastReadTime returns the last read time for a file in a
686// session.
687func (c *Client) FileTrackerLastReadTime(ctx context.Context, id string, sessionID, path string) (time.Time, error) {
688 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/filetracker/lastread", id), url.Values{
689 "session_id": []string{sessionID},
690 "path": []string{path},
691 }, nil)
692 if err != nil {
693 return time.Time{}, fmt.Errorf("failed to get last read time: %w", err)
694 }
695 defer rsp.Body.Close()
696 if rsp.StatusCode != http.StatusOK {
697 return time.Time{}, fmt.Errorf("failed to get last read time: status code %d", rsp.StatusCode)
698 }
699 var t time.Time
700 if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil {
701 return time.Time{}, fmt.Errorf("failed to decode last read time: %w", err)
702 }
703 return t, nil
704}
705
706// FileTrackerListReadFiles returns the list of read files for a session.
707func (c *Client) FileTrackerListReadFiles(ctx context.Context, id string, sessionID string) ([]string, error) {
708 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/filetracker/files", id, sessionID), nil, nil)
709 if err != nil {
710 return nil, fmt.Errorf("failed to get read files: %w", err)
711 }
712 defer rsp.Body.Close()
713 if rsp.StatusCode != http.StatusOK {
714 return nil, fmt.Errorf("failed to get read files: status code %d", rsp.StatusCode)
715 }
716 var files []string
717 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
718 return nil, fmt.Errorf("failed to decode read files: %w", err)
719 }
720 return files, nil
721}
722
723// LSPStart starts an LSP server for a path.
724func (c *Client) LSPStart(ctx context.Context, id string, path string) error {
725 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/start", id), nil, jsonBody(struct {
726 Path string `json:"path"`
727 }{Path: path}), http.Header{"Content-Type": []string{"application/json"}})
728 if err != nil {
729 return fmt.Errorf("failed to start LSP: %w", err)
730 }
731 defer rsp.Body.Close()
732 if rsp.StatusCode != http.StatusOK {
733 return fmt.Errorf("failed to start LSP: status code %d", rsp.StatusCode)
734 }
735 return nil
736}
737
738// LSPStopAll stops all LSP servers for a workspace.
739func (c *Client) LSPStopAll(ctx context.Context, id string) error {
740 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/stop", id), nil, nil, nil)
741 if err != nil {
742 return fmt.Errorf("failed to stop LSPs: %w", err)
743 }
744 defer rsp.Body.Close()
745 if rsp.StatusCode != http.StatusOK {
746 return fmt.Errorf("failed to stop LSPs: status code %d", rsp.StatusCode)
747 }
748 return nil
749}