diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index 924ee0f546d0..4020beb0875a 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -36,6 +36,8 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi openExternal: api.openExternal, readWorkspaceGitRepositories: api.readWorkspaceGitRepositories, getActiveEditorSelection: api.getActiveEditorSelection, + listFileInWorkspace: api.listFileInWorkspace, + readFileContent: api.readFileContent, }, }); } diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts index 721468c01069..0bd8076f00a6 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -1,11 +1,12 @@ import path from "path"; -import { TextEditor, Position as VSCodePosition, Range as VSCodeRange, Uri, workspace } from "vscode"; +import { Position as VSCodePosition, Range as VSCodeRange, Uri, workspace, TextEditor } from "vscode"; import type { Filepath, Position as ChatPanelPosition, LineRange, PositionRange, Location, + ListFileItem, FilepathInGitRepository, } from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; @@ -221,3 +222,15 @@ export function generateLocalNotebookCellUri(notebook: Uri, handle: number): Uri const fragment = `${p}${s}s${Buffer.from(notebook.scheme).toString("base64")}`; return notebook.with({ scheme: DocumentSchemes.vscodeNotebookCell, fragment }); } + +export function uriToListFileItem(uri: Uri, gitProvider: GitProvider): ListFileItem { + return { + label: workspace.asRelativePath(uri), + filepath: localUriToChatPanelFilepath(uri, gitProvider), + }; +} + +export function escapeGlobPattern(query: string): string { + // escape special glob characters: * ? [ ] { } ( ) ! @ + return query.replace(/[*?[\]{}()!@]/g, "\\$&"); +} diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index 9cef59828d50..52f8b50293c5 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -14,6 +14,7 @@ import { ProgressLocation, Location, LocationLink, + TabInputText, } from "vscode"; import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel"; import type { @@ -26,6 +27,9 @@ import type { FileLocation, GitRepository, EditorFileContext, + ListFilesInWorkspaceParams, + ListFileItem, + FileRange, } from "tabby-chat-panel"; import * as semver from "semver"; import type { StatusInfo, Config } from "tabby-agent"; @@ -42,6 +46,8 @@ import { vscodeRangeToChatPanelPositionRange, chatPanelLocationToVSCodeRange, isValidForSyncActiveEditorSelection, + uriToListFileItem, + escapeGlobPattern, } from "./utils"; import mainHtml from "./html/main.html"; import errorHtml from "./html/error.html"; @@ -456,6 +462,41 @@ export class ChatWebview { const fileContext = await getFileContextFromSelection(editor, this.gitProvider); return fileContext; }, + listFileInWorkspace: async (params: ListFilesInWorkspaceParams): Promise => { + const maxResults = params.limit || 50; + const query = params.query?.toLowerCase(); + + if (!query) { + // TODO: check tab file stat, check if exists + const openTabs = window.tabGroups.all.flatMap((group) => group.tabs); + this.logger.info(`No query provided, listing ${openTabs.length} opened editors.`); + return openTabs.map((tab) => uriToListFileItem((tab.input as TabInputText).uri, this.gitProvider)); + } + + const globPattern = `**/${escapeGlobPattern(query)}*`; + + // validate the glob pattern + this.logger.info(`Searching files with pattern: ${globPattern}, limit: ${maxResults}.`); + try { + const files = await workspace.findFiles(globPattern, null, maxResults); + this.logger.info(`Found ${files.length} files.`); + return files.map((uri) => uriToListFileItem(uri, this.gitProvider)); + } catch (error) { + this.logger.warn("Failed to find files:", error); + window.showErrorMessage("Failed to find files."); + return []; + } + }, + + readFileContent: async (info: FileRange): Promise => { + const uri = chatPanelFilepathToLocalUri(info.filepath, this.gitProvider); + if (!uri) { + this.logger.warn(`Could not resolve URI from filepath: ${JSON.stringify(info.filepath)}`); + return null; + } + const document = await workspace.openTextDocument(uri); + return document.getText(chatPanelLocationToVSCodeRange(info.range) ?? undefined); + }, }); }