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