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