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