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