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