diff --git a/internal/client/proto.go b/internal/client/proto.go index 0bbbb02b3a9f82bfd859a66a7c61f3f5a7c210e0..7d6b7d95e0b88e6b8ced10046d19b0d3045e66e8 100644 --- a/internal/client/proto.go +++ b/internal/client/proto.go @@ -82,6 +82,7 @@ func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, er } if rsp.StatusCode != http.StatusOK { + rsp.Body.Close() return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode) } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b224277edbade7398b0f8a1449ed053c80ec8408..ad9face77bd1f30853c4896ff63bf374438a0009 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -281,7 +281,29 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot Env: os.Environ(), }) if err != nil { - return nil, nil, fmt.Errorf("failed to create workspace: %v", err) + // The server socket may exist before the HTTP handler is ready. + // Retry a few times with a short backoff. + for range 5 { + select { + case <-ctx.Done(): + return nil, nil, ctx.Err() + case <-time.After(200 * time.Millisecond): + } + ws, err = c.CreateWorkspace(ctx, proto.Workspace{ + Path: cwd, + DataDir: dataDir, + Debug: debug, + YOLO: yolo, + Version: version.Version, + Env: os.Environ(), + }) + if err == nil { + break + } + } + if err != nil { + return nil, nil, fmt.Errorf("failed to create workspace: %v", err) + } } if shouldEnableMetrics(ws.Config) { diff --git a/internal/config/resolve.go b/internal/config/resolve.go index 3ef3522b09e504d3c57105e8bbe393b0f7c38b2b..b9e7753386bb8c95b877b99172897ea4fdb0a045 100644 --- a/internal/config/resolve.go +++ b/internal/config/resolve.go @@ -14,6 +14,20 @@ type VariableResolver interface { ResolveValue(value string) (string, error) } +// identityResolver is a no-op resolver that returns values unchanged. +// Used in client mode where variable resolution is handled server-side. +type identityResolver struct{} + +func (identityResolver) ResolveValue(value string) (string, error) { + return value, nil +} + +// IdentityResolver returns a VariableResolver that passes values through +// unchanged. +func IdentityResolver() VariableResolver { + return identityResolver{} +} + type Shell interface { Exec(ctx context.Context, command string) (stdout, stderr string, err error) } diff --git a/internal/proto/mcp.go b/internal/proto/mcp.go index e7491e79b203a781dedbb1f0a9e5bbf2eda2766c..cc9dcbe78bd89c7176fc67b9332798b844a4ddac 100644 --- a/internal/proto/mcp.go +++ b/internal/proto/mcp.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "time" ) // MCPState represents the current state of an MCP client. @@ -125,13 +126,13 @@ func (e *MCPEvent) UnmarshalJSON(data []byte) error { // MCPClientInfo is the wire-format representation of an MCP client's // state, suitable for JSON transport between server and client. type MCPClientInfo struct { - Name string `json:"name"` - State MCPState `json:"state"` - Error error `json:"error,omitempty"` - ToolCount int `json:"tool_count,omitempty"` - PromptCount int `json:"prompt_count,omitempty"` - ResourceCount int `json:"resource_count,omitempty"` - ConnectedAt int64 `json:"connected_at,omitempty"` + Name string `json:"name"` + State MCPState `json:"state"` + Error error `json:"error,omitempty"` + ToolCount int `json:"tool_count,omitempty"` + PromptCount int `json:"prompt_count,omitempty"` + ResourceCount int `json:"resource_count,omitempty"` + ConnectedAt time.Time `json:"connected_at"` } // MarshalJSON implements the [json.Marshaler] interface. diff --git a/internal/server/config.go b/internal/server/config.go index b449ac0260207223b3a82a405df16259859dd66f..d516505fb22dad6b10d3fc9f912eb3e82cb1b3a3 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -253,7 +253,7 @@ func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *htt ToolCount: v.Counts.Tools, PromptCount: v.Counts.Prompts, ResourceCount: v.Counts.Resources, - ConnectedAt: v.ConnectedAt.Unix(), + ConnectedAt: v.ConnectedAt, } } jsonEncode(w, result) diff --git a/internal/server/proto.go b/internal/server/proto.go index 966a08f04e1b36834593067703e6fd1e862535c7..588086960eca2c39f38428f376de7f0b0b83cf0e 100644 --- a/internal/server/proto.go +++ b/internal/server/proto.go @@ -366,8 +366,6 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - w.Header().Set("Accept", "application/json") - var msg proto.AgentMessage if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { c.server.logError(r, "Failed to decode request", "error", err) @@ -379,6 +377,7 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R c.handleError(w, r, err) return } + w.WriteHeader(http.StatusOK) } func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) { @@ -387,6 +386,7 @@ func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *ht c.handleError(w, r, err) return } + w.WriteHeader(http.StatusOK) } func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) { @@ -395,6 +395,7 @@ func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r * c.handleError(w, r, err) return } + w.WriteHeader(http.StatusOK) } func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) { @@ -439,13 +440,14 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.Respons w.WriteHeader(http.StatusOK) } -func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) { +func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil { c.handleError(w, r, err) return } + w.WriteHeader(http.StatusOK) } func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) { @@ -484,6 +486,7 @@ func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter c.handleError(w, r, err) return } + w.WriteHeader(http.StatusOK) } func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) { diff --git a/internal/server/server.go b/internal/server/server.go index 5fb05015e495e6a80e9ea4762903ba79d5639d61..ec84bef777c1fe6c3c6e1e4daabee4ba841ee259 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -143,7 +143,7 @@ func NewServer(cfg *config.ConfigStore, network, address string) *Server { mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued) mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/list", c.handleGetWorkspaceAgentSessionPromptList) mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/prompts/clear", c.handlePostWorkspaceAgentSessionPromptClear) - mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handleGetWorkspaceAgentSessionSummarize) + mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handlePostWorkspaceAgentSessionSummarize) mux.HandleFunc("GET /v1/workspaces/{id}/agent/default-small-model", c.handleGetWorkspaceAgentDefaultSmallModel) mux.HandleFunc("POST /v1/workspaces/{id}/config/set", c.handlePostWorkspaceConfigSet) mux.HandleFunc("POST /v1/workspaces/{id}/config/remove", c.handlePostWorkspaceConfigRemove) diff --git a/internal/workspace/client_workspace.go b/internal/workspace/client_workspace.go index d61b1124d2df499aca2487640e292b7affb20ee3..cc68d3d9bf97fe2e4a23d26d2be1489a0ddb4c99 100644 --- a/internal/workspace/client_workspace.go +++ b/internal/workspace/client_workspace.go @@ -350,8 +350,7 @@ func (w *ClientWorkspace) WorkingDir() string { } func (w *ClientWorkspace) Resolver() config.VariableResolver { - // In client mode, variable resolution is handled server-side. - return nil + return config.IdentityResolver() } // -- Config mutations -- @@ -447,7 +446,7 @@ func (w *ClientWorkspace) MCPGetStates() map[string]mcp.ClientInfo { Prompts: v.PromptCount, Resources: v.ResourceCount, }, - ConnectedAt: time.Unix(v.ConnectedAt, 0), + ConnectedAt: v.ConnectedAt, } } return result @@ -590,7 +589,8 @@ func translateEvent(ev any) tea.Msg { }, } default: - return ev.(tea.Msg) + slog.Warn("Unknown event type in translateEvent", "type", fmt.Sprintf("%T", ev)) + return nil } }