@@ -1,28 +1,19 @@
1package server
1package server
5 "encoding/json"
4 "encoding/json"
13− "github.com/charmbracelet/crush/internal/app"
9+ "github.com/charmbracelet/crush/internal/backend"
14− "github.com/charmbracelet/crush/internal/config"
15− "github.com/charmbracelet/crush/internal/db"
16− "github.com/charmbracelet/crush/internal/permission"
17 "github.com/charmbracelet/crush/internal/proto"
10 "github.com/charmbracelet/crush/internal/proto"
18 "github.com/charmbracelet/crush/internal/session"
11 "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"
24type controllerV1 struct {
14type controllerV1 struct {
15+ backend *backend.Backend
25− *Server
16+ server *Server
28func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
19func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
@@ -30,641 +21,360 @@ func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
32func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
23func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
33− jsonEncode(w, proto.VersionInfo{
24+ jsonEncode(w, c.backend.VersionInfo())
34− Version: version.Version,
35− Commit: version.Commit,
36− GoVersion: runtime.Version(),
37− Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
41func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
27func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
42 var req proto.ServerControl
28 var req proto.ServerControl
43 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
29 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)
45 jsonError(w, http.StatusBadRequest, "failed to decode request")
31 jsonError(w, http.StatusBadRequest, "failed to decode request")
49 switch req.Command {
35 switch req.Command {
50 case "shutdown":
36 case "shutdown":
51− go func() {
37+ c.backend.Shutdown()
52− slog.Info("Shutting down server...")
53− if err := c.Shutdown(context.Background()); err != nil {
54− slog.Error("Failed to shutdown server", "error", err)
58− c.logError(r, "Unknown command", "command", req.Command)
39+ c.server.logError(r, "Unknown command", "command", req.Command)
59 jsonError(w, http.StatusBadRequest, "unknown command")
40 jsonError(w, http.StatusBadRequest, "unknown command")
64func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
45func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
65− jsonEncode(w, c.cfg)
46+ jsonEncode(w, c.backend.Config())
68func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
49func (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{
74− YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
75− DataDir: ws.cfg.Options.DataDirectory,
76− Debug: ws.cfg.Options.Debug,
80− jsonEncode(w, workspaces)
50+ jsonEncode(w, c.backend.ListWorkspaces())
83−func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
53+func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
84 id := r.PathValue("id")
54 id := r.PathValue("id")
85− ws, ok := c.workspaces.Get(id)
55+ ws, err := c.backend.GetWorkspaceProto(id)
86− if !ok {
56+ if err != nil {
87− c.logError(r, "Workspace not found", "id", id)
57+ c.handleError(w, r, err)
88− jsonError(w, http.StatusNotFound, "workspace not found")
92− lspName := r.PathValue("lsp")
63+func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
93− var found bool
64+ var args proto.Workspace
94− for name, client := range ws.LSPManager.Clients().Seq2() {
65+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
95− if name == lspName {
66+ c.server.logError(r, "Failed to decode request", "error", err)
96− diagnostics := client.GetDiagnostics()
67+ jsonError(w, http.StatusBadRequest, "failed to decode request")
97− jsonEncode(w, diagnostics)
68+ return
103− if !found {
71+ _, result, err := c.backend.CreateWorkspace(args)
104− c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
72+ if err != nil {
105− jsonError(w, http.StatusNotFound, "LSP client not found")
73+ c.handleError(w, r, err)
76+ jsonEncode(w, result)
109−func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
79+func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
110 id := r.PathValue("id")
80 id := r.PathValue("id")
111− _, ok := c.workspaces.Get(id)
81+ c.backend.DeleteWorkspace(id)
113− c.logError(r, "Workspace not found", "id", id)
114− jsonError(w, http.StatusNotFound, "workspace not found")
118− lspClients := app.GetLSPStates()
119− jsonEncode(w, lspClients)
122−func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
84+func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
123 id := r.PathValue("id")
85 id := r.PathValue("id")
124− ws, ok := c.workspaces.Get(id)
86+ cfg, err := c.backend.GetWorkspaceConfig(id)
125− if !ok {
87+ if err != nil {
126− c.logError(r, "Workspace not found", "id", id)
88+ c.handleError(w, r, err)
127− jsonError(w, http.StatusNotFound, "workspace not found")
131− sid := r.PathValue("sid")
132− queued := ws.App.AgentCoordinator.QueuedPrompts(sid)
133− jsonEncode(w, queued)
91+ jsonEncode(w, cfg)
136−func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
94+func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
137 id := r.PathValue("id")
95 id := r.PathValue("id")
138− ws, ok := c.workspaces.Get(id)
96+ providers, err := c.backend.GetWorkspaceProviders(id)
139− if !ok {
97+ if err != nil {
140− c.logError(r, "Workspace not found", "id", id)
98+ c.handleError(w, r, err)
141− jsonError(w, http.StatusNotFound, "workspace not found")
144−
101+ jsonEncode(w, providers)
145− sid := r.PathValue("sid")
146− ws.App.AgentCoordinator.ClearQueue(sid)
147− w.WriteHeader(http.StatusOK)
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)
151 id := r.PathValue("id")
106 id := r.PathValue("id")
152− ws, ok := c.workspaces.Get(id)
107+ events, err := c.backend.SubscribeEvents(id)
153− if !ok {
108+ if err != nil {
154− c.logError(r, "Workspace not found", "id", id)
109+ c.handleError(w, r, err)
155− jsonError(w, http.StatusNotFound, "workspace not found")
159− sid := r.PathValue("sid")
113+ w.Header().Set("Content-Type", "text/event-stream")
160− if err := ws.App.AgentCoordinator.Summarize(r.Context(), sid); err != nil {
114+ w.Header().Set("Cache-Control", "no-cache")
161− c.logError(r, "Failed to summarize session", "error", err, "id", id, "sid", sid)
115+ w.Header().Set("Connection", "keep-alive")
162− jsonError(w, http.StatusInternalServerError, "failed to summarize session")
167−func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
117+ for {
168− id := r.PathValue("id")
118+ select {
169− ws, ok := c.workspaces.Get(id)
119+ case <-r.Context().Done():
120+ c.server.logDebug(r, "Stopping event stream")
122+ case ev, ok := <-events:
170− if !ok {
123+ if !ok {
171− c.logError(r, "Workspace not found", "id", id)
124+ return
172− jsonError(w, http.StatusNotFound, "workspace not found")
125+ }
173− return
126+ c.server.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
127+ data, err := json.Marshal(ev)
129+ c.server.logError(r, "Failed to marshal event", "error", err)
176− sid := r.PathValue("sid")
133+ fmt.Fprintf(w, "data: %s\n\n", data)
177− if ws.App.AgentCoordinator != nil {
134+ flusher.Flush()
178− ws.App.AgentCoordinator.Cancel(sid)
135+ }
180− w.WriteHeader(http.StatusOK)
183−func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
139+func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
184 id := r.PathValue("id")
140 id := r.PathValue("id")
185− ws, ok := c.workspaces.Get(id)
141+ states, err := c.backend.GetLSPStates(id)
187− c.logError(r, "Workspace not found", "id", id)
188− jsonError(w, http.StatusNotFound, "workspace not found")
192− sid := r.PathValue("sid")
193− se, err := ws.App.Sessions.Get(r.Context(), sid)
194 if err != nil {
142 if err != nil {
195− c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
143+ c.handleError(w, r, err)
196− jsonError(w, http.StatusInternalServerError, "failed to get session")
200− var isSessionBusy bool
201− if ws.App.AgentCoordinator != nil {
202− isSessionBusy = ws.App.AgentCoordinator.IsSessionBusy(sid)
205− jsonEncode(w, proto.AgentSession{
146+ jsonEncode(w, states)
206− Session: proto.Session{
210− IsBusy: isSessionBusy,
214−func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
149+func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
215 id := r.PathValue("id")
150 id := r.PathValue("id")
216− ws, ok := c.workspaces.Get(id)
151+ lspName := r.PathValue("lsp")
217− if !ok {
152+ diagnostics, err := c.backend.GetLSPDiagnostics(id, lspName)
218− c.logError(r, "Workspace not found", "id", id)
153+ if err != nil {
219− jsonError(w, http.StatusNotFound, "workspace not found")
154+ c.handleError(w, r, err)
223− w.Header().Set("Accept", "application/json")
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")
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")
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")
157+ jsonEncode(w, diagnostics)
245−func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
160+func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
246 id := r.PathValue("id")
161 id := r.PathValue("id")
247− ws, ok := c.workspaces.Get(id)
162+ sessions, err := c.backend.ListSessions(r.Context(), id)
248− if !ok {
163+ if err != nil {
249− c.logError(r, "Workspace not found", "id", id)
164+ c.handleError(w, r, err)
250− jsonError(w, http.StatusNotFound, "workspace not found")
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(),
262− jsonEncode(w, agentInfo)
167+ jsonEncode(w, sessions)
265−func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
170+func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
266 id := r.PathValue("id")
171 id := r.PathValue("id")
267− ws, ok := c.workspaces.Get(id)
172+
268− if !ok {
173+ var args session.Session
269− c.logError(r, "Workspace not found", "id", id)
174+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
270− jsonError(w, http.StatusNotFound, "workspace not found")
175+ c.server.logError(r, "Failed to decode request", "error", err)
176+ jsonError(w, http.StatusBadRequest, "failed to decode request")
274− if err := ws.App.UpdateAgentModel(r.Context()); err != nil {
180+ sess, err := c.backend.CreateSession(r.Context(), id, args.Title)
275− c.logError(r, "Failed to update agent model", "error", err)
181+ if err != nil {
276− jsonError(w, http.StatusInternalServerError, "failed to update agent model")
182+ c.handleError(w, r, err)
281−func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
188+func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
282 id := r.PathValue("id")
189 id := r.PathValue("id")
283− ws, ok := c.workspaces.Get(id)
190+ sid := r.PathValue("sid")
284− if !ok {
191+ sess, err := c.backend.GetSession(r.Context(), id, sid)
285− c.logError(r, "Workspace not found", "id", id)
192+ if err != nil {
286− jsonError(w, http.StatusNotFound, "workspace not found")
193+ c.handleError(w, r, err)
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")
297func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
199func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
298 id := r.PathValue("id")
200 id := r.PathValue("id")
299− ws, ok := c.workspaces.Get(id)
301− c.logError(r, "Workspace not found", "id", id)
302− jsonError(w, http.StatusNotFound, "workspace not found")
306 sid := r.PathValue("sid")
201 sid := r.PathValue("sid")
307− historyItems, err := ws.App.History.ListBySession(r.Context(), sid)
202+ history, err := c.backend.ListSessionHistory(r.Context(), id, sid)
308 if err != nil {
203 if err != nil {
309− c.logError(r, "Failed to list history", "error", err, "id", id, "sid", sid)
204+ c.handleError(w, r, err)
310− jsonError(w, http.StatusInternalServerError, "failed to list history")
314− jsonEncode(w, historyItems)
207+ jsonEncode(w, history)
317func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
210func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
318 id := r.PathValue("id")
211 id := r.PathValue("id")
319− ws, ok := c.workspaces.Get(id)
321− c.logError(r, "Workspace not found", "id", id)
322− jsonError(w, http.StatusNotFound, "workspace not found")
326 sid := r.PathValue("sid")
212 sid := r.PathValue("sid")
327− messages, err := ws.App.Messages.List(r.Context(), sid)
213+ messages, err := c.backend.ListSessionMessages(r.Context(), id, sid)
328 if err != nil {
214 if err != nil {
329− c.logError(r, "Failed to list messages", "error", err, "id", id, "sid", sid)
215+ c.handleError(w, r, err)
330− jsonError(w, http.StatusInternalServerError, "failed to list messages")
334 jsonEncode(w, messages)
218 jsonEncode(w, messages)
337−func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
221+func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
338 id := r.PathValue("id")
222 id := r.PathValue("id")
339− ws, ok := c.workspaces.Get(id)
223+ info, err := c.backend.GetAgentInfo(id)
341− c.logError(r, "Workspace not found", "id", id)
342− jsonError(w, http.StatusNotFound, "workspace not found")
346− sid := r.PathValue("sid")
347− sess, err := ws.App.Sessions.Get(r.Context(), sid)
348 if err != nil {
224 if err != nil {
349− c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
225+ c.handleError(w, r, err)
350− jsonError(w, http.StatusInternalServerError, "failed to get session")
354− jsonEncode(w, sess)
228+ jsonEncode(w, info)
357−func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
231+func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
358 id := r.PathValue("id")
232 id := r.PathValue("id")
359− ws, ok := c.workspaces.Get(id)
361− c.logError(r, "Workspace not found", "id", id)
362− jsonError(w, http.StatusNotFound, "workspace not found")
366− var args session.Session
234+ w.Header().Set("Accept", "application/json")
236+ var msg proto.AgentMessage
367− if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
237+ if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
368− c.logError(r, "Failed to decode request", "error", err)
238+ c.server.logError(r, "Failed to decode request", "error", err)
369 jsonError(w, http.StatusBadRequest, "failed to decode request")
239 jsonError(w, http.StatusBadRequest, "failed to decode request")
373− sess, err := ws.App.Sessions.Create(r.Context(), args.Title)
243+ if err := c.backend.SendMessage(r.Context(), id, msg); err != nil {
374− if err != nil {
244+ c.handleError(w, r, err)
375− c.logError(r, "Failed to create session", "error", err, "id", id)
376− jsonError(w, http.StatusInternalServerError, "failed to create session")
383−func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
249+func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
384 id := r.PathValue("id")
250 id := r.PathValue("id")
385− ws, ok := c.workspaces.Get(id)
251+ if err := c.backend.InitAgent(r.Context(), id); err != nil {
386− if !ok {
252+ c.handleError(w, r, err)
387− c.logError(r, "Workspace not found", "id", id)
388− jsonError(w, http.StatusNotFound, "workspace not found")
392− sessions, err := ws.App.Sessions.List(r.Context())
394− c.logError(r, "Failed to list sessions", "error", err)
395− jsonError(w, http.StatusInternalServerError, "failed to list sessions")
399− jsonEncode(w, sessions)
402−func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
257+func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
403 id := r.PathValue("id")
258 id := r.PathValue("id")
404− ws, ok := c.workspaces.Get(id)
259+ if err := c.backend.UpdateAgent(r.Context(), id); err != nil {
405− if !ok {
260+ c.handleError(w, r, err)
406− c.logError(r, "Workspace not found", "id", id)
407− jsonError(w, http.StatusNotFound, "workspace not found")
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")
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,
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)
437− c.logError(r, "Invalid permission action", "action", req.Action)
438− jsonError(w, http.StatusBadRequest, "invalid permission action")
443−func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
265+func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
444 id := r.PathValue("id")
266 id := r.PathValue("id")
445− ws, ok := c.workspaces.Get(id)
267+ sid := r.PathValue("sid")
446− if !ok {
268+ agentSession, err := c.backend.GetAgentSession(r.Context(), id, sid)
447− c.logError(r, "Workspace not found", "id", id)
269+ if err != nil {
448− jsonError(w, http.StatusNotFound, "workspace not found")
270+ c.handleError(w, r, err)
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")
458−
273+ jsonEncode(w, agentSession)
459− ws.App.Permissions.SetSkipRequests(req.Skip)
462−func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
276+func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
463 id := r.PathValue("id")
277 id := r.PathValue("id")
464− ws, ok := c.workspaces.Get(id)
278+ sid := r.PathValue("sid")
465− if !ok {
279+ if err := c.backend.CancelSession(id, sid); err != nil {
466− c.logError(r, "Workspace not found", "id", id)
280+ c.handleError(w, r, err)
467− jsonError(w, http.StatusNotFound, "workspace not found")
470−
283+ w.WriteHeader(http.StatusOK)
471− skip := ws.App.Permissions.SkipRequests()
472− jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
475−func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
286+func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
476 id := r.PathValue("id")
287 id := r.PathValue("id")
477− ws, ok := c.workspaces.Get(id)
288+ sid := r.PathValue("sid")
478− if !ok {
289+ queued, err := c.backend.QueuedPrompts(id, sid)
479− c.logError(r, "Workspace not found", "id", id)
290+ if err != nil {
480− jsonError(w, http.StatusNotFound, "workspace not found")
291+ c.handleError(w, r, err)
484− providers, _ := config.Providers(ws.cfg)
485− jsonEncode(w, providers)
294+ jsonEncode(w, queued)
488−func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
297+func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
489− flusher := http.NewResponseController(w)
490 id := r.PathValue("id")
298 id := r.PathValue("id")
491− ws, ok := c.workspaces.Get(id)
299+ sid := r.PathValue("sid")
492− if !ok {
300+ if err := c.backend.ClearQueue(id, sid); err != nil {
493− c.logError(r, "Workspace not found", "id", id)
301+ c.handleError(w, r, err)
494− jsonError(w, http.StatusNotFound, "workspace not found")
497−
304+ w.WriteHeader(http.StatusOK)
498− w.Header().Set("Content-Type", "text/event-stream")
499− w.Header().Set("Cache-Control", "no-cache")
500− w.Header().Set("Connection", "keep-alive")
502− events := ws.App.Events()
506− case <-r.Context().Done():
507− c.logDebug(r, "Stopping event stream")
509− case ev, ok := <-events:
513− c.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
514− data, err := json.Marshal(ev)
516− c.logError(r, "Failed to marshal event", "error", err)
520− fmt.Fprintf(w, "data: %s\n\n", data)
526−func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
307+func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
527 id := r.PathValue("id")
308 id := r.PathValue("id")
528− ws, ok := c.workspaces.Get(id)
309+ sid := r.PathValue("sid")
529− if !ok {
310+ if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
530− c.logError(r, "Workspace not found", "id", id)
311+ c.handleError(w, r, err)
531− jsonError(w, http.StatusNotFound, "workspace not found")
535− jsonEncode(w, ws.cfg)
538−func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
539− id := r.PathValue("id")
540− ws, ok := c.workspaces.Get(id)
544− c.workspaces.Del(id)
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...")
550− if err := c.Shutdown(context.Background()); err != nil {
551− slog.Error("Failed to shutdown server", "error", err)
557−func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
316+func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
558 id := r.PathValue("id")
317 id := r.PathValue("id")
559− ws, ok := c.workspaces.Get(id)
561− c.logError(r, "Workspace not found", "id", id)
562− jsonError(w, http.StatusNotFound, "workspace not found")
566− jsonEncode(w, proto.Workspace{
569− YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
570− DataDir: ws.cfg.Options.DataDirectory,
571− Debug: ws.cfg.Options.Debug,
576−func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
319+ var req proto.PermissionGrant
577− var args proto.Workspace
578− if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
320+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
579− c.logError(r, "Failed to decode request", "error", err)
321+ c.server.logError(r, "Failed to decode request", "error", err)
580 jsonError(w, http.StatusBadRequest, "failed to decode request")
322 jsonError(w, http.StatusBadRequest, "failed to decode request")
584− if args.Path == "" {
326+ if err := c.backend.GrantPermission(id, req); err != nil {
585− c.logError(r, "Path is required")
327+ c.handleError(w, r, err)
586− jsonError(w, http.StatusBadRequest, "path is required")
590− id := uuid.New().String()
591− cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
593− c.logError(r, "Failed to initialize config", "error", err)
594− jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
598− if cfg.Permissions == nil {
332+func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
599− cfg.Permissions = &config.Permissions{}
333+ id := r.PathValue("id")
601− cfg.Permissions.SkipRequests = args.YOLO
603− if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
335+ var req proto.PermissionSkipRequest
336+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
604− c.logError(r, "Failed to create data directory", "error", err)
337+ c.server.logError(r, "Failed to decode request", "error", err)
605− jsonError(w, http.StatusInternalServerError, "failed to create data directory")
338+ jsonError(w, http.StatusBadRequest, "failed to decode request")
609− conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
342+ if err := c.backend.SetPermissionsSkip(id, req.Skip); err != nil {
610− if err != nil {
343+ c.handleError(w, r, err)
611− c.logError(r, "Failed to connect to database", "error", err)
612− jsonError(w, http.StatusInternalServerError, "failed to connect to database")
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)
617 if err != nil {
351 if err != nil {
618− slog.Error("Failed to create app workspace", "error", err)
352+ c.handleError(w, r, err)
619− jsonError(w, http.StatusInternalServerError, "failed to create app workspace")
631− c.workspaces.Set(id, ws)
633− if args.Version != "" && args.Version != version.Version {
634− slog.Warn("Client/server version mismatch",
635− "client", args.Version,
636− "server", version.Version,
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,
644− jsonEncode(w, proto.Workspace{
355+ jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
647− DataDir: cfg.Options.DataDirectory,
648− Debug: cfg.Options.Debug,
649− YOLO: cfg.Permissions.SkipRequests,
655−func createDotCrushDir(dir string) error {
358+// handleError maps backend errors to HTTP status codes and writes the
656− if err := os.MkdirAll(dir, 0o700); err != nil {
359+// JSON error response.
657− return fmt.Errorf("failed to create data directory: %q %w", dir, err)
360+func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err error) {
658− }
361+ status := http.StatusInternalServerError
660− gitIgnorePath := filepath.Join(dir, ".gitignore")
363+ case errors.Is(err, backend.ErrWorkspaceNotFound):
661− if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
364+ status = http.StatusNotFound
662− if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
365+ case errors.Is(err, backend.ErrLSPClientNotFound):
663− return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
366+ status = http.StatusNotFound
664− }
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
666−
376+ c.server.logError(r, err.Error())
667− return nil
377+ jsonError(w, status, err.Error())
670func jsonEncode(w http.ResponseWriter, v any) {
380func jsonEncode(w http.ResponseWriter, v any) {