fix(sesions): extract title from reasoning content, when necessary

Christian Rocha created

💘 Generated with Crush

Assisted-by: GLM-4.7 via Crush <crush@charm.land>

Change summary

internal/agent/agent.go | 32 ++++++++++++++++++++++++++++++--
1 file changed, 30 insertions(+), 2 deletions(-)

Detailed changes

internal/agent/agent.go 🔗

@@ -766,6 +766,8 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
 		)
 	}
 
+	var reasoningContent strings.Builder
+
 	streamCall := fantasy.AgentStreamCall{
 		Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", userPrompt),
 		PrepareStep: func(callCtx context.Context, opts fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
@@ -777,6 +779,11 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
 			}
 			return callCtx, prepared, nil
 		},
+		OnReasoningDelta: func(id string, text string) error {
+			// Also capture reasoning for title fallback.
+			reasoningContent.WriteString(text)
+			return nil
+		},
 	}
 
 	// Use the small model to generate the title.
@@ -822,9 +829,23 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
 	title = strings.ReplaceAll(resp.Response.Content.Text(), "\n", " ")
 
 	// Remove thinking tags if present.
-	title = thinkTagRegex.ReplaceAllString(title, "")
+	title = removeThinkingTags(title)
+
+	// If title is empty, try reasoning content (models may put the title in
+	// reasoning).
+	if title == "" && reasoningContent.Len() > 0 {
+		reasoningTitle := strings.ReplaceAll(reasoningContent.String(), "\n", " ")
+		reasoningTitle = removeThinkingTags(reasoningTitle)
+		// Extract last sentence or reasonable length from reasoning, if
+		// present.
+		if len(reasoningTitle) > 0 {
+			if sentences := strings.Split(reasoningTitle, "."); len(sentences) > 1 {
+				reasoningTitle = strings.TrimSpace(sentences[len(sentences)-1])
+			}
+		}
+		title = reasoningTitle
+	}
 
-	title = strings.TrimSpace(title)
 	if title == "" {
 		slog.Warn("empty title; using fallback")
 		title = defaultSessionName
@@ -1135,3 +1156,10 @@ func buildSummaryPrompt(todos []session.Todo) string {
 	}
 	return sb.String()
 }
+
+// removeThinkingTags removes <think>...</think> tags from the given string.
+// Used to clean up generated session titles.
+func removeThinkingTags(s string) string {
+	s = thinkTagRegex.ReplaceAllString(s, "")
+	return strings.TrimSpace(s)
+}