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