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