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