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