diff --git a/src/extension/actions/git-project-actions.ts b/src/extension/actions/git-project-actions.ts index bee4c60..1ba42e3 100644 --- a/src/extension/actions/git-project-actions.ts +++ b/src/extension/actions/git-project-actions.ts @@ -1,4 +1,9 @@ import { gitUtils } from '@extension/file-utils/git' +import { + traverseFileOrFolders, + type FileInfo, + type FolderInfo +} from '@extension/file-utils/traverse-fs' import { vfs } from '@extension/file-utils/vfs' import { gitProjectSchemeHandler } from '@extension/file-utils/vfs/schemes/git-project-scheme' import { gitProjectDB } from '@extension/lowdb/git-project-db' @@ -166,7 +171,6 @@ export class GitProjectActionsCollection extends ServerActionCollection { // Remove project directories await settledPromiseResults( projects.map(async project => { - if (!project) return const schemeUri = gitProjectSchemeHandler.createSchemeUri({ name: project.name, type: project.type, @@ -218,4 +222,42 @@ export class GitProjectActionsCollection extends ServerActionCollection { return project } + + async getGitProjectFilesAndFolders( + context: ActionContext<{ projectIds?: string[] }> + ): Promise> { + const { actionParams } = context + const projectIds = + actionParams.projectIds ?? (await gitProjectDB.getAll()).map(p => p.id) + const projects = await gitProjectDB.getAll() + const result: Record = {} + + // Get files and folders for each project + await settledPromiseResults( + projectIds.map(async projectId => { + const project = projects.find(p => p.id === projectId) + if (!project) { + result[projectId] = [] + return + } + + const gitProjectSchemeUri = gitProjectSchemeHandler.createSchemeUri({ + name: project.name, + type: project.type, + relativePath: './' + }) + + const items = await traverseFileOrFolders({ + type: 'fileOrFolder', + schemeUris: [gitProjectSchemeUri], + isGetFileContent: false, + itemCallback: item => item + }) + + result[projectId] = items + }) + ) + + return result + } } diff --git a/src/extension/actions/project-actions.ts b/src/extension/actions/project-actions.ts index 52c2673..dcc4989 100644 --- a/src/extension/actions/project-actions.ts +++ b/src/extension/actions/project-actions.ts @@ -1,7 +1,14 @@ +import { + traverseFileOrFolders, + type FileInfo, + type FolderInfo +} from '@extension/file-utils/traverse-fs' +import { projectSchemeHandler } from '@extension/file-utils/vfs/schemes/project-scheme' import { projectDB } from '@extension/lowdb/project-db' import { ServerActionCollection } from '@shared/actions/server-action-collection' import type { ActionContext } from '@shared/actions/types' import type { Project } from '@shared/entities' +import { settledPromiseResults } from '@shared/utils/common' import { z } from 'zod' // Create schema for validation @@ -115,4 +122,41 @@ export class ProjectActionsCollection extends ServerActionCollection { project.path.toLowerCase().includes(query.toLowerCase()) ) } + + async getProjectFilesAndFolders( + context: ActionContext<{ projectIds?: string[] }> + ): Promise> { + const { actionParams } = context + const projectIds = + actionParams.projectIds ?? (await projectDB.getAll()).map(p => p.id) + const projects = await projectDB.getAll() + const result: Record = {} + + // Get files and folders for each project + await settledPromiseResults( + projectIds.map(async projectId => { + const project = projects.find(p => p.id === projectId) + if (!project) { + result[projectId] = [] + return + } + + const projectSchemeUri = projectSchemeHandler.createSchemeUri({ + name: project.name, + relativePath: './' + }) + + const items = await traverseFileOrFolders({ + type: 'fileOrFolder', + schemeUris: [projectSchemeUri], + isGetFileContent: false, + itemCallback: item => item + }) + + result[projectId] = items + }) + ) + + return result + } } diff --git a/src/extension/actions/prompt-snippet-actions.ts b/src/extension/actions/prompt-snippet-actions.ts index 18f2560..3d54e27 100644 --- a/src/extension/actions/prompt-snippet-actions.ts +++ b/src/extension/actions/prompt-snippet-actions.ts @@ -9,14 +9,66 @@ import type { ActionContext } from '@shared/actions/types' import type { PromptSnippet, SettingsSaveType } from '@shared/entities' import { settledPromiseResults } from '@shared/utils/common' import { v4 as uuidv4 } from 'uuid' +import { z } from 'zod' export type PromptSnippetWithSaveType = PromptSnippet & { saveType: SettingsSaveType } +// Add schema validation +const promptSnippetSchema = z.object({ + title: z + .string() + .min(1, 'Title is required') + .refine( + async title => { + const globalSnippets = await promptSnippetsGlobalDB.getAll() + const workspaceSnippets = await promptSnippetsWorkspaceDB.getAll() + const allSnippets = [...globalSnippets, ...workspaceSnippets] + return !allSnippets.some(s => s.title === title) + }, + { + message: 'Title must be unique' + } + ), + contents: z.array( + z.object({ + type: z.literal('text'), + text: z.string() + }) + ), + richText: z.string().optional(), + mentions: z.array(z.any()).optional() +}) + export class PromptSnippetActionsCollection extends ServerActionCollection { readonly categoryName = 'promptSnippet' + // Add validation method + private async validateSnippet( + data: Partial, + excludeId?: string + ): Promise { + const globalSnippets = await promptSnippetsGlobalDB.getAll() + const workspaceSnippets = await promptSnippetsWorkspaceDB.getAll() + const allSnippets = [...globalSnippets, ...workspaceSnippets] + + const schema = promptSnippetSchema.extend({ + title: z + .string() + .min(1, 'Title is required') + .refine( + async title => + !allSnippets.some(s => s.title === title && s.id !== excludeId), + { + message: 'Title must be unique' + } + ) + }) + + await schema.parseAsync(data) + } + async refreshSnippet( context: ActionContext<{ snippet: PromptSnippet }> ): Promise { @@ -171,6 +223,9 @@ export class PromptSnippetActionsCollection extends ServerActionCollection { const { saveType, snippet, isRefresh } = actionParams try { + // Add validation + await this.validateSnippet(snippet) + const now = Date.now() let updatedSnippet = { ...snippet, @@ -211,6 +266,9 @@ export class PromptSnippetActionsCollection extends ServerActionCollection { const { actionParams } = context const { id, updates, isRefresh } = actionParams try { + // Add validation + await this.validateSnippet(updates, id) + const originalSnippet = await this.getSnippet({ ...context, actionParams: { id, isRefresh } diff --git a/src/extension/chat/utils/doc-crawler.ts b/src/extension/chat/utils/doc-crawler.ts index 8be503c..2337cd2 100644 --- a/src/extension/chat/utils/doc-crawler.ts +++ b/src/extension/chat/utils/doc-crawler.ts @@ -4,6 +4,7 @@ import url from 'url' import { aidePaths, getSemanticHashName } from '@extension/file-utils/paths' import { vfs } from '@extension/file-utils/vfs' import { logger } from '@extension/logger' +import { settledPromiseResults } from '@shared/utils/common' import * as cheerio from 'cheerio' import type { Element } from 'domhandler' import TurndownService from 'turndown' @@ -162,7 +163,7 @@ export class DocCrawler { while (this.queue.length > 0 && this.visited.size < this.options.maxPages) { const batch = this.queue.splice(0, this.options.concurrency) const promises = batch.map(item => this.crawlPage(item.url, item.depth)) - await Promise.allSettled(promises) + await settledPromiseResults(promises) await new Promise(resolve => setTimeout(resolve, this.options.delay)) this.progressReporter.setProcessedItems(this.visited.size) } diff --git a/src/extension/chat/vectordb/base-indexer.ts b/src/extension/chat/vectordb/base-indexer.ts index b363cb0..076bd81 100644 --- a/src/extension/chat/vectordb/base-indexer.ts +++ b/src/extension/chat/vectordb/base-indexer.ts @@ -3,6 +3,7 @@ import type { BaseEmbeddings } from '@extension/ai/embeddings/types' import { getFileHash } from '@extension/file-utils/get-file-hash' import { vfs } from '@extension/file-utils/vfs' import { logger } from '@extension/logger' +import { settledPromiseResults } from '@shared/utils/common' import { Field, FixedSizeList, @@ -218,7 +219,7 @@ export abstract class BaseIndexer { } }) - await Promise.allSettled(tasksPromises) + await settledPromiseResults(tasksPromises) this.totalFiles = fileSchemeUrisNeedReindex.length this.progressReporter.setTotalItems(this.totalFiles) @@ -246,7 +247,7 @@ export abstract class BaseIndexer { ) try { - await Promise.allSettled(processingPromises) + await settledPromiseResults(processingPromises) } catch (error) { logger.error(`Error indexing files:`, error) } finally { diff --git a/src/extension/commands/batch-processor/command.ts b/src/extension/commands/batch-processor/command.ts index 0493e28..da45d92 100644 --- a/src/extension/commands/batch-processor/command.ts +++ b/src/extension/commands/batch-processor/command.ts @@ -103,7 +103,7 @@ export class BatchProcessorCommand extends BaseCommand { ) ) - await Promise.allSettled(promises) + await settledPromiseResults(promises) hideProcessLoading() diff --git a/src/extension/file-utils/ignore-patterns.ts b/src/extension/file-utils/ignore-patterns.ts index 858eb1a..361e14a 100644 --- a/src/extension/file-utils/ignore-patterns.ts +++ b/src/extension/file-utils/ignore-patterns.ts @@ -1,6 +1,7 @@ +import path from 'path' import { getConfigKey } from '@extension/config' import { logger } from '@extension/logger' -import { toUnixPath } from '@shared/utils/common' +import { settledPromiseResults, toUnixPath } from '@shared/utils/common' import { SchemeUriHelper } from '@shared/utils/scheme-uri-helper' import { glob } from 'glob' import ignore from 'ignore' @@ -20,6 +21,7 @@ export const createShouldIgnore = async ( ) => { const ignorePatterns = await getConfigKey('ignorePatterns') const respectGitIgnore = await getConfigKey('respectGitIgnore') + const fullDirPath = await vfs.resolveFullPathProAsync(dirSchemeUri, false) if (customIgnorePatterns) { ignorePatterns.push(...customIgnorePatterns) @@ -58,20 +60,32 @@ export const createShouldIgnore = async ( * @returns A boolean indicating whether the file should be ignored. */ const shouldIgnore = (schemeUriOrFileFullPath: string) => { - const relativePath = vfs.resolveRelativePathProSync(schemeUriOrFileFullPath) - const unixRelativePath = toUnixPath(relativePath) + try { + let relativePath - if (!unixRelativePath) return false + if (vfs.isSchemeUri(schemeUriOrFileFullPath)) { + relativePath = vfs.resolveRelativePathProSync(schemeUriOrFileFullPath) + } else { + relativePath = path.relative(fullDirPath, schemeUriOrFileFullPath) + } - if (['.', './', '..', '../', '/'].includes(unixRelativePath)) { - return false - } + const unixRelativePath = toUnixPath(relativePath) - if (ig && ig.ignores(unixRelativePath)) { - return true - } + if (!unixRelativePath) return false - return mms.some(mm => mm.match(unixRelativePath)) + if (['.', './', '..', '../', '/'].includes(unixRelativePath)) { + return false + } + + if (ig && ig.ignores(unixRelativePath)) { + return true + } + + return mms.some(mm => mm.match(unixRelativePath)) + } catch (error) { + logger.warn('shouldIgnore error', error) + return false + } } return shouldIgnore @@ -96,11 +110,16 @@ export const getAllValidFiles = async ( nodir: true, absolute: true, follow: false, + posix: true, dot: true, fs: vfs, ignore: { ignored(p) { - return shouldIgnore(p.fullpath()) + try { + return shouldIgnore(p.fullpath()) + } catch { + return false + } }, childrenIgnored(p) { try { @@ -147,10 +166,15 @@ export const getAllValidFolders = async ( absolute: true, follow: false, dot: true, + posix: true, fs: vfs, ignore: { ignored(p) { - return shouldIgnore(p.fullpath()) + try { + return shouldIgnore(p.fullpath()) + } catch { + return false + } }, childrenIgnored(p) { try { @@ -170,7 +194,7 @@ export const getAllValidFolders = async ( } }) - await Promise.allSettled(promises) + await settledPromiseResults(promises) if (!vfs.isSchemeUri(dirSchemeUri)) { return folders diff --git a/src/extension/file-utils/traverse-fs.ts b/src/extension/file-utils/traverse-fs.ts index fc1cfe5..8326d34 100644 --- a/src/extension/file-utils/traverse-fs.ts +++ b/src/extension/file-utils/traverse-fs.ts @@ -113,12 +113,12 @@ const traverseOneProjectFs = async ( return await getAllValidFiles(schemeUri, shouldIgnore) } // For 'fileOrFolder' type, get both files and folders - const files = await getAllValidFiles(schemeUri, shouldIgnore) const folders = await getAllValidFolders(schemeUri, shouldIgnore) + const files = await getAllValidFiles(schemeUri, shouldIgnore) return [...files, ...folders] } - await Promise.allSettled( + await settledPromiseResults( schemeUris.map(async schemeUri => { const stat = await vfs.promises.stat(schemeUri) @@ -130,7 +130,7 @@ const traverseOneProjectFs = async ( const allItemSchemeUris = await getAllValidItemsWithCustomIgnore(schemeUri) - await Promise.allSettled( + await settledPromiseResults( allItemSchemeUris.map(async itemSchemeUri => { const itemStat = await vfs.promises.stat(itemSchemeUri) diff --git a/src/extension/file-utils/vfs/helpers/utils.ts b/src/extension/file-utils/vfs/helpers/utils.ts index 9ce0b0a..134fc73 100644 --- a/src/extension/file-utils/vfs/helpers/utils.ts +++ b/src/extension/file-utils/vfs/helpers/utils.ts @@ -37,51 +37,69 @@ export abstract class BaseSchemeHandler implements SchemeHandler { /** * @param uri - The URI to resolve. + * @param skipValidateError - If true, do not throw an error if the URI is invalid. * @returns The base URI. * @example * project:/// -> project:// */ - abstract resolveBaseUriSync(uri: string): string + abstract resolveBaseUriSync(uri: string, skipValidateError?: boolean): string /** * @param uri - The URI to resolve. + * @param skipValidateError - If true, do not throw an error if the URI is invalid. * @returns The base URI. * @example * project:/// -> project:// */ - abstract resolveBaseUriAsync(uri: string): Promise + abstract resolveBaseUriAsync( + uri: string, + noThrowError?: boolean + ): Promise /** * @param uri - The URI to resolve. + * @param skipValidateError - If true, do not throw an error if the URI is invalid. * @returns The base path. * @example * project:/// -> /projects/ */ - abstract resolveBasePathSync(uri: string): string + abstract resolveBasePathSync(uri: string, skipValidateError?: boolean): string /** * @param uri - The URI to resolve. + * @param skipValidateError - If true, do not throw an error if the URI is invalid. * @returns The base path. * @example * project:/// -> /projects/ */ - abstract resolveBasePathAsync(uri: string): Promise + abstract resolveBasePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise /** * @param uri - The URI to resolve. + * @param skipValidateError - If true, do not throw an error if the URI is invalid. * @returns The relative path. * @example * project:/// -> */ - abstract resolveRelativePathSync(uri: string): string + abstract resolveRelativePathSync( + uri: string, + skipValidateError?: boolean + ): string /** * @param uri - The URI to resolve. + * @param skipValidateError - If true, do not throw an error if the URI is invalid. * @returns The relative path. * @example * project:/// -> */ - abstract resolveRelativePathAsync(uri: string): Promise + abstract resolveRelativePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise /** * @param uri - The URI to resolve. diff --git a/src/extension/file-utils/vfs/schemes/doc-scheme.ts b/src/extension/file-utils/vfs/schemes/doc-scheme.ts index c9b81b5..72fe94e 100644 --- a/src/extension/file-utils/vfs/schemes/doc-scheme.ts +++ b/src/extension/file-utils/vfs/schemes/doc-scheme.ts @@ -21,44 +21,56 @@ export class DocSchemeHandler extends BaseSchemeHandler { return toUnixPath(docCrawlerPath) } - resolveBaseUriSync(uri: string): string { + resolveBaseUriSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) const [siteName] = uriHelper.getPathSegments() - if (!siteName) throw new Error('Invalid doc URI: missing site name') + if (!siteName && !skipValidateError) + throw new Error('Invalid doc URI: missing site name') - return SchemeUriHelper.create(this.scheme, siteName) + return SchemeUriHelper.create(this.scheme, siteName || '') } - async resolveBaseUriAsync(uri: string): Promise { - return this.resolveBaseUriSync(uri) + async resolveBaseUriAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveBaseUriSync(uri, skipValidateError) } resolveBasePathSync(): string { throw new Error('Not implemented') } - async resolveBasePathAsync(uri: string): Promise { + async resolveBasePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { const uriHelper = new SchemeUriHelper(uri) const [siteName] = uriHelper.getPathSegments() - if (!siteName) throw new Error('Invalid doc URI: missing site name') + if (!siteName && !skipValidateError) + throw new Error('Invalid doc URI: missing site name') - const docPath = await this.getDocPath(siteName) + const docPath = await this.getDocPath(siteName || '') return docPath } - resolveRelativePathSync(uri: string): string { + resolveRelativePathSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) const [siteName, ...relativePathParts] = uriHelper.getPathSegments() - if (!siteName) throw new Error('Invalid doc URI: missing site name') + if (!siteName && !skipValidateError) + throw new Error('Invalid doc URI: missing site name') return relativePathParts.join('/') || './' } - async resolveRelativePathAsync(uri: string): Promise { - return this.resolveRelativePathSync(uri) + async resolveRelativePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveRelativePathSync(uri, skipValidateError) } resolveFullPathSync(): string { @@ -66,8 +78,8 @@ export class DocSchemeHandler extends BaseSchemeHandler { } async resolveFullPathAsync(uri: string): Promise { - const basePath = await this.resolveBaseUriAsync(uri) - const relativePath = this.resolveRelativePathSync(uri) + const basePath = await this.resolveBaseUriAsync(uri, true) + const relativePath = this.resolveRelativePathSync(uri, true) return SchemeUriHelper.join(basePath, relativePath) } diff --git a/src/extension/file-utils/vfs/schemes/git-project-scheme.ts b/src/extension/file-utils/vfs/schemes/git-project-scheme.ts index b24bc07..a98e103 100644 --- a/src/extension/file-utils/vfs/schemes/git-project-scheme.ts +++ b/src/extension/file-utils/vfs/schemes/git-project-scheme.ts @@ -12,57 +12,75 @@ export class GitProjectSchemeHandler extends BaseSchemeHandler { } private async getGitProjectPath( - name: string, - type: GitProjectType + type: GitProjectType, + name: string ): Promise { const gitProjectsPath = await aidePaths.getGitProjectsPath() return SchemeUriHelper.join(gitProjectsPath, type, name) } - resolveBaseUriSync(uri: string): string { + resolveBaseUriSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) const [type, name] = uriHelper.getPathSegments() - if (!type) throw new Error('Invalid git project URI: missing type') - if (!name) throw new Error('Invalid git project URI: missing project name') + if (!type && !skipValidateError) + throw new Error('Invalid git project URI: missing type') + if (!name && !skipValidateError) + throw new Error('Invalid git project URI: missing project name') - return SchemeUriHelper.create(this.scheme, SchemeUriHelper.join(type, name)) + return SchemeUriHelper.create( + this.scheme, + SchemeUriHelper.join(type || '', name || '') + ) } - async resolveBaseUriAsync(uri: string): Promise { - return this.resolveBaseUriSync(uri) + async resolveBaseUriAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveBaseUriSync(uri, skipValidateError) } resolveBasePathSync(): string { throw new Error('Not implemented') } - async resolveBasePathAsync(uri: string): Promise { + async resolveBasePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { const uriHelper = new SchemeUriHelper(uri) const [type, name] = uriHelper.getPathSegments() - if (!type) throw new Error('Invalid git project URI: missing type') - if (!name) throw new Error('Invalid git project URI: missing project name') + if (!type && !skipValidateError) + throw new Error('Invalid git project URI: missing type') + if (!name && !skipValidateError) + throw new Error('Invalid git project URI: missing project name') const projectPath = await this.getGitProjectPath( - name, - type as GitProjectType + type as GitProjectType, + name || '' ) return projectPath } - resolveRelativePathSync(uri: string): string { + resolveRelativePathSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) const [type, name, ...relativePathParts] = uriHelper.getPathSegments() - if (!type) throw new Error('Invalid git project URI: missing type') - if (!name) throw new Error('Invalid git project URI: missing project name') + if (!type && !skipValidateError) + throw new Error('Invalid git project URI: missing type') + if (!name && !skipValidateError) + throw new Error('Invalid git project URI: missing project name') return relativePathParts.join('/') || './' } - async resolveRelativePathAsync(uri: string): Promise { - return this.resolveRelativePathSync(uri) + async resolveRelativePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveRelativePathSync(uri, skipValidateError) } resolveFullPathSync(): string { @@ -70,8 +88,8 @@ export class GitProjectSchemeHandler extends BaseSchemeHandler { } async resolveFullPathAsync(uri: string): Promise { - const basePath = await this.resolveBasePathAsync(uri) - const relativePath = this.resolveRelativePathSync(uri) + const basePath = await this.resolveBasePathAsync(uri, true) + const relativePath = this.resolveRelativePathSync(uri, true) return SchemeUriHelper.join(basePath, relativePath) } diff --git a/src/extension/file-utils/vfs/schemes/project-scheme.ts b/src/extension/file-utils/vfs/schemes/project-scheme.ts index e755a53..c03fbc9 100644 --- a/src/extension/file-utils/vfs/schemes/project-scheme.ts +++ b/src/extension/file-utils/vfs/schemes/project-scheme.ts @@ -12,54 +12,63 @@ export class ProjectSchemeHandler extends BaseSchemeHandler { super(UriScheme.Project) } - private async getProjectPath(projectName: string): Promise { + private async getProjectPath(name: string): Promise { const projects = await projectDB.getAll() - const project = projects.find(p => p.name === projectName) - if (!project) throw new Error(`Project: ${projectName} not found`) + const project = projects.find(p => p.name === name) + if (!project) throw new Error(`Project: ${name} not found`) return toUnixPath(project.path) } - resolveBaseUriSync(uri: string): string { + resolveBaseUriSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) const [projectName] = uriHelper.getPathSegments() - if (!projectName) + if (!projectName && !skipValidateError) throw new Error('Invalid project URI: missing project name') - return SchemeUriHelper.create(this.scheme, projectName) + return SchemeUriHelper.create(this.scheme, projectName || '') } - async resolveBaseUriAsync(uri: string): Promise { - return this.resolveBaseUriSync(uri) + async resolveBaseUriAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveBaseUriSync(uri, skipValidateError) } resolveBasePathSync(): string { throw new Error('Not implemented') } - async resolveBasePathAsync(uri: string): Promise { + async resolveBasePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { const uriHelper = new SchemeUriHelper(uri) const [projectName] = uriHelper.getPathSegments() - if (!projectName) + if (!projectName && !skipValidateError) throw new Error('Invalid project URI: missing project name') - const projectPath = await this.getProjectPath(projectName) + const projectPath = await this.getProjectPath(projectName || '') return projectPath } - resolveRelativePathSync(uri: string): string { + resolveRelativePathSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) const [projectName, ...relativePathParts] = uriHelper.getPathSegments() - if (!projectName) + if (!projectName && !skipValidateError) throw new Error('Invalid project URI: missing project name') return relativePathParts.join('/') || './' } - async resolveRelativePathAsync(uri: string): Promise { - return this.resolveRelativePathSync(uri) + async resolveRelativePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveRelativePathSync(uri, skipValidateError) } resolveFullPathSync(): string { @@ -67,18 +76,15 @@ export class ProjectSchemeHandler extends BaseSchemeHandler { } async resolveFullPathAsync(uri: string): Promise { - const basePath = await this.resolveBasePathAsync(uri) - const relativePath = this.resolveRelativePathSync(uri) + const basePath = await this.resolveBasePathAsync(uri, true) + const relativePath = this.resolveRelativePathSync(uri, true) return SchemeUriHelper.join(basePath, relativePath) } - createSchemeUri(props: { - projectName: string - relativePath: string - }): string { + createSchemeUri(props: { name: string; relativePath: string }): string { return SchemeUriHelper.create( this.scheme, - SchemeUriHelper.join(props.projectName, props.relativePath) + SchemeUriHelper.join(props.name, props.relativePath) ) } } diff --git a/src/extension/file-utils/vfs/schemes/workspace-scheme.ts b/src/extension/file-utils/vfs/schemes/workspace-scheme.ts index 5dcf1e1..63186ee 100644 --- a/src/extension/file-utils/vfs/schemes/workspace-scheme.ts +++ b/src/extension/file-utils/vfs/schemes/workspace-scheme.ts @@ -1,3 +1,4 @@ +/* eslint-disable unused-imports/no-unused-vars */ import { t } from '@extension/i18n' import { getWorkspaceFolder } from '@extension/utils' import { toUnixPath } from '@shared/utils/common' @@ -12,38 +13,46 @@ export class WorkspaceSchemeHandler extends BaseSchemeHandler { super(UriScheme.Workspace) } - // eslint-disable-next-line unused-imports/no-unused-vars - resolveBaseUriSync(uri: string): string { + resolveBaseUriSync(uri: string, skipValidateError?: boolean): string { return SchemeUriHelper.create(this.scheme, '') } - async resolveBaseUriAsync(uri: string): Promise { - return this.resolveBaseUriSync(uri) + async resolveBaseUriAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveBaseUriSync(uri, skipValidateError) } - resolveBasePathSync(): string { + resolveBasePathSync(uri: string, skipValidateError?: boolean): string { const workspaceFolder = getWorkspaceFolder() if (!workspaceFolder) throw new Error(t('error.noWorkspace')) return toUnixPath(workspaceFolder.uri.fsPath) } - async resolveBasePathAsync(): Promise { - return this.resolveBasePathSync() + async resolveBasePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveBasePathSync(uri, skipValidateError) } - resolveRelativePathSync(uri: string): string { + resolveRelativePathSync(uri: string, skipValidateError?: boolean): string { const uriHelper = new SchemeUriHelper(uri) return uriHelper.getPath() || './' } - async resolveRelativePathAsync(uri: string): Promise { - return this.resolveRelativePathSync(uri) + async resolveRelativePathAsync( + uri: string, + skipValidateError?: boolean + ): Promise { + return this.resolveRelativePathSync(uri, skipValidateError) } resolveFullPathSync(uri: string): string { - const basePath = this.resolveBasePathSync() - const relativePath = this.resolveRelativePathSync(uri) + const basePath = this.resolveBasePathSync(uri, true) + const relativePath = this.resolveRelativePathSync(uri, true) return SchemeUriHelper.join(basePath, relativePath) } diff --git a/src/extension/registers/server-plugin-register.ts b/src/extension/registers/server-plugin-register.ts index 5592a94..ba75eb5 100644 --- a/src/extension/registers/server-plugin-register.ts +++ b/src/extension/registers/server-plugin-register.ts @@ -3,6 +3,7 @@ import { AgentServerPluginRegistry } from '@shared/plugins/agents/_base/server/a import { createAgentServerPlugins } from '@shared/plugins/agents/_base/server/agent-server-plugins' import { MentionServerPluginRegistry } from '@shared/plugins/mentions/_base/server/mention-server-plugin-registry' import { createMentionServerPlugins } from '@shared/plugins/mentions/_base/server/mention-server-plugins' +import { settledPromiseResults } from '@shared/utils/common' import * as vscode from 'vscode' import { BaseRegister } from './base-register' @@ -27,7 +28,7 @@ export class ServerPluginRegister extends BaseRegister { const agentServerPluginRegistry = new AgentServerPluginRegistry() const agentPlugins = createAgentServerPlugins() - await Promise.allSettled([ + await settledPromiseResults([ ...mentionPlugins.map(plugin => mentionServerPluginRegistry.loadPlugin(plugin) ), diff --git a/src/shared/plugins/_shared/merge-code-snippets.ts b/src/shared/plugins/_shared/merge-code-snippets.ts index a095ac0..8388ffa 100644 --- a/src/shared/plugins/_shared/merge-code-snippets.ts +++ b/src/shared/plugins/_shared/merge-code-snippets.ts @@ -1,5 +1,6 @@ import { vfs } from '@extension/file-utils/vfs' import type { CodeSnippet } from '@shared/plugins/agents/codebase-search-agent-plugin/types' +import { settledPromiseResults } from '@shared/utils/common' export type MergeCodeSnippetsMode = 'default' | 'expanded' @@ -51,7 +52,7 @@ const processSnippets = async ( mode: MergeCodeSnippetsMode, minLines: number ): Promise => { - await Promise.allSettled( + await settledPromiseResults( Object.values(mergedSnippets).map(async snippet => { const fullCode = await vfs.promises.readFile(snippet.schemeUri, 'utf-8') const lines = fullCode.split('\n') diff --git a/src/shared/plugins/mentions/_base/base-to-state.ts b/src/shared/plugins/mentions/_base/base-to-state.ts index 98982ca..951f8d2 100644 --- a/src/shared/plugins/mentions/_base/base-to-state.ts +++ b/src/shared/plugins/mentions/_base/base-to-state.ts @@ -36,6 +36,23 @@ export abstract class BaseToState { return data } + getMentionDataByTypes( + types: T + ): T extends [...infer U] + ? Extract['data'][] + : never { + if (!this.mentions?.length) return [] as any + const data: Mention['data'][] = [] + + this.mentions?.forEach(mention => { + if (types.includes(mention.type) && mention.data) { + data.push(mention.data) + } + }) + + return data as any + } + isMentionExit(type: T): boolean { if (!this.mentions?.length) return false return this.mentions?.some(mention => mention.type === type) || false diff --git a/src/shared/plugins/mentions/doc-mention-plugin/client/doc-mention-client-plugin.tsx b/src/shared/plugins/mentions/doc-mention-plugin/client/doc-mention-client-plugin.tsx index 26aac98..72fcecd 100644 --- a/src/shared/plugins/mentions/doc-mention-plugin/client/doc-mention-client-plugin.tsx +++ b/src/shared/plugins/mentions/doc-mention-plugin/client/doc-mention-client-plugin.tsx @@ -36,7 +36,7 @@ const createUseMentionOptions = }) }) - const docSiteNamesSettingMentionOption: MentionOption = { + const docSettingMentionOption: MentionOption = { id: DocMentionType.DocSetting, type: DocMentionType.DocSetting, label: 'docs setting', @@ -52,7 +52,7 @@ const createUseMentionOptions = } } - const docSiteNamesMentionOptions: MentionOption[] = docSites.map( + const docMentionOptions: MentionOption[] = docSites.map( site => ({ id: `${DocMentionType.Doc}#${site.id}`, @@ -79,10 +79,7 @@ const createUseMentionOptions = icon: , label: 'Docs' }, - children: [ - docSiteNamesSettingMentionOption, - ...docSiteNamesMentionOptions - ] + children: [docSettingMentionOption, ...docMentionOptions] } ] } diff --git a/src/shared/plugins/mentions/fs-mention-plugin/client/fs-mention-client-plugin.tsx b/src/shared/plugins/mentions/fs-mention-plugin/client/fs-mention-client-plugin.tsx index 65e9ef4..4f7665f 100644 --- a/src/shared/plugins/mentions/fs-mention-plugin/client/fs-mention-client-plugin.tsx +++ b/src/shared/plugins/mentions/fs-mention-plugin/client/fs-mention-client-plugin.tsx @@ -1,9 +1,13 @@ import type { FileInfo, FolderInfo } from '@extension/file-utils/traverse-fs' +import { UriScheme } from '@extension/file-utils/vfs/helpers/types' import { CardStackIcon, CubeIcon, - ExclamationTriangleIcon + DashboardIcon, + ExclamationTriangleIcon, + GearIcon } from '@radix-ui/react-icons' +import type { GitProject, GitProjectType, Project } from '@shared/entities' import { createMentionClientPlugin, type MentionClientPluginSetupProps @@ -18,9 +22,16 @@ import { api } from '@webview/network/actions-api' import { SearchSortStrategy, type MentionOption } from '@webview/types/chat' import { getFileNameFromPath } from '@webview/utils/path' import { optimizeSchemeUriRender } from '@webview/utils/scheme-uri' -import { ChevronRightIcon, FileIcon, FolderTreeIcon } from 'lucide-react' +import { + ChevronRightIcon, + FileIcon, + FolderGit2Icon, + FolderTreeIcon +} from 'lucide-react' +import { useNavigate } from 'react-router' import { FsMentionType, type TreeInfo } from '../types' +import { BitbucketIcon, GithubIcon, GitlabIcon } from './icons' import { MentionFilePreview } from './mention-file-preview' import { MentionFolderPreview } from './mention-folder-preview' import { MentionTreePreview } from './mention-tree-preview' @@ -38,6 +49,7 @@ export const FsMentionClientPlugin = createMentionClientPlugin({ const createUseMentionOptions = (props: MentionClientPluginSetupProps) => (): UseMentionOptionsReturns => { + const navigate = useNavigate() const { data: files = [] } = useQuery({ queryKey: ['realtime', 'files'], queryFn: () => @@ -76,6 +88,40 @@ const createUseMentionOptions = }) }) + const { data: projects = [] } = useQuery({ + queryKey: ['realtime', 'projects'], + queryFn: () => + api.actions().server.project.getProjects({ + actionParams: {} + }) + }) + + const { data: projectFilesAndFolders = {} } = useQuery({ + queryKey: ['realtime', 'projectFilesAndFolders'], + queryFn: () => + api.actions().server.project.getProjectFilesAndFolders({ + actionParams: {} + }), + enabled: projects.length > 0 + }) + + const { data: gitProjects = [] } = useQuery({ + queryKey: ['realtime', 'gitProjects'], + queryFn: () => + api.actions().server.gitProject.getGitProjects({ + actionParams: {} + }) + }) + + const { data: gitProjectFilesAndFolders = {} } = useQuery({ + queryKey: ['realtime', 'gitProjectFilesAndFolders'], + queryFn: () => + api.actions().server.gitProject.getGitProjectFilesAndFolders({ + actionParams: {} + }), + enabled: gitProjects.length > 0 + }) + const filesMentionOptions: MentionOption[] = files.map(file => { const label = getFileNameFromPath(file.schemeUri) const { path } = SchemeUriHelper.parse(file.schemeUri, false) @@ -149,6 +195,220 @@ const createUseMentionOptions = } satisfies MentionOption }) + const localProjectSettingMentionOption: MentionOption = { + id: FsMentionType.ProjectSetting, + type: FsMentionType.ProjectSetting, + label: 'Local Projects setting', + disableAddToEditor: true, + onSelect: () => { + navigate(`/settings?pageId=projectManagement`) + }, + searchKeywords: [ + 'setting', + 'local', + 'projectsetting', + 'localprojectssetting' + ], + itemLayoutProps: { + icon: , + label: 'Local projects setting', + details: '' + } + } + + const localProjectMentionOptions: MentionOption[] = projects.map( + project => { + const projectItems = projectFilesAndFolders[project.id] || [] + const fileOptions: MentionOption[] = [] + const folderOptions: MentionOption[] = [] + + projectItems.forEach(item => { + const label = getFileNameFromPath(item.schemeUri) || 'ROOT' + const { path } = SchemeUriHelper.parse(item.schemeUri, false) + const schemeUriForRender = optimizeSchemeUriRender(item.schemeUri, { + removeSchemes: [UriScheme.Project], + removePathPrefixPart: 1 + }) + + if (item.type === 'file') { + fileOptions.push({ + id: `${FsMentionType.ProjectFile}#${item.schemeUri}`, + type: FsMentionType.ProjectFile, + label, + labelForInsertEditor: item.schemeUri, + data: item, + searchKeywords: [path, label], + searchSortStrategy: SearchSortStrategy.EndMatch, + itemLayoutProps: { + icon: ( + + ), + label, + details: schemeUriForRender + }, + customRenderPreview: MentionFilePreview + }) + } else { + folderOptions.push({ + id: `${FsMentionType.ProjectFolder}#${item.schemeUri}`, + type: FsMentionType.ProjectFolder, + label, + labelForInsertEditor: item.schemeUri, + data: item, + searchKeywords: [path, label], + searchSortStrategy: SearchSortStrategy.EndMatch, + itemLayoutProps: { + icon: ( + <> + + + + ), + label, + details: schemeUriForRender + }, + customRenderPreview: MentionFolderPreview + }) + } + }) + + return { + id: `${FsMentionType.Project}#${project.name}`, + type: FsMentionType.Project, + label: project.name, + data: project, + searchKeywords: [project.name, project.path], + searchSortStrategy: SearchSortStrategy.EndMatch, + itemLayoutProps: { + icon: , + label: project.name, + details: project.path + }, + children: [...fileOptions, ...folderOptions] + } satisfies MentionOption + } + ) + + const gitProjectSettingMentionOption: MentionOption = { + id: FsMentionType.GitProjectSetting, + type: FsMentionType.GitProjectSetting, + label: 'Git Projects setting', + disableAddToEditor: true, + onSelect: () => { + navigate(`/settings?pageId=gitProjectManagement`) + }, + searchKeywords: [ + 'setting', + 'git', + 'projectsetting', + 'gitprojectssetting' + ], + itemLayoutProps: { + icon: , + label: 'Git projects setting', + details: '' + } + } + + const gitProjectMentionOptions: MentionOption[] = gitProjects.map( + project => { + const projectItems = gitProjectFilesAndFolders[project.id] || [] + const fileOptions: MentionOption[] = [] + const folderOptions: MentionOption[] = [] + + projectItems.forEach(item => { + const label = getFileNameFromPath(item.schemeUri) || 'ROOT' + const { path } = SchemeUriHelper.parse(item.schemeUri, false) + const schemeUriForRender = optimizeSchemeUriRender(item.schemeUri, { + removeSchemes: [UriScheme.GitProject], + removePathPrefixPart: 2 + }) + + if (item.type === 'file') { + fileOptions.push({ + id: `${FsMentionType.GitProjectFile}#${item.schemeUri}`, + type: FsMentionType.GitProjectFile, + label, + labelForInsertEditor: item.schemeUri, + data: item, + searchKeywords: [path, label], + searchSortStrategy: SearchSortStrategy.EndMatch, + itemLayoutProps: { + icon: ( + + ), + label, + details: schemeUriForRender + }, + customRenderPreview: MentionFilePreview + }) + } else { + folderOptions.push({ + id: `${FsMentionType.GitProjectFolder}#${item.schemeUri}`, + type: FsMentionType.GitProjectFolder, + label, + labelForInsertEditor: item.schemeUri, + data: item, + searchKeywords: [path, label], + searchSortStrategy: SearchSortStrategy.EndMatch, + itemLayoutProps: { + icon: ( + <> + + + + ), + label, + details: schemeUriForRender + }, + customRenderPreview: MentionFolderPreview + }) + } + }) + + const typeIconMap: Record< + GitProjectType, + React.FC> + > = { + github: GithubIcon, + gitlab: GitlabIcon, + bitbucket: BitbucketIcon + } + const Icon = typeIconMap[project.type] + + return { + id: `${FsMentionType.GitProject}#${project.name}`, + type: FsMentionType.GitProject, + label: project.name, + data: project, + searchKeywords: [project.name, project.repoUrl, project.type], + searchSortStrategy: SearchSortStrategy.EndMatch, + itemLayoutProps: { + icon: , + label: project.name, + details: project.repoUrl + }, + children: [...fileOptions, ...folderOptions] + } satisfies MentionOption + } + ) + return [ { id: FsMentionType.Files, @@ -214,7 +474,7 @@ const createUseMentionOptions = type: FsMentionType.Errors, label: 'Errors', data: editorErrors, - topLevelSort: 7, + topLevelSort: editorErrors.length > 0 ? 7 : -1, searchKeywords: ['errors', 'warnings', 'diagnostics'], itemLayoutProps: { icon: , @@ -227,6 +487,33 @@ const createUseMentionOptions = ) } + }, + { + id: FsMentionType.Projects, + type: FsMentionType.Projects, + label: 'Local Projects', + topLevelSort: 8, + searchKeywords: ['projects', 'local', 'localprojects'], + children: [ + localProjectSettingMentionOption, + ...localProjectMentionOptions + ], + itemLayoutProps: { + icon: , + label: 'Local Projects' + } + }, + { + id: FsMentionType.GitProjects, + type: FsMentionType.GitProjects, + label: 'Git Projects', + topLevelSort: 9, + searchKeywords: ['git', 'projects', 'repositories', 'gitprojects'], + children: [gitProjectSettingMentionOption, ...gitProjectMentionOptions], + itemLayoutProps: { + icon: , + label: 'Git Projects' + } } ] } diff --git a/src/shared/plugins/mentions/fs-mention-plugin/client/icons.tsx b/src/shared/plugins/mentions/fs-mention-plugin/client/icons.tsx new file mode 100644 index 0000000..c993fa3 --- /dev/null +++ b/src/shared/plugins/mentions/fs-mention-plugin/client/icons.tsx @@ -0,0 +1,38 @@ +export const GitlabIcon: React.FC> = props => ( + + GitLab + + +) + +export const GithubIcon: React.FC> = props => ( + + GitHub + + +) + +export const BitbucketIcon: React.FC> = props => ( + + Bitbucket + + +) diff --git a/src/shared/plugins/mentions/fs-mention-plugin/fs-to-state.ts b/src/shared/plugins/mentions/fs-mention-plugin/fs-to-state.ts index 0caba82..96ab98a 100644 --- a/src/shared/plugins/mentions/fs-mention-plugin/fs-to-state.ts +++ b/src/shared/plugins/mentions/fs-mention-plugin/fs-to-state.ts @@ -8,8 +8,16 @@ import { FsMentionType, type FsMention } from './types' export class FsToState extends BaseToState { toMentionsState() { return { - selectedFiles: this.getMentionDataByType(FsMentionType.File), - selectedFolders: this.getMentionDataByType(FsMentionType.Folder), + selectedFiles: this.getMentionDataByTypes([ + FsMentionType.File, + FsMentionType.ProjectFile, + FsMentionType.GitProjectFile + ] as const), + selectedFolders: this.getMentionDataByTypes([ + FsMentionType.Folder, + FsMentionType.ProjectFolder, + FsMentionType.GitProjectFolder + ] as const), selectedTrees: this.getMentionDataByType(FsMentionType.Tree), codeChunks: this.getMentionDataByType(FsMentionType.Code), enableCodebaseAgent: this.isMentionExit(FsMentionType.Codebase), diff --git a/src/shared/plugins/mentions/fs-mention-plugin/server/fs-mention-server-utils-provider.ts b/src/shared/plugins/mentions/fs-mention-plugin/server/fs-mention-server-utils-provider.ts index a02f4e9..e0a0a81 100644 --- a/src/shared/plugins/mentions/fs-mention-plugin/server/fs-mention-server-utils-provider.ts +++ b/src/shared/plugins/mentions/fs-mention-plugin/server/fs-mention-server-utils-provider.ts @@ -1,6 +1,6 @@ import type { FileInfo, FolderInfo } from '@extension/file-utils/traverse-fs' import type { ActionRegister } from '@extension/registers/action-register' -import type { Mention } from '@shared/entities' +import type { GitProject, Mention, Project } from '@shared/entities' import type { MentionServerUtilsProvider } from '@shared/plugins/mentions/_base/server/create-mention-provider-manager' import { FsMentionType, type FsMention, type TreeInfo } from '../types' @@ -39,6 +39,16 @@ export class FsMentionServerUtilsProvider } }) + const projects = await actionRegister.actions().server.project.getProjects({ + actionParams: {} + }) + + const gitProjects = await actionRegister + .actions() + .server.gitProject.getGitProjects({ + actionParams: {} + }) + const fileSchemeUriMapFile = new Map() for (const file of files) { @@ -57,6 +67,16 @@ export class FsMentionServerUtilsProvider fileSchemeUriMapTree.set(tree.schemeUri, tree) } + const projectIdMapProject = new Map() + for (const project of projects) { + projectIdMapProject.set(project.id, project) + } + + const gitProjectIdMapProject = new Map() + for (const project of gitProjects) { + gitProjectIdMapProject.set(project.id, project) + } + return (_mention: Mention) => { const mention = { ..._mention } as FsMention switch (mention.type) { @@ -79,6 +99,16 @@ export class FsMentionServerUtilsProvider mention.data = editorErrors break + case FsMentionType.Project: + const project = projectIdMapProject.get(mention.data.id) + if (project) mention.data = project + break + + case FsMentionType.GitProject: + const gitProject = gitProjectIdMapProject.get(mention.data.id) + if (gitProject) mention.data = gitProject + break + default: break } diff --git a/src/shared/plugins/mentions/fs-mention-plugin/types.ts b/src/shared/plugins/mentions/fs-mention-plugin/types.ts index 20113bb..b499c25 100644 --- a/src/shared/plugins/mentions/fs-mention-plugin/types.ts +++ b/src/shared/plugins/mentions/fs-mention-plugin/types.ts @@ -1,5 +1,5 @@ import type { FileInfo, FolderInfo } from '@extension/file-utils/traverse-fs' -import type { Mention } from '@shared/entities' +import type { GitProject, Mention, Project } from '@shared/entities' import type { CodeSnippet } from '@shared/plugins/agents/codebase-search-agent-plugin/types' import { MentionPluginId } from '../_base/types' @@ -13,7 +13,17 @@ export enum FsMentionType { Tree = `${MentionPluginId.Fs}#tree`, Code = `${MentionPluginId.Fs}#code`, Codebase = `${MentionPluginId.Fs}#codebase`, - Errors = `${MentionPluginId.Fs}#errors` + Errors = `${MentionPluginId.Fs}#errors`, + Projects = `${MentionPluginId.Fs}#projects`, + ProjectSetting = `${MentionPluginId.Fs}#project-setting`, + Project = `${MentionPluginId.Fs}#project`, + ProjectFile = `${MentionPluginId.Fs}#project-file`, + ProjectFolder = `${MentionPluginId.Fs}#project-folder`, + GitProjects = `${MentionPluginId.Fs}#git-projects`, + GitProject = `${MentionPluginId.Fs}#git-project`, + GitProjectSetting = `${MentionPluginId.Fs}#git-project-setting`, + GitProjectFile = `${MentionPluginId.Fs}#git-project-file`, + GitProjectFolder = `${MentionPluginId.Fs}#git-project-folder` } export type FileMention = Mention @@ -22,6 +32,8 @@ export type TreeMention = Mention export type CodeMention = Mention export type CodebaseMention = Mention export type ErrorMention = Mention +export type ProjectMention = Mention +export type GitProjectMention = Mention export type FsMention = | FileMention @@ -30,6 +42,8 @@ export type FsMention = | CodeMention | CodebaseMention | ErrorMention + | ProjectMention + | GitProjectMention export interface CodeChunk { code: string diff --git a/src/shared/plugins/mentions/prompt-snippet-mention-plugin/client/prompt-snippet-mention-client-plugin.tsx b/src/shared/plugins/mentions/prompt-snippet-mention-plugin/client/prompt-snippet-mention-client-plugin.tsx index a6170d8..6c095f1 100644 --- a/src/shared/plugins/mentions/prompt-snippet-mention-plugin/client/prompt-snippet-mention-client-plugin.tsx +++ b/src/shared/plugins/mentions/prompt-snippet-mention-plugin/client/prompt-snippet-mention-client-plugin.tsx @@ -43,7 +43,7 @@ const createUseMentionOptions = const textContent = getAllTextFromConversationContents(snippet.contents) return { - id: `${PromptSnippetMentionType.PromptSnippet}#${snippet.id}`, + id: `${PromptSnippetMentionType.PromptSnippet}#${snippet.title}`, type: PromptSnippetMentionType.PromptSnippet, label, data: snippet, diff --git a/src/shared/plugins/mentions/terminal-mention-plugin/client/terminal-mention-client-plugin.tsx b/src/shared/plugins/mentions/terminal-mention-plugin/client/terminal-mention-client-plugin.tsx index aaab772..0c0dc0f 100644 --- a/src/shared/plugins/mentions/terminal-mention-plugin/client/terminal-mention-client-plugin.tsx +++ b/src/shared/plugins/mentions/terminal-mention-plugin/client/terminal-mention-client-plugin.tsx @@ -53,7 +53,7 @@ const createUseMentionOptions = id: TerminalMentionType.Terminals, type: TerminalMentionType.Terminals, label: 'Terminals', - topLevelSort: 6, + topLevelSort: terminalMentionOptions.length > 0 ? 6 : -1, searchKeywords: ['terminal', 'shell', 'command'], itemLayoutProps: { icon: , diff --git a/src/shared/utils/scheme-uri-helper.ts b/src/shared/utils/scheme-uri-helper.ts index 9483969..c5d959c 100644 --- a/src/shared/utils/scheme-uri-helper.ts +++ b/src/shared/utils/scheme-uri-helper.ts @@ -75,7 +75,9 @@ export class SchemeUriHelper { if (!paths.length) return uri const { scheme, path: basePath } = SchemeUriHelper.parse(uri, false) - const normalizedPaths = paths.map(p => SchemeUriHelper.normalizePath(p)) + const normalizedPaths = paths + .map(p => SchemeUriHelper.normalizePath(p)) + .filter(Boolean) // Handle absolute paths in arguments if (normalizedPaths.some(p => p.startsWith('/'))) { @@ -85,12 +87,14 @@ export class SchemeUriHelper { ) const relevantPaths = normalizedPaths.slice(lastAbsolutePathIndex) const joinedPath = SchemeUriHelper.normalizePath(relevantPaths.join('/')) + return scheme ? SchemeUriHelper.create(scheme, joinedPath) : joinedPath } const joinedPath = SchemeUriHelper.normalizePath( - [basePath, normalizedPaths].filter(Boolean).join('/') + [basePath, ...normalizedPaths].filter(Boolean).join('/') ) + return scheme ? SchemeUriHelper.create(scheme, joinedPath) : joinedPath } diff --git a/src/webview/components/settings/custom-renders/project-management/project-card.tsx b/src/webview/components/settings/custom-renders/project-management/project-card.tsx index 8107344..38807ea 100644 --- a/src/webview/components/settings/custom-renders/project-management/project-card.tsx +++ b/src/webview/components/settings/custom-renders/project-management/project-card.tsx @@ -77,7 +77,7 @@ export const ProjectCard = ({
{project.description && renderField('Description', project.description)} {renderField( - 'Path', + 'Folder Path',