mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Merge pull request #459 from voideditor/model-selection
Dump full files into context; better onboarding; better nav
This commit is contained in:
commit
38eeee8e30
26 changed files with 1485 additions and 1457 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.3.0",
|
||||
"voidVersion": "1.3.2",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
"linkProtectionTrustedDomains": [
|
||||
"https://voideditor.com",
|
||||
"https://voideditor.dev",
|
||||
"https://github.com/voideditor/void"
|
||||
"https://github.com/voideditor/void",
|
||||
"https://ollama.com"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,3 +8,19 @@ export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
|||
export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff'
|
||||
|
||||
export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff'
|
||||
|
||||
export const VOID_GOTO_NEXT_DIFF_ACTION_ID = 'void.goToNextDiff'
|
||||
|
||||
export const VOID_GOTO_PREV_DIFF_ACTION_ID = 'void.goToPrevDiff'
|
||||
|
||||
export const VOID_GOTO_NEXT_URI_ACTION_ID = 'void.goToNextUri'
|
||||
|
||||
export const VOID_GOTO_PREV_URI_ACTION_ID = 'void.goToPrevUri'
|
||||
|
||||
export const VOID_ACCEPT_FILE_ACTION_ID = 'void.acceptFile'
|
||||
|
||||
export const VOID_REJECT_FILE_ACTION_ID = 'void.rejectFile'
|
||||
|
||||
export const VOID_ACCEPT_ALL_DIFFS_ACTION_ID = 'void.acceptAllDiffs'
|
||||
|
||||
export const VOID_REJECT_ALL_DIFFS_ACTION_ID = 'void.rejectAllDiffs'
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
|||
import { timeout } from '../../../../base/common/async.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
|
||||
|
||||
// related to retrying when LLM message has error
|
||||
|
|
@ -100,6 +102,13 @@ const defaultMessageState: UserMessageState = {
|
|||
|
||||
// a 'thread' means a chat message history
|
||||
|
||||
type WhenMounted = {
|
||||
textAreaRef: { current: HTMLTextAreaElement | null }; // the textarea that this thread has, gets set in SidebarChat
|
||||
scrollToBottom: () => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type ThreadType = {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
|
|
@ -120,6 +129,15 @@ export type ThreadType = {
|
|||
[codespanName: string]: CodespanLocationLink
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mountedInfo?: {
|
||||
whenMounted: Promise<WhenMounted>
|
||||
_whenMountedResolver: (res: WhenMounted) => void
|
||||
mountedIsResolvedRef: { current: boolean };
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +285,9 @@ export interface IChatThreadService {
|
|||
|
||||
// jump to history
|
||||
jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void;
|
||||
|
||||
focusCurrentChat: () => Promise<void>
|
||||
blurCurrentChat: () => Promise<void>
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
|
|
@ -300,6 +321,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
|
|
@ -333,6 +356,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
}
|
||||
|
||||
async focusCurrentChat() {
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
const s = await thread.state.mountedInfo?.whenMounted
|
||||
if (!this.isCurrentlyFocusingMessage()) {
|
||||
s?.textAreaRef.current?.focus()
|
||||
}
|
||||
}
|
||||
async blurCurrentChat() {
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
const s = await thread.state.mountedInfo?.whenMounted
|
||||
if (!this.isCurrentlyFocusingMessage()) {
|
||||
s?.textAreaRef.current?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
dangerousSetState = (newState: ThreadsState) => {
|
||||
this.state = newState
|
||||
|
|
@ -377,7 +420,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
private _setState(state: Partial<ThreadsState>, doNotRefreshMountInfo?: boolean) {
|
||||
const newState = {
|
||||
...this.state,
|
||||
...state
|
||||
|
|
@ -385,8 +428,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
this.state = newState
|
||||
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
|
||||
|
||||
// if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming
|
||||
|
|
@ -408,6 +450,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
}
|
||||
|
||||
|
||||
// if we did not just set the state to true, set mount info
|
||||
if (doNotRefreshMountInfo) return
|
||||
|
||||
let whenMountedResolver: (w: WhenMounted) => void
|
||||
const whenMountedPromise = new Promise<WhenMounted>((res) => whenMountedResolver = res)
|
||||
|
||||
this._setThreadState(threadId, {
|
||||
mountedInfo: {
|
||||
whenMounted: whenMountedPromise,
|
||||
mountedIsResolvedRef: { current: false },
|
||||
_whenMountedResolver: (w: WhenMounted) => {
|
||||
whenMountedResolver(w)
|
||||
const mountInfo = this.state.allThreads[threadId]?.state.mountedInfo
|
||||
if (mountInfo) mountInfo.mountedIsResolvedRef.current = true
|
||||
},
|
||||
}
|
||||
}, true) // do not trigger an update
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -724,8 +787,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) })
|
||||
const llmRes = await messageIsDonePromise // wait for message to complete
|
||||
|
||||
// if something else started running in the meantime
|
||||
if (this.streamState[threadId]?.isRunning !== 'LLM') {
|
||||
console.log('Unexpected chat agent state when', this.streamState[threadId]?.isRunning)
|
||||
// console.log('Chat thread interrupted by a newer chat thread', this.streamState[threadId]?.isRunning)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -823,7 +888,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1091,7 +1156,10 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
class: undefined,
|
||||
run: () => {
|
||||
this.switchToThread(threadId)
|
||||
// TODO!!! scroll to bottom
|
||||
// scroll to bottom
|
||||
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
|
||||
m.scrollToBottom()
|
||||
})
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
|
@ -1125,14 +1193,12 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
const { chatMode } = this._settingsService.state.globalSettings
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections
|
||||
const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const
|
||||
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content)
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns, { directoryStrService: this._directoryStringService, fileService: this._fileService }) // user message + names of files (NOT content)
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
|
|
@ -1142,6 +1208,11 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
|
||||
threadId,
|
||||
)
|
||||
|
||||
// scroll to bottom
|
||||
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
|
||||
m.scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1164,7 +1235,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
};
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ allThreads: newThreads }, true);
|
||||
this._setState({ allThreads: newThreads });
|
||||
}
|
||||
|
||||
// Now call the original method to add the user message and stream the response
|
||||
|
|
@ -1194,7 +1265,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
})
|
||||
|
||||
// re-add the message and stream it
|
||||
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
|
||||
|
|
@ -1467,7 +1538,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1498,7 +1569,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
this._setState({ currentThreadId: threadId }, true)
|
||||
this._setState({ currentThreadId: threadId })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1521,7 +1592,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1534,7 +1605,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
|
||||
// store the updated threads
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ ...this.state, allThreads: newThreads }, true)
|
||||
this._setState({ ...this.state, allThreads: newThreads })
|
||||
}
|
||||
|
||||
duplicateThread(threadId: string) {
|
||||
|
|
@ -1550,7 +1621,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
[newThread.id]: newThread,
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true)
|
||||
this._setState({ allThreads: newThreads })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1571,7 +1642,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
// sets the currently selected message (must be undefined if no message is selected)
|
||||
|
|
@ -1592,7 +1663,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
})
|
||||
|
||||
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
|
||||
// if (messageIdx !== undefined)
|
||||
|
|
@ -1680,12 +1751,12 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
)
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// set thread.state
|
||||
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>): void {
|
||||
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>, doNotRefreshMountInfo?: boolean): void {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
|
|
@ -1700,7 +1771,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
}, doNotRefreshMountInfo)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/p
|
|||
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
|
||||
import { IDirectoryStrService } from './directoryStrService.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
|
@ -38,7 +38,7 @@ type SimpleLLMMessage = {
|
|||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
|
||||
const TRIM_TO_LEN = 120
|
||||
|
||||
|
||||
|
|
@ -271,7 +271,10 @@ const prepareOpenAIOrAnthropicMessages = ({
|
|||
reservedOutputTokenSpace: number | null | undefined,
|
||||
}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => {
|
||||
|
||||
reservedOutputTokenSpace = reservedOutputTokenSpace ?? 4_096 // default to 4096
|
||||
reservedOutputTokenSpace = Math.max(
|
||||
contextWindow * 1 / 2, // reserve at least 1/4 of the token window length
|
||||
reservedOutputTokenSpace ?? 4_096 // defaults to 4096
|
||||
)
|
||||
let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_)
|
||||
|
||||
// ================ system message ================
|
||||
|
|
@ -337,7 +340,7 @@ const prepareOpenAIOrAnthropicMessages = ({
|
|||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - Math.max(
|
||||
(contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad
|
||||
4_096 // ensure we don't trim at least 4096 chars (just a random small value)
|
||||
5_000 // ensure we don't trim at least 5k chars (just a random small value)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -359,6 +362,7 @@ const prepareOpenAIOrAnthropicMessages = ({
|
|||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
// trim remainingCharsToTrim + '...'.length chars
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...'
|
||||
break
|
||||
}
|
||||
|
|
@ -498,19 +502,15 @@ const prepareMessages = (params: {
|
|||
providerName: ProviderName
|
||||
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
|
||||
|
||||
const specialFormat = params.specialToolFormat // this is just for ts idiocy
|
||||
if (params.providerName === 'gemini') {
|
||||
// treat as anthropic style, then convert to gemini style
|
||||
const specialFormat = params.specialToolFormat // this is just for ts stupidness
|
||||
|
||||
// if need to convert to gemini style of messaes, do that (treat as anthropic style, then convert to gemini style)
|
||||
if (params.providerName === 'gemini' || specialFormat === 'gemini-style') {
|
||||
const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined })
|
||||
const messages = res.messages as AnthropicLLMChatMessage[]
|
||||
const messages2 = prepareGeminiMessages(messages)
|
||||
return { messages: messages2, separateSystemMessage: res.separateSystemMessage }
|
||||
}
|
||||
else {
|
||||
if (specialFormat === 'gemini-style') {
|
||||
throw new Error(`Tried preparing messages with tool format ${params.specialToolFormat} but the provider was ${params.providerName}, not Gemini.`)
|
||||
}
|
||||
}
|
||||
|
||||
return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat })
|
||||
}
|
||||
|
|
@ -585,6 +585,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
`...Directories string cut off, use tools to read more...`
|
||||
: `...Directories string cut off, ask user for more if necessary...`
|
||||
})
|
||||
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
||||
|
|
|
|||
|
|
@ -269,7 +269,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
|
||||
public processRawKeybindingText(keybindingStr: string): string {
|
||||
return keybindingStr
|
||||
.replace(/Enter/g, '↵') // ⏎
|
||||
.replace(/Backspace/g, '⌫');
|
||||
}
|
||||
|
||||
// private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
// const details = errorDetails(e.fullError)
|
||||
|
|
@ -2255,13 +2259,6 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager);
|
|||
|
||||
|
||||
|
||||
const processRawKeybindingText = (keybindingStr: string) => {
|
||||
return keybindingStr
|
||||
.replace(/Enter/g, '↵') // ⏎
|
||||
.replace(/Backspace/g, '⌫')
|
||||
|
||||
}
|
||||
|
||||
class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
public getId(): string {
|
||||
|
|
@ -2289,7 +2286,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
|||
offsetLines: number
|
||||
},
|
||||
@IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IEditCodeService private readonly _editCodeService: IEditCodeService,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -2313,8 +2311,10 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
|
|||
const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID);
|
||||
const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID);
|
||||
|
||||
const acceptKeybindLabel = processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || '');
|
||||
const rejectKeybindLabel = processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '')
|
||||
// Use the standalone function directly since we're in a nested class that
|
||||
// can't access EditCodeService's methods
|
||||
const acceptKeybindLabel = this._editCodeService.processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || '');
|
||||
const rejectKeybindLabel = this._editCodeService.processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '');
|
||||
|
||||
const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath];
|
||||
const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
|
|||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
processRawKeybindingText(keybindingStr: string): string;
|
||||
|
||||
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
|
||||
|
|
|
|||
|
|
@ -158,7 +158,6 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
|||
}
|
||||
|
||||
return <Codespan
|
||||
// text={link?.displayText || text}
|
||||
text={link?.displayText || text}
|
||||
onClick={onClick}
|
||||
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useIsDark, useSidebarState } from '../util/services.js'
|
||||
import { useIsDark } from '../util/services.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { QuickEditChat } from './QuickEditChat.js'
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
import { useIsDark } from '../util/services.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
|
|
@ -12,8 +12,6 @@ import { SidebarChat } from './SidebarChat.js';
|
|||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
export const Sidebar = ({ className }: { className: string }) => {
|
||||
const sidebarState = useSidebarState()
|
||||
const { currentTab: tab } = sidebarState
|
||||
|
||||
const isDark = useIsDark()
|
||||
return <div
|
||||
|
|
@ -29,34 +27,12 @@ export const Sidebar = ({ className }: { className: string }) => {
|
|||
`}
|
||||
>
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
const index = tabs.indexOf(tab)
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<div className={`w-full h-full`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarChat />
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* <ErrorBoundary>
|
||||
<ModelSelectionSettings />
|
||||
</ErrorBoundary> */}
|
||||
</div>
|
||||
|
||||
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js';
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js';
|
||||
|
||||
import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
|
|
@ -14,7 +14,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
|||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { OldSidebarThreadSelector, PastThreadsList } from './SidebarThreadSelector.js';
|
||||
import { PastThreadsList } from './SidebarThreadSelector.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
|
|
@ -950,7 +950,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
|
|||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
// global state
|
||||
let isBeingEdited = false
|
||||
|
|
@ -1046,7 +1045,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
|
|||
} catch (e) {
|
||||
console.error('Error while editing message:', e)
|
||||
}
|
||||
sidebarStateService.fireFocusChat()
|
||||
await chatThreadsService.focusCurrentChat()
|
||||
requestAnimationFrame(() => _scrollToBottom?.())
|
||||
}
|
||||
|
||||
|
|
@ -1553,8 +1552,8 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
export const CodeChildren = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className='bg-void-bg-3 p-1 rounded-sm overflow-auto text-sm'>
|
||||
export const CodeChildren = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <div className={`${className ?? ''} p-1 rounded-sm overflow-auto text-sm`}>
|
||||
<div className='!select-text cursor-auto'>
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -1643,7 +1642,7 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin
|
|||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
|
||||
componentParams.children = <ToolChildrenWrapper>
|
||||
<CodeChildren>
|
||||
<CodeChildren className='bg-void-bg-3'>
|
||||
{message}
|
||||
</CodeChildren>
|
||||
</ToolChildrenWrapper>
|
||||
|
|
@ -2043,7 +2042,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
componentParams.numResults = result.lines.length;
|
||||
componentParams.children = result.lines.length === 0 ? undefined :
|
||||
<ToolChildrenWrapper>
|
||||
<CodeChildren>
|
||||
<CodeChildren className='bg-void-bg-3'>
|
||||
<pre className='font-mono whitespace-pre'>
|
||||
{toolsService.stringOfResult['search_in_file'](params, result)}
|
||||
</pre>
|
||||
|
|
@ -2177,7 +2176,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
|
||||
componentParams.children = componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
{result}
|
||||
</CodeChildren>
|
||||
|
|
@ -2420,53 +2419,6 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const AcceptAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => (
|
||||
<button
|
||||
className={`
|
||||
px-1 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
rounded-md
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: acceptAllBg,
|
||||
border: acceptBorder,
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
>
|
||||
{text ? <span>{text}</span> : <Check size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => (
|
||||
<button
|
||||
className={`
|
||||
px-1 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
rounded-md
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: rejectAllBg,
|
||||
border: rejectBorder,
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
>
|
||||
{text ? <span>{text}</span> : <X size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
|
||||
const CommandBarInChat = () => {
|
||||
const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const numFilesChanged = sortedCommandBarURIs.length
|
||||
|
|
@ -2771,18 +2723,6 @@ export const SidebarChat = () => {
|
|||
|
||||
const settingsState = useSettingsState()
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { !chatThreadsService.isCurrentlyFocusingMessage() && textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { !chatThreadsService.isCurrentlyFocusingMessage() && textAreaRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, textAreaRef])
|
||||
|
||||
const { isHistoryOpen } = useSidebarState()
|
||||
|
||||
// threads state
|
||||
const chatThreadsState = useChatThreadsState()
|
||||
|
|
@ -2841,16 +2781,24 @@ export const SidebarChat = () => {
|
|||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel()
|
||||
|
||||
// scroll to top on thread switch
|
||||
useEffect(() => {
|
||||
if (isHistoryOpen)
|
||||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
|
||||
const threadId = currentThread.id
|
||||
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity)
|
||||
|
||||
|
||||
|
||||
// resolve mount info
|
||||
const isResolved = chatThreadsState.allThreads[threadId]?.state.mountedInfo?.mountedIsResolvedRef.current
|
||||
useEffect(() => {
|
||||
if (isResolved) return
|
||||
chatThreadsState.allThreads[threadId]?.state.mountedInfo?._whenMountedResolver?.({
|
||||
textAreaRef: textAreaRef,
|
||||
scrollToBottom: () => scrollToBottom(scrollContainerRef),
|
||||
})
|
||||
}, [chatThreadsState, threadId, textAreaRef, scrollContainerRef, isResolved])
|
||||
|
||||
|
||||
|
||||
|
||||
const previousMessagesHTML = useMemo(() => {
|
||||
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||
// tool request shows up as Editing... if in progress
|
||||
|
|
@ -3020,7 +2968,7 @@ export const SidebarChat = () => {
|
|||
{landingPageInput}
|
||||
</ErrorBoundary>
|
||||
|
||||
{Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads
|
||||
{Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads
|
||||
<ErrorBoundary>
|
||||
<div className='pt-8 mb-2 text-void-fg-3 text-root select-none pointer-events-none'>Previous Threads</div>
|
||||
<PastThreadsList />
|
||||
|
|
|
|||
|
|
@ -11,138 +11,6 @@ import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserChe
|
|||
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
export const OldSidebarThreadSelector = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
return (
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
||||
|
||||
<div className="w-full relative flex justify-center items-center">
|
||||
{/* title */}
|
||||
<h2 className='font-bold text-lg'>{`History`}</h2>
|
||||
{/* X button at top right */}
|
||||
<button
|
||||
type='button'
|
||||
className='absolute top-0 right-0'
|
||||
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
{/* <OldPastThreadsList /> */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OldPastThreadsList = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
|
||||
return <div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false })
|
||||
}}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const numInitialThreads = 3
|
||||
|
||||
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
||||
|
|
@ -313,7 +181,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
|||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
// const settingsState = useSettingsState()
|
||||
// const convertService = accessor.get('IConvertToLLMMessageService')
|
||||
|
|
@ -369,7 +236,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
|||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false });
|
||||
}}
|
||||
onMouseEnter={() => setHoveredIdx(idx)}
|
||||
onMouseLeave={() => setHoveredIdx(null)}
|
||||
|
|
|
|||
|
|
@ -249,7 +249,6 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
|
|||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`;
|
||||
|
||||
console.log('filepath', currentPath);
|
||||
|
||||
// Create a proper directory URI
|
||||
const directoryUri = URI.joinPath(
|
||||
|
|
@ -383,8 +382,21 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
// Focus the textarea first
|
||||
textarea.focus();
|
||||
|
||||
// Insert the @ to mention text in the editor (we decided not to do this for now)
|
||||
// document.execCommand('insertText', false, text + ' '); // add space after too
|
||||
// delete the @ and set the cursor position
|
||||
// Get cursor position
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
|
||||
// Get the text before the cursor, excluding the @ symbol that triggered the menu
|
||||
const textBeforeCursor = textarea.value.substring(0, startPos - 1);
|
||||
const textAfterCursor = textarea.value.substring(endPos);
|
||||
|
||||
// Replace the text including the @ symbol with the selected option
|
||||
textarea.value = textBeforeCursor + textAfterCursor;
|
||||
|
||||
// Set cursor position after the inserted text
|
||||
const newCursorPos = textBeforeCursor.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
|
||||
// React's onChange relies on a SyntheticEvent system
|
||||
// The best way to ensure it runs is to call callbacks directly
|
||||
|
|
@ -407,17 +419,21 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(option.abbreviatedName)
|
||||
|
||||
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
|
||||
let newSelection: StagingSelectionItem
|
||||
if (option.leafNodeType === 'File') newSelection = {
|
||||
type: 'File',
|
||||
uri: option.uri,
|
||||
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : option.leafNodeType === 'Folder' ? {
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
}
|
||||
else if (option.leafNodeType === 'Folder') newSelection = {
|
||||
type: 'Folder',
|
||||
uri: option.uri,
|
||||
language: undefined,
|
||||
state: undefined,
|
||||
} : (undefined as never)
|
||||
}
|
||||
else throw new Error(`Unexpected leafNodeType ${option.leafNodeType}`)
|
||||
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
console.log('selected', option.uri?.fsPath)
|
||||
}
|
||||
|
|
@ -855,15 +871,44 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
|
|||
compact?: boolean;
|
||||
passwordBlur?: boolean;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) => {
|
||||
// Create a ref for the input element to maintain the same DOM node between renders
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Track if we need to restore selection
|
||||
const selectionRef = useRef<{ start: number | null, end: number | null }>({
|
||||
start: null,
|
||||
end: null
|
||||
});
|
||||
|
||||
// Handle value changes without recreating the input
|
||||
useEffect(() => {
|
||||
const input = inputRef.current;
|
||||
if (input && input.value !== value) {
|
||||
// Store current selection positions
|
||||
selectionRef.current.start = input.selectionStart;
|
||||
selectionRef.current.end = input.selectionEnd;
|
||||
|
||||
// Update the value
|
||||
input.value = value;
|
||||
|
||||
// Restore selection if we had it before
|
||||
if (selectionRef.current.start !== null && selectionRef.current.end !== null) {
|
||||
input.setSelectionRange(selectionRef.current.start, selectionRef.current.end);
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeValue(e.target.value);
|
||||
}, [onChangeValue]);
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeValue(e.target.value)}
|
||||
ref={inputRef}
|
||||
defaultValue={value} // Use defaultValue instead of value to avoid recreation
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
|
||||
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
|
||||
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
|
||||
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
|
||||
rounded
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
|
@ -23,7 +22,6 @@ import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js'
|
|||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
|
|
@ -58,9 +56,6 @@ import { ISearchService } from '../../../../../../services/search/common/search.
|
|||
// even if React hasn't mounted yet, the variables are always updated to the latest state.
|
||||
// React listens by adding a setState function to these listeners.
|
||||
|
||||
let sidebarState: VoidSidebarState
|
||||
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
|
||||
|
||||
let chatThreadsState: ThreadsState
|
||||
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
|
||||
|
|
@ -91,7 +86,6 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
_registerAccessor(accessor)
|
||||
|
||||
const stateServices = {
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
chatThreadsStateService: accessor.get(IChatThreadService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
|
|
@ -101,15 +95,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
modelService: accessor.get(IModelService),
|
||||
}
|
||||
|
||||
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
|
||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
|
||||
|
||||
|
||||
|
||||
sidebarState = sidebarStateService.state
|
||||
disposables.push(
|
||||
sidebarStateService.onDidChangeState(() => {
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateListeners.forEach(l => l(sidebarState))
|
||||
})
|
||||
)
|
||||
|
||||
chatThreadsState = chatThreadsStateService.state
|
||||
disposables.push(
|
||||
|
|
@ -193,7 +182,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IEditCodeService: accessor.get(IEditCodeService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IChatThreadService: accessor.get(IChatThreadService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
|
|
@ -250,16 +238,6 @@ export const useAccessor = () => {
|
|||
|
||||
// -- state of services --
|
||||
|
||||
export const useSidebarState = () => {
|
||||
const [s, ss] = useState(sidebarState)
|
||||
useEffect(() => {
|
||||
ss(sidebarState)
|
||||
sidebarStateListeners.add(ss)
|
||||
return () => { sidebarStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useSettingsState = () => {
|
||||
const [s, ss] = useState(settingsState)
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,19 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
|
|||
import '../styles.css'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
|
||||
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react';
|
||||
import {
|
||||
VOID_GOTO_NEXT_DIFF_ACTION_ID,
|
||||
VOID_GOTO_PREV_DIFF_ACTION_ID,
|
||||
VOID_GOTO_NEXT_URI_ACTION_ID,
|
||||
VOID_GOTO_PREV_URI_ACTION_ID,
|
||||
VOID_ACCEPT_FILE_ACTION_ID,
|
||||
VOID_REJECT_FILE_ACTION_ID,
|
||||
VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
|
||||
VOID_REJECT_ALL_DIFFS_ACTION_ID
|
||||
} from '../../../actionIDs.js';
|
||||
|
||||
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const isDark = useIsDark()
|
||||
|
|
@ -25,14 +35,55 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
|
||||
|
||||
|
||||
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
|
||||
if (len === 0) return null
|
||||
return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset
|
||||
}
|
||||
export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
className={`
|
||||
px-2 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
h-full rounded-none
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: 'var(--vscode-button-background)',
|
||||
color: 'var(--vscode-button-foreground)',
|
||||
border: 'none',
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{text ? <span>{text}</span> : <Check size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
export const RejectAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
className={`
|
||||
px-2 py-0.5
|
||||
flex items-center gap-1
|
||||
text-white text-[11px] text-nowrap
|
||||
h-full rounded-none
|
||||
cursor-pointer
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: 'var(--vscode-button-secondaryBackground)',
|
||||
color: 'var(--vscode-button-secondaryForeground)',
|
||||
border: 'none',
|
||||
}}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{text ? <span>{text}</span> : <X size={16} />}
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
|
||||
const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const editorService = accessor.get('ICodeEditorService')
|
||||
|
|
@ -40,12 +91,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
const commandService = accessor.get('ICommandService')
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const voidModelService = accessor.get('IVoidModelService')
|
||||
const keybindingService = accessor.get('IKeybindingService')
|
||||
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log('MOUNTING!!!')
|
||||
// }, [])
|
||||
const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false)
|
||||
|
||||
// latestUriIdx is used to remember place in leftRight
|
||||
const _latestValidUriIdxRef = useRef<number | null>(null)
|
||||
|
|
@ -70,56 +118,18 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
const s = commandBarService.stateOfURI[uri.fsPath]
|
||||
if (!s) return
|
||||
const { diffIdx } = s
|
||||
goToDiffIdx(diffIdx ?? 0)
|
||||
commandBarService.goToDiffIdx(diffIdx ?? 0)
|
||||
}, 50)
|
||||
}, [uri, commandBarService])
|
||||
|
||||
if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files
|
||||
|
||||
const getNextDiffIdx = (step: 1 | -1) => {
|
||||
// check undefined
|
||||
if (!uri) return null
|
||||
const s = commandBarState[uri.fsPath]
|
||||
if (!s) return null
|
||||
const { diffIdx, sortedDiffIds } = s
|
||||
// get next idx
|
||||
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
|
||||
return nextDiffIdx
|
||||
}
|
||||
const goToDiffIdx = (idx: number | null) => {
|
||||
if (idx === null) return
|
||||
// check undefined
|
||||
if (!uri) return
|
||||
const s = commandBarState[uri.fsPath]
|
||||
if (!s) return
|
||||
const { sortedDiffIds } = s
|
||||
// reveal
|
||||
const diffid = sortedDiffIds[idx]
|
||||
if (diffid === undefined) return
|
||||
const diff = editCodeService.diffOfId[diffid]
|
||||
if (!diff) return
|
||||
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
|
||||
commandBarService.setDiffIdx(uri, idx)
|
||||
}
|
||||
const getNextUriIdx = (step: 1 | -1) => {
|
||||
return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step)
|
||||
}
|
||||
const goToURIIdx = async (idx: number | null) => {
|
||||
if (idx === null) return
|
||||
const nextURI = sortedCommandBarURIs[idx]
|
||||
editCodeService.diffAreasOfURI
|
||||
const { model } = await voidModelService.getModelSafe(nextURI)
|
||||
if (model) {
|
||||
// switch to the URI
|
||||
editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor)
|
||||
}
|
||||
}
|
||||
// Using service methods directly
|
||||
|
||||
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
|
||||
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : []
|
||||
const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : []
|
||||
|
||||
|
||||
const isADiffInThisFile = sortedDiffIds.length !== 0
|
||||
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
|
||||
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
|
||||
|
|
@ -127,191 +137,247 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
|||
const streamState = uri ? commandBarService.getStreamState(uri) : null
|
||||
const showAcceptRejectAll = streamState === 'idle-has-changes'
|
||||
|
||||
const nextDiffIdx = getNextDiffIdx(1)
|
||||
const prevDiffIdx = getNextDiffIdx(-1)
|
||||
const nextURIIdx = getNextUriIdx(1)
|
||||
const prevURIIdx = getNextUriIdx(-1)
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1)
|
||||
const prevDiffIdx = commandBarService.getNextDiffIdx(-1)
|
||||
const nextURIIdx = commandBarService.getNextUriIdx(1)
|
||||
const prevURIIdx = commandBarService.getNextUriIdx(-1)
|
||||
|
||||
const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null
|
||||
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile)
|
||||
|
||||
const upButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}// --border border-void-border-3 focus:border-void-border-1
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => { goToDiffIdx(prevDiffIdx) }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToDiffIdx(prevDiffIdx);
|
||||
}
|
||||
}}
|
||||
>↑</button>
|
||||
|
||||
const downButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => { goToDiffIdx(nextDiffIdx) }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}}
|
||||
>↓</button>
|
||||
|
||||
const leftButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => goToURIIdx(prevURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToURIIdx(prevURIIdx);
|
||||
}
|
||||
}}
|
||||
>←</button>
|
||||
|
||||
const rightButton = <button
|
||||
className={`
|
||||
size-6 rounded cursor-default
|
||||
hover:bg-void-bg-1-alt
|
||||
`}
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => goToURIIdx(nextURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goToURIIdx(nextURIIdx);
|
||||
}
|
||||
}}
|
||||
>→</button>
|
||||
|
||||
|
||||
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null
|
||||
|
||||
// accept/reject if current URI has changes
|
||||
const onAcceptAll = () => {
|
||||
const onAcceptFile = () => {
|
||||
if (!uri) return
|
||||
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
|
||||
metricsService.capture('Accept All', {})
|
||||
metricsService.capture('Accept File', {})
|
||||
}
|
||||
const onRejectAll = () => {
|
||||
const onRejectFile = () => {
|
||||
if (!uri) return
|
||||
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
|
||||
metricsService.capture('Reject All', {})
|
||||
metricsService.capture('Reject File', {})
|
||||
}
|
||||
|
||||
const onAcceptAll = () => {
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
|
||||
metricsService.capture('Accept All', {})
|
||||
setShowAcceptRejectAllButtons(false);
|
||||
}
|
||||
|
||||
const onRejectAll = () => {
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
|
||||
metricsService.capture('Reject All', {})
|
||||
setShowAcceptRejectAllButtons(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const _upKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_DIFF_ACTION_ID);
|
||||
const _downKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_DIFF_ACTION_ID);
|
||||
const _leftKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_URI_ACTION_ID);
|
||||
const _rightKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_URI_ACTION_ID);
|
||||
const _acceptFileKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_FILE_ACTION_ID);
|
||||
const _rejectFileKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_FILE_ACTION_ID);
|
||||
const _acceptAllKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_ALL_DIFFS_ACTION_ID);
|
||||
const _rejectAllKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_ALL_DIFFS_ACTION_ID);
|
||||
|
||||
const upKeybindLabel = editCodeService.processRawKeybindingText(_upKeybinding?.getLabel() || '');
|
||||
const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || '');
|
||||
const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || '');
|
||||
const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || '');
|
||||
const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getAriaLabel() || '');
|
||||
const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getAriaLabel() || '');
|
||||
const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getAriaLabel() || '');
|
||||
const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getAriaLabel() || '');
|
||||
|
||||
|
||||
if (!isADiffZoneInAnyFile) return null
|
||||
|
||||
// const acceptAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onAcceptAll}
|
||||
// style={{
|
||||
// backgroundColor: acceptAllBg,
|
||||
// border: acceptBorder,
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Accept File
|
||||
// </button>
|
||||
|
||||
// const rejectAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onRejectAll}
|
||||
// style={{
|
||||
// backgroundColor: rejectBg,
|
||||
// border: rejectBorder,
|
||||
// color: 'white',
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Reject File
|
||||
// </button>
|
||||
|
||||
const acceptAllButton = <AcceptAllButtonWrapper
|
||||
text={'Keep Changes'}
|
||||
onClick={onAcceptAll}
|
||||
/>
|
||||
|
||||
const rejectAllButton = <RejectAllButtonWrapper
|
||||
text={'Reject All'}
|
||||
onClick={onRejectAll}
|
||||
/>
|
||||
|
||||
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
|
||||
{acceptAllButton}
|
||||
{rejectAllButton}
|
||||
</div>
|
||||
|
||||
// const closeCommandBar = useCallback(() => {
|
||||
// commandService.executeCommand('void.hideCommandBar');
|
||||
// }, [commandService]);
|
||||
|
||||
// const hideButton = <button
|
||||
// className='ml-auto pointer-events-auto'
|
||||
// onClick={closeCommandBar}
|
||||
// style={{
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// title="Close command bar"
|
||||
// >x
|
||||
// </button>
|
||||
|
||||
const leftRightUpDownButtons = <div className='p-1 gap-1 flex flex-col items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 w-full'>
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Changes in file */}
|
||||
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
|
||||
{upButton}
|
||||
{downButton}
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{isADiffInThisFile ?
|
||||
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming' ?
|
||||
'No changes yet'
|
||||
: `No changes`
|
||||
}
|
||||
</span>
|
||||
// For pages without a current file index, show a simplified command bar
|
||||
if (currFileIdx === null) {
|
||||
return (
|
||||
<div className="pointer-events-auto">
|
||||
<div className="flex bg-void-bg-2 shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
|
||||
<div className="flex items-center px-3">
|
||||
<span className="text-xs whitespace-nowrap">
|
||||
{`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-xs whitespace-nowrap cursor-pointer flex items-center justify-center gap-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] hover:opacity-90 h-full px-3"
|
||||
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToURIIdx(nextURIIdx);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next <MoveRight className='size-3 my-1' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Files */}
|
||||
<div className={`${!isADiffZoneInAnyFile ? 'hidden' : ''} flex items-center ${leftRightDisabled ? 'opacity-50' : ''}`}>
|
||||
{leftButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
{rightButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{currFileIdx !== null ?
|
||||
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
|
||||
}
|
||||
</span>
|
||||
return (
|
||||
<div className="pointer-events-auto">
|
||||
|
||||
|
||||
{/* Accept All / Reject All buttons that appear when the vertical ellipsis is clicked */}
|
||||
{showAcceptRejectAllButtons && showAcceptRejectAll && (
|
||||
<div className="flex justify-end mb-1">
|
||||
<div className="inline-flex bg-void-bg-2 rounded shadow-md border border-void-border-2 overflow-hidden">
|
||||
<div className="flex items-center [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
|
||||
<AcceptAllButtonWrapper
|
||||
// text={`Accept All${acceptAllKeybindLabel ? ` ${acceptAllKeybindLabel}` : ''}`}
|
||||
text={`Accept All`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={acceptAllKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onAcceptAll}
|
||||
/>
|
||||
<RejectAllButtonWrapper
|
||||
// text={`Reject All${rejectAllKeybindLabel ? ` ${rejectAllKeybindLabel}` : ''}`}
|
||||
text={`Reject All`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={rejectAllKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onRejectAll}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:px-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
|
||||
|
||||
{/* Diff Navigation Group */}
|
||||
<div className="flex items-center py-0.5">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => commandBarService.goToDiffIdx(prevDiffIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToDiffIdx(prevDiffIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${upKeybindLabel ? `${upKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveUp className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
<span className={`text-xs whitespace-nowrap px-1 ${!isADiffInThisFile ? 'opacity-70' : ''}`}>
|
||||
{isADiffInThisFile
|
||||
? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming'
|
||||
? 'No changes yet'
|
||||
: 'No changes'
|
||||
}
|
||||
|
||||
</span>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={upDownDisabled}
|
||||
onClick={() => commandBarService.goToDiffIdx(nextDiffIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${downKeybindLabel ? `${downKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveDown className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* File Navigation Group */}
|
||||
<div className="flex items-center py-0.5">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => commandBarService.goToURIIdx(prevURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToURIIdx(prevURIIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveLeft className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
<span className="text-xs whitespace-nowrap px-1 mx-0.5">
|
||||
{currFileIdx !== null
|
||||
? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`
|
||||
}
|
||||
</span>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
disabled={leftRightDisabled}
|
||||
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
commandBarService.goToURIIdx(nextURIIdx);
|
||||
}
|
||||
}}
|
||||
data-tooltip-id="void-tooltip"
|
||||
data-tooltip-content={`${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`}
|
||||
data-tooltip-delay-show={500}
|
||||
>
|
||||
<MoveRight className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Accept/Reject buttons - only shown when appropriate */}
|
||||
{showAcceptRejectAll && (
|
||||
<div className='flex self-stretch gap-0 !px-0 !py-0'>
|
||||
<AcceptAllButtonWrapper
|
||||
// text={`Accept File${acceptFileKeybindLabel ? ` ${acceptFileKeybindLabel}` : ''}`}
|
||||
text={`Accept File`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={acceptFileKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onAcceptFile}
|
||||
/>
|
||||
<RejectAllButtonWrapper
|
||||
// text={`Reject File${rejectFileKeybindLabel ? ` ${rejectFileKeybindLabel}` : ''}`}
|
||||
text={`Reject File`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={rejectFileKeybindLabel}
|
||||
data-tooltip-delay-show={500}
|
||||
onClick={onRejectFile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Triple colon menu button */}
|
||||
{showAcceptRejectAll && <div className='!px-0 !py-0 self-stretch flex justify-center items-center'>
|
||||
<div
|
||||
className="cursor-pointer px-1 self-stretch flex justify-center items-center"
|
||||
onClick={() => setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)}
|
||||
>
|
||||
<EllipsisVertical
|
||||
className="size-3"
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
|
||||
{showAcceptRejectAll && acceptRejectAllButtons}
|
||||
{leftRightUpDownButtons}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
|
||||
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
import { isLinux } from '../../../../../../../base/common/platform.js';
|
||||
|
|
@ -27,9 +26,10 @@ export const VoidOnboarding = () => {
|
|||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div
|
||||
className={`
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full z-[99999]
|
||||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<VoidOnboardingContent />
|
||||
|
|
@ -90,6 +90,180 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch
|
|||
}
|
||||
|
||||
// Onboarding
|
||||
|
||||
// =============================================
|
||||
// New AddProvidersPage Component and helpers
|
||||
// =============================================
|
||||
|
||||
const tabNames = ['Free', 'Paid', 'Local'] as const;
|
||||
|
||||
type TabName = typeof tabNames[number] | 'Cloud/Other';
|
||||
|
||||
// Data for cloud providers tab
|
||||
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible'];
|
||||
|
||||
// Data structures for provider tabs
|
||||
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
|
||||
Free: ['gemini', 'openRouter'],
|
||||
Local: localProviderNames,
|
||||
Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
|
||||
'Cloud/Other': cloudProviders,
|
||||
};
|
||||
|
||||
const descriptionOfTab: Record<TabName, string> = {
|
||||
Free: `Providers with a 100% free tier. Add as many as you'd like!`,
|
||||
Paid: `Connect directly with any provider (bring your own key).`,
|
||||
Local: `Add as many local providers as you'd like! Active providers should appear automatically.`,
|
||||
'Cloud/Other': `Reach out for custom configuration requests.`,
|
||||
};
|
||||
|
||||
|
||||
const featureNameMap: { display: string, featureName: FeatureName }[] = [
|
||||
{ display: 'Chat', featureName: 'Chat' },
|
||||
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
|
||||
{ display: 'Autocomplete', featureName: 'Autocomplete' },
|
||||
{ display: 'Fast Apply', featureName: 'Apply' },
|
||||
];
|
||||
|
||||
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
|
||||
const [currentTab, setCurrentTab] = useState<TabName>('Free');
|
||||
const settingsState = useSettingsState();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Clear error message after 5 seconds
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (errorMessage) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setErrorMessage(null);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Cleanup function to clear the timeout if component unmounts or error changes
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [errorMessage]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row w-full h-[80vh] gap-6 max-w-[900px] mx-auto relative">
|
||||
{/* Left Column - Fixed */}
|
||||
<div className="md:w-1/4 w-full flex flex-col gap-6 p-6 border-r border-void-border-2 h-full overflow-y-auto">
|
||||
{/* Tab Selector */}
|
||||
<div className="flex md:flex-col gap-2">
|
||||
{[...tabNames, 'Cloud/Other'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`py-2 px-4 rounded-md text-left ${currentTab === tab
|
||||
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
|
||||
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'
|
||||
} transition-all duration-200`}
|
||||
onClick={() => {
|
||||
setCurrentTab(tab as TabName);
|
||||
setErrorMessage(null); // Reset error message when changing tabs
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Feature Checklist */}
|
||||
<div className="flex flex-col gap-1 mt-4 text-sm opacity-80">
|
||||
{featureNameMap.map(({ display, featureName }) => {
|
||||
const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null;
|
||||
return (
|
||||
<div key={featureName} className="flex items-center gap-2">
|
||||
{hasModel ? (
|
||||
<Check className="w-4 h-4 text-emerald-500" />
|
||||
) : (
|
||||
<div className="w-3 h-3 rounded-full flex items-center justify-center">
|
||||
<div className="w-1 h-1 rounded-full bg-white/70"></div>
|
||||
</div>
|
||||
)}
|
||||
<span>{display}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="flex-1 flex flex-col items-center justify-start p-6 h-full overflow-y-auto">
|
||||
<div className="text-4xl font-light mb-2 text-center w-full">{currentTab}</div>
|
||||
<div className="text-sm text-void-fg-3 mb-2 text-center w-full max-w-lg">{descriptionOfTab[currentTab]}</div>
|
||||
|
||||
{providerNamesOfTab[currentTab].map((providerName) => (
|
||||
<div key={providerName} className="w-full max-w-xl mb-10">
|
||||
<div className="text-xl mb-2">Add {displayInfoOfProviderName(providerName).title}</div>
|
||||
<SettingsForProvider providerName={providerName} showProviderTitle={false} showProviderSuggestions={true} />
|
||||
{providerName === 'ollama' && <OllamaSetupInstructions />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(currentTab === 'Local' || currentTab === 'Cloud/Other') && (
|
||||
<div className="w-full max-w-xl mt-8 bg-void-bg-2/50 rounded-lg p-6 border border-void-border-2/30">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="text-xl font-medium text-[#0e70c0]">Models</div>
|
||||
<div className="h-px flex-grow bg-void-border-2/30"></div>
|
||||
</div>
|
||||
|
||||
{currentTab === 'Local' && (
|
||||
<div className="text-sm text-void-fg-3 mb-4 bg-void-bg-3/30 p-3 rounded border-l-2 border-[#0e70c0]/70">
|
||||
Local models should be detected automatically. You can add custom models below.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentTab === 'Local' && <ModelDump filteredProviders={localProviderNames} />}
|
||||
{currentTab === 'Cloud/Other' && <ModelDump filteredProviders={cloudProviders} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{currentTab === 'Free' && <div className='opacity-80'>
|
||||
|
||||
<div className="pl-2 flex flex-col gap-y-4 text-sm text-void-fg-3 mb-4 w-full">
|
||||
<ChatMarkdownRender string={`
|
||||
Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500.
|
||||
We recommend using models down the line as you run out of free credits. More information [here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits).
|
||||
`} chatMessageLocation={undefined} /></div>
|
||||
<div className="pl-2 flex flex-col gap-y-4 text-sm text-void-fg-3 mb-4 w-full">
|
||||
<ChatMarkdownRender string={`
|
||||
OpenRouter offers 50 free messages a day, and 1000 if you deposit $10.
|
||||
Only applies to models labeled \`:free\`. More information [here](https://openrouter.ai/docs/api-reference/limits).
|
||||
`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
}
|
||||
{/* Navigation buttons in right column */}
|
||||
<div className="flex flex-col items-end w-full mt-auto pt-8">
|
||||
{errorMessage && (
|
||||
<div className="text-amber-400 mb-2 text-sm opacity-80 transition-opacity duration-300">{errorMessage}</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviousButton onClick={() => setPageIndex(pageIndex - 1)} />
|
||||
<NextButton
|
||||
onClick={() => {
|
||||
const isDisabled = isFeatureNameDisabled('Chat', settingsState)
|
||||
|
||||
if (!isDisabled) {
|
||||
setPageIndex(pageIndex + 1);
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
// Show error message
|
||||
setErrorMessage("Please set up at least one Chat model before moving on.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// =============================================
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
|
|
@ -179,7 +353,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
|
|||
className?: string,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
<div className={`h-[80vh] text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
|
||||
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
|
||||
{bottom && <div className='w-full pb-8'>{bottom}</div>}
|
||||
|
|
@ -188,8 +362,6 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
|
|||
}
|
||||
|
||||
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
|
||||
|
||||
|
||||
// for now just link to the ollama download page
|
||||
return <a
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
|
|
@ -200,76 +372,6 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
|
|||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
// if (isModelInstalled) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Uninstall</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Trash}
|
||||
// onClick={() => {
|
||||
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// else if (isModelInstalling) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Square}
|
||||
// onClick={() => {
|
||||
// // abort()
|
||||
|
||||
// // TODO!!!!!!!!!!! don't do this
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
// else if (!isModelInstalled) {
|
||||
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Download}
|
||||
// onClick={() => {
|
||||
// // this is a check for whether the model was installed:
|
||||
|
||||
// if (isModelInstalling) return
|
||||
|
||||
|
||||
// // TODO!!!!!! don't do this
|
||||
|
||||
|
||||
// // install(modelname), callback = setIsModelInstalling(false);
|
||||
|
||||
// setIsModelInstalling(true);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
|
||||
// }
|
||||
|
||||
// return <></>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -306,114 +408,7 @@ const abbreviateNumber = (num: number): string => {
|
|||
}
|
||||
}
|
||||
|
||||
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
|
||||
// const providerCapabilities = getProviderCapabilities(providerName)
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.type !== 'custom',
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
||||
// special case columns for ollama; show recommended models as default
|
||||
if (providerName === 'ollama') {
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
|
||||
<thead>
|
||||
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
|
||||
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
|
||||
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
|
||||
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName, undefined)
|
||||
const {
|
||||
downloadable,
|
||||
cost,
|
||||
supportsFIM,
|
||||
reasoningCapabilities,
|
||||
contextWindow,
|
||||
|
||||
isUnrecognizedModel,
|
||||
reservedOutputTokenSpace,
|
||||
supportsSystemMessage,
|
||||
} = capabilities
|
||||
|
||||
// TODO update this when tools work
|
||||
|
||||
const removeModelButton = <button
|
||||
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
|
||||
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
</td>
|
||||
<td className="py-2 px-3">${cost.output ?? ''}</td>
|
||||
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
|
||||
<td className="py-2 px-3"><YesNoText val={true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
|
||||
{/* <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} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<ErrorBoundary>
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -431,22 +426,16 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil
|
|||
|
||||
${ringSize === 'xl' ? `
|
||||
gap-2 px-16 py-8
|
||||
hover:ring-8 active:ring-8
|
||||
transition-all duration-300 ease-in-out
|
||||
`
|
||||
: ringSize === 'screen' ? `
|
||||
gap-2 px-16 py-8
|
||||
ring-[3000px]
|
||||
transition-all duration-1000 ease-in-out
|
||||
`: ringSize === undefined ? `
|
||||
gap-1 px-4 py-2
|
||||
hover:ring-2 active:ring-2
|
||||
transition-all duration-300 ease-in-out
|
||||
`: ''}
|
||||
|
||||
hover:ring-black/90 dark:hover:ring-white/90
|
||||
active:ring-black/90 dark:active:ring-white/90
|
||||
|
||||
rounded-lg
|
||||
group
|
||||
${className}
|
||||
|
|
@ -534,7 +523,6 @@ const VoidOnboardingContent = () => {
|
|||
/>
|
||||
<NextButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -612,7 +600,7 @@ const VoidOnboardingContent = () => {
|
|||
delayMs={1000}
|
||||
>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
onClick={() => { setPageIndex(1) }}
|
||||
>
|
||||
Get Started
|
||||
</PrimaryActionButton>
|
||||
|
|
@ -621,255 +609,13 @@ const VoidOnboardingContent = () => {
|
|||
</div>
|
||||
}
|
||||
/>,
|
||||
1: <OnboardingPageShell
|
||||
|
||||
hasMaxWidth={false}
|
||||
top={<></>}
|
||||
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
|
||||
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
|
||||
|
||||
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Lock size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Private</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>}
|
||||
bottom={
|
||||
<div className='mx-auto w-full max-w-[800px]'>
|
||||
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
|
||||
</div>
|
||||
1: <OnboardingPageShell hasMaxWidth={false}
|
||||
content={
|
||||
<AddProvidersPage pageIndex={pageIndex} setPageIndex={setPageIndex} />
|
||||
}
|
||||
/>,
|
||||
2: <OnboardingPageShell
|
||||
top={
|
||||
<>
|
||||
{/* Title */}
|
||||
|
||||
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
|
||||
|
||||
{/* Preference Selector */}
|
||||
|
||||
<div
|
||||
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
|
||||
>
|
||||
{[
|
||||
{ id: 'smart', label: 'Intelligent' },
|
||||
{ id: 'private', label: 'Private' },
|
||||
{ id: 'cheap', label: 'Affordable' },
|
||||
{ id: 'all', label: 'All' }
|
||||
].map(option => (
|
||||
<ErrorBoundary
|
||||
key={option.id}
|
||||
>
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
{/* Provider Buttons - Modified to use separate components for each tab */}
|
||||
<div className="mb-2 w-full">
|
||||
{/* Intelligent tab */}
|
||||
<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>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Private tab */}
|
||||
<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>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Affordable tab */}
|
||||
<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>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* All tab */}
|
||||
<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>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<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 */}
|
||||
<ErrorBoundary>
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
|
||||
<div className='mb-5 mt-8 mx-auto'>
|
||||
<ErrorBoundary>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
<div className='my-4'>
|
||||
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary>
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Button and status indicators */}
|
||||
<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>}
|
||||
</>
|
||||
}
|
||||
|
||||
bottom={
|
||||
<ErrorBoundary>
|
||||
<FadeIn delayMs={50} durationMs={10}>
|
||||
{prevAndNextButtons}
|
||||
</FadeIn>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
/>,
|
||||
|
||||
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
// <FadeIn>
|
||||
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
|
||||
|
||||
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
|
||||
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
|
||||
|
||||
// </div>
|
||||
// </FadeIn>
|
||||
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <OnboardingPageShell
|
||||
|
||||
content={
|
||||
<div>
|
||||
|
|
@ -884,32 +630,11 @@ const VoidOnboardingContent = () => {
|
|||
</div>
|
||||
}
|
||||
bottom={lastPagePrevAndNextButtons}
|
||||
// bottom={prevAndNextButtons}
|
||||
/>,
|
||||
// 4: <OnboardingPageShell
|
||||
// content={
|
||||
// <>
|
||||
// <div
|
||||
// className='flex justify-center'
|
||||
// >
|
||||
// <PrimaryActionButton
|
||||
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
// className='text-4xl'
|
||||
// >Enter the Void</PrimaryActionButton>
|
||||
// </div>
|
||||
// </>
|
||||
// }
|
||||
// bottom={
|
||||
// <PreviousButton
|
||||
// onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
// />
|
||||
// }
|
||||
// />,
|
||||
}
|
||||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
|
||||
return <div key={pageIndex} className="w-full h-[80vh] text-left mx-auto flex flex-col items-center justify-center">
|
||||
<ErrorBoundary>
|
||||
{contentOfIdx[pageIndex]}
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -361,127 +361,9 @@ const SimpleModelSettingsDialog = ({
|
|||
};
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCheckmark, setShowCheckmark] = useState(false)
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const providerName = permanentProviderName ?? userChosenProviderName;
|
||||
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
|
||||
|
||||
if (showCheckmark) {
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return <div
|
||||
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
|
||||
>
|
||||
<div>
|
||||
{numModels > 0 ? `Add a different model?` : `Add a model`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
return <>
|
||||
<form className={`flex items-center gap-2 ${className}`}>
|
||||
|
||||
{/* X button
|
||||
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
|
||||
|
||||
{/* provider input */}
|
||||
<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 */}
|
||||
<ErrorBoundary>
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* add button */}
|
||||
<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('')
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap mt-1'>
|
||||
{errorString}
|
||||
</div>}
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderName[] }) => {
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
const settingsState = useSettingsState()
|
||||
|
|
@ -493,9 +375,20 @@ export const ModelDump = () => {
|
|||
type: 'autodetected' | 'custom' | 'default'
|
||||
} | null>(null);
|
||||
|
||||
// States for add model functionality
|
||||
const [isAddModelOpen, setIsAddModelOpen] = useState(false);
|
||||
const [showCheckmark, setShowCheckmark] = useState(false);
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null);
|
||||
const [modelName, setModelName] = useState<string>('');
|
||||
const [errorString, setErrorString] = useState('');
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
for (let providerName of providerNames) {
|
||||
|
||||
// Use either filtered providers or all providers
|
||||
const providersToShow = filteredProviders || providerNames;
|
||||
|
||||
for (let providerName of providersToShow) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
|
||||
|
|
@ -506,6 +399,34 @@ export const ModelDump = () => {
|
|||
return Number(b.providerEnabled) - Number(a.providerEnabled)
|
||||
})
|
||||
|
||||
// Add model handler
|
||||
const handleAddModel = () => {
|
||||
if (!userChosenProviderName) {
|
||||
setErrorString('Please select a provider.');
|
||||
return;
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if model already exists
|
||||
if (settingsState.settingsOfProvider[userChosenProviderName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
settingsStateService.addModel(userChosenProviderName, modelName);
|
||||
setShowCheckmark(true);
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false);
|
||||
setIsAddModelOpen(false);
|
||||
setUserChosenProviderName(null);
|
||||
setModelName('');
|
||||
}, 1500);
|
||||
setErrorString('');
|
||||
};
|
||||
|
||||
return <div className=''>
|
||||
{modelDump.map((m, i) => {
|
||||
const { isHidden, type, modelName, providerName, providerEnabled } = m
|
||||
|
|
@ -584,6 +505,82 @@ export const ModelDump = () => {
|
|||
</div>
|
||||
})}
|
||||
|
||||
{/* Add Model Section */}
|
||||
{showCheckmark ? (
|
||||
<div className="mt-4">
|
||||
<AnimatedCheckmarkButton text='Added' className="bg-[#0e70c0] text-white px-3 py-1 rounded-sm" />
|
||||
</div>
|
||||
) : isAddModelOpen ? (
|
||||
<div className="mt-4">
|
||||
<form className="flex items-center gap-2">
|
||||
|
||||
{/* Provider dropdown */}
|
||||
<ErrorBoundary>
|
||||
<VoidCustomDropdownBox
|
||||
options={providersToShow}
|
||||
selectedOption={userChosenProviderName}
|
||||
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-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 name input */}
|
||||
<ErrorBoundary>
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
compact={true}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
className='max-w-32'
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Add button */}
|
||||
<ErrorBoundary>
|
||||
<AddButton
|
||||
type='button'
|
||||
disabled={!modelName || !userChosenProviderName}
|
||||
onClick={handleAddModel}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* X button to cancel */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsAddModelOpen(false);
|
||||
setErrorString('');
|
||||
setModelName('');
|
||||
setUserChosenProviderName(null);
|
||||
}}
|
||||
className='text-void-fg-4'
|
||||
>
|
||||
<X className='size-4' />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{errorString && (
|
||||
<div className='text-red-500 truncate whitespace-nowrap mt-1'>
|
||||
{errorString}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer mt-4"
|
||||
onClick={() => setIsAddModelOpen(true)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} />
|
||||
<span>Add a model</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model Settings Dialog */}
|
||||
<SimpleModelSettingsDialog
|
||||
isOpen={openSettingsModel !== null}
|
||||
|
|
@ -610,15 +607,17 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam
|
|||
console.log('Error: Provider setting had a non-string value.')
|
||||
return
|
||||
}
|
||||
|
||||
// Create a stable callback reference using useCallback with proper dependencies
|
||||
const handleChangeValue = useCallback((newVal: string) => {
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
}, [voidSettingsService, providerName, settingName]);
|
||||
|
||||
return <ErrorBoundary>
|
||||
<div className='my-1'>
|
||||
<VoidSimpleInputBox
|
||||
value={settingValue}
|
||||
onChangeValue={useCallback((newVal) => {
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
onChangeValue={handleChangeValue}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
passwordBlur={isPasswordField}
|
||||
compact={true}
|
||||
|
|
@ -720,8 +719,8 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
|
|||
|
||||
{showProviderSuggestions && needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox className="mt-1" text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox className="mt-1" text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
<WarningBox className="pl-2 mb-4" text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox className="pl-2 mb-4" text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
: null}
|
||||
</div>
|
||||
</div >
|
||||
|
|
@ -804,7 +803,7 @@ const FastApplyMethodDropdown = () => {
|
|||
}
|
||||
|
||||
|
||||
export const OllamaSetupInstructions = () => {
|
||||
export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?: boolean }) => {
|
||||
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>
|
||||
|
|
@ -815,7 +814,7 @@ export const OllamaSetupInstructions = () => {
|
|||
>
|
||||
<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>
|
||||
{sayWeAutoDetect && <div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
@ -1176,17 +1175,20 @@ export const Settings = () => {
|
|||
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<div className='w-full h-[1px] my-2' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<RedoOnboardingButton />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ModelDump />
|
||||
<AddModelInputBox className='mt-4' compact />
|
||||
<RedoOnboardingButton className='mt-2 mb-4' />
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
|
|
@ -1196,7 +1198,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 sayWeAutoDetect={true} />
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -112,11 +112,14 @@ export const VoidTooltip = () => {
|
|||
</div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>gemma3</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
|
||||
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
<span style={{ opacity: 0.8 }}>Use the largest version of these you can!</span>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -14,23 +14,18 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
|
|||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
|
||||
import { IChatThreadService } from './chatThreadService.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
return null
|
||||
|
|
@ -72,68 +67,21 @@ registerAction2(class extends Action2 {
|
|||
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
const chatThreadsService = accessor.get(IChatThreadService)
|
||||
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID)
|
||||
await chatThreadsService.focusCurrentChat()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
|
||||
if (!model)
|
||||
return
|
||||
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
|
||||
// select whole lines
|
||||
if (selectionRange) {
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
}
|
||||
|
||||
|
||||
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : {
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
}
|
||||
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// cmd L
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
|
||||
title: localize2('voidCmdL', 'Void: Add Selection to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
|
|
@ -141,72 +89,115 @@ registerAction2(class extends Action2 {
|
|||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
|
||||
// Get the views service to check if the sidebar is open
|
||||
// const viewsService = accessor.get(IViewsService)
|
||||
const commandService = accessor.get(ICommandService)
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
// await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
|
||||
}
|
||||
|
||||
|
||||
// New chat menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.newChatAction',
|
||||
title: 'New Chat',
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
||||
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
|
||||
if (!wasAlreadyOpen) {
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// if was already open
|
||||
|
||||
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
|
||||
if (!model) return
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
// if has no selection, close + return
|
||||
if (!selectionRange) {
|
||||
viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// if has selection, add it
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
chatThreadService.addNewStagingSelection({
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
|
||||
await chatThreadService.focusCurrentChat()
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// New chat keybind
|
||||
|
||||
// New chat keybind + menu button
|
||||
const VOID_CMD_SHIFT_L_ACTION_ID = 'void.cmdShiftL'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.newChatKeybindAction',
|
||||
title: 'New Chat Keybind',
|
||||
id: VOID_CMD_SHIFT_L_ACTION_ID,
|
||||
title: 'New Chat',
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
},
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const commandService = accessor.get(ICommandService)
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
|
||||
const chatThreadsService = accessor.get(IChatThreadService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
metricsService.capture('Chat Navigation', { type: 'Start New Chat' })
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
// get current selections and value to transfer
|
||||
const oldThreadId = chatThreadsService.state.currentThreadId
|
||||
const oldThread = chatThreadsService.state.allThreads[oldThreadId]
|
||||
|
||||
// add user's selection to chat
|
||||
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
|
||||
|
||||
const oldSelns = oldThread?.state.stagingSelections
|
||||
const oldVal = oldUI?.textAreaRef.current?.value
|
||||
|
||||
// open and focus new thread
|
||||
chatThreadsService.openNewThread()
|
||||
await chatThreadsService.focusCurrentChat()
|
||||
|
||||
|
||||
// set new thread values
|
||||
const newThreadId = chatThreadsService.state.currentThreadId
|
||||
const newThread = chatThreadsService.state.allThreads[newThreadId]
|
||||
|
||||
const newUI = await newThread?.state.mountedInfo?.whenMounted
|
||||
chatThreadsService.setCurrentThreadState({ stagingSelections: oldSelns, })
|
||||
if (newUI?.textAreaRef?.current && oldVal) newUI.textAreaRef.current.value = oldVal
|
||||
|
||||
|
||||
// if has selection, add it
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const model = editor?.getModel()
|
||||
if (!model) return
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
if (!selectionRange) return
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
chatThreadsService.addNewStagingSelection({
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -229,18 +220,12 @@ registerAction2(class extends Action2 {
|
|||
return;
|
||||
}
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
const commandService = accessor.get(ICommandService)
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'History' })
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
// doesnt do anything right now
|
||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||
stateService.fireBlurChat()
|
||||
|
||||
commandService.executeCommand(VOID_CMD_SHIFT_L_ACTION_ID)
|
||||
|
||||
}
|
||||
})
|
||||
|
|
@ -265,28 +250,28 @@ registerAction2(class extends Action2 {
|
|||
|
||||
|
||||
|
||||
export class TabSwitchListener extends Disposable {
|
||||
// export class TabSwitchListener extends Disposable {
|
||||
|
||||
constructor(
|
||||
onSwitchTab: () => void,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
// constructor(
|
||||
// onSwitchTab: () => void,
|
||||
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
// ) {
|
||||
// super()
|
||||
|
||||
// when editor switches tabs (models)
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidChangeModel(e => {
|
||||
if (e.newModelUrl?.scheme !== 'file') return
|
||||
onSwitchTab()
|
||||
}))
|
||||
}
|
||||
// // when editor switches tabs (models)
|
||||
// const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
// this._register(editor.onDidChangeModel(e => {
|
||||
// if (e.newModelUrl?.scheme !== 'file') return
|
||||
// onSwitchTab()
|
||||
// }))
|
||||
// }
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
addTabSwitchListeners(editor)
|
||||
}
|
||||
// const initializeEditor = (editor: ICodeEditor) => {
|
||||
// addTabSwitchListeners(editor)
|
||||
// }
|
||||
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
}
|
||||
}
|
||||
// // initialize current editors + any new editors
|
||||
// for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
||||
|
||||
|
||||
// service that manages sidebar's state
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean; // this isn't doing anything right now
|
||||
currentTab: 'chat';
|
||||
}
|
||||
|
||||
export interface ISidebarStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidSidebarState; // readonly to the user
|
||||
setState(newState: Partial<VoidSidebarState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
}
|
||||
|
||||
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
|
||||
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidSidebarStateService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidSidebarState
|
||||
|
||||
constructor(
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { isHistoryOpen: false, currentTab: 'chat', }
|
||||
}
|
||||
|
||||
|
||||
setState(newState: Partial<VoidSidebarState>) {
|
||||
// make sure view is open if the tab changes
|
||||
if ('currentTab' in newState) {
|
||||
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
|
||||
|
|
@ -12,7 +12,7 @@ import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsSe
|
|||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from '../common/directoryStrService.js'
|
||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import './editCodeService.js'
|
|||
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
|
||||
import './sidebarActions.js'
|
||||
import './sidebarPane.js'
|
||||
import './sidebarStateService.js'
|
||||
|
||||
// register quick edit (Ctrl+K)
|
||||
import './quickEditActions.js'
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ import { ITextModel } from '../../../../editor/common/model.js';
|
|||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js';
|
||||
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID, VOID_GOTO_NEXT_DIFF_ACTION_ID, VOID_GOTO_PREV_DIFF_ACTION_ID, VOID_GOTO_NEXT_URI_ACTION_ID, VOID_GOTO_PREV_URI_ACTION_ID, VOID_ACCEPT_FILE_ACTION_ID, VOID_REJECT_FILE_ACTION_ID, VOID_ACCEPT_ALL_DIFFS_ACTION_ID, VOID_REJECT_ALL_DIFFS_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { KeyMod } from '../../../../editor/common/services/editorBaseApi.js';
|
||||
import { KeyCode } from '../../../../base/common/keyCodes.js';
|
||||
import { ScrollType } from '../../../../editor/common/editorCommon.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -41,6 +43,11 @@ export interface IVoidCommandBarService {
|
|||
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
|
||||
setDiffIdx(uri: URI, newIdx: number | null): void;
|
||||
|
||||
getNextDiffIdx(step: 1 | -1): number | null;
|
||||
getNextUriIdx(step: 1 | -1): number | null;
|
||||
goToDiffIdx(idx: number | null): void;
|
||||
goToURIIdx(idx: number | null): Promise<void>;
|
||||
|
||||
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void;
|
||||
anyFileIsStreaming(): boolean;
|
||||
|
||||
|
|
@ -93,6 +100,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
|||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IEditCodeService private readonly _editCodeService: IEditCodeService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -374,6 +382,99 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
|||
return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming')
|
||||
}
|
||||
|
||||
getNextDiffIdx(step: 1 | -1): number | null {
|
||||
// If no active URI, return null
|
||||
if (!this.activeURI) return null;
|
||||
|
||||
const state = this.stateOfURI[this.activeURI.fsPath];
|
||||
if (!state) return null;
|
||||
|
||||
const { diffIdx, sortedDiffIds } = state;
|
||||
|
||||
// If no diffs, return null
|
||||
if (sortedDiffIds.length === 0) return null;
|
||||
|
||||
// Calculate next index with wrapping
|
||||
const nextIdx = ((diffIdx ?? 0) + step + sortedDiffIds.length) % sortedDiffIds.length;
|
||||
return nextIdx;
|
||||
}
|
||||
|
||||
getNextUriIdx(step: 1 | -1): number | null {
|
||||
// If no URIs with changes, return null
|
||||
if (this.sortedURIs.length === 0) return null;
|
||||
|
||||
// If no active URI, return first or last based on step
|
||||
if (!this.activeURI) {
|
||||
return step === 1 ? 0 : this.sortedURIs.length - 1;
|
||||
}
|
||||
|
||||
// Find current index
|
||||
const currentIdx = this.sortedURIs.findIndex(uri => uri.fsPath === this.activeURI?.fsPath);
|
||||
|
||||
// If not found, return first or last based on step
|
||||
if (currentIdx === -1) {
|
||||
return step === 1 ? 0 : this.sortedURIs.length - 1;
|
||||
}
|
||||
|
||||
// Calculate next index with wrapping
|
||||
const nextIdx = (currentIdx + step + this.sortedURIs.length) % this.sortedURIs.length;
|
||||
return nextIdx;
|
||||
}
|
||||
|
||||
goToDiffIdx(idx: number | null): void {
|
||||
// If null or no active URI, return
|
||||
if (idx === null || !this.activeURI) return;
|
||||
|
||||
// Get state for the current URI
|
||||
const state = this.stateOfURI[this.activeURI.fsPath];
|
||||
if (!state) return;
|
||||
|
||||
const { sortedDiffIds } = state;
|
||||
|
||||
// Find the diff at the specified index
|
||||
const diffid = sortedDiffIds[idx];
|
||||
if (diffid === undefined) return;
|
||||
|
||||
// Get the diff object
|
||||
const diff = this._editCodeService.diffOfId[diffid];
|
||||
if (!diff) return;
|
||||
|
||||
// Find an active editor to focus
|
||||
const editor = this._codeEditorService.getFocusedCodeEditor() ||
|
||||
this._codeEditorService.getActiveCodeEditor();
|
||||
if (!editor) return;
|
||||
|
||||
// Reveal the line in the editor
|
||||
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate);
|
||||
|
||||
// Update the current diff index
|
||||
this.setDiffIdx(this.activeURI, idx);
|
||||
}
|
||||
|
||||
async goToURIIdx(idx: number | null): Promise<void> {
|
||||
// If null or no URIs, return
|
||||
if (idx === null || this.sortedURIs.length === 0) return;
|
||||
|
||||
// Get the URI at the specified index
|
||||
const nextURI = this.sortedURIs[idx];
|
||||
if (!nextURI) return;
|
||||
|
||||
// Get the model for this URI
|
||||
const { model } = await this._voidModelService.getModelSafe(nextURI);
|
||||
if (!model) return;
|
||||
|
||||
// Find an editor to use
|
||||
const editor = this._codeEditorService.getFocusedCodeEditor() ||
|
||||
this._codeEditorService.getActiveCodeEditor();
|
||||
if (!editor) return;
|
||||
|
||||
// Open the URI in the editor
|
||||
await this._codeEditorService.openCodeEditor(
|
||||
{ resource: model.uri, options: { revealIfVisible: true } },
|
||||
editor
|
||||
);
|
||||
}
|
||||
|
||||
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) {
|
||||
const { behavior } = opts
|
||||
// if anything is streaming, do nothing
|
||||
|
|
@ -497,8 +598,13 @@ registerAction2(class extends Action2 {
|
|||
if (!diffid) return;
|
||||
|
||||
metricsService.capture('Accept Diff', { diffid, keyboard: true });
|
||||
editCodeService.acceptDiff({ diffid: parseInt(diffid) })
|
||||
editCodeService.acceptDiff({ diffid: parseInt(diffid) });
|
||||
|
||||
// After accepting the diff, navigate to the next diff
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
|
||||
if (nextDiffIdx !== null) {
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -534,6 +640,232 @@ registerAction2(class extends Action2 {
|
|||
if (!diffid) return;
|
||||
|
||||
metricsService.capture('Reject Diff', { diffid, keyboard: true });
|
||||
editCodeService.rejectDiff({ diffid: parseInt(diffid) })
|
||||
editCodeService.rejectDiff({ diffid: parseInt(diffid) });
|
||||
|
||||
// After rejecting the diff, navigate to the next diff
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
|
||||
if (nextDiffIdx !== null) {
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Go to next diff action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_NEXT_DIFF_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToNextDiffAction', 'Void: Go to Next Diff'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.DownArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
|
||||
if (nextDiffIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate Diff', { direction: 'next', keyboard: true });
|
||||
commandBarService.goToDiffIdx(nextDiffIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to previous diff action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_PREV_DIFF_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToPrevDiffAction', 'Void: Go to Previous Diff'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.UpArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const prevDiffIdx = commandBarService.getNextDiffIdx(-1);
|
||||
if (prevDiffIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate Diff', { direction: 'previous', keyboard: true });
|
||||
commandBarService.goToDiffIdx(prevDiffIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to next URI action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_NEXT_URI_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToNextUriAction', 'Void: Go to Next File with Diffs'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const nextUriIdx = commandBarService.getNextUriIdx(1);
|
||||
if (nextUriIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate URI', { direction: 'next', keyboard: true });
|
||||
await commandBarService.goToURIIdx(nextUriIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to previous URI action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_GOTO_PREV_URI_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidGoToPrevUriAction', 'Void: Go to Previous File with Diffs'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow },
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const prevUriIdx = commandBarService.getNextUriIdx(-1);
|
||||
if (prevUriIdx === null) return;
|
||||
|
||||
metricsService.capture('Navigate URI', { direction: 'previous', keyboard: true });
|
||||
await commandBarService.goToURIIdx(prevUriIdx);
|
||||
}
|
||||
});
|
||||
|
||||
// Accept current file action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_ACCEPT_FILE_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidAcceptFileAction', 'Void: Accept All Diffs in Current File'),
|
||||
keybinding: {
|
||||
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const editCodeService = accessor.get(IEditCodeService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const activeURI = commandBarService.activeURI;
|
||||
if (!activeURI) return;
|
||||
|
||||
metricsService.capture('Accept File', { keyboard: true });
|
||||
editCodeService.acceptOrRejectAllDiffAreas({
|
||||
uri: activeURI,
|
||||
behavior: 'accept',
|
||||
removeCtrlKs: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reject current file action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_REJECT_FILE_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidRejectFileAction', 'Void: Reject All Diffs in Current File'),
|
||||
keybinding: {
|
||||
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const editCodeService = accessor.get(IEditCodeService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
const activeURI = commandBarService.activeURI;
|
||||
if (!activeURI) return;
|
||||
|
||||
metricsService.capture('Reject File', { keyboard: true });
|
||||
editCodeService.acceptOrRejectAllDiffAreas({
|
||||
uri: activeURI,
|
||||
behavior: 'reject',
|
||||
removeCtrlKs: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Accept all diffs in all files action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidAcceptAllDiffsAction', 'Void: Accept All Diffs in All Files'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
if (commandBarService.anyFileIsStreaming()) return;
|
||||
|
||||
metricsService.capture('Accept All Files', { keyboard: true });
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reject all diffs in all files action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_REJECT_ALL_DIFFS_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidRejectAllDiffsAction', 'Void: Reject All Diffs in All Files'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandBarService = accessor.get(IVoidCommandBarService);
|
||||
const metricsService = accessor.get(IMetricsService);
|
||||
|
||||
if (commandBarService.anyFileIsStreaming()) return;
|
||||
|
||||
metricsService.capture('Reject All Files', { keyboard: true });
|
||||
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,13 +7,10 @@ import { URI } from '../../../../base/common/uri.js';
|
|||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
|
||||
|
||||
|
||||
const MAX_FILES_TOTAL = 1000;
|
||||
|
|
@ -28,8 +25,10 @@ const DEFAULT_MAX_ITEMS_PER_DIR = 3;
|
|||
export interface IDirectoryStrService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
|
||||
getDirectoryStrTool(uri: URI): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
|
||||
|
||||
getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
|
|
@ -38,35 +37,35 @@ export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidD
|
|||
|
||||
|
||||
// Check if it's a known filtered type like .git
|
||||
const shouldExcludeDirectory = (item: ExplorerItem) => {
|
||||
if (item.name === '.git' ||
|
||||
item.name === 'node_modules' ||
|
||||
item.name.startsWith('.') ||
|
||||
item.name === 'dist' ||
|
||||
item.name === 'build' ||
|
||||
item.name === 'out' ||
|
||||
item.name === 'bin' ||
|
||||
item.name === 'coverage' ||
|
||||
item.name === '__pycache__' ||
|
||||
item.name === 'env' ||
|
||||
item.name === 'venv' ||
|
||||
item.name === 'tmp' ||
|
||||
item.name === 'temp' ||
|
||||
item.name === 'artifacts' ||
|
||||
item.name === 'target' ||
|
||||
item.name === 'obj' ||
|
||||
item.name === 'vendor' ||
|
||||
item.name === 'logs' ||
|
||||
item.name === 'cache' ||
|
||||
item.name === 'resource' ||
|
||||
item.name === 'resources'
|
||||
const shouldExcludeDirectory = (name: string) => {
|
||||
if (name === '.git' ||
|
||||
name === 'node_modules' ||
|
||||
name.startsWith('.') ||
|
||||
name === 'dist' ||
|
||||
name === 'build' ||
|
||||
name === 'out' ||
|
||||
name === 'bin' ||
|
||||
name === 'coverage' ||
|
||||
name === '__pycache__' ||
|
||||
name === 'env' ||
|
||||
name === 'venv' ||
|
||||
name === 'tmp' ||
|
||||
name === 'temp' ||
|
||||
name === 'artifacts' ||
|
||||
name === 'target' ||
|
||||
name === 'obj' ||
|
||||
name === 'vendor' ||
|
||||
name === 'logs' ||
|
||||
name === 'cache' ||
|
||||
name === 'resource' ||
|
||||
name === 'resources'
|
||||
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.name.match(/\bout\b/)) return true
|
||||
if (item.name.match(/\bbuild\b/)) return true
|
||||
if (name.match(/\bout\b/)) return true
|
||||
if (name.match(/\bbuild\b/)) return true
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -138,10 +137,16 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re
|
|||
|
||||
// ---------- IN GENERAL ----------
|
||||
|
||||
const resolveChildren = async (children: undefined | IFileStat[], fileService: IFileService): Promise<IFileStat[]> => {
|
||||
const res = await fileService.resolveAll(children ?? [])
|
||||
const stats = res.map(s => s.success ? s.stat : null).filter(s => !!s)
|
||||
return stats
|
||||
}
|
||||
|
||||
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
|
||||
const computeAndStringifyDirectoryTree = async (
|
||||
eItem: ExplorerItem,
|
||||
explorerService: IExplorerService,
|
||||
eItem: IFileStat,
|
||||
fileService: IFileService,
|
||||
MAX_CHARS: number,
|
||||
fileCount: { count: number } = { count: 0 },
|
||||
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
|
||||
|
|
@ -181,12 +186,13 @@ const computeAndStringifyDirectoryTree = async (
|
|||
let remainingChars = MAX_CHARS - nodeLine.length;
|
||||
|
||||
// Check if it's a directory we should skip
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem.name);
|
||||
|
||||
|
||||
// Fetch and process children if not a filtered directory
|
||||
if (eItem.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
|
||||
const eChildren = await resolveChildren(eItem.children, fileService)
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
|
|
@ -194,7 +200,7 @@ const computeAndStringifyDirectoryTree = async (
|
|||
eChildren,
|
||||
remainingChars,
|
||||
'',
|
||||
explorerService,
|
||||
fileService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
|
||||
);
|
||||
|
|
@ -208,10 +214,10 @@ const computeAndStringifyDirectoryTree = async (
|
|||
|
||||
// Helper function to render children with proper tree formatting
|
||||
const renderChildrenCombined = async (
|
||||
children: ExplorerItem[],
|
||||
children: IFileStat[],
|
||||
maxChars: number,
|
||||
parentPrefix: string,
|
||||
explorerService: IExplorerService,
|
||||
fileService: IFileService,
|
||||
fileCount: { count: number },
|
||||
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
|
||||
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
|
||||
|
|
@ -263,12 +269,12 @@ const renderChildrenCombined = async (
|
|||
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
|
||||
|
||||
// Skip processing children for git ignored directories
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child.name);
|
||||
|
||||
// Create the prefix for the next level (continuation line or space)
|
||||
if (child.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await child.fetchChildren(SortOrder.Modified);
|
||||
const eChildren = await resolveChildren(child.children, fileService)
|
||||
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const {
|
||||
|
|
@ -278,7 +284,7 @@ const renderChildrenCombined = async (
|
|||
eChildren,
|
||||
remainingChars,
|
||||
nextLevelPrefix,
|
||||
explorerService,
|
||||
fileService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
|
||||
);
|
||||
|
|
@ -311,7 +317,68 @@ const renderChildrenCombined = async (
|
|||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------
|
||||
// ------------------------- FOLDERS -------------------------
|
||||
|
||||
export async function getAllUrisInDirectory(
|
||||
directoryUri: URI,
|
||||
maxResults: number,
|
||||
fileService: IFileService,
|
||||
): Promise<URI[]> {
|
||||
const result: URI[] = [];
|
||||
|
||||
// Helper function to recursively collect URIs
|
||||
async function visitAll(folderStat: IFileStat): Promise<boolean> {
|
||||
// Stop if we've reached the limit
|
||||
if (result.length >= maxResults) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (!folderStat.isDirectory || !folderStat.children) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const eChildren = await resolveChildren(folderStat.children, fileService)
|
||||
|
||||
// Process files first (common convention to list files before directories)
|
||||
for (const child of eChildren) {
|
||||
if (!child.isDirectory) {
|
||||
result.push(child.resource);
|
||||
|
||||
// Check if we've hit the limit
|
||||
if (result.length >= maxResults) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then process directories recursively
|
||||
for (const child of eChildren) {
|
||||
const isGitIgnored = shouldExcludeDirectory(child.name)
|
||||
if (child.isDirectory && !isGitIgnored) {
|
||||
const shouldContinue = await visitAll(child);
|
||||
if (!shouldContinue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error processing directory ${folderStat.resource.fsPath}: ${error}`);
|
||||
return true; // Continue despite errors in a specific directory
|
||||
}
|
||||
}
|
||||
|
||||
const rootStat = await fileService.resolve(directoryUri)
|
||||
await visitAll(rootStat);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
|
|
@ -319,21 +386,25 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||
const eRoot = this.explorerService.findClosest(uri)
|
||||
async getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]> {
|
||||
return getAllUrisInDirectory(uri, opts.maxResults, this.fileService)
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI) {
|
||||
const eRoot = await this.fileService.resolve(uri)
|
||||
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
|
||||
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
const maxItemsPerDir = START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
// First try with START_MAX_DEPTH
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
this.fileService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
|
||||
|
|
@ -344,7 +415,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
this.fileService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
|
|
@ -363,7 +434,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
return c
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
|
||||
async getAllDirectoriesStr({ cutOffMessage, }: { cutOffMessage: string, }) {
|
||||
let str: string = '';
|
||||
let cutOff = false;
|
||||
const folders = this.workspaceContextService.getWorkspace().folders;
|
||||
|
|
@ -371,7 +442,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
return '(NO WORKSPACE OPEN)';
|
||||
|
||||
// Use START_MAX_ITEMS_PER_DIR if not specified
|
||||
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
|
||||
const startMaxItemsPerDir = START_MAX_ITEMS_PER_DIR;
|
||||
|
||||
for (let i = 0; i < folders.length; i += 1) {
|
||||
if (i > 0) str += '\n';
|
||||
|
|
@ -381,13 +452,13 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
str += `Directory of ${f.uri.fsPath}:\n`;
|
||||
const rootURI = f.uri;
|
||||
|
||||
const eRoot = this.explorerService.findClosestRoot(rootURI);
|
||||
const eRoot = await this.fileService.resolve(rootURI)
|
||||
if (!eRoot) continue;
|
||||
|
||||
// First try with START_MAX_DEPTH and startMaxItemsPerDir
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
this.fileService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
|
||||
|
|
@ -398,7 +469,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
this.fileService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
|
|
@ -417,11 +488,8 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
}
|
||||
}
|
||||
|
||||
if (cutOff) {
|
||||
return `${str.trimEnd()}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
const ans = cutOff ? `${str.trimEnd()}\n${cutOffMessage}` : str
|
||||
return ans
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -352,7 +352,7 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
|
||||
|
||||
// keep modelName, but use the fallback's defaults
|
||||
const extensiveOAICompatModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => {
|
||||
const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => {
|
||||
|
||||
const lower = modelName.toLowerCase()
|
||||
|
||||
|
|
@ -1018,32 +1018,32 @@ export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq',
|
|||
const vLLMSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
||||
modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const lmStudioSettings: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' }, contextWindow: 4_096 }),
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' }, contextWindow: 4_096 }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const ollamaSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: ollamaModelOptions,
|
||||
}
|
||||
|
||||
const openaiCompatible: VoidStaticProviderInfo = {
|
||||
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
|
||||
modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName),
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName),
|
||||
modelOptions: {},
|
||||
}
|
||||
|
||||
const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } },
|
||||
modelOptionsFallback: (modelName) => extensiveOAICompatModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
|
|
@ -1197,11 +1197,11 @@ const openRouterSettings: VoidStaticProviderInfo = {
|
|||
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
||||
// TODO!!! send a query to openrouter to get the price, etc.
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const res = extensiveOAICompatModelOptionsFallback(modelName)
|
||||
// openRouter does not support gemini-style, use openai-style instead
|
||||
if (res?.specialToolFormat === 'gemini-style') {
|
||||
res.specialToolFormat = 'openai-style'
|
||||
}
|
||||
const res = extensiveModelOptionsFallback(modelName)
|
||||
// // openRouter does not support gemini-style, use openai-style instead
|
||||
// if (res?.specialToolFormat === 'gemini-style') {
|
||||
// res.specialToolFormat = 'openai-style'
|
||||
// }
|
||||
return res
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IDirectoryStrService } from '../directoryStrService.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
// Triple backtick wrapper used throughout the prompts for code blocks
|
||||
|
|
@ -524,40 +525,66 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
|||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
|
||||
// }
|
||||
|
||||
const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
|
||||
val: string,
|
||||
truncated: boolean,
|
||||
fullFileLen: number,
|
||||
} | {
|
||||
val: null,
|
||||
truncated?: undefined
|
||||
fullFileLen?: undefined,
|
||||
}> => {
|
||||
try {
|
||||
const fileContent = await fileService.readFile(uri)
|
||||
const val = fileContent.value.toString()
|
||||
if (val.length > fileSizeLimit) return { val: val.substring(0, fileSizeLimit), truncated: true, fullFileLen: val.length }
|
||||
return { val, truncated: false, fullFileLen: val.length }
|
||||
}
|
||||
catch (e) {
|
||||
return { val: null }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
|
||||
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
|
||||
opts: { directoryStrService: IDirectoryStrService, fileService: IFileService }
|
||||
) => {
|
||||
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
let selnsStrs: string[] = []
|
||||
if (opts.type === 'references') {
|
||||
selnsStrs = currSelns?.map((s) => {
|
||||
if (s.type === 'File') return `${s.uri.fsPath}`
|
||||
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
|
||||
if (s.type === 'Folder') return `${s.uri.fsPath}/`
|
||||
return ''
|
||||
}) ?? []
|
||||
}
|
||||
if (opts.type === 'fullCode') {
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const voidModelService = opts.voidModelService
|
||||
const { model } = await voidModelService.getModelSafe(s.uri)
|
||||
if (!model) return ''
|
||||
const val = model.getValue(EndOfLinePreference.LF)
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, 2_000_000)
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}`
|
||||
return str
|
||||
}
|
||||
else if (s.type === 'Folder') {
|
||||
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
|
||||
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
|
||||
|
||||
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: 1_000 })
|
||||
const strOfFiles = await Promise.all(uris.map(async uri => {
|
||||
const { val, truncated } = await readFile(opts.fileService, uri, 100_000)
|
||||
const truncationStr = truncated ? `\n... file truncated ...` : ''
|
||||
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
|
||||
const str = `${uri.fsPath}:\n${content}`
|
||||
return str
|
||||
}
|
||||
if (s.type === 'Folder') {
|
||||
// TODO
|
||||
return ''
|
||||
}
|
||||
}))
|
||||
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
|
||||
return contentStr
|
||||
}
|
||||
else
|
||||
return ''
|
||||
}) ?? [])
|
||||
}
|
||||
}) ?? [])
|
||||
|
||||
const selnsStr = selnsStrs.join('\n') ?? ''
|
||||
let str = ''
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
return { title: 'Groq', }
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return { title: 'xAI', }
|
||||
return { title: 'Grok (xAI)', }
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return { title: 'Mistral', }
|
||||
|
|
@ -120,9 +120,9 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
|||
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
|
||||
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).'
|
||||
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue