Merge pull request #197 from voideditor/model-selection

Fix line indexing for CtrlK
This commit is contained in:
Andrew Pareles 2025-01-02 18:59:08 -08:00 committed by GitHub
commit a3d3309b9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 149 additions and 95 deletions

View file

@ -43,19 +43,19 @@ First, run `npm install -g node-gyp`. Then:
### Building Void
To build Void, open `void/` inside VSCode. Then:
To build Void, open `void/` inside VSCode. Then open your terminal and run:
1. `npm install` to install all dependencies.
2. `npm run watchreact` to build Void's browser dependencies like React.
3. Build.
3. Build Void.
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
- This step can take ~5 min. The build is done when you see two check marks.
4. Run.
4. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
- If you would like to reset Void back to its default settings, you can run `./scripts/code.sh --user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` (mac). This will save all data and extensions to the `.tmp` folder. You can delete this folder to reset your settings.
#### Building Void from Terminal

View file

@ -144,8 +144,8 @@ export const defaultProviderSettings = {
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames: ProviderName[] = ['ollama'] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !localProviderNames.includes(name)) // all non-local names
export const localProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
@ -379,7 +379,7 @@ export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
@ -405,7 +405,7 @@ type FeatureFlagDisplayInfo = {
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
if (featureFlag === 'autoRefreshModels') {
return {
description: `Automatically detect local providers and models (like Ollama).`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
description: `Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`,
}
}
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)

View file

