Skip to content

Commit 9149980

Browse files
committed
Refactor find-relevant-files command into modular structure with utility files
1 parent 2f95cdc commit 9149980

12 files changed

Lines changed: 758 additions & 702 deletions

apps/editor/src/commands/find-relevant-files-command.ts

Lines changed: 0 additions & 700 deletions
This file was deleted.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as vscode from 'vscode'
2+
import { WorkspaceProvider } from '../../context/providers/workspace/workspace-provider'
3+
import { ModelProvidersManager } from '../../services/model-providers-manager'
4+
import {
5+
LAST_FIND_RELEVANT_FILES_QUERY_STATE_KEY,
6+
LAST_FIND_RELEVANT_FILES_SHRINK_STATE_KEY
7+
} from '../../constants/state-keys'
8+
import { t } from '@/i18n'
9+
10+
import { get_target_folder_path } from './utils/get-target-folder-path'
11+
import { prompt_for_instructions } from './utils/prompt-for-instructions'
12+
import { analyze_workspace_files } from './utils/analyze-workspace-files'
13+
import { prompt_for_shrink_mode } from './utils/prompt-for-shrink-mode'
14+
import { prompt_for_config } from './utils/prompt-for-config'
15+
import { fetch_relevant_files_from_api } from './utils/fetch-relevant-files-from-api'
16+
import { show_results_and_apply } from './utils/show-results-and-apply'
17+
18+
export const find_relevant_files_command = (
19+
workspace_provider: WorkspaceProvider,
20+
extension_context: vscode.ExtensionContext
21+
) => {
22+
return vscode.commands.registerCommand(
23+
'codeWebChat.findRelevantFiles',
24+
async (item?: any) => {
25+
const folder_path = await get_target_folder_path(item)
26+
27+
if (!folder_path) {
28+
vscode.window.showErrorMessage(
29+
t('command.find-relevant-files.error.no-folder-selected')
30+
)
31+
return
32+
}
33+
34+
let initial_instructions =
35+
extension_context.workspaceState.get<string>(
36+
LAST_FIND_RELEVANT_FILES_QUERY_STATE_KEY
37+
) || ''
38+
39+
while (true) {
40+
const instructions = await prompt_for_instructions(initial_instructions)
41+
if (!instructions) return
42+
43+
await extension_context.workspaceState.update(
44+
LAST_FIND_RELEVANT_FILES_QUERY_STATE_KEY,
45+
instructions
46+
)
47+
initial_instructions = instructions
48+
49+
const analysis = await analyze_workspace_files({
50+
workspace_provider,
51+
folder_path
52+
})
53+
let go_back_to_input = false
54+
while (true) {
55+
const should_shrink = extension_context.workspaceState.get<boolean>(
56+
LAST_FIND_RELEVANT_FILES_SHRINK_STATE_KEY,
57+
false
58+
)
59+
const shrink_result = await prompt_for_shrink_mode({
60+
should_shrink,
61+
full_tokens: analysis.full_tokens,
62+
shrink_tokens: analysis.shrink_tokens
63+
})
64+
65+
if (shrink_result === 'back') {
66+
go_back_to_input = true
67+
break
68+
}
69+
if (shrink_result === 'cancel') return
70+
71+
await extension_context.workspaceState.update(
72+
LAST_FIND_RELEVANT_FILES_SHRINK_STATE_KEY,
73+
shrink_result
74+
)
75+
76+
const api_providers_manager = new ModelProvidersManager(
77+
extension_context
78+
)
79+
const configs =
80+
await api_providers_manager.get_find_relevant_files_tool_configs()
81+
82+
if (configs.length === 0) {
83+
vscode.commands.executeCommand('codeWebChat.settings')
84+
vscode.window.showInformationMessage(
85+
t('command.find-relevant-files.error.no-configs')
86+
)
87+
return
88+
}
89+
90+
let go_back_to_shrink = false
91+
while (true) {
92+
const tokens_to_process = shrink_result
93+
? analysis.shrink_tokens
94+
: analysis.full_tokens
95+
const config_result = await prompt_for_config({
96+
api_providers_manager,
97+
extension_context,
98+
configs,
99+
tokens_to_process
100+
})
101+
102+
if (config_result === 'back') {
103+
go_back_to_shrink = true
104+
break
105+
}
106+
if (config_result === 'cancel') return
107+
108+
const {
109+
config: selected_config,
110+
provider,
111+
skipped: skipped_config
112+
} = config_result
113+
114+
const api_result = await fetch_relevant_files_from_api(
115+
analysis.files_data,
116+
shrink_result as boolean,
117+
instructions,
118+
provider,
119+
selected_config
120+
)
121+
122+
if (api_result === 'cancel' || api_result === 'error') return
123+
if (api_result === 'error_no_files') {
124+
vscode.window.showWarningMessage(
125+
t('command.find-relevant-files.error.no-files-found')
126+
)
127+
go_back_to_input = true
128+
break
129+
}
130+
131+
const apply_result = await show_results_and_apply({
132+
extracted_files: api_result,
133+
analysis,
134+
workspace_provider
135+
})
136+
137+
if (apply_result === 'back') {
138+
if (skipped_config) {
139+
go_back_to_shrink = true
140+
break
141+
} else continue
142+
}
143+
144+
if (apply_result === 'cancel' || apply_result === 'success') return
145+
}
146+
147+
if (go_back_to_shrink) continue
148+
if (go_back_to_input) break
149+
}
150+
151+
if (go_back_to_input) continue
152+
}
153+
}
154+
)
155+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './find-relevant-files-command'
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as vscode from 'vscode'
2+
import * as path from 'path'
3+
import * as fs from 'fs'
4+
import { WorkspaceProvider } from '../../../context/providers/workspace/workspace-provider'
5+
import { shrink_file } from '../../../context/utils/shrink-file/shrink-file'
6+
import { t } from '@/i18n'
7+
8+
export interface FileData {
9+
file_path: string
10+
relative_path: string
11+
content: string
12+
shrunk_content: string
13+
}
14+
15+
export interface FileAnalysisResult {
16+
full_tokens: number
17+
shrink_tokens: number
18+
files_data: FileData[]
19+
workspace_root: string
20+
}
21+
22+
export const analyze_workspace_files = async (params: {
23+
workspace_provider: WorkspaceProvider
24+
folder_path: string
25+
}): Promise<FileAnalysisResult> => {
26+
const all_files = await params.workspace_provider.find_all_files(
27+
params.folder_path
28+
)
29+
const workspace_root =
30+
params.workspace_provider.get_workspace_root_for_file(params.folder_path) ||
31+
params.folder_path
32+
33+
let full_tokens = 0
34+
let shrink_tokens = 0
35+
const files_data: FileData[] = []
36+
37+
await vscode.window.withProgress(
38+
{
39+
location: vscode.ProgressLocation.Window,
40+
title: t('command.find-relevant-files.progress.analyzing')
41+
},
42+
async () => {
43+
for (const file_path of all_files) {
44+
try {
45+
const stats = await fs.promises.stat(file_path)
46+
if (stats.size > 1024 * 1024) continue
47+
48+
const content = await fs.promises.readFile(file_path, 'utf8')
49+
const shrunk_content = shrink_file(content, path.extname(file_path))
50+
const relative_path = path.relative(workspace_root, file_path)
51+
52+
files_data.push({ file_path, relative_path, content, shrunk_content })
53+
54+
const token_count =
55+
await params.workspace_provider.calculate_file_tokens(file_path)
56+
full_tokens += token_count.total
57+
shrink_tokens += token_count.shrink
58+
} catch (e) {}
59+
}
60+
}
61+
)
62+
63+
return { full_tokens, shrink_tokens, files_data, workspace_root }
64+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as vscode from 'vscode'
2+
import axios from 'axios'
3+
import { make_api_request } from '../../../utils/make-api-request'
4+
import {
5+
find_relevant_files_instructions,
6+
find_relevant_files_format
7+
} from '../../../constants/instructions'
8+
import { apply_reasoning_effort } from '../../../utils/apply-reasoning-effort'
9+
import { build_user_content } from '../../../utils/build-user-content'
10+
import { Logger } from '@shared/utils/logger'
11+
import { FileData } from './analyze-workspace-files'
12+
import { t } from '@/i18n'
13+
14+
export const fetch_relevant_files_from_api = async (
15+
files_data: FileData[],
16+
shrink_result: boolean,
17+
instructions: string,
18+
provider: any,
19+
selected_config: any
20+
): Promise<string[] | 'cancel' | 'error_no_files' | 'error'> => {
21+
let xml_files = `<files>\n`
22+
for (const file of files_data) {
23+
const content_to_use = shrink_result ? file.shrunk_content : file.content
24+
xml_files += `<file path="${file.relative_path}">\n<![CDATA[\n${content_to_use}\n]]>\n</file>\n`
25+
}
26+
xml_files += `</files>`
27+
28+
const system_instructions_xml = `${find_relevant_files_format}\n${find_relevant_files_instructions}`
29+
const part2 = `${system_instructions_xml}\n${instructions}`
30+
const user_content = build_user_content({
31+
provider_name: provider.name,
32+
part1: xml_files,
33+
part2,
34+
disable_cache: true
35+
})
36+
37+
const messages = [{ role: 'user', content: user_content }]
38+
const body: { [key: string]: any } = {
39+
messages,
40+
model: selected_config.model,
41+
temperature: selected_config.temperature
42+
}
43+
44+
apply_reasoning_effort({
45+
body,
46+
provider,
47+
reasoning_effort: selected_config.reasoning_effort
48+
})
49+
50+
const cancel_token_source = axios.CancelToken.source()
51+
52+
try {
53+
const completion_result = await vscode.window.withProgress(
54+
{
55+
location: vscode.ProgressLocation.Notification,
56+
title: t('command.find-relevant-files.progress.finding'),
57+
cancellable: true
58+
},
59+
async (progress, token) => {
60+
token.onCancellationRequested(() => {
61+
cancel_token_source.cancel(
62+
t('command.find-relevant-files.cancel.user')
63+
)
64+
})
65+
progress.report({ message: t('common.progress.waiting-for-server') })
66+
return await make_api_request({
67+
endpoint_url: provider.base_url,
68+
api_key: provider.api_key,
69+
body,
70+
cancellation_token: cancel_token_source.token,
71+
on_chunk: () =>
72+
progress.report({ message: t('common.progress.receiving') }),
73+
on_thinking_chunk: () =>
74+
progress.report({ message: t('common.progress.thinking') })
75+
})
76+
}
77+
)
78+
79+
if (completion_result) {
80+
const match = completion_result.response.match(
81+
/<relevant-files>([\s\S]*?)<\/relevant-files>/
82+
)
83+
const extracted_files: string[] = []
84+
if (match && match[1]) {
85+
const file_matches = match[1].matchAll(/<file-path>(.*?)<\/file-path>/g)
86+
for (const m of file_matches) extracted_files.push(m[1].trim())
87+
}
88+
return extracted_files.length === 0 ? 'error_no_files' : extracted_files
89+
}
90+
return 'cancel'
91+
} catch (error) {
92+
if (!axios.isCancel(error)) {
93+
Logger.error({
94+
function_name: 'fetch_relevant_files_from_api',
95+
message: 'Error finding relevant files',
96+
data: error
97+
})
98+
vscode.window.showErrorMessage(
99+
t('command.find-relevant-files.error.finding')
100+
)
101+
}
102+
return 'error'
103+
}
104+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as path from 'path'
2+
import * as fs from 'fs'
3+
4+
export const get_target_folder_path = async (
5+
item?: any
6+
): Promise<string | undefined> => {
7+
let folder_path = item?.resourceUri?.fsPath
8+
if (folder_path) {
9+
try {
10+
const stats = await fs.promises.stat(folder_path)
11+
if (!stats.isDirectory()) {
12+
folder_path = path.dirname(folder_path)
13+
}
14+
} catch (error) {
15+
folder_path = undefined
16+
}
17+
}
18+
return folder_path
19+
}

0 commit comments

Comments
 (0)