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