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