mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
commit
868736330a
23 changed files with 5674 additions and 7937 deletions
11111
package-lock.json
generated
11111
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -106,8 +106,6 @@
|
|||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
|
|
@ -175,7 +173,6 @@
|
|||
"@vscode/v8-heap-parser": "^0.1.0",
|
||||
"@vscode/vscode-perf": "^0.0.19",
|
||||
"@webgpu/types": "^0.1.44",
|
||||
"ajv": "^8.17.1",
|
||||
"ansi-colors": "^3.2.3",
|
||||
"asar": "^3.0.3",
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ registerAction2(class extends Action2 {
|
|||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
console.log('hi')
|
||||
const n = accessor.get(IDummyService)
|
||||
console.log('Hi', n._serviceBrand)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sen
|
|||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { IToolsService } from './toolsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
|
|
@ -33,8 +33,13 @@ import { truncate } from '../../../../base/common/strings.js';
|
|||
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
|
||||
|
||||
// related to retrying when LLM message has error
|
||||
const CHAT_RETRIES = 3
|
||||
const RETRY_DELAY = 2500
|
||||
|
||||
|
||||
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
|
@ -180,9 +185,12 @@ export interface IChatThreadService {
|
|||
|
||||
getCurrentThread(): ThreadType;
|
||||
openNewThread(): void;
|
||||
deleteThread(threadId: string): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
// thread selector
|
||||
deleteThread(threadId: string): void;
|
||||
duplicateThread(threadId: string): void;
|
||||
|
||||
// exposed getters/setters
|
||||
// these all apply to current thread
|
||||
getCurrentMessageState: (messageIdx: number) => UserMessageState
|
||||
|
|
@ -194,6 +202,10 @@ export interface IChatThreadService {
|
|||
getCurrentFocusedMessageIdx(): number | undefined;
|
||||
isCurrentlyFocusingMessage(): boolean;
|
||||
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
dangerousSetState: (newState: ThreadsState) => void;
|
||||
resetState: () => void;
|
||||
|
||||
// // current thread's staging selections
|
||||
// closeCurrentStagingSelectionsInMessage(opts: { messageIdx: number }): void;
|
||||
// closeCurrentStagingSelectionsInThread(): void;
|
||||
|
|
@ -285,6 +297,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
|
||||
dangerousSetState = (newState: ThreadsState) => {
|
||||
this.state = newState
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
resetState = () => {
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // see constructor
|
||||
this.openNewThread()
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
// !!! this is important for properly restoring URIs from storage
|
||||
// should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough
|
||||
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
|
||||
|
|
@ -368,7 +390,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (!messages) return false
|
||||
const lastMsg = messages[messages.length - 1]
|
||||
if (!lastMsg) return false
|
||||
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
|
||||
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
this._editMessageInThread(threadId, messages.length - 1, tool)
|
||||
return true
|
||||
}
|
||||
|
|
@ -385,9 +408,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
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
|
||||
if (!(lastMsg.role === 'tool' && lastMsg.type === 'tool_request')) return // should never happen
|
||||
|
||||
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
|
||||
|
||||
|
|
@ -403,7 +424,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
|
||||
let params: ToolCallParams[ToolName]
|
||||
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
params = lastMsg.params
|
||||
}
|
||||
else return
|
||||
|
|
@ -486,11 +507,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
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
|
||||
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
if (approvalType) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
|
||||
// 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, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
|
|
@ -519,6 +542,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
}
|
||||
catch (error) {
|
||||
delete this._currentlyRunningToolInterruptor[threadId]
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
|
||||
|
||||
|
|
@ -538,6 +562,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
delete this._currentlyRunningToolInterruptor[threadId]
|
||||
|
||||
return {}
|
||||
};
|
||||
|
|
@ -582,12 +607,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
isRunningWhenEnd = undefined
|
||||
nMessagesSent += 1
|
||||
|
||||
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 chatMessages = this.state.allThreads[threadId]?.messages ?? []
|
||||
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
|
||||
chatMessages,
|
||||
|
|
@ -595,15 +614,18 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
chatMode
|
||||
})
|
||||
|
||||
|
||||
let aborted = false
|
||||
|
||||
let shouldRetry = true
|
||||
let nAttempts = 0
|
||||
|
||||
while (shouldRetry) {
|
||||
shouldRetry = false
|
||||
|
||||
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 llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
|
|
@ -621,15 +643,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: async (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
if (nAttempts < CHAT_RETRIES) {
|
||||
nAttempts += 1
|
||||
shouldRetry = true
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
timeout(2500).then(() => { resMessageIsDonePromise() })
|
||||
await timeout(RETRY_DELAY)
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
else {
|
||||
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
|
|
@ -641,9 +664,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
aborted = true
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
aborted = true
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -656,14 +679,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (shouldRetry) {
|
||||
continue
|
||||
}
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
|
||||
// this is a complete hack to make it so if an error loop was aborted, we stop (because onAbort does not get called if error happens instantly)
|
||||
// maybe we should remove all the abort stuff and just make it so that we only go by state?
|
||||
if (!this.streamState[threadId]?.isRunning) { return }
|
||||
|
||||
if (aborted) { return }
|
||||
if (shouldRetry) { continue }
|
||||
|
||||
// call tool if there is one
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
|
|
@ -671,9 +695,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// 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
|
||||
if (!this.streamState[threadId]?.isRunning) { return }
|
||||
if (aborted) { return }
|
||||
if (interrupted) { return }
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
console.log('awaiting...')
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
}
|
||||
else {
|
||||
|
|
@ -684,7 +711,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
} // end while (attempts)
|
||||
} // end while (send message)
|
||||
|
||||
|
||||
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||
this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge')
|
||||
|
||||
|
|
@ -881,7 +907,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const [_, toIdx] = c
|
||||
if (toIdx === fromIdx) return
|
||||
|
||||
console.log(`going from ${fromIdx} to ${toIdx}`)
|
||||
// console.log(`going from ${fromIdx} to ${toIdx}`)
|
||||
|
||||
// update the user's checkpoint
|
||||
this._addUserModificationsToCurrCheckpoint({ threadId })
|
||||
|
|
@ -1019,8 +1045,8 @@ 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
|
||||
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning) {
|
||||
// if about to call the other LLM, just wait for and stop now
|
||||
return
|
||||
}
|
||||
// stop it (this simply resolves the promise to free up space)
|
||||
|
|
@ -1199,8 +1225,9 @@ 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, searchInFolder: null, pageNumber: 0 })
|
||||
uris = result.uris
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ query: target, includePattern: null, pageNumber: 0 })
|
||||
const { uris: uris_ } = await result
|
||||
uris = uris_
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -1398,10 +1425,9 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId]!.messages.length === 0) {
|
||||
|
||||
// switch to the thread
|
||||
// switch to the existing empty thread and exit
|
||||
this.switchToThread(threadId)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
|
|
@ -1429,6 +1455,22 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
this._setState({ ...this.state, allThreads: newThreads }, true)
|
||||
}
|
||||
|
||||
duplicateThread(threadId: string) {
|
||||
const { allThreads: currentThreads } = this.state
|
||||
const threadToDuplicate = currentThreads[threadId]
|
||||
if (!threadToDuplicate) return
|
||||
const newThread = {
|
||||
...deepClone(threadToDuplicate),
|
||||
id: generateUuid(),
|
||||
}
|
||||
const newThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread,
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true)
|
||||
}
|
||||
|
||||
|
||||
private _addMessageToThread(threadId: string, message: ChatMessage) {
|
||||
const { allThreads } = this.state
|
||||
|
|
|
|||
|
|
@ -452,7 +452,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
return voidRules.trim();
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Could not read .voidrules, continuing...')
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
|
|||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
|
||||
|
||||
const MAX_FILES_TOTAL = 300;
|
||||
|
|
@ -111,14 +110,14 @@ export const computeDirectoryTree1Deep = async (
|
|||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.rootURI} is not a directory`;
|
||||
return `Error: ${params.uri} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) { // is first page
|
||||
output += `${params.rootURI.fsPath}\n`;
|
||||
output += `${params.uri.fsPath}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
|
|
@ -419,7 +418,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
}
|
||||
|
||||
if (cutOff) {
|
||||
return `${str}\n${cutOffMessage}`
|
||||
return `${str.trimEnd()}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
|
|
|
|||
|
|
@ -31,14 +31,11 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
|
|||
import { QuickEditPropsType } from './quickEditActions.js';
|
||||
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
|
||||
import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { INotificationService, } from '../../../../platform/notification/common/notification.js';
|
||||
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
|
||||
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
|
|
@ -48,6 +45,8 @@ 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';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
|
|
@ -199,7 +198,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
@IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
// @ICommandService private readonly _commandService: ICommandService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
|
|
@ -279,24 +278,24 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
const details = errorDetails(e.fullError)
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: `Void Error: ${e.message}`,
|
||||
actions: {
|
||||
secondary: [{
|
||||
id: 'void.onerror.opensettings',
|
||||
enabled: true,
|
||||
label: `Open Void's settings`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
}]
|
||||
},
|
||||
source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
})
|
||||
}
|
||||
// private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
// const details = errorDetails(e.fullError)
|
||||
// this._notificationService.notify({
|
||||
// severity: Severity.Warning,
|
||||
// message: `Void Error: ${e.message}`,
|
||||
// actions: {
|
||||
// secondary: [{
|
||||
// id: 'void.onerror.opensettings',
|
||||
// enabled: true,
|
||||
// label: `Open Void's settings`,
|
||||
// tooltip: '',
|
||||
// class: undefined,
|
||||
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
// }]
|
||||
// },
|
||||
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
|
@ -1393,7 +1392,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
// throws
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
this._notifyError(e)
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError
|
||||
|
|
@ -1612,7 +1611,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
this._notifyError(e)
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError || new Error(e.message) // throw error h
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,12 +3,12 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
import { Check, Trash2, X } from 'lucide-react';
|
||||
import { ThreadType } from '../../../chatThreadService.js';
|
||||
import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react';
|
||||
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
export const OldSidebarThreadSelector = () => {
|
||||
|
|
@ -153,6 +153,14 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
|||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
const streamState = useFullChatThreadsStreamState()
|
||||
|
||||
const runningThreadIds: { [threadId: string]: IsRunningType | undefined } = {}
|
||||
for (const threadId in streamState) {
|
||||
const isRunning = streamState[threadId]?.isRunning
|
||||
if (isRunning) { runningThreadIds[threadId] = isRunning }
|
||||
}
|
||||
|
||||
if (!allThreads) {
|
||||
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
|
@ -183,6 +191,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
|||
idx={i}
|
||||
hoveredIdx={hoveredIdx}
|
||||
setHoveredIdx={setHoveredIdx}
|
||||
isRunning={runningThreadIds[pastThread.id]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
@ -238,6 +247,21 @@ const formatTime = (date: Date) => {
|
|||
};
|
||||
|
||||
|
||||
const DuplicateButton = ({ threadId }: { threadId: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
return <IconShell1
|
||||
Icon={Copy}
|
||||
className='size-[11px]'
|
||||
onClick={() => { chatThreadsService.duplicateThread(threadId); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Duplicate thread'
|
||||
>
|
||||
</IconShell1>
|
||||
|
||||
}
|
||||
|
||||
const TrashButton = ({ threadId }: { threadId: string }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
|
|
@ -271,18 +295,51 @@ const TrashButton = ({ threadId }: { threadId: string }) => {
|
|||
onClick={() => { setIsTrashPressed(true); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Delete thread?'
|
||||
data-tooltip-content='Delete thread'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pastThread: ThreadType, idx: number, hoveredIdx: number | null, setHoveredIdx: (idx: number | null) => void }) => {
|
||||
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunning }: {
|
||||
pastThread: ThreadType,
|
||||
idx: number,
|
||||
hoveredIdx: number | null,
|
||||
setHoveredIdx: (idx: number | null) => void,
|
||||
isRunning: IsRunningType | undefined,
|
||||
}
|
||||
|
||||
) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
// const settingsState = useSettingsState()
|
||||
// const convertService = accessor.get('IConvertToLLMMessageService')
|
||||
// const chatMode = settingsState.globalSettings.chatMode
|
||||
// const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null
|
||||
// const copyChatButton = <CopyButton
|
||||
// codeStr={async () => {
|
||||
// const { messages } = await convertService.prepareLLMChatMessages({
|
||||
// chatMessages: currentThread.messages,
|
||||
// chatMode,
|
||||
// modelSelection,
|
||||
// })
|
||||
// return JSON.stringify(messages, null, 2)
|
||||
// }}
|
||||
// toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`}
|
||||
// />
|
||||
|
||||
|
||||
// const currentThread = chatThreadsService.getCurrentThread()
|
||||
// const copyChatButton2 = <CopyButton
|
||||
// codeStr={async () => {
|
||||
// return JSON.stringify(currentThread.messages, null, 2)
|
||||
// }}
|
||||
// toolTipName={`Copy As Void Chat`}
|
||||
// />
|
||||
|
||||
let firstMsg = null;
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
|
|
@ -319,13 +376,28 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pas
|
|||
>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||
{/* spinner */}
|
||||
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
null}
|
||||
{/* name */}
|
||||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2 opacity-60">
|
||||
<div className="flex items-center gap-x-1 opacity-60">
|
||||
{idx === hoveredIdx ?
|
||||
<TrashButton threadId={pastThread.id} />
|
||||
: detailsHTML
|
||||
<>
|
||||
{/* trash icon */}
|
||||
<DuplicateButton threadId={pastThread.id} />
|
||||
|
||||
{/* trash icon */}
|
||||
<TrashButton threadId={pastThread.id} />
|
||||
</>
|
||||
: <>
|
||||
{detailsHTML}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,80 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
|||
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
|
||||
}
|
||||
|
||||
type GenerateNextOptions = (newPathText: string) => Option[]
|
||||
|
||||
type Option = {
|
||||
name: string,
|
||||
displayName: string,
|
||||
} & (
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
|
||||
| { nextOptions?: undefined, generateNextOptions?: undefined }
|
||||
)
|
||||
|
||||
|
||||
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
|
||||
|
||||
|
||||
const allOptions: Option[] = [
|
||||
{
|
||||
name: 'files',
|
||||
displayName: 'files',
|
||||
generateNextOptions: () => [
|
||||
{ name: 'a.txt', displayName: 'a.txt', },
|
||||
{ name: 'b.txt', displayName: 'b.txt', },
|
||||
{ name: 'c.txt', displayName: 'c.txt', },
|
||||
{ name: 'd.txt', displayName: 'd.txt', },
|
||||
{ name: 'e.txt', displayName: 'e.txt', },
|
||||
{ name: 'f.txt', displayName: 'f.txt', },
|
||||
{ name: 'g.txt', displayName: 'g.txt', },
|
||||
{ name: '!a.txt', displayName: '!a.txt', },
|
||||
{ name: '!b.txt', displayName: '!b.txt', },
|
||||
{ name: '!c.txt', displayName: '!c.txt', },
|
||||
{ name: '!d.txt', displayName: '!d.txt', },
|
||||
{ name: '!e.txt', displayName: '!e.txt', },
|
||||
{ name: '!f.txt', displayName: '!f.txt', },
|
||||
{ name: '!g.txt', displayName: '!g.txt', },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'folders',
|
||||
displayName: 'folders',
|
||||
nextOptions: [
|
||||
{ name: 'FOLDER', displayName: 'FOLDER', },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// follow the path in the optionsTree (until the last path element)
|
||||
|
||||
let nextOptionsAtPath = allOptions
|
||||
let generateNextOptionsAtPath: GenerateNextOptions | undefined = undefined
|
||||
|
||||
for (const pn of path) {
|
||||
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
|
||||
|
||||
if (!selectedOption) return;
|
||||
|
||||
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
|
||||
generateNextOptionsAtPath = selectedOption.generateNextOptions
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (generateNextOptionsAtPath) {
|
||||
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
|
||||
}
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
|
||||
|
||||
|
||||
return optionsAtPath
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
|
||||
type InputBox2Props = {
|
||||
|
|
@ -64,8 +138,235 @@ type InputBox2Props = {
|
|||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const accessor = useAccessor()
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const selectedOptionRef = useRef<HTMLDivElement>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const [path, setPath] = useState<string[]>([]);
|
||||
const [optionIdx, setOptionIdx] = useState<number>(0);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [newPathText, setNewPathText] = useState<string>('');
|
||||
|
||||
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// Focus the textarea first
|
||||
textarea.focus();
|
||||
|
||||
// The most reliable way to simulate typing is to use execCommand
|
||||
// which will trigger all the appropriate native events
|
||||
document.execCommand('insertText', false, text);
|
||||
|
||||
// React's onChange relies on a SyntheticEvent system
|
||||
// The best way to ensure it runs is to call callbacks directly
|
||||
if (onChangeText) {
|
||||
onChangeText(textarea.value);
|
||||
}
|
||||
adjustHeight();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const onSelectOption = () => {
|
||||
|
||||
if (!options.length) { return; }
|
||||
|
||||
const option = options[optionIdx];
|
||||
const newPath = [...path, option.name]
|
||||
const isLastOption = !option.generateNextOptions && !option.nextOptions
|
||||
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionIdx(0)
|
||||
if (isLastOption) {
|
||||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(`TODO-${option.displayName}`)
|
||||
}
|
||||
else {
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveOption = () => {
|
||||
const newPath = [...path.slice(0, path.length - 1)]
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionIdx(0)
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
}
|
||||
|
||||
const onOpenOptionMenu = () => {
|
||||
setPath([])
|
||||
setNewPathText('')
|
||||
setIsMenuOpen(true);
|
||||
setOptionIdx(0);
|
||||
setOptions(getOptionsAtPath(accessor, [], '') || []);
|
||||
}
|
||||
const onCloseOptionMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
|
||||
const onNavigateUp = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
|
||||
}
|
||||
const onNavigateDown = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
|
||||
}
|
||||
|
||||
const onPathTextChange = (newStr: string) => {
|
||||
setNewPathText(newStr);
|
||||
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
|
||||
|
||||
}
|
||||
|
||||
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'ArrowUp') {
|
||||
onNavigateUp();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
onNavigateDown();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Enter') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Escape') {
|
||||
onCloseOptionMenu()
|
||||
} else if (e.key === 'Backspace') {
|
||||
|
||||
if (!newPathText) { // No text remaining
|
||||
if (path.length === 0) {
|
||||
onCloseOptionMenu()
|
||||
} else {
|
||||
onRemoveOption();
|
||||
}
|
||||
}
|
||||
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
|
||||
onPathTextChange('')
|
||||
}
|
||||
else { // Backspace
|
||||
onPathTextChange(newPathText.slice(0, -1))
|
||||
}
|
||||
} else if (e.key.length === 1) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
|
||||
// do nothing
|
||||
}
|
||||
else { // letter
|
||||
onPathTextChange(newPathText + e.key)
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
};
|
||||
|
||||
// scroll the selected optionIdx into view on optionIdx and newPathText changes
|
||||
useEffect(() => {
|
||||
if (isMenuOpen && selectedOptionRef.current) {
|
||||
selectedOptionRef.current.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
|
||||
|
||||
|
||||
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const gapPx = 2
|
||||
const offsetPx = 2
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
strategy,
|
||||
refs,
|
||||
middlewareData,
|
||||
update
|
||||
} = useFloating({
|
||||
open: isMenuOpen,
|
||||
onOpenChange: setIsMenuOpen,
|
||||
placement: 'top',
|
||||
|
||||
middleware: [
|
||||
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
|
||||
flip({
|
||||
boundary: document.body,
|
||||
padding: 8
|
||||
}),
|
||||
shift({
|
||||
boundary: document.body,
|
||||
padding: 8,
|
||||
}),
|
||||
size({
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
const maxHeight = Math.min(availableHeight)
|
||||
|
||||
Object.assign(elements.floating.style, {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
// Ensure the width isn't constrained by the parent
|
||||
width: `${Math.max(
|
||||
rects.reference.width,
|
||||
measureRef.current?.offsetWidth ?? 0
|
||||
)}px`
|
||||
});
|
||||
},
|
||||
padding: 8,
|
||||
// Use viewport as boundary instead of any parent element
|
||||
boundary: document.body,
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: 'fixed',
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
const floating = refs.floating.current;
|
||||
const reference = refs.reference.current;
|
||||
|
||||
// Check if reference is an HTML element before using contains
|
||||
const isReferenceHTMLElement = reference && 'contains' in reference;
|
||||
|
||||
if (
|
||||
floating &&
|
||||
(!isReferenceHTMLElement || !reference.contains(target)) &&
|
||||
!floating.contains(target)
|
||||
) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isMenuOpen, refs.floating, refs.reference]);
|
||||
|
||||
|
||||
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
|
|
@ -104,18 +405,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
|
||||
|
||||
return (
|
||||
return <>
|
||||
<textarea
|
||||
autoFocus={false}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
|
||||
refs.setReference(r)
|
||||
|
||||
textAreaRef.current = r
|
||||
if (typeof ref === 'function') ref(r)
|
||||
else if (ref) ref.current = r
|
||||
adjustHeight()
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref, refs])}
|
||||
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
|
|
@ -130,7 +433,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
// inputBorder: asCssVariable(inputBorder),
|
||||
}}
|
||||
|
||||
onChange={useCallback(() => {
|
||||
onInput={useCallback((event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const latestChange = (event.nativeEvent as InputEvent).data;
|
||||
|
||||
if (latestChange === '@') {
|
||||
onOpenOptionMenu()
|
||||
}
|
||||
|
||||
}, [onOpenOptionMenu, accessor])}
|
||||
|
||||
onChange={useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
onChangeText?.(r.value)
|
||||
|
|
@ -138,18 +450,75 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
}, [onChangeText, adjustHeight])}
|
||||
|
||||
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
if (isMenuOpen) {
|
||||
onMenuKeyDown(e)
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
// Shift + Enter when multiline = newline
|
||||
const shouldAddNewline = e.shiftKey && multiline
|
||||
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
|
||||
}
|
||||
onKeyDown?.(e)
|
||||
}, [onKeyDown, multiline])}
|
||||
}, [onKeyDown, onMenuKeyDown, multiline])}
|
||||
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
<div>{`idx ${optionIdx}`}</div>
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0
|
||||
}}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="py-1">
|
||||
{/* Path navigation breadcrumbs */}
|
||||
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
|
||||
{[...path, newPathText].join(' > ')}
|
||||
</div>
|
||||
|
||||
{/* Options list */}
|
||||
{options.length === 0 ? (
|
||||
<div className="px-3 py-2 text-void-fg-3">No options available</div>
|
||||
) : (
|
||||
options.map((o, oIdx) => (
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
|
||||
key={o.name}
|
||||
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-void-fg-1">{o.displayName}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M4.5 2.5L8 6L4.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -304,6 +304,16 @@ export const useChatThreadsStreamState = (threadId: string) => {
|
|||
return s
|
||||
}
|
||||
|
||||
export const useFullChatThreadsStreamState = () => {
|
||||
const [s, ss] = useState(chatThreadsStreamState)
|
||||
useEffect(() => {
|
||||
ss(chatThreadsStreamState)
|
||||
const listener = () => { ss(chatThreadsStreamState) }
|
||||
chatThreadsStreamStateListeners.add(listener)
|
||||
return () => { chatThreadsStreamStateListeners.delete(listener) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'l
|
|||
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
|
||||
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
|
||||
const OVERRIDE_VALUE = false
|
||||
|
||||
|
|
@ -29,7 +30,9 @@ export const VoidOnboarding = () => {
|
|||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
>
|
||||
<VoidOnboardingContent />
|
||||
<ErrorBoundary>
|
||||
<VoidOnboardingContent />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -303,11 +306,11 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.type === 'default',
|
||||
showAsDefault: m.type !== 'custom',
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
|
@ -317,7 +320,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
...infoOfModelName[modelName],
|
||||
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -339,7 +342,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName)
|
||||
|
|
@ -380,7 +383,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
|
||||
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
|
|
@ -388,10 +391,13 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true} />
|
||||
<ErrorBoundary>
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
|
|
@ -672,19 +678,22 @@ const VoidOnboardingContent = () => {
|
|||
{ id: 'cheap', label: 'Affordable' },
|
||||
{ id: 'all', label: 'All' }
|
||||
].map(option => (
|
||||
<button
|
||||
<ErrorBoundary
|
||||
key={option.id}
|
||||
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
|
||||
? 'dark:text-white text-black font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={`${option.label} providers`}
|
||||
data-tooltip-place='bottom'
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
|
||||
? 'dark:text-white text-black font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={`${option.label} providers`}
|
||||
data-tooltip-place='bottom'
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -693,108 +702,129 @@ const VoidOnboardingContent = () => {
|
|||
{/* Provider Buttons - Modified to use separate components for each tab */}
|
||||
<div className="mb-2 w-full">
|
||||
{/* Intelligent tab */}
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
|
||||
const isSelected = selectedIntelligentProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedIntelligentProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
|
||||
const isSelected = selectedIntelligentProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedIntelligentProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Private tab */}
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['private'].map((providerName) => {
|
||||
const isSelected = selectedPrivateProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedPrivateProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['private'].map((providerName) => {
|
||||
const isSelected = selectedPrivateProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedPrivateProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Affordable tab */}
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
|
||||
const isSelected = selectedAffordableProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAffordableProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
|
||||
const isSelected = selectedAffordableProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAffordableProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* All tab */}
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
|
||||
{providerNames.map((providerName) => {
|
||||
const isSelected = selectedAllProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAllProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
|
||||
{providerNames.map((providerName) => {
|
||||
const isSelected = selectedAllProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAllProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
|
||||
|
||||
|
||||
{/* Models Table */}
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
<ErrorBoundary>
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
|
||||
<div className='mb-5 mt-8 mx-auto'>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
<ErrorBoundary>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
<div className='my-4'>
|
||||
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
|
||||
</div>
|
||||
|
||||
<div className='my-4'>
|
||||
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
</div>
|
||||
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
<ErrorBoundary>
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Button and status indicators */}
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
|
||||
<ErrorBoundary>
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
|
|
@ -802,10 +832,11 @@ const VoidOnboardingContent = () => {
|
|||
}
|
||||
|
||||
bottom={
|
||||
<FadeIn delayMs={50} durationMs={10}>
|
||||
{prevAndNextButtons}
|
||||
</FadeIn>
|
||||
|
||||
<ErrorBoundary>
|
||||
<FadeIn delayMs={50} durationMs={10}>
|
||||
{prevAndNextButtons}
|
||||
</FadeIn>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
/>,
|
||||
|
|
@ -864,7 +895,9 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
|
||||
{contentOfIdx[pageIndex]}
|
||||
<ErrorBoundary>
|
||||
{contentOfIdx[pageIndex]}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
|
|
@ -16,7 +16,8 @@ import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
|||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
|
||||
import Severity from '../../../../../../../base/common/severity.js'
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
|
|
@ -152,6 +153,36 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
|
|||
|
||||
}
|
||||
|
||||
// ConfirmButton prompts for a second click to confirm an action, cancels if clicking outside
|
||||
const ConfirmButton = ({ children, onConfirm, className }: { children: React.ReactNode, onConfirm: () => void, className?: string }) => {
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!confirm) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setConfirm(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, [confirm]);
|
||||
return (
|
||||
<div ref={ref} className={`inline-block`}>
|
||||
<VoidButtonBgDarken className={className} onClick={() => {
|
||||
if (!confirm) {
|
||||
setConfirm(true);
|
||||
} else {
|
||||
onConfirm();
|
||||
setConfirm(false);
|
||||
}
|
||||
}}>
|
||||
{confirm ? `Confirm Reset` : children}
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
|
|
@ -165,14 +196,14 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
|
|||
const [showCheckmark, setShowCheckmark] = useState(false)
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const providerName = permanentProviderName ?? userChosenProviderName;
|
||||
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
const numModels = settingsState.settingsOfProvider[providerName].models.length
|
||||
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
|
||||
|
||||
if (showCheckmark) {
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white dark:text-black px-3 py-1 rounded-sm ${className}`} />
|
||||
|
|
@ -198,59 +229,66 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
|
|||
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
|
||||
|
||||
{/* provider input */}
|
||||
{!permanentProviderName &&
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setUserChosenProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
|
||||
className={`max-w-32 mx-2 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 py-1 px-2 rounded`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
}
|
||||
<ErrorBoundary>
|
||||
{!permanentProviderName &&
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setUserChosenProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
|
||||
className={`max-w-32 mx-2 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 py-1 px-2 rounded`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* model input */}
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* add button */}
|
||||
<AddButton
|
||||
type='submit'
|
||||
disabled={!modelName}
|
||||
onClick={(e) => {
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
// setErrorString(`This model already exists under ${providerName}.`)
|
||||
setErrorString(`This model already exists.`)
|
||||
return
|
||||
}
|
||||
<ErrorBoundary>
|
||||
<AddButton
|
||||
type='submit'
|
||||
disabled={!modelName}
|
||||
onClick={(e) => {
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
// setErrorString(`This model already exists under ${providerName}.`)
|
||||
setErrorString(`This model already exists.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
/>
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
</form>
|
||||
|
|
@ -551,18 +589,20 @@ const FastApplyMethodDropdown = () => {
|
|||
}
|
||||
|
||||
|
||||
export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
|
||||
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
|
||||
<div
|
||||
className='pl-6 flex items-center w-fit'
|
||||
data-tooltip-id='void-tooltip-ollama-settings'
|
||||
>
|
||||
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
|
||||
export const OllamaSetupInstructions = () => {
|
||||
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
|
||||
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
|
||||
<div
|
||||
className='pl-6 flex items-center w-fit'
|
||||
data-tooltip-id='void-tooltip-ollama-settings'
|
||||
>
|
||||
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const RedoOnboardingButton = ({ className }: { className?: string }) => {
|
||||
|
|
@ -711,6 +751,34 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const ToolApprovalTypeSwitch = ({ approvalType, size, desc }: { approvalType: ToolApprovalType, size: "xxs" | "xs" | "sm" | "sm+" | "md", desc: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const onToggleAutoApprove = useCallback((approvalType: ToolApprovalType, newValue: boolean) => {
|
||||
voidSettingsService.setGlobalSetting('autoApprove', {
|
||||
...voidSettingsService.state.globalSettings.autoApprove,
|
||||
[approvalType]: newValue
|
||||
})
|
||||
metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue })
|
||||
}, [voidSettingsService, metricsService])
|
||||
|
||||
return <>
|
||||
<VoidSwitch
|
||||
size={size}
|
||||
value={voidSettingsState.globalSettings.autoApprove[approvalType] ?? false}
|
||||
onChange={(newVal) => onToggleAutoApprove(approvalType, newVal)}
|
||||
/>
|
||||
<span className="text-void-fg-3 text-xs">{desc}</span>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
|
|
@ -737,7 +805,28 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
|||
setTransferState({ type: 'loading' })
|
||||
|
||||
let errAcc = ''
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
// Define extensions to skip when transferring
|
||||
const extensionBlacklist = [
|
||||
// ignore extensions
|
||||
'ms-vscode-remote.remote-ssh',
|
||||
'ms-vscode-remote.remote-wsl',
|
||||
// ignore other AI copilots that could conflict with Void keybindings
|
||||
'sourcegraph.cody-ai',
|
||||
'continue.continue',
|
||||
'codeium.codeium',
|
||||
'saoudrizwan.claude-dev', // cline
|
||||
'rooveterinaryinc.roo-cline', // roo
|
||||
];
|
||||
for (const { from, to } of transferTheseFiles) {
|
||||
try {
|
||||
// find a blacklisted item
|
||||
const isBlacklisted = extensionBlacklist.find(blacklistItem => {
|
||||
return from.fsPath?.includes(blacklistItem)
|
||||
})
|
||||
if (isBlacklisted) continue
|
||||
|
||||
} catch { }
|
||||
|
||||
console.log('transferring', from, to)
|
||||
// Check if the source file exists before attempting to copy
|
||||
try {
|
||||
|
|
@ -794,6 +883,72 @@ export const Settings = () => {
|
|||
const nativeHostService = accessor.get('INativeHostService')
|
||||
const settingsState = useSettingsState()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
|
||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||
let dataStr: string
|
||||
let downloadName: string
|
||||
if (t === 'Chats') {
|
||||
// Export chat threads
|
||||
dataStr = JSON.stringify(chatThreadsService.state, null, 2)
|
||||
downloadName = 'void-chats.json'
|
||||
}
|
||||
else if (t === 'Settings') {
|
||||
// Export user settings
|
||||
dataStr = JSON.stringify(voidSettingsService.state, null, 2)
|
||||
downloadName = 'void-settings.json'
|
||||
}
|
||||
else {
|
||||
dataStr = ''
|
||||
downloadName = ''
|
||||
}
|
||||
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = downloadName
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
|
||||
// Add file input refs
|
||||
const fileInputSettingsRef = useRef<HTMLInputElement>(null)
|
||||
const fileInputChatsRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [s, ss] = useState(0)
|
||||
|
||||
const handleUpload = (t: 'Chats' | 'Settings') => (e: React.ChangeEvent<HTMLInputElement>,) => {
|
||||
const files = e.target.files
|
||||
if (!files) return;
|
||||
const file = files[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const json = JSON.parse(reader.result as string);
|
||||
|
||||
if (t === 'Chats') {
|
||||
chatThreadsService.dangerousSetState(json as any)
|
||||
}
|
||||
else if (t === 'Settings') {
|
||||
voidSettingsService.dangerousSetState(json as any)
|
||||
}
|
||||
|
||||
notificationService.info(`${t} imported successfully!`)
|
||||
} catch (err) {
|
||||
notificationService.notify({ message: `Failed to import ${t}`, source: err + '', severity: Severity.Error, })
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = '';
|
||||
|
||||
ss(s => s + 1)
|
||||
}
|
||||
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
|
|
@ -805,6 +960,8 @@ export const Settings = () => {
|
|||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
|
|
@ -820,7 +977,7 @@ export const Settings = () => {
|
|||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
{ollamaSetupInstructions}
|
||||
<OllamaSetupInstructions />
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
|
|
@ -933,15 +1090,12 @@ export const Settings = () => {
|
|||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<ErrorBoundary>
|
||||
{[...toolApprovalTypes].map((approvalType) => {
|
||||
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div>
|
||||
})}
|
||||
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.autoApprove}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
|
|
@ -985,10 +1139,10 @@ export const Settings = () => {
|
|||
{/* General section (formerly GeneralTab) */}
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
|
|
@ -996,6 +1150,41 @@ export const Settings = () => {
|
|||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section, as its own block right after One-Click Switch */}
|
||||
<div className='mt-12'>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<div className='flex gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<h3 className='text-xl mb-2'>Settings</h3>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<h3 className='text-xl mb-2'>Chat</h3>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
|
|
@ -1004,23 +1193,17 @@ export const Settings = () => {
|
|||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const WarningBox = ({ text, onClick, className }: { text: string; onClick
|
|||
>
|
||||
<IconWarning
|
||||
size={14}
|
||||
className='mr-1'
|
||||
className='mr-1 flex-shrink-0'
|
||||
/>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
|
||||
import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
|
||||
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
|
||||
import { MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
|
||||
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
|
||||
import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -18,9 +18,12 @@ export interface ITerminalToolService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
listTerminalIds(): string[];
|
||||
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>;
|
||||
openTerminal(terminalId: string): Promise<void>
|
||||
runCommand(command: string, bgTerminalId: string | null): Promise<{ terminalId: string, resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
|
||||
focusTerminal(terminalId: string): Promise<void>
|
||||
terminalExists(terminalId: string): boolean
|
||||
|
||||
createTerminal(): Promise<string>
|
||||
killTerminal(terminalId: string): Promise<void>
|
||||
}
|
||||
|
||||
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
|
||||
|
|
@ -103,12 +106,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
|||
throw new Error('This should never be reached by pigeonhole principle');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async _getOrCreateTerminal(proposedTerminalId: string) {
|
||||
// if terminal ID exists, return it
|
||||
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
|
||||
|
||||
async createTerminal() {
|
||||
// create new terminal and return its ID
|
||||
const terminalId = this.getValidNewTerminalId();
|
||||
const terminal = await this.terminalService.createTerminal({
|
||||
|
|
@ -127,13 +125,21 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
|||
})
|
||||
disposables.push(d)
|
||||
})
|
||||
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 1000) })
|
||||
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
|
||||
|
||||
await Promise.any([waitForMount, waitForTimeout,])
|
||||
disposables.forEach(d => d.dispose())
|
||||
|
||||
this.terminalInstanceOfId[terminalId] = terminal
|
||||
return { terminalId, didCreateTerminal: true }
|
||||
return terminalId
|
||||
}
|
||||
|
||||
async killTerminal(terminalId: string) {
|
||||
const terminal = this.terminalInstanceOfId[terminalId]
|
||||
if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`);
|
||||
terminal.dispose(TerminalExitReason.Extension)
|
||||
delete this.terminalInstanceOfId[terminalId]
|
||||
return
|
||||
}
|
||||
|
||||
terminalExists(terminalId: string): boolean {
|
||||
|
|
@ -141,7 +147,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
|||
}
|
||||
|
||||
|
||||
openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => {
|
||||
focusTerminal: ITerminalToolService['focusTerminal'] = async (terminalId) => {
|
||||
if (!terminalId) return
|
||||
const terminal = this.terminalInstanceOfId[terminalId]
|
||||
if (!terminal) return // should never happen
|
||||
|
|
@ -151,81 +157,104 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
|||
|
||||
|
||||
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
|
||||
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, bgTerminalId) => {
|
||||
await this.terminalService.whenConnected;
|
||||
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
|
||||
const terminal = this.terminalInstanceOfId[terminalId];
|
||||
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
|
||||
|
||||
// focus the terminal about to run
|
||||
this.terminalService.setActiveInstance(terminal)
|
||||
await this.terminalService.focusActiveInstance()
|
||||
|
||||
let result: string = ''
|
||||
let resolveReason: TerminalResolveReason | undefined = undefined
|
||||
|
||||
let terminal: ITerminalInstance
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
const waitUntilDone = new Promise<void>((res, rej) => {
|
||||
const d2 = terminal.onData(async newData => {
|
||||
if (resolveReason) return
|
||||
const isBG = bgTerminalId !== null
|
||||
let terminalId: string
|
||||
if (isBG) { // BG process
|
||||
terminal = this.terminalInstanceOfId[bgTerminalId];
|
||||
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} did not exist.`);
|
||||
terminalId = bgTerminalId
|
||||
}
|
||||
else {
|
||||
terminalId = await this.createTerminal()
|
||||
terminal = this.terminalInstanceOfId[terminalId]
|
||||
if (!terminal) throw new Error(`Unexpected error: Terminal could not be created.`)
|
||||
}
|
||||
|
||||
result += newData
|
||||
|
||||
// onPageFull
|
||||
if (result.length > MAX_TERMINAL_CHARS_PAGE) {
|
||||
result = result.substring(0, MAX_TERMINAL_CHARS_PAGE)
|
||||
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
|
||||
resolveReason = { type: 'toofull' }
|
||||
res()
|
||||
return
|
||||
}
|
||||
const waitForResult = async () => {
|
||||
// focus the terminal about to run
|
||||
this.terminalService.setActiveInstance(terminal)
|
||||
await this.terminalService.focusActiveInstance()
|
||||
|
||||
// onDone
|
||||
const isDone = isCommandComplete(result)
|
||||
if (isDone) {
|
||||
resolveReason = { type: 'done', exitCode: isDone.exitCode }
|
||||
res()
|
||||
return
|
||||
}
|
||||
let result: string = ''
|
||||
let resolveReason: TerminalResolveReason | undefined = undefined
|
||||
|
||||
|
||||
// create this before we send so that we don't miss events on terminal
|
||||
const waitUntilDone = new Promise<void>((res, rej) => {
|
||||
const d2 = terminal.onData(async newData => {
|
||||
if (resolveReason) return
|
||||
result += newData
|
||||
// onDone
|
||||
const isDone = isCommandComplete(result)
|
||||
if (isDone) {
|
||||
resolveReason = { type: 'done', exitCode: isDone.exitCode }
|
||||
res()
|
||||
return
|
||||
}
|
||||
})
|
||||
disposables.push(d2)
|
||||
})
|
||||
disposables.push(d2)
|
||||
})
|
||||
|
||||
|
||||
// send the command here
|
||||
await terminal.sendText(command, true)
|
||||
// send the command here
|
||||
await terminal.sendText(command, true)
|
||||
|
||||
// timeout promise
|
||||
const waitUntilTimeout = new Promise<void>((res, rej) => {
|
||||
setTimeout(async () => {
|
||||
if (resolveReason) return
|
||||
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
|
||||
resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' }
|
||||
res()
|
||||
return
|
||||
}, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000)
|
||||
})
|
||||
// inactivity-based timeout
|
||||
const waitUntilInactive = new Promise<void>(res => {
|
||||
let globalTimeoutId: ReturnType<typeof setTimeout>;
|
||||
const resetTimer = () => {
|
||||
clearTimeout(globalTimeoutId);
|
||||
globalTimeoutId = setTimeout(() => {
|
||||
if (resolveReason) return
|
||||
|
||||
await Promise.any([
|
||||
waitUntilDone,
|
||||
waitUntilTimeout,
|
||||
])
|
||||
resolveReason = { type: 'timeout' };
|
||||
res();
|
||||
}, MAX_TERMINAL_INACTIVE_TIME * 1000);
|
||||
};
|
||||
|
||||
disposables.forEach(d => d.dispose())
|
||||
const dTimeout = terminal.onData(() => { resetTimer(); });
|
||||
disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId)));
|
||||
resetTimer();
|
||||
});
|
||||
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
// wait for result
|
||||
await Promise.any([waitUntilDone, waitUntilInactive,])
|
||||
|
||||
disposables.forEach(d => d.dispose())
|
||||
if (!isBG) {
|
||||
await this.killTerminal(terminalId)
|
||||
}
|
||||
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
|
||||
.join('\n')
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
|
||||
return { terminalId, didCreateTerminal, result, resolveReason }
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
|
||||
.join('\n')
|
||||
|
||||
if (result.length > MAX_TERMINAL_CHARS) {
|
||||
const half = MAX_TERMINAL_CHARS / 2
|
||||
result = result.slice(0, half)
|
||||
+ '\n...\n'
|
||||
+ result.slice(result.length - half, Infinity)
|
||||
}
|
||||
|
||||
return { result, resolveReason }
|
||||
|
||||
}
|
||||
const resPromise = waitForResult()
|
||||
|
||||
return { terminalId, resPromise }
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
|
|||
import { IMarkerService, MarkerSeverity } 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'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
|
||||
|
||||
|
|
@ -27,20 +27,11 @@ import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
|||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
|
||||
|
||||
|
||||
// pagination info
|
||||
export const MAX_FILE_CHARS_PAGE = 500_000
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
export const MAX_TERMINAL_CHARS_PAGE = 20_000
|
||||
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
|
||||
export const TERMINAL_BG_WAIT_TIME = 1
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
return !u || u === 'null' || u === 'undefined'
|
||||
}
|
||||
|
|
@ -176,12 +167,12 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
return { rootURI: uri, pageNumber }
|
||||
return { uri, pageNumber }
|
||||
},
|
||||
get_dir_structure: (params: RawToolParamsObj) => {
|
||||
get_dir_tree: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, } = params
|
||||
const uri = validateURI(uriStr)
|
||||
return { rootURI: uri }
|
||||
return { uri }
|
||||
},
|
||||
search_pathnames_only: (params: RawToolParamsObj) => {
|
||||
const {
|
||||
|
|
@ -192,9 +183,9 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown)
|
||||
const includePattern = validateOptionalStr('include_pattern', includeUnknown)
|
||||
|
||||
return { queryStr, searchInFolder, pageNumber }
|
||||
return { query: queryStr, includePattern, pageNumber }
|
||||
|
||||
},
|
||||
search_for_files: (params: RawToolParamsObj) => {
|
||||
|
|
@ -204,14 +195,23 @@ export class ToolsService implements IToolsService {
|
|||
is_regex: isRegexUnknown,
|
||||
page_number: pageNumberUnknown
|
||||
} = params
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const searchInFolder = validateOptionalURI(searchInFolderUnknown)
|
||||
const isRegex = validateBoolean(isRegexUnknown, { default: false })
|
||||
|
||||
return { queryStr, searchInFolder, isRegex, pageNumber }
|
||||
return {
|
||||
query: queryStr,
|
||||
isRegex,
|
||||
searchInFolder,
|
||||
pageNumber
|
||||
}
|
||||
},
|
||||
search_in_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, query: queryUnknown, is_regex: isRegexUnknown } = params;
|
||||
const uri = validateURI(uriStr);
|
||||
const query = validateStr('query', queryUnknown);
|
||||
const isRegex = validateBoolean(isRegexUnknown, { default: false });
|
||||
return { uri, query, isRegex };
|
||||
},
|
||||
|
||||
read_lint_errors: (params: RawToolParamsObj) => {
|
||||
|
|
@ -242,18 +242,28 @@ export class ToolsService implements IToolsService {
|
|||
},
|
||||
|
||||
edit_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
|
||||
const { uri: uriStr, change_diff: changeDiffUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
|
||||
return { uri, changeDescription }
|
||||
const changeDiff = validateStr('changeDiff', changeDiffUnknown)
|
||||
return { uri, changeDiff }
|
||||
},
|
||||
|
||||
command_tool: (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 })
|
||||
return { command, proposedTerminalId, waitForCompletion }
|
||||
// ---
|
||||
|
||||
run_command: (params: RawToolParamsObj) => {
|
||||
const { command: commandUnknown, terminal_id: terminalIdUnknown } = params;
|
||||
const command = validateStr('command', commandUnknown);
|
||||
const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null;
|
||||
return { command, bgTerminalId: proposedTerminalId };
|
||||
},
|
||||
open_persistent_terminal: (_params: RawToolParamsObj) => {
|
||||
// No parameters needed; will open a new background terminal
|
||||
return {};
|
||||
},
|
||||
kill_persistent_terminal: (params: RawToolParamsObj) => {
|
||||
const { terminal_id: terminalIdUnknown } = params;
|
||||
const terminalId = validateProposedTerminalId(terminalIdUnknown);
|
||||
return { terminalId };
|
||||
},
|
||||
|
||||
}
|
||||
|
|
@ -283,21 +293,21 @@ export class ToolsService implements IToolsService {
|
|||
return { result: { fileContents, totalFileLen, hasNextPage } }
|
||||
},
|
||||
|
||||
ls_dir: async ({ rootURI, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber)
|
||||
ls_dir: async ({ uri, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryTree1Deep(fileService, uri, pageNumber)
|
||||
return { result: dirResult }
|
||||
},
|
||||
|
||||
get_dir_structure: async ({ rootURI }) => {
|
||||
const str = await this.directoryStrService.getDirectoryStrTool(rootURI)
|
||||
get_dir_tree: async ({ uri }) => {
|
||||
const str = await this.directoryStrService.getDirectoryStrTool(uri)
|
||||
return { result: { str } }
|
||||
},
|
||||
|
||||
search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => {
|
||||
search_pathnames_only: async ({ query: queryStr, includePattern, pageNumber }) => {
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: searchInFolder ?? undefined,
|
||||
includePattern: includePattern ?? undefined,
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -311,7 +321,7 @@ export class ToolsService implements IToolsService {
|
|||
return { result: { uris, hasNextPage } }
|
||||
},
|
||||
|
||||
search_for_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
search_for_files: async ({ query: queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
const searchFolders = searchInFolder === null ?
|
||||
workspaceContextService.getWorkspace().folders.map(f => f.uri)
|
||||
: [searchInFolder]
|
||||
|
|
@ -332,6 +342,24 @@ export class ToolsService implements IToolsService {
|
|||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
return { result: { queryStr, uris, hasNextPage } }
|
||||
},
|
||||
search_in_file: async ({ uri, query, isRegex }) => {
|
||||
await voidModelService.initializeModel(uri);
|
||||
const { model } = await voidModelService.getModelSafe(uri);
|
||||
if (model === null) { throw new Error(`No contents; File does not exist.`); }
|
||||
const contents = model.getValue(EndOfLinePreference.LF);
|
||||
const contentOfLine = contents.split('\n');
|
||||
const totalLines = contentOfLine.length;
|
||||
const regex = isRegex ? new RegExp(query) : null;
|
||||
const lines: number[] = []
|
||||
for (let i = 0; i < totalLines; i++) {
|
||||
const line = contentOfLine[i];
|
||||
if ((isRegex && regex!.test(line)) || (!isRegex && line.includes(query))) {
|
||||
const matchLine = i + 1;
|
||||
lines.push(matchLine);
|
||||
}
|
||||
}
|
||||
return { result: { lines } };
|
||||
},
|
||||
|
||||
read_lint_errors: async ({ uri }) => {
|
||||
await timeout(1000)
|
||||
|
|
@ -355,14 +383,14 @@ export class ToolsService implements IToolsService {
|
|||
return { result: {} }
|
||||
},
|
||||
|
||||
edit_file: async ({ uri, changeDescription }) => {
|
||||
edit_file: async ({ uri, changeDiff }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
if (this.commandBarService.getStreamState(uri) === 'streaming') {
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and resume later.`)
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
|
||||
}
|
||||
const opts = {
|
||||
uri,
|
||||
applyStr: changeDescription,
|
||||
applyStr: changeDiff,
|
||||
from: 'ClickApply',
|
||||
startBehavior: 'keep-conflicts',
|
||||
} as const
|
||||
|
|
@ -385,10 +413,25 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
return { result: lintErrorsPromise, interruptTool }
|
||||
},
|
||||
command_tool: async ({ command, proposedTerminalId, waitForCompletion }) => {
|
||||
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
|
||||
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
|
||||
// ---
|
||||
run_command: async ({ command, bgTerminalId }) => {
|
||||
const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId)
|
||||
const interruptTool = () => {
|
||||
this.terminalToolService.killTerminal(terminalId)
|
||||
}
|
||||
return { result: resPromise, interruptTool }
|
||||
},
|
||||
open_persistent_terminal: async () => {
|
||||
// Open a new background terminal without waiting for completion
|
||||
const terminalId = await this.terminalToolService.createTerminal()
|
||||
return { result: { terminalId } }
|
||||
},
|
||||
kill_persistent_terminal: async ({ terminalId }) => {
|
||||
// Close the background terminal by sending exit
|
||||
await this.terminalToolService.killTerminal(terminalId)
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -401,7 +444,7 @@ export class ToolsService implements IToolsService {
|
|||
.substring(0, MAX_FILE_CHARS_PAGE)
|
||||
}
|
||||
|
||||
// given to the LLM after the call
|
||||
// given to the LLM after the call for successful tool calls
|
||||
this.stringOfResult = {
|
||||
read_file: (params, result) => {
|
||||
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
|
||||
|
|
@ -410,7 +453,7 @@ export class ToolsService implements IToolsService {
|
|||
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
|
||||
return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining
|
||||
},
|
||||
get_dir_structure: (params, result) => {
|
||||
get_dir_tree: (params, result) => {
|
||||
return result.str
|
||||
},
|
||||
search_pathnames_only: (params, result) => {
|
||||
|
|
@ -419,6 +462,15 @@ export class ToolsService implements IToolsService {
|
|||
search_for_files: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search_in_file: (params, result) => {
|
||||
const { model } = voidModelService.getModel(params.uri)
|
||||
if (!model) return '<Error getting string of result>'
|
||||
const lines = result.lines.map(n => {
|
||||
const lineContent = model.getValueInRange({ startLineNumber: n, startColumn: 1, endLineNumber: n, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
|
||||
return `Line ${n}:\n\`\`\`\n${lineContent}\n\`\`\``
|
||||
}).join('\n\n');
|
||||
return lines;
|
||||
},
|
||||
read_lint_errors: (params, result) => {
|
||||
return result.lintErrors ?
|
||||
stringifyLintErrors(result.lintErrors)
|
||||
|
|
@ -440,30 +492,41 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
|
||||
},
|
||||
command_tool: (params, result) => {
|
||||
run_command: (params, result) => {
|
||||
const {
|
||||
terminalId,
|
||||
didCreateTerminal,
|
||||
resolveReason,
|
||||
result: result_,
|
||||
} = result
|
||||
const { bgTerminalId } = params
|
||||
|
||||
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
|
||||
// success
|
||||
if (resolveReason.type === 'done') {
|
||||
const desc = bgTerminalId ? ` in terminal ${bgTerminalId}` : ''
|
||||
return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
|
||||
}
|
||||
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command ran in ${terminalDesc}, but did not complete after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
|
||||
// bg command
|
||||
if (bgTerminalId !== null) {
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command is running in the background in terminal ${bgTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\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_}`
|
||||
}
|
||||
else if (resolveReason.type === 'toofull') {
|
||||
return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}`
|
||||
}
|
||||
else if (resolveReason.type === 'done') {
|
||||
return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
|
||||
// normal command
|
||||
else {
|
||||
if (resolveReason.type === 'timeout') {
|
||||
return `Terminal command ran, but was interrupted after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. Full output:\n${result_}`
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
|
||||
},
|
||||
open_persistent_terminal: (_params, result) => {
|
||||
const { terminalId } = result;
|
||||
return `Successfully created background terminal with ID ${terminalId}`;
|
||||
},
|
||||
kill_persistent_terminal: (params, _result) => {
|
||||
return `Successfully closed terminal ${params.terminalId}.`;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ export const defaultProviderSettings = {
|
|||
liteLLM: { // https://docs.litellm.ai/docs/providers/openai_compatible
|
||||
endpoint: '',
|
||||
},
|
||||
googleVertex: { // google https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
region: 'us-west2',
|
||||
project: '',
|
||||
},
|
||||
// googleVertex: { // google https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
// region: 'us-west2',
|
||||
// project: '',
|
||||
// },
|
||||
microsoftAzure: { // microsoft Azure Foundry
|
||||
project: '', // really 'resource'
|
||||
apiKey: '',
|
||||
|
|
@ -129,7 +129,7 @@ export const defaultModelsOfProvider = {
|
|||
'ministral-8b-latest',
|
||||
],
|
||||
openAICompatible: [], // fallback
|
||||
googleVertex: [],
|
||||
// googleVertex: [],
|
||||
microsoftAzure: [],
|
||||
liteLLM: [],
|
||||
|
||||
|
|
@ -830,12 +830,12 @@ const groqSettings: VoidStaticProviderInfo = {
|
|||
|
||||
|
||||
// ---------------- GOOGLE VERTEX ----------------
|
||||
const googleVertexModelOptions = {
|
||||
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||
const googleVertexSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: googleVertexModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
// const googleVertexModelOptions = {
|
||||
// } as const satisfies Record<string, VoidStaticModelInfo>
|
||||
// const googleVertexSettings: VoidStaticProviderInfo = {
|
||||
// modelOptions: googleVertexModelOptions,
|
||||
// modelOptionsFallback: (modelName) => { return null }
|
||||
// }
|
||||
|
||||
// ---------------- MICROSOFT AZURE ----------------
|
||||
const microsoftAzureModelOptions = {
|
||||
|
|
@ -1081,7 +1081,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
|||
liteLLM: liteLLMSettings,
|
||||
lmStudio: lmStudioSettings,
|
||||
|
||||
googleVertex: googleVertexSettings,
|
||||
// googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
} as const
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
|||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
|
||||
import { approvalTypeOfToolName, ToolResultType } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
|
|
@ -20,6 +20,14 @@ export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000
|
|||
export const MAX_DIRSTR_RESULTS_TOTAL_BEGINNING = 100
|
||||
export const MAX_DIRSTR_RESULTS_TOTAL_TOOL = 100
|
||||
|
||||
// tool info
|
||||
export const MAX_FILE_CHARS_PAGE = 500_000
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
|
||||
// terminal tool info
|
||||
export const MAX_TERMINAL_CHARS = 100_000
|
||||
export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds
|
||||
|
||||
|
||||
// Maximum character limits for prefix and suffix context
|
||||
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
|
|
@ -66,7 +74,36 @@ const paginationParam = {
|
|||
} as const
|
||||
|
||||
|
||||
|
||||
|
||||
// export type SnakeCase<S extends string> =
|
||||
// // exact acronym URI
|
||||
// S extends 'URI' ? 'uri'
|
||||
// // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri'
|
||||
// : S extends `${infer Prefix}URI` ? `${SnakeCase<Prefix>}_uri`
|
||||
// // default: for each char, prefix '_' on uppercase letters
|
||||
// : S extends `${infer C}${infer Rest}`
|
||||
// ? `${C extends Lowercase<C> ? C : `_${Lowercase<C>}`}${SnakeCase<Rest>}`
|
||||
// : S;
|
||||
|
||||
// export type SnakeCaseKeys<T extends Record<string, any>> = {
|
||||
// [K in keyof T as SnakeCase<Extract<K, string>>]: T[K]
|
||||
// };
|
||||
|
||||
|
||||
|
||||
export const voidTools = {
|
||||
// export const voidTools
|
||||
// : {
|
||||
// [T in keyof ToolCallParams]: {
|
||||
// name: string;
|
||||
// description: string;
|
||||
// params: {
|
||||
// [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
read_file: {
|
||||
|
|
@ -74,8 +111,8 @@ export const voidTools = {
|
|||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to 1.' },
|
||||
end_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to Infinity.' },
|
||||
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
|
@ -89,8 +126,8 @@ export const voidTools = {
|
|||
},
|
||||
},
|
||||
|
||||
get_dir_structure: {
|
||||
name: 'get_dir_structure',
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
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')
|
||||
|
|
@ -106,7 +143,7 @@ export const voidTools = {
|
|||
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: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
|
@ -118,12 +155,23 @@ export const voidTools = {
|
|||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Returns all lint errors on a given file.`,
|
||||
|
|
@ -156,33 +204,46 @@ export const voidTools = {
|
|||
description: `Edits the contents of a file given the file's URI and a description.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
change_description: {
|
||||
change_diff: {
|
||||
description: `\
|
||||
Your description MUST be wrapped in triple backticks. \
|
||||
A code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
|
||||
NEVER re-write the whole file. Bias towards writing as little as possible. \
|
||||
Here's an example of a good description:\n${editToolDescriptionExample}`
|
||||
A code diff describing the change to make to the file. \
|
||||
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
|
||||
Your DIFF MUST be wrapped in triple backticks. \
|
||||
NEVER re-write the whole file. Always bias towards writing as little as possible. \
|
||||
Use comments like "// ... existing code ..." to condense your writing. \
|
||||
Here's an example of a good output:\n${editToolDescriptionExample}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
command_tool: {
|
||||
name: 'command_tool',
|
||||
description: `Runs a terminal command. You can use this tool to run any command: sed, grep, etc. We just prefer you edit with the edit tool, not this tool if possible.`,
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
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.' },
|
||||
bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' },
|
||||
},
|
||||
},
|
||||
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {}
|
||||
},
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Closes a BG terminal with the given ID.`,
|
||||
params: { terminal_id: { description: `The terminal ID to interrupt and close.` } }
|
||||
}
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [name: string]: InternalToolInfo }
|
||||
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
|
||||
|
||||
|
||||
export type ToolName = keyof typeof voidTools
|
||||
export type ToolName = keyof ToolResultType
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
||||
|
|
@ -197,7 +258,7 @@ export const isAToolName = (toolName: string): toolName is ToolName => {
|
|||
|
||||
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 === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
: undefined
|
||||
|
||||
|
|
@ -447,7 +508,7 @@ export const DIVIDER = `=======`
|
|||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
export const searchReplace_systemMessage = `\
|
||||
You are a coding assistant that takes in a diff describing of a change to make, and outputs SEARCH/REPLACE code blocks which implement the change.
|
||||
You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff.
|
||||
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
|
||||
|
||||
Format your SEARCH/REPLACE blocks as follows:
|
||||
|
|
@ -459,11 +520,11 @@ ${DIVIDER}
|
|||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
|
||||
1. Every single item written in \`CHANGE\` should show up in the final result, except for comments explicitly saying things like "// ... existing code". Make sure to include ALL other comments (even descriptive ones), code, whitespace, etc. in the final result.
|
||||
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
|
||||
|
||||
2. Your SEARCH/REPLACE block(s) must implement the change EXACTLY. You should use comments like "// ... existing code" as reference points, and everything else in the change should be written verbatim.
|
||||
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
|
||||
|
||||
3. You are allowed to output multiple SEARCH/REPLACE blocks.
|
||||
3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
|
||||
|
||||
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ToolName } from './prompt/prompts.js';
|
|||
|
||||
|
||||
|
||||
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
|
||||
export type TerminalResolveReason = { type: 'timeout' } | { type: 'done', exitCode: number }
|
||||
|
||||
export type LintErrorItem = { code: string, message: string, startLineNumber: number, endLineNumber: number }
|
||||
|
||||
|
|
@ -16,39 +16,57 @@ export type ShallowDirectoryItem = {
|
|||
}
|
||||
|
||||
|
||||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'edit_file': 'edits',
|
||||
'run_command': 'terminal',
|
||||
}
|
||||
|
||||
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'command_tool'] as const satisfies readonly ToolName[]
|
||||
export type ToolNameWithApproval = typeof toolNamesWithApproval[number]
|
||||
export const toolNamesThatRequireApproval = new Set<ToolName>(toolNamesWithApproval)
|
||||
|
||||
|
||||
// {{add: define new type for approval types}}
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>(
|
||||
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
|
||||
)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
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, searchInFolder: string | null, pageNumber: number },
|
||||
'search_for_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
'ls_dir': { uri: URI, pageNumber: number },
|
||||
'get_dir_tree': { uri: URI },
|
||||
'search_pathnames_only': { query: string, includePattern: string | null, pageNumber: number },
|
||||
'search_for_files': { query: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
'search_in_file': { uri: URI, query: string, isRegex: boolean },
|
||||
'read_lint_errors': { uri: URI },
|
||||
// ---
|
||||
'edit_file': { uri: URI, changeDescription: string },
|
||||
'edit_file': { uri: URI, changeDiff: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
|
||||
'command_tool': { command: string, proposedTerminalId: string, waitForCompletion: boolean },
|
||||
// ---
|
||||
'run_command': { command: string; bgTerminalId: string | null },
|
||||
'open_persistent_terminal': {},
|
||||
'kill_persistent_terminal': { terminalId: string },
|
||||
}
|
||||
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_structure': { str: string, },
|
||||
'get_dir_tree': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
'search_for_files': { uris: URI[], hasNextPage: boolean },
|
||||
'search_in_file': { lines: number[]; },
|
||||
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
|
||||
// ---
|
||||
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
|
||||
'create_file_or_folder': {},
|
||||
'delete_file_or_folder': {},
|
||||
'command_tool': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
|
||||
// ---
|
||||
'run_command': { result: string; resolveReason: TerminalResolveReason; },
|
||||
'open_persistent_terminal': { terminalId: string },
|
||||
'kill_persistent_terminal': {},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ export interface IVoidSettingsService {
|
|||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
dangerousSetState(newState: VoidSettingsState): Promise<void>;
|
||||
resetState(): Promise<void>;
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
|
|
@ -231,12 +234,31 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
this.readAndInitializeState()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
dangerousSetState = async (newState: VoidSettingsState) => {
|
||||
this.state = _validatedModelState(newState)
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async readAndInitializeState() {
|
||||
let readS: VoidSettingsState
|
||||
try {
|
||||
readS = await this._readState();
|
||||
// 1.0.3 addition, remove when enough users have had this code run
|
||||
if (readS.globalSettings.includeToolLintErrors === undefined) readS.globalSettings.includeToolLintErrors = true
|
||||
|
||||
// autoapprove is now an obj not a boolean (1.2.5)
|
||||
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
|
||||
}
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
|
||||
import { ToolApprovalType } from './toolsServiceTypes.js';
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
|
|
@ -96,9 +97,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
else if (providerName === 'mistral') {
|
||||
return { title: 'Mistral', }
|
||||
}
|
||||
else if (providerName === 'googleVertex') {
|
||||
return { title: 'Google Vertex AI', }
|
||||
}
|
||||
// else if (providerName === 'googleVertex') {
|
||||
// return { title: 'Google Vertex AI', }
|
||||
// }
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
return { title: 'Microsoft Azure OpenAI', }
|
||||
}
|
||||
|
|
@ -117,7 +118,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
|||
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
|
||||
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
|
||||
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
|
||||
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
|
||||
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
|
||||
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
|
|
@ -148,9 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-key...' :
|
||||
providerName === 'mistral' ? 'api-key...' :
|
||||
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
'',
|
||||
// providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||
providerName === 'microsoftAzure' ? 'key-...' :
|
||||
'',
|
||||
|
||||
isPasswordField: true,
|
||||
}
|
||||
|
|
@ -161,10 +162,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
// providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
|
|
@ -176,14 +177,14 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
|
||||
}
|
||||
}
|
||||
else if (settingName === 'region') {
|
||||
// vertex only
|
||||
return {
|
||||
title: 'Region',
|
||||
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
: ''
|
||||
}
|
||||
}
|
||||
// else if (settingName === 'region') {
|
||||
// // vertex only
|
||||
// return {
|
||||
// title: 'Region',
|
||||
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||
// : ''
|
||||
// }
|
||||
// }
|
||||
else if (settingName === 'azureApiVersion') {
|
||||
// azure only
|
||||
return {
|
||||
|
|
@ -194,12 +195,12 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
}
|
||||
else if (settingName === 'project') {
|
||||
return {
|
||||
title: providerName === 'googleVertex' ? 'Project'
|
||||
: providerName === 'microsoftAzure' ? 'Resource'
|
||||
: '',
|
||||
placeholder: providerName === 'googleVertex' ? 'my-project'
|
||||
: providerName === 'microsoftAzure' ? 'my-resource'
|
||||
: ''
|
||||
title: providerName === 'microsoftAzure' ? 'Resource'
|
||||
// : providerName === 'googleVertex' ? 'Project'
|
||||
: '',
|
||||
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
|
||||
// : providerName === 'googleVertex' ? 'my-project'
|
||||
: ''
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +228,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
region: undefined,
|
||||
// region: undefined, // googleVertex
|
||||
project: undefined,
|
||||
azureApiVersion: undefined,
|
||||
}
|
||||
|
|
@ -323,12 +324,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
googleVertex: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.googleVertex,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
// googleVertex: { // aggregator (serves models from multiple providers)
|
||||
// ...defaultCustomSettings,
|
||||
// ...defaultProviderSettings.googleVertex,
|
||||
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
|
||||
// _didFillInProviderSettings: undefined,
|
||||
// },
|
||||
microsoftAzure: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.microsoftAzure,
|
||||
|
|
@ -425,7 +426,7 @@ export type GlobalSettings = {
|
|||
syncApplyToChat: boolean;
|
||||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: boolean;
|
||||
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
|
||||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
|
|
@ -438,7 +439,7 @@ export const defaultGlobalSettings: GlobalSettings = {
|
|||
syncApplyToChat: true,
|
||||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: false,
|
||||
autoApprove: {},
|
||||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Ollama } from 'ollama';
|
|||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||
import { GoogleAuth } from 'google-auth-library'
|
||||
// import { GoogleAuth } from 'google-auth-library'
|
||||
/* eslint-enable */
|
||||
|
||||
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||
|
|
@ -42,13 +42,13 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
|
|||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
|
||||
const getGoogleApiKey = async () => {
|
||||
// module‑level singleton
|
||||
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
|
||||
const key = await auth.getAccessToken()
|
||||
if (!key) throw new Error(`Google API failed to generate a key.`)
|
||||
return key
|
||||
}
|
||||
// const getGoogleApiKey = async () => {
|
||||
// // module‑level singleton
|
||||
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
|
||||
// const key = await auth.getAccessToken()
|
||||
// if (!key) throw new Error(`Google API failed to generate a key.`)
|
||||
// return key
|
||||
// }
|
||||
|
||||
|
||||
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
|
|
@ -92,13 +92,12 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
|||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'googleVertex') {
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
const apiKey = await getGoogleApiKey()
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
|
||||
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
// else if (providerName === 'googleVertex') {
|
||||
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
|
||||
// const thisConfig = settingsOfProvider[providerName]
|
||||
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
|
||||
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
|
||||
// }
|
||||
else if (providerName === 'microsoftAzure') {
|
||||
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
|
|
@ -692,11 +691,11 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
googleVertex: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
// googleVertex: {
|
||||
// sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
// sendFIM: null,
|
||||
// list: null,
|
||||
// },
|
||||
microsoftAzure: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
|
|
|
|||
Loading…
Reference in a new issue