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