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