Merge pull request #381 from voideditor/tools-in-plaintext

Increased Support for Agent Mode (Ollama, vLLM, etc)
This commit is contained in:
Andrew Pareles 2025-04-09 08:29:29 -07:00 committed by GitHub
commit b6408512d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 4068 additions and 1698 deletions

37
package-lock.json generated
View file

@ -62,6 +62,7 @@
"posthog-node": "^4.8.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-tooltip": "^5.28.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-html-languageservice": "^5.3.1",
@ -6621,6 +6622,12 @@
"node": ">=0.10.0"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/cli-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
@ -15094,10 +15101,11 @@
}
},
"node_modules/nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/nanoid": {
@ -17999,6 +18007,20 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-tooltip": {
"version": "5.28.1",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.1.tgz",
"integrity": "sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.1",
"classnames": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -18807,10 +18829,11 @@
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"dev": true,
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.25.0",

View file

@ -123,6 +123,7 @@
"posthog-node": "^4.8.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-tooltip": "^5.28.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-html-languageservice": "^5.3.1",

View file

@ -3,6 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This bootstrap-fork module handles the initialization of a forked process in VS Code.
// It sets up logging, exception handling, and loads the ESM module system.
import * as performance from './vs/base/common/performance.js';
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js';
import { bootstrapESM } from './bootstrap-esm.js';

View file

@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution {
}
this._state = this._state.map(state => state.mov(forward));
const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
// Void changed this to skip over added whitespace when using smartSelect
// // Store the original selections for comparison
// const originalSelections = selections;
// // Keep skipping while we're only adding/removing whitespace
// let keepSkipping = true;
// let skipCount = 0;
// const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit
// while (keepSkipping && skipCount < MAX_SKIPS) {
// keepSkipping = false; // Reset for each iteration
// // Check if all selections only added/removed whitespace
// if (originalSelections.length === newSelections.length) {
// for (let i = 0; i < originalSelections.length; i++) {
// const oldSel = originalSelections[i];
// const newSel = newSelections[i];
// if (forward) { // For expanding (^+Shift+Right)
// // Skip if only whitespace was added
// const oldText = model.getValueInRange(oldSel).trim();
// const newText = model.getValueInRange(newSel).trim();
// const onlyWhitespaceAdded = oldText === newText && oldText.length > 0;
// if (onlyWhitespaceAdded) {
// console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, {
// reason: 'only whitespace added',
// oldText: model.getValueInRange(oldSel),
// newText: model.getValueInRange(newSel)
// });
// keepSkipping = true;
// break;
// }
// } else { // For shrinking (^+Shift+Left)
// // Skip if only whitespace was removed
// const oldText = model.getValueInRange(oldSel).trim();
// const newText = model.getValueInRange(newSel).trim();
// const onlyWhitespaceRemoved = oldText === newText && newText.length > 0;
// if (onlyWhitespaceRemoved) {
// console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, {
// reason: 'only whitespace removed',
// oldText: model.getValueInRange(oldSel),
// newText: model.getValueInRange(newSel)
// });
// keepSkipping = true;
// break;
// }
// }
// }
// }
// // If we need to skip, move one more time
// if (keepSkipping) {
// skipCount++;
// // Try to move to the next range
// const prevState = this._state;
// this._state = this._state.map(state => state.mov(forward));
// // Check if we've reached the end of available ranges
// const stateUnchanged = this._state.every((state, idx) =>
// state.index === prevState[idx].index
// );
// if (stateUnchanged) {
// // We can't move any further, so stop skipping
// keepSkipping = false;
// } else {
// // Update selections for the next iteration
// newSelections = this._state.map(state => Selection.fromPositions(
// state.ranges[state.index].getStartPosition(),
// state.ranges[state.index].getEndPosition()
// ));
// }
// }
// }
// // Print AFTER selection (before actually setting it)
// console.log('SMART SELECT - AFTER:', newSelections.map(s => {
// return {
// range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`,
// text: model.getValueInRange(s)
// };
// }));
this._ignoreSelection = true;
try {
this._editor.setSelections(newSelections);

View file

@ -612,7 +612,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
// On Mac, the delay is 1500.
'default': isMacintosh ? 1500 : 500,
'default': 300, // Void changed this from isMacintosh ? 1500 : 500,
'minimum': 0
},
'workbench.reduceMotion': {

View file

@ -648,8 +648,9 @@ const defaultChat = {
providerSetting: product.defaultChatAgent?.providerSetting ?? '',
};
// Void commented this out - copilot head
// Add next to the command center if command center is disabled
MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
/* MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
submenu: MenuId.ChatTitleBarMenu,
title: localize('title4', "Copilot"),
icon: Codicon.copilot,
@ -672,7 +673,7 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, {
ContextKeyExpr.has('config.window.commandCenter').negate(),
),
order: 1
});
}); */
registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {
constructor() {

View file

@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { InlineCompletion, } from '../../../../editor/common/languages.js';
import { Range } from '../../../../editor/common/core/range.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
@ -633,8 +632,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
async _provideInlineCompletionItems(
model: ITextModel,
position: Position,
context: InlineCompletionContext,
token: CancellationToken,
): Promise<InlineCompletion[]> {
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
@ -852,7 +849,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
onAbort: () => { },
onAbort: () => { reject('Aborted autocomplete') },
})
newAutocompletion.requestId = requestId
@ -897,9 +894,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
) {
super()
this._langFeatureService.inlineCompletionsProvider.register('*', {
this._register(this._langFeatureService.inlineCompletionsProvider.register('*', {
provideInlineCompletions: async (model, position, context, token) => {
const items = await this._provideInlineCompletionItems(model, position, context, token)
const items = await this._provideInlineCompletionItems(model, position)
// console.log('item: ', items?.[0]?.insertText)
return { items: items, }
@ -936,7 +933,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
});
},
})
}))
}

View file

@ -11,13 +11,13 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, voidTools } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, InternalToolInfo } from '../common/toolsServiceTypes.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
@ -37,6 +37,7 @@ import { IModelService } from '../../../../editor/common/services/model.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { deepClone } from '../../../../base/common/objects.js';
/*
@ -61,28 +62,6 @@ A checkpoint appears before every LLM message, and before every user message (be
*/
const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
for (const c of chatMessages) {
if (c.role === 'user') {
llmChatMessages.push({ role: c.role, content: c.content })
}
else if (c.role === 'assistant')
llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning })
else if (c.role === 'tool')
llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content })
else if (c.role === 'decorative_canceled_tool') { // pass
}
else if (c.role === 'checkpoint') { // pass
}
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
return llmChatMessages
}
type UserMessageType = ChatMessage & { role: 'user' }
type UserMessageState = UserMessageType['state']
const defaultMessageState: UserMessageState = {
@ -139,10 +118,9 @@ export type ThreadStreamState = {
// streaming related - when streaming message
streamingToken?: string;
messageSoFar?: string;
displayContentSoFar?: string;
reasoningSoFar?: string;
toolNameSoFar?: string;
toolParamsSoFar?: string;
toolCallSoFar?: RawToolCallObj;
}
}
@ -380,9 +358,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
else if (behavior === 'set') {
this.streamState[threadId] = state
}
else throw new Error(`setStreamState`)
}
this._onDidChangeStreamState.fire({ threadId })
}
@ -442,7 +420,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
return false
}
private _updateLatestToolTo = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
private _updateLatestTool = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
const swapped = this._swapOutLatestStreamingToolWithResult(threadId, tool)
if (swapped) return
this._addMessageToThread(threadId, tool)
@ -452,33 +430,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const lastMsg = thread.messages[thread.messages.length - 1]
if (!(
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
)) return // should never happen
const lastUserMsgIdx = findLastIdx(thread.messages, m => m.role === 'user')
const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' }
if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen
const instructions = lastUserMessage.displayContent || ''
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
this._updateLatestToolTo(threadId, {
role: 'tool',
type: 'running_now',
name: lastMsg.name,
paramsStr: lastMsg.paramsStr,
id: lastMsg.id,
params: lastMsg.params,
content: '(value not received yet...)', // this typically shouldn't ever get read
result: null
})
this._wrapRunAgentToNotify(
this._runChatAgent({ callThisToolFirst, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
this._runChatAgent({ callThisToolFirst, threadId, ...this._currentModelSelectionProps() })
, threadId
)
}
@ -494,29 +454,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
else return
const { name, paramsStr, id } = lastMsg
const { name } = lastMsg
const errorMessage = this.errMsgs.rejected
this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null })
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
this._setStreamState(threadId, {}, 'set')
}
// private _rejectLatestStreamingTool(threadId: string) {
// const thread = this.state.allThreads[threadId]
// if (!thread) return // should never happen
// const lastMessage = thread.messages[thread.messages.length - 1]
// if (lastMessage.role !== 'tool') return
// const { name, paramsStr, id, result } = lastMessage
// if (result.type !== 'running_now') return
// const { params } = result
// const errorMessage = this.errMsgs.rejected
// this._swapOutLatestStreamingToolWithResult(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
// this._setStreamState(threadId, {}, 'set')
// }
stopRunning(threadId: string) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
@ -531,19 +475,20 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const isRunning = this.streamState[threadId]?.isRunning
if (isRunning === 'LLM') {
// abort the stream first so it doesn't change any state
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const displayContentSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolInProgress = this.streamState[threadId]?.toolNameSoFar
console.log('toolInProgress', toolInProgress)
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
if (toolInProgress) {
this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress })
if (toolCallSoFar) {
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
}
this._addUserCheckpoint({ threadId })
}
this._setStreamState(threadId, {}, 'set')
@ -551,18 +496,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private _tools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
: undefined
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
return tools
}
private readonly errMsgs = {
rejected: 'Tool call was rejected by the user.',
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
@ -571,140 +504,162 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {}
// system message
private _generateSystemMessage = async (chatMode: ChatMode) => {
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
const directoryStr = await this._directoryStrService.getAllDirectoriesStr({
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const runningTerminalIds = this._terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
return systemMessage
}
private _generateLLMMessages = async (threadId: string) => {
const thread = this.state.allThreads[threadId]
if (!thread) return []
const chatMessages = deepClone(thread.messages)
const llmChatMessages: LLMChatMessage[] = []
// merge tools into user message
for (const c of chatMessages) {
if (c.role === 'assistant') {
// if called a tool, re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content = c.displayContent
if (c.toolCall) {
content = `${content}\n\n${toolCallXMLStr(c.toolCall)}`
}
llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning })
}
else if (c.role === 'user' || c.role === 'tool') {
if (c.role === 'tool')
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
llmChatMessages.push({ role: 'user', content: c.content })
else
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
}
else if (c.role === 'interrupted_streaming_tool') { // pass
}
else if (c.role === 'checkpoint') { // pass
}
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
return llmChatMessages
}
// returns true when the tool call is waiting for user approval
private _runToolCall = async (
threadId: string,
toolName: ToolName,
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below
let toolParams: ToolCallParams[ToolName]
let toolResult: Awaited<ToolResultType[typeof toolName]>
let toolResultStr: string
if (!opts.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
toolParams = params
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
return {}
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const toolRequiresApproval = toolNamesThatRequireApproval.has(toolName)
if (toolRequiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
}
}
else {
toolParams = opts.validatedParams
}
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null })
let interrupted = false
try {
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
this._currentlyRunningToolInterruptor[threadId] = () => {
interrupted = true;
interruptTool?.();
delete this._currentlyRunningToolInterruptor[threadId];
}
toolResult = await result // ts is bad... await is needed
}
catch (error) {
if (interrupted) {
// the tool result is added when we stop running
return { interrupted: true }
}
const errorMessage = getErrorMessage(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 5. add to history and keep going
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
return {}
};
private async _runChatAgent({
threadId,
modelSelection,
modelSelectionOptions,
userMessageContent,
callThisToolFirst,
}: {
threadId: string,
modelSelection: ModelSelection | null,
modelSelectionOptions: ModelSelectionOptions | undefined,
userMessageContent: string, // content of LATEST user message
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
}) {
const userMessageFullContent = userMessageContent
const getLatestMessages = async () => {
// replace last userMessage with userMessageFullContent (which contains all the files too)
const thread = this.state.allThreads[threadId]
const latestMessages = thread?.messages ?? []
const messages_ = toLLMChatMessages(latestMessages)
const lastUserMsgIdx = findLastIdx(messages_, m => m.role === 'user')
if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!)
// system message
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
const { wasCutOff, str: directoryStr_ } = await this._directoryStrService.getAllDirectoriesStr()
const directoryStr = wasCutOff ? (
chatMode === 'agent' || chatMode === 'gather' ? `${directoryStr_}\nString cut off, use tools to read more.`
: `${directoryStr_}\nString cut off, ask user for more if necessary.`
) : directoryStr_
const runningTerminalIds = this._terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
// all messages so far in the chat history (including tools)
const messages: LLMChatMessage[] = [
{ role: 'system', content: systemMessage, },
...messages_.slice(0, lastUserMsgIdx),
{ role: 'user', content: userMessageFullContent },
...messages_.slice(lastUserMsgIdx + 1, Infinity),
]
// console.log('MESSAGES!!!', messages)
return messages
}
// returns true when the tool call is waiting for user approval
const handleToolCall = async (
tool: ToolCallType,
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
const toolName: ToolName = tool.name
const toolParamsStr = tool.paramsStr
const toolId = tool.id
// compute these below
let toolParams: ToolCallParams[ToolName]
let toolResult: ToolResultType[typeof toolName]
let toolResultStr: string
if (!opts?.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
toolParams = params
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
return {}
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const requiresApproval = toolNamesThatRequireApproval.has(toolName)
if (requiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
}
}
else {
toolParams = opts.toolParams
}
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
let interrupted = false
try {
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
this._currentlyRunningToolInterruptor[threadId] = () => {
interrupted = true;
interruptTool?.();
delete this._currentlyRunningToolInterruptor[threadId];
}
toolResult = await result // ts is bad... await is needed
}
catch (error) {
if (interrupted) {
// the tool result is added when we stop running
return { interrupted: true }
}
const errorMessage = getErrorMessage(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
return {}
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
return {}
}
// 5. add to history and keep going
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, })
return {}
};
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
const tools = this._tools(chatMode)
// clear any previous error
this._setStreamState(threadId, { error: undefined }, 'set')
@ -716,7 +671,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
if (interrupted) return
}
@ -727,34 +682,39 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ToolCallType[] | undefined>((res, rej) => { resMessageIsDonePromise = res })
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const messages = await getLatestMessages()
const systemMessage = await this._generateSystemMessage(chatMode)
const llmMessages = await this._generateLLMMessages(threadId)
const messages: LLMChatMessage[] = [
{ role: 'system', content: systemMessage },
...llmMessages
]
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages,
tools: tools,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => {
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge')
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge')
// resolve with tool calls
resMessageIsDonePromise(toolCalls)
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
@ -774,14 +734,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
break
}
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
const toolCalls = await messageIsDonePromise // wait for message to complete
const toolCall = await messageIsDonePromise // wait for message to complete
if (aborted) { return }
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
// call tool if there is one
const tool: ToolCallType | undefined = toolCalls?.[0]
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await handleToolCall(tool)
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
@ -1118,14 +1078,21 @@ We only need to do it for files that were edited since `from`, ie files between
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
return
}
// 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 })
}
// if the current thread is already streaming, stop it (this simply resolves the promise to free up space)
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
const { chatMode } = this._settingsService.state.globalSettings
@ -1141,7 +1108,7 @@ We only need to do it for files that were edited since `from`, ie files between
this._setThreadState(threadId, { currCheckpointIdx: null }) // no longer at a checkpoint because started streaming
this._wrapRunAgentToNotify(
this._runChatAgent({ threadId, userMessageContent, ...this._currentModelSelectionProps(), }),
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
threadId,
)
}
@ -1245,7 +1212,7 @@ We only need to do it for files that were edited since `from`, ie files between
// else search codebase for `target`
let uris: URI[] = []
try {
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, include: null, pageNumber: 0 })
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 })
uris = result.uris
} catch (e) {
return null
@ -1517,6 +1484,10 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}, true)
// when change focused message idx, jump
if (messageIdx !== undefined)
this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
}
// set message.state

View file

@ -15,18 +15,17 @@ import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { VoidDirectoryItem } from '../common/directoryStrTypes.js';
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
const MAX_CHARS_TOTAL_BEGINNING = 20_000
const MAX_CHARS_TOTAL_TOOL = 20_000
// const MAX_FILES_TOTAL = 200
export interface IDirectoryStrService {
readonly _serviceBrand: undefined;
getDirectoryStrTool(uri: URI): Promise<{ wasCutOff: boolean, str: string }>
getAllDirectoriesStr(): Promise<{ wasCutOff: boolean, str: string }>
getDirectoryStrTool(uri: URI): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
@ -275,18 +274,21 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_TOOL);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_TOOL);
return {
str: `Directory of ${uri.fsPath}:\n${content}`,
wasCutOff,
}
let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL)
c = `Directory of ${uri.fsPath}:\n${content}`
if (wasCutOff) c = `${c}\n...Result was truncated...`
return c
}
async getAllDirectoriesStr() {
async getAllDirectoriesStr({ cutOffMessage }: { cutOffMessage: string }) {
let str: string = '';
let cutOff = false;
const folders = this.workspaceContextService.getWorkspace().folders;
if (folders.length === 0)
return '(NO WORKSPACE OPEN)';
for (let i = 0; i < folders.length; i += 1) {
if (i > 0) str += '\n';
@ -301,8 +303,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
// Use our new approach with direct explorer service
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
console.log('dirtree', dirTree)
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_BEGINNING - str.length);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length);
str += content;
if (wasCutOff) {
cutOff = true;
@ -310,7 +311,10 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
}
}
return { wasCutOff: cutOff, str };
if (cutOff) {
return `${str}\n${cutOffMessage}`
}
return str
}
}

View file

