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			data, err := json.Marshal(ev)
128			if err != nil {
129				c.server.logError(r, "Failed to marshal event", "error", err)
130				continue
131			}
132
133			fmt.Fprintf(w, "data: %s\n\n", data)
134			flusher.Flush()
135		}
136	}
137}
138
139func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
140	id := r.PathValue("id")
141	states, err := c.backend.GetLSPStates(id)
142	if err != nil {
143		c.handleError(w, r, err)
144		return
145	}
146	jsonEncode(w, states)
147}
148
149func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
150	id := r.PathValue("id")
151	lspName := r.PathValue("lsp")
152	diagnostics, err := c.backend.GetLSPDiagnostics(id, lspName)
153	if err != nil {
154		c.handleError(w, r, err)
155		return
156	}
157	jsonEncode(w, diagnostics)
158}
159
160func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
161	id := r.PathValue("id")
162	sessions, err := c.backend.ListSessions(r.Context(), id)
163	if err != nil {
164		c.handleError(w, r, err)
165		return
166	}
167	jsonEncode(w, sessions)
168}
169
170func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
171	id := r.PathValue("id")
172
173	var args session.Session
174	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
175		c.server.logError(r, "Failed to decode request", "error", err)
176		jsonError(w, http.StatusBadRequest, "failed to decode request")
177		return
178	}
179
180	sess, err := c.backend.CreateSession(r.Context(), id, args.Title)
181	if err != nil {
182		c.handleError(w, r, err)
183		return
184	}
185	jsonEncode(w, sess)
186}
187
188func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
189	id := r.PathValue("id")
190	sid := r.PathValue("sid")
191	sess, err := c.backend.GetSession(r.Context(), id, sid)
192	if err != nil {
193		c.handleError(w, r, err)
194		return
195	}
196	jsonEncode(w, sess)
197}
198
199func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
200	id := r.PathValue("id")
201	sid := r.PathValue("sid")
202	history, err := c.backend.ListSessionHistory(r.Context(), id, sid)
203	if err != nil {
204		c.handleError(w, r, err)
205		return
206	}
207	jsonEncode(w, history)
208}
209
210func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
211	id := r.PathValue("id")
212	sid := r.PathValue("sid")
213	messages, err := c.backend.ListSessionMessages(r.Context(), id, sid)
214	if err != nil {
215		c.handleError(w, r, err)
216		return
217	}
218	jsonEncode(w, messages)
219}
220
221func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
222	id := r.PathValue("id")
223	info, err := c.backend.GetAgentInfo(id)
224	if err != nil {
225		c.handleError(w, r, err)
226		return
227	}
228	jsonEncode(w, info)
229}
230
231func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
232	id := r.PathValue("id")
233
234	w.Header().Set("Accept", "application/json")
235
236	var msg proto.AgentMessage
237	if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
238		c.server.logError(r, "Failed to decode request", "error", err)
239		jsonError(w, http.StatusBadRequest, "failed to decode request")
240		return
241	}
242
243	if err := c.backend.SendMessage(r.Context(), id, msg); err != nil {
244		c.handleError(w, r, err)
245		return
246	}
247}
248
249func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
250	id := r.PathValue("id")
251	if err := c.backend.InitAgent(r.Context(), id); err != nil {
252		c.handleError(w, r, err)
253		return
254	}
255}
256
257func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
258	id := r.PathValue("id")
259	if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
260		c.handleError(w, r, err)
261		return
262	}
263}
264
265func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
266	id := r.PathValue("id")
267	sid := r.PathValue("sid")
268	agentSession, err := c.backend.GetAgentSession(r.Context(), id, sid)
269	if err != nil {
270		c.handleError(w, r, err)
271		return
272	}
273	jsonEncode(w, agentSession)
274}
275
276func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
277	id := r.PathValue("id")
278	sid := r.PathValue("sid")
279	if err := c.backend.CancelSession(id, sid); err != nil {
280		c.handleError(w, r, err)
281		return
282	}
283	w.WriteHeader(http.StatusOK)
284}
285
286func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
287	id := r.PathValue("id")
288	sid := r.PathValue("sid")
289	queued, err := c.backend.QueuedPrompts(id, sid)
290	if err != nil {
291		c.handleError(w, r, err)
292		return
293	}
294	jsonEncode(w, queued)
295}
296
297func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
298	id := r.PathValue("id")
299	sid := r.PathValue("sid")
300	if err := c.backend.ClearQueue(id, sid); err != nil {
301		c.handleError(w, r, err)
302		return
303	}
304	w.WriteHeader(http.StatusOK)
305}
306
307func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
308	id := r.PathValue("id")
309	sid := r.PathValue("sid")
310	if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
311		c.handleError(w, r, err)
312		return
313	}
314}
315
316func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
317	id := r.PathValue("id")
318
319	var req proto.PermissionGrant
320	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
321		c.server.logError(r, "Failed to decode request", "error", err)
322		jsonError(w, http.StatusBadRequest, "failed to decode request")
323		return
324	}
325
326	if err := c.backend.GrantPermission(id, req); err != nil {
327		c.handleError(w, r, err)
328		return
329	}
330}
331
332func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
333	id := r.PathValue("id")
334
335	var req proto.PermissionSkipRequest
336	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
337		c.server.logError(r, "Failed to decode request", "error", err)
338		jsonError(w, http.StatusBadRequest, "failed to decode request")
339		return
340	}
341
342	if err := c.backend.SetPermissionsSkip(id, req.Skip); err != nil {
343		c.handleError(w, r, err)
344		return
345	}
346}
347
348func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
349	id := r.PathValue("id")
350	skip, err := c.backend.GetPermissionsSkip(id)
351	if err != nil {
352		c.handleError(w, r, err)
353		return
354	}
355	jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
356}
357
358// handleError maps backend errors to HTTP status codes and writes the
359// JSON error response.
360func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err error) {
361	status := http.StatusInternalServerError
362	switch {
363	case errors.Is(err, backend.ErrWorkspaceNotFound):
364		status = http.StatusNotFound
365	case errors.Is(err, backend.ErrLSPClientNotFound):
366		status = http.StatusNotFound
367	case errors.Is(err, backend.ErrAgentNotInitialized):
368		status = http.StatusBadRequest
369	case errors.Is(err, backend.ErrPathRequired):
370		status = http.StatusBadRequest
371	case errors.Is(err, backend.ErrInvalidPermissionAction):
372		status = http.StatusBadRequest
373	case errors.Is(err, backend.ErrUnknownCommand):
374		status = http.StatusBadRequest
375	}
376	c.server.logError(r, err.Error())
377	jsonError(w, status, err.Error())
378}
379
380func jsonEncode(w http.ResponseWriter, v any) {
381	w.Header().Set("Content-Type", "application/json")
382	_ = json.NewEncoder(w).Encode(v)
383}
384
385func jsonError(w http.ResponseWriter, status int, message string) {
386	w.Header().Set("Content-Type", "application/json")
387	w.WriteHeader(status)
388	_ = json.NewEncoder(w).Encode(proto.Error{Message: message})
389}