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