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