Qt's default message handler prefers journald/syslog where the platform offers it, so this app's own qInfo()/qWarning() went somewhere nobody was looking. The cost is not cosmetic. Launching with a broken plugin printed NOTHING -- not 'No metadata.json or manifest.json in plugin directory', not 'Failed to load QML', not 'Timeout waiting for ui-host ready' -- while the window showed only a bare 'Failed to load UI plugin'. Every diagnostic this app already writes was invisible, including the one naming the exact QML line at fault. That is worse than having no diagnostics: an absent warning reads like a passed check, so it actively misleads. It cost me a long detour into a non-existent ui-host startup bug before I found the real one-line QML error that the app had been reporting all along. Set before QApplication so it lands before the logging backend is first used, and only when the caller has not chosen for themselves -- exporting QT_FORCE_STDERR_LOGGING=0 still suppresses. VERIFIED: with no env var, 'No metadata.json ... in plugin directory' now appears on stderr; with QT_FORCE_STDERR_LOGGING=0 it is still suppressed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
logos-standalone-app
A generic Qt6 shell for loading and testing Logos UI plugins in isolation, without requiring a full Logos node or the complete logos-app stack.
Overview
logos-standalone is a minimal host application that:
- Starts the Logos core backend (
logos-liblogos) - Loads the backend modules a plugin declares as dependencies
- Displays the plugin's UI in a window
It supports three plugin formats:
- View modules (
type: "ui"with"view"field) — process-isolated C++ backend + QML view. The C++ plugin runs in a separateui-hostprocess; the QML view is loaded in the standalone app and communicates with the backend vialogos.callModuleAsync(). - QML plugins (
type: "ui_qml") — pure QML UI loaded into aQQuickWidget; thelogoscontext property exposes a bridge for calling backend modules from QML. - Legacy dylib plugins (
type: "ui", no"view"field) — loaded viaQPluginLoader, must export acreateWidget(LogosAPI*)method. Can be passed as a raw file path or as a directory containing ametadata.json.
Usage
logos-standalone [options] <plugin-path>
Options
| Flag | Short | Description |
|---|---|---|
--plugin <path> |
-p |
Path to the plugin directory (alternative to positional argument) |
--modules-dir <dir> |
-m |
Directory containing backend modules (default: ../modules relative to the binary) |
--user-dir <dir> |
-u |
Session data directory; isolates module state for this instance (default: the platform application data location) |
--load <module> |
-l |
Load a named backend module before showing the UI; can be repeated |
--title <title> |
-t |
Window title (default: name from metadata.json, then plugin filename) |
--width <px> |
Window width in pixels (default: 1024) |
|
--height <px> |
Window height in pixels (default: 768) |
|
--help |
-h |
Show help and exit |
Examples
# Load a view module directory (C++ backend + QML view)
logos-standalone ./calc_ui_cpp
# Load a pure QML plugin directory
logos-standalone ./wallet_ui
# Load a legacy dylib plugin directly (raw .dylib/.so file)
logos-standalone ./result/lib/accounts_ui.dylib
# Load a plugin directory with backend modules
# (capability_module is loaded by the runtime itself — no --load needed)
logos-standalone --plugin ./chat_ui --modules-dir ./modules --load waku_module
# Override the modules directory
logos-standalone --plugin ./chat_ui --modules-dir ./result/modules
# Run directly via Nix against a local plugin build
nix run github:logos-co/logos-standalone-app -- ./result/lib/chat_ui
--user-dir: one directory per session
Backend modules do not choose where they store their state: the host assigns
each module instance a directory, and the app derives it from the session
directory. A module instance gets <user-dir>/module_data/<module>/<instance>.
The default session directory is the platform application data location (on
Linux, ~/.local/share/Logos/LogosStandalone). Pass --user-dir to select
another one, which is how two instances of the same plugin run side by side
without sharing state:
logos-standalone --user-dir /tmp/alice ./chat_ui &
logos-standalone --user-dir /tmp/bob ./chat_ui &
The directory is created if it does not exist. Setting LOGOS_USER_DIR selects
the same directory, and the flag wins when both are given. This matches Logos
Basecamp's --user-dir / LOGOS_USER_DIR, so a session means the same thing
under either host.
Plugin Metadata
When given a plugin directory, the app reads metadata.json (or manifest.json) to determine the plugin type and auto-load declared dependencies before the UI is shown.
View module (C++ backend + QML view, process-isolated):
{
"name": "calc_ui",
"type": "ui",
"view": "qml/Main.qml",
"dependencies": ["calc_module"]
}
Pure QML plugin:
{
"type": "ui_qml",
"main": "Main.qml",
"dependencies": ["waku_module", "chat"]
}
Legacy dylib plugin (type: "ui" without "view", or a raw .dylib/.so file):
{
"type": "ui",
"dependencies": ["capability_module"]
}
The app determines the loading strategy from these fields:
type: "ui"+"view"present → view module (spawnui-host, load QML view)type: "ui"without"view"→ legacy dylib (load viaQPluginLoader, callcreateWidget)type: "ui_qml"→ pure QML (load intoQQuickWidget)
When a raw .dylib/.so/.dll file is passed directly (instead of a directory), the app loads it via QPluginLoader without requiring a metadata file.
Icon support
If metadata.json contains an "icon" field with a relative file path, the app sets it as the window icon. The path is resolved relative to the directory containing metadata.json.
QML hot reload — edit, save, see it
For ui_qml plugins, setting DEV_QML_PATH to a directory does two things: it
redirects the view entry file (named by metadata.json's view field) to that
directory, and it turns on hot reload for the whole tree underneath. Edit any
.qml/.js file and save — the view re-renders in roughly 200 ms. No rebuild,
no relaunch.
# view: "qml/MyView.qml" → loaded from $DEV_QML_PATH/MyView.qml
export DEV_QML_PATH=$PWD/src/qml
logos-standalone ./result/lib/my_ui_module
Most module authors do not need to set this by hand — nix build .#ui-dev
produces a launcher that detects it automatically. See
logos-module-builder's README.
The directory must contain the basename of the view entry. The engine's base
URL is set to DEV_QML_PATH, so relative imports (sub-components, icons)
resolve alongside the entry file. If the env var is unset, invalid, or missing
the entry file, the installed view is used and a warning is logged.
What a reload does and doesn't touch:
- Sub-components and new files are covered. The whole tree under
DEV_QML_PATHis watched recursively, including files and folders created after launch. - The backend keeps running. A module's C++ backend lives in a separate
ui-hostprocess; reloading rebuilds only the QML, so module state, connections and loaded data survive. - QML-side state resets — scroll position, text fields, current tab. This is inherent to re-instantiating the view.
- A syntax error is recoverable. The error is logged with its line number and the view goes blank; the next save that compiles brings it back.
- C++ changes still need a rebuild — only QML is reloaded.
Set LOGOS_QML_HOT_RELOAD=0 to keep the DEV_QML_PATH redirect but disable
watching.
Implementation: app/QmlLiveView.cpp. Each reload builds a fresh QQmlEngine
rather than re-setSource()-ing the existing one — clearComponentCache() only
frees compilation units nothing still references, so re-sourcing can silently
replay the previous QML while reporting success.
Adding nix run to a UI module
logos-standalone-app is bundled inside logos-module-builder. UI modules automatically get apps.default wired up — no separate flake input needed. Use the builder that matches your module type:
| Module type | Builder | When to use |
|---|---|---|
| C++ + QML view | mkViewModule |
Process-isolated UI: C++ backend in ui-host, QML view in host app |
| Pure QML | mkLogosQmlModule |
QML-only UI, no C++ compilation |
| Legacy C++ widget | mkLogosModule |
C++ plugin with createWidget() (legacy pattern) |
# View module (C++ + QML)
logos-module-builder.lib.mkViewModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
# Pure QML module
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
# Legacy C++ widget module (type: "ui", no "view" field)
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
Dependencies listed in metadata.json are automatically bundled from their LGX packages and loaded at runtime.
Then build and run:
nix build && nix run .
# Pass options after --
nix run . -- --title "My Plugin" --width 1280 --height 800
To override the standalone app with a custom build, pass logosStandalone to mkViewModule, mkLogosModule, or mkLogosQmlModule.
Building
Nix (recommended)
nix build # produces ./result/bin/logos-standalone
nix run -- <plugin-path> # build and run in one step
To use a local checkout of a dependency:
nix build --override-input logos-liblogos path:../logos-liblogos
nix build --override-input logos-cpp-sdk path:../logos-cpp-sdk
nix build --override-input logos-capability-module path:../logos-capability-module
Manual CMake
Requires Qt6, logos-liblogos, and logos-cpp-sdk to be available.
cmake -S app -B build -GNinja \
-DLOGOS_LIBLOGOS_ROOT=/path/to/logos-liblogos \
-DLOGOS_CPP_SDK_ROOT=/path/to/logos-cpp-sdk
cmake --build build
Dev shell
nix develop # sets LOGOS_CPP_SDK_ROOT and LOGOS_LIBLOGOS_ROOT automatically
QML Inspector
The app includes an optional QML Inspector server (logos-qt-mcp) that enables runtime UI introspection over TCP. This allows AI assistants (via MCP) and test frameworks to interact with the running UI — clicking buttons, reading properties, taking screenshots, etc.
The inspector is enabled by default in non-release builds. In nix builds it is controlled by the enableInspector parameter (default: true).
Using the inspector interactively
# Build the app (inspector enabled by default)
nix build
# Build the MCP server + test framework (one-time)
nix build .#logos-qt-mcp -o result-mcp
# Run the app with a plugin — inspector starts on localhost:3768
./result/bin/logos-standalone-app ./my-plugin/result/lib
# Connect Claude Code via .mcp.json (automatic when working from this directory)
# Or connect any MCP client to localhost:3768
The inspector port defaults to 3768 and can be changed via the QML_INSPECTOR_PORT environment variable.
Available MCP tools
| Tool | Description |
|---|---|
qml_screenshot |
Capture a screenshot of the current app state |
qml_find_and_click |
Find a UI element by text and click it |
qml_find_by_type |
Locate elements by QML type name |
qml_find_by_property |
Locate elements by property value |
qml_list_interactive |
List all clickable/interactive elements |
qml_get_tree |
Get the full QML element tree |
Disabling the inspector
# Via CMake
cmake -DENABLE_QML_INSPECTOR=OFF ...
# Via nix (pass enableInspector = false to nix/app.nix)
Plugin integration tests (lib.mkPluginTest)
logos-standalone-app exposes a reusable test builder that UI modules can use to run headless integration tests. This is wired up automatically by logos-module-builder — any UI module with .mjs files in its tests/ directory gets a nix build .#integration-test output for free.
If you need to use it directly:
# In a module's flake.nix
integration-test = logos-standalone-app.lib.${system}.mkPluginTest {
inherit pkgs;
pluginPkg = myModulePackage;
testFiles = [ ./tests/smoke.mjs ./tests/interactions.mjs ];
name = "my-module-integration-test";
};
The builder launches the plugin in logos-standalone-app with QT_QPA_PLATFORM=offscreen, connects to the QML inspector, and runs each test file sequentially.
Architecture
app/
main.cpp # CLI argument parsing, core init, module loading
mainwindow.h/cpp # Plugin loading (dylib + QML) and window setup
nix/
app.nix # Nix derivation: build, install, Qt wrapping, library bundling, capability_module bundling
mkPluginTest.nix # Reusable integration test builder for UI plugins
flake.nix # Flake outputs: packages, apps, lib, devShells (4 platforms)
The nix build bundles capability_module (via nix-bundle-lgx) into the output's modules/ directory so it is always available at runtime. The app loads capability_module first (required by all UI plugins), then any modules declared in the plugin's metadata.json, then any additional modules passed via --load. The default modules directory is ../modules relative to the binary; use --modules-dir to override.
Supported Platforms
aarch64-darwin(Apple Silicon)x86_64-darwin(Intel Mac)aarch64-linuxx86_64-linux
Related
- logos-liblogos — core C++ plugin system
- logos-cpp-sdk — code generation SDK for module wrappers
- logos-app — full Qt desktop application
- logos-module-builder — Nix helpers and templates for creating modules