diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 620548ec..4d1b7462 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -86,7 +86,7 @@ function prepareDebPackage(arch) { const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch); gulp.src('resources/linux/debian/control.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`)) + .pipe(replace('@@VERSION@@', `${product.voidVersion}.${packageJson.release}`)) .pipe(replace('@@ARCHITECTURE@@', debArch)) .pipe(replace('@@DEPENDS@@', dependencies.join(', '))) .pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', '))) @@ -201,7 +201,7 @@ function prepareRpmPackage(arch) { .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@NAME_LONG@@', product.nameLong)) .pipe(replace('@@ICON@@', product.linuxIconName)) - .pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`)) + .pipe(replace('@@VERSION@@', `${product.voidVersion}.${packageJson.release}`)) .pipe(replace('@@ARCHITECTURE@@', rpmArch)) .pipe(replace('@@LICENSE@@', product.licenseName)) .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) @@ -277,7 +277,7 @@ function prepareSnapPackage(arch) { const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`)) + .pipe(replace('@@VERSION@@', `${product.voidVersion}.${packageJson.release}`)) // Possible run-on values https://snapcraft.io/docs/architectures .pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch)) .pipe(rename('snap/snapcraft.yaml')); diff --git a/create-appimage.sh b/create-appimage.sh deleted file mode 100644 index 175e7151..00000000 --- a/create-appimage.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -set -e # Exit on error -set -x # Print commands as they are executed - -# Configuration -APP_NAME="void" -APP_VERSION="1.0.0" -ARCH="x86_64" - -export ARCH - -# Check if void binary exists in current directory -if [ ! -f "./void" ]; then - echo "Error: void binary not found in current directory" - exit 1 -fi - -# Check if icon exists -if [ ! -f "./void.png" ]; then - echo "Error: void.png icon not found in current directory" - exit 1 -fi - -# Create temporary directory -TEMP_DIR="$(mktemp -d)" -echo "Created temporary directory: $TEMP_DIR" -APP_DIR="$TEMP_DIR/$APP_NAME.AppDir" - -# Create basic AppDir structure -mkdir -pv "$APP_DIR/usr/bin" -mkdir -pv "$APP_DIR/usr/lib" -mkdir -pv "$APP_DIR/usr/share/applications" -mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps" - -# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied -echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..." - -cp -v ./void "$APP_DIR/usr/bin/" - -# Copy the icon to required locations -cp -v ./void.png "$APP_DIR/void.png" -cp -v ./void.png "$APP_DIR/usr/share/icons/hicolor/256x256/apps/void.png" - -# Copy dependencies with error checking -echo "Copying dependencies..." -for lib in $(ldd ./void | grep "=> /" | awk '{print $3}'); do - if [ -f "$lib" ]; then - cp -v "$lib" "$APP_DIR/usr/lib/" || echo "Failed to copy $lib" - else - echo "Warning: Library $lib not found" - fi -done - -# Create desktop file with error checking -echo "Creating desktop file..." -if ! cat > "$APP_DIR/$APP_NAME.desktop" < "$APP_DIR/AppRun" < { this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID) // if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) { @@ -198,7 +208,19 @@ export class EditorGroupWatermark extends Disposable { // this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder'); // } } - voidIconBox.appendChild(openFolderButton.root); + buttonContainer.appendChild(openFolderButton.root); + + // Open SSH button + const openSSHButton = h('button') + openSSHButton.root.classList.add('void-openssh-button') + openSSHButton.root.style.display = 'block' + openSSHButton.root.style.backgroundColor = '#5a5a5a' // Made darker than the default gray + openSSHButton.root.style.width = '124px' // Set width to 124px as requested + openSSHButton.root.textContent = 'Open SSH' + openSSHButton.root.onclick = () => { + this.viewsService.openViewContainer(REMOTE_EXPLORER_VIEWLET_ID); + } + buttonContainer.appendChild(openSSHButton.root); // Recents @@ -244,6 +266,9 @@ export class EditorGroupWatermark extends Disposable { const dirSpan = $('span'); dirSpan.style.paddingLeft = '4px'; + dirSpan.style.whiteSpace = 'nowrap'; + dirSpan.style.overflow = 'hidden'; + dirSpan.style.maxWidth = '300px'; dirSpan.innerText = parentPath; dirSpan.title = fullPath; diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 93cf0be6..f79d0c9c 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -50,6 +50,7 @@ } /* light */ +.void-void-icon, .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress { width: 100%; max-height: 100%; @@ -60,14 +61,17 @@ background-repeat: no-repeat; } +.void-void-icon, .monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { background-image: url('./void_cube_noshadow.png'); /* // Void */ } +.void-void-icon, .monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { background-image: url('./void_cube_noshadow.png'); /* // Void */ } +.void-void-icon, .monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { background-image: url('./void_cube_noshadow.png'); /* // Void */ } diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 5bf1200d..95ec65c2 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -792,7 +792,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const featureName: FeatureName = 'Autocomplete' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined - const aiInstructions = this._settingsService.state.globalSettings.aiInstructions // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { @@ -804,8 +803,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ prefix: llmPrefix, suffix: llmSuffix, stopTokens: stopTokens, - }, - aiInstructions + } }), modelSelection, modelSelectionOptions, diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index cb2816ee..7a206021 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -25,8 +25,6 @@ import { Position } from '../../../../editor/common/core/position.js'; import { IMetricsService } from '../common/metricsService.js'; import { shorten } from '../../../../base/common/labels.js'; import { IVoidModelService } from '../common/voidModelService.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js'; import { IEditCodeService } from './editCodeServiceInterface.js'; import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; @@ -246,8 +244,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IMetricsService private readonly _metricsService: IMetricsService, - @IEditorService private readonly _editorService: IEditorService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IEditCodeService private readonly _editCodeService: IEditCodeService, @INotificationService private readonly _notificationService: INotificationService, @IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService, @@ -266,9 +262,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // always be in a thread this.openNewThread() - // when the user changes files, automatically add the new file as a stagingSelection - this._register(this._editorService.onDidActiveEditorChange(() => this._addCurrentFileAsStagingSelectionDuringFileChange())); - // keep track of user-modified files // const disposablesOfModelId: { [modelId: string]: IDisposable[] } = {} @@ -288,40 +281,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - // add the current file to the thread being edited - private _addCurrentFileAsStagingSelectionDuringFileChange() { - const newModel = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null - if (!newModel) { return } - - const isCurrentlyFocusing = this.isCurrentlyFocusingMessage() - if (isCurrentlyFocusing) return - - // only add if the user hasn't sent a message yet - if (this.getCurrentThread().messages.length !== 0) return - - const newStagingSelection: StagingSelectionItem = { - type: 'File', - uri: newModel.uri, - language: newModel.getLanguageId(), - state: { wasAddedAsCurrentFile: true } - } - - const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; - - // remove all old selectons that are marked as `wasAddedAsCurrentFile` - const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile) - - const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath) - - if (!fileIsAlreadyHere) { - newStagingSelections.push(newStagingSelection) - } - - this.setCurrentThreadState({ stagingSelections: newStagingSelections }); - - } - - // !!! this is important for properly restoring URIs from storage // should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { @@ -390,36 +349,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { - editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => { - - const thread = this.state.allThreads[threadId] - if (!thread) return // should never happen - - if (thread.messages?.[messageIdx]?.role !== 'user') { - throw new Error(`Error: editing a message with role !=='user'`) - } - - // get prev and curr selections before clearing the message - const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message - - // clear messages up to the index - const slicedMessages = thread.messages.slice(0, messageIdx) - this._setState({ - allThreads: { - ...this.state.allThreads, - [thread.id]: { - ...thread, - messages: slicedMessages - } - } - }, true) - - // re-add the message and stream it - this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId }) - - } - - private _currentModelSelectionProps = () => { // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) const featureName: FeatureName = 'Chat' @@ -924,7 +853,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const [_, toIdx] = c if (toIdx === fromIdx) return - // console.log(`going from ${fromIdx} to ${toIdx}`) + console.log(`going from ${fromIdx} to ${toIdx}`) // update the user's checkpoint this._addUserModificationsToCurrCheckpoint({ threadId }) @@ -936,15 +865,14 @@ A,B,C are all files. x means a checkpoint where the file changed. A B C D E F G H I -x x x x x x x x x -| | | | | | | | | -x | | | | | | | x ----x-|-|-|-x-|-x-|----- <-- to - x | | | | | x - | | x x | - | | | | --------x-|---x-x------- <-- from - x + x x x x x x <-- you can't always go up to find the "before" version; sometimes you need to go down + | | | | | | x +--x-|-|-|-x---x-|----- <-- to + | | | | x x + | | x x | + | | | | +----x-|---x-x------- <-- from + x We need to revert anything that happened between to+1 and from. **We do this by finding the last x from 0...`to` for each file and applying those contents.** @@ -952,9 +880,19 @@ We only need to do it for files that were edited since `to`, ie files between to */ if (toIdx < fromIdx) { const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx }) + + const idxes = function* () { + for (let k = toIdx; k >= 0; k -= 1) { // first go up + yield k + } + for (let k = toIdx + 1; k < thread.messages.length; k += 1) { // then go down + yield k + } + } + for (const fsPath in lastIdxOfURI) { - // apply lowest down content for each uri (or original if not found) - for (let k = toIdx; k >= 0; k -= 1) { + // find the first instance of this file starting at toIdx (go up to latest file; if there is none, go down) + for (const k of idxes()) { const message = thread.messages[k] if (message.role !== 'checkpoint') continue const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) @@ -970,16 +908,16 @@ We only need to do it for files that were edited since `to`, ie files between to /* if redoing -A B C D E F G H I -x x x x x x x x x -| | | | | | | | | -x | | | | | | | x ----x-|-|-|-x-|-x-|----- <-- from - x | | | | | x - | | x x | - | | | | --------x-|---x-x------- <-- to - x +A B C D E F G H I J + x x x x x x x + | | | | | | x x x +--x-|-|-|-x---x-|-|--- <-- from + | | | | x x + | | x x | + | | | | +----x-|---x-x-----|--- <-- to + x x + We need to apply latest change for anything that happened between from+1 and to. We only need to do it for files that were edited since `from`, ie files between from+1...to. @@ -995,7 +933,6 @@ We only need to do it for files that were edited since `from`, ie files between if (!res) continue const { voidFileSnapshot } = res if (!voidFileSnapshot) continue - this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot) break } @@ -1044,11 +981,15 @@ We only need to do it for files that were edited since `from`, ie files between }) } - async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) { + dismissStreamError(threadId: string): void { + this._setStreamState(threadId, { error: undefined }, 'merge') + } + + + private async _addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') { // if about to call the other LLM, just wait for it by stopping right now @@ -1057,14 +998,11 @@ We only need to do it for files that were edited since `from`, ie files between // stop it (this simply resolves the promise to free up space) if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - - // add dummy before this message to keep checkpoint before user message idea consistent if (thread.messages.length === 0) { this._addUserCheckpoint({ threadId }) } - const { chatMode } = this._settingsService.state.globalSettings // add user's message to chat history @@ -1084,11 +1022,61 @@ We only need to do it for files that were edited since `from`, ie files between ) } - dismissStreamError(threadId: string): void { - this._setStreamState(threadId, { error: undefined }, 'merge') + + async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) { + const thread = this.state.allThreads[threadId]; + if (!thread) return + + // if there's a current checkpoint, delete all messages after it + if (thread.state.currCheckpointIdx !== null) { + const checkpointIdx = thread.state.currCheckpointIdx; + const newMessages = thread.messages.slice(0, checkpointIdx + 1); + + // Update the thread with truncated messages + const newThreads = { + ...this.state.allThreads, + [threadId]: { + ...thread, + lastModified: new Date().toISOString(), + messages: newMessages, + } + }; + this._storeAllThreads(newThreads); + this._setState({ allThreads: newThreads }, true); + } + + // Now call the original method to add the user message and stream the response + await this._addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }); + } + editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => { + const thread = this.state.allThreads[threadId] + if (!thread) return // should never happen + + if (thread.messages?.[messageIdx]?.role !== 'user') { + throw new Error(`Error: editing a message with role !=='user'`) + } + + // get prev and curr selections before clearing the message + const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message + + // clear messages up to the index + const slicedMessages = thread.messages.slice(0, messageIdx) + this._setState({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + messages: slicedMessages + } + } + }, true) + + // re-add the message and stream it + this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId }) + } // ---------- the rest ---------- @@ -1386,21 +1374,6 @@ We only need to do it for files that were edited since `from`, ie files between // switch to the thread this.switchToThread(threadId) - // add the current file as a staging selection - const model = this._codeEditorService.getActiveCodeEditor()?.getModel() - if (model) { - this._setThreadState(this.state.currentThreadId, { - stagingSelections: [{ - type: 'File', - uri: model.uri, - language: model.getLanguageId(), - state: { - wasAddedAsCurrentFile: true - } - }] - }) - } - return; } } // otherwise, start a new thread diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 3d64033e..19606415 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -7,12 +7,14 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ChatMessage } from '../common/chatThreadServiceTypes.js'; import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js'; -import { toolCallXMLStr, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; +import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js'; import { IDirectoryStrService } from './directoryStrService.js'; import { ITerminalToolService } from './terminalToolService.js'; +import { IVoidModelService } from '../common/voidModelService.js'; +import { URI } from '../../../../base/common/uri.js'; @@ -143,13 +145,19 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA // add anthropic reasoning if (currMsg.role === 'assistant') { if (currMsg.anthropicReasoning && supportsAnthropicReasoning) { - const content = currMsg.content newMessages[i] = { role: 'assistant', content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning } } + else { + newMessages[i] = { + role: 'assistant', + content: currMsg.content, + // strip away anthropicReasoning + } + } continue } @@ -199,7 +207,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop // alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere let content: LLMChatMessage['content'] = c.content if (next?.role === 'tool') { - content = `${content}\n\n${toolCallXMLStr(next.name, next.rawParams)}` + content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}` } // anthropic reasoning @@ -406,9 +414,9 @@ const prepareMessages = ({ export interface IConvertToLLMMessageService { readonly _serviceBrand: undefined; - prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }; + prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined } prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> - prepareFIMMessage(opts: { messages: LLMFIMMessage, aiInstructions: string, }): { prefix: string, suffix: string, stopTokens: string[] } + prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] } } @@ -425,10 +433,35 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess @IDirectoryStrService private readonly directoryStrService: IDirectoryStrService, @ITerminalToolService private readonly terminalToolService: ITerminalToolService, @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, + @IVoidModelService private readonly voidModelService: IVoidModelService, ) { super() } + // Read .voidinstructions files from workspace folders + private _getVoidInstructionsFileContents(): string { + const workspaceFolders = this.workspaceContextService.getWorkspace().folders; + let voidInstructions = ''; + for (const folder of workspaceFolders) { + const uri = URI.joinPath(folder.uri, '.voidinstructions') + const { model } = this.voidModelService.getModel(uri) + if (!model) continue + voidInstructions += model.getValue() + '\n\n'; + } + return voidInstructions.trim(); + } + + // Get combined AI instructions from settings and .voidinstructions files + private _getCombinedAIInstructions(): string { + const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions; + const voidInstructionsFileContent = this._getVoidInstructionsFileContents(); + + const ans: string[] = [] + if (globalAIInstructions) ans.push(globalAIInstructions) + if (voidInstructionsFileContent) ans.push(voidInstructionsFileContent) + return ans.join('\n\n') + } + // system message private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => { @@ -442,7 +475,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess `...Directories string cut off, use tools to read more...` : `...Directories string cut off, ask user for more if necessary...` }) - const includeXMLToolDefinitions = specialToolFormat === undefined + const includeXMLToolDefinitions = !specialToolFormat const runningTerminalIds = this.terminalToolService.listTerminalIds() const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions }) @@ -496,7 +529,9 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess } = getModelCapabilities(providerName, modelName) const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] - const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions + + // Get combined AI instructions + const aiInstructions = this._getCombinedAIInstructions(); const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions) const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled }) @@ -512,7 +547,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess maxOutputTokens, }) return { messages, separateSystemMessage }; - } prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => { if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } @@ -525,7 +559,9 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName] - const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions + + // Get combined AI instructions + const aiInstructions = this._getCombinedAIInstructions(); const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions) const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled }) @@ -542,19 +578,20 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess maxOutputTokens, }) return { messages, separateSystemMessage }; - } // --- FIM --- - prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages, aiInstructions }) => { + prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages }) => { + // Get combined AI instructions with the provided aiInstructions as the base + const combinedInstructions = this._getCombinedAIInstructions(); let prefix = `\ -${!aiInstructions ? '' : `\ +${!combinedInstructions ? '' : `\ // Instructions: // Do not output an explanation. Try to avoid outputting comments. Only output the middle code. -${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} +${combinedInstructions.split('\n').map(line => `//${line}`).join('\n')}`} ${messages.prefix}` @@ -567,7 +604,6 @@ ${messages.prefix}` } -// pick one and delete the other: registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageWorkbenchContrib.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageWorkbenchContrib.ts new file mode 100644 index 00000000..9e65f9da --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageWorkbenchContrib.ts @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IVoidModelService } from '../common/voidModelService.js'; + +class ConvertContribWorkbenchContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.void.convertcontrib' + _serviceBrand: undefined; + + constructor( + @IVoidModelService private readonly voidModelService: IVoidModelService, + @IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService, + ) { + super() + + const initializeURI = (uri: URI) => { + this.workspaceContext.getWorkspace() + const voidInstrsURI = URI.joinPath(uri, '.voidinstructions') + this.voidModelService.initializeModel(voidInstrsURI) + } + + // call + this._register(this.workspaceContext.onDidChangeWorkspaceFolders((e) => { + [...e.changed, ...e.added].forEach(w => { initializeURI(w.uri) }) + })) + this.workspaceContext.getWorkspace().folders.forEach(w => { initializeURI(w.uri) }) + } +} + + +registerWorkbenchContribution2(ConvertContribWorkbenchContribution.ID, ConvertContribWorkbenchContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 653cf3af..72d90ce7 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -258,6 +258,22 @@ class EditCodeService extends Disposable implements IEditCodeService { this._realignAllDiffAreasLines(uri, change.text, change.range) } this._refreshStylesAndDiffsInURI(uri) + + // if diffarea has no diffs after a user edit, delete it + const diffAreasToDelete: DiffZone[] = [] + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] ?? []) { + const diffArea = this.diffAreaOfId[diffareaid] ?? null + const shouldDelete = diffArea?.type === 'DiffZone' && Object.keys(diffArea._diffOfId).length === 0 + if (shouldDelete) { + diffAreasToDelete.push(diffArea) + } + } + if (diffAreasToDelete.length !== 0) { + const { onFinishEdit } = this._addToHistory(uri) + diffAreasToDelete.forEach(da => this._deleteDiffZone(da)) + onFinishEdit() + } + } @@ -1562,20 +1578,22 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const errContentOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { + const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { const descStr = str === `Not found` ? `The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` : str === `Not unique` ? `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` - : `` + : str === 'Has overlap' ? + `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + : `` // string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has // const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n') // const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : '' // const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : '' // const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}` - const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error.' + const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' const errMsg = `${descStr}\n${soFarStr}` return errMsg @@ -1610,7 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const addedTrackingZoneOfBlockNum: TrackingZone[] = [] diffZone._streamState.line = 1 - const N_RETRIES = 2 + const N_RETRIES = 4 // allowed to throw errors - this is called inside a promise that handles everything const runSearchReplace = async () => { @@ -1684,17 +1702,25 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it if (!(blockNum in addedTrackingZoneOfBlockNum)) { - const originalBounds = findTextInCode(block.orig, originalFileCode, true) // if error - if (typeof originalBounds === 'string') { + // Check for overlap with existing modified ranges + const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { + const [existingStart, existingEnd] = trackingZone.metadata.originalBounds; + const hasNoOverlap = endLine < existingStart || startLine > existingEnd + return !hasNoOverlap + }); + + if (typeof originalBounds === 'string' || hasOverlap) { + const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const + console.log('--------------Error finding text in code:') console.log('originalFileCode', { originalFileCode }) console.log('fullText', { fullText }) - console.log('error:', originalBounds) + console.log('error:', errorMessage) console.log('block.orig:', block.orig) console.log('---------') - const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) + const content = errContentOfInvalidStr(errorMessage, block.orig) messages.push( { role: 'assistant', content: fullText }, // latest output { role: 'user', content: content } // user explanation of what's wrong diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index ac5ff5f3..d732c4bf 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ - .monaco-editor .void-sweepIdxBG { +.monaco-editor .void-sweepIdxBG { background-color: var(--vscode-void-sweepIdxBG); } @@ -23,10 +23,30 @@ background-color: var(--vscode-void-redBG); } -.void-watermark-button { - margin: 8px 0; + +/* Renamed from void-watermark-button to void-openfolder-button */ +.void-openfolder-button { padding: 8px 20px; - background-color: #3b82f6; + background-color: #306dce; + color: white; + border: none; + border-radius: 4px; + outline: none !important; + box-shadow: none !important; + cursor: pointer; + transition: background-color 0.2s ease; +} +.void-openfolder-button:hover { + background-color: #2563eb; +} +.void-openfolder-button:active { + background-color: #2563eb; +} + +/* Added for Open SSH button with slightly darker color */ +.void-openssh-button { + padding: 8px 20px; + background-color: #656565; /* Slightly darker than the #5a5a5a in the TS file */ color: white; border: none; border-radius: 4px; @@ -35,43 +55,41 @@ cursor: pointer; transition: background-color 0.2s ease; } -.void-watermark-button:hover { - background-color: #2563eb; +.void-openssh-button:hover { + background-color: #474747; /* Darker on hover */ } -.void-watermark-button:active { - background-color: #2563eb; +.void-openssh-button:active { + background-color: #474747; } - - .void-settings-watermark-button { - margin: 8px 0; - padding: 8px 20px; - background-color: var(--vscode-input-background); + margin: 8px 0; + padding: 8px 20px; + background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); - border: none; - border-radius: 4px; - outline: none !important; + border: none; + border-radius: 4px; + outline: none !important; box-shadow: none !important; - cursor: pointer; - transition: all 0.2s ease; + cursor: pointer; + transition: all 0.2s ease; } + .void-settings-watermark-button:hover { filter: brightness(1.1); } + .void-settings-watermark-button:active { filter: brightness(1.1); } - - - .void-link { color: #3b82f6; cursor: pointer; transition: all 0.2s ease; } + .void-link:hover { opacity: 80%; } @@ -86,7 +104,8 @@ .void-scope, .void-scope * { scrollbar-width: thin !important; - scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */ + scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; + /* For Firefox */ } .void-scope::-webkit-scrollbar, @@ -133,13 +152,16 @@ background-color: var(--vscode-editor-background); --scrollbar-vertical-width: 14px; --scrollbar-horizontal-height: 6px; - overflow: auto; /* Ensure scrollbars are shown when needed */ + overflow: auto; + /* Ensure scrollbars are shown when needed */ } .void-scrollable-element, .void-scrollable-element * { - scrollbar-width: thin !important; /* For Firefox */ - scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */ + scrollbar-width: thin !important; + /* For Firefox */ + scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; + /* For Firefox */ } .void-scrollable-element::-webkit-scrollbar, diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx index 2ccc54b2..3720d1a6 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx @@ -5,6 +5,7 @@ import React, { Component, ErrorInfo, ReactNode } from 'react'; import { ErrorDisplay } from './ErrorDisplay.js'; +import { WarningBox } from '../void-settings-tsx/WarningBox.js'; interface Props { children: ReactNode; @@ -51,11 +52,12 @@ class ErrorBoundary extends Component { // Use ErrorDisplay component as the default error UI return ( - + + // ); } diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index d58b7559..0cefd52d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js'; +import ErrorBoundary from './ErrorBoundary.js'; @@ -1364,12 +1365,12 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => style={{ background: 'none' }} > Lint errors
{lintErrors.map((error, i) => ( @@ -1949,10 +1950,14 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon onClick={() => { if (threadIsRunning) return - chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) + chatThreadService.jumpToCheckpointBeforeMessageIdx({ + threadId, + messageIdx, + jumpToUserModified: messageIdx === (chatThreadService.state.allThreads[threadId]?.messages.length ?? 0) - 1 + }) }} > - Checkpoint + Checkpoint
} @@ -2479,7 +2484,7 @@ export const SidebarChat = () => { const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity) const previousMessagesHTML = useMemo(() => { - const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') + // const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') // tool request shows up as Editing... if in progress return previousMessages.map((message, i) => { return {
{/* History selector */}
- + + +
- {messagesHTML} + + {messagesHTML} +
- {inputForm} + + {inputForm} +
) diff --git a/src/vs/workbench/contrib/void/browser/react/src/styles.css b/src/vs/workbench/contrib/void/browser/react/src/styles.css index e7a3da15..13257709 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/void/browser/react/src/styles.css @@ -11,7 +11,7 @@ --void-bg-1: var(--vscode-input-background); --void-bg-1-alt: var(--vscode-badge-background); --void-bg-2: var(--vscode-sideBar-background); - --void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%); + --void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%); --void-bg-3: var(--vscode-editor-background); --void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%); diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index b7e31757..c0f00c53 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -664,57 +664,60 @@ export const VoidCustomDropdownBox = >({ {isOpen && (
- {options.map((option) => { - const thisOptionIsSelected = getOptionsEqual(option, selectedOption); - const optionName = getOptionDropdownName(option); - const optionDetail = getOptionDropdownDetail?.(option) || ''; + onWheel={(e) => e.stopPropagation()} + >
- return ( -
{ + const thisOptionIsSelected = getOptionsEqual(option, selectedOption); + const optionName = getOptionDropdownName(option); + const optionDetail = getOptionDropdownDetail?.(option) || ''; + + return ( +
{ - onChangeOption(option); - setIsOpen(false); - }} - > -
- {thisOptionIsSelected && ( - - - - )} + onClick={() => { + onChangeOption(option); + setIsOpen(false); + }} + > +
+ {thisOptionIsSelected && ( + + + + )} +
+ + {optionName} + {optionDetail} +
- - {optionName} - {optionDetail} - -
- ); - })} + ); + })} +
+
)}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index dcf97f8e..40b34d14 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -111,7 +111,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const { model } = await voidModelService.getModelSafe(nextURI) if (model) { // switch to the URI - editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor) + editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor) } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index aa440078..40ee767c 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -3,15 +3,16 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useAccessor, useIsDark, useSettingsState } from '../util/services.js'; -import { Check, ExternalLink, X } from 'lucide-react'; +import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react'; import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js'; import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js'; +import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'; -const OVERRIDE_VALUE = true +const OVERRIDE_VALUE = false export const VoidOnboarding = () => { @@ -24,7 +25,6 @@ export const VoidOnboarding = () => {