Skip to content

Commit 415e64f

Browse files
committed
First pass at Title Summarizer
1 parent 13534cc commit 415e64f

File tree

5 files changed

+420
-0
lines changed

5 files changed

+420
-0
lines changed

packages/types/src/global-settings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ export const globalSettingsSchema = z.object({
146146
customSupportPrompts: customSupportPromptsSchema.optional(),
147147
enhancementApiConfigId: z.string().optional(),
148148
includeTaskHistoryInEnhance: z.boolean().optional(),
149+
autoSummarizeLongTitles: z.boolean().optional(),
150+
titleSummarizationThreshold: z.number().min(50).optional(),
149151
historyPreviewCollapsed: z.boolean().optional(),
150152
reasoningBlockCollapsed: z.boolean().optional(),
151153
profileThresholds: z.record(z.string(), z.number()).optional(),
@@ -318,6 +320,9 @@ export const EVALS_SETTINGS: RooCodeSettings = {
318320

319321
mode: "code", // "architect",
320322

323+
autoSummarizeLongTitles: true,
324+
titleSummarizationThreshold: 150,
325+
321326
customModes: [],
322327
}
323328

src/core/webview/ClineProvider.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import type { ClineMessage } from "@roo-code/types"
9494
import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
9595
import { getNonce } from "./getNonce"
9696
import { getUri } from "./getUri"
97+
import { TitleSummarizer } from "./titleSummarizer"
9798

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

2574+
// Trigger asynchronous title summarization for long task messages
2575+
const autoSummarize = this.contextProxy.getValue("autoSummarizeLongTitles") ?? true
2576+
const threshold = this.contextProxy.getValue("titleSummarizationThreshold") ?? 150
2577+
2578+
if (autoSummarize && text && text.length > threshold) {
2579+
this.summarizeTaskTitle(task.taskId, text).catch((error: unknown) => {
2580+
this.log(`Failed to summarize task title: ${error instanceof Error ? error.message : String(error)}`)
2581+
})
2582+
}
2583+
25732584
return task
25742585
}
25752586

2587+
/**
2588+
* Summarize long task titles asynchronously and update the task history
2589+
* @param taskId - The ID of the task to update
2590+
* @param originalText - The original task text to summarize
2591+
*/
2592+
private async summarizeTaskTitle(taskId: string, originalText: string): Promise<void> {
2593+
try {
2594+
// Get the current state to check for API configuration
2595+
const state = await this.getState()
2596+
2597+
// Get the configurable threshold
2598+
const threshold = this.contextProxy.getValue("titleSummarizationThreshold") ?? 150
2599+
2600+
// Try to summarize the title
2601+
const result = await TitleSummarizer.summarizeTitle({
2602+
text: originalText,
2603+
apiConfiguration: state.apiConfiguration,
2604+
customSupportPrompts: state.customSupportPrompts,
2605+
enhancementApiConfigId: state.enhancementApiConfigId,
2606+
listApiConfigMeta: state.listApiConfigMeta,
2607+
providerSettingsManager: this.providerSettingsManager,
2608+
maxLength: threshold,
2609+
})
2610+
2611+
// If summarization succeeded and produced a shorter title, update the task history
2612+
if (result.success && result.summarizedTitle && result.summarizedTitle.length < originalText.length) {
2613+
const history = this.getGlobalState("taskHistory") ?? []
2614+
const taskHistoryItem = history.find((item) => item.id === taskId)
2615+
2616+
if (taskHistoryItem) {
2617+
// Update the task field with the summarized version
2618+
taskHistoryItem.task = result.summarizedTitle
2619+
2620+
// Update the history
2621+
await this.updateTaskHistory(taskHistoryItem)
2622+
2623+
// Capture telemetry
2624+
TitleSummarizer.captureTelemetry(taskId, originalText.length, result.summarizedTitle.length)
2625+
2626+
this.log(
2627+
`Task title summarized from ${originalText.length} to ${result.summarizedTitle.length} characters`,
2628+
)
2629+
}
2630+
} else if (!result.success && result.error) {
2631+
this.log(`Title summarization failed: ${result.error}`)
2632+
}
2633+
} catch (error) {
2634+
// Silently fail - title summarization is not critical
2635+
this.log(`Title summarization error: ${error instanceof Error ? error.message : String(error)}`)
2636+
}
2637+
}
2638+
25762639
public async cancelTask(): Promise<void> {
25772640
const task = this.getCurrentTask()
25782641

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import { TitleSummarizer } from "../titleSummarizer"
3+
import type { ProviderSettings, ProviderSettingsEntry } from "@roo-code/types"
4+
import { TelemetryService } from "@roo-code/telemetry"
5+
import { singleCompletionHandler } from "../../../utils/single-completion-handler"
6+
7+
// Mock TelemetryService
8+
vi.mock("@roo-code/telemetry", () => ({
9+
TelemetryService: {
10+
instance: {
11+
captureEvent: vi.fn(),
12+
},
13+
},
14+
}))
15+
16+
// Mock singleCompletionHandler
17+
vi.mock("../../../utils/single-completion-handler", () => ({
18+
singleCompletionHandler: vi.fn(),
19+
}))
20+
21+
describe("TitleSummarizer", () => {
22+
const mockApiConfiguration: ProviderSettings = {
23+
apiProvider: "anthropic",
24+
apiKey: "test-key",
25+
apiModelId: "claude-3-opus-20240229",
26+
}
27+
28+
const mockListApiConfigMeta: ProviderSettingsEntry[] = [
29+
{
30+
id: "default",
31+
name: "Default",
32+
apiProvider: "anthropic",
33+
},
34+
{
35+
id: "enhancement",
36+
name: "Enhancement",
37+
apiProvider: "openai",
38+
},
39+
]
40+
41+
const mockProviderSettingsManager = {
42+
getProfile: vi.fn().mockResolvedValue({
43+
id: "enhancement",
44+
name: "Enhancement",
45+
apiProvider: "openai",
46+
openAiApiKey: "test-openai-key",
47+
openAiModelId: "gpt-4",
48+
}),
49+
} as any // Mock the ProviderSettingsManager type
50+
51+
beforeEach(() => {
52+
vi.clearAllMocks()
53+
// Set default mock behavior
54+
vi.mocked(singleCompletionHandler).mockResolvedValue("Short concise title")
55+
})
56+
57+
afterEach(() => {
58+
vi.restoreAllMocks()
59+
})
60+
61+
describe("summarizeTitle", () => {
62+
it("should successfully summarize a long title", async () => {
63+
const longText =
64+
"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"
65+
66+
const result = await TitleSummarizer.summarizeTitle({
67+
text: longText,
68+
apiConfiguration: mockApiConfiguration,
69+
maxLength: 150,
70+
})
71+
72+
expect(result.success).toBe(true)
73+
expect(result.summarizedTitle).toBe("Short concise title")
74+
expect(result.summarizedTitle!.length).toBeLessThan(longText.length)
75+
})
76+
77+
it("should return original text if it's already short", async () => {
78+
const shortText = "Fix bug in login"
79+
80+
const result = await TitleSummarizer.summarizeTitle({
81+
text: shortText,
82+
apiConfiguration: mockApiConfiguration,
83+
maxLength: 150,
84+
})
85+
86+
expect(result.success).toBe(true)
87+
// Text is already shorter than max length, so it returns as-is
88+
expect(result.summarizedTitle).toBe(shortText)
89+
})
90+
91+
it("should use enhancement API configuration when provided", async () => {
92+
const longText =
93+
"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"
94+
95+
const result = await TitleSummarizer.summarizeTitle({
96+
text: longText,
97+
apiConfiguration: mockApiConfiguration,
98+
enhancementApiConfigId: "enhancement",
99+
listApiConfigMeta: mockListApiConfigMeta,
100+
providerSettingsManager: mockProviderSettingsManager,
101+
maxLength: 150,
102+
})
103+
104+
// The function will call providerSettingsManager.getProfile if all conditions are met
105+
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "enhancement" })
106+
expect(result.success).toBe(true)
107+
expect(result.summarizedTitle).toBe("Short concise title")
108+
})
109+
110+
it("should handle API errors gracefully", async () => {
111+
const longText =
112+
"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"
113+
vi.mocked(singleCompletionHandler).mockRejectedValueOnce(new Error("API Error"))
114+
115+
const result = await TitleSummarizer.summarizeTitle({
116+
text: longText,
117+
apiConfiguration: mockApiConfiguration,
118+
maxLength: 150,
119+
})
120+
121+
expect(result.success).toBe(false)
122+
expect(result.error).toBe("API Error")
123+
expect(result.summarizedTitle).toBe(longText)
124+
})
125+
126+
it("should handle missing API configuration", async () => {
127+
const result = await TitleSummarizer.summarizeTitle({
128+
text: "Some text",
129+
apiConfiguration: undefined as any,
130+
maxLength: 150,
131+
})
132+
133+
expect(result.success).toBe(false)
134+
expect(result.error).toBe("No API configuration available")
135+
expect(result.summarizedTitle).toBe("Some text")
136+
})
137+
138+
it("should respect custom maxLength parameter", async () => {
139+
const shortText = "Short text"
140+
141+
const result = await TitleSummarizer.summarizeTitle({
142+
text: shortText,
143+
apiConfiguration: mockApiConfiguration,
144+
maxLength: 100,
145+
})
146+
147+
expect(result.success).toBe(true)
148+
// Text is already shorter than maxLength, returns as-is
149+
expect(result.summarizedTitle).toBe(shortText)
150+
})
151+
152+
it("should use custom support prompts when provided", async () => {
153+
const customPrompts = {
154+
SUMMARIZE_TITLE: "Custom summarization prompt: {{userInput}} (max 150 chars)",
155+
}
156+
157+
// Short text doesn't need summarization
158+
const result = await TitleSummarizer.summarizeTitle({
159+
text: "Text to summarize",
160+
apiConfiguration: mockApiConfiguration,
161+
customSupportPrompts: customPrompts,
162+
maxLength: 150,
163+
})
164+
165+
expect(result.success).toBe(true)
166+
expect(result.summarizedTitle).toBe("Text to summarize")
167+
})
168+
169+
it("should handle empty response from API", async () => {
170+
const longText =
171+
"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"
172+
vi.mocked(singleCompletionHandler).mockResolvedValueOnce("")
173+
174+
const result = await TitleSummarizer.summarizeTitle({
175+
text: longText,
176+
apiConfiguration: mockApiConfiguration,
177+
maxLength: 150,
178+
})
179+
180+
expect(result.success).toBe(false)
181+
expect(result.error).toBe("Received empty summarized title")
182+
expect(result.summarizedTitle).toBe(longText)
183+
})
184+
185+
it("should trim whitespace from summarized title", async () => {
186+
const longText =
187+
"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"
188+
vi.mocked(singleCompletionHandler).mockResolvedValueOnce(" Trimmed title \n")
189+
190+
const result = await TitleSummarizer.summarizeTitle({
191+
text: longText,
192+
apiConfiguration: mockApiConfiguration,
193+
maxLength: 150,
194+
})
195+
196+
expect(result.success).toBe(true)
197+
expect(result.summarizedTitle).toBe("Trimmed title")
198+
})
199+
})
200+
201+
describe("captureTelemetry", () => {
202+
it("should not capture telemetry events (currently disabled)", () => {
203+
const taskId = "test-task-123"
204+
const originalLength = 250
205+
const summarizedLength = 100
206+
207+
TitleSummarizer.captureTelemetry(taskId, originalLength, summarizedLength)
208+
209+
// Since telemetry is commented out in the implementation, it should not be called
210+
expect(TelemetryService.instance.captureEvent).not.toHaveBeenCalled()
211+
})
212+
})
213+
})

0 commit comments

Comments
 (0)