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