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