diff --git a/apps/editor/src/commands/apply-chat-response-command/apply-chat-response-command.ts b/apps/editor/src/commands/apply-chat-response-command/apply-chat-response-command.ts index 1767c5340..e0e4a2718 100644 --- a/apps/editor/src/commands/apply-chat-response-command/apply-chat-response-command.ts +++ b/apps/editor/src/commands/apply-chat-response-command/apply-chat-response-command.ts @@ -91,8 +91,9 @@ export const apply_chat_response_command = (params: { response: chat_response, is_single_root_folder_workspace }) + const is_relevant_files = clipboard_items.some( - (item) => item.type == 'relevant-files' + (item) => item.type == 'relevant-files' || item.type == 'subtasks' ) if (response_preview_promise_resolve && !is_relevant_files) { @@ -188,7 +189,7 @@ export const apply_chat_response_command = (params: { command: 'HIDE_PROGRESS' }) - if (preview_data) { + if (preview_data && preview_data.original_states.length > 0) { let created_at_for_preview = args?.created_at if (!args?.files_with_content) { let total_lines_added = 0 diff --git a/apps/editor/src/commands/apply-chat-response-command/response-processor.ts b/apps/editor/src/commands/apply-chat-response-command/response-processor.ts index dffa36bc8..da45c4ec4 100644 --- a/apps/editor/src/commands/apply-chat-response-command/response-processor.ts +++ b/apps/editor/src/commands/apply-chat-response-command/response-processor.ts @@ -8,7 +8,8 @@ import { parse_response, FileItem, DiffItem, - RelevantFilesItem + RelevantFilesItem, + SubtasksItem } from './utils/clipboard-parser' import { create_safe_path } from '@/utils/path-sanitizer' import { dictionary } from '@shared/constants/dictionary' @@ -29,6 +30,7 @@ import { WorkspaceProvider } from '@/context/providers/workspace/workspace-provi import { natural_sort } from '@/utils/natural-sort' import { t } from '@/i18n' import { is_truncation_line } from './utils/edit-formats/truncations' +import { TasksUtils } from '@/utils/tasks-utils' export type PreviewData = { original_states: OriginalFileState[] @@ -108,87 +110,299 @@ export const process_chat_response = async ( is_single_root_folder_workspace }) - if (clipboard_items.some((item) => item.type == 'relevant-files')) { - const relevant_files_item = clipboard_items.find( - (item) => item.type == 'relevant-files' - ) as RelevantFilesItem - - const current_checked_files = - workspace_provider.get_export_state().regular.checked_files - - const workspace_roots = workspace_provider.get_workspace_roots() - const all_paths_to_process = new Set(relevant_files_item.file_paths) - - const files_for_modal = ( - await Promise.all( - Array.from(all_paths_to_process).map(async (rel_path) => { - let absolute_path: string | undefined - for (const root of workspace_roots) { - const potential = path.join(root, rel_path) - if (fs.existsSync(potential)) { - absolute_path = potential - break - } - } + // Check for single subtask scenario alongside code edits + const subtasks_item = clipboard_items.find( + (item) => item.type == 'subtasks' + ) as SubtasksItem | undefined + + const has_code = clipboard_items.some( + (item) => + item.type == 'diff' || + item.type == 'file' || + item.type == 'code-at-cursor' + ) + + let extracted_commit_message: string | undefined + + if (subtasks_item && subtasks_item.subtasks.length === 1 && has_code) { + extracted_commit_message = subtasks_item.subtasks[0].commit_message + // Remove subtasks item so it is ignored by handle_meta_items + clipboard_items = clipboard_items.filter((item) => item !== subtasks_item) + } + + // 1. Handle Meta Items (Relevant Files / Subtasks) + await handle_meta_items( + clipboard_items, + context, + panel_provider, + workspace_provider, + is_single_root_folder_workspace + ) + + // 2. Handle Code Items (Diffs / Files / Code-at-cursor) + const result = await handle_code_items({ + clipboard_items, + chat_response, + context, + panel_provider, + args, + is_single_root_folder_workspace, + on_progress + }) + + // 3. Apply extracted commit message if operation succeeded + if (result && extracted_commit_message) { + panel_provider.send_message({ + command: 'SET_ACTIVE_COMMIT_MESSAGE', + commit_message: extracted_commit_message + } as any) + } - let token_count: number | undefined - if (absolute_path) { - const count = - await workspace_provider.calculate_file_tokens(absolute_path) - token_count = count.total + return result +} + +const handle_meta_items = async ( + clipboard_items: any[], + context: vscode.ExtensionContext, + panel_provider: PanelProvider, + workspace_provider: WorkspaceProvider, + is_single_root_folder_workspace: boolean +): Promise => { + const relevant_files_item = clipboard_items.find( + (item) => item.type == 'relevant-files' + ) as RelevantFilesItem | undefined + const subtasks_item = clipboard_items.find( + (item) => item.type == 'subtasks' + ) as SubtasksItem | undefined + + if (!relevant_files_item && !subtasks_item) { + return + } + + const current_checked_files = + workspace_provider.get_export_state().regular.checked_files + + const workspace_roots = workspace_provider.get_workspace_roots() + + const all_paths_to_process = new Set() + if (relevant_files_item) { + relevant_files_item.file_paths.forEach((p) => all_paths_to_process.add(p)) + } + if (subtasks_item) { + subtasks_item.subtasks.forEach((st: any) => + st.files.forEach((f: string) => all_paths_to_process.add(f)) + ) + } + + const files_for_modal = ( + await Promise.all( + Array.from(all_paths_to_process).map(async (rel_path) => { + let absolute_path: string | undefined + for (const root of workspace_roots) { + const potential = path.join(root, rel_path) + if (fs.existsSync(potential)) { + absolute_path = potential + break } + } + + let token_count: number | undefined + if (absolute_path) { + const count = + await workspace_provider.calculate_file_tokens(absolute_path) + token_count = count.total + } + + return { + file_path: absolute_path, + relative_path: rel_path, + token_count + } + }) + ) + ).filter( + ( + f + ): f is { + file_path: string + relative_path: string + token_count: number + } => !!f.file_path + ) + + files_for_modal.sort((a, b) => natural_sort(a.relative_path, b.relative_path)) + + panel_provider.send_message({ + command: 'SHOW_RELEVANT_FILES_MODAL', + files: files_for_modal + }) + + const selected_files = await new Promise((resolve) => { + panel_provider.relevant_files_choice_resolver = resolve + }) + + if (selected_files) { + const shared_context_state = panel_provider.shared_context_state + const was_frf = workspace_provider.is_frf_mode + + if (was_frf) { + shared_context_state.switch_context_state(false) + } + + if (subtasks_item) { + const is_path_match = (absolute: string, relative: string) => { + const norm_abs = absolute.replace(/\\/g, '/') + const norm_rel = relative.replace(/\\/g, '/') + return norm_abs.endsWith(norm_rel) || norm_abs.includes(norm_rel) + } + + const tasks_array = subtasks_item.subtasks.map( + (st: any, index: number) => { + const raw_files = Array.isArray(st.files) ? st.files : [] + // Ensure we only store files in the task that the user actually approved in the modal + const approved_files = raw_files.filter((rel_f: string) => + selected_files.some((sf) => is_path_match(sf, rel_f)) + ) + + const task_files = approved_files.map((rel_f: string) => { + const matched_modal_file = files_for_modal.find((f) => + is_path_match(f.file_path, rel_f) + ) + return { + path: rel_f, + tokens: matched_modal_file?.token_count + } + }) return { - file_path: absolute_path, - relative_path: rel_path, - token_count + text: ( + st.instruction || + st.description || + st.title || + st.text || + 'Execute subtask' + ) + .toString() + .trim(), + commit_message: st.commit_message, + is_checked: false, + created_at: Date.now() + index, + files: task_files } - }) + } ) - ).filter( - ( - f - ): f is { - file_path: string - relative_path: string - token_count: number - } => !!f.file_path - ) - files_for_modal.sort((a, b) => - natural_sort(a.relative_path, b.relative_path) - ) + if (tasks_array.length > 1) { + // Only save to the task list if there are multiple subtasks + const default_workspace = + vscode.workspace.workspaceFolders![0].uri.fsPath - panel_provider.send_message({ - command: 'SHOW_RELEVANT_FILES_MODAL', - files: files_for_modal - }) + // Use TasksUtils instead of workspaceState to prevent desync with deletion logic + const tasks_record = TasksUtils.load_all(context) - const selected_files = await new Promise( - (resolve) => { - panel_provider.relevant_files_choice_resolver = resolve - } - ) + // Append new tasks to existing ones to prevent data loss + tasks_record[default_workspace] = [ + ...(tasks_record[default_workspace] || []), + ...tasks_array + ] - if (selected_files) { - const shared_context_state = panel_provider.shared_context_state - const was_frf = workspace_provider.is_frf_mode + TasksUtils.save_all({ context, tasks: tasks_record }) - if (was_frf) { - shared_context_state.switch_context_state(false) - } + panel_provider.send_message({ + command: 'TASKS', + tasks: tasks_record as any + }) - const presented_files = files_for_modal.map((f) => f.file_path) + if (was_frf) { + shared_context_state.switch_context_state(true) + } + panel_provider.send_message({ command: 'RETURN_HOME' }) + panel_provider.send_message({ + command: 'SHOW_AUTO_CLOSING_MODAL', + title: 'Subtasks saved. Select one to start.', + type: 'success' + }) + } else if (tasks_array.length === 1) { + const first_task = tasks_array[0] + + const approved_first_task_absolute_paths = selected_files.filter((sf) => + first_task.files.some((rel_f: any) => is_path_match(sf, rel_f.path)) + ) + + const presented_files = files_for_modal.map((f) => f.file_path) + const filtered_current_files = current_checked_files.filter( + (f) => !presented_files.includes(f) + ) + const merged_files = Array.from( + new Set([ + ...filtered_current_files, + ...approved_first_task_absolute_paths + ]) + ) + + await workspace_provider.set_checked_files(merged_files) + + panel_provider.edit_context_instructions.instructions[ + panel_provider.edit_context_instructions.active_index + ] = first_task.text + panel_provider.caret_position = first_task.text.length + + panel_provider.send_message({ + command: 'INSTRUCTIONS', + ask_about_context: panel_provider.ask_about_context_instructions, + edit_context: panel_provider.edit_context_instructions, + no_context: panel_provider.no_context_instructions, + code_at_cursor: panel_provider.code_at_cursor_instructions, + find_relevant_files: panel_provider.find_relevant_files_instructions, + caret_position: panel_provider.caret_position + }) + + if (first_task.commit_message) { + panel_provider.send_message({ + command: 'SET_ACTIVE_COMMIT_MESSAGE', + commit_message: first_task.commit_message + } as any) + } + if (was_frf) { + shared_context_state.switch_context_state(true) + } + + panel_provider.send_message({ + command: 'SHOW_AUTO_CLOSING_MODAL', + title: 'Subtask ready.', + type: 'success' + }) + + await panel_provider.switch_to_edit_context() + panel_provider.send_context_files() + } + } else { + const presented_files = files_for_modal.map((f) => f.file_path) const filtered_current_files = current_checked_files.filter( (f) => !presented_files.includes(f) ) - const merged_files = Array.from( new Set([...filtered_current_files, ...selected_files]) ) await workspace_provider.set_checked_files(merged_files) + // Clear the old prompt from cache + panel_provider.edit_context_instructions.instructions[ + panel_provider.edit_context_instructions.active_index + ] = '' + panel_provider.caret_position = 0 + + panel_provider.send_message({ + command: 'INSTRUCTIONS', + ask_about_context: panel_provider.ask_about_context_instructions, + edit_context: panel_provider.edit_context_instructions, + no_context: panel_provider.no_context_instructions, + code_at_cursor: panel_provider.code_at_cursor_instructions, + find_relevant_files: panel_provider.find_relevant_files_instructions, + caret_position: panel_provider.caret_position + }) + if (was_frf) { shared_context_state.switch_context_state(true) } @@ -200,10 +414,30 @@ export const process_chat_response = async ( }) await panel_provider.switch_to_edit_context() + panel_provider.send_context_files() } + } +} - return null - } else if (clipboard_items.some((item) => item.type == 'diff')) { +const handle_code_items = async ({ + clipboard_items, + chat_response, + context, + panel_provider, + args, + is_single_root_folder_workspace, + on_progress +}: { + clipboard_items: any[] + chat_response: string + context: vscode.ExtensionContext + panel_provider: PanelProvider + args: CommandArgs | undefined + is_single_root_folder_workspace: boolean + on_progress: (progress: number) => void +}): Promise => { + // Logic for applying code, diffs, etc. + if (clipboard_items.some((item) => item.type == 'diff')) { const patches = clipboard_items.filter( (item): item is DiffItem => item.type == 'diff' ) @@ -325,6 +559,7 @@ export const process_chat_response = async ( return null } else { + // Check for code-at-cursor if (clipboard_items.some((item) => item.type == 'code-at-cursor')) { const completion = clipboard_items.find( (item) => item.type == 'code-at-cursor' diff --git a/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/clipboard-parser.ts b/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/clipboard-parser.ts index 5c8765ffd..7cfd72946 100644 --- a/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/clipboard-parser.ts +++ b/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/clipboard-parser.ts @@ -4,6 +4,10 @@ import { parse_multiple_files, parse_relevant_files } from './parsers' +import { + parse_subtask_directives, + SubtaskDirective +} from './parsers/subtask-directives-parser' export type FileItem = { type: 'file' @@ -36,6 +40,11 @@ export type RelevantFilesItem = { file_paths: string[] } +export type SubtasksItem = { + type: 'subtasks' + subtasks: SubtaskDirective[] +} + export type TextItem = { type: 'text' content: string @@ -54,6 +63,7 @@ export type ClipboardItem = | TextItem | InlineFileItem | RelevantFilesItem + | SubtasksItem export const extract_workspace_and_path = (params: { raw_file_path: string @@ -82,21 +92,32 @@ export const parse_response = (params: { const is_single_root_folder_workspace = params.is_single_root_folder_workspace ?? true + // Standardize the response by cleaning up common code block formatting issues + const processed_response = params.response.replace(/``````/g, '```\n```') + + const items: ClipboardItem[] = [] + + // Use processed_response for all parsers to ensure consistency and robustness const code_at_cursor_items = parse_code_at_cursor({ - response: params.response, + response: processed_response, is_single_root_folder_workspace }) if (code_at_cursor_items && code_at_cursor_items.length > 0) { - return code_at_cursor_items + items.push(...code_at_cursor_items) } - const relevant_files = parse_relevant_files({ response: params.response }) - if (relevant_files) { - return [relevant_files] + const subtasks = parse_subtask_directives( + processed_response + ) as SubtasksItem[] + if (subtasks && subtasks.length > 0) { + items.push(...subtasks) } - const processed_response = params.response.replace(/``````/g, '```\n```') + const relevant_files = parse_relevant_files({ response: processed_response }) + if (relevant_files) { + items.push(relevant_files) + } const hunk_header_regex = /^(@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s+@@)/m const diff_header_regex = /^---\s+.+\n\+\+\+\s+.+/m @@ -114,14 +135,15 @@ export const parse_response = (params: { is_single_root: is_single_root_folder_workspace }) if (patches_or_text.length) { - return patches_or_text + items.push(...patches_or_text) } } - const items = parse_multiple_files({ + const file_items = parse_multiple_files({ response: processed_response, is_single_root_folder_workspace }) + items.push(...file_items) return items } diff --git a/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/relevant-files-parser.ts b/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/relevant-files-parser.ts index 79460c7f5..82d670446 100644 --- a/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/relevant-files-parser.ts +++ b/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/relevant-files-parser.ts @@ -3,8 +3,9 @@ import { RelevantFilesItem } from '../clipboard-parser' export const parse_relevant_files = (params: { response: string }): RelevantFilesItem | null => { - const trimmed_response = params.response.trim() - const lines = trimmed_response.split('\n') + // Strip out everything inside markdown code blocks to prevent false positive keyword matches + const clean_response = params.response.replace(/```[\s\S]*?```/g, '') + const lines = clean_response.trim().split('\n') if (lines.length == 0) { return null diff --git a/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/subtask-directives-parser.ts b/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/subtask-directives-parser.ts new file mode 100644 index 000000000..07ee64b14 --- /dev/null +++ b/apps/editor/src/commands/apply-chat-response-command/utils/clipboard-parser/parsers/subtask-directives-parser.ts @@ -0,0 +1,102 @@ +export interface SubtaskDirective { + instruction: string + commit_message?: string + files: string[] +} + +export const parse_subtask_directives = (response: string) => { + const items: any[] = [] + const subtasks: SubtaskDirective[] = [] + + // Strip out everything inside markdown code blocks to prevent false positive keyword matches + const clean_response = response.replace(/```[\s\S]*?```/g, '') + + // 1. Try old XML format (for backward compatibility with history) + const xml_subtasks_match = clean_response.match( + /([\s\S]*?)<\/subtasks>/i + ) + if (xml_subtasks_match) { + const subtasks_content = xml_subtasks_match[1] + const subtask_regex = /([\s\S]*?)<\/subtask>/gi + let subtask_match + + while ((subtask_match = subtask_regex.exec(subtasks_content)) !== null) { + const subtask_content = subtask_match[1] + + const instruction_match = subtask_content.match( + /([\s\S]*?)<\/instruction>/i + ) + const instruction = instruction_match ? instruction_match[1].trim() : '' + + const commit_match = subtask_content.match( + /([\s\S]*?)<\/commit_message>/i + ) + const commit = commit_match ? commit_match[1].trim() : '' + + const files_match = subtask_content.match(/([\s\S]*?)<\/files>/i) + const files: string[] = [] + if (files_match) { + const file_regex = /([\s\S]*?)<\/file>/gi + let file_match + while ((file_match = file_regex.exec(files_match[1])) !== null) { + files.push(file_match[1].trim()) + } + } + + if (instruction || files.length > 0) { + subtasks.push({ instruction, commit_message: commit, files }) + } + } + } else { + // 2. Try new Markdown format + const md_subtasks_match = clean_response.match( + /\*\*Subtasks:\*\*([\s\S]+)/i + ) + if (md_subtasks_match) { + const content = md_subtasks_match[1] + // Split safely by LLM generated headings (### Subtask 1, **Subtask 1**, or 1. **Instruction:**) + const blocks = content.split( + /(?:###\s*Subtask\s*\d+|\*\*Subtask\s*\d+\*\*|(?:\n|^)\d+\.\s*(?=\*\*Instruction:\*\*))/i + ) + + for (const block of blocks) { + if (!block.trim()) continue + + const instruction_match = block.match( + /\*\*Instruction:\*\*\s*([\s\S]*?)(?=\n\*\*Commit message:\*\*|\n\*\*Files:\*\*|$)/i + ) + const commit_match = block.match( + /\*\*Commit message:\*\*\s*([\s\S]*?)(?=\n\*\*Files:\*\*|$)/i + ) + const files_match = block.match(/\*\*Files:\*\*([\s\S]*)/i) + + const instruction = instruction_match ? instruction_match[1].trim() : '' + const commit = commit_match ? commit_match[1].trim() : '' + + const files: string[] = [] + if (files_match) { + // Extracts paths from standard markdown lists like: - `src/file.ts` or * src/file.ts + const file_regex = /^[\s]*[-*]\s*`?([^`\n]+)`?/gm + let file_match + while ((file_match = file_regex.exec(files_match[1])) !== null) { + const path = file_match[1].trim() + if (path) files.push(path) + } + } + + if (instruction || files.length > 0) { + subtasks.push({ instruction, commit_message: commit, files }) + } + } + } + } + + if (subtasks.length > 0) { + items.push({ + type: 'subtasks', + subtasks + }) + } + + return items +} diff --git a/apps/editor/src/constants/instructions.ts b/apps/editor/src/constants/instructions.ts index 6ec3df465..ffc5809c5 100644 --- a/apps/editor/src/constants/instructions.ts +++ b/apps/editor/src/constants/instructions.ts @@ -39,6 +39,22 @@ Your response must begin with "**Relevant files:**", then list paths one under a - \`src/index.ts\` - \`src/hello.ts\` - \`src/welcome.ts\` + +If the task is complex and requires multiple logical steps, break it down into a plan formatted strictly as a Markdown list of subtasks. For simple requests, you can provide just a single subtask. Do NOT use XML. Use the following exact headings: + +**Subtasks:** + +### Subtask 1 +**Instruction:** Implement the greeting logic in hello.ts. Ensure you handle undefined inputs. +**Commit message:** feat: add greeting logic and handle undefined inputs +**Files:** +- \`src/hello.ts\` + +### Subtask 2 +**Instruction:** Export the new functions in index.ts so they are available to other modules. +**Commit message:** feat: export greeting functions in index.ts +**Files:** +- \`src/index.ts\` ` export const find_relevant_files_format_for_panel = ` @@ -51,6 +67,22 @@ Your response must begin with "**Relevant files:**", then list paths one under a - \`src/welcome.ts\` These files contain the core greeting logic and module exports. + +If the task is complex and requires multiple logical steps, break it down into a plan formatted strictly as a Markdown list of subtasks. For simple requests, you can provide just a single subtask. Do NOT use XML. Use the following exact headings: + +**Subtasks:** + +### Subtask 1 +**Instruction:** Implement the greeting logic in hello.ts. Ensure you handle undefined inputs. +**Commit message:** feat: add greeting logic and handle undefined inputs +**Files:** +- \`src/hello.ts\` + +### Subtask 2 +**Instruction:** Export the new functions in index.ts so they are available to other modules. +**Commit message:** feat: export greeting functions in index.ts +**Files:** +- \`src/index.ts\` ` export const voice_input_instructions = diff --git a/apps/editor/src/constants/state-keys.ts b/apps/editor/src/constants/state-keys.ts index 3ce884e57..4cfc5adc1 100644 --- a/apps/editor/src/constants/state-keys.ts +++ b/apps/editor/src/constants/state-keys.ts @@ -57,6 +57,8 @@ export const HISTORY_FIND_RELEVANT_FILES_STATE_KEY = export const HISTORY_NO_CONTEXT_STATE_KEY = 'history-no-context' export const FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY = 'find-relevant-files-shrink-source-code' +export const FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY = + 'find-relevant-files-only-file-tree' export const RECENTLY_USED_CODE_AT_CURSOR_CONFIG_IDS_STATE_KEY = 'recently-used-code-at-cursor-config-ids' diff --git a/apps/editor/src/context/providers/workspace/modules/token-calculator.ts b/apps/editor/src/context/providers/workspace/modules/token-calculator.ts index 620248555..f17d34e15 100644 --- a/apps/editor/src/context/providers/workspace/modules/token-calculator.ts +++ b/apps/editor/src/context/providers/workspace/modules/token-calculator.ts @@ -6,7 +6,7 @@ import { Logger } from '@shared/utils/logger' import type { IWorkspaceProvider } from '../workspace-provider' import { shrink_file } from '@/context/utils/shrink-file' -type TokenData = [number, number, number] +type TokenData = [number, number, number, number] type TokenCacheNode = { [key: string]: TokenCacheNode | TokenData @@ -28,11 +28,14 @@ const TOKEN_CACHE_FILE_NAME = 'token-counts-cache.json' export class TokenCalculator implements vscode.Disposable { private _file_token_counts: Map = new Map() private _file_shrink_token_counts: Map = new Map() + private _file_path_token_counts: Map = new Map() private _directory_token_counts: Map = new Map() private _directory_shrink_token_counts: Map = new Map() + private _directory_path_token_counts: Map = new Map() private _directory_selected_token_counts: Map = new Map() private _directory_selected_shrink_token_counts: Map = new Map() + private _directory_selected_path_token_counts: Map = new Map() private _token_cache: TokenCountsCache = {} private _session_cache: TokenCountsCache = {} private _token_cache_update_timeout: NodeJS.Timeout | null = null @@ -145,7 +148,8 @@ export class TokenCalculator implements vscode.Disposable { relative_path: string, mtime: number, token_count: number, - shrink_token_count: number + shrink_token_count: number, + path_token_count: number ) { if (!this._session_cache[workspace_root]) { this._session_cache[workspace_root] = { @@ -156,7 +160,8 @@ export class TokenCalculator implements vscode.Disposable { this._session_cache[workspace_root].files[relative_path] = [ mtime, token_count, - shrink_token_count + shrink_token_count, + path_token_count ] this._session_cache[workspace_root].modified_at = Date.now() this._update_token_counts_cache() @@ -198,18 +203,22 @@ export class TokenCalculator implements vscode.Disposable { if (!workspace_root) { this._file_token_counts.delete(changed_file_path) this._file_shrink_token_counts.delete(changed_file_path) + this._file_path_token_counts.delete(changed_file_path) return } this._file_token_counts.delete(changed_file_path) this._file_shrink_token_counts.delete(changed_file_path) + this._file_path_token_counts.delete(changed_file_path) let dir_path = path.dirname(changed_file_path) while (dir_path.startsWith(workspace_root)) { this._directory_token_counts.delete(dir_path) this._directory_shrink_token_counts.delete(dir_path) + this._directory_path_token_counts.delete(dir_path) this._directory_selected_token_counts.delete(dir_path) this._directory_selected_shrink_token_counts.delete(dir_path) + this._directory_selected_path_token_counts.delete(dir_path) dir_path = path.dirname(dir_path) } } @@ -217,50 +226,89 @@ export class TokenCalculator implements vscode.Disposable { public invalidate_directory_counts(dir_path: string) { this._directory_token_counts.delete(dir_path) this._directory_shrink_token_counts.delete(dir_path) + this._directory_path_token_counts.delete(dir_path) this._directory_selected_token_counts.delete(dir_path) this._directory_selected_shrink_token_counts.delete(dir_path) + this._directory_selected_path_token_counts.delete(dir_path) } public invalidate_directory_selected_count(dir_path: string) { this._directory_selected_token_counts.delete(dir_path) this._directory_selected_shrink_token_counts.delete(dir_path) + this._directory_selected_path_token_counts.delete(dir_path) } public clear_caches() { this._file_token_counts.clear() this._file_shrink_token_counts.clear() + this._file_path_token_counts.clear() this._directory_token_counts.clear() this._directory_shrink_token_counts.clear() + this._directory_path_token_counts.clear() this._directory_selected_token_counts.clear() this._directory_selected_shrink_token_counts.clear() + this._directory_selected_path_token_counts.clear() } public clear_selected_counts() { this._directory_selected_token_counts.clear() this._directory_selected_shrink_token_counts.clear() + this._directory_selected_path_token_counts.clear() + } + + public is_directory_cached(dir_path: string): boolean { + return this._directory_token_counts.has(dir_path) + } + + public is_file_cached(file_path: string): boolean { + return this._file_token_counts.has(file_path) } public get_cached_token_count( file_path: string - ): { total: number; shrink: number } | undefined { + ): { total: number; shrink: number; path: number } | undefined { const total = this._file_token_counts.get(file_path) const shrink = this._file_shrink_token_counts.get(file_path) - if (total !== undefined && shrink !== undefined) { - return { total, shrink } + const path_tokens = this._file_path_token_counts.get(file_path) + if ( + total !== undefined && + shrink !== undefined && + path_tokens !== undefined + ) { + return { total, shrink, path: path_tokens } } return undefined } + private _calculate_path_tokens( + workspace_root: string | undefined, + file_path: string + ): number { + let display_path = file_path + if (workspace_root) { + display_path = path + .relative(workspace_root, file_path) + .replace(/\\/g, '/') + if (this._provider.get_workspace_roots().length > 1) { + const workspace_name = this._provider.get_workspace_name(workspace_root) + display_path = `${workspace_name}/${display_path}` + } + } + return Math.floor(display_path.length / 4) + } + public async calculate_file_tokens( file_path: string - ): Promise<{ total: number; shrink: number }> { + ): Promise<{ total: number; shrink: number; path: number }> { if ( this._file_token_counts.has(file_path) && - this._file_shrink_token_counts.has(file_path) + this._file_shrink_token_counts.has(file_path) && + this._file_path_token_counts.has(file_path) ) { return { total: this._file_token_counts.get(file_path)!, - shrink: this._file_shrink_token_counts.get(file_path)! + shrink: this._file_shrink_token_counts.get(file_path)!, + path: this._file_path_token_counts.get(file_path)! } } @@ -268,6 +316,11 @@ export class TokenCalculator implements vscode.Disposable { const range = this._provider.get_range(file_path) let mtime = 0 + const path_token_count = this._calculate_path_tokens( + workspace_root, + file_path + ) + if (workspace_root && !range) { try { const stats = await fs.promises.stat(file_path) @@ -282,22 +335,30 @@ export class TokenCalculator implements vscode.Disposable { if ( cached_file && Array.isArray(cached_file) && - cached_file[0] == mtime + cached_file[0] == mtime && + cached_file[2] !== undefined ) { const tokens = cached_file[1] const shrink_tokens = cached_file[2] + const cached_path_tokens = cached_file[3] ?? path_token_count this._file_token_counts.set(file_path, tokens) this._file_shrink_token_counts.set(file_path, shrink_tokens) + this._file_path_token_counts.set(file_path, cached_path_tokens) this._update_session_cache( workspace_root, relative_path, mtime, tokens, - shrink_tokens + shrink_tokens, + cached_path_tokens ) - return { total: tokens, shrink: shrink_tokens } + return { + total: tokens, + shrink: shrink_tokens, + path: cached_path_tokens + } } } catch { // Continue to calculate if stat fails @@ -339,6 +400,7 @@ export class TokenCalculator implements vscode.Disposable { this._file_token_counts.set(file_path, token_count) this._file_shrink_token_counts.set(file_path, shrink_token_count) + this._file_path_token_counts.set(file_path, path_token_count) if (workspace_root && !range && mtime > 0) { if ( @@ -358,7 +420,8 @@ export class TokenCalculator implements vscode.Disposable { this._token_cache[workspace_root].files[relative_path] = [ mtime, token_count, - shrink_token_count + shrink_token_count, + path_token_count ] this._token_cache[workspace_root].modified_at = Date.now() @@ -367,31 +430,38 @@ export class TokenCalculator implements vscode.Disposable { relative_path, mtime, token_count, - shrink_token_count + shrink_token_count, + path_token_count ) } - return { total: token_count, shrink: shrink_token_count } + return { + total: token_count, + shrink: shrink_token_count, + path: path_token_count + } } catch (error) { Logger.error({ function_name: 'calculate_file_tokens', message: `Error calculating tokens for ${file_path}`, data: error }) - return { total: 0, shrink: 0 } + return { total: 0, shrink: 0, path: path_token_count } } } public async calculate_directory_tokens( dir_path: string - ): Promise<{ total: number; shrink: number }> { + ): Promise<{ total: number; shrink: number; path: number }> { if ( this._directory_token_counts.has(dir_path) && - this._directory_shrink_token_counts.has(dir_path) + this._directory_shrink_token_counts.has(dir_path) && + this._directory_path_token_counts.has(dir_path) ) { return { total: this._directory_token_counts.get(dir_path)!, - shrink: this._directory_shrink_token_counts.get(dir_path)! + shrink: this._directory_shrink_token_counts.get(dir_path)!, + path: this._directory_path_token_counts.get(dir_path)! } } @@ -399,7 +469,7 @@ export class TokenCalculator implements vscode.Disposable { const workspace_root = this._provider.get_workspace_root_for_file(dir_path) if (!workspace_root) { - return { total: 0, shrink: 0 } + return { total: 0, shrink: 0, path: 0 } } const relative_dir_path = path.relative(workspace_root, dir_path) @@ -410,7 +480,8 @@ export class TokenCalculator implements vscode.Disposable { ) { this._directory_token_counts.set(dir_path, 0) this._directory_shrink_token_counts.set(dir_path, 0) - return { total: 0, shrink: 0 } + this._directory_path_token_counts.set(dir_path, 0) + return { total: 0, shrink: 0, path: 0 } } const entries = await fs.promises.readdir(dir_path, { @@ -418,6 +489,7 @@ export class TokenCalculator implements vscode.Disposable { }) let total_tokens = 0 let total_shrink_tokens = 0 + let total_path_tokens = 0 for (const entry of entries) { const full_path = path.join(dir_path, entry.name) @@ -458,53 +530,63 @@ export class TokenCalculator implements vscode.Disposable { } if (is_directory && !is_broken_link) { - // Recurse into subdirectory (including resolved symlinks that are directories) + // Recurse into subdirectory const counts = await this.calculate_directory_tokens(full_path) total_tokens += counts.total total_shrink_tokens += counts.shrink + total_path_tokens += counts.path } else if ( entry.isFile() || (is_symbolic_link && !is_broken_link && !is_directory) ) { - // Add file tokens (including resolved symlinks that are files) + // Add file tokens const counts = await this.calculate_file_tokens(full_path) total_tokens += counts.total total_shrink_tokens += counts.shrink + total_path_tokens += counts.path } } this._directory_token_counts.set(dir_path, total_tokens) this._directory_shrink_token_counts.set(dir_path, total_shrink_tokens) + this._directory_path_token_counts.set(dir_path, total_path_tokens) - return { total: total_tokens, shrink: total_shrink_tokens } + return { + total: total_tokens, + shrink: total_shrink_tokens, + path: total_path_tokens + } } catch (error) { Logger.error({ function_name: 'calculate_directory_tokens', message: `Error calculating tokens for directory ${dir_path}`, data: error }) - return { total: 0, shrink: 0 } + return { total: 0, shrink: 0, path: 0 } } } public async calculate_directory_selected_tokens( dir_path: string - ): Promise<{ total: number; shrink: number }> { + ): Promise<{ total: number; shrink: number; path: number }> { if (!dir_path) { - return { total: 0, shrink: 0 } + return { total: 0, shrink: 0, path: 0 } } if ( this._directory_selected_token_counts.has(dir_path) && - this._directory_selected_shrink_token_counts.has(dir_path) + this._directory_selected_shrink_token_counts.has(dir_path) && + this._directory_selected_path_token_counts.has(dir_path) ) { return { total: this._directory_selected_token_counts.get(dir_path)!, - shrink: this._directory_selected_shrink_token_counts.get(dir_path)! + shrink: this._directory_selected_shrink_token_counts.get(dir_path)!, + path: this._directory_selected_path_token_counts.get(dir_path)! } } let selected_tokens = 0 let selected_shrink_tokens = 0 + let selected_path_tokens = 0 try { const workspace_root = @@ -514,7 +596,7 @@ export class TokenCalculator implements vscode.Disposable { function_name: 'calculate_directory_selected_tokens', message: `No workspace root found for directory ${dir_path}` }) - return { total: 0, shrink: 0 } + return { total: 0, shrink: 0, path: 0 } } const relative_dir_path = path.relative(workspace_root, dir_path) @@ -525,7 +607,8 @@ export class TokenCalculator implements vscode.Disposable { ) { this._directory_selected_token_counts.set(dir_path, 0) this._directory_selected_shrink_token_counts.set(dir_path, 0) - return { total: 0, shrink: 0 } + this._directory_selected_path_token_counts.set(dir_path, 0) + return { total: 0, shrink: 0, path: 0 } } const entries = await fs.promises.readdir(dir_path, { @@ -569,17 +652,20 @@ export class TokenCalculator implements vscode.Disposable { const counts = await this.calculate_directory_tokens(full_path) selected_tokens += counts.total selected_shrink_tokens += counts.shrink + selected_path_tokens += counts.path } else if (this._provider.is_partially_checked(full_path)) { const counts = await this.calculate_directory_selected_tokens(full_path) selected_tokens += counts.total selected_shrink_tokens += counts.shrink + selected_path_tokens += counts.path } } else { if (checkbox_state === vscode.TreeItemCheckboxState.Checked) { const counts = await this.calculate_file_tokens(full_path) selected_tokens += counts.total selected_shrink_tokens += counts.shrink + selected_path_tokens += counts.path } } } @@ -589,51 +675,50 @@ export class TokenCalculator implements vscode.Disposable { message: `Error calculating selected tokens for dir ${dir_path}`, data: error }) - return { total: 0, shrink: 0 } + return { total: 0, shrink: 0, path: 0 } } this._directory_selected_token_counts.set(dir_path, selected_tokens) this._directory_selected_shrink_token_counts.set( dir_path, selected_shrink_tokens ) - return { total: selected_tokens, shrink: selected_shrink_tokens } + this._directory_selected_path_token_counts.set( + dir_path, + selected_path_tokens + ) + return { + total: selected_tokens, + shrink: selected_shrink_tokens, + path: selected_path_tokens + } } public async get_checked_files_token_count(options?: { exclude_file_path?: string - }): Promise<{ total: number; shrink: number }> { - const checked_files = this._provider.get_checked_files() - const result = { total: 0, shrink: 0 } - - for (const file_path of checked_files) { - try { - if ( - options?.exclude_file_path && - file_path == options.exclude_file_path - ) { - continue - } + }): Promise<{ total: number; shrink: number; path: number }> { + let result_total = 0 + let result_shrink = 0 + let result_path = 0 + + for (const root of this._provider.get_workspace_roots()) { + const counts = await this.calculate_directory_selected_tokens(root) + result_total += counts.total + result_shrink += counts.shrink + result_path += counts.path + } - if (fs.statSync(file_path).isFile()) { - if (this._file_token_counts.has(file_path)) { - result.total += this._file_token_counts.get(file_path)! - result.shrink += this._file_shrink_token_counts.get(file_path)! - } else { - const count = await this.calculate_file_tokens(file_path) - result.total += count.total - result.shrink += count.shrink - } - } - } catch (error) { - Logger.error({ - function_name: 'get_checked_files_token_count', - message: `Error accessing file ${file_path} for token count`, - data: error - }) - } + if ( + options?.exclude_file_path && + this._provider.get_check_state(options.exclude_file_path) === + vscode.TreeItemCheckboxState.Checked + ) { + const counts = await this.calculate_file_tokens(options.exclude_file_path) + result_total = Math.max(0, result_total - counts.total) + result_shrink = Math.max(0, result_shrink - counts.shrink) + result_path = Math.max(0, result_path - counts.path) } - return result + return { total: result_total, shrink: result_shrink, path: result_path } } public dispose() { diff --git a/apps/editor/src/context/providers/workspace/workspace-provider.ts b/apps/editor/src/context/providers/workspace/workspace-provider.ts index 66aaaf152..18116e917 100644 --- a/apps/editor/src/context/providers/workspace/workspace-provider.ts +++ b/apps/editor/src/context/providers/workspace/workspace-provider.ts @@ -7,7 +7,8 @@ import { CONTEXT_CHECKED_TIMESTAMPS_STATE_KEY, CONTEXT_CHECKED_PATHS_FRF_STATE_KEY, CONTEXT_CHECKED_TIMESTAMPS_FRF_STATE_KEY, - RANGES_STATE_KEY + RANGES_STATE_KEY, + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY } from '@/constants/state-keys' import { IGNORE_PATTERNS } from '@/constants/ignore-patterns' import { natural_sort } from '@/utils/natural-sort' @@ -695,35 +696,65 @@ export class WorkspaceProvider element.checkboxState = checkbox_state - const total_token_count = this.use_shrink_token_count - ? element.shrinkTokenCount - : element.tokenCount - const selected_token_count = this.use_shrink_token_count - ? element.selectedShrinkTokenCount - : element.selectedTokenCount + const only_file_tree = this._context.workspaceState.get( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + false + ) + const show_only_path_tokens = this._is_frf_mode && only_file_tree + const show_shrink_tokens = this.use_shrink_token_count - const formatted_total = - total_token_count !== undefined && total_token_count > 0 - ? display_token_count(total_token_count) - : undefined + let active_total = element.tokenCount + let active_selected = element.selectedTokenCount + + if (show_only_path_tokens) { + active_total = element.pathTokenCount + } else if (show_shrink_tokens) { + active_total = element.shrinkTokenCount + } - const formatted_selected = - selected_token_count !== undefined && selected_token_count > 0 - ? display_token_count(selected_token_count) + if (show_only_path_tokens) { + active_selected = element.selectedPathTokenCount + } else if (show_shrink_tokens) { + active_selected = element.selectedShrinkTokenCount + } + + const formatted_total = + element.tokenCount !== undefined && element.tokenCount > 0 + ? display_token_count(element.tokenCount) : undefined let display_description = '' + let tooltip_tokens_info = '' - if (element.isDirectory) { - if (formatted_total) { - if (formatted_selected && selected_token_count! < total_token_count!) { - display_description = `${formatted_total} · ${formatted_selected} selected` - } else { - display_description = formatted_total - } + if (formatted_total) { + display_description = formatted_total + tooltip_tokens_info = `About ${formatted_total} tokens` + + let modified_total_str: string | undefined + if (show_only_path_tokens && element.pathTokenCount !== undefined) { + modified_total_str = display_token_count(element.pathTokenCount) + tooltip_tokens_info = `About ${formatted_total} original tokens (${modified_total_str} Tokens for path only)` + } else if (show_shrink_tokens && element.shrinkTokenCount !== undefined) { + modified_total_str = display_token_count(element.shrinkTokenCount) + tooltip_tokens_info = `About ${formatted_total} original tokens (${modified_total_str} Tokens after shrinking)` + } + + if (modified_total_str) { + display_description += ` (${modified_total_str})` + } + + const formatted_selected = + active_selected !== undefined && active_selected > 0 + ? display_token_count(active_selected) + : undefined + + if ( + element.isDirectory && + formatted_selected && + active_selected! < (active_total ?? 0) + ) { + display_description += ` · ${formatted_selected} selected` } - } else { - display_description = formatted_total ?? '' } if (!element.isDirectory && element.range) { @@ -736,29 +767,32 @@ export class WorkspaceProvider trimmed_description == '' ? undefined : trimmed_description const tooltip_parts = [element.resourceUri.fsPath] - if (formatted_total) { - tooltip_parts.push(`· About ${formatted_total} tokens`) + if (tooltip_tokens_info) { + tooltip_parts.push(`· ${tooltip_tokens_info}`) } - if (element.isDirectory && formatted_selected) { - if ( - total_token_count !== undefined && - selected_token_count == total_token_count - ) { + + if ( + element.isDirectory && + active_selected !== undefined && + active_selected > 0 + ) { + if (active_total !== undefined && active_selected === active_total) { tooltip_parts.push('· Fully selected') } else { - tooltip_parts.push(`· ${formatted_selected} selected`) + const formatted_sel = display_token_count(active_selected) + tooltip_parts.push(`· ${formatted_sel} selected`) } } element.tooltip = tooltip_parts.join(' ') if (element.isWorkspaceRoot) { - // Workspace root tooltip is primarily its name and role, token info is appended if available let root_tooltip = `${element.label} (Workspace Root)` - if (formatted_total) { - root_tooltip += ` • About ${formatted_total} tokens` - if (formatted_selected) { - root_tooltip += ` (${formatted_selected} selected)` + if (tooltip_tokens_info) { + root_tooltip += ` • ${tooltip_tokens_info}` + if (active_selected !== undefined && active_selected > 0) { + const formatted_sel = display_token_count(active_selected) + root_tooltip += ` (${formatted_sel} selected)` } } element.tooltip = root_tooltip @@ -892,8 +926,10 @@ export class WorkspaceProvider false, total_tokens.total, total_tokens.shrink, + total_tokens.path, selected_tokens.total, selected_tokens.shrink, + selected_tokens.path, undefined, true ) @@ -1006,13 +1042,13 @@ export class WorkspaceProvider public get_cached_token_count( file_path: string - ): { total: number; shrink: number } | undefined { + ): { total: number; shrink: number; path: number } | undefined { return this._token_calculator.get_cached_token_count(file_path) } public async calculate_file_tokens( file_path: string - ): Promise<{ total: number; shrink: number }> { + ): Promise<{ total: number; shrink: number; path: number }> { return this._token_calculator.calculate_file_tokens(file_path) } @@ -1158,8 +1194,10 @@ export class WorkspaceProvider false, tokens.total, tokens.shrink, + tokens.path, selected_tokens?.total, selected_tokens?.shrink, + selected_tokens?.path, undefined, false, range @@ -1425,19 +1463,28 @@ export class WorkspaceProvider public get_checked_files(): string[] { return Array.from(this._checked_items.entries()) - .filter( - ([file_path, state]) => - state == vscode.TreeItemCheckboxState.Checked && - fs.existsSync(file_path) && - (fs.lstatSync(file_path).isFile() || - fs.lstatSync(file_path).isSymbolicLink()) && - (() => { - const workspace_root = this.get_workspace_root_for_file(file_path) - return workspace_root - ? !this.is_excluded(path.relative(workspace_root, file_path)) - : false - })() - ) + .filter(([file_path, state]) => { + if (state !== vscode.TreeItemCheckboxState.Checked) return false + + // Fast path: Check caches to avoid slow synchronous fs calls + if (this._token_calculator.is_directory_cached(file_path)) return false + if (this._token_calculator.is_file_cached(file_path)) { + const workspace_root = this.get_workspace_root_for_file(file_path) + return workspace_root + ? !this.is_excluded(path.relative(workspace_root, file_path)) + : false + } + + // Fallback if not cached + if (!fs.existsSync(file_path)) return false + const stat = fs.lstatSync(file_path) + if (!stat.isFile() && !stat.isSymbolicLink()) return false + + const workspace_root = this.get_workspace_root_for_file(file_path) + return workspace_root + ? !this.is_excluded(path.relative(workspace_root, file_path)) + : false + }) .map(([path, _]) => path) } @@ -1701,7 +1748,7 @@ export class WorkspaceProvider public async get_checked_files_token_count(options?: { exclude_file_path?: string - }): Promise<{ total: number; shrink: number }> { + }): Promise<{ total: number; shrink: number; path: number }> { return this._token_calculator.get_checked_files_token_count(options) } @@ -1825,8 +1872,10 @@ export class FileItem extends vscode.TreeItem { public isOpenFile: boolean = false, public tokenCount?: number, public shrinkTokenCount?: number, + public pathTokenCount?: number, public selectedTokenCount?: number, public selectedShrinkTokenCount?: number, + public selectedPathTokenCount?: number, description?: string, public isWorkspaceRoot: boolean = false, public range?: string diff --git a/apps/editor/src/utils/files-collector.ts b/apps/editor/src/utils/files-collector.ts index c1f9b802a..373381916 100644 --- a/apps/editor/src/utils/files-collector.ts +++ b/apps/editor/src/utils/files-collector.ts @@ -24,6 +24,7 @@ export class FilesCollector { additional_paths?: string[] no_context?: boolean shrink?: boolean + only_file_tree?: boolean }): Promise<{ other_files: string; recent_files: string }> { const additional_paths = (params?.additional_paths ?? []).map((p) => { if (this.workspace_roots.length > 0) { @@ -64,6 +65,35 @@ export class FilesCollector { if (stats.isDirectory()) continue + const workspace_root = this._get_workspace_root_for_file(file_path) + + let display_path = file_path.replace(/\\/g, '/') + if (workspace_root) { + const relative_path = path + .relative(workspace_root, file_path) + .replace(/\\/g, '/') + + if (this.workspace_roots.length > 1) { + const workspace_name = + this.workspace_provider.get_workspace_name(workspace_root) + display_path = `${workspace_name}/${relative_path}` + } else { + display_path = relative_path + } + } + + if (params?.only_file_tree) { + const cached_tokens = + this.workspace_provider.get_cached_token_count(file_path) + const token_count = cached_tokens?.total || 0 + const count_str = + token_count >= 1000 + ? `${Number((token_count / 1000).toFixed(1))}k` + : token_count.toString() + collected_text += `- ${display_path} (${count_str})\n` + continue + } + let content = fs.readFileSync(file_path, 'utf8') const range = this.workspace_provider.get_range(file_path) @@ -78,28 +108,6 @@ export class FilesCollector { content = shrink_file(content, path.extname(file_path)) } - const workspace_root = this._get_workspace_root_for_file(file_path) - - if (!workspace_root) { - collected_text += `\n\n\n` - continue - } - - const relative_path = path - .relative(workspace_root, file_path) - .replace(/\\/g, '/') - - // Get the workspace name to prefix the path if there are multiple workspaces - let display_path = relative_path - if (this.workspace_roots.length > 1) { - const workspace_name = - this.workspace_provider.get_workspace_name(workspace_root) - display_path = `${workspace_name}/${relative_path}` - } - collected_text += `\n\n\n` } catch (error) { console.error(`Error reading file ${file_path}:`, error) diff --git a/apps/editor/src/views/panel/backend/message-handlers/handle-copy-prompt.ts b/apps/editor/src/views/panel/backend/message-handlers/handle-copy-prompt.ts index b774b6c63..26dbdca85 100644 --- a/apps/editor/src/views/panel/backend/message-handlers/handle-copy-prompt.ts +++ b/apps/editor/src/views/panel/backend/message-handlers/handle-copy-prompt.ts @@ -27,7 +27,10 @@ import { EDIT_FORMAT_INSTRUCTIONS_BEFORE_AFTER, EDIT_FORMAT_INSTRUCTIONS_DIFF } from '@/constants/edit-format-instructions' -import { FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY } from '@/constants/state-keys' +import { + FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY, + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY +} from '@/constants/state-keys' export const handle_copy_prompt = async (params: { panel_provider: PanelProvider @@ -188,9 +191,16 @@ export const handle_copy_prompt = async (params: { false ) + const only_file_tree = + params.panel_provider.context.workspaceState.get( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + false + ) + const collected = await files_collector.collect_files({ no_context: params.panel_provider.web_prompt_type == 'no-context', - shrink: is_in_find_relevant_files_prompt_type && shrink_source_code + shrink: is_in_find_relevant_files_prompt_type && shrink_source_code, + only_file_tree: is_in_find_relevant_files_prompt_type && only_file_tree }) const context_text = collected.other_files + collected.recent_files diff --git a/apps/editor/src/views/panel/backend/message-handlers/handle-find-relevant-files.ts b/apps/editor/src/views/panel/backend/message-handlers/handle-find-relevant-files.ts index 6d38f2f6a..c4322a429 100644 --- a/apps/editor/src/views/panel/backend/message-handlers/handle-find-relevant-files.ts +++ b/apps/editor/src/views/panel/backend/message-handlers/handle-find-relevant-files.ts @@ -10,7 +10,8 @@ import { import axios from 'axios' import { RECENTLY_USED_FIND_RELEVANT_FILES_CONFIG_IDS_STATE_KEY, - FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY + FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY, + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY } from '@/constants/state-keys' import { replace_changes_symbol, @@ -395,8 +396,14 @@ export const handle_find_relevant_files = async ( false ) + const only_file_tree = panel_provider.context.workspaceState.get( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + false + ) + const collected = await files_collector.collect_files({ - shrink: shrink_source_code + shrink: shrink_source_code, + only_file_tree }) const collected_files = collected.other_files + collected.recent_files diff --git a/apps/editor/src/views/panel/backend/message-handlers/handle-send-to-browser.ts b/apps/editor/src/views/panel/backend/message-handlers/handle-send-to-browser.ts index 1a82be805..bf98af3c7 100644 --- a/apps/editor/src/views/panel/backend/message-handlers/handle-send-to-browser.ts +++ b/apps/editor/src/views/panel/backend/message-handlers/handle-send-to-browser.ts @@ -20,7 +20,8 @@ import { } from '@/constants/instructions' import { get_recently_used_presets_or_groups_key, - FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY + FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY, + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY } from '@/constants/state-keys' import { ConfigPresetFormat } from '../utils/preset-format-converters' import { MODE } from '@/views/panel/types/main-view-mode' @@ -219,12 +220,21 @@ export const handle_send_to_browser = async (params: { false ) + const only_file_tree = + params.panel_provider.context.workspaceState.get( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + false + ) + const collected = await files_collector.collect_files({ additional_paths, no_context: params.panel_provider.web_prompt_type == 'no-context', shrink: params.panel_provider.web_prompt_type == 'find-relevant-files' && - shrink_source_code + shrink_source_code, + only_file_tree: + params.panel_provider.web_prompt_type == 'find-relevant-files' && + only_file_tree }) const context_text = collected.other_files + collected.recent_files diff --git a/apps/editor/src/views/panel/backend/panel-provider.ts b/apps/editor/src/views/panel/backend/panel-provider.ts index 103262e00..b56123248 100644 --- a/apps/editor/src/views/panel/backend/panel-provider.ts +++ b/apps/editor/src/views/panel/backend/panel-provider.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode' import * as path from 'path' +import * as fs from 'fs' import { ChildProcessWithoutNullStreams } from 'child_process' import { WebSocketManager } from '@/services/websocket-manager' import { @@ -93,7 +94,6 @@ import { handle_delete_configuration, handle_update_last_used_preset_or_group, handle_get_find_relevant_files_shrink_source_code, - handle_save_find_relevant_files_shrink_source_code, handle_return_home_and_switch_to_edit_context } from './message-handlers' import { SelectionState } from '../types/messages' @@ -112,7 +112,8 @@ import { RECENTLY_USED_FIND_RELEVANT_FILES_CONFIG_IDS_STATE_KEY, RECENTLY_USED_EDIT_CONTEXT_CONFIG_IDS_STATE_KEY, get_recently_used_presets_or_groups_key, - FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY + FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY, + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY } from '@/constants/state-keys' import { config_preset_to_ui_format, @@ -308,6 +309,7 @@ export class PanelProvider implements vscode.WebviewViewProvider { } this.update_providers_shrink_mode() this.update_providers_context_state() + this.send_token_count() } public async send_checkpoints() { @@ -375,6 +377,69 @@ export class PanelProvider implements vscode.WebviewViewProvider { }) } + public async send_token_count(default_token_count?: number) { + const is_find_relevant_files = + (this.mode == MODE.WEB && + this.web_prompt_type == 'find-relevant-files') || + (this.mode == MODE.API && this.api_prompt_type == 'find-relevant-files') + + if (!is_find_relevant_files && default_token_count !== undefined) { + this.send_message({ + command: 'TOKEN_COUNT_UPDATED', + token_count: default_token_count + }) + return + } + + let total = 0 + + try { + const open_editors_counts = (this.open_editors_provider as any) + .get_checked_files_token_count + ? await ( + this.open_editors_provider as any + ).get_checked_files_token_count() + : { total: 0, shrink: 0, path: 0 } + const workspace_counts = + await this.workspace_provider.get_checked_files_token_count() + + if (is_find_relevant_files) { + const only_file_tree = this.context.workspaceState.get( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + false + ) + const shrink_source_code = this.context.workspaceState.get( + FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY, + false + ) + + if (only_file_tree) { + total = (workspace_counts.path || 0) + (open_editors_counts.path || 0) + } else if (shrink_source_code) { + total = + (workspace_counts.shrink || 0) + (open_editors_counts.shrink || 0) + } else { + total = + (workspace_counts.total || 0) + (open_editors_counts.total || 0) + } + } else { + total = (workspace_counts.total || 0) + (open_editors_counts.total || 0) + } + + this.send_message({ + command: 'TOKEN_COUNT_UPDATED', + token_count: total + }) + } catch (e) { + if (default_token_count !== undefined) { + this.send_message({ + command: 'TOKEN_COUNT_UPDATED', + token_count: default_token_count + }) + } + } + } + constructor(params: { extension_uri: vscode.Uri workspace_provider: WorkspaceProvider @@ -526,16 +591,15 @@ export class PanelProvider implements vscode.WebviewViewProvider { } ) - token_count_emitter.on('token-count-updated', (token_count: number) => { - if (this._webview_view) { - this.send_message({ - command: 'TOKEN_COUNT_UPDATED', - token_count - }) - - this.send_context_files() + token_count_emitter.on( + 'token-count-updated', + async (token_count: number) => { + if (this._webview_view) { + await this.send_token_count(token_count) + this.send_context_files() + } } - }) + ) this.context.subscriptions.push(this._config_listener) @@ -884,12 +948,14 @@ export class PanelProvider implements vscode.WebviewViewProvider { await handle_save_web_prompt_type(this, message.prompt_type) this.update_providers_shrink_mode() this.update_providers_context_state() + this.send_token_count() } else if (message.command == 'GET_API_PROMPT_TYPE') { handle_get_api_prompt_type(this) } else if (message.command == 'SAVE_API_PROMPT_TYPE') { await handle_save_api_prompt_type(this, message.prompt_type) this.update_providers_shrink_mode() this.update_providers_context_state() + this.send_token_count() } else if (message.command == 'GET_EDIT_FORMAT_INSTRUCTIONS') { handle_get_edit_format_instructions(this) } else if (message.command == 'GET_EDIT_FORMAT') { @@ -902,6 +968,7 @@ export class PanelProvider implements vscode.WebviewViewProvider { await handle_save_mode(this, message) this.update_providers_shrink_mode() this.update_providers_context_state() + this.send_token_count() } else if (message.command == 'GET_MODE') { handle_get_mode(this) } else if (message.command == 'GET_VERSION') { @@ -1008,15 +1075,87 @@ export class PanelProvider implements vscode.WebviewViewProvider { } else if ( message.command == 'SAVE_FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE' ) { - await handle_save_find_relevant_files_shrink_source_code( - this, + await this.context.workspaceState.update( + FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE_STATE_KEY, message.shrink_source_code ) + this.update_providers_shrink_mode() + this.workspace_provider.refresh() + if ('refresh' in this.open_editors_provider) { + ;(this.open_editors_provider as any).refresh() + } + await this.send_token_count() + } else if ( + message.command == 'GET_FIND_RELEVANT_FILES_ONLY_FILE_TREE' + ) { + const only_file_tree = this.context.workspaceState.get( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + false + ) + this.send_message({ + command: 'FIND_RELEVANT_FILES_ONLY_FILE_TREE', + only_file_tree + }) + } else if ( + message.command == 'SAVE_FIND_RELEVANT_FILES_ONLY_FILE_TREE' + ) { + await this.context.workspaceState.update( + FIND_RELEVANT_FILES_ONLY_FILE_TREE_STATE_KEY, + message.only_file_tree + ) + this.workspace_provider.refresh() + if ('refresh' in this.open_editors_provider) { + ;(this.open_editors_provider as any).refresh() + } + await this.send_token_count() } else if (message.command == 'RELEVANT_FILES_MODAL_RESPONSE') { if (this.relevant_files_choice_resolver) { this.relevant_files_choice_resolver(message.files) this.relevant_files_choice_resolver = undefined } + } else if (message.command == 'SET_TASK_FILES') { + if (message.files && message.files.length > 0) { + const workspace_roots = + this.workspace_provider.get_workspace_roots() + const absolute_paths = message.files + .map((rel_path: string) => { + for (const root of workspace_roots) { + const potential = path.join(root, rel_path) + if (fs.existsSync(potential)) return potential + } + return null + }) + .filter(Boolean) as string[] + + if (absolute_paths.length > 0) { + await this.workspace_provider.set_checked_files(absolute_paths) + this.send_context_files() + await this.send_token_count() + } + } + } else if ((message as any).command == 'FILL_SCM_COMMIT') { + try { + const gitExtension = vscode.extensions.getExtension('vscode.git') + if (gitExtension) { + const git = gitExtension.exports.getAPI(1) + if (git.repositories.length > 0) { + git.repositories[0].inputBox.value = ( + message as any + ).commit_message + vscode.commands.executeCommand('workbench.view.scm') + } else { + vscode.window.showInformationMessage( + 'No active Git repositories found.' + ) + } + } + } catch (error) { + Logger.error({ + function_name: 'FILL_SCM_COMMIT', + message: 'Failed to fill SCM commit message', + data: error + }) + } } } catch (error: any) { Logger.error({ diff --git a/apps/editor/src/views/panel/frontend/Home/Home.tsx b/apps/editor/src/views/panel/frontend/Home/Home.tsx index 9b84990d7..b3230b2cb 100644 --- a/apps/editor/src/views/panel/frontend/Home/Home.tsx +++ b/apps/editor/src/views/panel/frontend/Home/Home.tsx @@ -42,7 +42,7 @@ type Props = { tasks: Record on_tasks_change: (root: string, tasks: Task[]) => void on_task_delete: (root: string, timestamp: number) => void - on_task_forward: (text: string) => void + on_task_forward: (task: Task) => void is_setup_complete: boolean } @@ -82,6 +82,13 @@ export const Home: React.FC = (props) => { } as FrontendMessage) } + const handle_go_to_file = (file_path: string) => { + post_message(props.vscode, { + command: 'GO_TO_FILE', + file_path + } as FrontendMessage) + } + const handle_create_checkpoint_click = () => { post_message(props.vscode, { command: 'CREATE_CHECKPOINT' @@ -257,9 +264,10 @@ export const Home: React.FC = (props) => { on_delete={(timestamp) => { handle_delete(workspace_root_folder, timestamp) }} - on_forward={(text) => { - props.on_task_forward(text) + on_forward={(task) => { + props.on_task_forward(task) }} + on_go_to_file={handle_go_to_file} placeholder={t('home.tasks.placeholder')} /> )} diff --git a/apps/editor/src/views/panel/frontend/Main/Main.tsx b/apps/editor/src/views/panel/frontend/Main/Main.tsx index aa9dcb16f..7fc8343b7 100644 --- a/apps/editor/src/views/panel/frontend/Main/Main.tsx +++ b/apps/editor/src/views/panel/frontend/Main/Main.tsx @@ -74,12 +74,16 @@ type Props = { is_setup_complete: boolean find_relevant_files_shrink_source_code: boolean on_find_relevant_files_shrink_source_code_change: (shrink: boolean) => void + find_relevant_files_only_file_tree: boolean + on_find_relevant_files_only_file_tree_change: (only: boolean) => void tabs_count: number active_tab_index: number on_tab_change: (index: number) => void on_new_tab: () => void on_tab_delete: (index: number) => void missing_preset?: boolean + active_commit_message?: string + on_fill_scm_commit: () => void } export const Main: React.FC = (props) => { @@ -861,12 +865,20 @@ export const Main: React.FC = (props) => { on_find_relevant_files_shrink_source_code_change={ props.on_find_relevant_files_shrink_source_code_change } + find_relevant_files_only_file_tree={ + props.find_relevant_files_only_file_tree + } + on_find_relevant_files_only_file_tree_change={ + props.on_find_relevant_files_only_file_tree_change + } is_setup_complete={props.is_setup_complete} tabs_count={props.tabs_count} active_tab_index={props.active_tab_index} on_tab_change={props.on_tab_change} on_new_tab={props.on_new_tab} on_tab_delete={props.on_tab_delete} + active_commit_message={props.active_commit_message} + on_fill_scm_commit={props.on_fill_scm_commit} /> ) } diff --git a/apps/editor/src/views/panel/frontend/Main/MainView/MainView.tsx b/apps/editor/src/views/panel/frontend/Main/MainView/MainView.tsx index 70bf06f6f..26e97fbbb 100644 --- a/apps/editor/src/views/panel/frontend/Main/MainView/MainView.tsx +++ b/apps/editor/src/views/panel/frontend/Main/MainView/MainView.tsx @@ -119,12 +119,16 @@ type Props = { on_recording_finished: () => void find_relevant_files_shrink_source_code: boolean on_find_relevant_files_shrink_source_code_change: (shrink: boolean) => void + find_relevant_files_only_file_tree: boolean + on_find_relevant_files_only_file_tree_change: (only: boolean) => void is_setup_complete: boolean tabs_count: number active_tab_index: number on_tab_change: (index: number) => void on_new_tab: () => void on_tab_delete: (index: number) => void + active_commit_message?: string + on_fill_scm_commit?: () => void } export const MainView: React.FC = (props) => { @@ -198,6 +202,10 @@ export const MainView: React.FC = (props) => { configurations: props.configurations }) + const is_only_file_tree_active = + is_in_find_relevant_files_prompt_type && + props.find_relevant_files_only_file_tree + return ( <>
= (props) => { {is_in_find_relevant_files_prompt_type && ( -
- - +
+
+ + +
+
+ + +
)} @@ -254,6 +274,55 @@ export const MainView: React.FC = (props) => { /> )} + {props.active_commit_message && ( +
+
+ + Pending Commit: + {' '} + {props.active_commit_message} +
+ +
+ )} +
= (props) => { props.context_size_warning_threshold } is_context_disabled={is_in_no_context_prompt_type} + is_only_file_tree_active={is_only_file_tree_active} + file_tree_token_count={props.token_count} />
diff --git a/apps/editor/src/views/panel/frontend/Panel.tsx b/apps/editor/src/views/panel/frontend/Panel.tsx index 36d443841..0d063e44e 100644 --- a/apps/editor/src/views/panel/frontend/Panel.tsx +++ b/apps/editor/src/views/panel/frontend/Panel.tsx @@ -63,7 +63,7 @@ export const Panel = () => { presets_collapsed, send_with_shift_enter, configurations_collapsed, - handle_instructions_change, + set_instructions, handle_web_prompt_type_change, handle_api_prompt_type_change, handle_mode_change, @@ -81,10 +81,14 @@ export const Panel = () => { handle_set_recording_state, find_relevant_files_shrink_source_code, handle_find_relevant_files_shrink_source_code_change, + find_relevant_files_only_file_tree, + handle_find_relevant_files_only_file_tree_change, is_setup_complete, handle_tab_change, handle_new_tab, - handle_tab_delete + handle_tab_delete, + active_commit_message, + handle_fill_scm_commit } = use_panel(vscode) const { @@ -378,7 +382,7 @@ export const Panel = () => { find_relevant_files_instructions.active_index ] || '' } - set_instructions={handle_instructions_change} + set_instructions={set_instructions} mode={mode} web_prompt_type={web_prompt_type} api_prompt_type={api_prompt_type} @@ -431,12 +435,20 @@ export const Panel = () => { on_find_relevant_files_shrink_source_code_change={ handle_find_relevant_files_shrink_source_code_change } + find_relevant_files_only_file_tree={ + find_relevant_files_only_file_tree + } + on_find_relevant_files_only_file_tree_change={ + handle_find_relevant_files_only_file_tree_change + } is_setup_complete={is_setup_complete} tabs_count={current_state?.instructions.length ?? 0} active_tab_index={current_state?.active_index ?? 0} on_tab_change={handle_tab_change} on_new_tab={handle_new_tab} on_tab_delete={handle_tab_delete} + active_commit_message={active_commit_message} + on_fill_scm_commit={handle_fill_scm_commit} />
{ find_relevant_files_shrink_source_code, set_find_relevant_files_shrink_source_code ] = useState(false) + const [ + find_relevant_files_only_file_tree, + set_find_relevant_files_only_file_tree + ] = useState(false) + + const [active_commit_message, set_active_commit_message] = useState< + string | undefined + >() + + const handle_instructions_change_with_commit_clear = ( + value: string, + prompt_type: any + ) => { + // If user manually types a new prompt, we keep the commit message unless they clear it entirely + if (!value) { + set_active_commit_message(undefined) + } + handle_instructions_change(value, prompt_type) + } + + const handle_task_forward = (task: Task) => { + handle_mode_change(MODE.WEB) + handle_web_prompt_type_change('edit-context') + handle_instructions_change_with_commit_clear(task.text, 'edit-context') + set_active_commit_message(task.commit_message) + + if (task.files && task.files.length > 0) { + const files = task.files + // Delay setting the files slightly to allow the backend to finish switching + // the workspace context state (which involves async state loading). + setTimeout(() => { + post_message(vscode, { + command: 'SET_TASK_FILES', + files: files.map((f) => f.path) + }) + }, 150) + } - const handle_task_forward = (text: string) => { - handle_instructions_change(text, 'edit-context') set_active_view('main') set_main_view_scroll_reset_key((k) => k + 1) } @@ -114,6 +150,41 @@ export const use_panel = (vscode: any) => { command: 'SAVE_FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE', shrink_source_code }) + + if (shrink_source_code) { + set_find_relevant_files_only_file_tree(false) + post_message(vscode, { + command: 'SAVE_FIND_RELEVANT_FILES_ONLY_FILE_TREE', + only_file_tree: false + }) + } + } + + const handle_find_relevant_files_only_file_tree_change = ( + only_file_tree: boolean + ) => { + set_find_relevant_files_only_file_tree(only_file_tree) + post_message(vscode, { + command: 'SAVE_FIND_RELEVANT_FILES_ONLY_FILE_TREE', + only_file_tree + }) + + if (only_file_tree) { + set_find_relevant_files_shrink_source_code(false) + post_message(vscode, { + command: 'SAVE_FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE', + shrink_source_code: false + }) + } + } + + const handle_fill_scm_commit = () => { + if (active_commit_message) { + post_message(vscode, { + command: 'FILL_SCM_COMMIT', + commit_message: active_commit_message + }) + } } useEffect(() => { @@ -155,8 +226,13 @@ export const use_panel = (vscode: any) => { set_setup_progress(message.setup_progress) } else if (message.command == 'FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE') { set_find_relevant_files_shrink_source_code(message.shrink_source_code) + } else if (message.command == 'FIND_RELEVANT_FILES_ONLY_FILE_TREE') { + set_find_relevant_files_only_file_tree(message.only_file_tree) + } else if (message.command == 'SET_ACTIVE_COMMIT_MESSAGE') { + set_active_commit_message(message.commit_message) } else if (message.command == 'RETURN_HOME') { set_active_view('home') + set_active_commit_message(undefined) } } window.addEventListener('message', handle_message) @@ -173,7 +249,8 @@ export const use_panel = (vscode: any) => { { command: 'GET_CHECKPOINTS' }, { command: 'REQUEST_CAN_UNDO' }, { command: 'GET_SETUP_PROGRESS' }, - { command: 'GET_FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE' } + { command: 'GET_FIND_RELEVANT_FILES_SHRINK_SOURCE_CODE' }, + { command: 'GET_FIND_RELEVANT_FILES_ONLY_FILE_TREE' } ] initial_messages.forEach((message) => post_message(vscode, message)) @@ -296,7 +373,7 @@ export const use_panel = (vscode: any) => { : false, is_timeline_collapsed, handle_task_forward, - handle_instructions_change, + set_instructions: handle_instructions_change_with_commit_clear, handle_web_prompt_type_change, handle_api_prompt_type_change, handle_mode_change, @@ -313,8 +390,12 @@ export const use_panel = (vscode: any) => { is_setup_complete, find_relevant_files_shrink_source_code, handle_find_relevant_files_shrink_source_code_change, + find_relevant_files_only_file_tree, + handle_find_relevant_files_only_file_tree_change, handle_tab_change, handle_new_tab, - handle_tab_delete + handle_tab_delete, + active_commit_message, + handle_fill_scm_commit } } diff --git a/apps/editor/src/views/panel/frontend/i18n/translations/home.ts b/apps/editor/src/views/panel/frontend/i18n/translations/home.ts index 0fbe23244..17029f94e 100644 --- a/apps/editor/src/views/panel/frontend/i18n/translations/home.ts +++ b/apps/editor/src/views/panel/frontend/i18n/translations/home.ts @@ -143,5 +143,23 @@ export const home = { cs: 'Odstranit těla funkcí a komentáře', hu: 'Függvénytestek és megjegyzések eltávolítása', bg: 'Премахване на телата на функциите и коментарите' + }, + 'home.only-file-tree': { + en: 'Provide only file tree', + pl: 'Dostarcz tylko drzewo plików', + 'zh-cn': '仅提供文件树', + ja: 'ファイルツリーのみを提供する', + 'zh-tw': '僅提供文件樹', + de: 'Nur Dateibaum bereitstellen', + es: 'Proporcionar solo el árbol de archivos', + fr: "Fournir uniquement l'arborescence des fichiers", + 'pt-br': 'Fornecer apenas a árvore de arquivos', + ru: 'Предоставить только дерево файлов', + ko: '파일 트리만 제공', + it: "Fornisci solo l'albero dei file", + tr: 'Sadece dosya ağacını sağla', + cs: 'Poskytnout pouze strom souborů', + hu: 'Csak a fájlfát biztosítsa', + bg: 'Предоставяне само на дървото на файловете' } } as const diff --git a/apps/editor/src/views/panel/types/messages.ts b/apps/editor/src/views/panel/types/messages.ts index 0aafa6597..2a16cdfd6 100644 --- a/apps/editor/src/views/panel/types/messages.ts +++ b/apps/editor/src/views/panel/types/messages.ts @@ -469,6 +469,11 @@ export interface SaveTasksMessage extends BaseMessage { tasks: Record } +export interface SetTaskFilesMessage extends BaseMessage { + command: 'SET_TASK_FILES' + files: string[] +} + export interface DeleteTaskMessage extends BaseMessage { command: 'DELETE_TASK' root: string @@ -549,6 +554,15 @@ export interface SaveFindRelevantFilesShrinkSourceCodeMessage extends BaseMessag shrink_source_code: boolean } +export interface GetFindRelevantFilesOnlyFileTreeMessage extends BaseMessage { + command: 'GET_FIND_RELEVANT_FILES_ONLY_FILE_TREE' +} + +export interface SaveFindRelevantFilesOnlyFileTreeMessage extends BaseMessage { + command: 'SAVE_FIND_RELEVANT_FILES_ONLY_FILE_TREE' + only_file_tree: boolean +} + export interface GetSetupProgressMessage extends BaseMessage { command: 'GET_SETUP_PROGRESS' } @@ -649,6 +663,7 @@ export type FrontendMessage = | PreviewSwitchChoiceMessage | GetTasksMessage | SaveTasksMessage + | SetTaskFilesMessage | DeleteTaskMessage | PreviewGeneratedCodeMessage | UpdateFileProgressMessage @@ -664,9 +679,12 @@ export type FrontendMessage = | SetRecordingStateMessage | GetFindRelevantFilesShrinkSourceCodeMessage | SaveFindRelevantFilesShrinkSourceCodeMessage + | GetFindRelevantFilesOnlyFileTreeMessage + | SaveFindRelevantFilesOnlyFileTreeMessage | GetSetupProgressMessage | RequestReturnHomeMessage | RelevantFilesModalResponseMessage + | { command: 'FILL_SCM_COMMIT'; commit_message: string } // === FROM BACKEND TO FRONTEND === export interface InstructionsMessage extends BaseMessage { @@ -915,6 +933,11 @@ export interface FindRelevantFilesShrinkSourceCodeMessage extends BaseMessage { shrink_source_code: boolean } +export interface FindRelevantFilesOnlyFileTreeMessage extends BaseMessage { + command: 'FIND_RELEVANT_FILES_ONLY_FILE_TREE' + only_file_tree: boolean +} + export interface SetupProgressMessage { command: 'SETUP_PROGRESS' setup_progress: SetupProgress @@ -983,7 +1006,9 @@ export type BackendMessage = | UpdateFileProgressMessage | RecordingStateMessage | FindRelevantFilesShrinkSourceCodeMessage + | FindRelevantFilesOnlyFileTreeMessage | SetupProgressMessage | InsertSymbolAtCursorMessage | ReturnHomeMessage | ShowRelevantFilesModalMessage + | { command: 'SET_ACTIVE_COMMIT_MESSAGE'; commit_message: string } diff --git a/packages/shared/src/types/task.ts b/packages/shared/src/types/task.ts index d3b3c16ec..86a8306f1 100644 --- a/packages/shared/src/types/task.ts +++ b/packages/shared/src/types/task.ts @@ -1,7 +1,14 @@ +export type TaskFile = { + path: string + tokens?: number +} + export type Task = { text: string is_checked: boolean created_at: number is_collapsed?: boolean children?: Task[] + files?: TaskFile[] + commit_message?: string } diff --git a/packages/ui/src/components/editor/panel/ContextUtilisation/ContextUtilisation.tsx b/packages/ui/src/components/editor/panel/ContextUtilisation/ContextUtilisation.tsx index a59b76bd4..48fe826d7 100644 --- a/packages/ui/src/components/editor/panel/ContextUtilisation/ContextUtilisation.tsx +++ b/packages/ui/src/components/editor/panel/ContextUtilisation/ContextUtilisation.tsx @@ -5,6 +5,8 @@ type Props = { current_context_size: number context_size_warning_threshold: number is_context_disabled?: boolean + is_only_file_tree_active?: boolean + file_tree_token_count?: number } const format_tokens = (tokens: number): string => { @@ -27,14 +29,44 @@ export const ContextUtilisation: React.FC = (props) => { ) } + const active_token_count = + props.is_only_file_tree_active && props.file_tree_token_count !== undefined + ? props.file_tree_token_count + : props.current_context_size + const is_above_threshold = - props.current_context_size > props.context_size_warning_threshold + active_token_count > props.context_size_warning_threshold const progress = Math.min( - (props.current_context_size / props.context_size_warning_threshold) * 100, + (active_token_count / props.context_size_warning_threshold) * 100, 100 ) + const display_progress = active_token_count > 0 ? Math.max(progress, 1) : 0 + + if ( + props.is_only_file_tree_active && + props.file_tree_token_count !== undefined + ) { + return ( +
+
+
+
+ + ~{format_tokens(props.file_tree_token_count)} file tree tokens + +
+ ) + } - const formatted_current_size = format_tokens(props.current_context_size) + const formatted_current_size = format_tokens(active_token_count) const formatted_threshold = format_tokens( props.context_size_warning_threshold ) @@ -44,14 +76,14 @@ export const ContextUtilisation: React.FC = (props) => { if (!is_above_threshold) { const remaining_tokens = props.context_size_warning_threshold - - (props.current_context_size < 1000 - ? props.current_context_size - : Math.floor(props.current_context_size / 1000) * 1000) + (active_token_count < 1000 + ? active_token_count + : Math.floor(active_token_count / 1000) * 1000) const formatted_remaining_tokens = format_tokens(remaining_tokens) title_text = `${formatted_remaining_tokens} tokens remaining until threshold warning (change in settings)` } else { const exceeded_by = - props.current_context_size - props.context_size_warning_threshold + active_token_count - props.context_size_warning_threshold const formatted_exceeded_by = format_tokens(exceeded_by) title_text = `Threshold of ${formatted_threshold} tokens is exceeded by ${formatted_exceeded_by} tokens` } @@ -63,7 +95,7 @@ export const ContextUtilisation: React.FC = (props) => { className={cn(styles.bar__progress, { [styles['bar__progress--warning']]: is_above_threshold })} - style={{ width: `${progress}%` }} + style={{ width: `${display_progress}%` }} />
diff --git a/packages/ui/src/components/editor/panel/Tasks/Tasks.module.scss b/packages/ui/src/components/editor/panel/Tasks/Tasks.module.scss index 2576e3ec6..ece471673 100644 --- a/packages/ui/src/components/editor/panel/Tasks/Tasks.module.scss +++ b/packages/ui/src/components/editor/panel/Tasks/Tasks.module.scss @@ -102,6 +102,68 @@ opacity: 0.5; } } + + &__commit { + font-size: 11px; + color: var(--cwc-text-color); + background: var(--vscode-textBlockQuote-background); + border-left: 2px solid var(--vscode-textBlockQuote-border); + padding: 4px 6px; + margin-top: 6px; + border-radius: 2px; + word-break: break-word; + + &-label { + font-weight: bold; + opacity: 0.8; + } + } + + &__files { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 6px; + + &--checked { + opacity: 0.5; + } + } + + &__file { + font-size: 11px; + color: var(--cwc-text-color-dimmed); + display: flex; + align-items: flex-start; + gap: 4px; + word-break: break-word; + line-height: 1.3; + cursor: pointer; + + &:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; + } + + &::before { + content: '\ea6f'; + font-family: 'codicon'; + font-size: 12px; + opacity: 0.7; + flex-shrink: 0; + margin-top: 1px; + } + + &-path { + flex-grow: 1; + } + + &-tokens { + opacity: 0.5; + font-size: 10px; + white-space: nowrap; + } + } } .add-button { diff --git a/packages/ui/src/components/editor/panel/Tasks/Tasks.tsx b/packages/ui/src/components/editor/panel/Tasks/Tasks.tsx index 0b3ba2f58..b550c342a 100644 --- a/packages/ui/src/components/editor/panel/Tasks/Tasks.tsx +++ b/packages/ui/src/components/editor/panel/Tasks/Tasks.tsx @@ -20,8 +20,9 @@ type Props = { on_change: (task: Task) => void on_add: () => void on_add_subtask?: (parent_task: Task) => void - on_forward: (text: string) => void + on_forward: (task: Task) => void on_delete: (created_at: number) => void + on_go_to_file?: (file_path: string) => void placeholder: string } @@ -77,6 +78,12 @@ export const Tasks: React.FC = (props) => { props.on_change({ ...rest, is_checked: checked }) } + const format_tokens = (tokens?: number) => { + if (tokens === undefined) return '' + if (tokens >= 1000) return `(${(tokens / 1000).toFixed(1)}k tokens)` + return `(${tokens} tokens)` + } + const render_item = (params: { task: Task is_visually_checked: boolean @@ -164,7 +171,7 @@ export const Tasks: React.FC = (props) => { codicon_icon="forward" on_click={(e) => { e.stopPropagation() - props.on_forward(params.task.text) + props.on_forward(params.task) set_forwarded_timestamp(params.task.created_at) }} title="Use" @@ -259,6 +266,42 @@ export const Tasks: React.FC = (props) => { {params.task.text || props.placeholder}
)} + + {params.task.commit_message && ( +
+ Commit:{' '} + {params.task.commit_message} +
+ )} + + {params.task.files && params.task.files.length > 0 && ( +
+ {params.task.files.map((file, index) => ( +
{ + e.stopPropagation() + if (props.on_go_to_file) { + props.on_go_to_file(file.path) + } + }} + title={props.on_go_to_file ? 'Click to open file' : undefined} + > + {file.path} + {file.tokens !== undefined && ( + + {format_tokens(file.tokens)} + + )} +
+ ))} +
+ )} )