Skip to content

Commit 83280a0

Browse files
feat: Add Task History Context to Prompt Enhancement (#6343)
1 parent 4b45a4e commit 83280a0

27 files changed

+671
-65
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export const globalSettingsSchema = z.object({
139139
customModePrompts: customModePromptsSchema.optional(),
140140
customSupportPrompts: customSupportPromptsSchema.optional(),
141141
enhancementApiConfigId: z.string().optional(),
142+
includeTaskHistoryInEnhance: z.boolean().optional(),
142143
historyPreviewCollapsed: z.boolean().optional(),
143144
profileThresholds: z.record(z.string(), z.number()).optional(),
144145
hasOpenedModeSelector: z.boolean().optional(),

src/core/webview/ClineProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,7 @@ export class ClineProvider
15161516
followupAutoApproveTimeoutMs,
15171517
includeDiagnosticMessages,
15181518
maxDiagnosticMessages,
1519+
includeTaskHistoryInEnhance,
15191520
} = await this.getState()
15201521

15211522
const telemetryKey = process.env.POSTHOG_API_KEY
@@ -1641,6 +1642,7 @@ export class ClineProvider
16411642
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
16421643
includeDiagnosticMessages: includeDiagnosticMessages ?? true,
16431644
maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
1645+
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? false,
16441646
}
16451647
}
16461648

