Skip to content

Commit 08c6146

Browse files
committed
feat: implement AI Deep Research with real-time SSE support
- Add aiDeepResearchTool.ts for handling AI deep research requests - Create AIDeepResearchService.ts for SSE communication with server - Add AIDeepResearchBlock.tsx UI component for displaying research progress - Update tool types and registration in shared/tools.ts - Add ai_deep_research to ClineSayTool interface - Add ai_deep_research_result to ClineSay types - Update presentAssistantMessage.ts to handle the new tool - Add UI integration in ChatRow.tsx - Add translation keys for AI Deep Research - Add comprehensive tests for aiDeepResearchTool This implementation provides real-time streaming of AI research progress including thinking, searching, reading, and analyzing states.
1 parent 1b12108 commit 08c6146

File tree

11 files changed

+711
-1
lines changed

11 files changed

+711
-1
lines changed

packages/types/src/message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export const clineSays = [
106106
"condense_context",
107107
"condense_context_error",
108108
"codebase_search_result",
109+
"ai_deep_research_result",
109110
"user_edit_todos",
110111
] as const
111112

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const toolNames = [
3434
"fetch_instructions",
3535
"codebase_search",
3636
"update_todo_list",
37+
"ai_deep_research",
3738
] as const
3839

3940
export const toolNamesSchema = z.enum(toolNames)

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { formatResponse } from "../prompts/responses"
3232
import { validateToolUse } from "../tools/validateToolUse"
3333
import { Task } from "../task/Task"
3434
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
35+
import { aiDeepResearchTool } from "../tools/aiDeepResearchTool"
3536
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
3637
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
3738

