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