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}