proto.go 31 KB

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