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