@@ -1825,6 +1827,8 @@ export class ClineProvider
18251827
// Add diagnostic message settings
18261828
includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
18271829
maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
1830+
// Add includeTaskHistoryInEnhance setting
1831+
includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? false,
18281832
}
18291833
}
18301834

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import { MessageEnhancer } from "../messageEnhancer"
3+
import { ProviderSettings, ClineMessage } from "@roo-code/types"
4+
import { TelemetryService } from "@roo-code/telemetry"
5+
import * as singleCompletionHandlerModule from "../../../utils/single-completion-handler"
6+
import { ProviderSettingsManager } from "../../config/ProviderSettingsManager"
7+
8+
// Mock dependencies
9+
vi.mock("../../../utils/single-completion-handler")
10+
vi.mock("@roo-code/telemetry")
11+
12+
describe("MessageEnhancer", () => {
13+
let mockProviderSettingsManager: ProviderSettingsManager
14+
let mockSingleCompletionHandler: ReturnType<typeof vi.fn>
15+
16+
const mockApiConfiguration: ProviderSettings = {
17+
apiProvider: "openai",
18+
apiKey: "test-key",
19+
apiModelId: "gpt-4",
20+
}
21+
22+
const mockListApiConfigMeta = [
23+
{ id: "config1", name: "Config 1" },
24+
{ id: "config2", name: "Config 2" },
25+
]
26+
27+
beforeEach(() => {
28+
// Reset all mocks
29+
vi.clearAllMocks()
30+
31+
// Mock provider settings manager
32+
mockProviderSettingsManager = {
33+
getProfile: vi.fn().mockResolvedValue({
34+
name: "Enhancement Config",
35+
apiProvider: "anthropic",
36+
apiKey: "enhancement-key",
37+
apiModelId: "claude-3",
38+
}),
39+
} as any
40+
41+
// Mock single completion handler
42+
mockSingleCompletionHandler = vi.fn().mockResolvedValue("Enhanced prompt text")
43+
vi.mocked(singleCompletionHandlerModule).singleCompletionHandler = mockSingleCompletionHandler
44+
45+
// Mock TelemetryService
46+
vi.mocked(TelemetryService).hasInstance = vi.fn().mockReturnValue(true)
47+
// Mock the instance getter
48+
Object.defineProperty(TelemetryService, "instance", {
49+
get: vi.fn().mockReturnValue({
50+
capturePromptEnhanced: vi.fn(),
51+
}),
52+
configurable: true,
53+
})
54+
})
55+
56+
afterEach(() => {
57+
vi.restoreAllMocks()
58+
})
59+
60+
describe("enhanceMessage", () => {
61+
it("should enhance a simple message successfully", async () => {
62+
const result = await MessageEnhancer.enhanceMessage({
63+
text: "Write a function to calculate fibonacci",
64+
apiConfiguration: mockApiConfiguration,
65+
listApiConfigMeta: mockListApiConfigMeta,
66+
providerSettingsManager: mockProviderSettingsManager,
67+
})
68+
69+
expect(result.success).toBe(true)
70+
expect(result.enhancedText).toBe("Enhanced prompt text")
71+
expect(result.error).toBeUndefined()
72+
73+
// Verify single completion handler was called with correct prompt
74+
expect(mockSingleCompletionHandler).toHaveBeenCalledWith(
75+
mockApiConfiguration,
76+
expect.stringContaining("Write a function to calculate fibonacci"),
77+
)
78+
})
79+
80+
it("should use enhancement API config when provided", async () => {
81+
const result = await MessageEnhancer.enhanceMessage({
82+
text: "Test prompt",
83+
apiConfiguration: mockApiConfiguration,
84+
customSupportPrompts: {},
85+
listApiConfigMeta: mockListApiConfigMeta,
86+
enhancementApiConfigId: "config2",
87+
providerSettingsManager: mockProviderSettingsManager,
88+
})
89+
90+
expect(result.success).toBe(true)
91+
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "config2" })
92+
93+
// Verify the enhancement config was used instead of default
94+
const expectedConfig = {
95+
apiProvider: "anthropic",
96+
apiKey: "enhancement-key",
97+
apiModelId: "claude-3",
98+
}
99+
expect(mockSingleCompletionHandler).toHaveBeenCalledWith(expectedConfig, expect.any(String))
100+
})
101+
102+
it("should include task history when enabled", async () => {
103+
const mockClineMessages: ClineMessage[] = [
104+
{ type: "ask", text: "Create a React component", ts: 1000 },
105+
{ type: "say", say: "text", text: "I'll create a React component for you", ts: 2000 },
106+
{ type: "ask", text: "Add props to the component", ts: 3000 },
107+
{ type: "say", say: "reasoning", text: "Using tool", ts: 4000 }, // Should be filtered out
108+
]
109+
110+
const result = await MessageEnhancer.enhanceMessage({
111+
text: "Improve the component",
112+
apiConfiguration: mockApiConfiguration,
113+
listApiConfigMeta: mockListApiConfigMeta,
114+
includeTaskHistoryInEnhance: true,
115+
currentClineMessages: mockClineMessages,
116+
providerSettingsManager: mockProviderSettingsManager,
117+
})
118+
119+
expect(result.success).toBe(true)
120+
121+
// Verify the prompt includes task history
122+
const calledPrompt = mockSingleCompletionHandler.mock.calls[0][1]
123+
expect(calledPrompt).toContain("Improve the component")
124+
expect(calledPrompt).toContain("previous conversation context")
125+
expect(calledPrompt).toContain("User: Create a React component")
126+
expect(calledPrompt).toContain("Assistant: I'll create a React component for you")
127+
expect(calledPrompt).toContain("User: Add props to the component")
128+
expect(calledPrompt).not.toContain("Using tool") // reasoning messages should be filtered
129+
})
130+
131+
it("should limit task history to last 10 messages", async () => {
132+
// Create 15 messages
133+
const mockClineMessages: ClineMessage[] = Array.from({ length: 15 }, (_, i) => ({
134+
type: i % 2 === 0 ? "ask" : "say",
135+
say: i % 2 === 1 ? "text" : undefined,
136+
text: `Message ${i + 1}`,
137+
ts: i * 1000,
138+
})) as ClineMessage[]
139+
140+
await MessageEnhancer.enhanceMessage({
141+
text: "Test",
142+
apiConfiguration: mockApiConfiguration,
143+
listApiConfigMeta: mockListApiConfigMeta,
144+
includeTaskHistoryInEnhance: true,
145+
currentClineMessages: mockClineMessages,
146+
providerSettingsManager: mockProviderSettingsManager,
147+
})
148+
149+
const calledPrompt = mockSingleCompletionHandler.mock.calls[0][1]
150+
151+
// Should include messages 6-15 (last 10)
152+
expect(calledPrompt).toContain("Message 6")
153+
expect(calledPrompt).toContain("Message 15")
154+
expect(calledPrompt).not.toContain("Message 5")
155+
})
156+
157+
it("should truncate long messages in task history", async () => {
158+
const longText = "A".repeat(600) // 600 characters
159+
const mockClineMessages: ClineMessage[] = [{ type: "ask", text: longText, ts: 1000 }]
160+
161+
await MessageEnhancer.enhanceMessage({
162+
text: "Test",
163+
apiConfiguration: mockApiConfiguration,
164+
listApiConfigMeta: mockListApiConfigMeta,
165+
includeTaskHistoryInEnhance: true,
166+
currentClineMessages: mockClineMessages,
167+
providerSettingsManager: mockProviderSettingsManager,
168+
})
169+
170+
const calledPrompt = mockSingleCompletionHandler.mock.calls[0][1]
171+
172+
// Should truncate to 500 chars + "..."
173+
expect(calledPrompt).toContain("A".repeat(500) + "...")
174+
expect(calledPrompt).not.toContain("A".repeat(501))
175+
})
176+
177+
it("should use custom support prompts when provided", async () => {
178+
const customSupportPrompts = {
179+
ENHANCE: "Custom enhancement template: ${userInput}",
180+
}
181+
182+
await MessageEnhancer.enhanceMessage({
183+
text: "Test prompt",
184+
apiConfiguration: mockApiConfiguration,
185+
customSupportPrompts,
186+
listApiConfigMeta: mockListApiConfigMeta,
187+
providerSettingsManager: mockProviderSettingsManager,
188+
})
189+
190+
const calledPrompt = mockSingleCompletionHandler.mock.calls[0][1]
191+
expect(calledPrompt).toBe("Custom enhancement template: Test prompt")
192+
})
193+
194+
it("should handle errors gracefully", async () => {
195+
mockSingleCompletionHandler.mockRejectedValue(new Error("API error"))
196+
197+
const result = await MessageEnhancer.enhanceMessage({
198+
text: "Test",
199+
apiConfiguration: mockApiConfiguration,
200+
listApiConfigMeta: mockListApiConfigMeta,
201+
providerSettingsManager: mockProviderSettingsManager,
202+
})
203+
204+
expect(result.success).toBe(false)
205+
expect(result.error).toBe("API error")
206+
expect(result.enhancedText).toBeUndefined()
207+
})
208+
209+
it("should handle non-Error exceptions", async () => {
210+
mockSingleCompletionHandler.mockRejectedValue("String error")
211+
212+
const result = await MessageEnhancer.enhanceMessage({
213+
text: "Test",
214+
apiConfiguration: mockApiConfiguration,
215+
listApiConfigMeta: mockListApiConfigMeta,
216+
providerSettingsManager: mockProviderSettingsManager,
217+
})
218+
219+
expect(result.success).toBe(false)
220+
expect(result.error).toBe("String error")
221+
})
222+
223+
it("should fall back to default config if enhancement config is invalid", async () => {
224+
mockProviderSettingsManager.getProfile = vi.fn().mockResolvedValue({
225+
name: "Invalid Config",
226+
// Missing apiProvider
227+
})
228+
229+
await MessageEnhancer.enhanceMessage({
230+
text: "Test",
231+
apiConfiguration: mockApiConfiguration,
232+
listApiConfigMeta: mockListApiConfigMeta,
233+
enhancementApiConfigId: "config2",
234+
providerSettingsManager: mockProviderSettingsManager,
235+
})
236+
237+
// Should use the default config
238+
expect(mockSingleCompletionHandler).toHaveBeenCalledWith(mockApiConfiguration, expect.any(String))
239+
})
240+
241+
it("should handle empty task history gracefully", async () => {
242+
const result = await MessageEnhancer.enhanceMessage({
243+
text: "Test",
244+
apiConfiguration: mockApiConfiguration,
245+
listApiConfigMeta: mockListApiConfigMeta,
246+
includeTaskHistoryInEnhance: true,
247+
currentClineMessages: [],
248+
providerSettingsManager: mockProviderSettingsManager,
249+
})
250+
251+
expect(result.success).toBe(true)
252+
253+
const calledPrompt = mockSingleCompletionHandler.mock.calls[0][1]
254+
// Should not include task history section
255+
expect(calledPrompt).not.toContain("previous conversation context")
256+
})
257+
})
258+
259+
describe("captureTelemetry", () => {
260+
it("should capture telemetry when TelemetryService is available", () => {
261+
const mockTaskId = "task-123"
262+
const mockCaptureEvent = vi.fn()
263+
vi.mocked(TelemetryService.instance).captureEvent = mockCaptureEvent
264+
265+
MessageEnhancer.captureTelemetry(mockTaskId, true)
266+
267+
expect(TelemetryService.hasInstance).toHaveBeenCalled()
268+
expect(mockCaptureEvent).toHaveBeenCalledWith(expect.any(String), {
269+
taskId: mockTaskId,
270+
includeTaskHistory: true,
271+
})
272+
})
273+
274+
it("should handle missing TelemetryService gracefully", () => {
275+
vi.mocked(TelemetryService).hasInstance = vi.fn().mockReturnValue(false)
276+
277+
// Should not throw
278+
expect(() => MessageEnhancer.captureTelemetry("task-123", true)).not.toThrow()
279+
})
280+
281+
it("should work without task ID", () => {
282+
const mockCaptureEvent = vi.fn()
283+
vi.mocked(TelemetryService.instance).captureEvent = mockCaptureEvent
284+
285+
MessageEnhancer.captureTelemetry(undefined, false)
286+
287+
expect(mockCaptureEvent).toHaveBeenCalledWith(expect.any(String), {
288+
includeTaskHistory: false,
289+
})
290+
})
291+
292+
it("should default includeTaskHistory to false when not provided", () => {
293+
const mockCaptureEvent = vi.fn()
294+
vi.mocked(TelemetryService.instance).captureEvent = mockCaptureEvent
295+
296+
MessageEnhancer.captureTelemetry("task-123")
297+
298+
expect(mockCaptureEvent).toHaveBeenCalledWith(expect.any(String), {
299+
taskId: "task-123",
300+
includeTaskHistory: false,
301+
})
302+
})
303+
})
304+
305+
describe("extractTaskHistory", () => {
306+
it("should filter and format messages correctly", () => {
307+
const messages: ClineMessage[] = [
308+
{ type: "ask", text: "User message 1", ts: 1000 },
309+
{ type: "say", say: "text", text: "Assistant message 1", ts: 2000 },
310+
{ type: "say", say: "reasoning", text: "Tool use", ts: 3000 },
311+
{ type: "ask", text: "", ts: 4000 }, // Empty text
312+
{ type: "say", say: "text", text: undefined, ts: 5000 }, // No text
313+
{ type: "ask", text: "User message 2", ts: 6000 },
314+
]
315+
316+
// Access private method through any type assertion for testing
317+
const history = (MessageEnhancer as any).extractTaskHistory(messages)
318+
319+
expect(history).toContain("User: User message 1")
320+
expect(history).toContain("Assistant: Assistant message 1")
321+
expect(history).toContain("User: User message 2")
322+
expect(history).not.toContain("Tool use")
323+
expect(history.split("\n").length).toBe(3) // Only 3 valid messages
324+
})
325+
326+
it("should handle malformed messages gracefully", () => {
327+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
328+
329+
// Create messages that will cause errors when accessed
330+
const malformedMessages = [
331+
null,
332+
undefined,
333+
{ type: "ask" }, // Missing required properties
334+
"not an object",
335+
] as any
336+
337+
// Access private method through any type assertion for testing
338+
const history = (MessageEnhancer as any).extractTaskHistory(malformedMessages)
339+
340+
// Should return empty string and log error
341+
expect(history).toBe("")
342+
expect(consoleSpy).toHaveBeenCalledWith("Failed to extract task history:", expect.any(Error))
343+
344+
consoleSpy.mockRestore()
345+
})
346+
347+
it("should handle messages with circular references", () => {
348+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
349+
350+
// Create a message with circular reference
351+
const circularMessage: any = { type: "ask", text: "Test" }
352+
circularMessage.self = circularMessage
353+
354+
const messages = [circularMessage] as ClineMessage[]
355+
356+
// Access private method through any type assertion for testing
357+
const history = (MessageEnhancer as any).extractTaskHistory(messages)
358+
359+
// Should handle gracefully
360+
expect(history).toBe("User: Test")
361+
362+
consoleSpy.mockRestore()
363+
})
364+
})
365+
})

0 commit comments

Comments
 (0)