Desktop App
Headlamp's desktop application is built using Electron. It packages the frontend UI and backend server into a standalone desktop application for Windows, macOS, and Linux.
The desktop app is written in TypeScript and uses Babel to transpile the Electron code. It provides a native desktop experience with features like system tray integration, window management, and plugin support.
Building and running
First, install the required dependencies by running:
npm run frontend:install
npm run app:install
The desktop app can be quickly built using:
npm run app:build
This command builds the frontend, sets up plugins, and compiles the Electron app into a distributable package.
To build without creating installers (faster for development):
npm run app:build:dir
Once built, it can be run in development mode using:
npm run app:start
For development with live-reload (requires the backend and frontend to be running separately):
npm run app:start:client
Or run everything together:
npm run start:with-app
Lint
The app code uses the frontend linting configuration. To lint the app/ code:
npm run app:lint
This command can fix some lint issues:
npm run app:lint:fix
Format
To format the app code:
npm run app:format
Test
Run unit tests:
npm run app:test:unit
Run end-to-end tests:
npm run app:test:e2e
Run all tests (unit + e2e):
npm run app:test
Type checking
To check TypeScript types:
npm run app:tsc
Translations (i18n)
The app uses i18next for translations. Translation keys are extracted from app/electron/main.ts using i18next-parser.
To regenerate translation files after changing translatable strings:
cd app && npm run i18n
To check that translation files are up to date (exits with an error if any file would change):
npm run app:i18n-check
This check runs automatically in CI to prevent translation regressions when code or dependencies change.
Packaging
To package the app for all platforms:
npm run app:package
Platform-specific packaging:
npm run app:package:win # Windows
npm run app:package:linux # Linux
npm run app:package:mac # macOS
npm run app:package:win:msi # Windows MSI installer
Custom product builds
Product builds can select an alternative app build manifest and record the source revision used to assemble the package:
| Environment variable | Default | Description |
|---|---|---|
HEADLAMP_BUILD_MANIFEST | app/app-build-manifest.json | Path to the JSON build manifest. The checked-in default configures both Electron packaging and generated frontend metadata. |
HEADLAMP_SOURCE_COMMIT | Current Git revision or unknown | Source revision written to REACT_APP_HEADLAMP_GIT_VERSION. Set this when building from packaged source without its Git repository. |
The manifest's product.productName and product.version fields configure the
identity displayed by the frontend as well as the packaged desktop app.
product.companyName configures the packaged app's Windows company metadata,
macOS copyright attribution, and Linux vendor. Set platforms.linux.maintainer
separately to a package contact in Name <email> form. Missing or empty fields
retain the values from app/package.json.
product.trayIcon optionally selects a system tray icon using a path relative
to the packaged resources directory. Declare the same file in resources so
Electron Builder copies it into the package.
product.errorReporting optionally configures the generic error page's crash
report action. Set githubIssueUrl to the full GitHub issue creation URL;
existing query parameters such as labels or template are preserved when
Headlamp adds the crash title and diagnostic body. Set buttonLabel to replace
the translated Open Issue on GitHub label with product-owned text.
For example:
{
"product": {
"name": "example-desktop",
"productName": "Example Desktop",
"companyName": "Example Company",
"version": "1.2.3",
"trayIcon": "assets/example-tray.png",
"errorReporting": {
"githubIssueUrl": "https://github.com/example/product/issues/new?labels=bug",
"buttonLabel": "Contact support"
}
},
"resources": {
"common": [{ "from": "./example-tray.png", "to": "assets/example-tray.png" }]
},
"platforms": {
"linux": {
"maintainer": "Example Company <support@example.com>"
}
}
}
Build the app with the selected manifest and source revision:
HEADLAMP_BUILD_MANIFEST=./product/app-build-manifest.json \
HEADLAMP_SOURCE_COMMIT=0123456789abcdef \
npm run app:build
Frontend environment generation resolves a relative manifest path from the
app/ directory. Electron packaging resolves it from the process working
directory. The root app scripts run Electron packaging from app/, so the
example above selects app/product/app-build-manifest.json for both consumers.
Use an absolute manifest path when invoking either consumer directly from a
different working directory.
The standard make image and npm run image:build commands pass the current
Git revision to container builds. Direct Docker callers selecting a custom
product manifest should pass both the manifest path and source revision:
docker buildx build \
--build-arg HEADLAMP_BUILD_MANIFEST=./product/app-build-manifest.json \
--build-arg HEADLAMP_SOURCE_COMMIT=0123456789abcdef \
-f Dockerfile .
The manifest must remain available throughout the frontend and app packaging
steps. An unreadable manifest, a non-object product, non-string
productName/version fields, invalid errorReporting fields, or values
containing newlines stop the frontend build rather than silently using
inconsistent product metadata. Electron packaging additionally rejects a
non-string companyName. The frontend environment generator also rejects values
containing both a single quote and a backtick when they additionally contain
either a double quote or a literal backslash-n/backslash-r sequence, because
no dotenv quote delimiter can represent them without changing the value.
Build manifest resources
The resources field appends common or platform-specific files and directories
to Electron Builder's existing extraResources. Resource from paths resolve
from the directory containing the selected manifest:
{
"resources": {
"common": [{ "from": "./shared", "to": "shared" }],
"linux": [{ "from": "./tools/linux", "to": "tools" }],
"mac": [{ "from": "./tools/mac", "to": "tools", "filter": ["**/*"] }],
"win": [{ "from": "./tools/windows.exe", "to": "tools/tool.exe" }]
}
}
Only common, linux, mac, and win resource groups are supported. Each
group must be an array of objects with a from string and optional to string
or filter string array. Invalid resource configuration stops packaging before
Electron Builder runs.
For example, from the repository root:
HEADLAMP_BUILD_MANIFEST=./product/app-build-manifest.json npm run app:package
Packaged resource verification
The verify field checks selected files after Electron Builder copies them into
the package. Each entry declares a path relative to the packaged resources
directory and its expected SHA-256 digest:
{
"verify": [
{
"path": "tools/helper",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"platforms": ["linux", "mac"]
}
]
}
The optional platforms array accepts linux, mac, and win. An entry without
platforms applies to every package. Verification stops packaging when a file
is missing, escapes the resources directory, is a directory or symbolic link,
or does not match its declared digest.
Development workflow
The typical development workflow for the desktop app:
- Make changes to the app code in
app/electron/ - Run linting and formatting:
npm run app:lint:fix && npm run app:format - Run type checking:
npm run app:tsc - If you added or changed translatable strings, regenerate translations:
cd app && npm run i18n - Test your changes:
npm run app:test - Build and run:
npm run app:start
Notes
- The app code in
app/electron/is written in TypeScript (.tsfiles) - Babel compiles TypeScript to JavaScript during the build process
- Compiled
.jsfiles inapp/electron/are git-ignored - The app uses the frontend's ESLint and Prettier configuration
- For linting/formatting, the app scripts delegate to the frontend scripts
- MCP child processes inherit the MCP SDK's platform-specific default environment
rather than the full desktop environment. Grant additional variables explicitly
in the MCP server's
envconfiguration. Values must be strings; malformed entries are reported and ignored. Windows variable names are normalized to uppercase so explicit overrides replace the SDK defaults. - Active cluster-dependent MCP servers restart when selected cluster membership
or the first/current cluster changes. Reordering only secondary selected
clusters does not restart them. The caller's cluster order is preserved when
expanding
HEADLAMP_CURRENT_CLUSTER, and cluster updates do not start dormant servers.