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}