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