proto.go

  1package server
  2
  3import (
  4	"encoding/json"
  5	"errors"
  6	"fmt"
  7	"net/http"
  8
  9	"github.com/charmbracelet/crush/internal/backend"
 10	"github.com/charmbracelet/crush/internal/proto"
 11	"github.com/charmbracelet/crush/internal/session"
 12)
 13
 14type controllerV1 struct {
 15	backend *backend.Backend
 16	server  *Server
 17}
 18
 19func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
 20	w.WriteHeader(http.StatusOK)
 21}
 22
 23func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
 24	jsonEncode(w, c.backend.VersionInfo())
 25}
 26
 27func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
 28	var req proto.ServerControl
 29	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 30		c.server.logError(r, "Failed to decode request", "error", err)
 31		jsonError(w, http.StatusBadRequest, "failed to decode request")
 32		return
 33	}
 34
 35	switch req.Command {
 36	case "shutdown":
 37		c.backend.Shutdown()
 38	default:
 39		c.server.logError(r, "Unknown command", "command", req.Command)
 40		jsonError(w, http.StatusBadRequest, "unknown command")
 41		return
 42	}
 43}
 44
 45func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
 46	jsonEncode(w, c.backend.Config())
 47}
 48
 49func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
 50	jsonEncode(w, c.backend.ListWorkspaces())
 51}
 52
 53func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
 54	id := r.PathValue("id")
 55	ws, err := c.backend.GetWorkspaceProto(id)
 56	if err != nil {
 57		c.handleError(w, r, err)
 58		return
 59	}
 60	jsonEncode(w, ws)
 61}
 62
 63func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
 64	var args proto.Workspace
 65	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
 66		c.server.logError(r, "Failed to decode request", "error", err)
 67		jsonError(w, http.StatusBadRequest, "failed to decode request")
 68		return
 69	}
 70
 71	_, result, err := c.backend.CreateWorkspace(args)
 72	if err != nil {
 73		c.handleError(w, r, err)
 74		return
 75	}
 76	jsonEncode(w, result)
 77}
 78
 79func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
 80	id := r.PathValue("id")
 81	c.backend.DeleteWorkspace(id)
 82}
 83
 84func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
 85	id := r.PathValue("id")
 86	cfg, err := c.backend.GetWorkspaceConfig(id)
 87	if err != nil {
 88		c.handleError(w, r, err)
 89		return
 90	}
 91	jsonEncode(w, cfg)
 92}
 93
 94func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
 95	id := r.PathValue("id")
 96	providers, err := c.backend.GetWorkspaceProviders(id)
 97	if err != nil {
 98		c.handleError(w, r, err)
 99		return
100	}
101	jsonEncode(w, providers)
102}
103
104func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
105	flusher := http.NewResponseController(w)
106	id := r.PathValue("id")
107	events, err := c.backend.SubscribeEvents(id)
108	if err != nil {
109		c.handleError(w, r, err)
110		return
111	}
112
113	w.Header().Set("Content-Type", "text/event-stream")
114	w.Header().Set("Cache-Control", "no-cache")
115	w.Header().Set("Connection", "keep-alive")
116
117	for {
118		select {
119		case <-r.Context().Done():
120			c.server.logDebug(r, "Stopping event stream")
121			return
122		case ev, ok := <-events:
123			if !ok {
124				return
125			}
126			c.server.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
127			wrapped := wrapEvent(ev)
128			if wrapped == nil {
129				continue
130			}
131			data, err := json.Marshal(wrapped)
132			if err != nil {
133				c.server.logError(r, "Failed to marshal event", "error", err)
134				continue
135			}
136
137			fmt.Fprintf(w, "data: %s\n\n", data)
138			flusher.Flush()
139		}
140	}
141}
142
143func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
144	id := r.PathValue("id")
145	states, err := c.backend.GetLSPStates(id)
146	if err != nil {
147		c.handleError(w, r, err)
148		return
149	}
150	result := make(map[string]proto.LSPClientInfo, len(states))
151	for k, v := range states {
152		result[k] = proto.LSPClientInfo{
153			Name:            v.Name,
154			State:           v.State,
155			Error:           v.Error,
156			DiagnosticCount: v.DiagnosticCount,
157			ConnectedAt:     v.ConnectedAt,
158		}
159	}
160	jsonEncode(w, result)
161}
162
163func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
164	id := r.PathValue("id")
165	lspName := r.PathValue("lsp")
166	diagnostics, err := c.backend.GetLSPDiagnostics(id, lspName)
167	if err != nil {
168		c.handleError(w, r, err)
169		return
170	}
171	jsonEncode(w, diagnostics)
172}
173
174func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
175	id := r.PathValue("id")
176	sessions, err := c.backend.ListSessions(r.Context(), id)
177	if err != nil {
178		c.handleError(w, r, err)
179		return
180	}
181	jsonEncode(w, sessions)
182}
183
184func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
185	id := r.PathValue("id")
186
187	var args session.Session
188	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
189		c.server.logError(r, "Failed to decode request", "error", err)
190		jsonError(w, http.StatusBadRequest, "failed to decode request")
191		return
192	}
193
194	sess, err := c.backend.CreateSession(r.Context(), id, args.Title)
195	if err != nil {
196		c.handleError(w, r, err)
197		return
198	}
199	jsonEncode(w, sess)
200}
201
202func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
203	id := r.PathValue("id")
204	sid := r.PathValue("sid")
205	sess, err := c.backend.GetSession(r.Context(), id, sid)
206	if err != nil {
207		c.handleError(w, r, err)
208		return
209	}
210	jsonEncode(w, sess)
211}
212
213func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
214	id := r.PathValue("id")
215	sid := r.PathValue("sid")
216	history, err := c.backend.ListSessionHistory(r.Context(), id, sid)
217	if err != nil {
218		c.handleError(w, r, err)
219		return
220	}
221	jsonEncode(w, history)
222}
223
224func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
225	id := r.PathValue("id")
226	sid := r.PathValue("sid")
227	messages, err := c.backend.ListSessionMessages(r.Context(), id, sid)
228	if err != nil {
229		c.handleError(w, r, err)
230		return
231	}
232	jsonEncode(w, messagesToProto(messages))
233}
234
235func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http.Request) {
236	id := r.PathValue("id")
237
238	var sess session.Session
239	if err := json.NewDecoder(r.Body).Decode(&sess); err != nil {
240		c.server.logError(r, "Failed to decode request", "error", err)
241		jsonError(w, http.StatusBadRequest, "failed to decode request")
242		return
243	}
244
245	saved, err := c.backend.SaveSession(r.Context(), id, sess)
246	if err != nil {
247		c.handleError(w, r, err)
248		return
249	}
250	jsonEncode(w, saved)
251}
252
253func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *http.Request) {
254	id := r.PathValue("id")
255	sid := r.PathValue("sid")
256	if err := c.backend.DeleteSession(r.Context(), id, sid); err != nil {
257		c.handleError(w, r, err)
258		return
259	}
260	w.WriteHeader(http.StatusOK)
261}
262
263func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWriter, r *http.Request) {
264	id := r.PathValue("id")
265	sid := r.PathValue("sid")
266	messages, err := c.backend.ListUserMessages(r.Context(), id, sid)
267	if err != nil {
268		c.handleError(w, r, err)
269		return
270	}
271	jsonEncode(w, messagesToProto(messages))
272}
273
274func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter, r *http.Request) {
275	id := r.PathValue("id")
276	messages, err := c.backend.ListAllUserMessages(r.Context(), id)
277	if err != nil {
278		c.handleError(w, r, err)
279		return
280	}
281	jsonEncode(w, messagesToProto(messages))
282}
283
284func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.ResponseWriter, r *http.Request) {
285	id := r.PathValue("id")
286	sid := r.PathValue("sid")
287	files, err := c.backend.FileTrackerListReadFiles(r.Context(), id, sid)
288	if err != nil {
289		c.handleError(w, r, err)
290		return
291	}
292	jsonEncode(w, files)
293}
294
295func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter, r *http.Request) {
296	id := r.PathValue("id")
297
298	var req struct {
299		SessionID string `json:"session_id"`
300		Path      string `json:"path"`
301	}
302	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
303		c.server.logError(r, "Failed to decode request", "error", err)
304		jsonError(w, http.StatusBadRequest, "failed to decode request")
305		return
306	}
307
308	if err := c.backend.FileTrackerRecordRead(r.Context(), id, req.SessionID, req.Path); err != nil {
309		c.handleError(w, r, err)
310		return
311	}
312	w.WriteHeader(http.StatusOK)
313}
314
315func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWriter, r *http.Request) {
316	id := r.PathValue("id")
317	sid := r.URL.Query().Get("session_id")
318	path := r.URL.Query().Get("path")
319
320	t, err := c.backend.FileTrackerLastReadTime(r.Context(), id, sid, path)
321	if err != nil {
322		c.handleError(w, r, err)
323		return
324	}
325	jsonEncode(w, t)
326}
327
328func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *http.Request) {
329	id := r.PathValue("id")
330
331	var req struct {
332		Path string `json:"path"`
333	}
334	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
335		c.server.logError(r, "Failed to decode request", "error", err)
336		jsonError(w, http.StatusBadRequest, "failed to decode request")
337		return
338	}
339
340	if err := c.backend.LSPStart(r.Context(), id, req.Path); err != nil {
341		c.handleError(w, r, err)
342		return
343	}
344	w.WriteHeader(http.StatusOK)
345}
346
347func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *http.Request) {
348	id := r.PathValue("id")
349	if err := c.backend.LSPStopAll(r.Context(), id); err != nil {
350		c.handleError(w, r, err)
351		return
352	}
353	w.WriteHeader(http.StatusOK)
354}
355
356func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
357	id := r.PathValue("id")
358	info, err := c.backend.GetAgentInfo(id)
359	if err != nil {
360		c.handleError(w, r, err)
361		return
362	}
363	jsonEncode(w, info)
364}
365
366func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
367	id := r.PathValue("id")
368
369	var msg proto.AgentMessage
370	if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
371		c.server.logError(r, "Failed to decode request", "error", err)
372		jsonError(w, http.StatusBadRequest, "failed to decode request")
373		return
374	}
375
376	if err := c.backend.SendMessage(r.Context(), id, msg); err != nil {
377		c.handleError(w, r, err)
378		return
379	}
380	w.WriteHeader(http.StatusOK)
381}
382
383func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
384	id := r.PathValue("id")
385	if err := c.backend.InitAgent(r.Context(), id); err != nil {
386		c.handleError(w, r, err)
387		return
388	}
389	w.WriteHeader(http.StatusOK)
390}
391
392func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
393	id := r.PathValue("id")
394	if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
395		c.handleError(w, r, err)
396		return
397	}
398	w.WriteHeader(http.StatusOK)
399}
400
401func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
402	id := r.PathValue("id")
403	sid := r.PathValue("sid")
404	agentSession, err := c.backend.GetAgentSession(r.Context(), id, sid)
405	if err != nil {
406		c.handleError(w, r, err)
407		return
408	}
409	jsonEncode(w, agentSession)
410}
411
412func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
413	id := r.PathValue("id")
414	sid := r.PathValue("sid")
415	if err := c.backend.CancelSession(id, sid); err != nil {
416		c.handleError(w, r, err)
417		return
418	}
419	w.WriteHeader(http.StatusOK)
420}
421
422func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
423	id := r.PathValue("id")
424	sid := r.PathValue("sid")
425	queued, err := c.backend.QueuedPrompts(id, sid)
426	if err != nil {
427		c.handleError(w, r, err)
428		return
429	}
430	jsonEncode(w, queued)
431}
432
433func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
434	id := r.PathValue("id")
435	sid := r.PathValue("sid")
436	if err := c.backend.ClearQueue(id, sid); err != nil {
437		c.handleError(w, r, err)
438		return
439	}
440	w.WriteHeader(http.StatusOK)
441}
442
443func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
444	id := r.PathValue("id")
445	sid := r.PathValue("sid")
446	if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
447		c.handleError(w, r, err)
448		return
449	}
450	w.WriteHeader(http.StatusOK)
451}
452
453func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
454	id := r.PathValue("id")
455	sid := r.PathValue("sid")
456	prompts, err := c.backend.QueuedPromptsList(id, sid)
457	if err != nil {
458		c.handleError(w, r, err)
459		return
460	}
461	jsonEncode(w, prompts)
462}
463
464func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseWriter, r *http.Request) {
465	id := r.PathValue("id")
466	providerID := r.URL.Query().Get("provider_id")
467	model, err := c.backend.GetDefaultSmallModel(id, providerID)
468	if err != nil {
469		c.handleError(w, r, err)
470		return
471	}
472	jsonEncode(w, model)
473}
474
475func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
476	id := r.PathValue("id")
477
478	var req proto.PermissionGrant
479	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
480		c.server.logError(r, "Failed to decode request", "error", err)
481		jsonError(w, http.StatusBadRequest, "failed to decode request")
482		return
483	}
484
485	if err := c.backend.GrantPermission(id, req); err != nil {
486		c.handleError(w, r, err)
487		return
488	}
489	w.WriteHeader(http.StatusOK)
490}
491
492func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
493	id := r.PathValue("id")
494
495	var req proto.PermissionSkipRequest
496	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
497		c.server.logError(r, "Failed to decode request", "error", err)
498		jsonError(w, http.StatusBadRequest, "failed to decode request")
499		return
500	}
501
502	if err := c.backend.SetPermissionsSkip(id, req.Skip); err != nil {
503		c.handleError(w, r, err)
504		return
505	}
506}
507
508func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
509	id := r.PathValue("id")
510	skip, err := c.backend.GetPermissionsSkip(id)
511	if err != nil {
512		c.handleError(w, r, err)
513		return
514	}
515	jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
516}
517
518// handleError maps backend errors to HTTP status codes and writes the
519// JSON error response.
520func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err error) {
521	status := http.StatusInternalServerError
522	switch {
523	case errors.Is(err, backend.ErrWorkspaceNotFound):
524		status = http.StatusNotFound
525	case errors.Is(err, backend.ErrLSPClientNotFound):
526		status = http.StatusNotFound
527	case errors.Is(err, backend.ErrAgentNotInitialized):
528		status = http.StatusBadRequest
529	case errors.Is(err, backend.ErrPathRequired):
530		status = http.StatusBadRequest
531	case errors.Is(err, backend.ErrInvalidPermissionAction):
532		status = http.StatusBadRequest
533	case errors.Is(err, backend.ErrUnknownCommand):
534		status = http.StatusBadRequest
535	}
536	c.server.logError(r, err.Error())
537	jsonError(w, status, err.Error())
538}
539
540func jsonEncode(w http.ResponseWriter, v any) {
541	w.Header().Set("Content-Type", "application/json")
542	_ = json.NewEncoder(w).Encode(v)
543}
544
545func jsonError(w http.ResponseWriter, status int, message string) {
546	w.Header().Set("Content-Type", "application/json")
547	w.WriteHeader(status)
548	_ = json.NewEncoder(w).Encode(proto.Error{Message: message})
549}