@@ -1,28 +1,19 @@
11package server
22
33import (
4− "context"
54 "encoding/json"
5+ "errors"
66 "fmt"
7− "log/slog"
87 "net/http"
9− "os"
10− "path/filepath"
11− "runtime"
12−
13− "github.com/charmbracelet/crush/internal/app"
14− "github.com/charmbracelet/crush/internal/config"
15− "github.com/charmbracelet/crush/internal/db"
16− "github.com/charmbracelet/crush/internal/permission"
8+
9+ "github.com/charmbracelet/crush/internal/backend"
1710 "github.com/charmbracelet/crush/internal/proto"
1811 "github.com/charmbracelet/crush/internal/session"
19− "github.com/charmbracelet/crush/internal/ui/util"
20− "github.com/charmbracelet/crush/internal/version"
21− "github.com/google/uuid"
2212)
2313
2414type controllerV1 struct {
25− *Server
15+ backend *backend.Backend
16+ server *Server
2617}
2718
2819func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
@@ -30,641 +21,360 @@ func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
3021}
3122
3223func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
33− jsonEncode(w, proto.VersionInfo{
34− Version: version.Version,
35− Commit: version.Commit,
36− GoVersion: runtime.Version(),
37− Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
38− })
24+ jsonEncode(w, c.backend.VersionInfo())
3925}
4026
4127func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
4228 var req proto.ServerControl
4329 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
44− c.logError(r, "Failed to decode request", "error", err)
30+ c.server.logError(r, "Failed to decode request", "error", err)
4531 jsonError(w, http.StatusBadRequest, "failed to decode request")
4632 return
4733 }
4834
4935 switch req.Command {
5036 case "shutdown":
51− go func() {
52− slog.Info("Shutting down server...")
53− if err := c.Shutdown(context.Background()); err != nil {
54− slog.Error("Failed to shutdown server", "error", err)
55− }
56− }()
37+ c.backend.Shutdown()
5738 default:
58− c.logError(r, "Unknown command", "command", req.Command)
39+ c.server.logError(r, "Unknown command", "command", req.Command)
5940 jsonError(w, http.StatusBadRequest, "unknown command")
6041 return
6142 }
6243}
6344
6445func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
65− jsonEncode(w, c.cfg)
46+ jsonEncode(w, c.backend.Config())
6647}
6748
6849func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
69− workspaces := []proto.Workspace{}
70− for _, ws := range c.workspaces.Seq2() {
71− workspaces = append(workspaces, proto.Workspace{
72− ID: ws.id,
73− Path: ws.path,
74− YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
75− DataDir: ws.cfg.Options.DataDirectory,
76− Debug: ws.cfg.Options.Debug,
77− Config: ws.cfg,
78− })
79− }
80− jsonEncode(w, workspaces)
50+ jsonEncode(w, c.backend.ListWorkspaces())
8151}
8252
83−func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
53+func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
8454 id := r.PathValue("id")
85− ws, ok := c.workspaces.Get(id)
86− if !ok {
87− c.logError(r, "Workspace not found", "id", id)
88− jsonError(w, http.StatusNotFound, "workspace not found")
55+ ws, err := c.backend.GetWorkspaceProto(id)
56+ if err != nil {
57+ c.handleError(w, r, err)
8958 return
9059 }
60+ jsonEncode(w, ws)
61+}
9162
92− lspName := r.PathValue("lsp")
93− var found bool
94− for name, client := range ws.LSPManager.Clients().Seq2() {
95− if name == lspName {
96− diagnostics := client.GetDiagnostics()
97− jsonEncode(w, diagnostics)
98− found = true
99− break
100− }
63+func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
64+ var args proto.Workspace
65+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
66+ c.server.logError(r, "Failed to decode request", "error", err)
67+ jsonError(w, http.StatusBadRequest, "failed to decode request")
68+ return
10169 }
10270
103− if !found {
104− c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
105− jsonError(w, http.StatusNotFound, "LSP client not found")
71+ _, result, err := c.backend.CreateWorkspace(args)
72+ if err != nil {
73+ c.handleError(w, r, err)
74+ return
10675 }
76+ jsonEncode(w, result)
10777}
10878
109−func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
79+func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
11080 id := r.PathValue("id")
111− _, ok := c.workspaces.Get(id)
112− if !ok {
113− c.logError(r, "Workspace not found", "id", id)
114− jsonError(w, http.StatusNotFound, "workspace not found")
115− return
116− }
117−
118− lspClients := app.GetLSPStates()
119− jsonEncode(w, lspClients)
81+ c.backend.DeleteWorkspace(id)
12082}
12183
122−func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
84+func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
12385 id := r.PathValue("id")
124− ws, ok := c.workspaces.Get(id)
125− if !ok {
126− c.logError(r, "Workspace not found", "id", id)
127− jsonError(w, http.StatusNotFound, "workspace not found")
86+ cfg, err := c.backend.GetWorkspaceConfig(id)
87+ if err != nil {
88+ c.handleError(w, r, err)
12889 return
12990 }
130−
131− sid := r.PathValue("sid")
132− queued := ws.App.AgentCoordinator.QueuedPrompts(sid)
133− jsonEncode(w, queued)
91+ jsonEncode(w, cfg)
13492}
13593
136−func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
94+func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
13795 id := r.PathValue("id")
138− ws, ok := c.workspaces.Get(id)
139− if !ok {
140− c.logError(r, "Workspace not found", "id", id)
141− jsonError(w, http.StatusNotFound, "workspace not found")
96+ providers, err := c.backend.GetWorkspaceProviders(id)
97+ if err != nil {
98+ c.handleError(w, r, err)
14299 return
143100 }
144−
145− sid := r.PathValue("sid")
146− ws.App.AgentCoordinator.ClearQueue(sid)
147− w.WriteHeader(http.StatusOK)
101+ jsonEncode(w, providers)
148102}
149103
150−func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
104+func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
105+ flusher := http.NewResponseController(w)
151106 id := r.PathValue("id")
152− ws, ok := c.workspaces.Get(id)
153− if !ok {
154− c.logError(r, "Workspace not found", "id", id)
155− jsonError(w, http.StatusNotFound, "workspace not found")
107+ events, err := c.backend.SubscribeEvents(id)
108+ if err != nil {
109+ c.handleError(w, r, err)
156110 return
157111 }
158112
159− sid := r.PathValue("sid")
160− if err := ws.App.AgentCoordinator.Summarize(r.Context(), sid); err != nil {
161− c.logError(r, "Failed to summarize session", "error", err, "id", id, "sid", sid)
162− jsonError(w, http.StatusInternalServerError, "failed to summarize session")
163− return
164− }
165−}
113+ w.Header().Set("Content-Type", "text/event-stream")
114+ w.Header().Set("Cache-Control", "no-cache")
115+ w.Header().Set("Connection", "keep-alive")
166116
167−func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
168− id := r.PathValue("id")
169− ws, ok := c.workspaces.Get(id)
170− if !ok {
171− c.logError(r, "Workspace not found", "id", id)
172− jsonError(w, http.StatusNotFound, "workspace not found")
173− return
174− }
117+ for {
118+ select {
119+ case <-r.Context().Done():
120+ c.server.logDebug(r, "Stopping event stream")
121+ return
122+ case ev, ok := <-events:
123+ if !ok {
124+ return
125+ }
126+ c.server.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
127+ data, err := json.Marshal(ev)
128+ if err != nil {
129+ c.server.logError(r, "Failed to marshal event", "error", err)
130+ continue
131+ }
175132
176− sid := r.PathValue("sid")
177− if ws.App.AgentCoordinator != nil {
178− ws.App.AgentCoordinator.Cancel(sid)
133+ fmt.Fprintf(w, "data: %s\n\n", data)
134+ flusher.Flush()
135+ }
179136 }
180− w.WriteHeader(http.StatusOK)
181137}
182138
183−func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
139+func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
184140 id := r.PathValue("id")
185− ws, ok := c.workspaces.Get(id)
186− if !ok {
187− c.logError(r, "Workspace not found", "id", id)
188− jsonError(w, http.StatusNotFound, "workspace not found")
189− return
190− }
191−
192− sid := r.PathValue("sid")
193− se, err := ws.App.Sessions.Get(r.Context(), sid)
141+ states, err := c.backend.GetLSPStates(id)
194142 if err != nil {
195− c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
196− jsonError(w, http.StatusInternalServerError, "failed to get session")
143+ c.handleError(w, r, err)
197144 return
198145 }
199−
200− var isSessionBusy bool
201− if ws.App.AgentCoordinator != nil {
202− isSessionBusy = ws.App.AgentCoordinator.IsSessionBusy(sid)
203− }
204−
205− jsonEncode(w, proto.AgentSession{
206− Session: proto.Session{
207− ID: se.ID,
208− Title: se.Title,
209− },
210− IsBusy: isSessionBusy,
211− })
146+ jsonEncode(w, states)
212147}
213148
214−func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
149+func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
215150 id := r.PathValue("id")
216− ws, ok := c.workspaces.Get(id)
217− if !ok {
218− c.logError(r, "Workspace not found", "id", id)
219− jsonError(w, http.StatusNotFound, "workspace not found")
220− return
221− }
222−
223− w.Header().Set("Accept", "application/json")
224−
225− var msg proto.AgentMessage
226− if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
227− c.logError(r, "Failed to decode request", "error", err)
228− jsonError(w, http.StatusBadRequest, "failed to decode request")
229− return
230− }
231−
232− if ws.App.AgentCoordinator == nil {
233− c.logError(r, "Agent coordinator not initialized", "id", id)
234− jsonError(w, http.StatusBadRequest, "agent coordinator not initialized")
235− return
236− }
237−
238− if _, err := ws.App.AgentCoordinator.Run(c.ctx, msg.SessionID, msg.Prompt); err != nil {
239− c.logError(r, "Failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
240− jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
151+ lspName := r.PathValue("lsp")
152+ diagnostics, err := c.backend.GetLSPDiagnostics(id, lspName)
153+ if err != nil {
154+ c.handleError(w, r, err)
241155 return
242156 }
157+ jsonEncode(w, diagnostics)
243158}
244159
245−func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
160+func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
246161 id := r.PathValue("id")
247− ws, ok := c.workspaces.Get(id)
248− if !ok {
249− c.logError(r, "Workspace not found", "id", id)
250− jsonError(w, http.StatusNotFound, "workspace not found")
162+ sessions, err := c.backend.ListSessions(r.Context(), id)
163+ if err != nil {
164+ c.handleError(w, r, err)
251165 return
252166 }
253−
254− var agentInfo proto.AgentInfo
255− if ws.App.AgentCoordinator != nil {
256− m := ws.App.AgentCoordinator.Model()
257− agentInfo = proto.AgentInfo{
258− Model: m.CatwalkCfg,
259− IsBusy: ws.App.AgentCoordinator.IsBusy(),
260− }
261− }
262− jsonEncode(w, agentInfo)
167+ jsonEncode(w, sessions)
263168}
264169
265−func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
170+func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
266171 id := r.PathValue("id")
267− ws, ok := c.workspaces.Get(id)
268− if !ok {
269− c.logError(r, "Workspace not found", "id", id)
270− jsonError(w, http.StatusNotFound, "workspace not found")
172+
173+ var args session.Session
174+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
175+ c.server.logError(r, "Failed to decode request", "error", err)
176+ jsonError(w, http.StatusBadRequest, "failed to decode request")
271177 return
272178 }
273179
274− if err := ws.App.UpdateAgentModel(r.Context()); err != nil {
275− c.logError(r, "Failed to update agent model", "error", err)
276− jsonError(w, http.StatusInternalServerError, "failed to update agent model")
180+ sess, err := c.backend.CreateSession(r.Context(), id, args.Title)
181+ if err != nil {
182+ c.handleError(w, r, err)
277183 return
278184 }
185+ jsonEncode(w, sess)
279186}
280187
281−func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
188+func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
282189 id := r.PathValue("id")
283− ws, ok := c.workspaces.Get(id)
284− if !ok {
285− c.logError(r, "Workspace not found", "id", id)
286− jsonError(w, http.StatusNotFound, "workspace not found")
287− return
288− }
289−
290− if err := ws.App.InitCoderAgent(r.Context()); err != nil {
291− c.logError(r, "Failed to initialize coder agent", "error", err)
292− jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
190+ sid := r.PathValue("sid")
191+ sess, err := c.backend.GetSession(r.Context(), id, sid)
192+ if err != nil {
193+ c.handleError(w, r, err)
293194 return
294195 }
196+ jsonEncode(w, sess)
295197}
296198
297199func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
298200 id := r.PathValue("id")
299− ws, ok := c.workspaces.Get(id)
300− if !ok {
301− c.logError(r, "Workspace not found", "id", id)
302− jsonError(w, http.StatusNotFound, "workspace not found")
303− return
304− }
305−
306201 sid := r.PathValue("sid")
307− historyItems, err := ws.App.History.ListBySession(r.Context(), sid)
202+ history, err := c.backend.ListSessionHistory(r.Context(), id, sid)
308203 if err != nil {
309− c.logError(r, "Failed to list history", "error", err, "id", id, "sid", sid)
310− jsonError(w, http.StatusInternalServerError, "failed to list history")
204+ c.handleError(w, r, err)
311205 return
312206 }
313−
314− jsonEncode(w, historyItems)
207+ jsonEncode(w, history)
315208}
316209
317210func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
318211 id := r.PathValue("id")
319− ws, ok := c.workspaces.Get(id)
320− if !ok {
321− c.logError(r, "Workspace not found", "id", id)
322− jsonError(w, http.StatusNotFound, "workspace not found")
323− return
324− }
325−
326212 sid := r.PathValue("sid")
327− messages, err := ws.App.Messages.List(r.Context(), sid)
213+ messages, err := c.backend.ListSessionMessages(r.Context(), id, sid)
328214 if err != nil {
329− c.logError(r, "Failed to list messages", "error", err, "id", id, "sid", sid)
330− jsonError(w, http.StatusInternalServerError, "failed to list messages")
215+ c.handleError(w, r, err)
331216 return
332217 }
333−
334218 jsonEncode(w, messages)
335219}
336220
337−func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
221+func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
338222 id := r.PathValue("id")
339− ws, ok := c.workspaces.Get(id)
340− if !ok {
341− c.logError(r, "Workspace not found", "id", id)
342− jsonError(w, http.StatusNotFound, "workspace not found")
343− return
344− }
345−
346− sid := r.PathValue("sid")
347− sess, err := ws.App.Sessions.Get(r.Context(), sid)
223+ info, err := c.backend.GetAgentInfo(id)
348224 if err != nil {
349− c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
350− jsonError(w, http.StatusInternalServerError, "failed to get session")
225+ c.handleError(w, r, err)
351226 return
352227 }
353−
354− jsonEncode(w, sess)
228+ jsonEncode(w, info)
355229}
356230
357−func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
231+func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
358232 id := r.PathValue("id")
359− ws, ok := c.workspaces.Get(id)
360− if !ok {
361− c.logError(r, "Workspace not found", "id", id)
362− jsonError(w, http.StatusNotFound, "workspace not found")
363− return
364− }
365233
366− var args session.Session
367− if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
368− c.logError(r, "Failed to decode request", "error", err)
234+ w.Header().Set("Accept", "application/json")
235+
236+ var msg proto.AgentMessage
237+ if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
238+ c.server.logError(r, "Failed to decode request", "error", err)
369239 jsonError(w, http.StatusBadRequest, "failed to decode request")
370240 return
371241 }
372242
373− sess, err := ws.App.Sessions.Create(r.Context(), args.Title)
374− if err != nil {
375− c.logError(r, "Failed to create session", "error", err, "id", id)
376− jsonError(w, http.StatusInternalServerError, "failed to create session")
243+ if err := c.backend.SendMessage(r.Context(), id, msg); err != nil {
244+ c.handleError(w, r, err)
377245 return
378246 }
379−
380− jsonEncode(w, sess)
381247}
382248
383−func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
249+func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
384250 id := r.PathValue("id")
385− ws, ok := c.workspaces.Get(id)
386− if !ok {
387− c.logError(r, "Workspace not found", "id", id)
388− jsonError(w, http.StatusNotFound, "workspace not found")
251+ if err := c.backend.InitAgent(r.Context(), id); err != nil {
252+ c.handleError(w, r, err)
389253 return
390254 }
391−
392− sessions, err := ws.App.Sessions.List(r.Context())
393− if err != nil {
394− c.logError(r, "Failed to list sessions", "error", err)
395− jsonError(w, http.StatusInternalServerError, "failed to list sessions")
396− return
397− }
398−
399− jsonEncode(w, sessions)
400255}
401256
402−func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
257+func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
403258 id := r.PathValue("id")
404− ws, ok := c.workspaces.Get(id)
405− if !ok {
406− c.logError(r, "Workspace not found", "id", id)
407− jsonError(w, http.StatusNotFound, "workspace not found")
408− return
409− }
410−
411− var req proto.PermissionGrant
412− if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
413− c.logError(r, "Failed to decode request", "error", err)
414− jsonError(w, http.StatusBadRequest, "failed to decode request")
415− return
416− }
417−
418− perm := permission.PermissionRequest{
419− ID: req.Permission.ID,
420− SessionID: req.Permission.SessionID,
421− ToolCallID: req.Permission.ToolCallID,
422− ToolName: req.Permission.ToolName,
423− Description: req.Permission.Description,
424− Action: req.Permission.Action,
425− Params: req.Permission.Params,
426− Path: req.Permission.Path,
427− }
428−
429− switch req.Action {
430− case proto.PermissionAllow:
431− ws.App.Permissions.Grant(perm)
432− case proto.PermissionAllowForSession:
433− ws.App.Permissions.GrantPersistent(perm)
434− case proto.PermissionDeny:
435− ws.App.Permissions.Deny(perm)
436− default:
437− c.logError(r, "Invalid permission action", "action", req.Action)
438− jsonError(w, http.StatusBadRequest, "invalid permission action")
259+ if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
260+ c.handleError(w, r, err)
439261 return
440262 }
441263}
442264
443−func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
265+func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
444266 id := r.PathValue("id")
445− ws, ok := c.workspaces.Get(id)
446− if !ok {
447− c.logError(r, "Workspace not found", "id", id)
448− jsonError(w, http.StatusNotFound, "workspace not found")
449− return
450− }
451−
452− var req proto.PermissionSkipRequest
453− if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
454− c.logError(r, "Failed to decode request", "error", err)
455− jsonError(w, http.StatusBadRequest, "failed to decode request")
267+ sid := r.PathValue("sid")
268+ agentSession, err := c.backend.GetAgentSession(r.Context(), id, sid)
269+ if err != nil {
270+ c.handleError(w, r, err)
456271 return
457272 }
458−
459− ws.App.Permissions.SetSkipRequests(req.Skip)
273+ jsonEncode(w, agentSession)
460274}
461275
462−func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
276+func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
463277 id := r.PathValue("id")
464− ws, ok := c.workspaces.Get(id)
465− if !ok {
466− c.logError(r, "Workspace not found", "id", id)
467− jsonError(w, http.StatusNotFound, "workspace not found")
278+ sid := r.PathValue("sid")
279+ if err := c.backend.CancelSession(id, sid); err != nil {
280+ c.handleError(w, r, err)
468281 return
469282 }
470−
471− skip := ws.App.Permissions.SkipRequests()
472− jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
283+ w.WriteHeader(http.StatusOK)
473284}
474285
475−func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
286+func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
476287 id := r.PathValue("id")
477− ws, ok := c.workspaces.Get(id)
478− if !ok {
479− c.logError(r, "Workspace not found", "id", id)
480− jsonError(w, http.StatusNotFound, "workspace not found")
288+ sid := r.PathValue("sid")
289+ queued, err := c.backend.QueuedPrompts(id, sid)
290+ if err != nil {
291+ c.handleError(w, r, err)
481292 return
482293 }
483−
484− providers, _ := config.Providers(ws.cfg)
485− jsonEncode(w, providers)
294+ jsonEncode(w, queued)
486295}
487296
488−func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
489− flusher := http.NewResponseController(w)
297+func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
490298 id := r.PathValue("id")
491− ws, ok := c.workspaces.Get(id)
492− if !ok {
493− c.logError(r, "Workspace not found", "id", id)
494− jsonError(w, http.StatusNotFound, "workspace not found")
299+ sid := r.PathValue("sid")
300+ if err := c.backend.ClearQueue(id, sid); err != nil {
301+ c.handleError(w, r, err)
495302 return
496303 }
497−
498− w.Header().Set("Content-Type", "text/event-stream")
499− w.Header().Set("Cache-Control", "no-cache")
500− w.Header().Set("Connection", "keep-alive")
501−
502− events := ws.App.Events()
503−
504− for {
505− select {
506− case <-r.Context().Done():
507− c.logDebug(r, "Stopping event stream")
508− return
509− case ev, ok := <-events:
510− if !ok {
511− return
512− }
513− c.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
514− data, err := json.Marshal(ev)
515− if err != nil {
516− c.logError(r, "Failed to marshal event", "error", err)
517− continue
518− }
519−
520− fmt.Fprintf(w, "data: %s\n\n", data)
521− flusher.Flush()
522− }
523− }
304+ w.WriteHeader(http.StatusOK)
524305}
525306
526−func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
307+func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
527308 id := r.PathValue("id")
528− ws, ok := c.workspaces.Get(id)
529− if !ok {
530− c.logError(r, "Workspace not found", "id", id)
531− jsonError(w, http.StatusNotFound, "workspace not found")
309+ sid := r.PathValue("sid")
310+ if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
311+ c.handleError(w, r, err)
532312 return
533313 }
534−
535− jsonEncode(w, ws.cfg)
536−}
537−
538−func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
539− id := r.PathValue("id")
540− ws, ok := c.workspaces.Get(id)
541− if ok {
542− ws.App.Shutdown()
543− }
544− c.workspaces.Del(id)
545−
546− // When the last workspace is removed, shut down the server.
547− if c.workspaces.Len() == 0 {
548− slog.Info("Last workspace removed, shutting down server...")
549− go func() {
550− if err := c.Shutdown(context.Background()); err != nil {
551− slog.Error("Failed to shutdown server", "error", err)
552− }
553− }()
554− }
555314}
556315
557−func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
316+func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
558317 id := r.PathValue("id")
559− ws, ok := c.workspaces.Get(id)
560− if !ok {
561− c.logError(r, "Workspace not found", "id", id)
562− jsonError(w, http.StatusNotFound, "workspace not found")
563− return
564− }
565−
566− jsonEncode(w, proto.Workspace{
567− ID: ws.id,
568− Path: ws.path,
569− YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
570− DataDir: ws.cfg.Options.DataDirectory,
571− Debug: ws.cfg.Options.Debug,
572− Config: ws.cfg,
573− })
574−}
575318
576−func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
577− var args proto.Workspace
578− if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
579− c.logError(r, "Failed to decode request", "error", err)
319+ var req proto.PermissionGrant
320+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
321+ c.server.logError(r, "Failed to decode request", "error", err)
580322 jsonError(w, http.StatusBadRequest, "failed to decode request")
581323 return
582324 }
583325
584− if args.Path == "" {
585− c.logError(r, "Path is required")
586− jsonError(w, http.StatusBadRequest, "path is required")
587− return
588− }
589−
590− id := uuid.New().String()
591− cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
592− if err != nil {
593− c.logError(r, "Failed to initialize config", "error", err)
594− jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
326+ if err := c.backend.GrantPermission(id, req); err != nil {
327+ c.handleError(w, r, err)
595328 return
596329 }
330+}
597331
598− if cfg.Permissions == nil {
599− cfg.Permissions = &config.Permissions{}
600− }
601− cfg.Permissions.SkipRequests = args.YOLO
332+func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
333+ id := r.PathValue("id")
602334
603− if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
604− c.logError(r, "Failed to create data directory", "error", err)
605− jsonError(w, http.StatusInternalServerError, "failed to create data directory")
335+ var req proto.PermissionSkipRequest
336+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
337+ c.server.logError(r, "Failed to decode request", "error", err)
338+ jsonError(w, http.StatusBadRequest, "failed to decode request")
606339 return
607340 }
608341
609− conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
610− if err != nil {
611− c.logError(r, "Failed to connect to database", "error", err)
612− jsonError(w, http.StatusInternalServerError, "failed to connect to database")
342+ if err := c.backend.SetPermissionsSkip(id, req.Skip); err != nil {
343+ c.handleError(w, r, err)
613344 return
614345 }
346+}
615347
616− appWorkspace, err := app.New(c.ctx, conn, cfg)
348+func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
349+ id := r.PathValue("id")
350+ skip, err := c.backend.GetPermissionsSkip(id)
617351 if err != nil {
618− slog.Error("Failed to create app workspace", "error", err)
619− jsonError(w, http.StatusInternalServerError, "failed to create app workspace")
352+ c.handleError(w, r, err)
620353 return
621354 }
622−
623− ws := &Workspace{
624− App: appWorkspace,
625− id: id,
626− path: args.Path,
627− cfg: cfg,
628− env: args.Env,
629− }
630−
631− c.workspaces.Set(id, ws)
632−
633− if args.Version != "" && args.Version != version.Version {
634− slog.Warn("Client/server version mismatch",
635− "client", args.Version,
636− "server", version.Version,
637− )
638− appWorkspace.SendEvent(util.NewWarnMsg(fmt.Sprintf(
639− "Server version %q differs from client version %q. Consider restarting the server.",
640− version.Version, args.Version,
641− )))
642− }
643−
644− jsonEncode(w, proto.Workspace{
645− ID: id,
646− Path: args.Path,
647− DataDir: cfg.Options.DataDirectory,
648− Debug: cfg.Options.Debug,
649− YOLO: cfg.Permissions.SkipRequests,
650− Config: cfg,
651− Env: args.Env,
652− })
355+ jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
653356}
654357
655−func createDotCrushDir(dir string) error {
656− if err := os.MkdirAll(dir, 0o700); err != nil {
657− return fmt.Errorf("failed to create data directory: %q %w", dir, err)
658− }
659−
660− gitIgnorePath := filepath.Join(dir, ".gitignore")
661− if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
662− if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
663− return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
664− }
665− }
666−
667− return nil
358+// handleError maps backend errors to HTTP status codes and writes the
359+// JSON error response.
360+func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err error) {
361+ status := http.StatusInternalServerError
362+ switch {
363+ case errors.Is(err, backend.ErrWorkspaceNotFound):
364+ status = http.StatusNotFound
365+ case errors.Is(err, backend.ErrLSPClientNotFound):
366+ status = http.StatusNotFound
367+ case errors.Is(err, backend.ErrAgentNotInitialized):
368+ status = http.StatusBadRequest
369+ case errors.Is(err, backend.ErrPathRequired):
370+ status = http.StatusBadRequest
371+ case errors.Is(err, backend.ErrInvalidPermissionAction):
372+ status = http.StatusBadRequest
373+ case errors.Is(err, backend.ErrUnknownCommand):
374+ status = http.StatusBadRequest
375+ }
376+ c.server.logError(r, err.Error())
377+ jsonError(w, status, err.Error())
668378}
669379
670380func jsonEncode(w http.ResponseWriter, v any) {