Architecture Overview
Technical architecture and design of GGUF Loader
GGUF Loader is a PySide6 desktop application with a strict layering rule: pure logic in core/, threading bridges in services/, dumb widgets in ui/. This keeps the hard parts unit-testable and the UI predictable.
ποΈ Layer Diagram
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β ui/ (MainWindow, ChatPanel, SidebarPanel) β presentation only;
β widgets/ (ChatBubble, FeedbackDialog) β emits signals, renders state
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β Qt signals
βββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β services/ (Model/Chat/Agent/Environment) β QObject bridges;
β β own QThreads + workers
βββββββββ¬βββββββββββββββββββββββ¬ββββββββββββββββββ
β β
βββββββββΌβββββββββ βββββββββββΌββββββββββββββββ
β core/llm β β core/agent β pure Python;
β ModelBackend β β AgentEngine, Tools β no Qt, no llama_cpp
β PromptBuilder β β ToolRegistry β
βββββββββ¬βββββββββ βββββββββββββββββββββββββββ
β
βββββββββΌβββββββββ
β llama-cpp-pythonβ the only dependency on the
β (Llama runtime) β native GGUF inference lib
ββββββββββββββββββ
βοΈ ModelBackend (core/llm/model_backend.py)
The only module that talks to llama_cpp. It wraps a loaded Llama object and serializes all access with a threading.Lock, so UI threads and addons can call it concurrently without crashing the runtime.
load()/unload()manage the runtime lifecycle.__call__(prompt, stream=True, ...)keeps the historical llama-cpp callable shape, so addons that called the raw object keep working.generate(prompt, **kwargs)returns a complete string;generate_stream(...)yields tokens.- Qt-free by design β unit-testable and reusable from any thread.
π§΅ ModelService (services/model_service.py)
Owns the current ModelBackend. Every load request creates a fresh QThread + worker (never subclassing QThread):
load(path, use_gpu, n_ctx)unloads any previous model and starts the thread.- The worker constructs a
ModelBackendand callsload(). loaded/errorsignals return to the UI thread; the thread is quit and garbage-collected.
Exposes backend (the ModelBackend) and model (a callable-compatible view used by addons).
π¬ ChatService (services/chat_service.py)
Streams generation off the UI thread. ChatService.generate(backend, prompt, ...) runs llama-cpp inference in a worker thread and emits token_received per token, finished, and error β so even a long 2048-token response never freezes the window.
π€ AgentEngine (core/agent/agent_engine.py)
A pure, testable tool-use loop. It receives a plain callable llm(prompt, max_tokens, temperature) -> str (the UI wires it to ModelBackend.__call__) plus a workspace path, and runs per user message:
- Optional quick analysis for complex requests.
- Ask the model for a JSON tool-call plan (
extract_jsonhandles fenced/bare/embedded JSON). - Execute each tool, streaming
statuscallbacks andtoolevents. - Ask for a natural-language final response, with a graceful fallback summary if the model returns nothing useful.
ToolRegistry (core/agent/tool_registry.py)
Sandboxed filesystem tools β list_directory, read_file, write_file, edit_file, search_files. Every tool resolves paths against a single workspace root and rejects any path that escapes it (Path.resolve() + containment check). Reading enforces a max size and BOM-aware encoding detection.
ποΈ MainWindow (ui/main_window.py)
The composition root. It:
- Owns the four services (Model/Chat/Agent/Environment) and a
PromptBuilder. - Builds the header (brand + status chip), sidebar, chat panel, and menu bar (File / View / Addons / Help).
- Exposes the addon-facing API that historical
AIChat/GGUFLoaderAppprovided:modelβ callableModelBackendorNonemodel_loaded/model_unloaded/generation_finished/generation_errorsignalschat_generatorβ alwaysNone(addons fall back to callingmodel)addon_manager,_floating_chat_addon
π¨ Theming (ui/theme.py)
A single QSS_TEMPLATE with $token placeholders is rendered twice β DARK_TOKENS (βMidnight & Amberβ: slate-charcoal surfaces + amber accent) and LIGHT_TOKENS. ThemeMixin.apply_styles() swaps palettes at runtime; widgets read self.tokens for stateful colors. Because one template drives both themes, they can never drift apart.
π§© Addons (addon_manager.py + addons/)
AddonManager scans the addons/ folder for packages with __init__.py, loads each module dynamically, and calls its register() function. A registered addon returns a widget (shown in the Addons menu) and/or runs in the background. See the Addon Development Guide and Addon API Reference.
The built-in floating_chat addon is a good reference: it locates the main window via QApplication.topLevelWidgets(), subscribes to model signals, and manages its own always-on-top windows.
π¦ Deployment
resource_manager.pydetects dev / installed-package / PyInstaller (sys.frozen) and resolves paths accordingly β models, config, cache, logs, addons, and llama.cpp libs.main.pyβssetup_library_path()registers the bundledllama_cpp/libso native DLLs are found in frozen builds.build_exe.spec+ hooks (build_hooks/) package everything;scripts/package_linux.shwraps the Linux binary into an installable tarball.
Related Documentation
- This page documents how the gguf loader codebase is organized. the app is a pyside6 desktop application; a single `main.py` bootstraps everything, and logic is split into `core/` (pure logic), `services/` (qt threading bridges), and `ui/` + `widgets/` (presentation). ## ποΈ top-level layout ``` gguf-loader/ βββ main.py # entry point: logging, dll paths, qapplication, mainwindow βββ config.py # central configuration constants βββ resource_manager.py # resource/path discovery (dev, package, or frozen) βββ addon_manager.py # loads and manages addons βββ requirements.txt # python dependencies βββ build_exe.spec # pyinstaller spec used for windows & linux builds β βββ core/ # pure, testable logic (no qt) β βββ llm/ β β βββ model_backend.py # thread-safe llama-cpp-python wrapper β β βββ prompt_builder.py # system-prompt & conversation assembly β βββ agent/ β βββ agent_engine.py # tool-use agent loop (no qt, no llama_cpp) β βββ tool_registry.py # sandboxed workspace tools β βββ services/ # qobject bridges that run work on threads β βββ model_service.py # load/unload models on a qthread β βββ chat_service.py # streaming generation on a qthread β βββ agent_service.py # runs agentengine on a worker thread β βββ environment_service.py # venv/dependency checks & pip tasks β βββ launcher_service.py # launches scripts/ utilities β βββ ui/ # main window & panels β βββ main_window.py # composition root + addon-facing api β βββ chat_panel.py # chat display, input, agent controls β βββ sidebar_panel.py # model settings sidebar β βββ theme.py # dark/light qss token system β βββ widgets/ # reusable widgets β βββ chat_bubble.py # chatgpt-style bubble β βββ feedback_dialog.py # feedback form dialog β βββ addons/ # addon packages (each has __init__.py with register()) β βββ floating_chat/ # built-in floating chat addon β βββ scripts/ # utility & release scripts β βββ capture_screenshots.py # regenerates readme/site screenshots β βββ install_linux.sh # linux installer/uninstaller β βββ package_linux.sh # builds the linux .tar.gz release β βββ ... # gpu install/monitor helpers β βββ build_hooks/ # pyinstaller hook modules ``` ## π key design rules - **`core/` never imports qt or llama_cpp.** it receives plain callables, so it can be unit-tested in isolation. - **`services/` are the only place qt threads are created.** ui never spins up threads directly. - **`ui/` widgets are "dumb"** β they render state and emit signals; `mainwindow` owns all logic. - **`resource_manager.py`** makes paths work identically in dev, as an installed package, and in a pyinstaller bundle (`sys._meipass`). ## π§΅ threading model ``` ui thread (mainwindow) worker thread β β βββ modelservice.load() βββββββ qthread: llama_cpp loads model ββββββββ loaded(modelbackend) β βββ chatservice.generate() ββββ qthread: streams tokens ββββββββ token_received(text) β βββ agentservice.process() ββββ qthread: agentengine tool loop ββββββββ status/tool/response β ``` a fresh `qthread` + worker is created per request (the professional qt pattern β `qthread` is never subclassed). ## π entry point flow `main.py` β `setup_library_path()` (finds bundled llama.cpp libs) β `qapplication` β `mainwindow()` β builds ui, wires services, checks environment, loads addons β `app.exec()`. ## π¦ packaging - **windows**: `build_exe.bat` / `build_exe.spec` β `ggufloader_vx.y.z.exe` - **linux**: build the spec inside a linux environment, then `scripts/package_linux.sh` wraps the binary + installer + icon into a `.tar.gz` - github actions publishes both to every release automatically. see the [architecture overview](/docs/architecture/) for deeper design rationale.
addon api reference
complete api reference for developing gguf loader addons
advanced 20 minutescomplete reference for the hooks, signals, and objects available to gguf loader addons.
πͺ addon contract
an addon is a folder under
addons/with an__init__.pyexposing:register(parent=none) -> qwidget | nonecalled by
addonmanagerwhen the addon loads. returns an optional widget shown in the addons menu (opened in a non-modal dialog), ornonefor background-only addons.parameters β
parent: the widget the addon is being attached to (may be the main window, a dialog, or another widget).return β a widget, or
none.def register(parent=none): return qlabel("my addon") # widget addonπ₯οΈ main window api
addons receive a reference to the main window (via
parentor by scanningqapplication.instance().toplevelwidgets()). it exposes:properties
| member | type | description | |β|β|β| |
model|modelbackend \| none| the loaded model backend; callable.noneuntil a model is loaded. | |chat_generator|none| legacy hook; alwaysnone. addons should callmodelinstead. | |addon_manager|addonmanager| the addon manager instance. | |_floating_chat_addon|floatingchataddon \| none| the built-in floating chat addon (if running). |signals
| signal | signature | description | |β|β|β| |
model_loaded|signal(object)| emitted with themodelbackendafter a model loads. | |model_unloaded|signal()| emitted after the model is released. | |generation_finished|signal()| emitted when a chat generation completes. | |generation_error|signal(str)| emitted with an error message when generation fails. | |theme_changed|signal(bool)| emitted when dark mode toggles (true= dark). |gguf_app.model_loaded.connect(self._on_model_loaded) gguf_app.generation_finished.connect(self._on_finished)π§ modelbackend
gguf_app.modelis acore.llm.model_backend.modelbackend. it is callable and thread-safe (all access serialized through a lock).calling convention
# raw llama-cpp shape (addon-compatible) result = gguf_app.model(prompt, stream=false, max_tokens=512, temperature=0.7) text = result["choices"][0]["text"] # streaming for token_data in gguf_app.model(prompt, stream=true, max_tokens=512): text = token_data["choices"][0]["text"]methods & attributes
| member | description | |β|β| |
model_path| path of the loaded gguf file. | |use_gpu| whether gpu acceleration is active. | |n_ctx| context length. | |is_loaded|trueonce the runtime exists. | |load()| create the underlyingllamaruntime (raises on failure). | |unload()| release the model and free memory. | |generate(prompt, **kwargs)| complete non-streamed response string. | |generate_stream(prompt, **kwargs)| iterator of token strings. | |__call__(prompt, **kwargs)| llama-cpp-compatible callable (stream returns token-dict generator). |kwargs are passed straight to llama_cpp:
max_tokens,temperature,top_p,top_k,repeat_penalty,stop, etc.π§° agent tools (for agent-capable addons)
the agent engine (
core/agent/) exposes sandboxed tools throughtoolregistry. tools are not qt-bound and are safe to call from any thread.tool params result list_directorypath(default.){status, result: [{name, type, size}]}read_filepath,max_size,encoding{status, result: content, lines, encoding}write_filepath,content{status, path, bytes_written}edit_filepath,operation(replace/insert_line/delete_line),find,replace,line_number,content{status, operation, changes_made}search_filespattern/query,path{status, result: [paths], total_matches}every tool result includes
status("success"/"error") andtool_name. all paths are sandboxed to the workspace β escaping paths raise.from core.agent.tool_registry import create_default_registry registry = create_default_registry("./workspace") result = registry.execute("list_directory", {"path": "."})π§© addonmanager
method description scan_addons(){name: path}of addons with__init__.py.load_addon(name, path)import + validate register;true/false.load_all_addons()load all; {name: success}.get_addon_widget(name, parent)widget from register(parent).open_addon_dialog(name, parent)show addon in a non-modal dialog. get_loaded_addons()list of successfully loaded names. πΎ persistence
use qtβs
qsettingswith a scoped org/app key to avoid collisions:from pyside6.qtcore import qsettings settings = qsettings("ggufloader", "myaddon") settings.setvalue("position", point) pos = settings.value("position", qpoint(100, 100))β‘ lifecycle
register()is called at startup and on addons β refresh addons.- the main windowβs
closeeventstops the floating chat addon; follow that pattern to stop your own background objects:if hasattr(gguf_app, '_my_addon') and gguf_app._my_addon: gguf_app._my_addon.stop()
π see also
- addon development guide β tutorial
- floating chat example β full real-world addon
related documentation
- gguf loader's addon system lets you extend the app without touching its source. addons are python packages dropped into the `addons/` folder; the app discovers, loads, and manages them automatically.
## π¦ what an addon looks like
```
addons/
βββ my_addon/
βββ __init__.py # must expose register()
βββ main.py # your logic (any structure you like)
βββ ... # widgets, resources, etc.
```
### minimum viable addon
```python
# addons/my_addon/__init__.py
from pyside6.qtwidgets import qlabel
def register(parent=none):
"""called by gguf loader when the addon loads."""
return qlabel("hello from my addon!")
```
that's it β drop the folder in `addons/`, restart (or **addons β refresh addons**), and your widget appears in the **addons** menu.
## π how loading works
`addonmanager` (`addon_manager.py`):
1. `scan_addons()` β lists subdirectories of the addons folder that contain `__init__.py`.
2. `load_addon(name, path)` β dynamically imports the module (`importlib.util.spec_from_file_location`) and requires a `register` attribute.
3. `load_all_addons()` β loads every addon and records success/failure in the console log.
4. `get_addon_widget(name, parent)` β calls `register(parent)` and returns the widget.
5. `open_addon_dialog(name, parent)` β shows the addon widget in a non-modal dialog from the addons menu.
the main window calls `get_addon_widget` for each loaded addon at startup, so `register()` runs even before the user opens the menu β this is how background addons (like the floating chat button) start automatically.
## π‘ talking to the app
your `register(parent)` receives the parent widget. the **main window** exposes an addon-facing api (see the [addon api reference](/docs/addon-api/)). the robust pattern used by the built-in addon:
```python
def register(parent=none):
gguf_app = none
# 1. parent might already be the main window
if parent and hasattr(parent, 'model') and hasattr(parent, 'model_loaded'):
gguf_app = parent
else:
# 2. walk up the parent chain
current = parent
while current is not none:
if hasattr(current, 'model') and hasattr(current, 'model_loaded'):
gguf_app = current
break
current = current.parent()
# 3. fall back to scanning top-level widgets
if gguf_app is none:
app = qapplication.instance()
for widget in app.toplevelwidgets():
if hasattr(widget, 'model') and hasattr(widget, 'model_loaded'):
gguf_app = widget
break
...
```
key api members: `model` (callable backend), `model_loaded` / `model_unloaded` / `generation_finished` / `generation_error` signals, `addon_manager`, and `_floating_chat_addon`.
## π€ calling the model
```python
# streaming
for token in gguf_app.model(prompt, stream=true, max_tokens=512):
print(token["choices"][0]["text"])
# or use the backend directly
response = gguf_app.model.generate(prompt, max_tokens=512, temperature=0.7)
```
`model` is `none` until the user loads a model β always check, and subscribe to `model_loaded` to react.
## π§© widget vs. background addons
- **return a widget** from `register()` β appears in the addons menu, opens in a dialog.
- **return `none`** (or also keep your own top-level windows) β runs in the background. the floating chat addon returns a small status widget but drives its own always-on-top windows.
## π‘ best practices
1. **guard everything** β wrap registration in `try/except` and log failures; a broken addon must never crash the app.
2. **clean up state** β if you keep references on the main window (e.g. `gguf_app._my_addon = ...`), stop them on app close (`mainwindow.closeevent` stops `_floating_chat_addon` β follow that pattern).
3. **use qsettings for persistence** β `qsettings("ggufloader", "youraddon")`.
4. **prefer signals** β let your widgets emit signals; don't reach into other widgets' internals.
5. **qt-free logic** β if you have non-ui logic (parsing, computation), put it in a module with no qt imports so it's unit-testable.
## β
debugging
addon load errors print to the console with a traceback (`failed to load addon
: ...`). run the app from a terminal to see them: ```bash python main.py ``` ## π next steps - [addon api reference](/docs/addon-api/) β every hook and member - [floating chat example](/docs/floating-chat-example/) β full walkthrough of the built-in addon - this page walks through **floating_chat**, gguf loader's built-in addon β a facebook messenger-style floating button that opens a chat window connected to the loaded model. it's the best real-world reference for writing a background addon with its own windows. ``` addons/floating_chat/ βββ __init__.py # exposes register() βββ main.py # floatingchataddon (qobject) + register() βββ floating_button.py # floatingchatbutton - draggable always-on-top button βββ chat_window.py # floatingchatwindow - the chat ui βββ status_widget.py # small sidebar widget returned by register() ``` ## π§ the design the addon is a `qobject` (`floatingchataddon`) that owns two windows: - **`floatingchatbutton`** β a frameless, always-on-top tool window that can be dragged anywhere and remembers its position via `qsettings`. - **`floatingchatwindow`** β a regular top-level window (`400Γ600`) positioned next to the button, with a message list, streaming replies, and copy all / clear buttons. ## πͺ registration `__init__.py` simply re-exports `register`: ```python from .main import register __all__ = ['register'] ``` `register()` finds the main window (parent β parent chain β `toplevelwidgets()` scan), stops any previous instance, starts the addon, and returns a **status widget** for the sidebar: ```python def register(parent=none): gguf_app = _find_main_window(parent) # model + model_loaded check if gguf_app is none: return none if getattr(gguf_app, '_floating_chat_addon', none): gguf_app._floating_chat_addon.stop() addon = floatingchataddon(gguf_app) if addon.start(): gguf_app._floating_chat_addon = addon return floatingchatstatuswidget(addon) return none ``` the returned `floatingchatstatuswidget` shows up in the **addons** menu, and because `register()` runs at startup, the button appears automatically β no user action needed. ## π‘ connecting to the app `floatingchataddon.__init__` subscribes to model lifecycle signals: ```python if hasattr(self.gguf_app, 'model_loaded'): self.gguf_app.model_loaded.connect(self._on_model_loaded) if hasattr(self.gguf_app, 'generation_finished'): self.gguf_app.generation_finished.connect(self._on_generation_finished) if hasattr(self.gguf_app, 'generation_error'): self.gguf_app.generation_error.connect(self._on_generation_error) ``` `_on_model_loaded` flips the chat window's status to **π’ model: ready**; generation events update the streaming bubble / error state. ## π lifecycle: start() / stop() ```python def start(self): self._floating_button = floatingchatbutton() self._floating_button.clicked.connect(self._on_button_clicked) self._load_button_position() # qsettings restore, clamped to screen self._floating_button.show() self.addon_started.emit() return true def stop(self): self._save_button_position() # persist for next session if self._chat_window: self._chat_window.close() if self._floating_button: self._floating_button.close() self.addon_stopped.emit() ``` `stop()` is idempotent and called on app close via `mainwindow.closeevent`, so there's never a dangling button. ## π±οΈ button click β window toggle the click handler distinguishes three states (a minimized window is "visible" to qt but off-screen, so it must be restored, not hidden): ```python def _on_button_clicked(self): chat = self._chat_window if chat and chat.isvisible(): if chat.isminimized(): chat.shownormal(); chat.raise_(); chat.activatewindow() else: chat.hide() else: self._show_chat_window() ``` `_show_chat_window` creates the window lazily, positions it next to the button (flipping to the left / up when it would overflow the screen's **available geometry**, which excludes taskbars and docks), then `shownormal()` + `raise_()` + `activatewindow()`. ## πͺ window flags (the platform subtleties) ```python flags = (qt.windowtype.framelesswindowhint | qt.windowtype.windowstaysontophint | qt.windowtype.x11bypasswindowmanagerhint) # linux/x11 if sys.platform != "darwin": flags |= qt.windowtype.tool # keeps it out of the taskbar ``` - **windows/linux**: `tool` keeps the button out of the taskbar. `x11bypasswindowmanagerhint` gives true always-on-top under x11. - **macos**: the `tool` flag would turn the button into a utility window that **auto-hides when the app loses focus** β so it's deliberately dropped. trade-off: the button appears in mission control. - **wayland**: compositors don't allow floating above *other* apps' windows; the button is confined to the app window (documented, and x11 is recommended for the full experience). the chat window drops the minimize button entirely β a minimized companion window is a trap state that can't be reliably restored. ## π resiliancy touches - `changeevent` bounces the button straight back to normal if a system shortcut (e.g. win+d) minimizes *every* window including the tool: ```python if event.type() == qevent.type.windowstatechange and self.isminimized(): qtimer.singleshot(0, self.shownormal) ``` - dragging clamps to `screen.availablegeometry()` (not `(0,0)`), so the button can't slide under the macos menu bar or a windows taskbar. - saved positions are clamped the same way on restore. ## π§© what you can reuse - the **window-finding pattern** in `register()` β works for any addon. - the **qsettings position persistence** with on-screen clamping. - the **platform-conditional window flags** β copy it for any always-on-top addon window. - the **show/hide/restore toggle** logic. ## π see also - [addon development guide](/docs/addon-development/) - [addon api reference](/docs/addon-api/)
- this page documents how the gguf loader codebase is organized. the app is a pyside6 desktop application; a single `main.py` bootstraps everything, and logic is split into `core/` (pure logic), `services/` (qt threading bridges), and `ui/` + `widgets/` (presentation). ## ποΈ top-level layout ``` gguf-loader/ βββ main.py # entry point: logging, dll paths, qapplication, mainwindow βββ config.py # central configuration constants βββ resource_manager.py # resource/path discovery (dev, package, or frozen) βββ addon_manager.py # loads and manages addons βββ requirements.txt # python dependencies βββ build_exe.spec # pyinstaller spec used for windows & linux builds β βββ core/ # pure, testable logic (no qt) β βββ llm/ β β βββ model_backend.py # thread-safe llama-cpp-python wrapper β β βββ prompt_builder.py # system-prompt & conversation assembly β βββ agent/ β βββ agent_engine.py # tool-use agent loop (no qt, no llama_cpp) β βββ tool_registry.py # sandboxed workspace tools β βββ services/ # qobject bridges that run work on threads β βββ model_service.py # load/unload models on a qthread β βββ chat_service.py # streaming generation on a qthread β βββ agent_service.py # runs agentengine on a worker thread β βββ environment_service.py # venv/dependency checks & pip tasks β βββ launcher_service.py # launches scripts/ utilities β βββ ui/ # main window & panels β βββ main_window.py # composition root + addon-facing api β βββ chat_panel.py # chat display, input, agent controls β βββ sidebar_panel.py # model settings sidebar β βββ theme.py # dark/light qss token system β βββ widgets/ # reusable widgets β βββ chat_bubble.py # chatgpt-style bubble β βββ feedback_dialog.py # feedback form dialog β βββ addons/ # addon packages (each has __init__.py with register()) β βββ floating_chat/ # built-in floating chat addon β βββ scripts/ # utility & release scripts β βββ capture_screenshots.py # regenerates readme/site screenshots β βββ install_linux.sh # linux installer/uninstaller β βββ package_linux.sh # builds the linux .tar.gz release β βββ ... # gpu install/monitor helpers β βββ build_hooks/ # pyinstaller hook modules ``` ## π key design rules - **`core/` never imports qt or llama_cpp.** it receives plain callables, so it can be unit-tested in isolation. - **`services/` are the only place qt threads are created.** ui never spins up threads directly. - **`ui/` widgets are "dumb"** β they render state and emit signals; `mainwindow` owns all logic. - **`resource_manager.py`** makes paths work identically in dev, as an installed package, and in a pyinstaller bundle (`sys._meipass`). ## π§΅ threading model ``` ui thread (mainwindow) worker thread β β βββ modelservice.load() βββββββ qthread: llama_cpp loads model ββββββββ loaded(modelbackend) β βββ chatservice.generate() ββββ qthread: streams tokens ββββββββ token_received(text) β βββ agentservice.process() ββββ qthread: agentengine tool loop ββββββββ status/tool/response β ``` a fresh `qthread` + worker is created per request (the professional qt pattern β `qthread` is never subclassed). ## π entry point flow `main.py` β `setup_library_path()` (finds bundled llama.cpp libs) β `qapplication` β `mainwindow()` β builds ui, wires services, checks environment, loads addons β `app.exec()`. ## π¦ packaging - **windows**: `build_exe.bat` / `build_exe.spec` β `ggufloader_vx.y.z.exe` - **linux**: build the spec inside a linux environment, then `scripts/package_linux.sh` wraps the binary + installer + icon into a `.tar.gz` - github actions publishes both to every release automatically. see the [architecture overview](/docs/architecture/) for deeper design rationale.
π related features
π― what's next?
you've completed this guide! here are some suggested next steps to continue your gguf loader journey:
πshare your addon
built something awesome? share it with the community and get featured on our homepage.
share with community βππ€π explore more features
π back to homepage
addon development guide
learn to create custom addons for gguf loader with examples and best practices
advanced 15 minutesgguf loaderβs addon system lets you extend the app without touching its source. addons are python packages dropped into the
addons/folder; the app discovers, loads, and manages them automatically.π¦ what an addon looks like
addons/ βββ my_addon/ βββ __init__.py # must expose register() βββ main.py # your logic (any structure you like) βββ ... # widgets, resources, etc.minimum viable addon
# addons/my_addon/__init__.py from pyside6.qtwidgets import qlabel def register(parent=none): """called by gguf loader when the addon loads.""" return qlabel("hello from my addon!")thatβs it β drop the folder in
addons/, restart (or addons β refresh addons), and your widget appears in the addons menu.π how loading works
addonmanager(addon_manager.py):scan_addons()β lists subdirectories of the addons folder that contain__init__.py.load_addon(name, path)β dynamically imports the module (importlib.util.spec_from_file_location) and requires aregisterattribute.load_all_addons()β loads every addon and records success/failure in the console log.get_addon_widget(name, parent)β callsregister(parent)and returns the widget.open_addon_dialog(name, parent)β shows the addon widget in a non-modal dialog from the addons menu.
the main window calls
get_addon_widgetfor each loaded addon at startup, soregister()runs even before the user opens the menu β this is how background addons (like the floating chat button) start automatically.π‘ talking to the app
your
register(parent)receives the parent widget. the main window exposes an addon-facing api (see the addon api reference). the robust pattern used by the built-in addon:def register(parent=none): gguf_app = none # 1. parent might already be the main window if parent and hasattr(parent, 'model') and hasattr(parent, 'model_loaded'): gguf_app = parent else: # 2. walk up the parent chain current = parent while current is not none: if hasattr(current, 'model') and hasattr(current, 'model_loaded'): gguf_app = current break current = current.parent() # 3. fall back to scanning top-level widgets if gguf_app is none: app = qapplication.instance() for widget in app.toplevelwidgets(): if hasattr(widget, 'model') and hasattr(widget, 'model_loaded'): gguf_app = widget break ...key api members:
model(callable backend),model_loaded/model_unloaded/generation_finished/generation_errorsignals,addon_manager, and_floating_chat_addon.π€ calling the model
# streaming for token in gguf_app.model(prompt, stream=true, max_tokens=512): print(token["choices"][0]["text"]) # or use the backend directly response = gguf_app.model.generate(prompt, max_tokens=512, temperature=0.7)modelisnoneuntil the user loads a model β always check, and subscribe tomodel_loadedto react.π§© widget vs. background addons
- return a widget from
register()β appears in the addons menu, opens in a dialog. - return
none(or also keep your own top-level windows) β runs in the background. the floating chat addon returns a small status widget but drives its own always-on-top windows.
π‘ best practices
- guard everything β wrap registration in
try/exceptand log failures; a broken addon must never crash the app. - clean up state β if you keep references on the main window (e.g.
gguf_app._my_addon = ...), stop them on app close (mainwindow.closeeventstops_floating_chat_addonβ follow that pattern). - use qsettings for persistence β
qsettings("ggufloader", "youraddon"). - prefer signals β let your widgets emit signals; donβt reach into other widgetsβ internals.
- qt-free logic β if you have non-ui logic (parsing, computation), put it in a module with no qt imports so itβs unit-testable.
β debugging
addon load errors print to the console with a traceback (
failed to load addon <name>: ...). run the app from a terminal to see them:python main.pyπ next steps
- addon api reference β every hook and member
- floating chat example β full walkthrough of the built-in addon
related documentation
addon api reference
complete api reference for developing gguf loader addons
advanced 20 minutescomplete reference for the hooks, signals, and objects available to gguf loader addons.
πͺ addon contract
an addon is a folder under
addons/with an__init__.pyexposing:register(parent=none) -> qwidget | nonecalled by
addonmanagerwhen the addon loads. returns an optional widget shown in the addons menu (opened in a non-modal dialog), ornonefor background-only addons.parameters β
parent: the widget the addon is being attached to (may be the main window, a dialog, or another widget).return β a widget, or
none.def register(parent=none): return qlabel("my addon") # widget addonπ₯οΈ main window api
addons receive a reference to the main window (via
parentor by scanningqapplication.instance().toplevelwidgets()). it exposes:properties
| member | type | description | |β|β|β| |
model|modelbackend \| none| the loaded model backend; callable.noneuntil a model is loaded. | |chat_generator|none| legacy hook; alwaysnone. addons should callmodelinstead. | |addon_manager|addonmanager| the addon manager instance. | |_floating_chat_addon|floatingchataddon \| none| the built-in floating chat addon (if running). |signals
| signal | signature | description | |β|β|β| |
model_loaded|signal(object)| emitted with themodelbackendafter a model loads. | |model_unloaded|signal()| emitted after the model is released. | |generation_finished|signal()| emitted when a chat generation completes. | |generation_error|signal(str)| emitted with an error message when generation fails. | |theme_changed|signal(bool)| emitted when dark mode toggles (true= dark). |gguf_app.model_loaded.connect(self._on_model_loaded) gguf_app.generation_finished.connect(self._on_finished)π§ modelbackend
gguf_app.modelis acore.llm.model_backend.modelbackend. it is callable and thread-safe (all access serialized through a lock).calling convention
# raw llama-cpp shape (addon-compatible) result = gguf_app.model(prompt, stream=false, max_tokens=512, temperature=0.7) text = result["choices"][0]["text"] # streaming for token_data in gguf_app.model(prompt, stream=true, max_tokens=512): text = token_data["choices"][0]["text"]methods & attributes
| member | description | |β|β| |
model_path| path of the loaded gguf file. | |use_gpu| whether gpu acceleration is active. | |n_ctx| context length. | |is_loaded|trueonce the runtime exists. | |load()| create the underlyingllamaruntime (raises on failure). | |unload()| release the model and free memory. | |generate(prompt, **kwargs)| complete non-streamed response string. | |generate_stream(prompt, **kwargs)| iterator of token strings. | |__call__(prompt, **kwargs)| llama-cpp-compatible callable (stream returns token-dict generator). |kwargs are passed straight to llama_cpp:
max_tokens,temperature,top_p,top_k,repeat_penalty,stop, etc.π§° agent tools (for agent-capable addons)
the agent engine (
core/agent/) exposes sandboxed tools throughtoolregistry. tools are not qt-bound and are safe to call from any thread.tool params result list_directorypath(default.){status, result: [{name, type, size}]}read_filepath,max_size,encoding{status, result: content, lines, encoding}write_filepath,content{status, path, bytes_written}edit_filepath,operation(replace/insert_line/delete_line),find,replace,line_number,content{status, operation, changes_made}search_filespattern/query,path{status, result: [paths], total_matches}every tool result includes
status("success"/"error") andtool_name. all paths are sandboxed to the workspace β escaping paths raise.from core.agent.tool_registry import create_default_registry registry = create_default_registry("./workspace") result = registry.execute("list_directory", {"path": "."})π§© addonmanager
method description scan_addons(){name: path}of addons with__init__.py.load_addon(name, path)import + validate register;true/false.load_all_addons()load all; {name: success}.get_addon_widget(name, parent)widget from register(parent).open_addon_dialog(name, parent)show addon in a non-modal dialog. get_loaded_addons()list of successfully loaded names. πΎ persistence
use qtβs
qsettingswith a scoped org/app key to avoid collisions:from pyside6.qtcore import qsettings settings = qsettings("ggufloader", "myaddon") settings.setvalue("position", point) pos = settings.value("position", qpoint(100, 100))β‘ lifecycle
register()is called at startup and on addons β refresh addons.- the main windowβs
closeeventstops the floating chat addon; follow that pattern to stop your own background objects:if hasattr(gguf_app, '_my_addon') and gguf_app._my_addon: gguf_app._my_addon.stop()
π see also
- addon development guide β tutorial
- floating chat example β full real-world addon
related documentation
- gguf loader's addon system lets you extend the app without touching its source. addons are python packages dropped into the `addons/` folder; the app discovers, loads, and manages them automatically.
## π¦ what an addon looks like
```
addons/
βββ my_addon/
βββ __init__.py # must expose register()
βββ main.py # your logic (any structure you like)
βββ ... # widgets, resources, etc.
```
### minimum viable addon
```python
# addons/my_addon/__init__.py
from pyside6.qtwidgets import qlabel
def register(parent=none):
"""called by gguf loader when the addon loads."""
return qlabel("hello from my addon!")
```
that's it β drop the folder in `addons/`, restart (or **addons β refresh addons**), and your widget appears in the **addons** menu.
## π how loading works
`addonmanager` (`addon_manager.py`):
1. `scan_addons()` β lists subdirectories of the addons folder that contain `__init__.py`.
2. `load_addon(name, path)` β dynamically imports the module (`importlib.util.spec_from_file_location`) and requires a `register` attribute.
3. `load_all_addons()` β loads every addon and records success/failure in the console log.
4. `get_addon_widget(name, parent)` β calls `register(parent)` and returns the widget.
5. `open_addon_dialog(name, parent)` β shows the addon widget in a non-modal dialog from the addons menu.
the main window calls `get_addon_widget` for each loaded addon at startup, so `register()` runs even before the user opens the menu β this is how background addons (like the floating chat button) start automatically.
## π‘ talking to the app
your `register(parent)` receives the parent widget. the **main window** exposes an addon-facing api (see the [addon api reference](/docs/addon-api/)). the robust pattern used by the built-in addon:
```python
def register(parent=none):
gguf_app = none
# 1. parent might already be the main window
if parent and hasattr(parent, 'model') and hasattr(parent, 'model_loaded'):
gguf_app = parent
else:
# 2. walk up the parent chain
current = parent
while current is not none:
if hasattr(current, 'model') and hasattr(current, 'model_loaded'):
gguf_app = current
break
current = current.parent()
# 3. fall back to scanning top-level widgets
if gguf_app is none:
app = qapplication.instance()
for widget in app.toplevelwidgets():
if hasattr(widget, 'model') and hasattr(widget, 'model_loaded'):
gguf_app = widget
break
...
```
key api members: `model` (callable backend), `model_loaded` / `model_unloaded` / `generation_finished` / `generation_error` signals, `addon_manager`, and `_floating_chat_addon`.
## π€ calling the model
```python
# streaming
for token in gguf_app.model(prompt, stream=true, max_tokens=512):
print(token["choices"][0]["text"])
# or use the backend directly
response = gguf_app.model.generate(prompt, max_tokens=512, temperature=0.7)
```
`model` is `none` until the user loads a model β always check, and subscribe to `model_loaded` to react.
## π§© widget vs. background addons
- **return a widget** from `register()` β appears in the addons menu, opens in a dialog.
- **return `none`** (or also keep your own top-level windows) β runs in the background. the floating chat addon returns a small status widget but drives its own always-on-top windows.
## π‘ best practices
1. **guard everything** β wrap registration in `try/except` and log failures; a broken addon must never crash the app.
2. **clean up state** β if you keep references on the main window (e.g. `gguf_app._my_addon = ...`), stop them on app close (`mainwindow.closeevent` stops `_floating_chat_addon` β follow that pattern).
3. **use qsettings for persistence** β `qsettings("ggufloader", "youraddon")`.
4. **prefer signals** β let your widgets emit signals; don't reach into other widgets' internals.
5. **qt-free logic** β if you have non-ui logic (parsing, computation), put it in a module with no qt imports so it's unit-testable.
## β
debugging
addon load errors print to the console with a traceback (`failed to load addon
: ...`). run the app from a terminal to see them: ```bash python main.py ``` ## π next steps - [addon api reference](/docs/addon-api/) β every hook and member - [floating chat example](/docs/floating-chat-example/) β full walkthrough of the built-in addon - this page walks through **floating_chat**, gguf loader's built-in addon β a facebook messenger-style floating button that opens a chat window connected to the loaded model. it's the best real-world reference for writing a background addon with its own windows. ``` addons/floating_chat/ βββ __init__.py # exposes register() βββ main.py # floatingchataddon (qobject) + register() βββ floating_button.py # floatingchatbutton - draggable always-on-top button βββ chat_window.py # floatingchatwindow - the chat ui βββ status_widget.py # small sidebar widget returned by register() ``` ## π§ the design the addon is a `qobject` (`floatingchataddon`) that owns two windows: - **`floatingchatbutton`** β a frameless, always-on-top tool window that can be dragged anywhere and remembers its position via `qsettings`. - **`floatingchatwindow`** β a regular top-level window (`400Γ600`) positioned next to the button, with a message list, streaming replies, and copy all / clear buttons. ## πͺ registration `__init__.py` simply re-exports `register`: ```python from .main import register __all__ = ['register'] ``` `register()` finds the main window (parent β parent chain β `toplevelwidgets()` scan), stops any previous instance, starts the addon, and returns a **status widget** for the sidebar: ```python def register(parent=none): gguf_app = _find_main_window(parent) # model + model_loaded check if gguf_app is none: return none if getattr(gguf_app, '_floating_chat_addon', none): gguf_app._floating_chat_addon.stop() addon = floatingchataddon(gguf_app) if addon.start(): gguf_app._floating_chat_addon = addon return floatingchatstatuswidget(addon) return none ``` the returned `floatingchatstatuswidget` shows up in the **addons** menu, and because `register()` runs at startup, the button appears automatically β no user action needed. ## π‘ connecting to the app `floatingchataddon.__init__` subscribes to model lifecycle signals: ```python if hasattr(self.gguf_app, 'model_loaded'): self.gguf_app.model_loaded.connect(self._on_model_loaded) if hasattr(self.gguf_app, 'generation_finished'): self.gguf_app.generation_finished.connect(self._on_generation_finished) if hasattr(self.gguf_app, 'generation_error'): self.gguf_app.generation_error.connect(self._on_generation_error) ``` `_on_model_loaded` flips the chat window's status to **π’ model: ready**; generation events update the streaming bubble / error state. ## π lifecycle: start() / stop() ```python def start(self): self._floating_button = floatingchatbutton() self._floating_button.clicked.connect(self._on_button_clicked) self._load_button_position() # qsettings restore, clamped to screen self._floating_button.show() self.addon_started.emit() return true def stop(self): self._save_button_position() # persist for next session if self._chat_window: self._chat_window.close() if self._floating_button: self._floating_button.close() self.addon_stopped.emit() ``` `stop()` is idempotent and called on app close via `mainwindow.closeevent`, so there's never a dangling button. ## π±οΈ button click β window toggle the click handler distinguishes three states (a minimized window is "visible" to qt but off-screen, so it must be restored, not hidden): ```python def _on_button_clicked(self): chat = self._chat_window if chat and chat.isvisible(): if chat.isminimized(): chat.shownormal(); chat.raise_(); chat.activatewindow() else: chat.hide() else: self._show_chat_window() ``` `_show_chat_window` creates the window lazily, positions it next to the button (flipping to the left / up when it would overflow the screen's **available geometry**, which excludes taskbars and docks), then `shownormal()` + `raise_()` + `activatewindow()`. ## πͺ window flags (the platform subtleties) ```python flags = (qt.windowtype.framelesswindowhint | qt.windowtype.windowstaysontophint | qt.windowtype.x11bypasswindowmanagerhint) # linux/x11 if sys.platform != "darwin": flags |= qt.windowtype.tool # keeps it out of the taskbar ``` - **windows/linux**: `tool` keeps the button out of the taskbar. `x11bypasswindowmanagerhint` gives true always-on-top under x11. - **macos**: the `tool` flag would turn the button into a utility window that **auto-hides when the app loses focus** β so it's deliberately dropped. trade-off: the button appears in mission control. - **wayland**: compositors don't allow floating above *other* apps' windows; the button is confined to the app window (documented, and x11 is recommended for the full experience). the chat window drops the minimize button entirely β a minimized companion window is a trap state that can't be reliably restored. ## π resiliancy touches - `changeevent` bounces the button straight back to normal if a system shortcut (e.g. win+d) minimizes *every* window including the tool: ```python if event.type() == qevent.type.windowstatechange and self.isminimized(): qtimer.singleshot(0, self.shownormal) ``` - dragging clamps to `screen.availablegeometry()` (not `(0,0)`), so the button can't slide under the macos menu bar or a windows taskbar. - saved positions are clamped the same way on restore. ## π§© what you can reuse - the **window-finding pattern** in `register()` β works for any addon. - the **qsettings position persistence** with on-screen clamping. - the **platform-conditional window flags** β copy it for any always-on-top addon window. - the **show/hide/restore toggle** logic. ## π see also - [addon development guide](/docs/addon-development/) - [addon api reference](/docs/addon-api/)
- this page documents how the gguf loader codebase is organized. the app is a pyside6 desktop application; a single `main.py` bootstraps everything, and logic is split into `core/` (pure logic), `services/` (qt threading bridges), and `ui/` + `widgets/` (presentation). ## ποΈ top-level layout ``` gguf-loader/ βββ main.py # entry point: logging, dll paths, qapplication, mainwindow βββ config.py # central configuration constants βββ resource_manager.py # resource/path discovery (dev, package, or frozen) βββ addon_manager.py # loads and manages addons βββ requirements.txt # python dependencies βββ build_exe.spec # pyinstaller spec used for windows & linux builds β βββ core/ # pure, testable logic (no qt) β βββ llm/ β β βββ model_backend.py # thread-safe llama-cpp-python wrapper β β βββ prompt_builder.py # system-prompt & conversation assembly β βββ agent/ β βββ agent_engine.py # tool-use agent loop (no qt, no llama_cpp) β βββ tool_registry.py # sandboxed workspace tools β βββ services/ # qobject bridges that run work on threads β βββ model_service.py # load/unload models on a qthread β βββ chat_service.py # streaming generation on a qthread β βββ agent_service.py # runs agentengine on a worker thread β βββ environment_service.py # venv/dependency checks & pip tasks β βββ launcher_service.py # launches scripts/ utilities β βββ ui/ # main window & panels β βββ main_window.py # composition root + addon-facing api β βββ chat_panel.py # chat display, input, agent controls β βββ sidebar_panel.py # model settings sidebar β βββ theme.py # dark/light qss token system β βββ widgets/ # reusable widgets β βββ chat_bubble.py # chatgpt-style bubble β βββ feedback_dialog.py # feedback form dialog β βββ addons/ # addon packages (each has __init__.py with register()) β βββ floating_chat/ # built-in floating chat addon β βββ scripts/ # utility & release scripts β βββ capture_screenshots.py # regenerates readme/site screenshots β βββ install_linux.sh # linux installer/uninstaller β βββ package_linux.sh # builds the linux .tar.gz release β βββ ... # gpu install/monitor helpers β βββ build_hooks/ # pyinstaller hook modules ``` ## π key design rules - **`core/` never imports qt or llama_cpp.** it receives plain callables, so it can be unit-tested in isolation. - **`services/` are the only place qt threads are created.** ui never spins up threads directly. - **`ui/` widgets are "dumb"** β they render state and emit signals; `mainwindow` owns all logic. - **`resource_manager.py`** makes paths work identically in dev, as an installed package, and in a pyinstaller bundle (`sys._meipass`). ## π§΅ threading model ``` ui thread (mainwindow) worker thread β β βββ modelservice.load() βββββββ qthread: llama_cpp loads model ββββββββ loaded(modelbackend) β βββ chatservice.generate() ββββ qthread: streams tokens ββββββββ token_received(text) β βββ agentservice.process() ββββ qthread: agentengine tool loop ββββββββ status/tool/response β ``` a fresh `qthread` + worker is created per request (the professional qt pattern β `qthread` is never subclassed). ## π entry point flow `main.py` β `setup_library_path()` (finds bundled llama.cpp libs) β `qapplication` β `mainwindow()` β builds ui, wires services, checks environment, loads addons β `app.exec()`. ## π¦ packaging - **windows**: `build_exe.bat` / `build_exe.spec` β `ggufloader_vx.y.z.exe` - **linux**: build the spec inside a linux environment, then `scripts/package_linux.sh` wraps the binary + installer + icon into a `.tar.gz` - github actions publishes both to every release automatically. see the [architecture overview](/docs/architecture/) for deeper design rationale.
π related features
π― what's next?
you've completed this guide! here are some suggested next steps to continue your gguf loader journey:
πshare your addon
built something awesome? share it with the community and get featured on our homepage.
share with community βππ€π explore more features
π back to homepage
- this page walks through **floating_chat**, gguf loader's built-in addon β a facebook messenger-style floating button that opens a chat window connected to the loaded model. it's the best real-world reference for writing a background addon with its own windows. ``` addons/floating_chat/ βββ __init__.py # exposes register() βββ main.py # floatingchataddon (qobject) + register() βββ floating_button.py # floatingchatbutton - draggable always-on-top button βββ chat_window.py # floatingchatwindow - the chat ui βββ status_widget.py # small sidebar widget returned by register() ``` ## π§ the design the addon is a `qobject` (`floatingchataddon`) that owns two windows: - **`floatingchatbutton`** β a frameless, always-on-top tool window that can be dragged anywhere and remembers its position via `qsettings`. - **`floatingchatwindow`** β a regular top-level window (`400Γ600`) positioned next to the button, with a message list, streaming replies, and copy all / clear buttons. ## πͺ registration `__init__.py` simply re-exports `register`: ```python from .main import register __all__ = ['register'] ``` `register()` finds the main window (parent β parent chain β `toplevelwidgets()` scan), stops any previous instance, starts the addon, and returns a **status widget** for the sidebar: ```python def register(parent=none): gguf_app = _find_main_window(parent) # model + model_loaded check if gguf_app is none: return none if getattr(gguf_app, '_floating_chat_addon', none): gguf_app._floating_chat_addon.stop() addon = floatingchataddon(gguf_app) if addon.start(): gguf_app._floating_chat_addon = addon return floatingchatstatuswidget(addon) return none ``` the returned `floatingchatstatuswidget` shows up in the **addons** menu, and because `register()` runs at startup, the button appears automatically β no user action needed. ## π‘ connecting to the app `floatingchataddon.__init__` subscribes to model lifecycle signals: ```python if hasattr(self.gguf_app, 'model_loaded'): self.gguf_app.model_loaded.connect(self._on_model_loaded) if hasattr(self.gguf_app, 'generation_finished'): self.gguf_app.generation_finished.connect(self._on_generation_finished) if hasattr(self.gguf_app, 'generation_error'): self.gguf_app.generation_error.connect(self._on_generation_error) ``` `_on_model_loaded` flips the chat window's status to **π’ model: ready**; generation events update the streaming bubble / error state. ## π lifecycle: start() / stop() ```python def start(self): self._floating_button = floatingchatbutton() self._floating_button.clicked.connect(self._on_button_clicked) self._load_button_position() # qsettings restore, clamped to screen self._floating_button.show() self.addon_started.emit() return true def stop(self): self._save_button_position() # persist for next session if self._chat_window: self._chat_window.close() if self._floating_button: self._floating_button.close() self.addon_stopped.emit() ``` `stop()` is idempotent and called on app close via `mainwindow.closeevent`, so there's never a dangling button. ## π±οΈ button click β window toggle the click handler distinguishes three states (a minimized window is "visible" to qt but off-screen, so it must be restored, not hidden): ```python def _on_button_clicked(self): chat = self._chat_window if chat and chat.isvisible(): if chat.isminimized(): chat.shownormal(); chat.raise_(); chat.activatewindow() else: chat.hide() else: self._show_chat_window() ``` `_show_chat_window` creates the window lazily, positions it next to the button (flipping to the left / up when it would overflow the screen's **available geometry**, which excludes taskbars and docks), then `shownormal()` + `raise_()` + `activatewindow()`. ## πͺ window flags (the platform subtleties) ```python flags = (qt.windowtype.framelesswindowhint | qt.windowtype.windowstaysontophint | qt.windowtype.x11bypasswindowmanagerhint) # linux/x11 if sys.platform != "darwin": flags |= qt.windowtype.tool # keeps it out of the taskbar ``` - **windows/linux**: `tool` keeps the button out of the taskbar. `x11bypasswindowmanagerhint` gives true always-on-top under x11. - **macos**: the `tool` flag would turn the button into a utility window that **auto-hides when the app loses focus** β so it's deliberately dropped. trade-off: the button appears in mission control. - **wayland**: compositors don't allow floating above *other* apps' windows; the button is confined to the app window (documented, and x11 is recommended for the full experience). the chat window drops the minimize button entirely β a minimized companion window is a trap state that can't be reliably restored. ## π resiliancy touches - `changeevent` bounces the button straight back to normal if a system shortcut (e.g. win+d) minimizes *every* window including the tool: ```python if event.type() == qevent.type.windowstatechange and self.isminimized(): qtimer.singleshot(0, self.shownormal) ``` - dragging clamps to `screen.availablegeometry()` (not `(0,0)`), so the button can't slide under the macos menu bar or a windows taskbar. - saved positions are clamped the same way on restore. ## π§© what you can reuse - the **window-finding pattern** in `register()` β works for any addon. - the **qsettings position persistence** with on-screen clamping. - the **platform-conditional window flags** β copy it for any always-on-top addon window. - the **show/hide/restore toggle** logic. ## π see also - [addon development guide](/docs/addon-development/) - [addon api reference](/docs/addon-api/)
- this page documents how the gguf loader codebase is organized. the app is a pyside6 desktop application; a single `main.py` bootstraps everything, and logic is split into `core/` (pure logic), `services/` (qt threading bridges), and `ui/` + `widgets/` (presentation). ## ποΈ top-level layout ``` gguf-loader/ βββ main.py # entry point: logging, dll paths, qapplication, mainwindow βββ config.py # central configuration constants βββ resource_manager.py # resource/path discovery (dev, package, or frozen) βββ addon_manager.py # loads and manages addons βββ requirements.txt # python dependencies βββ build_exe.spec # pyinstaller spec used for windows & linux builds β βββ core/ # pure, testable logic (no qt) β βββ llm/ β β βββ model_backend.py # thread-safe llama-cpp-python wrapper β β βββ prompt_builder.py # system-prompt & conversation assembly β βββ agent/ β βββ agent_engine.py # tool-use agent loop (no qt, no llama_cpp) β βββ tool_registry.py # sandboxed workspace tools β βββ services/ # qobject bridges that run work on threads β βββ model_service.py # load/unload models on a qthread β βββ chat_service.py # streaming generation on a qthread β βββ agent_service.py # runs agentengine on a worker thread β βββ environment_service.py # venv/dependency checks & pip tasks β βββ launcher_service.py # launches scripts/ utilities β βββ ui/ # main window & panels β βββ main_window.py # composition root + addon-facing api β βββ chat_panel.py # chat display, input, agent controls β βββ sidebar_panel.py # model settings sidebar β βββ theme.py # dark/light qss token system β βββ widgets/ # reusable widgets β βββ chat_bubble.py # chatgpt-style bubble β βββ feedback_dialog.py # feedback form dialog β βββ addons/ # addon packages (each has __init__.py with register()) β βββ floating_chat/ # built-in floating chat addon β βββ scripts/ # utility & release scripts β βββ capture_screenshots.py # regenerates readme/site screenshots β βββ install_linux.sh # linux installer/uninstaller β βββ package_linux.sh # builds the linux .tar.gz release β βββ ... # gpu install/monitor helpers β βββ build_hooks/ # pyinstaller hook modules ``` ## π key design rules - **`core/` never imports qt or llama_cpp.** it receives plain callables, so it can be unit-tested in isolation. - **`services/` are the only place qt threads are created.** ui never spins up threads directly. - **`ui/` widgets are "dumb"** β they render state and emit signals; `mainwindow` owns all logic. - **`resource_manager.py`** makes paths work identically in dev, as an installed package, and in a pyinstaller bundle (`sys._meipass`). ## π§΅ threading model ``` ui thread (mainwindow) worker thread β β βββ modelservice.load() βββββββ qthread: llama_cpp loads model ββββββββ loaded(modelbackend) β βββ chatservice.generate() ββββ qthread: streams tokens ββββββββ token_received(text) β βββ agentservice.process() ββββ qthread: agentengine tool loop ββββββββ status/tool/response β ``` a fresh `qthread` + worker is created per request (the professional qt pattern β `qthread` is never subclassed). ## π entry point flow `main.py` β `setup_library_path()` (finds bundled llama.cpp libs) β `qapplication` β `mainwindow()` β builds ui, wires services, checks environment, loads addons β `app.exec()`. ## π¦ packaging - **windows**: `build_exe.bat` / `build_exe.spec` β `ggufloader_vx.y.z.exe` - **linux**: build the spec inside a linux environment, then `scripts/package_linux.sh` wraps the binary + installer + icon into a `.tar.gz` - github actions publishes both to every release automatically. see the [architecture overview](/docs/architecture/) for deeper design rationale.
π related features
π― what's next?
you've completed this guide! here are some suggested next steps to continue your gguf loader journey:
πshare your addon
built something awesome? share it with the community and get featured on our homepage.
share with community βππ€π explore more features
π back to homepage
π Related Features
π― What's Next?
You've completed this guide! Here are some suggested next steps to continue your GGUF Loader journey:
Explore Homepage
Discover more features, download options, and community resources on our homepage.
Visit Homepage βMore Documentation
Continue learning with our comprehensive documentation library.
All Documentation β