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