1package server
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "os"
9 "path/filepath"
10
11 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/db"
14 "github.com/charmbracelet/crush/internal/lsp"
15 "github.com/charmbracelet/crush/internal/proto"
16 "github.com/charmbracelet/crush/internal/session"
17 "github.com/google/uuid"
18)
19
20type controllerV1 struct {
21 *Server
22}
23
24func (c *controllerV1) handleGetConfig(w http.ResponseWriter, r *http.Request) {
25 jsonEncode(w, c.cfg)
26}
27
28func (c *controllerV1) handleGetInstances(w http.ResponseWriter, r *http.Request) {
29 instances := []proto.Instance{}
30 for _, ins := range c.instances.Seq2() {
31 instances = append(instances, proto.Instance{
32 ID: ins.ID(),
33 Path: ins.Path(),
34 YOLO: ins.cfg.Permissions != nil && ins.cfg.Permissions.SkipRequests,
35 DataDir: ins.cfg.Options.DataDirectory,
36 Debug: ins.cfg.Options.Debug,
37 })
38 }
39 jsonEncode(w, instances)
40}
41
42func (c *controllerV1) handleGetInstanceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
43 id := r.PathValue("id")
44 ins, ok := c.instances.Get(id)
45 if !ok {
46 c.logError(r, "instance not found", "id", id)
47 jsonError(w, http.StatusNotFound, "instance not found")
48 return
49 }
50
51 var lsp *lsp.Client
52 lspName := r.PathValue("lsp")
53 for name, client := range ins.LSPClients.Seq2() {
54 if name == lspName {
55 lsp = client
56 break
57 }
58 }
59
60 if lsp == nil {
61 c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
62 jsonError(w, http.StatusNotFound, "LSP client not found")
63 return
64 }
65
66 diagnostics := lsp.GetDiagnostics()
67 jsonEncode(w, diagnostics)
68}
69
70func (c *controllerV1) handleGetInstanceLSPs(w http.ResponseWriter, r *http.Request) {
71 id := r.PathValue("id")
72 ins, ok := c.instances.Get(id)
73 if !ok {
74 c.logError(r, "instance not found", "id", id)
75 jsonError(w, http.StatusNotFound, "instance not found")
76 return
77 }
78
79 lspClients := ins.GetLSPStates()
80 jsonEncode(w, lspClients)
81}
82
83func (c *controllerV1) handleGetInstanceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
84 id := r.PathValue("id")
85 ins, ok := c.instances.Get(id)
86 if !ok {
87 c.logError(r, "instance not found", "id", id)
88 jsonError(w, http.StatusNotFound, "instance not found")
89 return
90 }
91
92 sid := r.PathValue("sid")
93 queued := ins.App.CoderAgent.QueuedPrompts(sid)
94 jsonEncode(w, queued)
95}
96
97func (c *controllerV1) handlePostInstanceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
98 id := r.PathValue("id")
99 ins, ok := c.instances.Get(id)
100 if !ok {
101 c.logError(r, "instance not found", "id", id)
102 jsonError(w, http.StatusNotFound, "instance not found")
103 return
104 }
105
106 sid := r.PathValue("sid")
107 ins.App.CoderAgent.ClearQueue(sid)
108}
109
110func (c *controllerV1) handleGetInstanceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
111 id := r.PathValue("id")
112 ins, ok := c.instances.Get(id)
113 if !ok {
114 c.logError(r, "instance not found", "id", id)
115 jsonError(w, http.StatusNotFound, "instance not found")
116 return
117 }
118
119 sid := r.PathValue("sid")
120 if err := ins.App.CoderAgent.Summarize(r.Context(), sid); err != nil {
121 c.logError(r, "failed to summarize session", "error", err, "id", id, "sid", sid)
122 jsonError(w, http.StatusInternalServerError, "failed to summarize session")
123 return
124 }
125}
126
127func (c *controllerV1) handlePostInstanceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
128 id := r.PathValue("id")
129 ins, ok := c.instances.Get(id)
130 if !ok {
131 c.logError(r, "instance not found", "id", id)
132 jsonError(w, http.StatusNotFound, "instance not found")
133 return
134 }
135
136 sid := r.PathValue("sid")
137 if ins.App.CoderAgent != nil {
138 ins.App.CoderAgent.Cancel(sid)
139 }
140}
141
142func (c *controllerV1) handleGetInstanceAgentSession(w http.ResponseWriter, r *http.Request) {
143 id := r.PathValue("id")
144 ins, ok := c.instances.Get(id)
145 if !ok {
146 c.logError(r, "instance not found", "id", id)
147 jsonError(w, http.StatusNotFound, "instance not found")
148 return
149 }
150
151 sid := r.PathValue("sid")
152 se, err := ins.App.Sessions.Get(r.Context(), sid)
153 if err != nil {
154 c.logError(r, "failed to get session", "error", err, "id", id, "sid", sid)
155 jsonError(w, http.StatusInternalServerError, "failed to get session")
156 return
157 }
158
159 var isSessionBusy bool
160 if ins.App.CoderAgent != nil {
161 isSessionBusy = ins.App.CoderAgent.IsSessionBusy(sid)
162 }
163
164 jsonEncode(w, proto.AgentSession{
165 Session: se,
166 IsBusy: isSessionBusy,
167 })
168}
169
170func (c *controllerV1) handlePostInstanceAgent(w http.ResponseWriter, r *http.Request) {
171 id := r.PathValue("id")
172 ins, ok := c.instances.Get(id)
173 if !ok {
174 c.logError(r, "instance not found", "id", id)
175 jsonError(w, http.StatusNotFound, "instance not found")
176 return
177 }
178
179 w.Header().Set("Accept", "application/json")
180
181 var msg proto.AgentMessage
182 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
183 c.logError(r, "failed to decode request", "error", err)
184 jsonError(w, http.StatusBadRequest, "failed to decode request")
185 return
186 }
187
188 if ins.App.CoderAgent == nil {
189 c.logError(r, "coder agent not initialized", "id", id)
190 jsonError(w, http.StatusBadRequest, "coder agent not initialized")
191 return
192 }
193
194 // NOTE: This needs to be on the server's context because the agent runs
195 // the request asynchronously.
196 // TODO: Look into this one more and make it work synchronously.
197 if _, err := ins.App.CoderAgent.Run(c.ctx, msg.SessionID, msg.Prompt, msg.Attachments...); err != nil {
198 c.logError(r, "failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
199 jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
200 return
201 }
202}
203
204func (c *controllerV1) handleGetInstanceAgent(w http.ResponseWriter, r *http.Request) {
205 id := r.PathValue("id")
206 ins, ok := c.instances.Get(id)
207 if !ok {
208 c.logError(r, "instance not found", "id", id)
209 jsonError(w, http.StatusNotFound, "instance not found")
210 return
211 }
212
213 var agentInfo proto.AgentInfo
214 if ins.App.CoderAgent != nil {
215 agentInfo = proto.AgentInfo{
216 Model: ins.App.CoderAgent.Model(),
217 IsBusy: ins.App.CoderAgent.IsBusy(),
218 }
219 }
220 jsonEncode(w, agentInfo)
221}
222
223func (c *controllerV1) handlePostInstanceAgentUpdate(w http.ResponseWriter, r *http.Request) {
224 id := r.PathValue("id")
225 ins, ok := c.instances.Get(id)
226 if !ok {
227 c.logError(r, "instance not found", "id", id)
228 jsonError(w, http.StatusNotFound, "instance not found")
229 return
230 }
231
232 if err := ins.App.UpdateAgentModel(); err != nil {
233 c.logError(r, "failed to update agent model", "error", err)
234 jsonError(w, http.StatusInternalServerError, "failed to update agent model")
235 return
236 }
237}
238
239func (c *controllerV1) handlePostInstanceAgentInit(w http.ResponseWriter, r *http.Request) {
240 id := r.PathValue("id")
241 ins, ok := c.instances.Get(id)
242 if !ok {
243 c.logError(r, "instance not found", "id", id)
244 jsonError(w, http.StatusNotFound, "instance not found")
245 return
246 }
247
248 if err := ins.App.InitCoderAgent(); err != nil {
249 c.logError(r, "failed to initialize coder agent", "error", err)
250 jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
251 return
252 }
253}
254
255func (c *controllerV1) handleGetInstanceSessionHistory(w http.ResponseWriter, r *http.Request) {
256 id := r.PathValue("id")
257 ins, ok := c.instances.Get(id)
258 if !ok {
259 c.logError(r, "instance not found", "id", id)
260 jsonError(w, http.StatusNotFound, "instance not found")
261 return
262 }
263
264 sid := r.PathValue("sid")
265 historyItems, err := ins.App.History.ListBySession(r.Context(), sid)
266 if err != nil {
267 c.logError(r, "failed to list history", "error", err, "id", id, "sid", sid)
268 jsonError(w, http.StatusInternalServerError, "failed to list history")
269 return
270 }
271
272 jsonEncode(w, historyItems)
273}
274
275func (c *controllerV1) handleGetInstanceSessionMessages(w http.ResponseWriter, r *http.Request) {
276 id := r.PathValue("id")
277 ins, ok := c.instances.Get(id)
278 if !ok {
279 c.logError(r, "instance not found", "id", id)
280 jsonError(w, http.StatusNotFound, "instance not found")
281 return
282 }
283
284 sid := r.PathValue("sid")
285 messages, err := ins.App.Messages.List(r.Context(), sid)
286 if err != nil {
287 c.logError(r, "failed to list messages", "error", err, "id", id, "sid", sid)
288 jsonError(w, http.StatusInternalServerError, "failed to list messages")
289 return
290 }
291
292 jsonEncode(w, messages)
293}
294
295func (c *controllerV1) handleGetInstanceSession(w http.ResponseWriter, r *http.Request) {
296 id := r.PathValue("id")
297 ins, ok := c.instances.Get(id)
298 if !ok {
299 c.logError(r, "instance not found", "id", id)
300 jsonError(w, http.StatusNotFound, "instance not found")
301 return
302 }
303
304 sid := r.PathValue("sid")
305 session, err := ins.App.Sessions.Get(r.Context(), sid)
306 if err != nil {
307 c.logError(r, "failedto get session", "error", err, "id", id, "sid", sid)
308 jsonError(w, http.StatusInternalServerError, "failed to get session")
309 return
310 }
311
312 jsonEncode(w, session)
313}
314
315func (c *controllerV1) handlePostInstanceSessions(w http.ResponseWriter, r *http.Request) {
316 id := r.PathValue("id")
317 ins, ok := c.instances.Get(id)
318 if !ok {
319 c.logError(r, "instance not found", "id", id)
320 jsonError(w, http.StatusNotFound, "instance not found")
321 return
322 }
323
324 var args session.Session
325 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
326 c.logError(r, "failed to decode request", "error", err)
327 jsonError(w, http.StatusBadRequest, "failed to decode request")
328 return
329 }
330
331 sess, err := ins.App.Sessions.Create(r.Context(), args.Title)
332 if err != nil {
333 c.logError(r, "failed to create session", "error", err, "id", id)
334 jsonError(w, http.StatusInternalServerError, "failed to create session")
335 return
336 }
337
338 jsonEncode(w, sess)
339}
340
341func (c *controllerV1) handleGetInstanceSessions(w http.ResponseWriter, r *http.Request) {
342 id := r.PathValue("id")
343 ins, ok := c.instances.Get(id)
344 if !ok {
345 c.logError(r, "instance not found", "id", id)
346 jsonError(w, http.StatusNotFound, "instance not found")
347 return
348 }
349
350 sessions, err := ins.App.Sessions.List(r.Context())
351 if err != nil {
352 c.logError(r, "failed to list sessions", "error", err)
353 jsonError(w, http.StatusInternalServerError, "failed to list sessions")
354 return
355 }
356
357 jsonEncode(w, sessions)
358}
359
360func (c *controllerV1) handlePostInstancePermissionsGrant(w http.ResponseWriter, r *http.Request) {
361 id := r.PathValue("id")
362 ins, ok := c.instances.Get(id)
363 if !ok {
364 c.logError(r, "instance not found", "id", id)
365 jsonError(w, http.StatusNotFound, "instance not found")
366 return
367 }
368
369 var req proto.PermissionGrant
370 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
371 c.logError(r, "failed to decode request", "error", err)
372 jsonError(w, http.StatusBadRequest, "failed to decode request")
373 return
374 }
375
376 switch req.Action {
377 case proto.PermissionAllow:
378 ins.App.Permissions.Grant(req.Permission)
379 case proto.PermissionAllowForSession:
380 ins.App.Permissions.GrantPersistent(req.Permission)
381 case proto.PermissionDeny:
382 ins.App.Permissions.Deny(req.Permission)
383 default:
384 c.logError(r, "invalid permission action", "action", req.Action)
385 jsonError(w, http.StatusBadRequest, "invalid permission action")
386 return
387 }
388}
389
390func (c *controllerV1) handlePostInstancePermissionsSkip(w http.ResponseWriter, r *http.Request) {
391 id := r.PathValue("id")
392 ins, ok := c.instances.Get(id)
393 if !ok {
394 c.logError(r, "instance not found", "id", id)
395 jsonError(w, http.StatusNotFound, "instance not found")
396 return
397 }
398
399 var req proto.PermissionSkipRequest
400 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
401 c.logError(r, "failed to decode request", "error", err)
402 jsonError(w, http.StatusBadRequest, "failed to decode request")
403 return
404 }
405
406 ins.App.Permissions.SetSkipRequests(req.Skip)
407}
408
409func (c *controllerV1) handleGetInstancePermissionsSkip(w http.ResponseWriter, r *http.Request) {
410 id := r.PathValue("id")
411 ins, ok := c.instances.Get(id)
412 if !ok {
413 c.logError(r, "instance not found", "id", id)
414 jsonError(w, http.StatusNotFound, "instance not found")
415 return
416 }
417
418 skip := ins.App.Permissions.SkipRequests()
419 jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
420}
421
422func (c *controllerV1) handleGetInstanceEvents(w http.ResponseWriter, r *http.Request) {
423 flusher := http.NewResponseController(w)
424 id := r.PathValue("id")
425 ins, ok := c.instances.Get(id)
426 if !ok {
427 c.logError(r, "instance not found", "id", id)
428 jsonError(w, http.StatusNotFound, "instance not found")
429 return
430 }
431
432 w.Header().Set("Content-Type", "text/event-stream")
433 w.Header().Set("Cache-Control", "no-cache")
434 w.Header().Set("Connection", "keep-alive")
435
436 for {
437 select {
438 case <-r.Context().Done():
439 c.logDebug(r, "stopping event stream")
440 return
441 case ev := <-ins.App.Events():
442 c.logDebug(r, "sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
443 data, err := json.Marshal(ev)
444 if err != nil {
445 c.logError(r, "failed to marshal event", "error", err)
446 continue
447 }
448
449 fmt.Fprintf(w, "data: %s\n\n", data)
450 flusher.Flush()
451 }
452 }
453}
454
455func (c *controllerV1) handleGetInstanceConfig(w http.ResponseWriter, r *http.Request) {
456 id := r.PathValue("id")
457 ins, ok := c.instances.Get(id)
458 if !ok {
459 c.logError(r, "instance not found", "id", id)
460 jsonError(w, http.StatusNotFound, "instance not found")
461 return
462 }
463
464 jsonEncode(w, ins.cfg)
465}
466
467func (c *controllerV1) handleDeleteInstances(w http.ResponseWriter, r *http.Request) {
468 var ids []string
469 id := r.URL.Query().Get("id")
470 if id != "" {
471 ids = append(ids, id)
472 }
473
474 // Get IDs from body
475 var args []proto.Instance
476 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
477 c.logError(r, "failed to decode request", "error", err)
478 jsonError(w, http.StatusBadRequest, "failed to decode request")
479 return
480 }
481 ids = append(ids, func() []string {
482 out := make([]string, len(args))
483 for i, arg := range args {
484 out[i] = arg.ID
485 }
486 return out
487 }()...)
488
489 for _, id := range ids {
490 ins, ok := c.instances.Get(id)
491 if ok {
492 ins.App.Shutdown()
493 }
494 c.instances.Del(id)
495 }
496}
497
498func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Request) {
499 var args proto.Instance
500 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
501 c.logError(r, "failed to decode request", "error", err)
502 jsonError(w, http.StatusBadRequest, "failed to decode request")
503 return
504 }
505
506 if args.Path == "" {
507 c.logError(r, "path is required")
508 jsonError(w, http.StatusBadRequest, "path is required")
509 return
510 }
511
512 id := uuid.New().String()
513 cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
514 if err != nil {
515 c.logError(r, "failed to initialize config", "error", err)
516 jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
517 return
518 }
519
520 if cfg.Permissions == nil {
521 cfg.Permissions = &config.Permissions{}
522 }
523 cfg.Permissions.SkipRequests = args.YOLO
524
525 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
526 c.logError(r, "failed to create data directory", "error", err)
527 jsonError(w, http.StatusInternalServerError, "failed to create data directory")
528 return
529 }
530
531 // Connect to DB; this will also run migrations.
532 conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
533 if err != nil {
534 c.logError(r, "failed to connect to database", "error", err)
535 jsonError(w, http.StatusInternalServerError, "failed to connect to database")
536 return
537 }
538
539 appInstance, err := app.New(c.ctx, conn, cfg)
540 if err != nil {
541 slog.Error("failed to create app instance", "error", err)
542 jsonError(w, http.StatusInternalServerError, "failed to create app instance")
543 return
544 }
545
546 ins := &Instance{
547 App: appInstance,
548 State: InstanceStateCreated,
549 id: id,
550 path: args.Path,
551 cfg: cfg,
552 }
553
554 c.instances.Set(id, ins)
555 jsonEncode(w, proto.Instance{
556 ID: id,
557 Path: args.Path,
558 DataDir: cfg.Options.DataDirectory,
559 Debug: cfg.Options.Debug,
560 YOLO: cfg.Permissions.SkipRequests,
561 })
562}
563
564func createDotCrushDir(dir string) error {
565 if err := os.MkdirAll(dir, 0o700); err != nil {
566 return fmt.Errorf("failed to create data directory: %q %w", dir, err)
567 }
568
569 gitIgnorePath := filepath.Join(dir, ".gitignore")
570 if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
571 if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
572 return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
573 }
574 }
575
576 return nil
577}
578
579func jsonEncode(w http.ResponseWriter, v any) {
580 w.Header().Set("Content-Type", "application/json")
581 _ = json.NewEncoder(w).Encode(v)
582}
583
584func jsonError(w http.ResponseWriter, status int, message string) {
585 w.Header().Set("Content-Type", "application/json")
586 w.WriteHeader(status)
587 _ = json.NewEncoder(w).Encode(proto.Error{Message: message})
588}