proto.go

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