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 "github.com/google/uuid"
13)
14
15type controllerV1 struct {
16 backend *backend.Backend
17 server *Server
18}
19
20// handleGetHealth checks server health.
21//
22// @Summary Health check
23// @Tags system
24// @Success 200
25// @Router /health [get]
26func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
27 w.WriteHeader(http.StatusOK)
28}
29
30// handleGetVersion returns server version information.
31//
32// @Summary Get server version
33// @Tags system
34// @Produce json
35// @Success 200 {object} proto.VersionInfo
36// @Router /version [get]
37func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
38 jsonEncode(w, c.backend.VersionInfo())
39}
40
41// handlePostControl sends a control command to the server.
42//
43// @Summary Send server control command
44// @Tags system
45// @Accept json
46// @Param request body proto.ServerControl true "Control command (e.g. shutdown)"
47// @Success 200
48// @Failure 400 {object} proto.Error
49// @Router /control [post]
50func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
51 var req proto.ServerControl
52 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
53 c.server.logError(r, "Failed to decode request", "error", err)
54 jsonError(w, http.StatusBadRequest, "failed to decode request")
55 return
56 }
57
58 switch req.Command {
59 case "shutdown":
60 c.backend.Shutdown()
61 default:
62 c.server.logError(r, "Unknown command", "command", req.Command)
63 jsonError(w, http.StatusBadRequest, "unknown command")
64 return
65 }
66}
67
68// handleGetConfig returns global server configuration.
69//
70// @Summary Get server config
71// @Tags system
72// @Produce json
73// @Success 200 {object} object
74// @Router /config [get]
75func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
76 jsonEncode(w, c.backend.Config())
77}
78
79// handleGetWorkspaces lists all workspaces.
80//
81// @Summary List workspaces
82// @Tags workspaces
83// @Produce json
84// @Success 200 {array} proto.Workspace
85// @Router /workspaces [get]
86func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
87 jsonEncode(w, c.backend.ListWorkspaces())
88}
89
90// handleGetWorkspace returns a single workspace by ID.
91//
92// @Summary Get workspace
93// @Tags workspaces
94// @Produce json
95// @Param id path string true "Workspace ID"
96// @Success 200 {object} proto.Workspace
97// @Failure 404 {object} proto.Error
98// @Failure 500 {object} proto.Error
99// @Router /workspaces/{id} [get]
100func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
101 id := r.PathValue("id")
102 ws, err := c.backend.GetWorkspaceProto(id)
103 if err != nil {
104 c.handleError(w, r, err)
105 return
106 }
107 jsonEncode(w, ws)
108}
109
110// handlePostWorkspaces creates a new workspace.
111//
112// @Summary Create workspace
113// @Tags workspaces
114// @Accept json
115// @Produce json
116// @Param request body proto.Workspace true "Workspace creation params"
117// @Success 200 {object} proto.Workspace
118// @Failure 400 {object} proto.Error
119// @Failure 500 {object} proto.Error
120// @Router /workspaces [post]
121func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
122 var args proto.Workspace
123 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
124 c.server.logError(r, "Failed to decode request", "error", err)
125 jsonError(w, http.StatusBadRequest, "failed to decode request")
126 return
127 }
128
129 _, result, err := c.backend.CreateWorkspace(args)
130 if err != nil {
131 c.handleError(w, r, err)
132 return
133 }
134 jsonEncode(w, result)
135}
136
137// requireClientID reads the client_id query parameter and validates it
138// as a UUID. On failure it writes a 400 and returns false.
139func (c *controllerV1) requireClientID(w http.ResponseWriter, r *http.Request) (string, bool) {
140 cid := r.URL.Query().Get("client_id")
141 if cid == "" {
142 c.server.logError(r, "Missing client_id query parameter")
143 jsonError(w, http.StatusBadRequest, "client_id is required")
144 return "", false
145 }
146 if _, err := uuid.Parse(cid); err != nil {
147 c.server.logError(r, "Invalid client_id", "error", err)
148 jsonError(w, http.StatusBadRequest, "client_id is not a valid UUID")
149 return "", false
150 }
151 return cid, true
152}
153
154// handlePostWorkspaceCurrentSession records the calling client's
155// current session selection for the workspace. An empty session_id
156// clears the entry (e.g. the client is on the landing screen).
157//
158// @Summary Set current session for a client
159// @Tags workspaces
160// @Accept json
161// @Produce json
162// @Param id path string true "Workspace ID"
163// @Param client_id query string true "Client ID (UUID)"
164// @Param request body proto.CurrentSession true "Current session selection"
165// @Success 200
166// @Failure 400 {object} proto.Error
167// @Failure 404 {object} proto.Error
168// @Router /workspaces/{id}/current-session [post]
169func (c *controllerV1) handlePostWorkspaceCurrentSession(w http.ResponseWriter, r *http.Request) {
170 id := r.PathValue("id")
171 clientID, ok := c.requireClientID(w, r)
172 if !ok {
173 return
174 }
175 var req proto.CurrentSession
176 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
177 c.server.logError(r, "Failed to decode request", "error", err)
178 jsonError(w, http.StatusBadRequest, "failed to decode request")
179 return
180 }
181 if err := c.backend.SetCurrentSession(id, clientID, req.SessionID); err != nil {
182 c.handleError(w, r, err)
183 return
184 }
185}
186
187// handleDeleteWorkspaces deletes a workspace.
188//
189// @Summary Delete workspace
190// @Tags workspaces
191// @Param id path string true "Workspace ID"
192// @Success 200
193// @Failure 404 {object} proto.Error
194// @Router /workspaces/{id} [delete]
195func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
196 id := r.PathValue("id")
197 clientID, ok := c.requireClientID(w, r)
198 if !ok {
199 return
200 }
201 if err := c.backend.DeleteWorkspace(id, clientID); err != nil {
202 c.handleError(w, r, err)
203 return
204 }
205}
206
207// handleGetWorkspaceConfig returns workspace configuration.
208//
209// @Summary Get workspace config
210// @Tags workspaces
211// @Produce json
212// @Param id path string true "Workspace ID"
213// @Success 200 {object} object
214// @Failure 404 {object} proto.Error
215// @Failure 500 {object} proto.Error
216// @Router /workspaces/{id}/config [get]
217func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
218 id := r.PathValue("id")
219 cfg, err := c.backend.GetWorkspaceConfig(id)
220 if err != nil {
221 c.handleError(w, r, err)
222 return
223 }
224 jsonEncode(w, cfg)
225}
226
227// handleGetWorkspaceProviders lists available providers for a workspace.
228//
229// @Summary Get workspace providers
230// @Tags workspaces
231// @Produce json
232// @Param id path string true "Workspace ID"
233// @Success 200 {object} object
234// @Failure 404 {object} proto.Error
235// @Failure 500 {object} proto.Error
236// @Router /workspaces/{id}/providers [get]
237func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
238 id := r.PathValue("id")
239 providers, err := c.backend.GetWorkspaceProviders(id)
240 if err != nil {
241 c.handleError(w, r, err)
242 return
243 }
244 jsonEncode(w, providers)
245}
246
247// handleGetWorkspaceEvents streams workspace events as Server-Sent Events.
248//
249// @Summary Stream workspace events (SSE)
250// @Tags workspaces
251// @Produce text/event-stream
252// @Param id path string true "Workspace ID"
253// @Success 200
254// @Failure 404 {object} proto.Error
255// @Failure 500 {object} proto.Error
256// @Router /workspaces/{id}/events [get]
257func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
258 flusher := http.NewResponseController(w)
259 id := r.PathValue("id")
260 clientID, ok := c.requireClientID(w, r)
261 if !ok {
262 return
263 }
264 if err := c.backend.AttachClient(id, clientID); err != nil {
265 c.handleError(w, r, err)
266 return
267 }
268 defer c.backend.DetachClient(id, clientID)
269 events, err := c.backend.SubscribeEvents(r.Context(), id)
270 if err != nil {
271 c.handleError(w, r, err)
272 return
273 }
274
275 w.Header().Set("Content-Type", "text/event-stream")
276 w.Header().Set("Cache-Control", "no-cache")
277 w.Header().Set("Connection", "keep-alive")
278 // Flush headers immediately so clients see the 200 response
279 // before any events arrive. Without this, a quiet workspace
280 // keeps the client's SubscribeEvents call blocked on the
281 // initial RoundTrip.
282 w.WriteHeader(http.StatusOK)
283 flusher.Flush()
284
285 for {
286 select {
287 case <-r.Context().Done():
288 c.server.logDebug(r, "Stopping event stream")
289 return
290 case ev, ok := <-events:
291 if !ok {
292 return
293 }
294 wrapped := wrapEvent(ev.Payload)
295 if wrapped == nil {
296 continue
297 }
298 data, err := json.Marshal(wrapped)
299 if err != nil {
300 c.server.logError(r, "Failed to marshal event", "error", err)
301 continue
302 }
303
304 fmt.Fprintf(w, "data: %s\n\n", data)
305 flusher.Flush()
306 }
307 }
308}
309
310// handleGetWorkspaceLSPs lists LSP clients for a workspace.
311//
312// @Summary List LSP clients
313// @Tags lsp
314// @Produce json
315// @Param id path string true "Workspace ID"
316// @Success 200 {object} map[string]proto.LSPClientInfo
317// @Failure 404 {object} proto.Error
318// @Failure 500 {object} proto.Error
319// @Router /workspaces/{id}/lsps [get]
320func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
321 id := r.PathValue("id")
322 states, err := c.backend.GetLSPStates(id)
323 if err != nil {
324 c.handleError(w, r, err)
325 return
326 }
327 result := make(map[string]proto.LSPClientInfo, len(states))
328 for k, v := range states {
329 result[k] = proto.LSPClientInfo{
330 Name: v.Name,
331 State: v.State,
332 Error: v.Error,
333 DiagnosticCount: v.DiagnosticCount,
334 ConnectedAt: v.ConnectedAt,
335 }
336 }
337 jsonEncode(w, result)
338}
339
340// handleGetWorkspaceLSPDiagnostics returns diagnostics for an LSP client.
341//
342// @Summary Get LSP diagnostics
343// @Tags lsp
344// @Produce json
345// @Param id path string true "Workspace ID"
346// @Param lsp path string true "LSP client name"
347// @Success 200 {object} object
348// @Failure 404 {object} proto.Error
349// @Failure 500 {object} proto.Error
350// @Router /workspaces/{id}/lsps/{lsp}/diagnostics [get]
351func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
352 id := r.PathValue("id")
353 lspName := r.PathValue("lsp")
354 diagnostics, err := c.backend.GetLSPDiagnostics(id, lspName)
355 if err != nil {
356 c.handleError(w, r, err)
357 return
358 }
359 jsonEncode(w, diagnostics)
360}
361
362// handleGetWorkspaceSessions lists sessions for a workspace.
363//
364// @Summary List sessions
365// @Tags sessions
366// @Produce json
367// @Param id path string true "Workspace ID"
368// @Success 200 {array} proto.Session
369// @Failure 404 {object} proto.Error
370// @Failure 500 {object} proto.Error
371// @Router /workspaces/{id}/sessions [get]
372func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
373 id := r.PathValue("id")
374 sessions, err := c.backend.ListSessions(r.Context(), id)
375 if err != nil {
376 c.handleError(w, r, err)
377 return
378 }
379 ws, _ := c.backend.GetWorkspace(id)
380 result := make([]proto.Session, len(sessions))
381 for i, s := range sessions {
382 result[i] = sessionToProto(s)
383 result[i].IsBusy = isSessionBusy(ws, s.ID)
384 result[i].AttachedClients = attachedClients(ws, s.ID)
385 }
386 jsonEncode(w, result)
387}
388
389// handlePostWorkspaceSessions creates a new session in a workspace.
390//
391// @Summary Create session
392// @Tags sessions
393// @Accept json
394// @Produce json
395// @Param id path string true "Workspace ID"
396// @Param request body proto.Session true "Session creation params (title)"
397// @Success 200 {object} proto.Session
398// @Failure 400 {object} proto.Error
399// @Failure 404 {object} proto.Error
400// @Failure 500 {object} proto.Error
401// @Router /workspaces/{id}/sessions [post]
402func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
403 id := r.PathValue("id")
404
405 var args session.Session
406 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
407 c.server.logError(r, "Failed to decode request", "error", err)
408 jsonError(w, http.StatusBadRequest, "failed to decode request")
409 return
410 }
411
412 sess, err := c.backend.CreateSession(r.Context(), id, args.Title)
413 if err != nil {
414 c.handleError(w, r, err)
415 return
416 }
417 ws, _ := c.backend.GetWorkspace(id)
418 out := sessionToProto(sess)
419 out.IsBusy = isSessionBusy(ws, sess.ID)
420 out.AttachedClients = attachedClients(ws, sess.ID)
421 jsonEncode(w, out)
422}
423
424// handleGetWorkspaceSession returns a single session.
425//
426// @Summary Get session
427// @Tags sessions
428// @Produce json
429// @Param id path string true "Workspace ID"
430// @Param sid path string true "Session ID"
431// @Success 200 {object} proto.Session
432// @Failure 404 {object} proto.Error
433// @Failure 500 {object} proto.Error
434// @Router /workspaces/{id}/sessions/{sid} [get]
435func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
436 id := r.PathValue("id")
437 sid := r.PathValue("sid")
438 sess, err := c.backend.GetSession(r.Context(), id, sid)
439 if err != nil {
440 c.handleError(w, r, err)
441 return
442 }
443 ws, _ := c.backend.GetWorkspace(id)
444 out := sessionToProto(sess)
445 out.IsBusy = isSessionBusy(ws, sess.ID)
446 out.AttachedClients = attachedClients(ws, sess.ID)
447 jsonEncode(w, out)
448}
449
450// handleGetWorkspaceSessionHistory returns the history for a session.
451//
452// @Summary Get session history
453// @Tags sessions
454// @Produce json
455// @Param id path string true "Workspace ID"
456// @Param sid path string true "Session ID"
457// @Success 200 {array} proto.File
458// @Failure 404 {object} proto.Error
459// @Failure 500 {object} proto.Error
460// @Router /workspaces/{id}/sessions/{sid}/history [get]
461func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
462 id := r.PathValue("id")
463 sid := r.PathValue("sid")
464 history, err := c.backend.ListSessionHistory(r.Context(), id, sid)
465 if err != nil {
466 c.handleError(w, r, err)
467 return
468 }
469 jsonEncode(w, history)
470}
471
472// handleGetWorkspaceSessionMessages returns all messages for a session.
473//
474// @Summary Get session messages
475// @Tags sessions
476// @Produce json
477// @Param id path string true "Workspace ID"
478// @Param sid path string true "Session ID"
479// @Success 200 {array} proto.Message
480// @Failure 404 {object} proto.Error
481// @Failure 500 {object} proto.Error
482// @Router /workspaces/{id}/sessions/{sid}/messages [get]
483func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
484 id := r.PathValue("id")
485 sid := r.PathValue("sid")
486 messages, err := c.backend.ListSessionMessages(r.Context(), id, sid)
487 if err != nil {
488 c.handleError(w, r, err)
489 return
490 }
491 jsonEncode(w, messagesToProto(messages))
492}
493
494// handlePutWorkspaceSession updates a session.
495//
496// @Summary Update session
497// @Tags sessions
498// @Accept json
499// @Produce json
500// @Param id path string true "Workspace ID"
501// @Param sid path string true "Session ID"
502// @Param request body proto.Session true "Updated session"
503// @Success 200 {object} proto.Session
504// @Failure 400 {object} proto.Error
505// @Failure 404 {object} proto.Error
506// @Failure 500 {object} proto.Error
507// @Router /workspaces/{id}/sessions/{sid} [put]
508func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http.Request) {
509 id := r.PathValue("id")
510
511 var sess session.Session
512 if err := json.NewDecoder(r.Body).Decode(&sess); err != nil {
513 c.server.logError(r, "Failed to decode request", "error", err)
514 jsonError(w, http.StatusBadRequest, "failed to decode request")
515 return
516 }
517
518 saved, err := c.backend.SaveSession(r.Context(), id, sess)
519 if err != nil {
520 c.handleError(w, r, err)
521 return
522 }
523 ws, _ := c.backend.GetWorkspace(id)
524 out := sessionToProto(saved)
525 out.IsBusy = isSessionBusy(ws, saved.ID)
526 out.AttachedClients = attachedClients(ws, saved.ID)
527 jsonEncode(w, out)
528}
529
530// handleDeleteWorkspaceSession deletes a session.
531//
532// @Summary Delete session
533// @Tags sessions
534// @Param id path string true "Workspace ID"
535// @Param sid path string true "Session ID"
536// @Success 200
537// @Failure 404 {object} proto.Error
538// @Failure 500 {object} proto.Error
539// @Router /workspaces/{id}/sessions/{sid} [delete]
540func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *http.Request) {
541 id := r.PathValue("id")
542 sid := r.PathValue("sid")
543 if err := c.backend.DeleteSession(r.Context(), id, sid); err != nil {
544 c.handleError(w, r, err)
545 return
546 }
547 w.WriteHeader(http.StatusOK)
548}
549
550// handleGetWorkspaceSessionUserMessages returns user messages for a session.
551//
552// @Summary Get user messages for session
553// @Tags sessions
554// @Produce json
555// @Param id path string true "Workspace ID"
556// @Param sid path string true "Session ID"
557// @Success 200 {array} proto.Message
558// @Failure 404 {object} proto.Error
559// @Failure 500 {object} proto.Error
560// @Router /workspaces/{id}/sessions/{sid}/messages/user [get]
561func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWriter, r *http.Request) {
562 id := r.PathValue("id")
563 sid := r.PathValue("sid")
564 messages, err := c.backend.ListUserMessages(r.Context(), id, sid)
565 if err != nil {
566 c.handleError(w, r, err)
567 return
568 }
569 jsonEncode(w, messagesToProto(messages))
570}
571
572// handleGetWorkspaceAllUserMessages returns all user messages across sessions.
573//
574// @Summary Get all user messages for workspace
575// @Tags workspaces
576// @Produce json
577// @Param id path string true "Workspace ID"
578// @Success 200 {array} proto.Message
579// @Failure 404 {object} proto.Error
580// @Failure 500 {object} proto.Error
581// @Router /workspaces/{id}/messages/user [get]
582func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter, r *http.Request) {
583 id := r.PathValue("id")
584 messages, err := c.backend.ListAllUserMessages(r.Context(), id)
585 if err != nil {
586 c.handleError(w, r, err)
587 return
588 }
589 jsonEncode(w, messagesToProto(messages))
590}
591
592// handleGetWorkspaceSessionFileTrackerFiles lists files read in a session.
593//
594// @Summary List tracked files for session
595// @Tags filetracker
596// @Produce json
597// @Param id path string true "Workspace ID"
598// @Param sid path string true "Session ID"
599// @Success 200 {array} string
600// @Failure 404 {object} proto.Error
601// @Failure 500 {object} proto.Error
602// @Router /workspaces/{id}/sessions/{sid}/filetracker/files [get]
603func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.ResponseWriter, r *http.Request) {
604 id := r.PathValue("id")
605 sid := r.PathValue("sid")
606 files, err := c.backend.FileTrackerListReadFiles(r.Context(), id, sid)
607 if err != nil {
608 c.handleError(w, r, err)
609 return
610 }
611 jsonEncode(w, files)
612}
613
614// handlePostWorkspaceFileTrackerRead records a file read event.
615//
616// @Summary Record file read
617// @Tags filetracker
618// @Accept json
619// @Param id path string true "Workspace ID"
620// @Param request body proto.FileTrackerReadRequest true "File tracker read request"
621// @Success 200
622// @Failure 400 {object} proto.Error
623// @Failure 404 {object} proto.Error
624// @Failure 500 {object} proto.Error
625// @Router /workspaces/{id}/filetracker/read [post]
626func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter, r *http.Request) {
627 id := r.PathValue("id")
628
629 var req proto.FileTrackerReadRequest
630 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
631 c.server.logError(r, "Failed to decode request", "error", err)
632 jsonError(w, http.StatusBadRequest, "failed to decode request")
633 return
634 }
635
636 if err := c.backend.FileTrackerRecordRead(r.Context(), id, req.SessionID, req.Path); err != nil {
637 c.handleError(w, r, err)
638 return
639 }
640 w.WriteHeader(http.StatusOK)
641}
642
643// handleGetWorkspaceFileTrackerLastRead returns the last read time for a file.
644//
645// @Summary Get last read time for file
646// @Tags filetracker
647// @Produce json
648// @Param id path string true "Workspace ID"
649// @Param session_id query string false "Session ID"
650// @Param path query string true "File path"
651// @Success 200 {object} object
652// @Failure 404 {object} proto.Error
653// @Failure 500 {object} proto.Error
654// @Router /workspaces/{id}/filetracker/lastread [get]
655func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWriter, r *http.Request) {
656 id := r.PathValue("id")
657 sid := r.URL.Query().Get("session_id")
658 path := r.URL.Query().Get("path")
659
660 t, err := c.backend.FileTrackerLastReadTime(r.Context(), id, sid, path)
661 if err != nil {
662 c.handleError(w, r, err)
663 return
664 }
665 jsonEncode(w, t)
666}
667
668// handlePostWorkspaceLSPStart starts an LSP server for a path.
669//
670// @Summary Start LSP server
671// @Tags lsp
672// @Accept json
673// @Param id path string true "Workspace ID"
674// @Param request body proto.LSPStartRequest true "LSP start request"
675// @Success 200
676// @Failure 400 {object} proto.Error
677// @Failure 404 {object} proto.Error
678// @Failure 500 {object} proto.Error
679// @Router /workspaces/{id}/lsps/start [post]
680func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *http.Request) {
681 id := r.PathValue("id")
682
683 var req proto.LSPStartRequest
684 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
685 c.server.logError(r, "Failed to decode request", "error", err)
686 jsonError(w, http.StatusBadRequest, "failed to decode request")
687 return
688 }
689
690 if err := c.backend.LSPStart(r.Context(), id, req.Path); err != nil {
691 c.handleError(w, r, err)
692 return
693 }
694 w.WriteHeader(http.StatusOK)
695}
696
697// handlePostWorkspaceLSPStopAll stops all LSP servers.
698//
699// @Summary Stop all LSP servers
700// @Tags lsp
701// @Param id path string true "Workspace ID"
702// @Success 200
703// @Failure 404 {object} proto.Error
704// @Failure 500 {object} proto.Error
705// @Router /workspaces/{id}/lsps/stop [post]
706func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *http.Request) {
707 id := r.PathValue("id")
708 if err := c.backend.LSPStopAll(r.Context(), id); err != nil {
709 c.handleError(w, r, err)
710 return
711 }
712 w.WriteHeader(http.StatusOK)
713}
714
715// handleGetWorkspaceAgent returns agent info for a workspace.
716//
717// @Summary Get agent info
718// @Tags agent
719// @Produce json
720// @Param id path string true "Workspace ID"
721// @Success 200 {object} proto.AgentInfo
722// @Failure 404 {object} proto.Error
723// @Failure 500 {object} proto.Error
724// @Router /workspaces/{id}/agent [get]
725func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
726 id := r.PathValue("id")
727 info, err := c.backend.GetAgentInfo(id)
728 if err != nil {
729 c.handleError(w, r, err)
730 return
731 }
732 jsonEncode(w, info)
733}
734
735// handlePostWorkspaceAgent sends a message to the agent.
736//
737// @Summary Send message to agent
738// @Tags agent
739// @Accept json
740// @Param id path string true "Workspace ID"
741// @Param request body proto.AgentMessage true "Agent message"
742// @Success 200
743// @Failure 400 {object} proto.Error
744// @Failure 404 {object} proto.Error
745// @Failure 500 {object} proto.Error
746// @Router /workspaces/{id}/agent [post]
747func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
748 id := r.PathValue("id")
749
750 var msg proto.AgentMessage
751 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
752 c.server.logError(r, "Failed to decode request", "error", err)
753 jsonError(w, http.StatusBadRequest, "failed to decode request")
754 return
755 }
756
757 if err := c.backend.SendMessage(r.Context(), id, msg); err != nil {
758 c.handleError(w, r, err)
759 return
760 }
761 w.WriteHeader(http.StatusOK)
762}
763
764// handlePostWorkspaceAgentInit initializes the agent for a workspace.
765//
766// @Summary Initialize agent
767// @Tags agent
768// @Param id path string true "Workspace ID"
769// @Success 200
770// @Failure 404 {object} proto.Error
771// @Failure 500 {object} proto.Error
772// @Router /workspaces/{id}/agent/init [post]
773func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
774 id := r.PathValue("id")
775 if err := c.backend.InitAgent(r.Context(), id); err != nil {
776 c.handleError(w, r, err)
777 return
778 }
779 w.WriteHeader(http.StatusOK)
780}
781
782// handlePostWorkspaceAgentUpdate updates the agent for a workspace.
783//
784// @Summary Update agent
785// @Tags agent
786// @Param id path string true "Workspace ID"
787// @Success 200
788// @Failure 404 {object} proto.Error
789// @Failure 500 {object} proto.Error
790// @Router /workspaces/{id}/agent/update [post]
791func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
792 id := r.PathValue("id")
793 if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
794 c.handleError(w, r, err)
795 return
796 }
797 w.WriteHeader(http.StatusOK)
798}
799
800// handleGetWorkspaceAgentSession returns a specific agent session.
801//
802// @Summary Get agent session
803// @Tags agent
804// @Produce json
805// @Param id path string true "Workspace ID"
806// @Param sid path string true "Session ID"
807// @Success 200 {object} proto.AgentSession
808// @Failure 404 {object} proto.Error
809// @Failure 500 {object} proto.Error
810// @Router /workspaces/{id}/agent/sessions/{sid} [get]
811func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
812 id := r.PathValue("id")
813 sid := r.PathValue("sid")
814 agentSession, err := c.backend.GetAgentSession(r.Context(), id, sid)
815 if err != nil {
816 c.handleError(w, r, err)
817 return
818 }
819 jsonEncode(w, agentSession)
820}
821
822// handlePostWorkspaceAgentSessionCancel cancels a running agent session.
823//
824// @Summary Cancel agent session
825// @Tags agent
826// @Param id path string true "Workspace ID"
827// @Param sid path string true "Session ID"
828// @Success 200
829// @Failure 404 {object} proto.Error
830// @Failure 500 {object} proto.Error
831// @Router /workspaces/{id}/agent/sessions/{sid}/cancel [post]
832func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
833 id := r.PathValue("id")
834 sid := r.PathValue("sid")
835 if err := c.backend.CancelSession(id, sid); err != nil {
836 c.handleError(w, r, err)
837 return
838 }
839 w.WriteHeader(http.StatusOK)
840}
841
842// handleGetWorkspaceAgentSessionPromptQueued returns whether a queued prompt exists.
843//
844// @Summary Get queued prompt status
845// @Tags agent
846// @Produce json
847// @Param id path string true "Workspace ID"
848// @Param sid path string true "Session ID"
849// @Success 200 {object} object
850// @Failure 404 {object} proto.Error
851// @Failure 500 {object} proto.Error
852// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/queued [get]
853func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
854 id := r.PathValue("id")
855 sid := r.PathValue("sid")
856 queued, err := c.backend.QueuedPrompts(id, sid)
857 if err != nil {
858 c.handleError(w, r, err)
859 return
860 }
861 jsonEncode(w, queued)
862}
863
864// handlePostWorkspaceAgentSessionPromptClear clears the prompt queue for a session.
865//
866// @Summary Clear prompt queue
867// @Tags agent
868// @Param id path string true "Workspace ID"
869// @Param sid path string true "Session ID"
870// @Success 200
871// @Failure 404 {object} proto.Error
872// @Failure 500 {object} proto.Error
873// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/clear [post]
874func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
875 id := r.PathValue("id")
876 sid := r.PathValue("sid")
877 if err := c.backend.ClearQueue(id, sid); err != nil {
878 c.handleError(w, r, err)
879 return
880 }
881 w.WriteHeader(http.StatusOK)
882}
883
884// handlePostWorkspaceAgentSessionSummarize summarizes a session.
885//
886// @Summary Summarize session
887// @Tags agent
888// @Param id path string true "Workspace ID"
889// @Param sid path string true "Session ID"
890// @Success 200
891// @Failure 404 {object} proto.Error
892// @Failure 500 {object} proto.Error
893// @Router /workspaces/{id}/agent/sessions/{sid}/summarize [post]
894func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
895 id := r.PathValue("id")
896 sid := r.PathValue("sid")
897 if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
898 c.handleError(w, r, err)
899 return
900 }
901 w.WriteHeader(http.StatusOK)
902}
903
904// handleGetWorkspaceAgentSessionPromptList returns the list of queued prompts.
905//
906// @Summary List queued prompts
907// @Tags agent
908// @Produce json
909// @Param id path string true "Workspace ID"
910// @Param sid path string true "Session ID"
911// @Success 200 {array} string
912// @Failure 404 {object} proto.Error
913// @Failure 500 {object} proto.Error
914// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/list [get]
915func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
916 id := r.PathValue("id")
917 sid := r.PathValue("sid")
918 prompts, err := c.backend.QueuedPromptsList(id, sid)
919 if err != nil {
920 c.handleError(w, r, err)
921 return
922 }
923 jsonEncode(w, prompts)
924}
925
926// handleGetWorkspaceAgentDefaultSmallModel returns the default small model for a provider.
927//
928// @Summary Get default small model
929// @Tags agent
930// @Produce json
931// @Param id path string true "Workspace ID"
932// @Param provider_id query string false "Provider ID"
933// @Success 200 {object} object
934// @Failure 404 {object} proto.Error
935// @Failure 500 {object} proto.Error
936// @Router /workspaces/{id}/agent/default-small-model [get]
937func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseWriter, r *http.Request) {
938 id := r.PathValue("id")
939 providerID := r.URL.Query().Get("provider_id")
940 model, err := c.backend.GetDefaultSmallModel(id, providerID)
941 if err != nil {
942 c.handleError(w, r, err)
943 return
944 }
945 jsonEncode(w, model)
946}
947
948// handlePostWorkspacePermissionsGrant grants a permission request.
949//
950// @Summary Grant permission
951// @Tags permissions
952// @Accept json
953// @Param id path string true "Workspace ID"
954// @Param request body proto.PermissionGrant true "Permission grant"
955// @Success 200 {object} proto.PermissionGrantResponse
956// @Failure 400 {object} proto.Error
957// @Failure 404 {object} proto.Error
958// @Failure 500 {object} proto.Error
959// @Router /workspaces/{id}/permissions/grant [post]
960func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
961 id := r.PathValue("id")
962
963 var req proto.PermissionGrant
964 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
965 c.server.logError(r, "Failed to decode request", "error", err)
966 jsonError(w, http.StatusBadRequest, "failed to decode request")
967 return
968 }
969
970 resolved, err := c.backend.GrantPermission(id, req)
971 if err != nil {
972 c.handleError(w, r, err)
973 return
974 }
975 jsonEncode(w, proto.PermissionGrantResponse{Resolved: resolved})
976}
977
978// handlePostWorkspacePermissionsSkip sets whether to skip permission prompts.
979//
980// @Summary Set skip permissions
981// @Tags permissions
982// @Accept json
983// @Param id path string true "Workspace ID"
984// @Param request body proto.PermissionSkipRequest true "Permission skip request"
985// @Success 200
986// @Failure 400 {object} proto.Error
987// @Failure 404 {object} proto.Error
988// @Failure 500 {object} proto.Error
989// @Router /workspaces/{id}/permissions/skip [post]
990func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
991 id := r.PathValue("id")
992
993 var req proto.PermissionSkipRequest
994 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
995 c.server.logError(r, "Failed to decode request", "error", err)
996 jsonError(w, http.StatusBadRequest, "failed to decode request")
997 return
998 }
999
1000 if err := c.backend.SetPermissionsSkip(id, req.Skip); err != nil {
1001 c.handleError(w, r, err)
1002 return
1003 }
1004}
1005
1006// handleGetWorkspacePermissionsSkip returns whether permission prompts are skipped.
1007//
1008// @Summary Get skip permissions status
1009// @Tags permissions
1010// @Produce json
1011// @Param id path string true "Workspace ID"
1012// @Success 200 {object} proto.PermissionSkipRequest
1013// @Failure 404 {object} proto.Error
1014// @Failure 500 {object} proto.Error
1015// @Router /workspaces/{id}/permissions/skip [get]
1016func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
1017 id := r.PathValue("id")
1018 skip, err := c.backend.GetPermissionsSkip(id)
1019 if err != nil {
1020 c.handleError(w, r, err)
1021 return
1022 }
1023 jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
1024}
1025
1026// handleError maps backend errors to HTTP status codes and writes the
1027// JSON error response.
1028func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err error) {
1029 status := http.StatusInternalServerError
1030 switch {
1031 case errors.Is(err, backend.ErrWorkspaceNotFound):
1032 status = http.StatusNotFound
1033 case errors.Is(err, backend.ErrLSPClientNotFound):
1034 status = http.StatusNotFound
1035 case errors.Is(err, backend.ErrAgentNotInitialized):
1036 status = http.StatusBadRequest
1037 case errors.Is(err, backend.ErrPathRequired):
1038 status = http.StatusBadRequest
1039 case errors.Is(err, backend.ErrInvalidPermissionAction):
1040 status = http.StatusBadRequest
1041 case errors.Is(err, backend.ErrUnknownCommand):
1042 status = http.StatusBadRequest
1043 case errors.Is(err, backend.ErrInvalidClientID):
1044 status = http.StatusBadRequest
1045 case errors.Is(err, backend.ErrClientNotAttached):
1046 status = http.StatusNotFound
1047 }
1048 c.server.logError(r, err.Error())
1049 jsonError(w, status, err.Error())
1050}
1051
1052func jsonEncode(w http.ResponseWriter, v any) {
1053 w.Header().Set("Content-Type", "application/json")
1054 _ = json.NewEncoder(w).Encode(v)
1055}
1056
1057func jsonError(w http.ResponseWriter, status int, message string) {
1058 w.Header().Set("Content-Type", "application/json")
1059 w.WriteHeader(status)
1060 _ = json.NewEncoder(w).Encode(proto.Error{Message: message})
1061}