@@ -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,
})
}
@@ -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)
}