diff --git a/server/handlers.go b/server/handlers.go index 528bda07bda562656533256d515481ccbfc9d7ae..03bbd0d89db6b01fe4e606af0550f3796fd719e5 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -782,7 +782,8 @@ type ContinueConversationRequest struct { } // handleContinueConversation handles POST /api/conversations/continue -// Creates a new conversation with a summary of the source conversation as the initial message +// Creates a new conversation with a summary of the source conversation as the initial user message, +// but does NOT start the agent. The user can then add additional instructions before sending. func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -826,7 +827,7 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque } summary := buildConversationSummary(sourceSlug, messages) - // Get LLM service for the requested model + // Determine model to use modelID := req.Model if modelID == "" && sourceConv.Model != nil { modelID = *sourceConv.Model @@ -835,13 +836,6 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque modelID = "qwen3-coder-fireworks" } - llmService, err := s.llmManager.GetService(modelID) - if err != nil { - s.logger.Error("Unsupported model requested", "model", modelID, "error", err) - http.Error(w, fmt.Sprintf("Unsupported model: %s", modelID), http.StatusBadRequest) - return - } - // Create new conversation with cwd from request or source conversation var cwdPtr *string if req.Cwd != "" { @@ -863,19 +857,8 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque Conversation: conversation, }) - // Get or create conversation manager - manager, err := s.getOrCreateConversationManager(ctx, conversationID) - if errors.Is(err, errConversationModelMismatch) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if err != nil { - s.logger.Error("Failed to get conversation manager", "conversationID", conversationID, "error", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Create user message with the summary + // Create and record the user message with the summary, but do NOT start the agent loop. + // This allows the user to see the summary and add additional instructions before sending. userMessage := llm.Message{ Role: llm.MessageRoleUser, Content: []llm.Content{ @@ -883,36 +866,29 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque }, } - firstMessage, err := manager.AcceptUserMessage(ctx, llmService, modelID, userMessage) - if errors.Is(err, errConversationModelMismatch) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if err != nil { - s.logger.Error("Failed to accept user message", "conversationID", conversationID, "error", err) + if err := s.recordMessage(ctx, conversationID, userMessage, llm.Usage{}); err != nil { + s.logger.Error("Failed to record summary message", "conversationID", conversationID, "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - // Generate slug for the new conversation - if firstMessage { - ctxNoCancel := context.WithoutCancel(ctx) - go func() { - slugCtx, cancel := context.WithTimeout(ctxNoCancel, 15*time.Second) - defer cancel() - _, err := slug.GenerateSlug(slugCtx, s.llmManager, s.db, s.logger, conversationID, summary, modelID) - if err != nil { - s.logger.Warn("Failed to generate slug for conversation", "conversationID", conversationID, "error", err) - } else { - go s.notifySubscribers(ctxNoCancel, conversationID) - } - }() - } + // Generate slug for the new conversation in background + ctxNoCancel := context.WithoutCancel(ctx) + go func() { + slugCtx, cancel := context.WithTimeout(ctxNoCancel, 15*time.Second) + defer cancel() + _, err := slug.GenerateSlug(slugCtx, s.llmManager, s.db, s.logger, conversationID, summary, modelID) + if err != nil { + s.logger.Warn("Failed to generate slug for conversation", "conversationID", conversationID, "error", err) + } else { + go s.notifySubscribers(ctxNoCancel, conversationID) + } + }() w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "accepted", + "status": "created", "conversation_id": conversationID, }) } diff --git a/test/server_test.go b/test/server_test.go index 1e27af8e3be76d97966af573145d474afc331335..223f555df507cedecd8602abe8da89b685272f0c 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -1455,5 +1455,73 @@ func TestContinueConversation(t *testing.T) { t.Error("Summary should contain tool name") } + // Verify that the agent was NOT started - there should only be a user message, + // no agent response. The user should be able to add instructions before sending. + var hasAgentMessage bool + for _, msg := range messages { + if msg.Type == string(db.MessageTypeAgent) { + hasAgentMessage = true + break + } + } + if hasAgentMessage { + t.Error("Expected no agent message - the agent should NOT be started automatically") + } + + // Verify the status is "created" not "accepted" (since agent wasn't started) + if status, ok := result["status"].(string); !ok || status != "created" { + t.Errorf("Expected status 'created', got %v", result["status"]) + } + t.Logf("Successfully continued conversation from %s to %s", sourceConv.ConversationID, newConversationID) + + // Now test that sending a follow-up message works correctly. + // The agent should receive both the summary message AND the new message. + followUpReq := map[string]string{ + "message": "Please focus on the bash commands.", + "model": "predictable", + } + followUpBody, _ := json.Marshal(followUpReq) + + followUpResp, err := http.Post(testServer.URL+"/api/conversation/"+newConversationID+"/chat", "application/json", bytes.NewBuffer(followUpBody)) + if err != nil { + t.Fatalf("Failed to send follow-up message: %v", err) + } + defer followUpResp.Body.Close() + + if followUpResp.StatusCode != http.StatusAccepted { + bodyBytes, _ := io.ReadAll(followUpResp.Body) + t.Fatalf("Expected status 202 for follow-up, got %d: %s", followUpResp.StatusCode, string(bodyBytes)) + } + + // Wait briefly for the agent to process + time.Sleep(200 * time.Millisecond) + + // Verify we now have messages: summary (user), follow-up (user), and agent response + updatedMessages, err := database.ListMessages(ctx, newConversationID) + if err != nil { + t.Fatalf("Failed to list updated messages: %v", err) + } + + // Count message types + userCount := 0 + agentCount := 0 + for _, msg := range updatedMessages { + switch msg.Type { + case string(db.MessageTypeUser): + userCount++ + case string(db.MessageTypeAgent): + agentCount++ + } + } + + // Should have 2 user messages (summary + follow-up) and at least 1 agent response + if userCount != 2 { + t.Errorf("Expected 2 user messages, got %d", userCount) + } + if agentCount < 1 { + t.Errorf("Expected at least 1 agent message after follow-up, got %d", agentCount) + } + + t.Logf("Follow-up message processed: %d user messages, %d agent messages", userCount, agentCount) }