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// handleDeleteWorkspaces deletes a workspace.
155//
156// @Summary Delete workspace
157// @Tags workspaces
158// @Param id path string true "Workspace ID"
159// @Success 200
160// @Failure 404 {object} proto.Error
161// @Router /workspaces/{id} [delete]
162func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
163 id := r.PathValue("id")
164 clientID, ok := c.requireClientID(w, r)
165 if !ok {
166 return
167 }
168 if err := c.backend.DeleteWorkspace(id, clientID); err != nil {
169 c.handleError(w, r, err)
170 return
171 }
172}
173
174// handleGetWorkspaceConfig returns workspace configuration.
175//
176// @Summary Get workspace config
177// @Tags workspaces
178// @Produce json
179// @Param id path string true "Workspace ID"
180// @Success 200 {object} object
181// @Failure 404 {object} proto.Error
182// @Failure 500 {object} proto.Error
183// @Router /workspaces/{id}/config [get]
184func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
185 id := r.PathValue("id")
186 cfg, err := c.backend.GetWorkspaceConfig(id)
187 if err != nil {
188 c.handleError(w, r, err)
189 return
190 }
191 jsonEncode(w, cfg)
192}
193
194// handleGetWorkspaceProviders lists available providers for a workspace.
195//
196// @Summary Get workspace providers
197// @Tags workspaces
198// @Produce json
199// @Param id path string true "Workspace ID"
200// @Success 200 {object} object
201// @Failure 404 {object} proto.Error
202// @Failure 500 {object} proto.Error
203// @Router /workspaces/{id}/providers [get]
204func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
205 id := r.PathValue("id")
206 providers, err := c.backend.GetWorkspaceProviders(id)
207 if err != nil {
208 c.handleError(w, r, err)
209 return
210 }
211 jsonEncode(w, providers)
212}
213
214// handleGetWorkspaceEvents streams workspace events as Server-Sent Events.
215//
216// @Summary Stream workspace events (SSE)
217// @Tags workspaces
218// @Produce text/event-stream
219// @Param id path string true "Workspace ID"
220// @Success 200
221// @Failure 404 {object} proto.Error
222// @Failure 500 {object} proto.Error
223// @Router /workspaces/{id}/events [get]
224func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
225 flusher := http.NewResponseController(w)
226 id := r.PathValue("id")
227 clientID, ok := c.requireClientID(w, r)
228 if !ok {
229 return
230 }
231 if err := c.backend.AttachClient(id, clientID); err != nil {
232 c.handleError(w, r, err)
233 return
234 }
235 defer c.backend.DetachClient(id, clientID)
236 events, err := c.backend.SubscribeEvents(r.Context(), id)
237 if err != nil {
238 c.handleError(w, r, err)
239 return
240 }
241
242 w.Header().Set("Content-Type", "text/event-stream")
243 w.Header().Set("Cache-Control", "no-cache")
244 w.Header().Set("Connection", "keep-alive")
245
246 for {
247 select {
248 case <-r.Context().Done():
249 c.server.logDebug(r, "Stopping event stream")
250 return
251 case ev, ok := <-events:
252 if !ok {
253 return
254 }
255 wrapped := wrapEvent(ev.Payload)
256 if wrapped == nil {
257 continue
258 }
259 data, err := json.Marshal(wrapped)
260 if err != nil {
261 c.server.logError(r, "Failed to marshal event", "error", err)
262 continue
263 }
264
265 fmt.Fprintf(w, "data: %s\n\n", data)
266 flusher.Flush()
267 }
268 }
269}
270
271// handleGetWorkspaceLSPs lists LSP clients for a workspace.
272//
273// @Summary List LSP clients
274// @Tags lsp
275// @Produce json
276// @Param id path string true "Workspace ID"
277// @Success 200 {object} map[string]proto.LSPClientInfo
278// @Failure 404 {object} proto.Error
279// @Failure 500 {object} proto.Error
280// @Router /workspaces/{id}/lsps [get]
281func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
282 id := r.PathValue("id")
283 states, err := c.backend.GetLSPStates(id)
284 if err != nil {
285 c.handleError(w, r, err)
286 return
287 }
288 result := make(map[string]proto.LSPClientInfo, len(states))
289 for k, v := range states {
290 result[k] = proto.LSPClientInfo{
291 Name: v.Name,
292 State: v.State,
293 Error: v.Error,
294 DiagnosticCount: v.DiagnosticCount,
295 ConnectedAt: v.ConnectedAt,
296 }
297 }
298 jsonEncode(w, result)
299}
300
301// handleGetWorkspaceLSPDiagnostics returns diagnostics for an LSP client.
302//
303// @Summary Get LSP diagnostics
304// @Tags lsp
305// @Produce json
306// @Param id path string true "Workspace ID"
307// @Param lsp path string true "LSP client name"
308// @Success 200 {object} object
309// @Failure 404 {object} proto.Error
310// @Failure 500 {object} proto.Error
311// @Router /workspaces/{id}/lsps/{lsp}/diagnostics [get]
312func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
313 id := r.PathValue("id")
314 lspName := r.PathValue("lsp")
315 diagnostics, err := c.backend.GetLSPDiagnostics(id, lspName)
316 if err != nil {
317 c.handleError(w, r, err)
318 return
319 }
320 jsonEncode(w, diagnostics)
321}
322
323// handleGetWorkspaceSessions lists sessions for a workspace.
324//
325// @Summary List sessions
326// @Tags sessions
327// @Produce json
328// @Param id path string true "Workspace ID"
329// @Success 200 {array} proto.Session
330// @Failure 404 {object} proto.Error
331// @Failure 500 {object} proto.Error
332// @Router /workspaces/{id}/sessions [get]
333func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
334 id := r.PathValue("id")
335 sessions, err := c.backend.ListSessions(r.Context(), id)
336 if err != nil {
337 c.handleError(w, r, err)
338 return
339 }
340 result := make([]proto.Session, len(sessions))
341 for i, s := range sessions {
342 result[i] = sessionToProto(s)
343 }
344 jsonEncode(w, result)
345}
346
347// handlePostWorkspaceSessions creates a new session in a workspace.
348//
349// @Summary Create session
350// @Tags sessions
351// @Accept json
352// @Produce json
353// @Param id path string true "Workspace ID"
354// @Param request body proto.Session true "Session creation params (title)"
355// @Success 200 {object} proto.Session
356// @Failure 400 {object} proto.Error
357// @Failure 404 {object} proto.Error
358// @Failure 500 {object} proto.Error
359// @Router /workspaces/{id}/sessions [post]
360func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
361 id := r.PathValue("id")
362
363 var args session.Session
364 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
365 c.server.logError(r, "Failed to decode request", "error", err)
366 jsonError(w, http.StatusBadRequest, "failed to decode request")
367 return
368 }
369
370 sess, err := c.backend.CreateSession(r.Context(), id, args.Title)
371 if err != nil {
372 c.handleError(w, r, err)
373 return
374 }
375 jsonEncode(w, sessionToProto(sess))
376}
377
378// handleGetWorkspaceSession returns a single session.
379//
380// @Summary Get session
381// @Tags sessions
382// @Produce json
383// @Param id path string true "Workspace ID"
384// @Param sid path string true "Session ID"
385// @Success 200 {object} proto.Session
386// @Failure 404 {object} proto.Error
387// @Failure 500 {object} proto.Error
388// @Router /workspaces/{id}/sessions/{sid} [get]
389func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
390 id := r.PathValue("id")
391 sid := r.PathValue("sid")
392 sess, err := c.backend.GetSession(r.Context(), id, sid)
393 if err != nil {
394 c.handleError(w, r, err)
395 return
396 }
397 jsonEncode(w, sessionToProto(sess))
398}
399
400// handleGetWorkspaceSessionHistory returns the history for a session.
401//
402// @Summary Get session history
403// @Tags sessions
404// @Produce json
405// @Param id path string true "Workspace ID"
406// @Param sid path string true "Session ID"
407// @Success 200 {array} proto.File
408// @Failure 404 {object} proto.Error
409// @Failure 500 {object} proto.Error
410// @Router /workspaces/{id}/sessions/{sid}/history [get]
411func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
412 id := r.PathValue("id")
413 sid := r.PathValue("sid")
414 history, err := c.backend.ListSessionHistory(r.Context(), id, sid)
415 if err != nil {
416 c.handleError(w, r, err)
417 return
418 }
419 jsonEncode(w, history)
420}
421
422// handleGetWorkspaceSessionMessages returns all messages for a session.
423//
424// @Summary Get session messages
425// @Tags sessions
426// @Produce json
427// @Param id path string true "Workspace ID"
428// @Param sid path string true "Session ID"
429// @Success 200 {array} proto.Message
430// @Failure 404 {object} proto.Error
431// @Failure 500 {object} proto.Error
432// @Router /workspaces/{id}/sessions/{sid}/messages [get]
433func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
434 id := r.PathValue("id")
435 sid := r.PathValue("sid")
436 messages, err := c.backend.ListSessionMessages(r.Context(), id, sid)
437 if err != nil {
438 c.handleError(w, r, err)
439 return
440 }
441 jsonEncode(w, messagesToProto(messages))
442}
443
444// handlePutWorkspaceSession updates a session.
445//
446// @Summary Update session
447// @Tags sessions
448// @Accept json
449// @Produce json
450// @Param id path string true "Workspace ID"
451// @Param sid path string true "Session ID"
452// @Param request body proto.Session true "Updated session"
453// @Success 200 {object} proto.Session
454// @Failure 400 {object} proto.Error
455// @Failure 404 {object} proto.Error
456// @Failure 500 {object} proto.Error
457// @Router /workspaces/{id}/sessions/{sid} [put]
458func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http.Request) {
459 id := r.PathValue("id")
460
461 var sess session.Session
462 if err := json.NewDecoder(r.Body).Decode(&sess); err != nil {
463 c.server.logError(r, "Failed to decode request", "error", err)
464 jsonError(w, http.StatusBadRequest, "failed to decode request")
465 return
466 }
467
468 saved, err := c.backend.SaveSession(r.Context(), id, sess)
469 if err != nil {
470 c.handleError(w, r, err)
471 return
472 }
473 jsonEncode(w, sessionToProto(saved))
474}
475
476// handleDeleteWorkspaceSession deletes a session.
477//
478// @Summary Delete session
479// @Tags sessions
480// @Param id path string true "Workspace ID"
481// @Param sid path string true "Session ID"
482// @Success 200
483// @Failure 404 {object} proto.Error
484// @Failure 500 {object} proto.Error
485// @Router /workspaces/{id}/sessions/{sid} [delete]
486func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *http.Request) {
487 id := r.PathValue("id")
488 sid := r.PathValue("sid")
489 if err := c.backend.DeleteSession(r.Context(), id, sid); err != nil {
490 c.handleError(w, r, err)
491 return
492 }
493 w.WriteHeader(http.StatusOK)
494}
495
496// handleGetWorkspaceSessionUserMessages returns user messages for a session.
497//
498// @Summary Get user messages for session
499// @Tags sessions
500// @Produce json
501// @Param id path string true "Workspace ID"
502// @Param sid path string true "Session ID"
503// @Success 200 {array} proto.Message
504// @Failure 404 {object} proto.Error
505// @Failure 500 {object} proto.Error
506// @Router /workspaces/{id}/sessions/{sid}/messages/user [get]
507func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWriter, r *http.Request) {
508 id := r.PathValue("id")
509 sid := r.PathValue("sid")
510 messages, err := c.backend.ListUserMessages(r.Context(), id, sid)
511 if err != nil {
512 c.handleError(w, r, err)
513 return
514 }
515 jsonEncode(w, messagesToProto(messages))
516}
517
518// handleGetWorkspaceAllUserMessages returns all user messages across sessions.
519//
520// @Summary Get all user messages for workspace
521// @Tags workspaces
522// @Produce json
523// @Param id path string true "Workspace ID"
524// @Success 200 {array} proto.Message
525// @Failure 404 {object} proto.Error
526// @Failure 500 {object} proto.Error
527// @Router /workspaces/{id}/messages/user [get]
528func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter, r *http.Request) {
529 id := r.PathValue("id")
530 messages, err := c.backend.ListAllUserMessages(r.Context(), id)
531 if err != nil {
532 c.handleError(w, r, err)
533 return
534 }
535 jsonEncode(w, messagesToProto(messages))
536}
537
538// handleGetWorkspaceSessionFileTrackerFiles lists files read in a session.
539//
540// @Summary List tracked files for session
541// @Tags filetracker
542// @Produce json
543// @Param id path string true "Workspace ID"
544// @Param sid path string true "Session ID"
545// @Success 200 {array} string
546// @Failure 404 {object} proto.Error
547// @Failure 500 {object} proto.Error
548// @Router /workspaces/{id}/sessions/{sid}/filetracker/files [get]
549func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.ResponseWriter, r *http.Request) {
550 id := r.PathValue("id")
551 sid := r.PathValue("sid")
552 files, err := c.backend.FileTrackerListReadFiles(r.Context(), id, sid)
553 if err != nil {
554 c.handleError(w, r, err)
555 return
556 }
557 jsonEncode(w, files)
558}
559
560// handlePostWorkspaceFileTrackerRead records a file read event.
561//
562// @Summary Record file read
563// @Tags filetracker
564// @Accept json
565// @Param id path string true "Workspace ID"
566// @Param request body proto.FileTrackerReadRequest true "File tracker read request"
567// @Success 200
568// @Failure 400 {object} proto.Error
569// @Failure 404 {object} proto.Error
570// @Failure 500 {object} proto.Error
571// @Router /workspaces/{id}/filetracker/read [post]
572func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter, r *http.Request) {
573 id := r.PathValue("id")
574
575 var req proto.FileTrackerReadRequest
576 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
577 c.server.logError(r, "Failed to decode request", "error", err)
578 jsonError(w, http.StatusBadRequest, "failed to decode request")
579 return
580 }
581
582 if err := c.backend.FileTrackerRecordRead(r.Context(), id, req.SessionID, req.Path); err != nil {
583 c.handleError(w, r, err)
584 return
585 }
586 w.WriteHeader(http.StatusOK)
587}
588
589// handleGetWorkspaceFileTrackerLastRead returns the last read time for a file.
590//
591// @Summary Get last read time for file
592// @Tags filetracker
593// @Produce json
594// @Param id path string true "Workspace ID"
595// @Param session_id query string false "Session ID"
596// @Param path query string true "File path"
597// @Success 200 {object} object
598// @Failure 404 {object} proto.Error
599// @Failure 500 {object} proto.Error
600// @Router /workspaces/{id}/filetracker/lastread [get]
601func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWriter, r *http.Request) {
602 id := r.PathValue("id")
603 sid := r.URL.Query().Get("session_id")
604 path := r.URL.Query().Get("path")
605
606 t, err := c.backend.FileTrackerLastReadTime(r.Context(), id, sid, path)
607 if err != nil {
608 c.handleError(w, r, err)
609 return
610 }
611 jsonEncode(w, t)
612}
613
614// handlePostWorkspaceLSPStart starts an LSP server for a path.
615//
616// @Summary Start LSP server
617// @Tags lsp
618// @Accept json
619// @Param id path string true "Workspace ID"
620// @Param request body proto.LSPStartRequest true "LSP start 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}/lsps/start [post]
626func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *http.Request) {
627 id := r.PathValue("id")
628
629 var req proto.LSPStartRequest
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.LSPStart(r.Context(), id, req.Path); err != nil {
637 c.handleError(w, r, err)
638 return
639 }
640 w.WriteHeader(http.StatusOK)
641}
642
643// handlePostWorkspaceLSPStopAll stops all LSP servers.
644//
645// @Summary Stop all LSP servers
646// @Tags lsp
647// @Param id path string true "Workspace ID"
648// @Success 200
649// @Failure 404 {object} proto.Error
650// @Failure 500 {object} proto.Error
651// @Router /workspaces/{id}/lsps/stop [post]
652func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *http.Request) {
653 id := r.PathValue("id")
654 if err := c.backend.LSPStopAll(r.Context(), id); err != nil {
655 c.handleError(w, r, err)
656 return
657 }
658 w.WriteHeader(http.StatusOK)
659}
660
661// handleGetWorkspaceAgent returns agent info for a workspace.
662//
663// @Summary Get agent info
664// @Tags agent
665// @Produce json
666// @Param id path string true "Workspace ID"
667// @Success 200 {object} proto.AgentInfo
668// @Failure 404 {object} proto.Error
669// @Failure 500 {object} proto.Error
670// @Router /workspaces/{id}/agent [get]
671func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
672 id := r.PathValue("id")
673 info, err := c.backend.GetAgentInfo(id)
674 if err != nil {
675 c.handleError(w, r, err)
676 return
677 }
678 jsonEncode(w, info)
679}
680
681// handlePostWorkspaceAgent sends a message to the agent.
682//
683// @Summary Send message to agent
684// @Tags agent
685// @Accept json
686// @Param id path string true "Workspace ID"
687// @Param request body proto.AgentMessage true "Agent message"
688// @Success 200
689// @Failure 400 {object} proto.Error
690// @Failure 404 {object} proto.Error
691// @Failure 500 {object} proto.Error
692// @Router /workspaces/{id}/agent [post]
693func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
694 id := r.PathValue("id")
695
696 var msg proto.AgentMessage
697 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
698 c.server.logError(r, "Failed to decode request", "error", err)
699 jsonError(w, http.StatusBadRequest, "failed to decode request")
700 return
701 }
702
703 if err := c.backend.SendMessage(r.Context(), id, msg); err != nil {
704 c.handleError(w, r, err)
705 return
706 }
707 w.WriteHeader(http.StatusOK)
708}
709
710// handlePostWorkspaceAgentInit initializes the agent for a workspace.
711//
712// @Summary Initialize agent
713// @Tags agent
714// @Param id path string true "Workspace ID"
715// @Success 200
716// @Failure 404 {object} proto.Error
717// @Failure 500 {object} proto.Error
718// @Router /workspaces/{id}/agent/init [post]
719func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
720 id := r.PathValue("id")
721 if err := c.backend.InitAgent(r.Context(), id); err != nil {
722 c.handleError(w, r, err)
723 return
724 }
725 w.WriteHeader(http.StatusOK)
726}
727
728// handlePostWorkspaceAgentUpdate updates the agent for a workspace.
729//
730// @Summary Update agent
731// @Tags agent
732// @Param id path string true "Workspace ID"
733// @Success 200
734// @Failure 404 {object} proto.Error
735// @Failure 500 {object} proto.Error
736// @Router /workspaces/{id}/agent/update [post]
737func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
738 id := r.PathValue("id")
739 if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
740 c.handleError(w, r, err)
741 return
742 }
743 w.WriteHeader(http.StatusOK)
744}
745
746// handleGetWorkspaceAgentSession returns a specific agent session.
747//
748// @Summary Get agent session
749// @Tags agent
750// @Produce json
751// @Param id path string true "Workspace ID"
752// @Param sid path string true "Session ID"
753// @Success 200 {object} proto.AgentSession
754// @Failure 404 {object} proto.Error
755// @Failure 500 {object} proto.Error
756// @Router /workspaces/{id}/agent/sessions/{sid} [get]
757func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
758 id := r.PathValue("id")
759 sid := r.PathValue("sid")
760 agentSession, err := c.backend.GetAgentSession(r.Context(), id, sid)
761 if err != nil {
762 c.handleError(w, r, err)
763 return
764 }
765 jsonEncode(w, agentSession)
766}
767
768// handlePostWorkspaceAgentSessionCancel cancels a running agent session.
769//
770// @Summary Cancel agent session
771// @Tags agent
772// @Param id path string true "Workspace ID"
773// @Param sid path string true "Session ID"
774// @Success 200
775// @Failure 404 {object} proto.Error
776// @Failure 500 {object} proto.Error
777// @Router /workspaces/{id}/agent/sessions/{sid}/cancel [post]
778func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
779 id := r.PathValue("id")
780 sid := r.PathValue("sid")
781 if err := c.backend.CancelSession(id, sid); err != nil {
782 c.handleError(w, r, err)
783 return
784 }
785 w.WriteHeader(http.StatusOK)
786}
787
788// handleGetWorkspaceAgentSessionPromptQueued returns whether a queued prompt exists.
789//
790// @Summary Get queued prompt status
791// @Tags agent
792// @Produce json
793// @Param id path string true "Workspace ID"
794// @Param sid path string true "Session ID"
795// @Success 200 {object} object
796// @Failure 404 {object} proto.Error
797// @Failure 500 {object} proto.Error
798// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/queued [get]
799func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
800 id := r.PathValue("id")
801 sid := r.PathValue("sid")
802 queued, err := c.backend.QueuedPrompts(id, sid)
803 if err != nil {
804 c.handleError(w, r, err)
805 return
806 }
807 jsonEncode(w, queued)
808}
809
810// handlePostWorkspaceAgentSessionPromptClear clears the prompt queue for a session.
811//
812// @Summary Clear prompt queue
813// @Tags agent
814// @Param id path string true "Workspace ID"
815// @Param sid path string true "Session ID"
816// @Success 200
817// @Failure 404 {object} proto.Error
818// @Failure 500 {object} proto.Error
819// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/clear [post]
820func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
821 id := r.PathValue("id")
822 sid := r.PathValue("sid")
823 if err := c.backend.ClearQueue(id, sid); err != nil {
824 c.handleError(w, r, err)
825 return
826 }
827 w.WriteHeader(http.StatusOK)
828}
829
830// handlePostWorkspaceAgentSessionSummarize summarizes a session.
831//
832// @Summary Summarize session
833// @Tags agent
834// @Param id path string true "Workspace ID"
835// @Param sid path string true "Session ID"
836// @Success 200
837// @Failure 404 {object} proto.Error
838// @Failure 500 {object} proto.Error
839// @Router /workspaces/{id}/agent/sessions/{sid}/summarize [post]
840func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
841 id := r.PathValue("id")
842 sid := r.PathValue("sid")
843 if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
844 c.handleError(w, r, err)
845 return
846 }
847 w.WriteHeader(http.StatusOK)
848}
849
850// handleGetWorkspaceAgentSessionPromptList returns the list of queued prompts.
851//
852// @Summary List queued prompts
853// @Tags agent
854// @Produce json
855// @Param id path string true "Workspace ID"
856// @Param sid path string true "Session ID"
857// @Success 200 {array} string
858// @Failure 404 {object} proto.Error
859// @Failure 500 {object} proto.Error
860// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/list [get]
861func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
862 id := r.PathValue("id")
863 sid := r.PathValue("sid")
864 prompts, err := c.backend.QueuedPromptsList(id, sid)
865 if err != nil {
866 c.handleError(w, r, err)
867 return
868 }
869 jsonEncode(w, prompts)
870}
871
872// handleGetWorkspaceAgentDefaultSmallModel returns the default small model for a provider.
873//
874// @Summary Get default small model
875// @Tags agent
876// @Produce json
877// @Param id path string true "Workspace ID"
878// @Param provider_id query string false "Provider ID"
879// @Success 200 {object} object
880// @Failure 404 {object} proto.Error
881// @Failure 500 {object} proto.Error
882// @Router /workspaces/{id}/agent/default-small-model [get]
883func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseWriter, r *http.Request) {
884 id := r.PathValue("id")
885 providerID := r.URL.Query().Get("provider_id")
886 model, err := c.backend.GetDefaultSmallModel(id, providerID)
887 if err != nil {
888 c.handleError(w, r, err)
889 return
890 }
891 jsonEncode(w, model)
892}
893
894// handlePostWorkspacePermissionsGrant grants a permission request.
895//
896// @Summary Grant permission
897// @Tags permissions
898// @Accept json
899// @Param id path string true "Workspace ID"
900// @Param request body proto.PermissionGrant true "Permission grant"
901// @Success 200
902// @Failure 400 {object} proto.Error
903// @Failure 404 {object} proto.Error
904// @Failure 500 {object} proto.Error
905// @Router /workspaces/{id}/permissions/grant [post]
906func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
907 id := r.PathValue("id")
908
909 var req proto.PermissionGrant
910 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
911 c.server.logError(r, "Failed to decode request", "error", err)
912 jsonError(w, http.StatusBadRequest, "failed to decode request")
913 return
914 }
915
916 if err := c.backend.GrantPermission(id, req); err != nil {
917 c.handleError(w, r, err)
918 return
919 }
920 w.WriteHeader(http.StatusOK)
921}
922
923// handlePostWorkspacePermissionsSkip sets whether to skip permission prompts.
924//
925// @Summary Set skip permissions
926// @Tags permissions
927// @Accept json
928// @Param id path string true "Workspace ID"
929// @Param request body proto.PermissionSkipRequest true "Permission skip request"
930// @Success 200
931// @Failure 400 {object} proto.Error
932// @Failure 404 {object} proto.Error
933// @Failure 500 {object} proto.Error
934// @Router /workspaces/{id}/permissions/skip [post]
935func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
936 id := r.PathValue("id")
937
938 var req proto.PermissionSkipRequest
939 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
940 c.server.logError(r, "Failed to decode request", "error", err)
941 jsonError(w, http.StatusBadRequest, "failed to decode request")
942 return
943 }
944
945 if err := c.backend.SetPermissionsSkip(id, req.Skip); err != nil {
946 c.handleError(w, r, err)
947 return
948 }
949}
950
951// handleGetWorkspacePermissionsSkip returns whether permission prompts are skipped.
952//
953// @Summary Get skip permissions status
954// @Tags permissions
955// @Produce json
956// @Param id path string true "Workspace ID"
957// @Success 200 {object} proto.PermissionSkipRequest
958// @Failure 404 {object} proto.Error
959// @Failure 500 {object} proto.Error
960// @Router /workspaces/{id}/permissions/skip [get]
961func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
962 id := r.PathValue("id")
963 skip, err := c.backend.GetPermissionsSkip(id)
964 if err != nil {
965 c.handleError(w, r, err)
966 return
967 }
968 jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
969}
970
971// handleError maps backend errors to HTTP status codes and writes the
972// JSON error response.
973func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err error) {
974 status := http.StatusInternalServerError
975 switch {
976 case errors.Is(err, backend.ErrWorkspaceNotFound):
977 status = http.StatusNotFound
978 case errors.Is(err, backend.ErrLSPClientNotFound):
979 status = http.StatusNotFound
980 case errors.Is(err, backend.ErrAgentNotInitialized):
981 status = http.StatusBadRequest
982 case errors.Is(err, backend.ErrPathRequired):
983 status = http.StatusBadRequest
984 case errors.Is(err, backend.ErrInvalidPermissionAction):
985 status = http.StatusBadRequest
986 case errors.Is(err, backend.ErrUnknownCommand):
987 status = http.StatusBadRequest
988 case errors.Is(err, backend.ErrInvalidClientID):
989 status = http.StatusBadRequest
990 }
991 c.server.logError(r, err.Error())
992 jsonError(w, status, err.Error())
993}
994
995func jsonEncode(w http.ResponseWriter, v any) {
996 w.Header().Set("Content-Type", "application/json")
997 _ = json.NewEncoder(w).Encode(v)
998}
999
1000func jsonError(w http.ResponseWriter, status int, message string) {
1001 w.Header().Set("Content-Type", "application/json")
1002 w.WriteHeader(status)
1003 _ = json.NewEncoder(w).Encode(proto.Error{Message: message})
1004}