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