diff --git a/.gitignore b/.gitignore
index 20537a4..f0707c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,18 +11,3 @@ Thumbs.db
# Yarn
.yarn/
.pnp.*
-
-t5xxl_fp8_e4m3fn.safetensors.aria2
-ae.safetensors
-clip_l.safetensors
-t5xxl_fp8_e4m3fn.safetensors
-flux1-kontext-dev-Q3_K_S.gguf
-gemma-3-4b-it.Q8_0.gguf
-.webui_secret_key
-chroma-unlocked-v29-Q3_K_L.gguf
-Qwen-Image-Edit-2509-Q4_K_S.gguf
-Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf
-Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf
-qwen_image_vae.safetensors
-flux1-kontext-dev-Q4_K_S.gguf
-chroma-unlocked-v45-Q4_0.gguf
diff --git a/package.json b/package.json
index b0b139d..1bf762e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
- "version": "1.9.0",
+ "version": "2.0.0",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@@ -40,11 +40,11 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
- "@types/react": "^19.2.5",
+ "@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"@types/yauzl": "^2.10.3",
- "@typescript-eslint/eslint-plugin": "^8.46.4",
- "@typescript-eslint/parser": "^8.46.4",
+ "@typescript-eslint/eslint-plugin": "^8.47.0",
+ "@typescript-eslint/parser": "^8.47.0",
"@vitejs/plugin-react": "^5.1.1",
"cross-env": "^10.1.0",
"electron": "^38.7.0",
@@ -67,7 +67,7 @@
"dependencies": {
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.3",
- "@codemirror/view": "^6.38.7",
+ "@codemirror/view": "^6.38.8",
"@fontsource/inter": "^5.2.8",
"@huggingface/gguf": "^0.3.2",
"@mantine/core": "^8.3.8",
@@ -75,7 +75,7 @@
"@uiw/react-codemirror": "^4.25.3",
"electron-updater": "^6.6.2",
"execa": "^9.6.0",
- "lucide-react": "^0.553.0",
+ "lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
diff --git a/src/components/screens/Launch/GeneralTab/index.tsx b/src/components/screens/Launch/GeneralTab/index.tsx
index 58e9aa7..e10df56 100644
--- a/src/components/screens/Launch/GeneralTab/index.tsx
+++ b/src/components/screens/Launch/GeneralTab/index.tsx
@@ -44,6 +44,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
onSelectFile={handleSelectModelFile}
searchUrl="https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending"
showAnalyze
+ paramType="model"
/>
diff --git a/src/components/screens/Launch/ImageGenerationTab.tsx b/src/components/screens/Launch/ImageGenerationTab.tsx
index 6057431..b8f4107 100644
--- a/src/components/screens/Launch/ImageGenerationTab.tsx
+++ b/src/components/screens/Launch/ImageGenerationTab.tsx
@@ -77,15 +77,17 @@ export const ImageGenerationTab = () => {
onSelectFile={handleSelectSdmodelFile}
searchUrl="https://huggingface.co/models?pipeline_tag=text-to-image&library=gguf&sort=trending"
showAnalyze
+ paramType="sdmodel"
/>
{
tooltip="CLIP-L text encoder model for text-image understanding."
onChange={handleSdcliplChange}
onSelectFile={handleSelectSdcliplFile}
+ paramType="sdclipl"
/>
{
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
onChange={handleSdclipgChange}
onSelectFile={handleSelectSdclipgFile}
+ paramType="sdclipg"
/>
{
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
onChange={handleSdphotomakerChange}
onSelectFile={handleSelectSdphotomakerFile}
+ paramType="sdphotomaker"
/>
{
tooltip="Variational Autoencoder model for improved image quality."
onChange={handleSdvaeChange}
onSelectFile={handleSelectSdvaeFile}
+ paramType="sdvae"
/>
{
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
onChange={handleSdloraChange}
onSelectFile={handleSelectSdloraFile}
+ paramType="sdlora"
/>
void;
searchUrl?: string;
showAnalyze?: boolean;
+ paramType: ModelParamType;
}
export const ModelFileField = ({
@@ -27,6 +36,7 @@ export const ModelFileField = ({
onSelectFile,
searchUrl,
showAnalyze = false,
+ paramType,
}: ModelFileFieldProps) => {
const validationState = getInputValidationState(value);
const [analysisModalOpened, setAnalysisModalOpened] = useState(false);
@@ -35,6 +45,26 @@ export const ModelFileField = ({
);
const [analysisLoading, setAnalysisLoading] = useState(false);
const [analysisError, setAnalysisError] = useState();
+ const [cachedModels, setCachedModels] = useState([]);
+ const combobox = useCombobox();
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const models =
+ await window.electronAPI.kobold.getLocalModels(paramType);
+ setCachedModels(models);
+ } catch (error) {
+ logError('Failed to load cached models:', error as Error);
+ }
+ })();
+ }, [paramType]);
+
+ const options = cachedModels.map((model) => (
+
+ {model.author}/{model.model}
+
+ ));
const getHelperText = () => {
if (validationState === 'neutral') return undefined;
@@ -72,12 +102,37 @@ export const ModelFileField = ({
- onChange(event.currentTarget.value)}
- error={validationState === 'invalid' ? getHelperText() : undefined}
- />
+ {
+ onChange(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ {
+ onChange(event.currentTarget.value);
+ combobox.openDropdown();
+ }}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ error={
+ validationState === 'invalid' ? getHelperText() : undefined
+ }
+ rightSection={}
+ rightSectionPointerEvents="none"
+ />
+
+
+ {options.length > 0 && (
+
+ {options}
+
+ )}
+