@@ -204,7 +205,9 @@ export async function presentAssistantMessage(cline: Task) {
204205
return `[${block.name}]`
205206
case "switch_mode":
206207
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
207-
case "codebase_search": // Add case for the new tool
208+
case "codebase_search":
209+
return `[${block.name} for '${block.params.query}']`
210+
case "ai_deep_research":
208211
return `[${block.name} for '${block.params.query}']`
209212
case "update_todo_list":
210213
return `[${block.name}]`
@@ -462,6 +465,9 @@ export async function presentAssistantMessage(cline: Task) {
462465
case "codebase_search":
463466
await codebaseSearchTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
464467
break
468+
case "ai_deep_research":
469+
await aiDeepResearchTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
470+
break
465471
case "list_code_definition_names":
466472
await listCodeDefinitionNamesTool(
467473
cline,
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { aiDeepResearchTool } from "../aiDeepResearchTool"
3+
import { Task } from "../../task/Task"
4+
import { AIDeepResearchService } from "../../../services/ai-deep-research/AIDeepResearchService"
5+
6+
// Mock the AIDeepResearchService
7+
vi.mock("../../../services/ai-deep-research/AIDeepResearchService")
8+
9+
describe("aiDeepResearchTool", () => {
10+
let mockCline: any
11+
let mockAskApproval: any
12+
let mockHandleError: any
13+
let mockPushToolResult: any
14+
let mockRemoveClosingTag: any
15+
let mockPerformResearch: any
16+
17+
beforeEach(() => {
18+
vi.clearAllMocks()
19+
20+
// Mock the Task instance
21+
mockCline = {
22+
say: vi.fn(),
23+
ask: vi.fn().mockResolvedValue(undefined),
24+
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
25+
consecutiveMistakeCount: 0,
26+
providerRef: {
27+
deref: vi.fn().mockReturnValue({
28+
context: {},
29+
}),
30+
},
31+
}
32+
33+
// Mock the callback functions
34+
mockAskApproval = vi.fn().mockResolvedValue(true)
35+
mockHandleError = vi.fn()
36+
mockPushToolResult = vi.fn()
37+
mockRemoveClosingTag = vi.fn((tag, content) => content || "")
38+
39+
// Mock AIDeepResearchService
40+
mockPerformResearch = vi.fn().mockResolvedValue("Research completed successfully")
41+
AIDeepResearchService.prototype.performResearch = mockPerformResearch
42+
})
43+
44+
it("should handle missing query parameter", async () => {
45+
const block = {
46+
type: "tool_use" as const,
47+
name: "ai_deep_research" as const,
48+
params: {},
49+
partial: false,
50+
}
51+
52+
await aiDeepResearchTool(
53+
mockCline,
54+
block,
55+
mockAskApproval,
56+
mockHandleError,
57+
mockPushToolResult,
58+
mockRemoveClosingTag,
59+
)
60+
61+
expect(mockCline.consecutiveMistakeCount).toBe(1)
62+
expect(mockPushToolResult).toHaveBeenCalledWith("Missing parameter error")
63+
expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("ai_deep_research", "query")
64+
})
65+
66+
it("should handle partial block", async () => {
67+
const block = {
68+
type: "tool_use" as const,
69+
name: "ai_deep_research" as const,
70+
params: { query: "test query" },
71+
partial: true,
72+
}
73+
74+
await aiDeepResearchTool(
75+
mockCline,
76+
block,
77+
mockAskApproval,
78+
mockHandleError,
79+
mockPushToolResult,
80+
mockRemoveClosingTag,
81+
)
82+
83+
expect(mockCline.ask).toHaveBeenCalledWith(
84+
"tool",
85+
JSON.stringify({
86+
tool: "aiDeepResearch",
87+
query: "test query",
88+
}),
89+
true,
90+
)
91+
expect(mockAskApproval).not.toHaveBeenCalled()
92+
})
93+
94+
it("should handle user rejection", async () => {
95+
mockAskApproval.mockResolvedValue(false)
96+
97+
const block = {
98+
type: "tool_use" as const,
99+
name: "ai_deep_research" as const,
100+
params: { query: "test query" },
101+
partial: false,
102+
}
103+
104+
await aiDeepResearchTool(
105+
mockCline,
106+
block,
107+
mockAskApproval,
108+
mockHandleError,
109+
mockPushToolResult,
110+
mockRemoveClosingTag,
111+
)
112+
113+
expect(mockAskApproval).toHaveBeenCalled()
114+
expect(mockPushToolResult).toHaveBeenCalledWith("The user denied this operation.")
115+
expect(mockPerformResearch).not.toHaveBeenCalled()
116+
})
117+
118+
it("should perform research successfully", async () => {
119+
const block = {
120+
type: "tool_use" as const,
121+
name: "ai_deep_research" as const,
122+
params: { query: "test query" },
123+
partial: false,
124+
}
125+
126+
await aiDeepResearchTool(
127+
mockCline,
128+
block,
129+
mockAskApproval,
130+
mockHandleError,
131+
mockPushToolResult,
132+
mockRemoveClosingTag,
133+
)
134+
135+
expect(mockAskApproval).toHaveBeenCalled()
136+
expect(mockCline.say).toHaveBeenCalledWith(
137+
"ai_deep_research_result",
138+
expect.stringContaining('"status":"thinking"'),
139+
)
140+
expect(mockPerformResearch).toHaveBeenCalledWith("test query", expect.any(Object))
141+
expect(mockPushToolResult).toHaveBeenCalledWith("Research completed successfully")
142+
})
143+
144+
it("should handle errors during research", async () => {
145+
const error = new Error("Research failed")
146+
mockPerformResearch.mockRejectedValue(error)
147+
148+
const block = {
149+
type: "tool_use" as const,
150+
name: "ai_deep_research" as const,
151+
params: { query: "test query" },
152+
partial: false,
153+
}
154+
155+
await aiDeepResearchTool(
156+
mockCline,
157+
block,
158+
mockAskApproval,
159+
mockHandleError,
160+
mockPushToolResult,
161+
mockRemoveClosingTag,
162+
)
163+
164+
expect(mockHandleError).toHaveBeenCalledWith("ai_deep_research", error)
165+
})
166+
167+
it("should handle missing context", async () => {
168+
mockCline.providerRef.deref.mockReturnValue(null)
169+
170+
const block = {
171+
type: "tool_use" as const,
172+
name: "ai_deep_research" as const,
173+
params: { query: "test query" },
174+
partial: false,
175+
}
176+
177+
await aiDeepResearchTool(
178+
mockCline,
179+
block,
180+
mockAskApproval,
181+
mockHandleError,
182+
mockPushToolResult,
183+
mockRemoveClosingTag,
184+
)
185+
186+
expect(mockHandleError).toHaveBeenCalledWith(
187+
"ai_deep_research",
188+
expect.objectContaining({
189+
message: "Extension context is not available.",
190+
}),
191+
)
192+
})
193+
194+
it("should call all callbacks during research", async () => {
195+
let capturedCallbacks: any = {}
196+
mockPerformResearch.mockImplementation(async (query: string, callbacks: any) => {
197+
capturedCallbacks = callbacks
198+
// Simulate calling each callback
199+
await callbacks.onThinking("Thinking about the query...")
200+
await callbacks.onSearching("machine learning")
201+
await callbacks.onReading("https://example.com/article")
202+
await callbacks.onAnalyzing("Analyzing the content...")
203+
await callbacks.onResult("Final research result")
204+
return "Research completed successfully"
205+
})
206+
207+
const block = {
208+
type: "tool_use" as const,
209+
name: "ai_deep_research" as const,
210+
params: { query: "test query" },
211+
partial: false,
212+
}
213+
214+
await aiDeepResearchTool(
215+
mockCline,
216+
block,
217+
mockAskApproval,
218+
mockHandleError,
219+
mockPushToolResult,
220+
mockRemoveClosingTag,
221+
)
222+
223+
// Verify all status updates were sent
224+
const sayCalls = mockCline.say.mock.calls
225+
expect(sayCalls.some((call: any[]) => call[1].includes('"status":"thinking"'))).toBe(true)
226+
expect(sayCalls.some((call: any[]) => call[1].includes('"status":"searching"'))).toBe(true)
227+
expect(sayCalls.some((call: any[]) => call[1].includes('"status":"reading"'))).toBe(true)
228+
expect(sayCalls.some((call: any[]) => call[1].includes('"status":"analyzing"'))).toBe(true)
229+
expect(sayCalls.some((call: any[]) => call[1].includes('"status":"completed"'))).toBe(true)
230+
})
231+
})

0 commit comments

Comments
 (0)