@ -44,7 +44,6 @@ import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApply
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
@ -72,6 +71,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const numLinesOfStr = (str: string) => str.split('\n').length
export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => {
let lengthOfTextPx = 0;
for (const char of content) {
if (char === '\t') {
lengthOfTextPx += tabWidth
} else {
lengthOfTextPx += spaceWidth;
}
}
return lengthOfTextPx
}
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
const model = editor.getModel();
@ -95,16 +109,14 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth;
const tabWidth = numSpacesInTab * spaceWidth;
let paddingLeft = 0;
for (const char of leadingWhitespace) {
if (char === '\t') {
paddingLeft += tabWidth
} else if (char === ' ') {
paddingLeft += spaceWidth;
}
}
const leftWhitespacePx = getLengthOfTextPx({
tabWidth,
spaceWidth,
content: leadingWhitespace
});
return paddingLeft;
return leftWhitespacePx;
};
@ -190,7 +202,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@ITextFileService private readonly _textFileService: ITextFileService,
) {
super();
@ -720,16 +731,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
resource: uri,
label: 'Void Agent',
code: 'undoredo.editCode',
undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); },
redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) }
undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) },
redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) }
}
this._undoRedoService.pushElement(elt)
const onFinishEdit = async () => {
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
})
await this._voidModelService.saveModel(uri)
}
return { onFinishEdit }
}
@ -1105,6 +1114,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
await this._voidModelService.saveModel(uri) // save the URI
}
@ -1400,6 +1410,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
chatMode: null, // not chat
onText: (params) => {
const { fullText: fullText_ } = params
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
@ -1586,7 +1597,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const N_RETRIES = 5
const N_RETRIES = 2
// allowed to throw errors - this is called inside a promise that handles everything
const runSearchReplace = async () => {
@ -1617,6 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
chatMode: null, // not chat
onText: (params) => {
const { fullText } = params
// blocks are [done done done ... {writingFinal|writingOriginal}]
@ -1876,6 +1888,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
interruptURIStreaming({ uri }: { uri: URI }) {
if (!this._uriIsStreaming(uri)) return
this._undoHistory(uri)
// brute force for now is OK
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
@ -1883,7 +1897,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!diffArea._streamState.isStreaming) continue
this._stopIfStreaming(diffArea)
}
this._undoHistory(uri)
}

View file

@ -76,93 +76,107 @@
opacity: 80%;
}
/* styles for all containers used by void */
.void-scope {
--scrollbar-vertical-width: 8px;
--scrollbar-horizontal-height: 6px;
}
/* Target both void-scope and all its descendants with scrollbars */
.void-scope,
.void-scope * {
scrollbar-width: thin !important;
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
}
.void-scope::-webkit-scrollbar,
.void-scope *::-webkit-scrollbar {
width: var(--scrollbar-vertical-width) !important;
height: var(--scrollbar-horizontal-height) !important;
background-color: var(--void-bg-3) !important;
}
.void-scope::-webkit-scrollbar-thumb,
.void-scope *::-webkit-scrollbar-thumb {
background-color: var(--void-bg-1) !important;
border-radius: 4px !important;
border: none !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.void-scope::-webkit-scrollbar-thumb:hover,
.void-scope *::-webkit-scrollbar-thumb:hover {
background-color: var(--void-bg-1) !important;
filter: brightness(1.1) !important;
}
.void-scope::-webkit-scrollbar-thumb:active,
.void-scope *::-webkit-scrollbar-thumb:active {
background-color: var(--void-bg-1) !important;
filter: brightness(1.2) !important;
}
.void-scope::-webkit-scrollbar-track,
.void-scope *::-webkit-scrollbar-track {
background-color: var(--void-bg-3) !important;
border: none !important;
}
.void-scope::-webkit-scrollbar-corner,
.void-scope *::-webkit-scrollbar-corner {
background-color: var(--void-bg-3) !important;
}
/* Add void-scrollable-element styles to match */
.void-scrollable-element {
background-color: var(--vscode-editor-background);
--scrollbar-vertical-width: 14px;
--scrollbar-horizontal-height: 6px;
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 */
}
.void-scrollable-element::-webkit-scrollbar,
.void-scrollable-element *::-webkit-scrollbar {
width: 14px !important;
height: 4px !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
.void-scrollable-element *::-webkit-scrollbar-track {
background: transparent !important;
width: var(--scrollbar-vertical-width) !important;
height: var(--scrollbar-horizontal-height) !important;
background-color: var(--void-bg-3) !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb,
.void-scrollable-element *::-webkit-scrollbar-thumb {
background-color: transparent !important;
border-radius: 0px !important;
background-color: var(--void-bg-1) !important;
border-radius: 4px !important;
border: none !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
background-color: var(--void-bg-1) !important;
filter: brightness(1.1) !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:active,
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
background-color: var(--void-bg-1) !important;
filter: brightness(1.2) !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
.void-scrollable-element *::-webkit-scrollbar-track {
background-color: var(--void-bg-3) !important;
border: none !important;
}
.void-scrollable-element::-webkit-scrollbar-corner,
.void-scrollable-element *::-webkit-scrollbar-corner {
background-color: transparent !important;
}
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background) !important;
background-color: var(--void-bg-3) !important;
}

View file

@ -74,7 +74,7 @@ function saveStylesFile() {
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 4000);
}, 6000);
}
const args = process.argv.slice(2);

View file