@ -117,7 +117,7 @@ type CtrlKZone = {
editorId: string; // the editor the input lives on
_mountInfo: null | {
inputBox: InputBox | null; // the input box that lives in the zone
inputBoxRef: { current: InputBox | null }; // the input box that lives in the zone
dispose: () => void;
refresh: () => void;
}
@ -136,7 +136,7 @@ type DiffZone = {
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
line: null;
line?: undefined;
};
editorId?: undefined;
} & CommonZoneProps
@ -210,9 +210,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._register(
model.onDidChangeContent(e => {
// it's as if we just called _write, now all we need to do is realign and refresh
const uri = model.uri
if (this.weAreWriting) return
const uri = model.uri
this._onUserChangeContent(uri, e)
})
)
@ -244,6 +243,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) {
if (shouldRealign) {
const { newText, oldRange } = shouldRealign
console.log('realiging', newText, oldRange)
this._realignAllDiffAreasLines(uri, newText, oldRange)
}
this._refreshStylesAndDiffsInURI(uri)
@ -329,7 +329,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
let zoneId: string | null = null
let viewZone_: IViewZone | null = null
let inputBox_: InputBox | null = null
const inputBoxRef: { current: InputBox | null } = { current: null }
const itemId = this._consistentEditorItemService.addToEditor(editor, () => {
const domNode = document.createElement('div');
@ -352,7 +352,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
mountCtrlK(domNode, accessor, {
diffareaid: ctrlKZone.diffareaid,
onGetInputBox: (inputBox) => {
inputBox_ = inputBox
inputBoxRef.current = inputBox
// if it's mounting for the first time, focus it
if (!(ctrlKZone.diffareaid in this.mostRecentTextOfCtrlKZoneId)) { // detect first mount this way (a hack)
this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = undefined
@ -381,10 +381,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
})
})
return {
inputBox: inputBox_,
inputBoxRef,
refresh: () => editor.changeViewZones(accessor => {
if (zoneId && viewZone_) {
viewZone_.afterLineNumber = ctrlKZone.startLine - 1
@ -394,7 +392,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
dispose: () => {
this._consistentEditorItemService.removeFromEditor(itemId)
},
}
} satisfies CtrlKZone['_mountInfo']
}
@ -405,6 +403,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
if (diffArea.type !== 'CtrlKZone') continue
if (!diffArea._mountInfo) {
diffArea._mountInfo = this._addCtrlKZoneInput(diffArea)
console.log('MOUNTED', diffArea.diffareaid)
}
else {
diffArea._mountInfo.refresh()
@ -579,10 +578,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
type: 'DiffZone',
_diffOfId: {},
_URI: uri,
_streamState: {
isStreaming: false,
line: null,
} as const,
_streamState: { isStreaming: false },
_removeStylesFns: new Set(),
}
}
@ -740,17 +736,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
console.log('DA', diffArea.startLine, diffArea.endLine)
console.log('CHANGE', startLine, endLine)
// if the diffArea is entirely above the range, it is not affected
if (diffArea.endLine < startLine) {
// console.log('DA FULLY ABOVE (doing nothing)')
// console.log('CHANGE FULLY BELOW DA (doing nothing)')
continue
}
// if a diffArea is entirely below the range, shift the diffArea up/down by the delta amount of newlines
else if (endLine < diffArea.startLine) {
// console.log('DA FULLY BELOW')
// console.log('CHANGE FULLY ABOVE DA')
const changedRangeHeight = endLine - startLine + 1
const deltaNewlines = newTextHeight - changedRangeHeight
diffArea.startLine += deltaNewlines
@ -771,7 +764,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
}
// if the change contains only the diffArea's top
else if (startLine < diffArea.startLine && diffArea.startLine <= endLine) {
// console.log('TOP ONLY')
// console.log('CHANGE CONTAINS TOP OF DA ONLY')
const numOverlappingLines = endLine - diffArea.startLine + 1
const numRemainingLinesInDA = diffArea.endLine - diffArea.startLine + 1 - numOverlappingLines
const newHeight = (numRemainingLinesInDA - 1) + (newTextHeight - 1) + 1
@ -780,7 +773,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
}
// if the change contains only the diffArea's bottom
else if (startLine <= diffArea.endLine && diffArea.endLine < endLine) {
// console.log('BOTTOM ONLY')
// console.log('CHANGE CONTAINS BOTTOM OF DA ONLY')
const numOverlappingLines = diffArea.endLine - startLine + 1
diffArea.endLine += newTextHeight - numOverlappingLines
}
@ -823,47 +816,41 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// if streaming, use diffs to figure out where to write new code
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let originalCodeStartLine: number // get original[oldStartingPoint...]
let newCodeEndLine: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted
let originalCodeStartLine: number // get original[oldStartingPoint...] (line in the original code, so starts at 1)
const lastDiff = computedDiffs.pop()
if (!lastDiff) {
console.log('!lastDiff')
// if the writing is identical so far, display no changes
newFileEndLine = diffZone.startLine
originalCodeStartLine = 1
newCodeEndLine = 1
}
else {
if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.endLine
originalCodeStartLine = lastDiff.originalStartLine
}
else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.startLine
originalCodeStartLine = lastDiff.originalStartLine
}
else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.endLine
originalCodeStartLine = lastDiff.originalStartLine
}
else {
originalCodeStartLine = lastDiff.originalStartLine
if (lastDiff.type === 'insertion' || lastDiff.type === 'edit')
newCodeEndLine = lastDiff.endLine
else if (lastDiff.type === 'deletion')
newCodeEndLine = lastDiff.startLine
else
throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
}
}
diffZone._streamState.line = newFileEndLine
// lines are 1-indexed
const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n')
const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n')
const newCodeTop = llmText.split('\n').slice(0, (newCodeEndLine - 1) + 1).join('\n')
const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1) + 1, Infinity).join('\n')
const newCode = `${newFileTop}\n${oldFileBottom}`
const newCode = `${newCodeTop}\n${oldFileBottom}`
this._writeText(uri, newCode,
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
// add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea)
diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine
return computedDiffs
@ -871,6 +858,54 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// // if streaming, use diffs to figure out where to write new code
// // these are two different coordinate systems - new and old line number
// let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
// let originalCodeStartLine: number // get original[oldStartingPoint...]
// const lastDiff = computedDiffs.pop()
// if (!lastDiff) {
// // if the writing is identical so far, display no changes
// newFileEndLine = diffZone.startLine
// originalCodeStartLine = 1
// }
// else {
// if (lastDiff.type === 'insertion') {
// newFileEndLine = lastDiff.endLine
// originalCodeStartLine = lastDiff.originalStartLine
// }
// else if (lastDiff.type === 'deletion') {
// newFileEndLine = lastDiff.startLine
// originalCodeStartLine = lastDiff.originalStartLine
// }
// else if (lastDiff.type === 'edit') {
// newFileEndLine = lastDiff.endLine
// originalCodeStartLine = lastDiff.originalStartLine
// }
// else {
// throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
// }
// }
// diffZone._streamState.line = newFileEndLine
// // lines are 1-indexed
// const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n')
// const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n')
// const newCode = `${newFileTop}\n${oldFileBottom}`
// this._writeText(uri, newCode,
// { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed
// { shouldRealignDiffAreas: true }
// )
// return computedDiffs
@ -887,7 +922,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
if (diffArea.type !== 'CtrlKZone') continue
const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine
if (!noOverlap) {
setTimeout(() => diffArea._mountInfo?.inputBox?.focus(), 0)
setTimeout(() => diffArea._mountInfo?.inputBoxRef.current?.focus(), 0)
return
}
}
@ -904,7 +939,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
_mountInfo: null,
}
const ctrlKZone = this._addDiffArea(adding)
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
@ -919,6 +953,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const uri = ctrlKZone._URI
const { onFinishEdit } = this._addToHistory(uri)
this._deleteCtrlKZone(ctrlKZone)
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
}
@ -983,14 +1018,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
startLine = startLine_
endLine = endLine_
if (!_mountInfo?.inputBox) return
userMessage = _mountInfo.inputBox?.value
if (!_mountInfo?.inputBoxRef.current) return
userMessage = _mountInfo.inputBoxRef.current?.value
}
else {
throw new Error(`Void: diff.type not recognized on: ${featureName}`)
}
const currentFileStr = this._readURI(uri)
if (currentFileStr === null) return
const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
@ -1055,7 +1089,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const latestOriginalFileStart: IPosition = { lineNumber: 1, column: 1 }
const onDone = () => {
diffZone._streamState = { isStreaming: false, line: null }
diffZone._streamState = { isStreaming: false, }
if (featureName === 'Ctrl+K') {
const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
@ -1111,11 +1145,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._llmMessageService.abort(streamRequestId)
diffZone._streamState = {
isStreaming: false,
streamRequestIdRef: undefined,
line: null
}
diffZone._streamState = { isStreaming: false, }
}

View file

@ -4,13 +4,42 @@
*--------------------------------------------------------------------------------------------*/
import { spawn, execSync } from 'child_process';
// Added lines below
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const __void_name = 'void'
// hack to refresh styles automatically
function saveStylesFile() {
setTimeout(() => {
try {
// Find "void" in __dirname and use that as our base:
const voidIdx = __dirname.indexOf(__void_name);
const baseDir = __dirname.substring(0, voidIdx + __void_name.length);
const target = path.join(
baseDir,
'src/vs/workbench/contrib/void/browser/react/src2/styles.css'
);
// Or re-write with the same content:
const content = fs.readFileSync(target, 'utf8');
fs.writeFileSync(target, content, 'utf8');
console.log('[scope-tailwind] Force-saved styles.css');
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 5000);
}
const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// Watch mode
// Create a watcher for scope-tailwind using nodemon
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
'--watch', 'src',
@ -19,15 +48,17 @@ if (isWatch) {
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
]);
// Create a watcher for tsup in watch mode
const tsupWatcher = spawn('npx', [
'tsup',
'--watch'
]);
// Handle scope-tailwind watcher output
scopeTailwindWatcher.stdout.on('data', (data) => {
console.log(`[scope-tailwind] ${data}`);
// If the output mentions "styles.css", trigger the save:
if (data.toString().includes('styles.css')) {
saveStylesFile();
}
});
scopeTailwindWatcher.stderr.on('data', (data) => {

View file

@ -70,7 +70,6 @@ export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, bu
return (<>
<div className={`relative group w-full bg-vscode-editor-bg overflow-hidden isolate`}>
{buttonsOnHover === null ? null : (
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>{buttonsOnHover}</div>

View file

@ -45,19 +45,18 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
})
}, [inlineDiffService])
const isSingleLine = !text.includes('\n')
return <>
<button
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-editor-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
onClick={onCopy}
>
{copyButtonState}
</button>
<button
// btn btn-secondary btn-sm border text-xs text-vscode-input-fg border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-editor-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
onClick={onApply}
>
Apply

View file

@ -276,11 +276,11 @@ export const SelectedFiles = (
>
{selections.map((selection, i) => {
const showSelectionText = !!(selection.selectionStr && selectionIsOpened[i])
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
return (
<div key={i} // container for `selectionSummary` and `selectionText`
className={`${showSelectionText ? 'w-full' : ''}`}
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
>
{/* selection summary */}
<div
@ -346,8 +346,8 @@ export const SelectedFiles = (
</div>
{/* selection text */}
{showSelectionText &&
<div className='w-full p-1 rounded-sm border-vscode-editor-border bg-vscode-sidebar-bg'>
{isThisSelectionOpened &&
<div className='w-full p-1 rounded-sm border-vscode-editor-border'>
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
</div>
}
@ -383,19 +383,15 @@ const ChatBubble = ({ chatMessage, isLoading }: {
}
return <div
// align chatbubble accoridng to role
className={`
${role === 'user' ? 'self-end' : 'self-start'}
${role === 'assistant' ? 'w-full' : ''}
`}
// style + align chatbubble accoridng to role
className={`p-2 mx-2 text-left space-y-2 rounded-lg max-w-full
${role === 'user' ? 'self-end' : 'self-start'}
${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''}
${role === 'assistant' ? 'w-full' : ''}
`}
>
<div
// style chatbubble
className={`p-2 mx-2 text-left space-y-2 rounded-lg ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full overflow-auto`}
>
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
</div>
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
</div>
}

View file

@ -301,7 +301,7 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
return <div ref={divRef}>
<WidgetComponent
className='relative z-0 text-sm !bg-vscode-editor-bg'
className='relative z-0 text-sm bg-vscode-editor-bg'
ctor={useCallback((container) =>
instantiationService.createInstance(
CodeEditorWidget,

View file

@ -195,7 +195,7 @@ export const ModelDump = () => {
const disabled = !providerEnabled
return <div key={`${modelName}${providerName}`} className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default ${isNewProviderName ? 'mt-4' : ''}`}>
return <div key={`${modelName}${providerName}`} className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate ${isNewProviderName ? 'mt-4' : ''}`}>
{/* left part is width:full */}
<div className={`w-full flex items-center gap-4`}>
<span className='min-w-40'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
@ -204,7 +204,7 @@ export const ModelDump = () => {
</div>
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50 whitespace-nowrap'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<span className='opacity-50'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<VoidSwitch
value={disabled ? false : !isHidden}
@ -420,13 +420,12 @@ export const Settings = () => {
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Instructions:`}</h3> */}
<h3 className={`text-md opacity-50 mb-2`}>{`Void can access any model that you host locally.`}</h3>
<div className='pl-4 select-text'>
<h4 className={`text-xs opacity-50 mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></h4>
<h4 className={`text-xs opacity-50 mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></h4>
<h4 className={`text-xs opacity-50 mb-2`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama model which is competitive with GPT-series models, and requires 5GB of memory.`} /></h4>
<h4 className={`text-xs opacity-50 mb-2`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This is a faster autocomplete model and requires 1GB of memory.`} /></h4>
<h4 className={`text-xs opacity-50 mb-2`}><ChatMarkdownRender string={`5. Void will automatically detect your Ollama models. You can customize the endpoint and models below.`} /></h4>
<h3 className={`text-md mb-2`}>{`Void can access any model that you host locally. By default, we automatically detect your local models.`}</h3>
<div className='pl-4 select-text opacity-50'>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></h4>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></h4>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama model which is competitive with GPT-series models. It requires 5GB of memory.`} /></h4>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This is a faster autocomplete model and requires 1GB of memory.`} /></h4>
{/* TODO we should create UI for downloading models without user going into terminal */}
</div>
@ -435,7 +434,7 @@ export const Settings = () => {
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-16`}>More Providers</h2>
<h3 className={`text-md opacity-50 mb-2`}>{`Void can also access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI.`}</h3>
<h3 className={`text-md mb-2`}>{`Void can also access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI.`}</h3>
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />