mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
file and folder dump!
This commit is contained in:
parent
fa83dfc529
commit
097d89366c
9 changed files with 266 additions and 112 deletions
|
|
@ -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
|
||||
|
|
@ -319,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
|
||||
|
|
@ -1189,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -581,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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -420,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)
|
||||
}
|
||||
|
|
@ -868,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
|
||||
|
|
|
|||
|
|
@ -130,6 +130,24 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
|
|||
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 */}
|
||||
|
|
@ -192,13 +210,13 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
|
|||
<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>
|
||||
|
|
@ -222,7 +240,7 @@ Only applies to models labeled \`:free\`. More information [here](https://openro
|
|||
{/* 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">{errorMessage}</div>
|
||||
<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)} />
|
||||
|
|
@ -235,7 +253,7 @@ Only applies to models labeled \`:free\`. More information [here](https://openro
|
|||
setErrorMessage(null);
|
||||
} else {
|
||||
// Show error message
|
||||
setErrorMessage("Please set up at least one Chat model first.");
|
||||
setErrorMessage("Please set up at least one Chat model before moving on.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -607,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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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: 'Grok', }
|
||||
return { title: 'Grok (xAI)', }
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return { title: 'Mistral', }
|
||||
|
|
|
|||
Loading…
Reference in a new issue