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