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	"time"
 14
 15	"github.com/charmbracelet/crush/internal/app"
 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// CreateWorkspace creates a new workspace on the server.
 26func (c *Client) CreateWorkspace(ctx context.Context, ws proto.Workspace) (*proto.Workspace, error) {
 27	rsp, err := c.post(ctx, "/workspaces", nil, jsonBody(ws), http.Header{"Content-Type": []string{"application/json"}})
 28	if err != nil {
 29		return nil, fmt.Errorf("failed to create workspace: %w", err)
 30	}
 31	defer rsp.Body.Close()
 32	if rsp.StatusCode != http.StatusOK {
 33		return nil, fmt.Errorf("failed to create workspace: status code %d", rsp.StatusCode)
 34	}
 35	var created proto.Workspace
 36	if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
 37		return nil, fmt.Errorf("failed to decode workspace: %w", err)
 38	}
 39	return &created, nil
 40}
 41
 42// DeleteWorkspace deletes a workspace on the server.
 43func (c *Client) DeleteWorkspace(ctx context.Context, id string) error {
 44	rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
 45	if err != nil {
 46		return fmt.Errorf("failed to delete workspace: %w", err)
 47	}
 48	defer rsp.Body.Close()
 49	if rsp.StatusCode != http.StatusOK {
 50		return fmt.Errorf("failed to delete workspace: status code %d", rsp.StatusCode)
 51	}
 52	return nil
 53}
 54
 55// SubscribeEvents subscribes to server-sent events for a workspace.
 56func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, error) {
 57	events := make(chan any, 100)
 58	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/events", id), nil, http.Header{
 59		"Accept":        []string{"text/event-stream"},
 60		"Cache-Control": []string{"no-cache"},
 61		"Connection":    []string{"keep-alive"},
 62	})
 63	if err != nil {
 64		return nil, fmt.Errorf("failed to subscribe to events: %w", err)
 65	}
 66
 67	if rsp.StatusCode != http.StatusOK {
 68		return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
 69	}
 70
 71	go func() {
 72		defer rsp.Body.Close()
 73
 74		scr := bufio.NewReader(rsp.Body)
 75		for {
 76			line, err := scr.ReadBytes('\n')
 77			if errors.Is(err, io.EOF) {
 78				break
 79			}
 80			if err != nil {
 81				slog.Error("Reading from events stream", "error", err)
 82				time.Sleep(time.Second * 2)
 83				continue
 84			}
 85			line = bytes.TrimSpace(line)
 86			if len(line) == 0 {
 87				continue
 88			}
 89
 90			data, ok := bytes.CutPrefix(line, []byte("data:"))
 91			if !ok {
 92				slog.Warn("Invalid event format", "line", string(line))
 93				continue
 94			}
 95
 96			data = bytes.TrimSpace(data)
 97
 98			var event pubsub.Event[any]
 99			if err := json.Unmarshal(data, &event); err != nil {
100				slog.Error("Unmarshaling event", "error", err)
101				continue
102			}
103
104			type alias pubsub.Event[any]
105			aux := &struct {
106				Payload json.RawMessage `json:"payload"`
107				*alias
108			}{
109				alias: (*alias)(&event),
110			}
111
112			if err := json.Unmarshal(data, &aux); err != nil {
113				slog.Error("Unmarshaling event payload", "error", err)
114				continue
115			}
116
117			var p pubsub.Payload
118			if err := json.Unmarshal(aux.Payload, &p); err != nil {
119				slog.Error("Unmarshaling event payload", "error", err)
120				continue
121			}
122
123			switch p.Type {
124			case pubsub.PayloadTypeLSPEvent:
125				var e pubsub.Event[proto.LSPEvent]
126				_ = json.Unmarshal(data, &e)
127				sendEvent(ctx, events, e)
128			case pubsub.PayloadTypeMCPEvent:
129				var e pubsub.Event[proto.MCPEvent]
130				_ = json.Unmarshal(data, &e)
131				sendEvent(ctx, events, e)
132			case pubsub.PayloadTypePermissionRequest:
133				var e pubsub.Event[proto.PermissionRequest]
134				_ = json.Unmarshal(data, &e)
135				sendEvent(ctx, events, e)
136			case pubsub.PayloadTypePermissionNotification:
137				var e pubsub.Event[proto.PermissionNotification]
138				_ = json.Unmarshal(data, &e)
139				sendEvent(ctx, events, e)
140			case pubsub.PayloadTypeMessage:
141				var e pubsub.Event[proto.Message]
142				_ = json.Unmarshal(data, &e)
143				sendEvent(ctx, events, e)
144			case pubsub.PayloadTypeSession:
145				var e pubsub.Event[proto.Session]
146				_ = json.Unmarshal(data, &e)
147				sendEvent(ctx, events, e)
148			case pubsub.PayloadTypeFile:
149				var e pubsub.Event[proto.File]
150				_ = json.Unmarshal(data, &e)
151				sendEvent(ctx, events, e)
152			case pubsub.PayloadTypeAgentEvent:
153				var e pubsub.Event[proto.AgentEvent]
154				_ = json.Unmarshal(data, &e)
155				sendEvent(ctx, events, e)
156			default:
157				slog.Warn("Unknown event type", "type", p.Type)
158				continue
159			}
160		}
161	}()
162
163	return events, nil
164}
165
166func sendEvent(ctx context.Context, evc chan any, ev any) {
167	slog.Info("Event received", "event", fmt.Sprintf("%T %+v", ev, ev))
168	select {
169	case evc <- ev:
170	case <-ctx.Done():
171		close(evc)
172		return
173	}
174}
175
176// GetLSPDiagnostics retrieves LSP diagnostics for a specific LSP client.
177func (c *Client) GetLSPDiagnostics(ctx context.Context, id string, lspName string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
178	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps/%s/diagnostics", id, lspName), nil, nil)
179	if err != nil {
180		return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
181	}
182	defer rsp.Body.Close()
183	if rsp.StatusCode != http.StatusOK {
184		return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
185	}
186	var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
187	if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
188		return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
189	}
190	return diagnostics, nil
191}
192
193// GetLSPs retrieves the LSP client states for a workspace.
194func (c *Client) GetLSPs(ctx context.Context, id string) (map[string]app.LSPClientInfo, error) {
195	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps", id), nil, nil)
196	if err != nil {
197		return nil, fmt.Errorf("failed to get LSPs: %w", err)
198	}
199	defer rsp.Body.Close()
200	if rsp.StatusCode != http.StatusOK {
201		return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
202	}
203	var lsps map[string]app.LSPClientInfo
204	if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
205		return nil, fmt.Errorf("failed to decode LSPs: %w", err)
206	}
207	return lsps, nil
208}
209
210// GetAgentSessionQueuedPrompts retrieves the number of queued prompts for a
211// session.
212func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) (int, error) {
213	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/queued", id, sessionID), nil, nil)
214	if err != nil {
215		return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
216	}
217	defer rsp.Body.Close()
218	if rsp.StatusCode != http.StatusOK {
219		return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
220	}
221	var count int
222	if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
223		return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
224	}
225	return count, nil
226}
227
228// ClearAgentSessionQueuedPrompts clears the queued prompts for a session.
229func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) error {
230	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/clear", id, sessionID), nil, nil, nil)
231	if err != nil {
232		return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
233	}
234	defer rsp.Body.Close()
235	if rsp.StatusCode != http.StatusOK {
236		return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
237	}
238	return nil
239}
240
241// GetAgentInfo retrieves the agent status for a workspace.
242func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) {
243	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, nil)
244	if err != nil {
245		return nil, fmt.Errorf("failed to get agent status: %w", err)
246	}
247	defer rsp.Body.Close()
248	if rsp.StatusCode != http.StatusOK {
249		return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
250	}
251	var info proto.AgentInfo
252	if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
253		return nil, fmt.Errorf("failed to decode agent status: %w", err)
254	}
255	return &info, nil
256}
257
258// UpdateAgent triggers an agent model update on the server.
259func (c *Client) UpdateAgent(ctx context.Context, id string) error {
260	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/update", id), nil, nil, nil)
261	if err != nil {
262		return fmt.Errorf("failed to update agent: %w", err)
263	}
264	defer rsp.Body.Close()
265	if rsp.StatusCode != http.StatusOK {
266		return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
267	}
268	return nil
269}
270
271// SendMessage sends a message to the agent for a workspace.
272func (c *Client) SendMessage(ctx context.Context, id string, sessionID, prompt string, attachments ...message.Attachment) error {
273	protoAttachments := make([]proto.Attachment, len(attachments))
274	for i, a := range attachments {
275		protoAttachments[i] = proto.Attachment{
276			FilePath: a.FilePath,
277			FileName: a.FileName,
278			MimeType: a.MimeType,
279			Content:  a.Content,
280		}
281	}
282	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, jsonBody(proto.AgentMessage{
283		SessionID:   sessionID,
284		Prompt:      prompt,
285		Attachments: protoAttachments,
286	}), http.Header{"Content-Type": []string{"application/json"}})
287	if err != nil {
288		return fmt.Errorf("failed to send message to agent: %w", err)
289	}
290	defer rsp.Body.Close()
291	if rsp.StatusCode != http.StatusOK {
292		return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
293	}
294	return nil
295}
296
297// GetAgentSessionInfo retrieves the agent session info for a workspace.
298func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) {
299	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s", id, sessionID), nil, nil)
300	if err != nil {
301		return nil, fmt.Errorf("failed to get session agent info: %w", err)
302	}
303	defer rsp.Body.Close()
304	if rsp.StatusCode != http.StatusOK {
305		return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
306	}
307	var info proto.AgentSession
308	if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
309		return nil, fmt.Errorf("failed to decode session agent info: %w", err)
310	}
311	return &info, nil
312}
313
314// AgentSummarizeSession requests a session summarization.
315func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
316	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
317	if err != nil {
318		return fmt.Errorf("failed to summarize session: %w", err)
319	}
320	defer rsp.Body.Close()
321	if rsp.StatusCode != http.StatusOK {
322		return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
323	}
324	return nil
325}
326
327// InitiateAgentProcessing triggers agent initialization on the server.
328func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error {
329	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/init", id), nil, nil, nil)
330	if err != nil {
331		return fmt.Errorf("failed to initiate session agent processing: %w", err)
332	}
333	defer rsp.Body.Close()
334	if rsp.StatusCode != http.StatusOK {
335		return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
336	}
337	return nil
338}
339
340// ListMessages retrieves all messages for a session.
341func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]message.Message, error) {
342	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
343	if err != nil {
344		return nil, fmt.Errorf("failed to get messages: %w", err)
345	}
346	defer rsp.Body.Close()
347	if rsp.StatusCode != http.StatusOK {
348		return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
349	}
350	var messages []message.Message
351	if err := json.NewDecoder(rsp.Body).Decode(&messages); err != nil {
352		return nil, fmt.Errorf("failed to decode messages: %w", err)
353	}
354	return messages, nil
355}
356
357// GetSession retrieves a specific session.
358func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*session.Session, error) {
359	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
360	if err != nil {
361		return nil, fmt.Errorf("failed to get session: %w", err)
362	}
363	defer rsp.Body.Close()
364	if rsp.StatusCode != http.StatusOK {
365		return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
366	}
367	var sess session.Session
368	if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
369		return nil, fmt.Errorf("failed to decode session: %w", err)
370	}
371	return &sess, nil
372}
373
374// ListSessionHistoryFiles retrieves history files for a session.
375func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]history.File, error) {
376	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/history", id, sessionID), nil, nil)
377	if err != nil {
378		return nil, fmt.Errorf("failed to get session history files: %w", err)
379	}
380	defer rsp.Body.Close()
381	if rsp.StatusCode != http.StatusOK {
382		return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
383	}
384	var files []history.File
385	if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
386		return nil, fmt.Errorf("failed to decode session history files: %w", err)
387	}
388	return files, nil
389}
390
391// CreateSession creates a new session in a workspace.
392func (c *Client) CreateSession(ctx context.Context, id string, title string) (*session.Session, error) {
393	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, jsonBody(session.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}})
394	if err != nil {
395		return nil, fmt.Errorf("failed to create session: %w", err)
396	}
397	defer rsp.Body.Close()
398	if rsp.StatusCode != http.StatusOK {
399		return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
400	}
401	var sess session.Session
402	if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
403		return nil, fmt.Errorf("failed to decode session: %w", err)
404	}
405	return &sess, nil
406}
407
408// ListSessions lists all sessions in a workspace.
409func (c *Client) ListSessions(ctx context.Context, id string) ([]session.Session, error) {
410	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, nil)
411	if err != nil {
412		return nil, fmt.Errorf("failed to get sessions: %w", err)
413	}
414	defer rsp.Body.Close()
415	if rsp.StatusCode != http.StatusOK {
416		return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
417	}
418	var sessions []session.Session
419	if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
420		return nil, fmt.Errorf("failed to decode sessions: %w", err)
421	}
422	return sessions, nil
423}
424
425// GrantPermission grants a permission on a workspace.
426func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) error {
427	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
428	if err != nil {
429		return fmt.Errorf("failed to grant permission: %w", err)
430	}
431	defer rsp.Body.Close()
432	if rsp.StatusCode != http.StatusOK {
433		return fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
434	}
435	return nil
436}
437
438// SetPermissionsSkipRequests sets the skip-requests flag for a workspace.
439func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error {
440	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"}})
441	if err != nil {
442		return fmt.Errorf("failed to set permissions skip requests: %w", err)
443	}
444	defer rsp.Body.Close()
445	if rsp.StatusCode != http.StatusOK {
446		return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
447	}
448	return nil
449}
450
451// GetPermissionsSkipRequests retrieves the skip-requests flag for a workspace.
452func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) {
453	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, nil)
454	if err != nil {
455		return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
456	}
457	defer rsp.Body.Close()
458	if rsp.StatusCode != http.StatusOK {
459		return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
460	}
461	var skip proto.PermissionSkipRequest
462	if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
463		return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
464	}
465	return skip.Skip, nil
466}
467
468// GetConfig retrieves the workspace-specific configuration.
469func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) {
470	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/config", id), nil, nil)
471	if err != nil {
472		return nil, fmt.Errorf("failed to get config: %w", err)
473	}
474	defer rsp.Body.Close()
475	if rsp.StatusCode != http.StatusOK {
476		return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
477	}
478	var cfg config.Config
479	if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
480		return nil, fmt.Errorf("failed to decode config: %w", err)
481	}
482	return &cfg, nil
483}
484
485func jsonBody(v any) *bytes.Buffer {
486	b := new(bytes.Buffer)
487	m, _ := json.Marshal(v)
488	b.Write(m)
489	return b
490}