From 168b92f6c4816fe00ec79b211e8d69945a6d01c7 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Fri, 9 May 2025 15:38:32 -0600 Subject: [PATCH 1/4] Add Void: Copy Prompt as menu item to the ExplorerContext window --- .../contrib/void/browser/fileService.ts | 89 +++++++++++++++++++ .../contrib/void/browser/void.contribution.ts | 3 + .../contrib/void/common/prompt/prompts.ts | 6 +- 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/fileService.ts diff --git a/src/vs/workbench/contrib/void/browser/fileService.ts b/src/vs/workbench/contrib/void/browser/fileService.ts new file mode 100644 index 00000000..8857feb4 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/fileService.ts @@ -0,0 +1,89 @@ +import { localize2 } from '../../../../nls.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Action2, registerAction2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { IDirectoryStrService } from '../common/directoryStrService.js'; +import { readFile, DEFAULT_FILE_SIZE_LIMIT } from '../common/prompt/prompts.js'; + + + +async function filePrompt(fileService: IFileService, uri: URI, clipboardService: IClipboardService): Promise { + const { val } = await readFile(fileService, uri, DEFAULT_FILE_SIZE_LIMIT) + const fileName = uri.fsPath.split('/').pop() ?? '' + if (!val) { + throw new Error('Failed to copy prompt') + } + const prompt = ` +${fileName}: +\`\`\` +${val} +\`\`\``.trim() + return prompt +} + +/** + * Add a menu item to the explorer context menu that copies a prompt for the selected file or directory. + * + * Example file prompt: + * + * ``` + * index.js: + * \`\`\` + * console.log('Hello World!'); + * \`\`\` + * + * Example directory prompt: + * ``` + * Directory of /path/to/src: + * src/ + * ├── index.ts + * ├── src.ts + * ├── latest/ + * │ ├── index.ts + * │ └── src.ts + * ├── types.ts + * └── util.ts + * ``` + */ +class FilePromptActionService extends Action2 { + private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt' + + constructor() { + super({ + id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID, + title: localize2('voidCopyPrompt', "Void: Copy Prompt"), + menu: [{ + id: MenuId.ExplorerContext, + group: '8_void', + order: 1, + }] + }); + } + + async run(accessor: ServicesAccessor, uri: URI): Promise { + try { + const fileService = accessor.get(IFileService); + const clipboardService = accessor.get(IClipboardService) + const directoryStrService = accessor.get(IDirectoryStrService) + const stat = await fileService.resolve(uri) + const prompt = stat.isFile + ? await filePrompt(fileService, uri, clipboardService) + : await directoryStrService.getDirectoryStrTool(uri) + await clipboardService.writeText(prompt) + } catch (error) { + const notificationService = accessor.get(INotificationService) + FilePromptActionService._onError(notificationService, error) + } + } + + private static _onError(notificationService: INotificationService, error: Error) { + const errorMessage = localize2('voidCopyPromptError', 'Failed to copy prompt') + notificationService.error(errorMessage.value) + throw error + } +} + +registerAction2(FilePromptActionService) diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index c0e84606..3ab89abf 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -58,6 +58,9 @@ import './voidOnboardingService.js' // register misc service import './miscWokrbenchContrib.js' +// register file service (for explorer context menu) +import './fileService.js' + // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- // llmMessage diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 613142b8..5f53cd07 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -525,7 +525,9 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`) // chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', })) // } -const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{ +export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000 + +export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{ val: string, truncated: boolean, fullFileLen: number, @@ -561,7 +563,7 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S selnsStrs = await Promise.all(currSelns?.map(async (s) => { if (s.type === 'File' || s.type === 'CodeSelection') { - const { val } = await readFile(opts.fileService, s.uri, 2_000_000) + const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT) const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : '' const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}` const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}` From 08274637e15ffc70ddca913f302c3cf5f28f3fd9 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Fri, 9 May 2025 15:40:34 -0600 Subject: [PATCH 2/4] Small touchups --- src/vs/workbench/contrib/void/browser/fileService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/fileService.ts b/src/vs/workbench/contrib/void/browser/fileService.ts index 8857feb4..518803ed 100644 --- a/src/vs/workbench/contrib/void/browser/fileService.ts +++ b/src/vs/workbench/contrib/void/browser/fileService.ts @@ -9,7 +9,6 @@ import { IDirectoryStrService } from '../common/directoryStrService.js'; import { readFile, DEFAULT_FILE_SIZE_LIMIT } from '../common/prompt/prompts.js'; - async function filePrompt(fileService: IFileService, uri: URI, clipboardService: IClipboardService): Promise { const { val } = await readFile(fileService, uri, DEFAULT_FILE_SIZE_LIMIT) const fileName = uri.fsPath.split('/').pop() ?? '' @@ -54,7 +53,7 @@ class FilePromptActionService extends Action2 { constructor() { super({ id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID, - title: localize2('voidCopyPrompt', "Void: Copy Prompt"), + title: localize2('voidCopyPrompt', 'Void: Copy Prompt'), menu: [{ id: MenuId.ExplorerContext, group: '8_void', From 7066b4285b281345c3d3ef80342c07aba21fff65 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 12 May 2025 18:20:44 -0700 Subject: [PATCH 3/4] file and folder copy uniform --- .../contrib/void/browser/fileService.ts | 88 ++++++++----------- .../contrib/void/common/prompt/prompts.ts | 86 +++++++++++------- 2 files changed, 93 insertions(+), 81 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/fileService.ts b/src/vs/workbench/contrib/void/browser/fileService.ts index 518803ed..93da1b1e 100644 --- a/src/vs/workbench/contrib/void/browser/fileService.ts +++ b/src/vs/workbench/contrib/void/browser/fileService.ts @@ -6,47 +6,11 @@ import { INotificationService } from '../../../../platform/notification/common/n import { IFileService } from '../../../../platform/files/common/files.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { IDirectoryStrService } from '../common/directoryStrService.js'; -import { readFile, DEFAULT_FILE_SIZE_LIMIT } from '../common/prompt/prompts.js'; +import { messageOfSelection } from '../common/prompt/prompts.js'; +import { IVoidModelService } from '../common/voidModelService.js'; -async function filePrompt(fileService: IFileService, uri: URI, clipboardService: IClipboardService): Promise { - const { val } = await readFile(fileService, uri, DEFAULT_FILE_SIZE_LIMIT) - const fileName = uri.fsPath.split('/').pop() ?? '' - if (!val) { - throw new Error('Failed to copy prompt') - } - const prompt = ` -${fileName}: -\`\`\` -${val} -\`\`\``.trim() - return prompt -} -/** - * Add a menu item to the explorer context menu that copies a prompt for the selected file or directory. - * - * Example file prompt: - * - * ``` - * index.js: - * \`\`\` - * console.log('Hello World!'); - * \`\`\` - * - * Example directory prompt: - * ``` - * Directory of /path/to/src: - * src/ - * ├── index.ts - * ├── src.ts - * ├── latest/ - * │ ├── index.ts - * │ └── src.ts - * ├── types.ts - * └── util.ts - * ``` - */ class FilePromptActionService extends Action2 { private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt' @@ -67,22 +31,48 @@ class FilePromptActionService extends Action2 { const fileService = accessor.get(IFileService); const clipboardService = accessor.get(IClipboardService) const directoryStrService = accessor.get(IDirectoryStrService) - const stat = await fileService.resolve(uri) - const prompt = stat.isFile - ? await filePrompt(fileService, uri, clipboardService) - : await directoryStrService.getDirectoryStrTool(uri) - await clipboardService.writeText(prompt) + const voidModelService = accessor.get(IVoidModelService) + + const stat = await fileService.stat(uri) + + const folderOpts = { + maxChildren: 1000, + maxCharsPerFile: 2_000_000, + } as const + + let m: string = 'No contents detected' + if (stat.isFile) { + m = await messageOfSelection({ + type: 'File', + uri, + language: (await voidModelService.getModelSafe(uri)).model?.getLanguageId() || '', + state: { wasAddedAsCurrentFile: false, }, + }, { + folderOpts, + directoryStrService, + fileService, + }) + } + + if (stat.isDirectory) { + m = await messageOfSelection({ + type: 'Folder', + uri, + }, { + folderOpts, + fileService, + directoryStrService, + }) + } + + await clipboardService.writeText(m) + } catch (error) { const notificationService = accessor.get(INotificationService) - FilePromptActionService._onError(notificationService, error) + notificationService.error(error + '') } } - private static _onError(notificationService: INotificationService, error: Error) { - const errorMessage = localize2('voidCopyPromptError', 'Failed to copy prompt') - notificationService.error(errorMessage.value) - throw error - } } registerAction2(FilePromptActionService) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 5f53cd07..a4c0daa8 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -551,46 +551,68 @@ export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimi +export const messageOfSelection = async ( + s: StagingSelectionItem, + opts: { + directoryStrService: IDirectoryStrService, + fileService: IFileService, + folderOpts: { + maxChildren: number, + maxCharsPerFile: number, + } + } +) => { + const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})` + + if (s.type === 'File' || s.type === 'CodeSelection') { + const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT) + const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : '' + const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}` + const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}` + return str + } + else if (s.type === 'Folder') { + const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri) + const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}` + + const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: opts.folderOpts.maxChildren }) + const strOfFiles = await Promise.all(uris.map(async uri => { + const { val, truncated } = await readFile(opts.fileService, uri, opts.folderOpts.maxCharsPerFile) + const truncationStr = truncated ? `\n... file truncated ...` : '' + const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}` + const str = `${uri.fsPath}:\n${content}` + return str + })) + const contentStr = [folderStructure, ...strOfFiles].join('\n\n') + return contentStr + } + else + return '' + +} - -export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null, - opts: { directoryStrService: IDirectoryStrService, fileService: IFileService } +export const chat_userMessageContent = async ( + instructions: string, + currSelns: StagingSelectionItem[] | null, + opts: { + directoryStrService: IDirectoryStrService, + fileService: IFileService + }, ) => { - const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})` - let selnsStrs: string[] = [] - selnsStrs = await Promise.all(currSelns?.map(async (s) => { + const selnsStrs = await Promise.all((currSelns ?? []).map(async (s) => + messageOfSelection(s, { + ...opts, + folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, } + }) + )) - if (s.type === 'File' || s.type === 'CodeSelection') { - const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT) - const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : '' - const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}` - const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}` - return str - } - else if (s.type === 'Folder') { - const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri) - const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}` - const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: 100 }) - const strOfFiles = await Promise.all(uris.map(async uri => { - const { val, truncated } = await readFile(opts.fileService, uri, 100_000) - const truncationStr = truncated ? `\n... file truncated ...` : '' - const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}` - const str = `${uri.fsPath}:\n${content}` - return str - })) - const contentStr = [folderStructure, ...strOfFiles].join('\n\n') - return contentStr - } - else - return '' - }) ?? []) - - const selnsStr = selnsStrs.join('\n') ?? '' let str = '' str += `${instructions}` + + const selnsStr = selnsStrs.join('\n\n') ?? '' if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}` return str; } From f98894ad77c36423a07ba29c54196827b6b04064 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 12 May 2025 18:22:59 -0700 Subject: [PATCH 4/4] format --- .../contrib/void/common/prompt/prompts.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index b5cae7cb..5746067e 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -605,12 +605,14 @@ export const chat_userMessageContent = async ( }, ) => { - const selnsStrs = await Promise.all((currSelns ?? []).map(async (s) => - messageOfSelection(s, { - ...opts, - folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, } - }) - )) + const selnsStrs = await Promise.all( + (currSelns ?? []).map(async (s) => + messageOfSelection(s, { + ...opts, + folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, } + }) + ) + ) let str = ''