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