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..93da1b1e --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/fileService.ts @@ -0,0 +1,78 @@ +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 { messageOfSelection } from '../common/prompt/prompts.js'; +import { IVoidModelService } from '../common/voidModelService.js'; + + + +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 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) + notificationService.error(error + '') + } + } + +} + +registerAction2(FilePromptActionService) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 6a4d393a..93e26b0d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -239,17 +239,94 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: } -export const ApplyButtonsHTML = ({ +const terminalLanguages = new Set([ + 'bash', + 'shellscript', + 'shell', + 'powershell', + 'bat', + 'zsh', + 'sh', + 'fish', + 'nushell', + 'ksh', + 'xonsh', + 'elvish', +]) + +const ApplyButtonsForTerminal = ({ codeStr, applyBoxId, uri, + language, }: { codeStr: string, applyBoxId: string, -} & ({ + language?: string, uri: URI | 'current'; -}) -) => { +}) => { + const accessor = useAccessor() + const metricsService = accessor.get('IMetricsService') + const terminalToolService = accessor.get('ITerminalToolService') + + const settingsState = useSettingsState() + + const [isShellRunning, setIsShellRunning] = useState(false) + const interruptToolRef = useRef<(() => void) | null>(null) + const isDisabled = isShellRunning + + const onClickSubmit = useCallback(async () => { + if (isShellRunning) return + try { + setIsShellRunning(true) + const terminalId = await terminalToolService.createPersistentTerminal({ cwd: null }) + const { interrupt } = await terminalToolService.runCommand( + codeStr, + { type: 'persistent', persistentTerminalId: terminalId } + ); + interruptToolRef.current = interrupt + metricsService.capture('Execute Shell', { length: codeStr.length }) + } catch (e) { + setIsShellRunning(false) + console.error('Failed to execute in terminal:', e) + } + }, [codeStr, uri, applyBoxId, metricsService, terminalToolService, isShellRunning]) + + if (isShellRunning) { + return ( + { + interruptToolRef.current?.(); + setIsShellRunning(false); + }} + {...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })} + /> + ); + } + if (isDisabled) { + return null + } + return +} + + + +const ApplyButtonsForEdit = ({ + codeStr, + applyBoxId, + uri, + language, +}: { + codeStr: string, + applyBoxId: string, + language?: string, + uri: URI | 'current'; +}) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') @@ -260,7 +337,6 @@ export const ApplyButtonsHTML = ({ const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId }) - const onClickSubmit = useCallback(async () => { if (currStreamStateRef.current === 'streaming') return @@ -287,7 +363,7 @@ export const ApplyButtonsHTML = ({ }) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService]) + }, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService]) const onClickStop = useCallback(() => { @@ -309,9 +385,7 @@ export const ApplyButtonsHTML = ({ if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false }) }, [uri, applyBoxId, editCodeService]) - const currStreamState = currStreamStateRef.current - if (currStreamState === 'streaming') { return } - if (isDisabled) { return null } - - if (currStreamState === 'idle-no-changes') { return } - if (currStreamState === 'idle-has-changes') { return { + const { language } = params + const isShellLanguage = !!language && terminalLanguages.has(language) + + if (isShellLanguage) { + return + } + else { + return + } +} + + + + + export const EditToolAcceptRejectButtonsHTML = ({ codeStr, applyBoxId, @@ -456,7 +547,7 @@ export const BlockCodeApplyWrapper = ({
{currStreamState === 'idle-no-changes' && } - +
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 0e962e09..4112745c 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -89,10 +89,7 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor): Promise { - - - // Get the views service to check if the sidebar is open - // const viewsService = accessor.get(IViewsService) + // Get services const commandService = accessor.get(ICommandService) const viewsService = accessor.get(IViewsService) const metricsService = accessor.get(IMetricsService) @@ -101,31 +98,28 @@ registerAction2(class extends Action2 { metricsService.capture('Ctrl+L', {}) + // capture selection and model before opening the chat panel + const editor = editorService.getActiveCodeEditor() + const model = editor?.getModel() + if (!model) return + + const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' }) + + // open panel const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID) if (!wasAlreadyOpen) { await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID) - return } - - // if was already open - - const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() - if (!model) return - - const editor = editorService.getActiveCodeEditor() - const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' }) - - // if has no selection, close + return - // if (!selectionRange) { - // viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID); - // return; - // } - - + // Add selection to chat // add line selection if (selectionRange) { - editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) + editor?.setSelection({ + startLineNumber: selectionRange.startLineNumber, + endLineNumber: selectionRange.endLineNumber, + startColumn: 1, + endColumn: Number.MAX_SAFE_INTEGER + }) chatThreadService.addNewStagingSelection({ type: 'CodeSelection', uri: model.uri, @@ -142,12 +136,9 @@ registerAction2(class extends Action2 { language: model.getLanguageId(), state: { wasAddedAsCurrentFile: false }, }) - } await chatThreadService.focusCurrentChat() - - } }) diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index b079eb5a..2e18511b 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -22,7 +22,12 @@ export interface ITerminalToolService { readonly _serviceBrand: undefined; listPersistentTerminalIds(): string[]; - runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>; + runCommand(command: string, opts: + | { type: 'persistent', persistentTerminalId: string } + | { type: 'temporary', cwd: string | null, terminalId: string } + // | { type: 'apply', terminalId: string } + ): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>; + focusPersistentTerminal(terminalId: string): Promise persistentTerminalExists(terminalId: string): boolean @@ -277,6 +282,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ terminal.dispose() if (!isPersistent) delete this.temporaryTerminalInstanceOfId[params.terminalId] + else + delete this.persistentTerminalInstanceOfId[params.persistentTerminalId] } const waitForResult = async () => { diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index bba085d8..02edf047 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -430,7 +430,7 @@ export class ToolsService implements IToolsService { }, // --- run_command: async ({ command, cwd, terminalId }) => { - const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId }) + const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'temporary', cwd, terminalId }) return { result: resPromise, interruptTool: interrupt } }, run_persistent_command: async ({ command, persistentTerminalId }) => { 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/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 8e338d07..fe1fc7ac 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -129,6 +129,7 @@ export const defaultModelsOfProvider = { mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ 'codestral-latest', 'mistral-large-latest', + 'mistral-medium-latest', 'ministral-3b-latest', 'ministral-8b-latest', ], @@ -188,7 +189,13 @@ export type VoidStaticModelInfo = { // not stateful export type ModelOverrides = Pick @@ -883,6 +890,15 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici supportsSystemMessage: 'system-role', reasoningCapabilities: false, }, + 'mistral-medium-latest': { // https://openrouter.ai/mistralai/mistral-medium-3 + contextWindow: 131_000, + reservedOutputTokenSpace: 8_192, + cost: { input: 0.40, output: 2.00 }, + supportsFIM: false, + downloadable: { sizeGb: 'not-known' }, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + }, 'codestral-latest': { contextWindow: 256_000, reservedOutputTokenSpace: 8_192, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index b83c1a0f..37f16c84 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -481,7 +481,6 @@ ${directoryStr} details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`) } - details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format: - Include a language if possible. Terminal should have the language 'shell'. - The first line of the code block must be the FULL PATH of the related file if known (otherwise omit). @@ -529,7 +528,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, @@ -553,46 +554,70 @@ const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: numb +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, 2_000_000) - 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; } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index a1c90675..2e05c62d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -147,7 +147,12 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => { - const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel) + const { + modelName, + supportsFIM, + additionalOpenAIPayload, + } = getModelCapabilities(providerName, modelName_, overridesOfModel) + if (!supportsFIM) { if (modelName === modelName_) onError({ message: `Model ${modelName} does not support FIM.`, fullError: null }) @@ -156,7 +161,7 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens return } - const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload: additionalOpenAIPayload }) openai.completions .create({ model: modelName, @@ -236,6 +241,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE modelName, specialToolFormat, reasoningCapabilities, + additionalOpenAIPayload, } = getModelCapabilities(providerName, modelName_, overridesOfModel) const { providerReasoningIOSettings } = getProviderCapabilities(providerName) @@ -243,7 +249,11 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE // reasoning const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {} const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here - const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + + const includeInPayload = { + ...providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo), + ...additionalOpenAIPayload + } // tools const potentialTools = chatMode !== null ? openAITools(chatMode) : null @@ -258,6 +268,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE messages: messages as any, stream: true, ...nativeToolsObj, + ...additionalOpenAIPayload // max_completion_tokens: maxTokens, }