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