mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
230 lines
12 KiB
Markdown
Executable file
230 lines
12 KiB
Markdown
Executable file
# Copilot Instructions for Gerbil
|
|
|
|
## Hard Rules
|
|
|
|
- **NEVER use `console.*`** — blocked by oxlint. Use `logError()` from `@/utils/node/logging` (main process) or `window.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](https://github.com/LostRuins/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:
|
|
|
|
```sh
|
|
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:**
|
|
|
|
1. **Promoted to UI** — High-value flags get a proper control (checkbox, slider, file picker) in the Launch screen tabs. These live in `launchConfig` store and are passed as CLI args to KoboldCpp at launch. They must be added to `UI_COVERED_ARGS` in `CommandLineArgumentsModal.tsx` so they're not duplicated in the modal.
|
|
|
|
2. **Available in the arguments modal** — Everything else is accessible via `CommandLineArgumentsModal` (`src/components/screens/Launch/CommandLineArgumentsModal.tsx`). This modal contains a hardcoded `COMMAND_LINE_ARGUMENTS` array 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 as `additionalArguments` in `KoboldConfig`.
|
|
|
|
**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`:
|
|
|
|
```typescript
|
|
export type IPCChannel = 'my-channel' | ...
|
|
export interface IPCChannelPayloads { 'my-channel': [arg: string] }
|
|
```
|
|
|
|
**Handle** in `src/main/ipc.ts`:
|
|
|
|
```typescript
|
|
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', { ... })`:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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 (see `Select.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
|
|
|
|
```typescript
|
|
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 ready
|
|
- `SILLYTAVERN`, `OPENWEBUI` — host/port/URL constants
|
|
- `GITHUB_API` — KoboldCpp release API URLs
|
|
- `DEFAULT_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` |
|