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