Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ export const globalSettingsSchema = z.object({
customSupportPrompts: customSupportPromptsSchema.optional(),
enhancementApiConfigId: z.string().optional(),
includeTaskHistoryInEnhance: z.boolean().optional(),
autoSummarizeLongTitles: z.boolean().optional(),
titleSummarizationThreshold: z.number().min(50).optional(),
historyPreviewCollapsed: z.boolean().optional(),
reasoningBlockCollapsed: z.boolean().optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
Expand Down Expand Up @@ -318,6 +320,9 @@ export const EVALS_SETTINGS: RooCodeSettings = {

mode: "code", // "architect",

autoSummarizeLongTitles: true,
titleSummarizationThreshold: 150,

customModes: [],
}

Expand Down
63 changes: 63 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import type { ClineMessage } from "@roo-code/types"
import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { TitleSummarizer } from "./titleSummarizer"

/**
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
Expand Down Expand Up @@ -2570,9 +2571,71 @@ export class ClineProvider
`[createTask] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
)

// Trigger asynchronous title summarization for long task messages
const autoSummarize = this.contextProxy.getValue("autoSummarizeLongTitles") ?? true
const threshold = this.contextProxy.getValue("titleSummarizationThreshold") ?? 150

if (autoSummarize && text && text.length > threshold) {
this.summarizeTaskTitle(task.taskId, text).catch((error: unknown) => {
this.log(`Failed to summarize task title: ${error instanceof Error ? error.message : String(error)}`)
})
}

return task
}

/**
* Summarize long task titles asynchronously and update the task history
* @param taskId - The ID of the task to update
* @param originalText - The original task text to summarize
*/
private async summarizeTaskTitle(taskId: string, originalText: string): Promise<void> {
try {
// Get the current state to check for API configuration
const state = await this.getState()

// Get the configurable threshold
const threshold = this.contextProxy.getValue("titleSummarizationThreshold") ?? 150

// Try to summarize the title
const result = await TitleSummarizer.summarizeTitle({
text: originalText,
apiConfiguration: state.apiConfiguration,
customSupportPrompts: state.customSupportPrompts,
enhancementApiConfigId: state.enhancementApiConfigId,
listApiConfigMeta: state.listApiConfigMeta,
providerSettingsManager: this.providerSettingsManager,
maxLength: threshold,
})

// If summarization succeeded and produced a shorter title, update the task history
if (result.success && result.summarizedTitle && result.summarizedTitle.length < originalText.length) {
const history = this.getGlobalState("taskHistory") ?? []
const taskHistoryItem = history.find((item) => item.id === taskId)

if (taskHistoryItem) {
// Update the task field with the summarized version
taskHistoryItem.task = result.summarizedTitle

// Update the history
await this.updateTaskHistory(taskHistoryItem)

// Capture telemetry
TitleSummarizer.captureTelemetry(taskId, originalText.length, result.summarizedTitle.length)

this.log(
`Task title summarized from ${originalText.length} to ${result.summarizedTitle.length} characters`,
)
}
} else if (!result.success && result.error) {
this.log(`Title summarization failed: ${result.error}`)
}
} catch (error) {
// Silently fail - title summarization is not critical
this.log(`Title summarization error: ${error instanceof Error ? error.message : String(error)}`)
}
}

public async cancelTask(): Promise<void> {
const task = this.getCurrentTask()

Expand Down
213 changes: 213 additions & 0 deletions src/core/webview/__tests__/titleSummarizer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { TitleSummarizer } from "../titleSummarizer"
import type { ProviderSettings, ProviderSettingsEntry } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { singleCompletionHandler } from "../../../utils/single-completion-handler"

// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))

// Mock singleCompletionHandler
vi.mock("../../../utils/single-completion-handler", () => ({
singleCompletionHandler: vi.fn(),
}))

describe("TitleSummarizer", () => {
const mockApiConfiguration: ProviderSettings = {
apiProvider: "anthropic",
apiKey: "test-key",
apiModelId: "claude-3-opus-20240229",
}

const mockListApiConfigMeta: ProviderSettingsEntry[] = [
{
id: "default",
name: "Default",
apiProvider: "anthropic",
},
{
id: "enhancement",
name: "Enhancement",
apiProvider: "openai",
},
]

const mockProviderSettingsManager = {
getProfile: vi.fn().mockResolvedValue({
id: "enhancement",
name: "Enhancement",
apiProvider: "openai",
openAiApiKey: "test-openai-key",
openAiModelId: "gpt-4",
}),
} as any // Mock the ProviderSettingsManager type

beforeEach(() => {
vi.clearAllMocks()
// Set default mock behavior
vi.mocked(singleCompletionHandler).mockResolvedValue("Short concise title")
})

afterEach(() => {
vi.restoreAllMocks()
})

describe("summarizeTitle", () => {
it("should successfully summarize a long title", async () => {
const longText =
"I need help implementing a comprehensive user authentication system with OAuth2 support for Google, Facebook, and GitHub providers, including secure session management, password reset functionality, email verification, two-factor authentication, and proper error handling with rate limiting to prevent brute force attacks"

const result = await TitleSummarizer.summarizeTitle({
text: longText,
apiConfiguration: mockApiConfiguration,
maxLength: 150,
})

expect(result.success).toBe(true)
expect(result.summarizedTitle).toBe("Short concise title")
expect(result.summarizedTitle!.length).toBeLessThan(longText.length)
})

it("should return original text if it's already short", async () => {
const shortText = "Fix bug in login"

const result = await TitleSummarizer.summarizeTitle({
text: shortText,
apiConfiguration: mockApiConfiguration,
maxLength: 150,
})

expect(result.success).toBe(true)
// Text is already shorter than max length, so it returns as-is
expect(result.summarizedTitle).toBe(shortText)
})

it("should use enhancement API configuration when provided", async () => {
const longText =
"This is a very long title that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"

const result = await TitleSummarizer.summarizeTitle({
text: longText,
apiConfiguration: mockApiConfiguration,
enhancementApiConfigId: "enhancement",
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
maxLength: 150,
})

// The function will call providerSettingsManager.getProfile if all conditions are met
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "enhancement" })
expect(result.success).toBe(true)
expect(result.summarizedTitle).toBe("Short concise title")
})

it("should handle API errors gracefully", async () => {
const longText =
"This is a very long text that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
vi.mocked(singleCompletionHandler).mockRejectedValueOnce(new Error("API Error"))

const result = await TitleSummarizer.summarizeTitle({
text: longText,
apiConfiguration: mockApiConfiguration,
maxLength: 150,
})

expect(result.success).toBe(false)
expect(result.error).toBe("API Error")
expect(result.summarizedTitle).toBe(longText)
})

it("should handle missing API configuration", async () => {
const result = await TitleSummarizer.summarizeTitle({
text: "Some text",
apiConfiguration: undefined as any,
maxLength: 150,
})

expect(result.success).toBe(false)
expect(result.error).toBe("No API configuration available")
expect(result.summarizedTitle).toBe("Some text")
Comment on lines +126 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has incorrect expectations. When text is "Some text" (9 characters) and maxLength is 150, the early return on line 46 of titleSummarizer.ts will execute, returning { success: true, summarizedTitle: text } without ever validating the API configuration.

The test expects success: false and an error about missing API configuration, but this validation never occurs due to the short text length.

To properly test missing API configuration handling, use text longer than maxLength to bypass the early return:

const longText = "a".repeat(151) // Exceeds maxLength of 150
const result = await TitleSummarizer.summarizeTitle({
  text: longText,
  apiConfiguration: undefined as any,
  maxLength: 150,
})

expect(result.success).toBe(false)
expect(result.error).toBe("No valid API configuration provided")

Note: The error message should also be "No valid API configuration provided" (from singleCompletionHandler.ts:14) not "No API configuration available".

})

it("should respect custom maxLength parameter", async () => {
const shortText = "Short text"

const result = await TitleSummarizer.summarizeTitle({
text: shortText,
apiConfiguration: mockApiConfiguration,
maxLength: 100,
})

expect(result.success).toBe(true)
// Text is already shorter than maxLength, returns as-is
expect(result.summarizedTitle).toBe(shortText)
})

it("should use custom support prompts when provided", async () => {
const customPrompts = {
SUMMARIZE_TITLE: "Custom summarization prompt: {{userInput}} (max 150 chars)",
}

// Short text doesn't need summarization
const result = await TitleSummarizer.summarizeTitle({
text: "Text to summarize",
apiConfiguration: mockApiConfiguration,
customSupportPrompts: customPrompts,
maxLength: 150,
})

expect(result.success).toBe(true)
expect(result.summarizedTitle).toBe("Text to summarize")
Comment on lines +152 to +166
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't actually verify custom support prompts functionality. The text "Text to summarize" (18 characters) is shorter than maxLength (150), triggering the early return on line 46 that bypasses prompt creation entirely.

To properly test custom support prompts:

const customPrompts = {
  SUMMARIZE_TITLE: "Custom summarization prompt: {{userInput}} (max 150 chars)",
}
const longText = "a".repeat(151)

const result = await TitleSummarizer.summarizeTitle({
  text: longText,
  apiConfiguration: mockApiConfiguration,
  customSupportPrompts: customPrompts,
  maxLength: 150,
})

// Verify the custom prompt was actually used
expect(singleCompletionHandler).toHaveBeenCalledWith(
  mockApiConfiguration,
  expect.stringContaining("Custom summarization prompt")
)

})

it("should handle empty response from API", async () => {
const longText =
"This is a very long text that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
vi.mocked(singleCompletionHandler).mockResolvedValueOnce("")

const result = await TitleSummarizer.summarizeTitle({
text: longText,
apiConfiguration: mockApiConfiguration,
maxLength: 150,
})

expect(result.success).toBe(false)
expect(result.error).toBe("Received empty summarized title")
expect(result.summarizedTitle).toBe(longText)
})

it("should trim whitespace from summarized title", async () => {
const longText =
"This is a very long text that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
vi.mocked(singleCompletionHandler).mockResolvedValueOnce(" Trimmed title \n")

const result = await TitleSummarizer.summarizeTitle({
text: longText,
apiConfiguration: mockApiConfiguration,
maxLength: 150,
})

expect(result.success).toBe(true)
expect(result.summarizedTitle).toBe("Trimmed title")
})
})

describe("captureTelemetry", () => {
it("should not capture telemetry events (currently disabled)", () => {
const taskId = "test-task-123"
const originalLength = 250
const summarizedLength = 100

TitleSummarizer.captureTelemetry(taskId, originalLength, summarizedLength)

// Since telemetry is commented out in the implementation, it should not be called
expect(TelemetryService.instance.captureEvent).not.toHaveBeenCalled()
})
})
})
Loading
Loading