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