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