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 && rsp.StatusCode != http.StatusAccepted {
427 if msg := decodeErrorMessage(rsp.Body); msg != "" {
428 return fmt.Errorf("failed to send message to agent: status code %d: %s", rsp.StatusCode, msg)
429 }
430 return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
431 }
432 return nil
433}
434
435// decodeErrorMessage attempts to decode the response body as a
436// proto.Error and returns its message. It returns an empty string
437// when the body is empty or cannot be decoded into a proto.Error
438// with a non-empty message, letting callers fall back to a
439// status-only error.
440func decodeErrorMessage(body io.Reader) string {
441 var e proto.Error
442 if err := json.NewDecoder(body).Decode(&e); err != nil {
443 return ""
444 }
445 return e.Message
446}
447
448// GetAgentSessionInfo retrieves the agent session info for a workspace.
449func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) {
450 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s", id, sessionID), nil, nil)
451 if err != nil {
452 return nil, fmt.Errorf("failed to get session agent info: %w", err)
453 }
454 defer rsp.Body.Close()
455 if rsp.StatusCode != http.StatusOK {
456 return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
457 }
458 var info proto.AgentSession
459 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
460 return nil, fmt.Errorf("failed to decode session agent info: %w", err)
461 }
462 return &info, nil
463}
464
465// AgentSummarizeSession requests a session summarization.
466func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
467 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
468 if err != nil {
469 return fmt.Errorf("failed to summarize session: %w", err)
470 }
471 defer rsp.Body.Close()
472 if rsp.StatusCode != http.StatusOK {
473 return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
474 }
475 return nil
476}
477
478// InitiateAgentProcessing triggers agent initialization on the server.
479func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error {
480 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/init", id), nil, nil, nil)
481 if err != nil {
482 return fmt.Errorf("failed to initiate session agent processing: %w", err)
483 }
484 defer rsp.Body.Close()
485 if rsp.StatusCode != http.StatusOK {
486 return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
487 }
488 return nil
489}
490
491// ListMessages retrieves all messages for a session as proto types.
492func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
493 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
494 if err != nil {
495 return nil, fmt.Errorf("failed to get messages: %w", err)
496 }
497 defer rsp.Body.Close()
498 if rsp.StatusCode != http.StatusOK {
499 return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
500 }
501 var msgs []proto.Message
502 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
503 return nil, fmt.Errorf("failed to decode messages: %w", err)
504 }
505 return msgs, nil
506}
507
508// GetSession retrieves a specific session as a proto type.
509func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*proto.Session, error) {
510 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
511 if err != nil {
512 return nil, fmt.Errorf("failed to get session: %w", err)
513 }
514 defer rsp.Body.Close()
515 if rsp.StatusCode != http.StatusOK {
516 return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
517 }
518 var sess proto.Session
519 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
520 return nil, fmt.Errorf("failed to decode session: %w", err)
521 }
522 return &sess, nil
523}
524
525// ListSessionHistoryFiles retrieves history files for a session as proto types.
526func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]proto.File, error) {
527 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/history", id, sessionID), nil, nil)
528 if err != nil {
529 return nil, fmt.Errorf("failed to get session history files: %w", err)
530 }
531 defer rsp.Body.Close()
532 if rsp.StatusCode != http.StatusOK {
533 return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
534 }
535 var files []proto.File
536 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
537 return nil, fmt.Errorf("failed to decode session history files: %w", err)
538 }
539 return files, nil
540}
541
542// CreateSession creates a new session in a workspace as a proto type.
543func (c *Client) CreateSession(ctx context.Context, id string, title string) (*proto.Session, error) {
544 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, jsonBody(proto.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}})
545 if err != nil {
546 return nil, fmt.Errorf("failed to create session: %w", err)
547 }
548 defer rsp.Body.Close()
549 if rsp.StatusCode != http.StatusOK {
550 return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
551 }
552 var sess proto.Session
553 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
554 return nil, fmt.Errorf("failed to decode session: %w", err)
555 }
556 return &sess, nil
557}
558
559// ListSessions lists all sessions in a workspace as proto types.
560func (c *Client) ListSessions(ctx context.Context, id string) ([]proto.Session, error) {
561 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, nil)
562 if err != nil {
563 return nil, fmt.Errorf("failed to get sessions: %w", err)
564 }
565 defer rsp.Body.Close()
566 if rsp.StatusCode != http.StatusOK {
567 return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
568 }
569 var sessions []proto.Session
570 if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
571 return nil, fmt.Errorf("failed to decode sessions: %w", err)
572 }
573 return sessions, nil
574}
575
576// GrantPermission grants a permission on a workspace. The returned
577// bool reports whether this call resolved the pending request (true)
578// or found it already resolved by a previous caller (false). A false
579// value is not an error — it just means another subscriber resolved
580// the same request first.
581func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) (bool, error) {
582 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
583 if err != nil {
584 return false, fmt.Errorf("failed to grant permission: %w", err)
585 }
586 defer rsp.Body.Close()
587 if rsp.StatusCode != http.StatusOK {
588 return false, fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
589 }
590 var resp proto.PermissionGrantResponse
591 if err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {
592 return false, fmt.Errorf("failed to decode grant permission response: %w", err)
593 }
594 return resp.Resolved, nil
595}
596
597// SetPermissionsSkipRequests sets the skip-requests flag for a workspace.
598func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error {
599 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"}})
600 if err != nil {
601 return fmt.Errorf("failed to set permissions skip requests: %w", err)
602 }
603 defer rsp.Body.Close()
604 if rsp.StatusCode != http.StatusOK {
605 return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
606 }
607 return nil
608}
609
610// GetPermissionsSkipRequests retrieves the skip-requests flag for a workspace.
611func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) {
612 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, nil)
613 if err != nil {
614 return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
615 }
616 defer rsp.Body.Close()
617 if rsp.StatusCode != http.StatusOK {
618 return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
619 }
620 var skip proto.PermissionSkipRequest
621 if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
622 return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
623 }
624 return skip.Skip, nil
625}
626
627// GetConfig retrieves the workspace-specific configuration.
628func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) {
629 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/config", id), nil, nil)
630 if err != nil {
631 return nil, fmt.Errorf("failed to get config: %w", err)
632 }
633 defer rsp.Body.Close()
634 if rsp.StatusCode != http.StatusOK {
635 return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
636 }
637 var cfg config.Config
638 if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
639 return nil, fmt.Errorf("failed to decode config: %w", err)
640 }
641 return &cfg, nil
642}
643
644func jsonBody(v any) *bytes.Buffer {
645 b := new(bytes.Buffer)
646 m, _ := json.Marshal(v)
647 b.Write(m)
648 return b
649}
650
651// SaveSession updates a session in a workspace, returning a proto type.
652func (c *Client) SaveSession(ctx context.Context, id string, sess proto.Session) (*proto.Session, error) {
653 rsp, err := c.put(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sess.ID), nil, jsonBody(sess), http.Header{"Content-Type": []string{"application/json"}})
654 if err != nil {
655 return nil, fmt.Errorf("failed to save session: %w", err)
656 }
657 defer rsp.Body.Close()
658 if rsp.StatusCode != http.StatusOK {
659 return nil, fmt.Errorf("failed to save session: status code %d", rsp.StatusCode)
660 }
661 var saved proto.Session
662 if err := json.NewDecoder(rsp.Body).Decode(&saved); err != nil {
663 return nil, fmt.Errorf("failed to decode session: %w", err)
664 }
665 return &saved, nil
666}
667
668// DeleteSession deletes a session from a workspace.
669func (c *Client) DeleteSession(ctx context.Context, id string, sessionID string) error {
670 rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
671 if err != nil {
672 return fmt.Errorf("failed to delete session: %w", err)
673 }
674 defer rsp.Body.Close()
675 if rsp.StatusCode != http.StatusOK {
676 return fmt.Errorf("failed to delete session: status code %d", rsp.StatusCode)
677 }
678 return nil
679}
680
681// ListUserMessages retrieves user-role messages for a session as proto types.
682func (c *Client) ListUserMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
683 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages/user", id, sessionID), nil, nil)
684 if err != nil {
685 return nil, fmt.Errorf("failed to get user messages: %w", err)
686 }
687 defer rsp.Body.Close()
688 if rsp.StatusCode != http.StatusOK {
689 return nil, fmt.Errorf("failed to get user messages: status code %d", rsp.StatusCode)
690 }
691 var msgs []proto.Message
692 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
693 return nil, fmt.Errorf("failed to decode user messages: %w", err)
694 }
695 return msgs, nil
696}
697
698// ListAllUserMessages retrieves all user-role messages across sessions as proto types.
699func (c *Client) ListAllUserMessages(ctx context.Context, id string) ([]proto.Message, error) {
700 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/messages/user", id), nil, nil)
701 if err != nil {
702 return nil, fmt.Errorf("failed to get all user messages: %w", err)
703 }
704 defer rsp.Body.Close()
705 if rsp.StatusCode != http.StatusOK {
706 return nil, fmt.Errorf("failed to get all user messages: status code %d", rsp.StatusCode)
707 }
708 var msgs []proto.Message
709 if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
710 return nil, fmt.Errorf("failed to decode all user messages: %w", err)
711 }
712 return msgs, nil
713}
714
715// CancelAgentSession cancels an ongoing agent operation for a session.
716func (c *Client) CancelAgentSession(ctx context.Context, id string, sessionID string) error {
717 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/cancel", id, sessionID), nil, nil, nil)
718 if err != nil {
719 return fmt.Errorf("failed to cancel agent session: %w", err)
720 }
721 defer rsp.Body.Close()
722 if rsp.StatusCode != http.StatusOK {
723 return fmt.Errorf("failed to cancel agent session: status code %d", rsp.StatusCode)
724 }
725 return nil
726}
727
728// GetAgentSessionQueuedPromptsList retrieves the list of queued prompt
729// strings for a session.
730func (c *Client) GetAgentSessionQueuedPromptsList(ctx context.Context, id string, sessionID string) ([]string, error) {
731 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/list", id, sessionID), nil, nil)
732 if err != nil {
733 return nil, fmt.Errorf("failed to get queued prompts list: %w", err)
734 }
735 defer rsp.Body.Close()
736 if rsp.StatusCode != http.StatusOK {
737 return nil, fmt.Errorf("failed to get queued prompts list: status code %d", rsp.StatusCode)
738 }
739 var prompts []string
740 if err := json.NewDecoder(rsp.Body).Decode(&prompts); err != nil {
741 return nil, fmt.Errorf("failed to decode queued prompts list: %w", err)
742 }
743 return prompts, nil
744}
745
746// GetDefaultSmallModel retrieves the default small model for a provider.
747func (c *Client) GetDefaultSmallModel(ctx context.Context, id string, providerID string) (*config.SelectedModel, error) {
748 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/default-small-model", id), url.Values{"provider_id": []string{providerID}}, nil)
749 if err != nil {
750 return nil, fmt.Errorf("failed to get default small model: %w", err)
751 }
752 defer rsp.Body.Close()
753 if rsp.StatusCode != http.StatusOK {
754 return nil, fmt.Errorf("failed to get default small model: status code %d", rsp.StatusCode)
755 }
756 var model config.SelectedModel
757 if err := json.NewDecoder(rsp.Body).Decode(&model); err != nil {
758 return nil, fmt.Errorf("failed to decode default small model: %w", err)
759 }
760 return &model, nil
761}
762
763// FileTrackerRecordRead records a file read for a session.
764func (c *Client) FileTrackerRecordRead(ctx context.Context, id string, sessionID, path string) error {
765 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/filetracker/read", id), nil, jsonBody(struct {
766 SessionID string `json:"session_id"`
767 Path string `json:"path"`
768 }{SessionID: sessionID, Path: path}), http.Header{"Content-Type": []string{"application/json"}})
769 if err != nil {
770 return fmt.Errorf("failed to record file read: %w", err)
771 }
772 defer rsp.Body.Close()
773 if rsp.StatusCode != http.StatusOK {
774 return fmt.Errorf("failed to record file read: status code %d", rsp.StatusCode)
775 }
776 return nil
777}
778
779// FileTrackerLastReadTime returns the last read time for a file in a
780// session.
781func (c *Client) FileTrackerLastReadTime(ctx context.Context, id string, sessionID, path string) (time.Time, error) {
782 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/filetracker/lastread", id), url.Values{
783 "session_id": []string{sessionID},
784 "path": []string{path},
785 }, nil)
786 if err != nil {
787 return time.Time{}, fmt.Errorf("failed to get last read time: %w", err)
788 }
789 defer rsp.Body.Close()
790 if rsp.StatusCode != http.StatusOK {
791 return time.Time{}, fmt.Errorf("failed to get last read time: status code %d", rsp.StatusCode)
792 }
793 var t time.Time
794 if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil {
795 return time.Time{}, fmt.Errorf("failed to decode last read time: %w", err)
796 }
797 return t, nil
798}
799
800// FileTrackerListReadFiles returns the list of read files for a session.
801func (c *Client) FileTrackerListReadFiles(ctx context.Context, id string, sessionID string) ([]string, error) {
802 rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/filetracker/files", id, sessionID), nil, nil)
803 if err != nil {
804 return nil, fmt.Errorf("failed to get read files: %w", err)
805 }
806 defer rsp.Body.Close()
807 if rsp.StatusCode != http.StatusOK {
808 return nil, fmt.Errorf("failed to get read files: status code %d", rsp.StatusCode)
809 }
810 var files []string
811 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
812 return nil, fmt.Errorf("failed to decode read files: %w", err)
813 }
814 return files, nil
815}
816
817// LSPStart starts an LSP server for a path.
818func (c *Client) LSPStart(ctx context.Context, id string, path string) error {
819 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/start", id), nil, jsonBody(struct {
820 Path string `json:"path"`
821 }{Path: path}), http.Header{"Content-Type": []string{"application/json"}})
822 if err != nil {
823 return fmt.Errorf("failed to start LSP: %w", err)
824 }
825 defer rsp.Body.Close()
826 if rsp.StatusCode != http.StatusOK {
827 return fmt.Errorf("failed to start LSP: status code %d", rsp.StatusCode)
828 }
829 return nil
830}
831
832// LSPStopAll stops all LSP servers for a workspace.
833func (c *Client) LSPStopAll(ctx context.Context, id string) error {
834 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/stop", id), nil, nil, nil)
835 if err != nil {
836 return fmt.Errorf("failed to stop LSPs: %w", err)
837 }
838 defer rsp.Body.Close()
839 if rsp.StatusCode != http.StatusOK {
840 return fmt.Errorf("failed to stop LSPs: status code %d", rsp.StatusCode)
841 }
842 return nil
843}