Skip to content

Commit 6f48861

Browse files
committed
Refactor list_files tool into separate handler class and improve documentation (RooCodeInc#2030)
1 parent e464c41 commit 6f48861

File tree

4 files changed

+172
-54
lines changed

4 files changed

+172
-54
lines changed

src/core/Cline.ts

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { ExitCodeDetails } from "../integrations/terminal/TerminalProcess"
3535
import { Terminal } from "../integrations/terminal/Terminal"
3636
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
3737
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
38-
import { listFiles } from "../services/glob/list-files"
38+
import { listFiles, ListFilesToolHandler } from "../services/glob/list-files"
3939
import { regexSearchFiles } from "../services/ripgrep"
4040
import { parseSourceCodeDefinitionsForFile, parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
4141
import { CheckpointStorage } from "../shared/checkpoints"
@@ -414,9 +414,29 @@ export class Cline extends EventEmitter<ClineEvents> {
414414
}
415415
}
416416

417-
// Communicate with webview
418-
419-
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
417+
/**
418+
* Sends a message to the webview and waits for a response.
419+
*
420+
* The webview will render the message as a prompt to the user, and
421+
* will respond with a `ClineAskResponse` depending on the action
422+
* taken by the user.
423+
*
424+
* The `partial` argument is a boolean value that indicates whether
425+
* the message is a partial message or a completion of a partial
426+
* message. If `partial` is `true`, the message will be rendered as
427+
* a partial message to the user. If `partial` is `false`, the
428+
* message will be rendered as a completion of the partial message.
429+
* If `partial` is `undefined`, the message will be rendered as a
430+
* complete message.
431+
*
432+
* The `progressStatus` argument is an object that indicates the
433+
* progress of a long-running tool. It is optional.
434+
*
435+
* @param type - The type of the message to send.
436+
* @param text - The text of the message to send.
437+
* @param partial - The partial state of the message.
438+
* @param progressStatus - The progress status of a long-running tool.
439+
*/
420440
async ask(
421441
type: ClineAsk,
422442
text?: string,
@@ -1315,7 +1335,9 @@ export class Cline extends EventEmitter<ClineEvents> {
13151335
//throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
13161336
}
13171337

1318-
const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
1338+
const block: AssistantMessageContent = cloneDeep(
1339+
this.assistantMessageContent[this.currentStreamingContentIndex],
1340+
) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
13191341

13201342
let isCheckpointPossible = false
13211343

@@ -2455,54 +2477,9 @@ export class Cline extends EventEmitter<ClineEvents> {
24552477
}
24562478

24572479
case "list_files": {
2458-
const relDirPath: string | undefined = block.params.path
2459-
const recursiveRaw: string | undefined = block.params.recursive
2460-
const recursive = recursiveRaw?.toLowerCase() === "true"
2461-
const sharedMessageProps: ClineSayTool = {
2462-
tool: !recursive ? "listFilesTopLevel" : "listFilesRecursive",
2463-
path: getReadablePath(this.cwd, removeClosingTag("path", relDirPath)),
2464-
}
2465-
try {
2466-
if (block.partial) {
2467-
const partialMessage = JSON.stringify({
2468-
...sharedMessageProps,
2469-
content: "",
2470-
} satisfies ClineSayTool)
2471-
await this.ask("tool", partialMessage, block.partial).catch(() => {})
2472-
break
2473-
} else {
2474-
if (!relDirPath) {
2475-
this.consecutiveMistakeCount++
2476-
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
2477-
break
2478-
}
2479-
this.consecutiveMistakeCount = 0
2480-
const absolutePath = path.resolve(this.cwd, relDirPath)
2481-
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
2482-
const { showRooIgnoredFiles = true } =
2483-
(await this.providerRef.deref()?.getState()) ?? {}
2484-
const result = formatResponse.formatFilesList(
2485-
absolutePath,
2486-
files,
2487-
didHitLimit,
2488-
this.rooIgnoreController,
2489-
showRooIgnoredFiles,
2490-
)
2491-
const completeMessage = JSON.stringify({
2492-
...sharedMessageProps,
2493-
content: result,
2494-
} satisfies ClineSayTool)
2495-
const didApprove = await askApproval("tool", completeMessage)
2496-
if (!didApprove) {
2497-
break
2498-
}
2499-
pushToolResult(result)
2500-
break
2501-
}
2502-
} catch (error) {
2503-
await handleError("listing files", error)
2504-
break
2505-
}
2480+
const handler = new ListFilesToolHandler(this, block, pushToolResult)
2481+
await handler.handle()
2482+
break
25062483
}
25072484
case "list_code_definition_names": {
25082485
const relDirPath: string | undefined = block.params.path

src/core/assistant-message/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,21 @@ export interface SearchFilesToolUse extends ToolUse {
103103
params: Partial<Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">>
104104
}
105105

106+
/**
107+
* Tool use for listing files in a directory.
108+
*
109+
*/
106110
export interface ListFilesToolUse extends ToolUse {
107111
name: "list_files"
112+
/**
113+
* Relative path to the directory to list files from.
114+
* Defaults to the current file's directory.
115+
*
116+
* @param path - Relative path to the directory to list files from.
117+
* Defaults to the current file's directory.
118+
* @param recursive - Whether to list files recursively in the directory.
119+
* Defaults to false.
120+
*/
108121
params: Partial<Pick<Record<ToolParamName, string>, "path" | "recursive">>
109122
}
110123

src/core/prompts/tools/list-files.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { ToolArgs } from "./types"
22

3+
/**
4+
* Get the tool description for the list_files tool
5+
* @param args The tool arguments including cwd and toolOptions
6+
* @returns The complete tool description including format requirements and examples
7+
*/
38
export function getListFilesDescription(args: ToolArgs): string {
49
return `## list_files
510
Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.

src/services/glob/list-files.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { globby, Options } from "globby"
22
import os from "os"
33
import * as path from "path"
4-
import { arePathsEqual } from "../../utils/path"
4+
import { arePathsEqual, getReadablePath } from "../../utils/path"
5+
import { formatResponse } from "../../core/prompts/responses"
6+
import { ClineSayTool } from "../../shared/ExtensionMessage"
57

68
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
79
const absolutePath = path.resolve(dirPath)
@@ -95,3 +97,124 @@ async function globbyLevelByLevel(limit: number, options?: Options) {
9597
return Array.from(results)
9698
}
9799
}
100+
101+
export class ListFilesTool {
102+
private readonly cwd: string
103+
private readonly relDirPath: string
104+
private readonly recursive: boolean
105+
private readonly providerRef: WeakRef<any>
106+
private readonly rooIgnoreController: any
107+
108+
constructor(
109+
cwd: string,
110+
relDirPath: string,
111+
recursive: boolean,
112+
providerRef: WeakRef<any>,
113+
rooIgnoreController: any,
114+
) {
115+
this.cwd = cwd
116+
this.relDirPath = relDirPath
117+
this.recursive = recursive
118+
this.providerRef = providerRef
119+
this.rooIgnoreController = rooIgnoreController
120+
}
121+
122+
public getSharedMessageProps(): ClineSayTool {
123+
return {
124+
tool: !this.recursive ? "listFilesTopLevel" : "listFilesRecursive",
125+
path: getReadablePath(this.cwd, removeClosingTag("path", this.relDirPath)),
126+
}
127+
}
128+
129+
public async getPartialMessage(): Promise<string> {
130+
return JSON.stringify({
131+
...this.getSharedMessageProps(),
132+
content: "",
133+
} satisfies ClineSayTool)
134+
}
135+
136+
public async execute(): Promise<string> {
137+
if (!this.relDirPath) {
138+
throw new Error("Missing required parameter: path")
139+
}
140+
141+
const absolutePath = path.resolve(this.cwd, this.relDirPath)
142+
const [files, didHitLimit] = await listFiles(absolutePath, this.recursive, 200)
143+
const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {}
144+
145+
return formatResponse.formatFilesList(
146+
absolutePath,
147+
files,
148+
didHitLimit,
149+
this.rooIgnoreController,
150+
showRooIgnoredFiles,
151+
)
152+
}
153+
}
154+
function removeClosingTag(arg0: string, relDirPath: string): string | undefined {
155+
throw new Error("Function not implemented.")
156+
}
157+
158+
export class ListFilesToolHandler {
159+
constructor(
160+
private readonly cline: any, // Type this properly based on your Cline class
161+
private readonly block: {
162+
params: { path?: string; recursive?: string }
163+
partial?: boolean
164+
},
165+
private readonly pushToolResult: (result: string) => void,
166+
) {}
167+
168+
async handle(): Promise<void> {
169+
const relDirPath = this.block.params.path
170+
const recursiveRaw = this.block.params.recursive
171+
const recursive = recursiveRaw?.toLowerCase() === "true"
172+
173+
const listFilesTool = new ListFilesTool(
174+
this.cline.cwd,
175+
relDirPath,
176+
recursive,
177+
this.cline.providerRef,
178+
this.cline.rooIgnoreController,
179+
)
180+
181+
try {
182+
if (this.block.partial) {
183+
await this.handlePartial(listFilesTool)
184+
return
185+
}
186+
187+
if (!relDirPath) {
188+
this.cline.consecutiveMistakeCount++
189+
this.pushToolResult(await this.cline.sayAndCreateMissingParamError("list_files", "path"))
190+
return
191+
}
192+
193+
await this.handleComplete(listFilesTool)
194+
} catch (error) {
195+
await this.cline.handleError("listing files", error)
196+
}
197+
}
198+
199+
private async handlePartial(listFilesTool: ListFilesTool): Promise<void> {
200+
const partialMessage = await listFilesTool.getPartialMessage()
201+
await this.cline.ask("tool", partialMessage, this.block.partial).catch(() => {})
202+
}
203+
204+
private async handleComplete(listFilesTool: ListFilesTool): Promise<void> {
205+
this.cline.consecutiveMistakeCount = 0
206+
const result = await listFilesTool.execute()
207+
208+
const completeMessage = JSON.stringify({
209+
...listFilesTool.getSharedMessageProps(),
210+
content: result,
211+
} satisfies ClineSayTool)
212+
213+
const didApprove = await this.cline.askApproval("tool", completeMessage)
214+
if (!didApprove) {
215+
return
216+
}
217+
218+
this.pushToolResult(result)
219+
}
220+
}

0 commit comments

Comments
 (0)