12 KiB
Executable file
Copilot Instructions for Gerbil
Hard Rules
- NEVER use
console.*— blocked by oxlint. UselogError()from@/utils/node/logging(main process) orwindow.electronAPI.logs.logError()(renderer) - Always use absolute imports:
import { X } from '@/components/X' - Never add explicit return types — rely on TypeScript inference
- Never create tests, docs, or GitHub workflows
- Never add comments — code should be self-explanatory; no inline comments, no block comments
- Move helper functions out of component files into
src/utils/
What Gerbil Is
Gerbil is an Electron desktop app that acts as a launcher and GUI for KoboldCpp. It is not a new LLM backend — it wraps KoboldCpp and makes it usable without touching the terminal.
The problem it solves: KoboldCpp is an excellent all-in-one local LLM backend (text gen, image gen, multimodal, agents) but its own launcher UI is bad. Gerbil replaces and significantly improves that launcher.
Gerbil vs KoboldCpp's launcher: Gerbil adds auto binary download, GPU auto-detection (CUDA/ROCm/Vulkan/Metal), image gen presets (FLUX, Chroma, Z-Image, Qwen), HuggingFace model search/download, SillyTavern and OpenWebUI auto-launch, config save/load, real-time system monitoring, Cloudflare tunnel support, and a proper desktop experience.
User Base
- People who want to run LLMs locally with real control over the backend
- SillyTavern users (roleplay/character AI) — Gerbil auto-launches ST alongside KoboldCpp
- Image generation users — Gerbil has first-class image gen with 4 presets
- Power users who want GPU acceleration configured correctly without guesswork
Stack
Electron + React + Zustand + Mantine + TypeScript + pnpm + oxlint. No test framework.
Validation
Always run after making changes:
pnpm check # oxlint + oxfmt (lint + format check)
Fix lint/format issues with pnpm fix. No test suite exists.
Gerbil's Role: KoboldCpp Orchestrator
Gerbil is a manager and orchestrator for KoboldCpp. It doesn't implement LLM inference — it configures, launches, monitors, and wraps KoboldCpp. KoboldCpp releases monthly updates that frequently add new CLI flags and capabilities.
How KoboldCpp flags surface in Gerbil:
-
Promoted to UI — High-value flags get a proper control (checkbox, slider, file picker) in the Launch screen tabs. These live in
launchConfigstore and are passed as CLI args to KoboldCpp at launch. They must be added toUI_COVERED_ARGSinCommandLineArgumentsModal.tsxso they're not duplicated in the modal. -
Available in the arguments modal — Everything else is accessible via
CommandLineArgumentsModal(src/components/screens/Launch/CommandLineArgumentsModal.tsx). This modal contains a hardcodedCOMMAND_LINE_ARGUMENTSarray with every KoboldCpp flag, its description, type, category, and aliases. Users can search and add any flag to the "Additional Arguments" field. These are stored asadditionalArgumentsinKoboldConfig.
When KoboldCpp adds new flags: Add entries to COMMAND_LINE_ARGUMENTS in CommandLineArgumentsModal.tsx. If a flag deserves first-class UI treatment, also wire it into the launch config store, the relevant Launch tab, and add it to UI_COVERED_ARGS.
UI_COVERED_ARGS is a Set<string> at the top of CommandLineArgumentsModal.tsx — it lists all flags already exposed by the UI so they're filtered out of the modal. Always keep this in sync when promoting a flag to the UI.
App Structure
Screen flow: Welcome → Download → Launch (tabs: General/Performance/Advanced/Image Gen/Network/Config) → Interface (tabs: Terminal/Chat-Text/Chat-Image)
Supported GPUs: CUDA, ROCm (via YellowRoseCx fork), Vulkan, Metal (macOS), CPU fallback
Frontends: KoboldAI Lite, llama.cpp (embedded in KoboldCpp), SillyTavern (localhost:3000, needs Node.js), OpenWebUI (localhost:8080, needs uv)
Image gen presets: FLUX.1-dev, Chroma-unlocked, Z-Image-Turbo, Qwen2.5-VL-7B (image edit)
CLI mode: headless binary execution — requires prior GUI setup to configure binary path
Source Layout
src/
├── main/ # Electron main process (Node.js)
│ ├── index.ts # Entry: routes to CLI or GUI mode
│ ├── gui.ts # GUI init: window, tray, IPC, lifecycle
│ ├── cli.ts # Headless mode: spawn binary, pipe stdio
│ ├── ipc.ts # All ipcMain.handle/on registrations
│ └── modules/ # Feature domains
│ ├── config.ts # Settings file (~/.config/Gerbil/config.json)
│ ├── hardware.ts # GPU/CPU detection
│ ├── monitoring.ts # Real-time CPU/GPU/RAM metrics
│ ├── tray.ts # System tray icon & menu
│ ├── window.ts # Main window creation & lifecycle
│ ├── auto-updater.ts # Electron auto-updater
│ ├── dependencies.ts # Check npm/uv/npx availability
│ ├── sillytavern.ts # Auto-launch SillyTavern
│ ├── openwebui.ts # Auto-launch OpenWebUI
│ └── koboldcpp/ # KoboldCpp-specific
│ ├── acceleration.ts # GPU acceleration detection
│ ├── analyze.ts # GGUF model analysis
│ ├── backend.ts # Installed binary management
│ ├── config.ts # Config file save/load
│ ├── download.ts # Binary download from GitHub
│ ├── launcher/ # Spawn KoboldCpp + frontends
│ ├── model-download.ts # Local model file detection
│ ├── proxy.ts # Reverse proxy for KoboldCpp
│ └── tunnel.ts # Cloudflare tunnel management
├── preload/
│ └── index.ts # contextBridge: exposes electronAPI to renderer
├── components/
│ ├── App/ # App shell: routing, titlebar, statusbar, modals
│ ├── screens/ # Full-screen views (Welcome, Download, Launch, Interface)
│ ├── settings/ # Settings modal tabs (General, Appearance, Backends, etc.)
│ ├── Notepad/ # Floating notepad widget
│ └── *.tsx # Reusable Mantine wrappers (Select, Modal, Switch, etc.)
├── stores/ # Zustand state
│ ├── launchConfig.ts # KoboldCpp launch parameters (model, GPU layers, flags)
│ ├── preferences.ts # UI prefs (theme, frontend choice, monitoring)
│ ├── koboldBackends.ts # Download state + installed backend versions
│ └── notepad.ts # Notepad state (tabs, content, position)
├── hooks/ # Custom React hooks
├── utils/
│ ├── *.ts # Renderer-safe utilities (format, platform, version, etc.)
│ └── node/ # Main-process-only utilities (fs, gpu, vram, logging, etc.)
├── types/
│ ├── index.d.ts # Core types: Screen, InterfaceTab, Acceleration, ModelParamType, etc.
│ ├── electron.d.ts # window.electronAPI interface (all renderer-accessible APIs)
│ ├── ipc.d.ts # IPC channel names + payload types
│ └── hardware.d.ts # GPU/CPU type definitions
└── constants/
├── index.ts # App-wide constants: URLs, dimensions, defaults, signals
├── imageModelPresets.ts # Image gen presets (FLUX, Chroma, Z-Image, Qwen)
└── notepad.ts # Notepad defaults
IPC Pattern
Main ↔ renderer communicate through a typed bridge. Never call Node APIs directly from renderer.
Define the channel in src/types/ipc.d.ts:
export type IPCChannel = 'my-channel' | ...
export interface IPCChannelPayloads { 'my-channel': [arg: string] }
Handle in src/main/ipc.ts:
ipcMain.handle('domain:action', async (_event, arg: string) => { ... })
ipcMain.on('domain:action', (_event, arg: string) => { ... }) // fire-and-forget
Expose in src/preload/index.ts via contextBridge.exposeInMainWorld('electronAPI', { ... }):
myDomain: {
doThing: (arg: string) => ipcRenderer.invoke('domain:action', arg),
onEvent: (cb: (data: string) => void) => {
ipcRenderer.on('my-channel', (_e, data) => cb(data))
return () => ipcRenderer.removeAllListeners('my-channel') // cleanup
}
}
Type it in src/types/electron.d.ts so renderer gets full type-checking on window.electronAPI.
Use in renderer:
const result = await window.electronAPI.myDomain.doThing('arg')
const cleanup = window.electronAPI.myDomain.onEvent((data) => { ... })
useEffect(() => cleanup, [])
Channel naming: domain:action (e.g., kobold:launchKoboldCpp, config:set).
Zustand Store Pattern
// src/stores/myStore.ts
interface MyStore {
value: string;
setValue: (v: string) => void;
loadFromConfig: () => Promise<void>; // if persisted
}
export const useMyStore = create<MyStore>((set) => ({
value: '',
setValue: (v) => {
set({ value: v });
window.electronAPI.config.set('myKey', v); // persist if needed
},
loadFromConfig: async () => {
const saved = await window.electronAPI.config.get('myKey');
if (saved) set({ value: saved });
},
}));
Stores load persisted state on init: void useMyStore.getState().loadFromConfig() called in App/index.tsx.
Component Conventions
- Reusable components live in
src/components/and wrap Mantine with sensible defaults (seeSelect.tsx,Modal.tsx,Switch.tsx) - Screen components live in
src/components/screens/ - Settings tabs live in
src/components/settings/ - All imports from Mantine:
import { Button } from '@mantine/core'
Key Types to Know
type Screen = 'welcome' | 'download' | 'launch' | 'interface'
type InterfaceTab = 'terminal' | 'chat-text' | 'chat-image'
type Acceleration = 'cpu' | 'cuda' | 'rocm' | 'vulkan' | ...
type ModelParamType = 'model' | 'sdmodel' | 'sdt5xxl' | 'sdvae' | ...
type FrontendPreference = 'koboldcpp' | 'llamacpp' | 'sillytavern' | 'openwebui'
Key Constants (src/constants/index.ts)
SERVER_READY_SIGNALS— strings that signal KoboldCpp/SillyTavern/OpenWebUI are readySILLYTAVERN,OPENWEBUI— host/port/URL constantsGITHUB_API— KoboldCpp release API URLsDEFAULT_CONTEXT_SIZE,DEFAULT_AUTO_GPU_LAYERS— launch defaults
Utilities Reference
| File | What it exports |
|---|---|
utils/format.ts |
formatBytes, formatDeviceName, formatDate |
utils/platform.ts |
filterAssetsByPlatform, isAssetCompatibleWithPlatform |
utils/version.ts |
compareVersions, getDisplayNameFromPath |
utils/validation.ts |
getInputValidationState (path validation) |
utils/terminal.ts |
handleTerminalOutput, processTerminalContent (ANSI handling) |
utils/interface.ts |
getDefaultInterfaceTab, getAvailableInterfaceOptions, getTunnelInterfaceUrl |
utils/logger.ts |
logError, withRetry, safeExecute |
utils/node/logging.ts |
Main process logError |
utils/node/fs.ts |
readJsonFile, ensureDir, pathExists |
utils/node/gpu.ts |
GPU capability detection |
utils/node/vram.ts |
calculateOptimalGpuLayers |
utils/node/path.ts |
getConfigDir, openUrl |