proto.go

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