@ -11,6 +11,7 @@ import { URI } from '../../../../../../../base/common/uri.js'
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { PlacesType, VariantType } from 'react-tooltip'
enum CopyButtonText {
Idle = 'Copy',
@ -20,30 +21,28 @@ enum CopyButtonText {
type IconButtonProps = {
onClick: () => void;
Icon: LucideIcon
disabled?: boolean
className?: string
}
export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => (
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClick?.();
onClick?.(e);
}}
// border border-void-border-1 rounded
className={`
size-[22px]
p-[4px]
size-[18px]
p-[2px]
flex items-center justify-center
text-sm bg-void-bg-3 text-void-fg-1
text-sm bg-void-bg-3 text-void-fg-3
hover:brightness-110
border border-void-border-1 rounded
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
<Icon />
</button>
@ -94,13 +93,14 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => {
return <IconShell1
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
onClick={onCopy}
{...tooltipPropsForApplyBlock({ tooltipName: 'Copy' })}
/>
}
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
@ -110,6 +110,8 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
{...props}
/>
)
return jumpToFileButton
@ -122,7 +124,6 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
<IconShell1
Icon={Terminal}
onClick={onClick}
className="text-void-fg-1"
/>
)
}
@ -163,10 +164,11 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
rerender(c => c + 1)
console.log('rerendering....')
}
}, [applyBoxId, applyBoxId, uri]))
}, [applyBoxId, uri]))
const currStreamState = getStreamState()
return {
getStreamState,
isDisabled,
@ -175,22 +177,61 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
}
export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null
export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
return (
<div className={`flex flex-row text-void-fg-3 text-xs items-center gap-1.5 ${className}`} {...props}>
{title && <span className='opacity-80'>{title}</span>}
<div
className={` size-1.5 rounded-full border
${indicatorColor === 'dark' ? 'bg-void-bg-3 border-void-border-1' :
indicatorColor === 'orange' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
indicatorColor === 'green' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
indicatorColor === 'yellow' ? 'bg-yellow-500 border-yellow-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
}
`}
/>
</div>
);
};
const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({
'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip',
'data-tooltip-place': position as PlacesType,
'data-tooltip-content': `${tooltipName}`,
'data-tooltip-offset': offset,
})
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
return <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
<div
className={` size-1.5 rounded-full border
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
}`
}
/>
</div>
const color = (
currStreamState === 'idle-no-changes' ? 'dark' :
currStreamState === 'streaming' ? 'orange' :
currStreamState === 'idle-has-changes' ? 'green' :
null
)
const tooltipName = (
currStreamState === 'idle-no-changes' ? 'Done' :
currStreamState === 'streaming' ? 'Applying' :
currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad
''
)
const statusIndicatorHTML = <StatusIndicator
key={currStreamState}
className='mx-2'
indicatorColor={color}
{...tooltipPropsForApplyBlock({ tooltipName, color, position: 'top', offset: 12 })}
/>
return statusIndicatorHTML
}
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
@ -216,7 +257,10 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
applyDonePromise?.catch(e => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
@ -251,11 +295,22 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
if (currStreamState === 'streaming') {
return <IconShell1 Icon={Square} onClick={onInterrupt} />
return <IconShell1
Icon={Square}
onClick={onInterrupt}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1 Icon={reapplyIcon ? RotateCw : Play} onClick={onClickSubmit} />
return <IconShell1
Icon={reapplyIcon ? RotateCw : Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
@ -267,19 +322,18 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
<IconShell1
Icon={X}
onClick={onReject}
className="text-red-600"
{...tooltipPropsForApplyBlock({ tooltipName: 'Reject file' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
className="text-green-600"
{...tooltipPropsForApplyBlock({ tooltipName: 'Accept file' })}
/>
</>
}
}
export const BlockCodeApplyWrapper = ({
children,
initValue,
@ -314,7 +368,7 @@ export const BlockCodeApplyWrapper = ({
{/* header */}
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
<div className="flex items-center">
<StatusIndicatorHTML uri={uri} applyBoxId={applyBoxId} />
<StatusIndicatorForApplyButton uri={uri} applyBoxId={applyBoxId} />
<span className="text-[13px] font-light text-void-fg-3">
{name}
</span>

View file

@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
export const QuickEditChat = ({
@ -89,8 +88,6 @@ export const QuickEditChat = ({
editCodeService.removeCtrlKZone({ diffareaid })
}, [editCodeService, diffareaid])
useScrollbarStyles(sizerRef)
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
const chatAreaRef = useRef<HTMLDivElement | null>(null)

View file

@ -15,17 +15,19 @@ import { ErrorDisplay } from './ErrorDisplay.js';
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react';
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { PlacesType } from 'react-tooltip';
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
@ -350,7 +352,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
<div className='flex flex-col gap-y-1'>
<ReasoningOptionSlider featureName={featureName} />
<div className='flex items-center flex-wrap gap-x-2 gap-y-1'>
<div className='flex items-center flex-wrap gap-x-2 gap-y-1 text-nowrap flex-nowrap'>
{featureName === 'Chat' && <ChatModeDropdown className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-2 rounded py-0.5 px-1' />}
<ModelDropdown featureName={featureName} className='text-xs text-void-fg-3 bg-void-bg-1 rounded' />
</div>
@ -391,6 +393,9 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'}
${className}
`}
// data-tooltip-id='void-tooltip'
// data-tooltip-content={'Send'}
// data-tooltip-place='left'
{...props}
>
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[2px]" />
@ -653,6 +658,7 @@ type ToolHeaderParams = {
numResults?: number;
hasNextPage?: boolean;
children?: React.ReactNode;
bottomChildren?: React.ReactNode;
onClick?: () => void;
isOpen?: boolean,
}
@ -680,23 +686,26 @@ const ToolHeaderWrapper = ({
return (<div className=''>
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ">
{/* header */}
<div
className={`select-none flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer' : ''} ${!isDropdown ? 'mx-1' : ''}`}
onClick={() => {
if (isDropdown) { setIsOpen(v => !v); }
if (onClick) { onClick(); }
}}
>
{isDropdown && (
<ChevronRight
className={`text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
/>
)}
<div className={`select-none flex items-center min-h-[24px] ${!isDropdown ? 'mx-1' : ''}`}>
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
{/* left */}
<div className={`flex items-center gap-x-2 min-w-0 overflow-hidden ${isClickable ? 'hover:brightness-125 transition-all duration-150' : ''}`}>
<div className={`
flex items-center min-w-0 overflow-hidden grow
${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''}
`}
onClick={() => {
if (isDropdown) { setIsOpen(v => !v); }
if (onClick) { onClick(); }
}}
>
{isDropdown && (<ChevronRight
className={`
text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)]
${isExpanded ? 'rotate-90' : ''}
`}
/>)}
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
<span className="text-void-fg-4 text-xs italic truncate">{desc1}</span>
<span className="text-void-fg-4 text-xs italic truncate ml-2">{desc1}</span>
</div>
{/* right */}
@ -772,7 +781,7 @@ const SimplifiedToolHeader = ({
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, currCheckpointIdx, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, currCheckpointIdx: number | undefined, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -923,7 +932,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
</VoidChatArea>
}
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
return <div
// align chatbubble accoridng to role
@ -933,7 +942,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
}
${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}
${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@ -952,25 +961,32 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
</div>
<EditSymbol
size={18}
className={`
absolute -top-1 -right-1
translate-x-0 -translate-y-0
cursor-pointer z-1
p-[2px]
bg-void-bg-1 border border-void-border-1 rounded-md
transition-opacity duration-200 ease-in-out
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>
<div
className="absolute -top-1 -right-1 translate-x-0 -translate-y-0 z-1"
// data-tooltip-id='void-tooltip'
// data-tooltip-content='Edit message'
// data-tooltip-place='left'
>
<EditSymbol
size={18}
className={`
cursor-pointer
p-[2px]
bg-void-bg-1 border border-void-border-1 rounded-md
transition-opacity duration-200 ease-in-out
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>
</div>
</div>
@ -1023,6 +1039,7 @@ const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => {
prose-blockquote:pl-2
prose-blockquote:my-2
prose-code:text-void-fg-3
prose-code:text-[12px]
prose-code:before:content-none
prose-code:after:content-none
@ -1074,7 +1091,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
const reasoningStr = chatMessage.reasoning?.trim() || null
const hasReasoning = !!reasoningStr
const isDoneReasoning = !!chatMessage.content
const isDoneReasoning = !!chatMessage.displayContent
const thread = chatThreadsService.getCurrentThread()
@ -1083,7 +1100,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
messageIdx: messageIdx,
}
const isEmpty = !chatMessage.content && !chatMessage.reasoning
const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning
if (isEmpty) return null
return <>
@ -1107,7 +1124,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ProseWrapper>
<ChatMarkdownRender
string={chatMessage.content || ''}
string={chatMessage.displayContent || ''}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={true}
isLinkDetectionEnabled={true}
@ -1236,7 +1253,7 @@ const ToolRequestAcceptRejectButtons = () => {
<button
onClick={onAccept}
className={`
px-4 py-1.5
px-2 py-1
bg-[var(--vscode-button-background)]
text-[var(--vscode-button-foreground)]
hover:bg-[var(--vscode-button-hoverBackground)]
@ -1252,7 +1269,7 @@ const ToolRequestAcceptRejectButtons = () => {
<button
onClick={onReject}
className={`
px-4 py-1.5
px-2 py-1
bg-[var(--vscode-button-secondaryBackground)]
text-[var(--vscode-button-secondaryForeground)]
hover:bg-[var(--vscode-button-secondaryHoverBackground)]
@ -1267,7 +1284,7 @@ const ToolRequestAcceptRejectButtons = () => {
const autoApproveToggle = (
<div className="flex items-center ml-2 gap-x-1">
<VoidSwitch
size="xs"
size="xxs"
value={voidSettingsState.globalSettings.autoApprove}
onChange={onToggleAutoApprove}
/>
@ -1290,7 +1307,7 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R
</div>
}
export const CodeChildren = ({ children }: { children: React.ReactNode }) => {
return <div className='bg-void-bg-3 p-1 rounded-sm font-mono overflow-auto text-sm'>
return <div className='bg-void-bg-3 p-1 rounded-sm overflow-auto text-sm'>
<div className='!select-text cursor-auto'>
{children}
</div>
@ -1324,7 +1341,9 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: string, uri: URI, codeStr: string }) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
return <div className='flex items-center gap-1'>
<StatusIndicatorHTML applyBoxId={applyBoxId} uri={uri} />
<StatusIndicatorForApplyButton applyBoxId={applyBoxId} uri={uri} />
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} />}
<ApplyButtonsHTML applyBoxId={applyBoxId} uri={uri} codeStr={codeStr} reapplyIcon={true} />
@ -1333,17 +1352,23 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
const InvalidTool = ({ toolName }: { toolName: string }) => {
const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'invalid_params' })
const desc1 = 'Invalid parameters'
const icon = null
const isError = true
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{message}
</CodeChildren>
</ToolChildrenWrapper>
return <ToolHeaderWrapper {...componentParams} />
}
const CanceledTool = ({ toolName }: { toolName: string }) => {
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'rejected' })
const desc1 = ''
@ -1699,7 +1724,10 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
// add children
if (toolMessage.type !== 'tool_error') {
const { params } = toolMessage
const { params, result } = toolMessage
// componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
@ -1763,18 +1791,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
resolveReason.type === 'toofull' ? `\n(truncated)`
: null
componentParams.children = <ToolChildrenWrapper className='font-mono whitespace-pre text-nowrap overflow-auto text-sm'>
componentParams.children = <ToolChildrenWrapper className='whitespace-pre text-nowrap overflow-auto text-sm'>
<div className='!select-text cursor-auto'>
<div>
<span>{`Ran command: `}</span>
<span className="text-void-fg-1">{command}</span>
<span className="text-void-fg-1 font-sans">{`Ran command: `}</span>
<span className="font-mono">{command}</span>
</div>
<div>
<span>{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}</span>
<span>{`Result: `}</span>
<span className="text-void-fg-1">{terminalResult}</span>
<span className="text-void-fg-1">{additionalDetailsStr}</span>
<span className="text-void-fg-1 font-mono">{terminalResult}</span>
<span className="text-void-fg-1 font-mono">{additionalDetailsStr}</span>
</div>
</div>
</ToolChildrenWrapper>
@ -1843,19 +1871,20 @@ type ChatBubbleProps = {
isCommitted: boolean,
chatIsRunning: IsRunningType,
threadId: string,
currCheckpointIdx: number,
currCheckpointIdx: number | undefined,
_scrollToBottom: (() => void) | null,
}
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
const role = chatMessage.role
const isCheckpointGhost = messageIdx > currCheckpointIdx && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
if (role === 'user') {
return <UserMessageComponent
chatMessage={chatMessage}
isCheckpointGhost={isCheckpointGhost}
currCheckpointIdx={currCheckpointIdx}
messageIdx={messageIdx}
_scrollToBottom={_scrollToBottom}
/>
@ -1899,7 +1928,7 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
if (chatMessage.type === 'invalid_params') {
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<InvalidTool toolName={chatMessage.name} />
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} />
</div>
}
@ -1921,7 +1950,7 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
return null
}
else if (role === 'decorative_canceled_tool') {
else if (role === 'interrupted_streaming_tool') {
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<CanceledTool toolName={chatMessage.name} />
</div>
@ -1942,28 +1971,291 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
export const AcceptAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => (
<button
className={`
px-1 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
rounded-md
cursor-pointer
${className}
`}
style={{
backgroundColor: acceptAllBg,
border: acceptBorder,
}}
type='button'
onClick={onClick}
>
{text ? <span>{text}</span> : <Check size={16} />}
</button>
)
export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => (
<button
className={`
px-1 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
rounded-md
cursor-pointer
${className}
`}
style={{
backgroundColor: rejectAllBg,
border: rejectBorder,
}}
type='button'
onClick={onClick}
>
{text ? <span>{text}</span> : <X size={16} />}
</button>
)
const CommandBarInChat = () => {
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
const [isExpanded, setIsExpanded] = useState(false)
const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
const numFilesChanged = sortedCommandBarURIs.length
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const commandService = accessor.get('ICommandService')
const chatThreadsState = useChatThreadsState()
const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
if (!sortedCommandBarURIs || sortedCommandBarURIs.length === 0) {
return null
}
const [fileDetailsOpenedState, setFileDetailsOpenedState] = useState<'auto-opened' | 'auto-closed' | 'user-opened' | 'user-closed'>('auto-closed');
const isFileDetailsOpened = fileDetailsOpenedState === 'auto-opened' || fileDetailsOpenedState === 'user-opened';
useEffect(() => {
// close the file details if there are no files
// this converts 'user-closed' to 'auto-closed'
if (numFilesChanged === 0) {
setFileDetailsOpenedState('auto-closed')
}
// open the file details if it hasnt been closed
if (numFilesChanged > 0 && fileDetailsOpenedState !== 'user-closed') {
setFileDetailsOpenedState('auto-opened')
}
}, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged])
const isFinishedMakingThreadChanges = numFilesChanged !== 0 && (chatThreadsStreamState ? !chatThreadsStreamState.isRunning : true)
// ======== status of agent ========
// This icon answers the question "is the LLM doing work on this thread?"
// assume it is single threaded for now
// green = Running
// orange = Requires action
// dark = Done
const threadStatus = (
chatThreadsStreamState?.isRunning === 'awaiting_user' ? { title: 'Needs Approval', color: 'yellow', } as const
: chatThreadsStreamState?.isRunning ? { title: 'Running', color: 'orange', } as const
: { title: 'Done', color: 'dark', } as const
)
const threadStatusHTML = <StatusIndicator className='mx-1' indicatorColor={threadStatus.color} title={threadStatus.title} />
// ======== info about changes ========
// num files changed
// acceptall + rejectall
// popup info about each change (each with num changes + acceptall + rejectall of their own)
const numFilesChangedStr = numFilesChanged === 0 ? 'No files with changes'
: `${sortedCommandBarURIs.length} file${numFilesChanged === 1 ? '' : 's'} with changes`
const acceptRejectAllButtons = <div
// do this with opacity so that the height remains the same at all times
className={`flex items-center gap-0.5
${isFinishedMakingThreadChanges ? '' : 'opacity-0 pointer-events-none'}`
}
>
<IconShell1 // RejectAllButtonWrapper
// text="Reject All"
// className="text-xs"
Icon={X}
onClick={() => {
sortedCommandBarURIs.forEach(uri => {
editCodeService.acceptOrRejectAllDiffAreas({
uri,
removeCtrlKs: true,
behavior: "reject",
_addToHistory: true,
});
});
}}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Reject all'
/>
<IconShell1 // AcceptAllButtonWrapper
// text="Accept All"
// className="text-xs"
Icon={Check}
onClick={() => {
sortedCommandBarURIs.forEach(uri => {
editCodeService.acceptOrRejectAllDiffAreas({
uri,
removeCtrlKs: true,
behavior: "accept",
_addToHistory: true,
});
});
}}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Accept all'
/>
</div>
// !select-text cursor-auto
const fileDetailsContent = <div className="px-2 gap-1 w-full">
{sortedCommandBarURIs.map((uri, i) => {
const basename = getBasename(uri.fsPath)
const { sortedDiffIds, isStreaming } = commandBarStateOfURI[uri.fsPath] ?? {}
const isFinishedMakingFileChanges = !isStreaming
const numDiffs = sortedDiffIds?.length || 0
const fileStatus = (isFinishedMakingFileChanges
? { title: 'Done', color: 'dark', } as const
: { title: 'Running', color: 'orange', } as const
)
const fileNameHTML = <div
className="flex items-center gap-1.5 text-void-fg-3 hover:brightness-125 transition-all duration-200 cursor-pointer"
onClick={() => commandService.executeCommand('vscode.open', uri, { preview: true })}
>
{/* <FileIcon size={14} className="text-void-fg-3" /> */}
<span className="text-void-fg-3">{basename}</span>
</div>
const detailsContent = <div className='flex px-4'>
<span className="text-void-fg-3 opacity-80">{numDiffs} diff{numDiffs !== 1 ? 's' : ''}</span>
</div>
const acceptRejectButtons = <div
// do this with opacity so that the height remains the same at all times
className={`flex items-center gap-0.5
${isFinishedMakingFileChanges ? '' : 'opacity-0 pointer-events-none'}
`}
>
<JumpToFileButton
uri={uri}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Go to file'
/>
<IconShell1 // RejectAllButtonWrapper
Icon={X}
onClick={() => { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "reject", _addToHistory: true, }); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Reject file'
/>
<IconShell1 // AcceptAllButtonWrapper
Icon={Check}
onClick={() => { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "accept", _addToHistory: true, }); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Accept file'
/>
</div>
const fileStatusHTML = <StatusIndicator className='mx-1' indicatorColor={fileStatus.color} title={fileStatus.title} />
return (
// name, details
<div key={i} className="flex justify-between items-center">
<div className="flex items-center">
{fileNameHTML}
{detailsContent}
</div>
<div className="flex items-center gap-2">
{acceptRejectButtons}
{fileStatusHTML}
</div>
</div>
)
})}
</div>
const fileDetailsButton = (
<button
className={`flex items-center gap-1 rounded ${numFilesChanged === 0 ? 'cursor-pointer' : 'cursor-pointer hover:brightness-125 transition-all duration-200'}`}
onClick={() => isFileDetailsOpened ? setFileDetailsOpenedState('user-closed') : setFileDetailsOpenedState('user-opened')}
type='button'
disabled={numFilesChanged === 0}
>
<svg
className="transition-transform duration-200 size-3.5"
style={{
transform: isFileDetailsOpened ? 'rotate(0deg)' : 'rotate(180deg)',
transition: 'transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1)'
}}
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="18 15 12 9 6 15"></polyline>
</svg>
{numFilesChangedStr}
</button>
)
return (
<SimplifiedToolHeader title={'Changes'}>
{sortedCommandBarURIs.map((uri, i) => (
<ListableToolItem
key={i}
name={getBasename(uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
))}
</SimplifiedToolHeader>
<>
{/* file details */}
<div className='px-2'>
<div
className={`
select-none
flex w-full rounded-t-lg bg-void-bg-3
text-void-fg-3 text-xs text-nowrap
overflow-hidden transition-all duration-200 ease-in-out
${isFileDetailsOpened ? 'max-h-24' : 'max-h-0'}
`}
>
{fileDetailsContent}
</div>
</div>
{/* main content */}
<div
className={`
select-none
flex w-full rounded-t-lg bg-void-bg-3
text-void-fg-3 text-xs text-nowrap
border-t border-l border-r border-zinc-300/10
px-2 py-1
justify-between
`}
>
<div className="flex gap-2 items-center">
{fileDetailsButton}
</div>
<div className="flex gap-2 items-center">
{acceptRejectAllButtons}
{threadStatusHTML}
</div>
</div>
</>
)
}
@ -2004,12 +2296,12 @@ export const SidebarChat = () => {
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
const isRunning = currThreadStreamState?.isRunning
const latestError = currThreadStreamState?.error
const messageSoFar = currThreadStreamState?.messageSoFar
const displayContentSoFar = currThreadStreamState?.displayContentSoFar
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
const toolNameSoFar = currThreadStreamState?.toolNameSoFar
const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar
const toolIsGenerating = !!toolNameSoFar && toolNameSoFar === 'edit_file' // show loading for slow tools (right now just edit)
// this is just if it's currently being generated, NOT if it's currently running
const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit)
// ----- SIDEBAR CHAT state (local) -----
@ -2022,8 +2314,6 @@ export const SidebarChat = () => {
const sidebarRef = useRef<HTMLDivElement>(null)
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
useScrollbarStyles(sidebarRef)
const onSubmit = useCallback(async () => {
if (isDisabled) return
@ -2061,11 +2351,10 @@ export const SidebarChat = () => {
const threadId = currentThread.id
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity)
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')
// tool request shows up as Editing... if in progress
return previousMessages.map((message, i) => {
return <ChatBubble
@ -2079,17 +2368,18 @@ export const SidebarChat = () => {
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
/>
})
}, [previousMessages, isRunning, threadId])
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
const streamingChatIdx = previousMessagesHTML.length
const currStreamingMessageHTML = reasoningSoFar || messageSoFar || isRunning ?
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
<ChatBubble
key={getChatBubbleId(threadId, streamingChatIdx)}
currCheckpointIdx={currCheckpointIdx} // if streaming, can't be the case
currCheckpointIdx={currCheckpointIdx}
chatMessage={{
role: 'assistant',
content: messageSoFar ?? '',
displayContent: displayContentSoFar ?? '',
reasoning: reasoningSoFar ?? '',
toolCall: toolCallSoFar,
anthropicReasoning: null,
}}
messageIdx={streamingChatIdx}
@ -2101,8 +2391,6 @@ export const SidebarChat = () => {
/> : null
const generatingToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
const messagesHTML = <ScrollToBottomContainer
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes
scrollContainerRef={scrollContainerRef}
@ -2112,18 +2400,20 @@ export const SidebarChat = () => {
w-full h-full
overflow-x-hidden
overflow-y-auto
${previousMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
${previousMessagesHTML.length === 0 && !displayContentSoFar ? 'hidden' : ''}
`}
>
{/* previous messages */}
{previousMessagesHTML}
{currStreamingMessageHTML}
{toolIsGenerating ?
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={generatingToolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)}
title={toolCallSoFar && toolNames.includes(toolCallSoFar.name as ToolName) ?
titleOfToolName[toolCallSoFar.name as ToolName]?.proposed
: toolCallSoFar?.name}
desc1={<span className='flex items-center'>Generating<IconLoading /></span>}
/>
: null}
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>
@ -2159,33 +2449,40 @@ export const SidebarChat = () => {
}
}, [onSubmit, onAbort, isRunning])
const inputForm = <div
key={'input' + chatThreadsState.currentThreadId}
className='px-2 pb-2'>
<VoidChatArea
featureName='Chat'
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={!!isRunning}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={previousMessagesHTML.length === 0}
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
const inputForm = <div key={'input' + chatThreadsState.currentThreadId}>
<div className='px-4'>
{previousMessages.length > 0 &&
<CommandBarInChat />
}
</div>
<div
className='px-2 pb-2'
>
<VoidInputBox2
className={`min-h-[81px] px-0.5 py-0.5`}
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
<VoidChatArea
featureName='Chat'
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={!!isRunning}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={previousMessagesHTML.length === 0}
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2
className={`min-h-[81px] px-0.5 py-0.5`}
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidChatArea>
</VoidChatArea>
</div>
</div>
return (

View file

@ -90,7 +90,7 @@ export const SidebarThreadSelector = () => {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>

View file

@ -106,6 +106,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
return (
<textarea
autoFocus={false}
ref={useCallback((r: HTMLTextAreaElement | null) => {
if (fnsRef)
fnsRef.current = fns
@ -153,12 +154,13 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
})
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, ...inputProps }: {
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, compact, ...inputProps }: {
value: string;
onChangeValue: (value: string) => void;
placeholder: string;
className?: string;
disabled?: boolean;
compact?: boolean;
passwordBlur?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
@ -168,7 +170,11 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
onChange={(e) => onChangeValue(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
rounded
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}`}
style={{
@ -635,7 +641,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
onClick={() => setIsOpen(!isOpen)}
>
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
<span className={`truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
{getOptionDisplayName(selectedOption)}
</span>
<svg
@ -954,9 +960,9 @@ export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: Bl
}
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { children: React.ReactNode; disabled?: boolean; onClick: () => void; className?: string }) => {
return <button disabled={disabled}
className='px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap'
className={`px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap flex items-center justify-center ${className || ''}`}
onClick={onClick}
>{children}</button>
}

View file

@ -350,9 +350,9 @@ export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
export const useCommandBarState = () => {
const accessor = useAccessor()
const commandBarService = accessor.get('IVoidCommandBarService')
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
const [s, ss] = useState({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
const listener = useCallback(() => {
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
ss({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
}, [commandBarService])
useCommandBarURIListener(listener)

View file

@ -1,128 +1,130 @@
import { useEffect } from 'react';
// Get rid of this as it was causing lag
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
useEffect(() => {
if (!containerRef.current) return;
// import { useEffect } from 'react';
// Create selector for specific overflow classes
const overflowSelector = [
'[class*="overflow-auto"]',
'[class*="overflow-x-auto"]',
'[class*="overflow-y-auto"]'
].join(',');
// export const useScrollbarStyles = (containerRef: React.RefObject<HTMLDivElement | null>) => {
// useEffect(() => {
// if (!containerRef.current) return;
// Function to initialize scrollbar styles for elements
const initializeScrollbarStyles = () => {
// Get all matching elements within the container, including the container itself
const scrollElements = [
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
];
// // Create selector for specific overflow classes
// const overflowSelector = [
// '[class*="overflow-auto"]',
// '[class*="overflow-x-auto"]',
// '[class*="overflow-y-auto"]'
// ].join(',');
// Apply basic styling to all elements
scrollElements.forEach(element => {
element.classList.add('void-scrollable-element');
});
// // Function to initialize scrollbar styles for elements
// const initializeScrollbarStyles = () => {
// // Get all matching elements within the container, including the container itself
// const scrollElements = [
// ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
// ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
// ];
// Only initialize fade effects for elements that haven't been initialized yet
scrollElements.forEach(element => {
if (!(element as any).__scrollbarCleanup) {
let fadeTimeout: NodeJS.Timeout | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
// // Apply basic styling to all elements
// scrollElements.forEach(element => {
// element.classList.add('void-scrollable-element');
// });
const fadeIn = () => {
if (fadeInterval) clearInterval(fadeInterval);
// // Only initialize fade effects for elements that haven't been initialized yet
// scrollElements.forEach(element => {
// if (!(element as any).__scrollbarCleanup) {
// let fadeTimeout: NodeJS.Timeout | null = null;
// let fadeInterval: NodeJS.Timeout | null = null;
let step = 0;
fadeInterval = setInterval(() => {
if (step <= 10) {
element.classList.remove(`show-scrollbar-${step - 1}`);
element.classList.add(`show-scrollbar-${step}`);
step++;
} else {
clearInterval(fadeInterval!);
}
}, 10);
};
// const fadeIn = () => {
// if (fadeInterval) clearInterval(fadeInterval);
const fadeOut = () => {
if (fadeInterval) clearInterval(fadeInterval);
// let step = 0;
// fadeInterval = setInterval(() => {
// if (step <= 10) {
// element.classList.remove(`show-scrollbar-${step - 1}`);
// element.classList.add(`show-scrollbar-${step}`);
// step++;
// } else {
// clearInterval(fadeInterval!);
// }
// }, 10);
// };
let step = 10;
fadeInterval = setInterval(() => {
if (step >= 0) {
element.classList.remove(`show-scrollbar-${step + 1}`);
element.classList.add(`show-scrollbar-${step}`);
step--;
} else {
clearInterval(fadeInterval!);
}
}, 60);
};
// const fadeOut = () => {
// if (fadeInterval) clearInterval(fadeInterval);
const onMouseEnter = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
fadeIn();
};
// let step = 10;
// fadeInterval = setInterval(() => {
// if (step >= 0) {
// element.classList.remove(`show-scrollbar-${step + 1}`);
// element.classList.add(`show-scrollbar-${step}`);
// step--;
// } else {
// clearInterval(fadeInterval!);
// }
// }, 60);
// };
const onMouseLeave = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
fadeTimeout = setTimeout(() => {
fadeOut();
}, 10);
};
// const onMouseEnter = () => {
// if (fadeTimeout) clearTimeout(fadeTimeout);
// if (fadeInterval) clearInterval(fadeInterval);
// fadeIn();
// };
element.addEventListener('mouseenter', onMouseEnter);
element.addEventListener('mouseleave', onMouseLeave);
// const onMouseLeave = () => {
// if (fadeTimeout) clearTimeout(fadeTimeout);
// fadeTimeout = setTimeout(() => {
// fadeOut();
// }, 10);
// };
// Store cleanup function
const cleanup = () => {
element.removeEventListener('mouseenter', onMouseEnter);
element.removeEventListener('mouseleave', onMouseLeave);
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
element.classList.remove('void-scrollable-element');
// Remove any remaining show-scrollbar classes
for (let i = 0; i <= 10; i++) {
element.classList.remove(`show-scrollbar-${i}`);
}
};
// element.addEventListener('mouseenter', onMouseEnter);
// element.addEventListener('mouseleave', onMouseLeave);
// Store the cleanup function on the element for later use
(element as any).__scrollbarCleanup = cleanup;
}
});
};
// // Store cleanup function
// const cleanup = () => {
// element.removeEventListener('mouseenter', onMouseEnter);
// element.removeEventListener('mouseleave', onMouseLeave);
// if (fadeTimeout) clearTimeout(fadeTimeout);
// if (fadeInterval) clearInterval(fadeInterval);
// element.classList.remove('void-scrollable-element');
// // Remove any remaining show-scrollbar classes
// for (let i = 0; i <= 10; i++) {
// element.classList.remove(`show-scrollbar-${i}`);
// }
// };
// Initialize for the first time
initializeScrollbarStyles();
// // Store the cleanup function on the element for later use
// (element as any).__scrollbarCleanup = cleanup;
// }
// });
// };
// Set up mutation observer to do the same
const observer = new MutationObserver(() => {
initializeScrollbarStyles();
});
// // Initialize for the first time
// initializeScrollbarStyles();
// Start observing the container for child changes
observer.observe(containerRef.current, {
childList: true,
subtree: true
});
// // Set up mutation observer to do the same
// const observer = new MutationObserver(() => {
// initializeScrollbarStyles();
// });
return () => {
observer.disconnect();
// Your existing cleanup code...
if (containerRef.current) {
const scrollElements = [
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
];
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
}
});
}
};
}, [containerRef]);
};
// // Start observing the container for child changes
// observer.observe(containerRef.current, {
// childList: true,
// subtree: true
// });
// return () => {
// observer.disconnect();
// // Your existing cleanup code...
// if (containerRef.current) {
// const scrollElements = [
// ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
// ...Array.from(containerRef.current.querySelectorAll(overflowSelector))
// ];
// scrollElements.forEach(element => {
// if ((element as any).__scrollbarCleanup) {
// (element as any).__scrollbarCleanup();
// }
// });
// }
// };
// }, [containerRef]);
// };

View file

@ -9,8 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
import '../styles.css'
import { useCallback, useEffect, useState, useRef } from 'react';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
@ -39,7 +40,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
// useEffect(() => {
@ -211,38 +212,47 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
if (!isADiffZoneInAnyFile) return null
const acceptAllButton = <button
className='text-nowrap'
// const acceptAllButton = <button
// className='text-nowrap'
// onClick={onAcceptAll}
// style={{
// backgroundColor: acceptAllBg,
// border: acceptBorder,
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Accept File
// </button>
// const rejectAllButton = <button
// className='text-nowrap'
// onClick={onRejectAll}
// style={{
// backgroundColor: rejectBg,
// border: rejectBorder,
// color: 'white',
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Reject File
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Accept File'}
onClick={onAcceptAll}
style={{
backgroundColor: acceptAllBg,
border: acceptBorder,
color: buttonTextColor,
fontSize: buttonFontSize,
padding: '2px 4px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Accept File
</button>
/>
const rejectAllButton = <button
className='text-nowrap'
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject File'}
onClick={onRejectAll}
style={{
backgroundColor: rejectAllBg,
border: rejectBorder,
color: 'white',
fontSize: buttonFontSize,
padding: '2px 4px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Reject File
</button>
/>
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
{acceptAllButton}
@ -273,7 +283,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
{upButton}
{downButton}
<span className="min-w-16 px-2 text-xs">
<span className="min-w-16 px-2 text-xs leading-[1]">
{isADiffInThisFile ?
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming' ?
@ -289,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
{rightButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
<span className="min-w-16 px-2 text-xs">
<span className="min-w-16 px-2 text-xs leading-[1]">
{currFileIdx !== null ?
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
@ -299,7 +309,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
</div>
</div>
return <div className={`flex flex-col items-center gap-y-2 mx-2 pointer-events-auto`}>
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
{showAcceptRejectAll && acceptRejectAllButtons}
{leftRightUpDownButtons}

View file

@ -0,0 +1,170 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useAccessor, useActiveURI, useIsDark, useSettingsState } from '../util/services.js';
import '../styles.css'
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { Circle, MoreVertical } from 'lucide-react';
import { useEffect, useState } from 'react';
import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
const isDark = useIsDark()
return <div
className={`@@void-scope ${isDark ? 'dark' : ''}`}
>
<VoidSelectionHelper {...props} />
</div>
}
const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
const accessor = useAccessor()
const keybindingService = accessor.get('IKeybindingService')
const commandService = accessor.get('ICommandService')
const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
const dividerHTML = <div className='w-[0.5px] bg-void-border-3'></div>
const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
useEffect(() => {
const disposable = commandService.onWillExecuteCommand(e => {
if (e.commandId === VOID_CTRL_L_ACTION_ID || e.commandId === VOID_CTRL_K_ACTION_ID) {
setClickState('clickedOption')
}
});
return () => {
disposable.dispose();
};
}, [commandService, setClickState]);
// rerender when the key changes
if (reactRerenderCount !== rerenderKey) {
setReactRerenderKey(rerenderKey)
setClickState('init')
}
// useEffect(() => {
// }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
// if the user selected an option, close
if (clickState === 'clickedOption') {
return null
}
const defaultHTML = <>
{ctrlLKeybind &&
<div
className='
flex items-center px-2 py-1.5
cursor-pointer
'
onClick={() => {
commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
setClickState('clickedOption');
}}
>
<span>Add to Chat</span>
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
{ctrlLKeybind.getLabel()}
</span>
</div>
}
{ctrlLKeybind && ctrlKKeybind &&
dividerHTML
}
{ctrlKKeybind &&
<div
className='
flex items-center px-2 py-1.5
cursor-pointer
'
onClick={() => {
commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
setClickState('clickedOption');
}}
>
<span className='ml-1'>Edit Inline</span>
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
{ctrlKKeybind.getLabel()}
</span>
</div>
}
{dividerHTML}
<div
className='
flex items-center px-0.5
cursor-pointer
'
onClick={() => {
setClickState('clickedMore');
}}
>
<MoreVertical className="w-4" />
</div>
</>
const moreOptionsHTML = <>
<div
className='
flex items-center px-2 py-1.5
cursor-pointer
'
onClick={() => {
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
setClickState('clickedOption');
}}
>
Disable Suggestions?
</div>
{dividerHTML}
<div
className='
flex items-center px-0.5
cursor-pointer
'
onClick={() => {
setClickState('init');
}}
>
<MoreVertical className="w-4" />
</div>
</>
return <div className='
pointer-events-auto select-none
z-[1000]
rounded-sm shadow-md flex flex-nowrap text-nowrap
border border-void-border-3 bg-void-bg-2
transition-all duration-200
'>
{clickState === 'init' ? defaultHTML
: clickState === 'clickedMore' ? moreOptionsHTML
: <></>
}
</div>
}

View file

@ -5,5 +5,9 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidCommandBarMain } from './VoidCommandBar.js'
import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)

View file

@ -0,0 +1,97 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Tooltip } from 'react-tooltip';
import 'react-tooltip/dist/react-tooltip.css';
import { useIsDark } from '../util/services.js';
/**
* Creates a configured global tooltip component with consistent styling
* To use:
* 1. Mount a Tooltip with some id eg id='void-tooltip'
* 2. Add data-tooltip-id="void-tooltip" and data-tooltip-content="Your tooltip text" to any element
*/
export const VoidTooltip = () => {
const isDark = useIsDark()
return (
// use native colors so we don't have to worry about @@void-scope styles
// --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-3: var(--vscode-editor-background);
// --void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
// --void-fg-1: var(--vscode-editor-foreground);
// --void-fg-2: var(--vscode-input-foreground);
// --void-fg-3: var(--vscode-input-placeholderForeground);
// /* --void-fg-4: var(--vscode-tab-inactiveForeground); */
// --void-fg-4: var(--vscode-list-deemphasizedForeground);
// --void-warning: var(--vscode-charts-yellow);
// --void-border-1: var(--vscode-commandCenter-activeBorder);
// --void-border-2: var(--vscode-commandCenter-border);
// --void-border-3: var(--vscode-commandCenter-inactiveBorder);
// --void-border-4: var(--vscode-editorGroup-border);
<>
<style>
{`
#void-tooltip, #void-tooltip-orange, #void-tooltip-green {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
z-index: 999;
}
#void-tooltip {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
#void-tooltip-orange {
background-color: #F6762A;
color: white;
}
#void-tooltip-green {
background-color: #228B22;
color: white;
}
.react-tooltip-arrow {
z-index: -1 !important; /* Keep arrow behind content (somehow this isnt done automatically) */
}
`}
</style>
<Tooltip
id="void-tooltip"
// border='1px solid var(--vscode-editorGroup-border)'
border='1px solid rgba(100,100,100,.2)'
opacity={1}
delayShow={50}
/>
<Tooltip
id="void-tooltip-orange"
border='1px solid rgba(200,200,200,.3)'
opacity={1}
delayShow={50}
/>
<Tooltip
id="void-tooltip-green"
border='1px solid rgba(200,200,200,.3)'
opacity={1}
delayShow={50}
/>
</>
);
};

View file

@ -0,0 +1,9 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidTooltip } from './VoidTooltip.js'
export const mountVoidTooltip = mountFnGenerator(VoidTooltip)

View file

@ -7,9 +7,10 @@ import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
'./src2/void-command-bar-tsx/index.tsx',
'./src2/void-editor-widgets-tsx/index.tsx',
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/void-tooltip/index.tsx',
'./src2/quick-edit-tsx/index.tsx',
'./src2/diff/index.tsx',
],

View file

@ -189,7 +189,7 @@ registerAction2(class extends Action2 {
super({
id: VOID_CTRL_L_ACTION_ID,
f1: true,
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension

View file

@ -108,7 +108,7 @@ export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
const container = viewContainerRegistry.registerViewContainer({
id: VOID_VIEW_CONTAINER_ID,
title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L)
title: nls.localize2('voidContainer', 'Chat'), // this is used to say "Void" (Ctrl + L)
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, {
mergeViewWithContainerWhenSingleView: true,
orientation: Orientation.HORIZONTAL,

View file

@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
import { ISearchService } from '../../../services/search/common/search.js'
import { IEditCodeService } from './editCodeServiceInterface.js'
import { ITerminalToolService } from './terminalToolService.js'
import { ToolCallParams, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { basename } from '../../../../base/common/path.js'
@ -16,6 +16,8 @@ import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js'
// tool use for AI
@ -23,7 +25,7 @@ import { timeout } from '../../../../base/common/async.js'
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => Promise<ToolCallParams[T]> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
@ -34,35 +36,16 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awai
export const MAX_FILE_CHARS_PAGE = 50_000
export const MAX_CHILDREN_URIs_PAGE = 500
export const MAX_TERMINAL_CHARS_PAGE = 20_000
export const TERMINAL_TIMEOUT_TIME = 15
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
export const TERMINAL_BG_WAIT_TIME = 1
const validateJSON = (s: string): { [s: string]: unknown } => {
try {
const o = JSON.parse(s)
if (typeof o !== 'object') throw new Error()
if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... }
return o.result
}
return o
}
catch (e) {
throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`)
}
}
const isFalsy = (u: unknown) => {
return !u || u === 'null' || u === 'undefined'
}
const validateStr = (argName: string, value: unknown) => {
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`)
return value
}
@ -70,7 +53,7 @@ const validateStr = (argName: string, value: unknown) => {
// We are NOT checking to make sure in workspace
// TODO!!!! check to make sure folder/file exists
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`)
const uri = URI.file(uriStr)
return uri
}
@ -109,6 +92,7 @@ const validateNumber = (numStr: unknown, opts: { default: number | null }) => {
}
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
if (!paramsUnknown) return false
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
const params = paramsUnknown
const isRecursive = params.includes('r')
@ -172,10 +156,8 @@ export class ToolsService implements IToolsService {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.validateParams = {
read_file: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o
read_file: async (params: RawToolParamsObj) => {
const { uri: uriStr, start_line: startLineUnknown, end_line: endLineUnknown, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
@ -184,43 +166,39 @@ export class ToolsService implements IToolsService {
return { uri, startLine, endLine, pageNumber }
},
ls_dir: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
ls_dir: async (params: RawToolParamsObj) => {
const { uri: uriStr, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { rootURI: uri, pageNumber }
},
get_dir_structure: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, } = o
get_dir_structure: async (params: RawToolParamsObj) => {
const { uri: uriStr, } = params
const uri = validateURI(uriStr)
return { rootURI: uri }
},
search_pathnames_only: async (params: string) => {
const o = validateJSON(params)
search_pathnames_only: async (params: RawToolParamsObj) => {
const {
query: queryUnknown,
include: includeUnknown,
pageNumber: pageNumberUnknown
} = o
search_in_folder: includeUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const include = validateOptionalStr('include', includeUnknown)
const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown)
return { queryStr, include, pageNumber }
return { queryStr, searchInFolder, pageNumber }
},
search_files: async (params: string) => {
const o = validateJSON(params)
search_files: async (params: RawToolParamsObj) => {
const {
query: queryUnknown,
searchInFolder: searchInFolderUnknown,
isRegex: isRegexUnknown,
pageNumber: pageNumberUnknown
} = o
search_in_folder: searchInFolderUnknown,
is_regex: isRegexUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
@ -233,18 +211,16 @@ export class ToolsService implements IToolsService {
// ---
create_file_or_folder: async (params: string) => {
const o = validateJSON(params)
const { uri: uriUnknown } = o
create_file_or_folder: async (params: RawToolParamsObj) => {
const { uri: uriUnknown } = params
const uri = validateURI(uriUnknown)
const uriStr = validateStr('uri', uriUnknown)
const isFolder = checkIfIsFolder(uriStr)
return { uri, isFolder }
},
delete_file_or_folder: async (params: string) => {
const o = validateJSON(params)
const { uri: uriUnknown, params: paramsStr } = o
delete_file_or_folder: async (params: RawToolParamsObj) => {
const { uri: uriUnknown, params: paramsStr } = params
const uri = validateURI(uriUnknown)
const isRecursive = validateRecursiveParamStr(paramsStr)
const uriStr = validateStr('uri', uriUnknown)
@ -252,17 +228,15 @@ export class ToolsService implements IToolsService {
return { uri, isRecursive, isFolder }
},
edit_file: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
edit_file: async (params: RawToolParamsObj) => {
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
return { uri, changeDescription }
},
run_terminal_command: async (s: string) => {
const o = validateJSON(s)
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
run_terminal_command: async (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params
const command = validateStr('command', commandUnknown)
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
@ -302,17 +276,15 @@ export class ToolsService implements IToolsService {
},
get_dir_structure: async ({ rootURI }) => {
const result = await this.directoryStrService.getDirectoryStrTool(rootURI)
let str = result.str
if (result.wasCutOff) str += '\n(Result was truncated)'
const str = await this.directoryStrService.getDirectoryStrTool(rootURI)
return { result: { str } }
},
search_pathnames_only: async ({ queryStr, include, pageNumber }) => {
search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => {
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
filePattern: queryStr,
includePattern: include ?? undefined,
includePattern: searchInFolder ?? undefined,
})
const data = await searchService.fileSearch(query, CancellationToken.None)
@ -387,13 +359,18 @@ export class ToolsService implements IToolsService {
const lintErrorsPromise = applyDonePromise.then(async () => {
await timeout(500)
const lintErrorsStr = this.markerService
.read({ resource: uri })
.map(l => l.message)
.join('\n')
if (!lintErrorsStr) return { lintErrorsStr: null }
return { lintErrorsStr }
const lintErrors = this.markerService
.read({ resource: uri })
.map(l => ({
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
message: l.message,
startLineNumber: l.startLineNumber,
endLineNumber: l.endLineNumber,
} satisfies LintErrorItem))
if (!lintErrors.length) return { lintErrors: null }
return { lintErrors, }
})
return { result: lintErrorsPromise, interruptTool }
@ -407,6 +384,8 @@ export class ToolsService implements IToolsService {
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
const lintErrorsStr = (lintErrors: LintErrorItem[]) => lintErrors.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`).join('\n\n')
// given to the LLM after the call
this.stringOfResult = {
read_file: (params, result) => {
@ -433,8 +412,10 @@ export class ToolsService implements IToolsService {
return `URI ${params.uri.fsPath} successfully deleted.`
},
edit_file: (params, result) => {
const additionalStr = result.lintErrorsStr ? `Lint errors found after change:\n${result.lintErrorsStr}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : `No lint errors found.`
return `Change successfully made to ${params.uri.fsPath}. ${additionalStr}`
const additionalStr = result.lintErrors ? `Lint errors found after change:\n${lintErrorsStr(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : `No lint errors found.`
return `Change successfully made to ${params.uri.fsPath}.${additionalStr}`
},
run_terminal_command: (params, result) => {
const {
@ -447,7 +428,7 @@ export class ToolsService implements IToolsService {
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
if (resolveReason.type === 'timeout') {
return `Terminal command ran in ${terminalDesc}, but timed out after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
return `Terminal command ran in ${terminalDesc}, but did not complete after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
}
else if (resolveReason.type === 'bgtask') {
return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`

View file

@ -0,0 +1,55 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { mountVoidTooltip } from './react/out/void-tooltip/index.js';
import { h, getActiveWindow } from '../../../../base/browser/dom.js';
// Tooltip contribution that mounts the component at startup
export class TooltipContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.voidTooltip';
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.initializeTooltip();
}
private initializeTooltip(): void {
// Get the active window reference for multi-window support
const targetWindow = getActiveWindow();
// Find the monaco-workbench element using the proper window reference
const workbench = targetWindow.document.querySelector('.monaco-workbench');
if (workbench) {
// Create a container element for the tooltip using h function
const tooltipContainer = h('div.void-tooltip-container').root;
workbench.appendChild(tooltipContainer);
// Mount the React component
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
const result = mountVoidTooltip(tooltipContainer, accessor);
if (result && typeof result.dispose === 'function') {
this._register(toDisposable(result.dispose));
}
});
// Register cleanup for the DOM element
this._register(toDisposable(() => {
if (tooltipContainer.parentElement) {
tooltipContainer.parentElement.removeChild(tooltipContainer);
}
}));
}
}
}
// Register the contribution to be initialized during the AfterRestored phase
registerWorkbenchContribution2(TooltipContribution.ID, TooltipContribution, WorkbenchPhase.AfterRestored);

View file

@ -46,6 +46,12 @@ import './metricsPollService.js'
// helper services
import './helperServices/consistentItemService.js'
// register selection helper
import './voidSelectionHelperWidget.js'
// register tooltip service
import './tooltipService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage

View file

@ -11,7 +11,7 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
import { mountVoidCommandBar } from './react/out/void-editor-widgets-tsx/index.js'
import { deepClone } from '../../../../base/common/objects.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IEditCodeService } from './editCodeServiceInterface.js';

View file

@ -0,0 +1,282 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
import { ICursorSelectionChangedEvent } from '../../../../editor/common/cursorEvents.js';
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
import { Selection } from '../../../../editor/common/core/selection.js';
import { RunOnceScheduler } from '../../../../base/common/async.js';
import * as dom from '../../../../base/browser/dom.js';
import { mountVoidSelectionHelper } from './react/out/void-editor-widgets-tsx/index.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { getLengthOfTextPx } from './editCodeService.js';
const minDistanceFromRightPx = 400;
const minLeftPx = 60;
export type VoidSelectionHelperProps = {
rerenderKey: number // alternates between 0 and 1
}
export class SelectionHelperContribution extends Disposable implements IEditorContribution, IOverlayWidget {
public static readonly ID = 'editor.contrib.voidSelectionHelper';
// react
private _rootHTML: HTMLElement;
private _rerender: (props?: any) => void = () => { };
private _rerenderKey: number = 0;
private _reactComponentDisposable: IDisposable | null = null;
// internal
private _isVisible = false;
private _showScheduler: RunOnceScheduler;
private _lastSelection: Selection | null = null;
constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IVoidSettingsService private readonly _voidSettingsService: IVoidSettingsService
) {
super();
// Create the container element for React component
const { root, content } = dom.h('div@root', [
dom.h('div@content', [])
]);
// Set styles for container
root.style.position = 'absolute';
root.style.display = 'none'; // Start hidden
root.style.pointerEvents = 'none';
root.style.marginLeft = '16px';
// Initialize React component
this._instantiationService.invokeFunction(accessor => {
if (this._reactComponentDisposable) {
this._reactComponentDisposable.dispose();
}
const res = mountVoidSelectionHelper(content, accessor);
if (!res) return;
this._reactComponentDisposable = res;
this._rerender = res.rerender;
this._register(this._reactComponentDisposable);
});
this._rootHTML = root;
// Register as overlay widget
this._editor.addOverlayWidget(this);
// Use scheduler to debounce showing widget
this._showScheduler = new RunOnceScheduler(() => {
if (this._lastSelection) {
this._showHelperForSelection(this._lastSelection);
}
}, 50);
// Register event listeners
this._register(this._editor.onDidChangeCursorSelection(e => this._onSelectionChange(e)));
// Add a flag to track if mouse is over the widget
let isMouseOverWidget = false;
this._rootHTML.addEventListener('mouseenter', () => {
isMouseOverWidget = true;
});
this._rootHTML.addEventListener('mouseleave', () => {
isMouseOverWidget = false;
});
// Only hide helper when text editor loses focus and mouse is not over the widget
this._register(this._editor.onDidBlurEditorText(() => {
if (!isMouseOverWidget) {
this._hideHelper();
}
}));
this._register(this._editor.onDidScrollChange(() => this._updatePositionIfVisible()));
this._register(this._editor.onDidLayoutChange(() => this._updatePositionIfVisible()));
}
// IOverlayWidget implementation
public getId(): string {
return SelectionHelperContribution.ID;
}
public getDomNode(): HTMLElement {
return this._rootHTML;
}
public getPosition(): IOverlayWidgetPosition | null {
return null; // We position manually
}
private _onSelectionChange(e: ICursorSelectionChangedEvent): void {
if (!this._editor.hasModel()) {
return;
}
if (this._editor.getModel().uri.scheme !== 'file') {
return;
}
const selection = this._editor.getSelection();
if (!selection || selection.isEmpty()) {
this._hideHelper();
return;
}
// Get selection text to check if it's worth showing the helper
const text = this._editor.getModel()!.getValueInRange(selection);
if (text.length < 3) {
this._hideHelper();
return;
}
// Store selection
this._lastSelection = new Selection(
selection.startLineNumber,
selection.startColumn,
selection.endLineNumber,
selection.endColumn
);
this._showScheduler.schedule();
}
// Update the _showHelperForSelection method to work with the React component
private _showHelperForSelection(selection: Selection): void {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel()!;
// get the longest length of the nearest neighbors of the target
const { tabSize: numSpacesInTab } = model.getFormattingOptions();
const spaceWidth = this._editor.getOption(EditorOption.fontInfo).spaceWidth;
const tabWidth = numSpacesInTab * spaceWidth;
const numLinesModel = model.getLineCount()
// Calculate right edge of visible editor area
const editorWidthPx = this._editor.getLayoutInfo().width;
const maxLeftPx = editorWidthPx - minDistanceFromRightPx
// returns the position where the box should go on the targetLine
const getBoxPosition = (targetLine: number): { top: number, left: number } => {
const targetPosition = this._editor.getScrolledVisiblePosition({ lineNumber: targetLine, column: 1 }) ?? { left: 0, top: 0 };
const { top: targetTop, left: targetLeft } = targetPosition
let targetWidth = 0;
for (let i = targetLine; i <= targetLine + 1; i++) {
// if not in range, continue
if (!(i >= 1) || !(i <= numLinesModel)) continue;
const content = model.getLineContent(i);
const currWidth = getLengthOfTextPx({
tabWidth,
spaceWidth,
content
})
targetWidth = Math.max(targetWidth, currWidth);
}
return {
top: targetTop,
left: targetLeft + targetWidth,
};
}
// Calculate the middle line of the selection
const startLine = selection.startLineNumber;
const endLine = selection.endLineNumber;
// const middleLine = Math.floor(startLine + (endLine - startLine) / 2);
const targetLine = endLine - startLine + 1 <= 2 ? startLine : startLine + 2;
let boxPos = getBoxPosition(targetLine);
// if the position of the box is too far to the right, keep searching for a good position
const lineDeltasToTry = [-1, -2, -3, 1, 2, 3];
if (boxPos.left > maxLeftPx) {
for (const lineDelta of lineDeltasToTry) {
boxPos = getBoxPosition(targetLine + lineDelta);
if (boxPos.left <= maxLeftPx) {
break;
}
}
}
if (boxPos.left > maxLeftPx) { // if still not found, make it 2 lines before
boxPos = getBoxPosition(targetLine - 2)
}
// Position the helper element at the end of the middle line but ensure it's visible
const xPosition = Math.max(Math.min(boxPos.left, maxLeftPx), minLeftPx);
const yPosition = boxPos.top;
// Update the React component position
this._rootHTML.style.left = `${xPosition}px`;
this._rootHTML.style.top = `${yPosition}px`;
this._rootHTML.style.display = 'flex'; // Show the container
this._isVisible = true;
// rerender
const enabled = this._voidSettingsService.state.globalSettings.showInlineSuggestions
&& this._editor.hasTextFocus() // needed since VS Code counts unfocused selections as selections, which causes this to rerender when it shouldnt (bad ux)
if (enabled) {
this._rerender({ rerenderKey: this._rerenderKey } satisfies VoidSelectionHelperProps)
this._rerenderKey = (this._rerenderKey + 1) % 2;
// this._reactComponentRerender();
}
}
private _hideHelper(): void {
this._rootHTML.style.display = 'none';
this._isVisible = false;
this._lastSelection = null;
}
private _updatePositionIfVisible(): void {
if (!this._isVisible || !this._lastSelection || !this._editor.hasModel()) {
return;
}
this._showHelperForSelection(this._lastSelection);
}
override dispose(): void {
this._hideHelper();
if (this._reactComponentDisposable) {
this._reactComponentDisposable.dispose();
}
this._editor.removeOverlayWidget(this);
this._showScheduler.dispose();
super.dispose();
}
}
// Register the contribution
registerEditorContribution(SelectionHelperContribution.ID, SelectionHelperContribution, EditorContributionInstantiation.Eager);

View file

@ -46,13 +46,23 @@ const notifyYesUpdate = (notifService: INotificationService, res: { message?: st
const { window } = dom.getActiveWindow()
window.open('https://voideditor.com/')
}
}],
secondary: [{
id: 'void.updater.close',
enabled: true,
label: `Keep Void outdated`,
tooltip: '',
class: undefined,
run: () => {
notifController.close()
}
}]
},
})
const d = notifController.onDidClose(() => {
notifyYesUpdate(notifService, res)
d.dispose()
})
// const d = notifController.onDidClose(() => {
// notifyYesUpdate(notifService, res)
// d.dispose()
// })
}
const notifyNoUpdate = (notifService: INotificationService) => {
notifService.notify({
@ -86,7 +96,7 @@ registerAction2(class extends Action2 {
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Void Update Manual: Checking...', {})
const res = await voidUpdateService.check()
const res = await voidUpdateService.check(true)
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(notifService, res); metricsService.capture('Void Update Manual: Yes', { res }) }
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) }
@ -104,7 +114,7 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo
super()
const autoCheck = async () => {
this.metricsService.capture('Void Update Startup: Checking...', {})
const res = await this.voidUpdateService.check()
const res = await this.voidUpdateService.check(false)
if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res); this.metricsService.capture('Void Update Startup: Yes', { res }) }
else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date

View file

@ -5,40 +5,31 @@
import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js';
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
role: 'tool';
paramsStr: string; // internal use
id: string; // apis require this tool use id
content: string; // give this result to LLM (string of value)
} & (
// in order of events:
| { type: 'invalid_params', result: null, params: null, name: string }
| { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, }
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
| { type: 'success', result: ToolResultType[T], name: T, params: ToolCallParams[T], }
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T], }
) // user rejected
export type DecorativeCanceledTool = {
role: 'decorative_canceled_tool';
name: string;
role: 'interrupted_streaming_tool';
name: ToolName;
}
// export type ToolRequestApproval<T extends ToolName> = {
// role: 'tool_request';
// name: T; // internal use
// params: ToolCallParams[T]; // internal use
// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
// id: string; // proposed tool's id
// }
// checkpoints
export type CheckpointEntry = {
@ -65,8 +56,9 @@ export type ChatMessage =
}
} | {
role: 'assistant';
content: string; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
toolCall: RawToolCallObj | undefined;
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}

View file

@ -3,9 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { OnText } from '../sendLLMMessageTypes.js'
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
class SurroundingsRemover {
readonly originalS: string
i: number
@ -174,7 +172,7 @@ export type ExtractedSearchReplaceBlock = {
// JS substring swaps indices, so "ab".substr(1,0) will NOT be '', it will be 'a'!
const voidSubstr = (str: string, start: number, end: number) => end < start ? '' : str.substring(start, end)
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
export const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
// for each prefix
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
const prefix = anyPrefix.slice(0, i)
@ -250,122 +248,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
let latestAddIdx = 0 // exclusive index in fullText_
let foundTag1 = false
let foundTag2 = false
let fullTextSoFar = ''
let fullReasoningSoFar = ''
let onText_ = onText
onText = (params) => {
onText_(params)
}
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
if (endsWithTag1) {
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
// wait until we get the full tag or know more
return
}
// if found the first tag
const tag1Index = fullText_.indexOf(thinkTags[0])
if (tag1Index !== -1) {
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
foundTag1 = true
// Add text before the tag to fullTextSoFar
fullTextSoFar += fullText_.substring(0, tag1Index)
// Update latestAddIdx to after the first tag
latestAddIdx = tag1Index + thinkTags[0].length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
// add the text to fullText
fullTextSoFar = fullText_
latestAddIdx = fullText_.length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag1>
// until found the second think tag, keep adding to fullReasoning
if (!foundTag2) {
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
if (endsWithTag2) {
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
// wait until we get the full tag or know more
return
}
// if found the second tag
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
if (tag2Index !== -1) {
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
foundTag2 = true
// Add everything between first and second tag to reasoning
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
// Update latestAddIdx to after the second tag
latestAddIdx = tag2Index + thinkTags[1].length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// add the text to fullReasoning (content after first tag but before second tag)
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
// If we have more text than we've processed, add it to reasoning
if (fullText_.length > latestAddIdx) {
fullReasoningSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag2> - content after the second tag is normal text
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
// Add any new text after the closing tag to fullTextSoFar
if (fullText_.length > latestAddIdx) {
fullTextSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
return newOnText
}
export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => {
const tag1Idx = fullText_.indexOf(thinkTags[0])
const tag2Idx = fullText_.indexOf(thinkTags[1])
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
return { fullText, fullReasoning }
}

View file

@ -6,6 +6,46 @@
import { FeatureName, ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js';
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
deepseek: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
vLLM: {
endpoint: 'http://localhost:8000',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: '',
},
xAI: {
apiKey: ''
},
} as const
export const defaultModelsOfProvider = {
openAI: [ // https://platform.openai.com/docs/models/gp
'o3-mini',
@ -68,20 +108,22 @@ export const defaultModelsOfProvider = {
type ModelOptions = {
export type VoidStaticModelInfo = { // not stateful
contextWindow: number; // input tokens
maxOutputTokens: number | null; // output tokens, defaults to 4092
cost: { // <-- UNUSED
cost: { // <-- UNUSED
input: number;
output: number;
cache_read?: number;
cache_write?: number;
}
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated';
supportsTools: false | 'anthropic-style' | 'openai-style';
downloadable: false | {
sizeGb: number | 'not-known'
}
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special parameter
supportsTools?: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style';
supportsFIM: boolean;
reasoningCapabilities: false | {
@ -109,20 +151,20 @@ type ProviderReasoningIOSettings = {
| { nameOfFieldInDelta?: undefined, needsManualParse?: true, };
}
type ProviderSettings = {
type VoidStaticProviderInfo = { // doesn't change (not stateful)
providerReasoningIOSettings?: ProviderReasoningIOSettings; // input/output settings around thinking (allowed to be empty) - only applied if the model supports reasoning output
modelOptions: { [key: string]: ModelOptions };
modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null;
modelOptions: { [key: string]: VoidStaticModelInfo };
modelOptionsFallback: (modelName: string, fallbackKnownValues?: Partial<VoidStaticModelInfo>) => (VoidStaticModelInfo & { modelName: string }) | null;
}
const modelOptionsDefaults: ModelOptions = {
const modelOptionsDefaults: VoidStaticModelInfo = {
contextWindow: 32_000,
maxOutputTokens: 4_096,
cost: { input: 0, output: 0 },
downloadable: false,
supportsSystemMessage: false,
supportsTools: false,
supportsFIM: false,
reasoningCapabilities: false,
}
@ -137,42 +179,36 @@ const openSourceModelOptions_assumingOAICompat = {
'deepseekR1': {
supportsFIM: false,
supportsSystemMessage: false,
supportsTools: false,
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'deepseekCoderV3': {
supportsFIM: false,
supportsSystemMessage: false, // unstable
supportsTools: false,
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'deepseekCoderV2': {
supportsFIM: false,
supportsSystemMessage: false, // unstable
supportsTools: false,
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'codestral': {
supportsFIM: true,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false, // built on qwen 2.5 32B instruct
contextWindow: 128_000, maxOutputTokens: 4_096
},
'phi4': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: false,
reasoningCapabilities: false,
contextWindow: 16_000, maxOutputTokens: 4_096,
},
@ -180,7 +216,6 @@ const openSourceModelOptions_assumingOAICompat = {
'gemma': { // https://news.ycombinator.com/item?id=43451406
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: false,
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
@ -188,14 +223,12 @@ const openSourceModelOptions_assumingOAICompat = {
'llama4-scout': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 10_000_000, maxOutputTokens: 4_096,
},
'llama4-maverick': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 10_000_000, maxOutputTokens: 4_096,
},
@ -204,28 +237,24 @@ const openSourceModelOptions_assumingOAICompat = {
'llama3': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'llama3.1': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'llama3.2': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'llama3.3': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
@ -233,14 +262,12 @@ const openSourceModelOptions_assumingOAICompat = {
'qwen2.5coder': {
supportsFIM: true,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
contextWindow: 32_000, maxOutputTokens: 4_096,
},
'qwq': {
supportsFIM: false, // no FIM, yes reasoning
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
contextWindow: 128_000, maxOutputTokens: 8_192,
},
@ -248,7 +275,6 @@ const openSourceModelOptions_assumingOAICompat = {
'starcoder2': {
supportsFIM: true,
supportsSystemMessage: false,
supportsTools: false,
reasoningCapabilities: false,
contextWindow: 128_000, maxOutputTokens: 8_192,
@ -256,26 +282,28 @@ const openSourceModelOptions_assumingOAICompat = {
'codegemma:2b': {
supportsFIM: true,
supportsSystemMessage: false,
supportsTools: false,
reasoningCapabilities: false,
contextWindow: 128_000, maxOutputTokens: 8_192,
},
} as const satisfies { [s: string]: Omit<ModelOptions, 'cost'> }
} as const satisfies { [s: string]: Partial<VoidStaticModelInfo> }
const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => {
const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => {
const lower = modelName.toLowerCase()
const toFallback = (opts: Omit<ModelOptions, 'cost'>): ModelOptions & { modelName: string } => {
const toFallback = (opts: Omit<VoidStaticModelInfo, 'cost' | 'downloadable'>): VoidStaticModelInfo & { modelName: string } => {
return {
modelName,
...opts,
supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false,
cost: { input: 0, output: 0 },
downloadable: false,
...fallbackKnownValues
}
}
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
@ -332,9 +360,9 @@ const anthropicModelOptions = {
contextWindow: 200_000,
maxOutputTokens: 8_192,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
@ -347,40 +375,40 @@ const anthropicModelOptions = {
contextWindow: 200_000,
maxOutputTokens: 8_192,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
},
'claude-3-5-haiku-20241022': {
contextWindow: 200_000,
maxOutputTokens: 8_192,
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
},
'claude-3-opus-20240229': {
contextWindow: 200_000,
maxOutputTokens: 4_096,
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
},
'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in
contextWindow: 200_000, cost: { input: 3.00, output: 15.00 },
downloadable: false,
maxOutputTokens: 4_096,
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
}
} as const satisfies { [s: string]: ModelOptions }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const anthropicSettings: ProviderSettings = {
const anthropicSettings: VoidStaticProviderInfo = {
providerReasoningIOSettings: {
input: {
includeInPayload: (reasoningInfo) => {
@ -412,8 +440,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
contextWindow: 128_000,
maxOutputTokens: 100_000,
cost: { input: 15.00, cache_read: 7.50, output: 60.00, },
downloadable: false,
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: 'developer-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, // it doesn't actually output reasoning, but our logic is fine with it
},
@ -421,8 +449,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
contextWindow: 200_000,
maxOutputTokens: 100_000,
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
downloadable: false,
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: 'developer-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false },
},
@ -430,8 +458,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
contextWindow: 128_000,
maxOutputTokens: 16_384,
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
downloadable: false,
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@ -439,8 +467,8 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
contextWindow: 128_000,
maxOutputTokens: 65_536,
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
downloadable: false,
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: false, // does not support any system
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false },
},
@ -448,15 +476,15 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
contextWindow: 128_000,
maxOutputTokens: 16_384,
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
downloadable: false,
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role', // ??
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: ModelOptions }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const openAISettings: ProviderSettings = {
const openAISettings: VoidStaticProviderInfo = {
modelOptions: openAIModelOptions,
modelOptionsFallback: (modelName) => {
const lower = modelName.toLowerCase()
@ -475,14 +503,14 @@ const xAIModelOptions = {
contextWindow: 131_072,
maxOutputTokens: null, // 131_072,
cost: { input: 2.00, output: 10.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: ModelOptions }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const xAISettings: ProviderSettings = {
const xAISettings: VoidStaticProviderInfo = {
modelOptions: xAIModelOptions,
modelOptionsFallback: (modelName) => {
const lower = modelName.toLowerCase()
@ -500,59 +528,59 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
contextWindow: 1_048_576,
maxOutputTokens: 8_192,
cost: { input: 0, output: 0 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
reasoningCapabilities: false,
},
'gemini-2.0-flash': {
contextWindow: 1_048_576,
maxOutputTokens: 8_192, // 8_192,
cost: { input: 0.10, output: 0.40 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
reasoningCapabilities: false,
},
'gemini-2.0-flash-lite-preview-02-05': {
contextWindow: 1_048_576,
maxOutputTokens: 8_192, // 8_192,
cost: { input: 0.075, output: 0.30 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'gemini-1.5-flash': {
contextWindow: 1_048_576,
maxOutputTokens: 8_192, // 8_192,
cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'gemini-1.5-pro': {
contextWindow: 2_097_152,
maxOutputTokens: 8_192,
cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'gemini-1.5-flash-8b': {
contextWindow: 1_048_576,
maxOutputTokens: 8_192,
cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: ModelOptions }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const geminiSettings: ProviderSettings = {
const geminiSettings: VoidStaticProviderInfo = {
modelOptions: geminiModelOptions,
modelOptionsFallback: (modelName) => { return null }
}
@ -566,17 +594,19 @@ const deepseekModelOptions = {
contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing
maxOutputTokens: 8_000, // 8_000,
cost: { cache_read: .07, input: .27, output: 1.10, },
downloadable: false,
},
'deepseek-reasoner': {
...openSourceModelOptions_assumingOAICompat.deepseekCoderV2,
contextWindow: 64_000,
maxOutputTokens: 8_000, // 8_000,
cost: { cache_read: .14, input: .55, output: 2.19, },
downloadable: false,
},
} as const satisfies { [s: string]: ModelOptions }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const deepseekSettings: ProviderSettings = {
const deepseekSettings: VoidStaticProviderInfo = {
modelOptions: deepseekModelOptions,
providerReasoningIOSettings: {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
@ -591,40 +621,40 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq
contextWindow: 128_000,
maxOutputTokens: 32_768, // 32_768,
cost: { input: 0.59, output: 0.79 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'llama-3.1-8b-instant': {
contextWindow: 128_000,
maxOutputTokens: 8_192,
cost: { input: 0.05, output: 0.08 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwen-2.5-coder-32b': {
contextWindow: 128_000,
maxOutputTokens: null, // not specified?
cost: { input: 0.79, output: 0.79 },
downloadable: false,
supportsFIM: false, // unfortunately looks like no FIM support on groq
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwen-qwq-32b': { // https://huggingface.co/Qwen/QwQ-32B
contextWindow: 128_000,
maxOutputTokens: null, // not specified?
cost: { input: 0.29, output: 0.39 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] }, // we're using reasoning_format:parsed so really don't need to know openSourceThinkTags
},
} as const satisfies { [s: string]: ModelOptions }
const groqSettings: ProviderSettings = {
} as const satisfies { [s: string]: VoidStaticModelInfo }
const groqSettings: VoidStaticProviderInfo = {
providerReasoningIOSettings: {
input: {
includeInPayload: (reasoningInfo) => {
@ -640,23 +670,71 @@ const groqSettings: ProviderSettings = {
modelOptionsFallback: (modelName) => { return null }
}
const ollamaModelOptions = {
'qwen2.5-coder:3b': {
contextWindow: 32_000,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: { sizeGb: 1.9 },
supportsFIM: true,
supportsSystemMessage: 'system-role',
supportsTools: false,
reasoningCapabilities: false,
},
'qwen2.5-coder': {
contextWindow: 128_000,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: { sizeGb: 4.7 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: false,
reasoningCapabilities: false,
},
'qwq': {
contextWindow: 128_000,
maxOutputTokens: 32_000,
cost: { input: 0, output: 0 },
downloadable: { sizeGb: 20 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'TODO-yes-but-we-handle-it-manually',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
'deepseek-r1': {
contextWindow: 128_000,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: { sizeGb: 4.7 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'TODO-yes-but-we-handle-it-manually',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
} as const satisfies Record<string, VoidStaticModelInfo>
export const ollamaRecommendedModels = ['qwen2.5-coder:3b', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
const vLLMSettings: ProviderSettings = {
const vLLMSettings: VoidStaticProviderInfo = {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
modelOptions: {},
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: {}, // TODO
}
const ollamaSettings: ProviderSettings = {
const ollamaSettings: VoidStaticProviderInfo = {
// reasoning: we need to filter out reasoning <think> tags manually
providerReasoningIOSettings: { output: { needsManualParse: true }, },
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
modelOptions: {},
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: ollamaModelOptions,
}
const openaiCompatible: ProviderSettings = {
const openaiCompatible: VoidStaticProviderInfo = {
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
modelOptions: {},
@ -669,8 +747,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 128_000,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: false,
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@ -678,8 +756,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 1_048_576,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: false,
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@ -687,8 +765,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 1_048_576,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: false,
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@ -696,8 +774,8 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 1_048_576,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: false,
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@ -706,14 +784,15 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 128_000,
maxOutputTokens: null,
cost: { input: 0.8, output: 2.4 },
downloadable: false,
},
'anthropic/claude-3.7-sonnet:thinking': {
contextWindow: 200_000,
maxOutputTokens: null,
cost: { input: 3.00, output: 15.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: { // same as anthropic, see above
supportsReasoning: true,
canTurnOffReasoning: false,
@ -726,18 +805,18 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 200_000,
maxOutputTokens: null,
cost: { input: 3.00, output: 15.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false, // stupidly, openrouter separates thinking from non-thinking
},
'anthropic/claude-3.5-sonnet': {
contextWindow: 200_000,
maxOutputTokens: null,
cost: { input: 3.00, output: 15.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'mistralai/codestral-2501': {
@ -745,6 +824,7 @@ const openRouterModelOptions_assumingOpenAICompat = {
contextWindow: 256_000,
maxOutputTokens: null,
cost: { input: 0.3, output: 0.9 },
downloadable: false,
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
@ -752,19 +832,19 @@ const openRouterModelOptions_assumingOpenAICompat = {
...openSourceModelOptions_assumingOAICompat['qwen2.5coder'],
contextWindow: 33_000,
maxOutputTokens: null,
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
cost: { input: 0.07, output: 0.16 },
downloadable: false,
},
'qwen/qwq-32b': {
...openSourceModelOptions_assumingOAICompat['qwq'],
contextWindow: 33_000,
maxOutputTokens: null,
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
cost: { input: 0.07, output: 0.16 },
downloadable: false,
}
} as const satisfies { [s: string]: ModelOptions }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const openRouterSettings: ProviderSettings = {
const openRouterSettings: VoidStaticProviderInfo = {
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
providerReasoningIOSettings: {
input: {
@ -791,7 +871,7 @@ const openRouterSettings: ProviderSettings = {
// ---------------- model settings of everything above ----------------
const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = {
const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = {
openAI: openAISettings,
anthropic: anthropicSettings,
xAI: xAISettings,
@ -817,8 +897,10 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting
// ---------------- exports ----------------
// returns the capabilities and the adjusted modelName if it was a fallback
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
export const getModelCapabilities = (providerName: ProviderName, modelName: string): VoidStaticModelInfo & { modelName: string; isUnrecognizedModel: boolean } => {
const lowercaseModelName = modelName.toLowerCase()
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
// search model options object directly first

View file

@ -3,16 +3,24 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { os } from '../helpers/systemInfo.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
import { InternalToolInfo } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
export const MAX_DIRSTR_CHARS_TOTAL_BEGINNING = 20_000
export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
// ======================================================== tools ========================================================
const changesExampleContent = `\
// ... existing code ...
// {{change 1}}
@ -22,35 +30,34 @@ const changesExampleContent = `\
// {{change 3}}
// ... existing code ...`
const editToolDescription = `\
const editToolDescriptionExample = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const fileNameEdit = `${tripleTick[0]}typescript
const fileNameEditExample = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
${tripleTick[1]}`
export type InternalToolInfo = {
name: string,
description: string,
params: {
[paramName: string]: { description: string }
},
}
// ======================================================== tools ========================================================
const paginationHelper = {
desc: `Very large results may be paginated (a note will always be included if pagination took place). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
} as const
const uriParam = (object: string) => ({
uri: { type: 'string', description: `The FULL path to the ${object}.` }
uri: { description: `The FULL path to the ${object}.` }
})
const searchParams = {
searchInFolder: { type: 'string', description: 'Only search files in this given folder. Leave as empty to search all available files.' },
isRegex: { type: 'string', description: 'Whether to treat the query as a regular expression. Default is "false".' },
const paginationParam = {
page_number: { description: 'Optional. The page number of the result. Default is 1.' }
} as const
@ -59,49 +66,56 @@ export const voidTools = {
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
description: `Returns file contents of a given URI.`,
params: {
...uriParam('file'),
startLine: { type: 'string', description: 'Line to start reading from. Default is "null", treated as 1.' },
endLine: { type: 'string', description: 'Line to stop reading from (inclusive). Default is "null", treated as Infinity.' },
...paginationHelper.param,
start_line: { description: 'Optional. Default is 1. Start reading on this line.' },
end_line: { description: 'Optional. Default is Infinity. Stop reading after this line.' },
...paginationParam,
},
},
ls_dir: {
name: 'ls_dir',
description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`,
description: `Lists all files and folders in the given URI.`,
params: {
...uriParam('folder'),
...paginationHelper.param,
...paginationParam,
},
},
get_dir_structure: {
name: 'get_dir_structure',
description: `This is a very effective way to learn about the user's codebase. You might want to use this instead of ls_dir. Returns a tree diagram of all the files and folders in the given folder URI. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
params: {
...uriParam('folder')
}
},
// pathname_search: {
// name: 'pathname_search',
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
search_pathnames_only: {
name: 'search_pathnames_only',
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { type: 'string', description: undefined },
...searchParams,
...paginationHelper.param,
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
...paginationParam,
},
},
search_files: {
name: 'search_files',
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. You can follow this with read_file to view result contents.`,
params: {
query: { type: 'string', description: undefined },
...searchParams,
...paginationHelper.param,
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
...paginationParam,
},
},
@ -109,7 +123,7 @@ export const voidTools = {
create_file_or_folder: {
name: 'create_file_or_folder',
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash.`,
params: {
...uriParam('file or folder'),
},
@ -117,25 +131,25 @@ export const voidTools = {
delete_file_or_folder: {
name: 'delete_file_or_folder',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
description: `Delete a file or folder at the given path.`,
params: {
...uriParam('file or folder'),
params: { type: 'string', description: 'Return -r here to delete recursively (if applicable). Default is the empty string.' }
params: { description: 'Optional. Return -r here to delete recursively.' }
},
},
edit_file: { // APPLY TOOL
name: 'edit_file',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
description: `Edits the contents of a file given the file's URI and a description.`,
params: {
...uriParam('file'),
changeDescription: {
type: 'string', description: `\
- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
- You must output your description in triple backticks.
Here's an example of a good description:\n${editToolDescription}.`
change_description: {
description: `\
A brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
Your description will be handed to a smaller model to make the change, so it must be clear and concise. \
Your description MUST be wrapped in triple backticks. \
Here's an example of a good description:\n${editToolDescriptionExample}`
}
},
},
@ -144,161 +158,199 @@ Here's an example of a good description:\n${editToolDescription}.`
name: 'run_terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' },
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
command: { description: 'The terminal command to execute.' },
wait_for_completion: { description: `Optional. Default is true. Make this value false when you want a command to run without waiting for it to complete.` },
terminal_id: { description: 'Optional. The ID of the terminal instance that should execute the command (if not provided, defaults to the preferred terminal ID). The primary purpose of this is to let you open a new terminal for testing or background processes (e.g. running a dev server for the user in a separate terminal). Must be an integer >= 1.' },
},
},
// go_to_definition
// go_to_usages
} satisfies { [name: string]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export const availableTools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
: undefined
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
return tools
}
const availableXMLToolsStr = (tools: InternalToolInfo[]) => {
return `${tools.map((t, i) => {
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
return `\
${i + 1}. ${t.name}
Description: ${t.description}
Format:
<${t.name}>${!params ? '' : `\n${params}`}
</${t.name}>`
}).join('\n\n')}`
}
export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
const t = toolCall
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\
<${toolCall.name}>${!params ? '' : `\n${params}`}
</${toolCall.name}>`
.replace('\t', ' ')
}
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode)
if (!tools || tools.length === 0) return ''
const toolXMLDefinitions = (`\
Available tools:
${availableXMLToolsStr(tools)}`)
const toolCallXMLGuidelines = (`\
Tool calling details:
- Once you write a tool call, you must STOP and WAIT for the result.
- All parameters are REQUIRED unless noted otherwise.
- To call a tool, write its name and parameters in one of the XML formats specified above.
- You are only allowed to output ONE tool call, and it must be at the END of your response.
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
return `\
${toolXMLDefinitions}
${toolCallXMLGuidelines}`
}
// ======================================================== chat (normal, gather, agent) ========================================================
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => {
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, and you may also be given a list of files that the user has specifically selected for context, \`SELECTIONS\`.
Please assist the user with their query.`)
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the user's IDE called Void. Your job is \
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query. The user's query is never invalid.
${/* system info */''}
The user's system information is as follows:
const sysInfo = (`Here is the user's system information:
<system_info>
- ${os}
- Open workspace(s): ${workspaceFolders.join(', ') || 'NO WORKSPACE OPEN'}
- Open tab(s): ${openedURIs.join(', ') || 'NO OPENED EDITORS'}
- Active tab: ${activeURI}
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `
- Open workspaces:
${workspaceFolders.join('\n') || 'NO WORKSPACE OPEN'}
- Active file:
${activeURI}
- Open files:
${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? `
- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''}
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
You will be given tools you can call.
${mode === 'agent' ? `\
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.
- You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.
- ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`
: mode === 'gather' ? `\
- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.
- You should extensively read files, types, etc and gather relevant context.`
: ''}
- If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc.
- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.
- Some tools only work if the user has a workspace open.${mode === 'agent' ? `
- NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''}
\
`: `\
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
\
`}
${/* code blocks */ mode === 'agent' ? `\
Behavior:
- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them.
- Prioritize taking as many steps as you need to complete your request over stopping early.\
`: `\
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
- The first line of the code block must be the FULL PATH of the file you want to change.
- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
Here's an example of a good code block:\n${fileNameEdit}.
If you write a code block that's related to a specific file, please use the same format as above:
- The first line of the code block must be the FULL PATH of the related file if known.
- The remaining contents of the file should proceed as usual.
\
`}
${/* misc */''}
Misc:
- Do not make things up.
- Do not be lazy.
- NEVER re-write the entire file.
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.
- Today's date is ${new Date().toDateString()}
The user's codebase is structured as follows:\n${directoryStr}
\
`
// agent mode doesn't know about 1st line paths yet
// - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path.
</system_info>`)
// type FileSelnLocal = { fileURI: URI, language: string, content: string }
// const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => {
// return `\
// ${fileURI.fsPath}
// ${tripleTick[0]}${language}
// ${content}
// ${tripleTick[1]}
// `
// }
// const stringifyCodeSelection = ({ uri, language, range }: StagingSelectionItem & { type: 'CodeSelection' }) => {
// return `\
const fsInfo = (`Here is an overview of the user's file system:
<files_overview>
${directoryStr}
</files_overview>`)
// ${tripleTick[0]}${language}
// ${selectionStr}
// ${tripleTick[1]}
// `
const toolDefinitions = systemToolsXMLPrompt(mode)
const details: string[] = []
if (mode === 'agent' || mode === 'gather') {
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
details.push('Only use ONE tool call at a time, and always wait for the result before proceeding.') // XML
details.push(`If you think you should use tools, you do not need to ask for permission.`)
details.push(`NEVER say something like "I'm going to use \`tool_name\`". Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc.`)
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
}
if (mode === 'agent') {
details.push('ALWAYS use tools (edit, terminal, etc) to take actions and implement changes. For example, if you would like to edit a file, you MUST use a tool.')
details.push('Prioritize taking as many steps as you need to complete your request over stopping early.')
details.push(`You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.`)
details.push(`ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`)
details.push(`NEVER modify a file outside the user's workspace(s) without permission from the user.`)
}
if (mode === 'gather') {
details.push(`Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.`)
details.push(`You should extensively read files, types, content, etc and gather relevant context.`)
}
if (mode === 'gather' || mode === 'normal') {
details.push(`If you write any code blocks, please use this format:
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents of the file should proceed as usual.`)
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents should be \
a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
Here's an example of a good edit suggestion:
${fileNameEditExample}.`)
}
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
const importantDetails = (`Important notes:
${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// return answer
const ansStrs: string[] = []
ansStrs.push(header)
ansStrs.push(sysInfo)
ansStrs.push(fsInfo)
if (toolDefinitions) ansStrs.push(toolDefinitions)
ansStrs.push(importantDetails)
ansStrs.push('Now, please assist the user with their query.')
const fullSystemMsgStr = ansStrs
.join('\n\n\n')
.trim()
.replace('\t', ' ')
return fullSystemMsgStr
}
// // log all prompts
// for (const chatMode of ['agent', 'gather', 'normal'] satisfies ChatMode[]) {
// console.log(`========================================= SYSTEM MESSAGE FOR ${chatMode} ===================================\n`,
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', runningTerminalIds: [], directoryStr: 'lol', }))
// }
// const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
// const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
// if (fileSelections.length === 0) return null
// const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
// const { model } = await voidModelService.getModelSafe(sel.fileURI)
// const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
// return { ...sel, content }
// }))
// return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
// }
// export const chat_selectionsString = async (
// prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
// voidModelService: IVoidModelService,
// ) => {
// // ADD IN FILES AT TOP
// const allSelections = [...currSelns || [], ...prevSelns || []]
// if (allSelections.length === 0) return null
// for (const selection of allSelections) {
// if (selection.type === 'Selection') {
// codeSelections.push(selection)
// }
// else if (selection.type === 'File') {
// const fileSelection = selection
// const path = fileSelection.fileURI.fsPath
// if (!filesURIs.has(path)) {
// filesURIs.add(path)
// fileSelections.push(fileSelection)
// }
// }
// }
// const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
// const selnsStr = stringifyCodeSelections(codeSelections)
// const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n')
// return fileContents || null
// }
// export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
// if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
// else return userMessage
// }
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
@ -453,8 +505,6 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_PREFIX_SUFFIX_CHARS = 20_000
/*
a
@ -560,6 +610,40 @@ ${tripleTick[1]}).`
// const toAnthropicTool = (toolInfo: InternalToolInfo) => {
// const { name, description, params } = toolInfo
// return {
// name: name,
// description: description,
// input_schema: {
// type: 'object',
// properties: params,
// // required: Object.keys(params),
// },
// } satisfies Anthropic.Messages.Tool
// }
// const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
// const { name, description, params } = toolInfo
// return {
// type: 'function',
// function: {
// name: name,
// // strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
// description: description,
// parameters: {
// type: 'object',
// properties: params,
// // required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
// // additionalProperties: false,
// },
// }
// } satisfies OpenAI.Chat.Completions.ChatCompletionTool
// }
/*
// ======================================================== ai search/replace ========================================================

View file

@ -3,8 +3,8 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ToolName, InternalToolInfo } from './toolsServiceTypes.js'
import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@ -37,25 +37,24 @@ export type LLMChatMessage = {
role: 'assistant',
content: string; // text content
anthropicReasoning: AnthropicReasoning[] | null;
} | {
role: 'tool';
content: string; // result
name: string;
params: string;
id: string;
}
export type ToolCallType = {
export type RawToolParamsObj = {
[paramName in ToolParamName]?: string;
}
export type RawToolCallObj = {
name: ToolName;
paramsStr: string;
id: string;
}
rawParams: RawToolParamsObj;
doneParams: ToolParamName[];
isDone: boolean;
};
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string; fullToolName: string; fullToolParams: string; }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnError = (p: { message: string; fullError: Error | null }) => void
export type OnAbort = () => void
export type AbortRef = { current: (() => void) | null }
@ -70,11 +69,11 @@ export type LLMFIMMessage = {
type SendLLMType = {
messagesType: 'chatMessages';
messages: LLMChatMessage[];
tools?: InternalToolInfo[];
chatMode: ChatMode | null;
} | {
messagesType: 'FIMMessage';
messages: LLMFIMMessage;
tools?: undefined;
chatMode?: undefined;
}
// service types

View file

@ -1,11 +1,12 @@
import { URI } from '../../../../base/common/uri.js'
import { voidTools } from './prompt/prompts.js';
import { ToolName } from './prompt/prompts.js';
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
export type LintErrorItem = { code: string, message: string, startLineNumber: number, endLineNumber: number }
// Partial of IFileStat
export type ShallowDirectoryItem = {
uri: URI;
@ -14,26 +15,6 @@ export type ShallowDirectoryItem = {
isSymbolicLink: boolean;
}
// we do this using Anthropic's style and convert to OpenAI style later
export type InternalToolInfo = {
name: string,
description: string,
params: {
[paramName: string]: { type: string, description: string | undefined } // name -> type
},
}
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'run_terminal_command'] as const satisfies readonly ToolName[]
@ -45,7 +26,7 @@ export type ToolCallParams = {
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
'ls_dir': { rootURI: URI, pageNumber: number },
'get_dir_structure': { rootURI: URI },
'search_pathnames_only': { queryStr: string, include: string | null, pageNumber: number },
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
// ---
'edit_file': { uri: URI, changeDescription: string },
@ -63,7 +44,7 @@ export type ToolResultType = {
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
'search_files': { uris: URI[], hasNextPage: boolean },
// ---
'edit_file': Promise<{ lintErrorsStr: string | null }>,
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
'create_file_or_folder': {},
'delete_file_or_folder': {},
'run_terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },

View file

@ -4,6 +4,7 @@ import { ITextModel } from '../../../../editor/common/model.js';
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
type VoidModelType = {
model: ITextModel | null;
@ -16,6 +17,8 @@ export interface IVoidModelService {
getModel(uri: URI): VoidModelType;
getModelFromFsPath(fsPath: string): VoidModelType;
getModelSafe(uri: URI): Promise<VoidModelType>;
saveModel(uri: URI): Promise<void>;
}
export const IVoidModelService = createDecorator<IVoidModelService>('voidVoidModelService');
@ -27,10 +30,17 @@ class VoidModelService extends Disposable implements IVoidModelService {
constructor(
@ITextModelService private readonly _textModelService: ITextModelService,
@ITextFileService private readonly _textFileService: ITextFileService,
) {
super();
}
saveModel = async (uri: URI) => {
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
})
}
initializeModel = async (uri: URI) => {
if (uri.fsPath in this._modelRefOfURI) return;
const editorModelRef = await this._textModelService.createModelReference(uri);

View file

@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { getModelCapabilities } from './modelCapabilities.js';
import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js';
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
// name is the name in the dropdown
@ -71,10 +71,10 @@ export interface IVoidSettingsService {
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => {
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => {
const { existingModels } = options
const existingModelsMap: Record<string, VoidModelInfo> = {}
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
for (const existingModel of existingModels) {
existingModelsMap[existingModel.modelName] = existingModel
}
@ -95,7 +95,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
'Chat': { filter: o => true, emptyMessage: null, },
'Ctrl+K': { filter: o => true, emptyMessage: null, },
'Apply': { filter: o => true, emptyMessage: null, },
}
@ -363,7 +363,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const modelIdx = models.findIndex(m => m.modelName === modelName)
if (modelIdx === -1) return
const newIsHidden = !models[modelIdx].isHidden
const newModels: VoidModelInfo[] = [
const newModels: VoidStatefulModelInfo[] = [
...models.slice(0, modelIdx),
{ ...models[modelIdx], isHidden: newIsHidden },
...models.slice(modelIdx + 1, Infinity)

View file

@ -4,49 +4,13 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { defaultModelsOfProvider } from './modelCapabilities.js';
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
import { VoidSettingsState } from './voidSettingsService.js'
type UnionOfKeys<T> = T extends T ? keyof T : never;
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
deepseek: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
vLLM: {
endpoint: 'http://localhost:8000',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: '',
},
xAI: {
apiKey: ''
},
} as const
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
@ -64,7 +28,7 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
export type VoidModelInfo = { // <-- STATEFUL
export type VoidStatefulModelInfo = { // <-- STATEFUL
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
isHidden: boolean, // whether or not the user is hiding it (switched off)
@ -75,7 +39,7 @@ export type VoidModelInfo = { // <-- STATEFUL
type CommonProviderSettings = {
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
models: VoidStatefulModelInfo[],
}
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
@ -227,7 +191,7 @@ const defaultCustomSettings: Record<CustomSettingName, undefined> = {
}
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => {
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidStatefulModelInfo[] } => {
return {
models: defaultModelNames.map((modelName, i) => ({
modelName,
@ -334,6 +298,8 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
// models that come with download buttons
export const hasDownloadButtonsOnModelsProviderNames = ['ollama'] as const satisfies ProviderName[]
@ -389,6 +355,7 @@ export type GlobalSettings = {
enableFastApply: boolean;
chatMode: ChatMode;
autoApprove: boolean;
showInlineSuggestions: boolean;
}
export const defaultGlobalSettings: GlobalSettings = {
@ -399,6 +366,7 @@ export const defaultGlobalSettings: GlobalSettings = {
enableFastApply: true,
chatMode: 'agent',
autoApprove: false,
showInlineSuggestions: true,
}
export type GlobalSettingName = keyof GlobalSettings

View file

@ -12,7 +12,7 @@ import { IMainProcessService } from '../../../../platform/ipc/common/mainProcess
export interface IVoidUpdateService {
readonly _serviceBrand: undefined;
check: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
check: (explicit: boolean) => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
}
@ -34,8 +34,8 @@ export class VoidUpdateService implements IVoidUpdateService {
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
check: IVoidUpdateService['check'] = async () => {
const res = await this.voidUpdateService.check()
check: IVoidUpdateService['check'] = async (explicit) => {
const res = await this.voidUpdateService.check(explicit)
return res
}
}

View file

@ -0,0 +1,340 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js'
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
import { OnFinalMessage, OnText, RawToolCallObj } from '../../common/sendLLMMessageTypes.js'
import { ChatMode } from '../../common/voidSettingsTypes.js'
import { createSaxParser } from './sax.js'
// =============== reasoning ===============
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
export const extractReasoningWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, thinkTags: [string, string]
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
let latestAddIdx = 0 // exclusive index in fullText_
let foundTag1 = false
let foundTag2 = false
let fullTextSoFar = ''
let fullReasoningSoFar = ''
let onText_ = onText
onText = (params) => {
onText_(params)
}
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
if (endsWithTag1) {
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
// wait until we get the full tag or know more
return
}
// if found the first tag
const tag1Index = fullText_.indexOf(thinkTags[0])
if (tag1Index !== -1) {
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
foundTag1 = true
// Add text before the tag to fullTextSoFar
fullTextSoFar += fullText_.substring(0, tag1Index)
// Update latestAddIdx to after the first tag
latestAddIdx = tag1Index + thinkTags[0].length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
// add the text to fullText
fullTextSoFar = fullText_
latestAddIdx = fullText_.length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag1>
// until found the second think tag, keep adding to fullReasoning
if (!foundTag2) {
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
if (endsWithTag2) {
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
// wait until we get the full tag or know more
return
}
// if found the second tag
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
if (tag2Index !== -1) {
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
foundTag2 = true
// Add everything between first and second tag to reasoning
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
// Update latestAddIdx to after the second tag
latestAddIdx = tag2Index + thinkTags[1].length
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// add the text to fullReasoning (content after first tag but before second tag)
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
// If we have more text than we've processed, add it to reasoning
if (fullText_.length > latestAddIdx) {
fullReasoningSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag2> - content after the second tag is normal text
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
// Add any new text after the closing tag to fullTextSoFar
if (fullText_.length > latestAddIdx) {
fullTextSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
const getOnFinalMessageParams = () => {
const fullText_ = fullTextSoFar
const tag1Idx = fullText_.indexOf(thinkTags[0])
const tag2Idx = fullText_.indexOf(thinkTags[1])
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
return { fullText, fullReasoning }
}
const newOnFinalMessage: OnFinalMessage = (params) => {
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
newOnText({ ...params })
const { fullText, fullReasoning } = getOnFinalMessageParams()
onFinalMessage({ ...params, fullText, fullReasoning })
}
return { newOnText, newOnFinalMessage }
}
// =============== tools ===============
type ToolsState = {
level: 'normal',
} | {
level: 'tool',
toolName: ToolName,
currentToolCall: RawToolCallObj,
} | {
level: 'param',
toolName: ToolName,
paramName: ToolParamName,
currentToolCall: RawToolCallObj,
}
export const extractToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
const tools = availableTools(chatMode)
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const toolOfToolName: { [toolName: string]: InternalToolInfo | undefined } = {}
for (const t of tools) { toolOfToolName[t.name] = t }
// detect <availableTools[0]></availableTools[0]>, etc
let fullText = '';
let trueFullText = ''
const firstToolCallRef: { current: RawToolCallObj | undefined } = { current: undefined }
let state: ToolsState = { level: 'normal' }
const getRawNewText = () => {
return trueFullText.substring(parser.startTagPosition, parser.position + 1)
}
const parser = createSaxParser()
// when see open tag <tagName>
parser.onopentag = (node) => {
const rawNewText = getRawNewText()
const tagName = node.name;
console.log('OPENING', tagName)
console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
if (state.level === 'normal') {
if (tagName in toolOfToolName) { // valid toolName
state = {
level: 'tool',
toolName: tagName as ToolName,
currentToolCall: { name: tagName as ToolName, rawParams: {}, doneParams: [], isDone: false }
}
firstToolCallRef.current = state.currentToolCall
}
else {
fullText += rawNewText // count as plaintext
console.log('adding raw a', rawNewText)
}
}
else if (state.level === 'tool') {
if (tagName in (toolOfToolName[state.toolName]?.params ?? {})) { // valid param
state = {
level: 'param',
toolName: state.toolName,
paramName: tagName as ToolParamName,
currentToolCall: state.currentToolCall,
}
}
else {
// would normally be rawNewText, but we ignore all text inside tools
}
}
else if (state.level === 'param') { // cannot double nest
fullText += rawNewText // count as plaintext
console.log('adding raw b', rawNewText)
}
console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
};
parser.onclosetag = (tagName) => {
const rawNewText = getRawNewText()
console.log('CLOSING', tagName)
console.log('state0:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
if (state.level === 'normal') {
fullText += rawNewText
console.log('adding raw A', rawNewText)
}
else if (state.level === 'tool') {
if (tagName === state.toolName) { // closed the tool
state.currentToolCall.isDone = true
state = {
level: 'normal',
}
}
else { // add as text
fullText += rawNewText
console.log('adding raw B', rawNewText)
}
}
else if (state.level === 'param') {
if (tagName === state.paramName) { // closed the param
state.currentToolCall.doneParams.push(state.paramName)
state = {
level: 'tool',
toolName: state.toolName,
currentToolCall: state.currentToolCall,
}
}
else {
fullText += rawNewText
console.log('adding raw C', rawNewText)
}
}
console.log('state1:', state.level, { toolName: (state as any).toolName, paramName: (state as any).paramName })
};
parser.ontext = (text) => {
if (state.level === 'normal') {
fullText += text
}
// start param
else if (state.level === 'tool') {
// ignore all text in a tool, all text should go in the param tags inside it
}
else if (state.level === 'param') {
if (!(state.paramName in state.currentToolCall.rawParams)) state.currentToolCall.rawParams[state.paramName] = ''
state.currentToolCall.rawParams[state.paramName] += text
}
}
let prevFullTextLen = 0
const newOnText: OnText = (params) => {
const newText = params.fullText.substring(prevFullTextLen)
prevFullTextLen = params.fullText.length
trueFullText = params.fullText
parser.write(newText)
// firstToolCallRef.current === state.currentToolCall is always true
onText({
...params,
fullText,
toolCall: firstToolCallRef.current,
});
};
const newOnFinalMessage: OnFinalMessage = (params) => {
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
newOnText({ ...params })
fullText = fullText.trimEnd()
const toolCall = firstToolCallRef.current
if (toolCall) {
// trim off all whitespace at and before first \n and after last \n for each param
for (const p in toolCall.rawParams) {
const paramName = p as ToolParamName
const orig = toolCall.rawParams[paramName]
if (orig === undefined) continue
toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig)
}
}
// console.log('final message!!!', trueFullText)
// console.log('----- returning ----\n', fullText)
// console.log('----- tools ----\n', JSON.stringify(firstToolCallRef.current, null, 2))
// console.log('----- toolCall ----\n', JSON.stringify(toolCall, null, 2))
onFinalMessage({ ...params, fullText, toolCall: toolCall })
}
return { newOnText, newOnFinalMessage };
}
// trim all whitespace up until the first newline, and all whitespace after the last newline
const trimBeforeAndAfterNewLines = (s: string) => {
if (!s) return s;
const firstNewLineIndex = s.indexOf('\n');
if (firstNewLineIndex !== -1 && s.substring(0, firstNewLineIndex).trim() === '') {
s = s.substring(firstNewLineIndex + 1, Infinity)
}
const lastNewLineIndex = s.lastIndexOf('\n');
if (lastNewLineIndex !== -1 && s.substring(lastNewLineIndex + 1, Infinity).trim() === '') {
s = s.substring(0, lastNewLineIndex)
}
return s
}

View file

@ -23,17 +23,10 @@ type InternalLLMChatMessage = {
} | {
role: 'assistant',
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
} | {
role: 'tool';
content: string; // result
name: string;
params: string;
id: string;
}
const EMPTY_MESSAGE = '(empty message)'
const EMPTY_TOOL_CONTENT = '(empty content)'
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => {
const messages = deepClone(messages_)
@ -145,7 +138,7 @@ const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputToke
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
const prepareMessages_systemMessage = ({
const prepareMessages_addSystemInstructions = ({
messages,
aiInstructions,
supportsSystemMessage,
@ -206,190 +199,190 @@ const prepareMessages_systemMessage = ({
// convert messages as if about to send to openai
/*
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
openai MESSAGE (role=assistant):
"tool_calls":[{
"type": "function",
"id": "call_12345xyz",
"function": {
"name": "get_weather",
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
}]
// // convert messages as if about to send to openai
// /*
// reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
// openai MESSAGE (role=assistant):
// "tool_calls":[{
// "type": "function",
// "id": "call_12345xyz",
// "function": {
// "name": "get_weather",
// "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
// }]
openai RESPONSE (role=user):
{ "role": "tool",
"tool_call_id": tool_call.id,
"content": str(result) }
// openai RESPONSE (role=user):
// { "role": "tool",
// "tool_call_id": tool_call.id,
// "content": str(result) }
also see
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
*/
// also see
// openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
// openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
// */
type PrepareMessagesToolsOpenAI = (
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
role: 'assistant',
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
tool_calls?: {
type: 'function';
id: string;
function: {
name: string;
arguments: string;
}
}[]
} | {
role: 'tool',
tool_call_id: string;
content: string;
}
)[]
const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
// type PrepareMessagesToolsOpenAI = (
// Exclude<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
// role: 'assistant',
// content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
// tool_calls?: {
// type: 'function';
// id: string;
// function: {
// name: string;
// arguments: string;
// }
// }[]
// } | {
// role: 'tool',
// tool_call_id: string;
// content: string;
// }
// )[]
// const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
const newMessages: PrepareMessagesToolsOpenAI = [];
// const newMessages: PrepareMessagesToolsOpenAI = [];
for (let i = 0; i < messages.length; i += 1) {
const currMsg = messages[i]
// for (let i = 0; i < messages.length; i += 1) {
// const currMsg = messages[i]
if (currMsg.role !== 'tool') {
newMessages.push(currMsg)
continue
}
// if (currMsg.role !== 'tool') {
// newMessages.push(currMsg)
// continue
// }
// edit previous assistant message to have called the tool
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
if (prevMsg?.role === 'assistant') {
prevMsg.tool_calls = [{
type: 'function',
id: currMsg.id,
function: {
name: currMsg.name,
arguments: JSON.stringify(currMsg.params)
}
}]
}
// // edit previous assistant message to have called the tool
// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
// if (prevMsg?.role === 'assistant') {
// prevMsg.tool_calls = [{
// type: 'function',
// id: currMsg.id,
// function: {
// name: currMsg.name,
// arguments: JSON.stringify(currMsg.params)
// }
// }]
// }
// add the tool
newMessages.push({
role: 'tool',
tool_call_id: currMsg.id,
content: currMsg.content || EMPTY_TOOL_CONTENT,
})
}
return { messages: newMessages }
// // add the tool
// newMessages.push({
// role: 'tool',
// tool_call_id: currMsg.id,
// content: currMsg.content || EMPTY_TOOL_CONTENT,
// })
// }
// return { messages: newMessages }
}
// }
// convert messages as if about to send to anthropic
/*
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
anthropic MESSAGE (role=assistant):
"content": [{
"type": "text",
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
}, {
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "San Francisco, CA", "unit": "celsius" }
}]
anthropic RESPONSE (role=user):
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "15 degrees"
}]
*/
// // convert messages as if about to send to anthropic
// /*
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
// anthropic MESSAGE (role=assistant):
// "content": [{
// "type": "text",
// "text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// }, {
// "type": "tool_use",
// "id": "toolu_01A09q90qw90lq917835lq9",
// "name": "get_weather",
// "input": { "location": "San Francisco, CA", "unit": "celsius" }
// }]
// anthropic RESPONSE (role=user):
// "content": [{
// "type": "tool_result",
// "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
// "content": "15 degrees"
// }]
// */
type PrepareMessagesToolsAnthropic = (
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
role: 'assistant',
content: string | (
| AnthropicReasoning
| {
type: 'text';
text: string;
}
| {
type: 'tool_use';
name: string;
input: Record<string, any>;
id: string;
})[]
} | {
role: 'user',
content: string | ({
type: 'text';
text: string;
} | {
type: 'tool_result';
tool_use_id: string;
content: string;
})[]
}
)[]
/*
Converts:
// type PrepareMessagesToolsAnthropic = (
// Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
// role: 'assistant',
// content: string | (
// | AnthropicReasoning
// | {
// type: 'text';
// text: string;
// }
// | {
// type: 'tool_use';
// name: string;
// input: Record<string, any>;
// id: string;
// })[]
// } | {
// role: 'user',
// content: string | ({
// type: 'text';
// text: string;
// } | {
// type: 'tool_result';
// tool_use_id: string;
// content: string;
// })[]
// }
// )[]
// /*
// Converts:
assistant: ...content
tool: (id, name, params)
->
assistant: ...content, call(name, id, params)
user: ...content, result(id, content)
*/
const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
const newMessages: PrepareMessagesToolsAnthropic = messages;
// assistant: ...content
// tool: (id, name, params)
// ->
// assistant: ...content, call(name, id, params)
// user: ...content, result(id, content)
// */
// const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
// const newMessages: PrepareMessagesToolsAnthropic = messages;
for (let i = 0; i < newMessages.length; i += 1) {
const currMsg = newMessages[i]
// for (let i = 0; i < newMessages.length; i += 1) {
// const currMsg = newMessages[i]
if (currMsg.role !== 'tool') continue
// if (currMsg.role !== 'tool') continue
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
if (prevMsg?.role === 'assistant') {
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
}
// if (prevMsg?.role === 'assistant') {
// if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
// prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
// }
// turn each tool into a user message with tool results at the end
newMessages[i] = {
role: 'user',
content: [
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const,
]
}
}
return { messages: newMessages }
}
// // turn each tool into a user message with tool results at the end
// newMessages[i] = {
// role: 'user',
// content: [
// ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const,
// ]
// }
// }
// return { messages: newMessages }
// }
type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI
// type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI
const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => {
if (!supportsTools) {
return { messages: messages }
}
else if (supportsTools === 'anthropic-style') {
return prepareMessages_tools_anthropic({ messages })
}
else if (supportsTools === 'openai-style') {
return prepareMessages_tools_openai({ messages })
}
else {
throw new Error(`supportsTools type not recognized`)
}
}
// const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => {
// if (!supportsTools) {
// return { messages: messages }
// }
// else if (supportsTools === 'anthropic-style') {
// return prepareMessages_tools_anthropic({ messages })
// }
// else if (supportsTools === 'openai-style') {
// return prepareMessages_tools_openai({ messages })
// }
// else {
// throw new Error(`supportsTools type not recognized`)
// }
// }
// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent
const prepareMessages_anthropicContent = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
const prepareMessages_anthropicReasoning = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
const newMessages: InternalLLMChatMessage[] = []
for (const m of messages) {
if (m.role !== 'assistant') {
@ -414,38 +407,18 @@ const prepareMessages_anthropicContent = ({ messages, supportsAnthropicReasoning
// do this at end
const prepareMessages_noEmptyMessage = ({ messages }: { messages: PrepareMessagesTools }): { messages: PrepareMessagesTools } => {
const prepareMessages_noEmptyMessage = ({ messages }: { messages: InternalLLMChatMessage[] }): { messages: InternalLLMChatMessage[] } => {
for (const currMsg of messages) {
// don't do this for tools
if (currMsg.role === 'tool') continue
// don't do this for assistant or user messages that have tool_calls or tool_results
const oai = currMsg as PrepareMessagesToolsOpenAI[0]
if (oai.role === 'assistant') {
if (oai.tool_calls) continue
}
const anth = currMsg as PrepareMessagesToolsAnthropic[0]
if (anth.role === 'assistant' || anth.role === 'user') {
if (typeof anth.content !== 'string') {
const hasContent = anth.content.find(c => c.type === 'tool_use' || c.type === 'tool_result')
if (hasContent) continue
}
}
if (typeof currMsg.content === 'string') {
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string')
currMsg.content = currMsg.content || EMPTY_MESSAGE
}
else {
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
else if (c.type === 'tool_use') { }
else if (c.type === 'tool_result') { }
}
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
}
}
return { messages }
}
@ -458,7 +431,6 @@ export const prepareMessages = ({
messages,
aiInstructions,
supportsSystemMessage,
supportsTools,
supportsAnthropicReasoningSignature,
contextWindow,
maxOutputTokens,
@ -466,7 +438,6 @@ export const prepareMessages = ({
messages: LLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
supportsTools: false | 'anthropic-style' | 'openai-style',
supportsAnthropicReasoningSignature: boolean,
contextWindow: number,
maxOutputTokens: number | null | undefined,
@ -475,13 +446,12 @@ export const prepareMessages = ({
const { messages: messages0 } = prepareMessages_normalize({ messages })
const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens })
const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature })
const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage })
const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools })
const { messages: messages5 } = prepareMessages_noEmptyMessage({ messages: messages4 })
const { messages: messages2 } = prepareMessages_anthropicReasoning({ messages: messages1, supportsAnthropicReasoningSignature })
const { messages: messages3, separateSystemMessageStr } = prepareMessages_addSystemInstructions({ messages: messages2, aiInstructions, supportsSystemMessage })
const { messages: messages4 } = prepareMessages_noEmptyMessage({ messages: messages3 })
return {
messages: messages5 as any,
messages: messages4 as any,
separateSystemMessageStr
} as const
}

View file

@ -0,0 +1,150 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// Define options for the parser.
export interface SaxParserOptions {
lowercase?: boolean;
}
// Define the structure for a parsed node.
export interface SaxNode {
name: string;
attributes: { [key: string]: string };
}
// Define the interface for the SAX-like parser.
export interface SaxParser {
// Event handlers that can be set by the consumer.
onopentag: ((node: SaxNode) => void) | null;
ontext: ((text: string) => void) | null;
onclosetag: ((tagName: string) => void) | null;
// Properties to track current positions (used for raw text extraction).
startTagPosition: number;
position: number;
// Processes a new chunk of text.
write(chunk: string): void;
}
/**
* Creates a minimal, event-driven SAX-like parser.
*
* @param options An object of type `SaxParserOptions`. Passing `{ lowercase: true }` will force all tag names to be lower-cased.
* @returns A parser object implementing the `SaxParser` interface.
*/
export function createSaxParser(options: SaxParserOptions = {}): SaxParser {
// Buffer to hold any leftover text (part of an incomplete tag).
let buffer: string = '';
// Global counter to track the total processed characters.
let globalPos: number = 0;
const parser: SaxParser = {
onopentag: null,
ontext: null,
onclosetag: null,
startTagPosition: 0,
position: 0,
write(chunk: string): void {
// Set the starting position before processing the new chunk.
this.startTagPosition = globalPos;
buffer += chunk;
globalPos += chunk.length;
// Set the current position to the end of the processed chunk.
this.position = globalPos - 1;
let cursor = 0;
// Flag to indicate if an incomplete tag was found.
let incompleteTagFound = false;
// This will mark the position in the buffer where the incomplete tag starts.
let incompleteStart = 0;
while (cursor < buffer.length) {
// Look for the next opening '<' character.
const ltIndex = buffer.indexOf('<', cursor);
if (ltIndex === -1) {
// No more tags found in the current buffer.
if (cursor < buffer.length && this.ontext) {
this.ontext(buffer.substring(cursor));
}
// All content is processed.
buffer = '';
cursor = buffer.length;
break;
}
// Emit any text between the current cursor and the opening tag.
if (ltIndex > cursor && this.ontext) {
this.ontext(buffer.substring(cursor, ltIndex));
}
// Look for the closing '>' character starting from the found '<'.
const gtIndex = buffer.indexOf('>', ltIndex);
if (gtIndex === -1) {
// Incomplete tag detected.
incompleteTagFound = true;
// Save the starting point of the incomplete tag.
incompleteStart = ltIndex;
break;
}
// Extract the tag content (excluding the '<' and '>').
let tagContent = buffer.substring(ltIndex + 1, gtIndex).trim();
if (!tagContent) {
cursor = gtIndex + 1;
continue;
}
// Check if this is a closing tag (starts with '/').
if (tagContent[0] === '/') {
let tagName = tagContent.substring(1).trim();
if (options.lowercase && tagName) {
tagName = tagName.toLowerCase();
}
if (this.onclosetag) {
this.onclosetag(tagName);
}
} else {
// Handle self-closing tags (ending with '/').
let selfClosing = false;
if (tagContent[tagContent.length - 1] === '/') {
selfClosing = true;
tagContent = tagContent.slice(0, -1).trim();
}
// Determine the tag name (first word before any whitespace).
const spaceIndex = tagContent.indexOf(' ');
let tagName =
spaceIndex !== -1
? tagContent.substring(0, spaceIndex).trim()
: tagContent;
if (options.lowercase && tagName) {
tagName = tagName.toLowerCase();
}
// Emit an open tag event.
if (this.onopentag) {
const node: SaxNode = { name: tagName, attributes: {} };
this.onopentag(node);
}
// If its a self-closing tag, immediately emit a close tag event.
if (selfClosing && this.onclosetag) {
this.onclosetag(tagName);
}
}
// Move the cursor past the current tag.
cursor = gtIndex + 1;
}
// If an incomplete tag was detected, preserve it.
if (incompleteTagFound) {
// Keep the incomplete portion starting from the '<'
buffer = buffer.substring(incompleteStart);
} else {
// Otherwise, remove all processed content.
buffer = buffer.substring(cursor);
}
},
};
return parser;
}

View file

@ -7,12 +7,11 @@ import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js';
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js';
type InternalCommonMessageParams = {
@ -27,7 +26,7 @@ type InternalCommonMessageParams = {
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] }
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
@ -35,34 +34,6 @@ export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayInfoOfProviderName(providerName).title} API key.`
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
type: 'function',
function: {
name: name,
strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
description: description,
parameters: {
type: 'object',
properties: params,
required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
additionalProperties: false,
},
}
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
}
type ToolCallOfIndex = { [index: string]: { name: string, paramsStr: string, id: string } } // type used to stream tool calls as they come in
type ToolCallsFrom_ReturnType = { name: ToolName, id: string, paramsStr: string }[] // return type of toolCallsFrom_<PROVIDER>
const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex): ToolCallsFrom_ReturnType => {
return Object.keys(toolCallOfIndex).map(index => {
const tool = toolCallOfIndex[index]
return isAToolName(tool.name) ? { name: tool.name, id: tool.id, paramsStr: tool.paramsStr } : null
}).filter(t => !!t)
}
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
const commonPayloadOpts: ClientOptions = {
@ -95,7 +66,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
}
else if (providerName === 'gemini') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'deepseek') {
const thisConfig = settingsOfProvider[providerName]
@ -152,11 +123,10 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
supportsTools,
contextWindow,
maxOutputTokens,
reasoningCapabilities,
@ -169,55 +139,44 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// tools
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
// max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
// instance
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName,
messages: messages,
stream: true,
// max_completion_tokens: maxTokens,
...toolsObj,
}
// open source models - manually parse think tokens
const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {}
const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags
if (manuallyParseReasoning) {
onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags)
const { newOnText, newOnFinalMessage } = extractReasoningWrapper(onText, onFinalMessage, openSourceThinkTags)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let fullToolName = ''
let fullToolParams = ''
const toolCallOfIndex: ToolCallOfIndex = {}
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
// tool call
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
const index = tool.index
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', paramsStr: '', id: '' }
toolCallOfIndex[index].name += tool.function?.name ?? ''
toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? '';
toolCallOfIndex[index].id += tool.id ?? ''
fullToolName += tool.function?.name ?? ''
fullToolParams += tool.function?.arguments ?? ''
}
// message
const newText = chunk.choices[0]?.delta?.content ?? ''
fullTextSoFar += newText
@ -230,20 +189,14 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
fullReasoningSoFar += newReasoning
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, fullToolName, fullToolParams })
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
// on final
const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex)
if (!fullTextSoFar && !fullReasoningSoFar && toolCalls.length === 0) {
if (!fullTextSoFar && !fullReasoningSoFar) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
if (manuallyParseReasoning) {
const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags)
onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning: null });
} else {
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls, anthropicReasoning: null });
}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null });
}
})
// when error/fail - this catches errors of both .create() and .then(for await)
@ -292,33 +245,11 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
// ------------ ANTHROPIC ------------
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
name: name,
description: description,
input_schema: {
type: 'object',
properties: params,
required: Object.keys(params),
},
} satisfies Anthropic.Messages.Tool
}
const toolCallsFrom_Anthropic = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => {
return content.map(c => {
if (c.type !== 'tool_use') return null
if (!isAToolName(c.name)) return null
return c.type === 'tool_use' ? { name: c.name, paramsStr: JSON.stringify(c.input), id: c.id } : null
}).filter(t => !!t)
}
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
contextWindow,
supportsTools,
maxOutputTokens,
reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_)
@ -330,18 +261,11 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// tools
const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined
const toolsObj: Partial<Anthropic.Messages.MessageStreamParams> = tools ? {
tools: tools,
tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool at a time
} : {}
// anthropic-specific - max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
// instance
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
const anthropic = new Anthropic({
apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true
@ -352,16 +276,22 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
messages: messages,
model: modelName,
max_tokens: maxTokens ?? 4_096, // anthropic requires this
...toolsObj,
...includeInPayload,
})
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
// when receive text
let fullText = ''
let fullReasoning = ''
let fullToolName = ''
let fullToolParams = ''
// let fullToolName = ''
// let fullToolParams = ''
// there are no events for tool_use, it comes in at the end
stream.on('streamEvent', e => {
@ -370,47 +300,46 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
if (e.content_block.type === 'text') {
if (fullText) fullText += '\n\n' // starting a 2nd text block
fullText += e.content_block.text
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
onText({ fullText, fullReasoning, })
}
else if (e.content_block.type === 'thinking') {
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += e.content_block.thinking
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
onText({ fullText, fullReasoning, })
}
else if (e.content_block.type === 'redacted_thinking') {
console.log('delta', e.content_block.type)
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += '[redacted_thinking]'
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.content_block.type === 'tool_use') {
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
onText({ fullText, fullReasoning, })
}
// else if (e.content_block.type === 'tool_use') {
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
// onText({ fullText, fullReasoning, })
// }
}
// delta
else if (e.type === 'content_block_delta') {
if (e.delta.type === 'text_delta') {
fullText += e.delta.text
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
onText({ fullText, fullReasoning, })
}
else if (e.delta.type === 'thinking_delta') {
fullReasoning += e.delta.thinking
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.delta.type === 'input_json_delta') { // tool use
fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
onText({ fullText, fullReasoning, })
}
// else if (e.delta.type === 'input_json_delta') { // tool use
// fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
// onText({ fullText, fullReasoning, })
// }
}
})
// on done - (or when error/fail) - this is called AFTER last streamEvent
stream.on('finalMessage', (response) => {
const toolCalls = toolCallsFrom_Anthropic(response.content)
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning })
onFinalMessage({ fullText, fullReasoning, anthropicReasoning })
})
// on error
stream.on('error', (error) => {
@ -420,23 +349,6 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
_setAborter(() => stream.controller.abort())
}
// // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming...
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
// stream.on('streamEvent', e => {
// if (e.type === 'content_block_start') {
// if (e.content_block.type !== 'tool_use') return
// const index = e.index
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
// toolCallOfIndex[index].name += e.content_block.name ?? ''
// toolCallOfIndex[index].args += e.content_block.input ?? ''
// }
// else if (e.type === 'content_block_delta') {
// if (e.delta.type !== 'input_json_delta') return
// toolCallOfIndex[e.index].args += e.delta.partial_json
// }
// })
// ------------ OLLAMA ------------
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in

View file

@ -21,7 +21,7 @@ export const sendLLMMessage = ({
settingsOfProvider,
modelSelection,
modelSelectionOptions,
tools,
chatMode,
}: SendLLMMessageParams,
metricsService: IMetricsService
@ -108,7 +108,7 @@ export const sendLLMMessage = ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, tools })
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode })
return
}
if (messagesType === 'FIMMessage') {

View file

@ -6,7 +6,7 @@
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
@ -17,22 +17,37 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
constructor(
@IProductService private readonly _productService: IProductService,
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
@IUpdateService private readonly _updateService: IUpdateService
) {
super()
}
async check() {
nIgnores = 0
async check(explicit: boolean) {
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
if (isDevMode) {
return { hasUpdate: false } as const
}
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
if (this._updateService.state.type === StateType.Ready) {
return { hasUpdate: true, message: 'Restart Void to update!' }
}
const wasAutomaticCheck = !explicit // ignore the first auto check, just use it to call updateService.check()
if (wasAutomaticCheck && this.nIgnores < 1) {
this.nIgnores += 1
return { hasUpdate: false } as const
}
try {
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
const resJSON = await res.json()
if (!resJSON) return null
if (!resJSON) return null // null means error
const { hasUpdate, downloadMessage } = resJSON ?? {}
if (hasUpdate === undefined)