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