Build TUI, web, desktop UI and runnable agent products from explicit
prepared inputs. Keep dependency preparation separate from distribution
packaging, with PM and native builds sharing uv environment construction.
Docker copies compiled frontend products instead of build dependencies.
Nix retains uv2nix environments and consumes shared assembly through store
references. Native desktop and Termux use the same launcher and frontend
contracts. Preserve the independent PM runtime and source imports from
arbitrary working directories.
Keep failed frontend builds from replacing the previous product, reject
source/output overlap, and bound dependency-process output draining.
Include hermes_wisdom in the Nix wheel: real CLI smoke tests exposed its
missing package declaration on the base revision too.
Verified focused Python and JavaScript suites, Docker build/runtime checks,
Nix desktop and CLI/ACP checks, standalone TUI and packaged Electron PTY,
and real full-Chromium interaction. Native signed installers, Android device
installation and the full repository suite remain CI verification.
Generate frontend icons from the shared source artwork during each build.
Keep the locked icon dependencies out of the application runtime.
Nix needs setuptools and CFFI to build the locked python-olm source.
Its offline npm manifest must omit semver overrides because direct
package specs become local tarball paths. Preserve the repository
manifest and lockfile.
Docker must use the PM interpreter rather than a second Python under
/root. Install the application into its venv explicitly, use the image
compiler, and make package facts readable by the runtime user.
Verified:
- nix build .#desktop and frontend-icons/package-contents/cli-commands
- Packaged Electron PTY spawn and libolm encryption roundtrip
- ARM Linux and macOS desktop evaluation, not native builds
- Docker linux/amd64 build and 8 focused runtime tests
The full test suite was not run. Leave llama integration unchanged.
* fix(nix): fold makeWrapper line continuations into optionalStrings
When rev == null (any dirty-tree build), the empty optionalString
expansion left the previous line's trailing backslash dangling onto a
blank line, ending the makeWrapper command early and running
`--suffix PYTHONPATH ...` as its own shell command (`--suffix: command
not found`, exit 127). Clean trees passed CI; dirty trees with
extraPythonPackages failed — exactly the path the NixOS module
exercises.
The continuation now lives inside each optionalString (" \\\n --set
..."), so the makeWrapper chain stays intact whether or not the
optional flags expand.
Verified by building with rev = null + extraPythonPackages =
[ pyfiglet ]: wrapper builds, PYTHONPATH suffix lands inside the
makeWrapper call, wrapped `hermes --version` runs, and the collision
check still executes (certifi correctly rejected).
* perf(nix): filter derivation sources to shrink rebuild scope
Every derivation previously saw the whole repo, so any file change
rebuilt everything. Each derivation now gets a filtered src with only
the files it consumes:
- lib.nix: derive npm workspace topology from the root package.json
`workspaces` globs (single source of truth — a new workspace member
is picked up with zero nix edits). pythonSrc (cleanSourceWith)
excludes the JS workspace trees, docs/website, docker/.github,
tests, nix/, flake.nix/flake.lock, root docs, and skills/ +
optional-skills/. importNpmLock reads from a fileset-filtered
npmRoot (root manifests + member package.jsons only).
- mkNpmPassthru takes `dirs` — the workspace dirs the package
contains — and builds a per-package fileset src from them. web and
desktop include apps/shared (file: dep). One shared
`nix run .#update-npm-lockfile` replaces the per-package
update_*_lockfile bins that only existed inside build sandboxes.
- python.nix: release venv loads the uv2nix workspace from pythonSrc.
The editable venv keeps an unfiltered ./.. root —
mkEditablePyprojectOverlay calls lib.path.splitRoot, which rejects
a cleanSourceWith set, and the editable install reads the live
checkout anyway.
- hermes-agent.nix: skills ship exclusively via HERMES_BUNDLED_SKILLS
/ HERMES_OPTIONAL_SKILLS (same mechanism as Homebrew packaging;
setup.py's _data_file_tree returns [] for missing dirs), so
SKILL.md edits no longer rebuild the venv. optional-mcps stays in
the wheel — pyproject.toml lists its manifests as explicit
data-files. Bundled assets are symlinked instead of copied, making
the wrapper drv near-instant when only an input changed.
__pycache__ filtered from bundled skills.
- checks.nix: find -L through the new symlinks; assert
optional-skills presence + HERMES_OPTIONAL_SKILLS in the wrapper.
- run_tests.sh: fall back to $HERMES_PYTHON when no local venv
exists, guarded by an `import pytest` probe (HERMES_PYTHON from a
wrapped hermes binary points at the release venv, which has no
pytest — without the guard every test file dies with "No module
named pytest" while the runner exits 0).
Verified: nix flake check exit 0; built .#default .#tui .#web
.#desktop; SKILL.md and flake.nix edits leave the venv drvPath
unchanged; .py edits leave the tui drvPath unchanged; .tsx edits
leave the venv drvPath unchanged (and do change the tui drv);
scripts/run_tests.sh runs 299 tests green through both the venv and
HERMES_PYTHON paths, and rejects a pytest-less HERMES_PYTHON.
* refactor(nix): overlay aliases the flake's own package instead of re-instantiating
The overlay previously re-called callPackage against the consumer's
nixpkgs (final), so pkgs.hermes-agent could be a different derivation
than nix build .#default and the NixOS module's default — an untested
build matrix against arbitrary consumer nixpkgs versions, for a
package whose Python side is uv2nix-locked anyway.
Now the overlay is a pure alias for the flake's own locked package:
one callPackage site (packages.nix), everything else references it.
.override { ... } still works — callPackage's makeOverridable travels
with the derivation.
Verified: direct drvPath == overlaid drvPath; .override produces a
distinct drv.
* fix(nix): dedupe extraPlugins assertions, replace MESSAGING_CWD with terminal.cwd
- Delete the duplicated extraPlugins duplicate-name assertions block
(same assertion declared twice back to back).
- Stop setting the deprecated MESSAGING_CWD env var, which made the
module trip hermes' own startup deprecation warning. The working
directory is now injected as terminal.cwd into the generated
config.yaml; cfg.settings wins via recursiveUpdate, and container
mode maps to the in-container mount path.
Consolidate per-package package-lock.json files into a single root-level
workspace lockfile. Update all consumers:
- Nix: shared src/npmDeps/npmDepsHash in lib.nix; devshell hook stamps
package.json paths then runs npm ci from root; individual .nix files
use mkNpmPassthru attrs instead of per-package fetchNpmDeps.
- Python CLI: new _workspace_root() helper so _tui_need_npm_install,
_make_tui_argv, _build_web_ui resolve lockfile/node_modules from the
workspace root.
- Desktop: replace --force-build/mtime heuristic with content-hash build
stamp (_compute_desktop_content_hash via pathspec). Remove --force-build
flag.
- Dockerfile: single root npm install; no per-directory lockfile copies.
- CI: nix-lockfile-fix and osv-scanner reference root package-lock.json;
apps/dashboard → apps/desktop.
- Tests: new test_tui_npm_install.py; desktop stamp tests in
test_gui_command.py; updated assertions in test_cmd_update.py,
test_web_ui_build.py, test_dockerfile_pid1_reaping.py.
- Docs: remove --force-build from desktop flag table.
Deleted: apps/desktop/package-lock.json, ui-tui/package-lock.json,
ui-tui/packages/hermes-ink/package-lock.json, web/package-lock.json.
The web/package-lock.json changed when bumping @nous-research/ui to
0.18.2, so the fetchNpmDeps fixed-output hash in nix/web.nix was stale.
Update it to the hash prefetch-npm-deps computes for the new lockfile.
Co-authored-by: Cursor <cursoragent@cursor.com>
Picks up the deferred GPU-tier detection fix (design-language) that
stops the synchronous WebGL probe from blocking first paint, which was
causing a boot-time flash in the dashboard backdrop.
nix/web.nix npmDepsHash is a placeholder here and is corrected in the
follow-up commit using the hash reported by the Nix CI job.
Co-authored-by: Cursor <cursoragent@cursor.com>
The web/package-lock.json changed when bumping @nous-research/ui to 0.18.0,
so the fetchNpmDeps fixed-output hash in nix/web.nix was stale and the nix
build failed. Update it to the hash prefetch-npm-deps computes for the new
lockfile.
Co-authored-by: Cursor <cursoragent@cursor.com>
Removes the global `uppercase` + `font-mondwest` from the App.tsx root
that forced every page to opt-out, replaces stacked-alpha text colors
with semantic tokens for WCAG-AA contrast across all 7 themes, and
applies the new `text-display` utility from @nous-research/ui@0.16.0
on intentional brand chrome (page titles, sidebar headings, segmented
filters) only. Bumps every sub-12px arbitrary text size to text-xs.
Also widens the dashboard plugin routes (/api/dashboard/agent-plugins/
{name:path}/...) so category-namespaced plugins like observability/
langfuse and image_gen/openai can be enable/disabled from the dashboard
— previously the FE encodeURIComponent-ed the slash and the backend
{name} route rejected it. _validate_plugin_name still blocks .. and
backslash, and strips leading/trailing slash.
Touches sessions/env/keys page chrome and adds two new i18n keys
(`overview`, `showMore`/`showLess`) across all 18 locales.
Squashes 19 commits from PR #28832.
Co-authored-by: Hermes <noreply@nousresearch.com>
* feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish
Replaces the hand-rolled shadcn-style `Checkbox` in `web/src/components/ui/`
with the Nous DS `Checkbox` (Radix-backed) from `@nous-research/ui`, bumps
the DS to 0.14.2, and picks up two regressions surfaced by the bump.
Checkbox migration
- bump `@nous-research/ui` 0.14.0 → ^0.14.2 and remove
`web/src/components/ui/checkbox.tsx`
- migrate `ProfilesPage` and `ModelPickerDialog` to the DS Checkbox API
(`onCheckedChange`, paired `<Label htmlFor>`)
- expose `Checkbox` on the dashboard plugin SDK
(`web/src/plugins/registry.ts`) so plugin bundles can use the same
DS component
- migrate the kanban dashboard plugin's 7 native `<input type="checkbox">`
call sites to the SDK `Checkbox`, with a native-input fallback shim so
the bundle still renders against older hosts that predate the SDK export
Fix: missing font registrations after the 0.14.x split
- import `@nous-research/ui/styles/fonts.css` before `globals.css` in
`web/src/index.css`. As of 0.14.x, `globals.css` only declares the
`--font-*` variables (Collapse, Mondwest, Rules Compressed/Expanded);
the `@font-face` registrations now live in a separate `fonts.css`, so
without this import the DS components silently fall back to a system
font stack and look unstyled.
Fix: right-align page header toolbars on sm+ viewports
- The mobile dashboard polish in #28127 flipped four pages'
`setEnd(...)` wrappers from `justify-end` to `w-full ... justify-start`
so toolbars stack below the title and align left on small screens.
But the outer `end` slot in `PageHeaderProvider` already has
`sm:justify-end`, and that has no effect when its only child is
`w-full` — once a flex child fills the row, the parent's `justify-*`
can't move it. The toolbar pinned to the *left* of the right-side
`sm:max-w-md` (~448px) slot, making the buttons appear to float a
couple-hundred pixels off the right edge on Analytics, Models, Logs,
and Plugins.
- Re-add `sm:justify-end` on the inner wrapper of each affected page,
preserving the mobile stacked layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(nix): update web npmDeps hash for package-lock bump
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(nix): refresh npm lockfile hashes
* chore(ci): re-trigger checks after nix lockfile hash fix
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(web): mobile dashboard UX polish
Bottom sheets for sidebar theme/language pickers on narrow viewports with
enter/exit animation and drag-to-close; inline header badges beside titles;
bottom padding on the route outlet for scroll clearance; profiles loading uses a
unicode braille spinner; align profile/cron card actions to the top; viewport-fit
cover and supporting layout tweaks across dashboard pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Nix web npm hash and mobile sheet accessibility.
Align fetchNpmDeps in nix/web.nix with web/package-lock.json for CI. Improve BottomPickSheet backdrop labeling, avoid aria-hidden on the dialog during exit animation, and wire theme/language sheets with listbox semantics and localized dismiss labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* change(nix): dedupe nix lockfile checking scripts in ci
* feat(nix): make .#fix-lockfiles run --apply if no args passed
* fix(nix): use same nodejs version everywhere & small lints
- prevent lockfile thrashing while using nix :3
- use lib.getExe instead of raw /bin/ paths
- use inputs'.self instead of passing system in manually
* fix(nix): update lock files yet again (hopefully for the last time)
* fix(nix): align indentation of collision check echo
---------
Co-authored-by: Hermes Agent <hermes@nousresearch.com>
check_for_updates() looked at __file__.parent.parent for a .git dir to
diff against origin/main. A nix-built hermes lives in /nix/store with
no .git there, so the check fell through to whatever editable-install
dev checkout last populated ~/.hermes/.update_check, producing stale
"X commits behind" warnings right after a fresh `nix run --refresh`.
Embed the locked flake rev into the wrapper as HERMES_REVISION (only
on
clean builds — dirty refs don't represent any upstream commit). When
set, banner.py compares it to upstream main via `git ls-remote`
instead
of inspecting a local checkout, and the cache key includes the rev so
nix updates invalidate immediately. Without local history we can't
count commits, so the message is a plain "update available" with no
suggested command — nix users may install via `nix run`, profile,
system flake, or home-manager, and we don't know which.
Also bump web/package-lock.json npmDepsHash via `nix run
.#fix-lockfiles`.
`web/package-lock.json` was updated by the design-system refactor
(merged via #17007 + follow-ups: spinner / select / badges / buttons)
without bumping `nix/web.nix::npmDeps.hash`, breaking nix builds on
every PR + main since 2026-04-28T18:46.
Hash sourced from the actual `Check flake` failure output:
specified: sha256-AahWmJ9gDQ9pMPa1FYwUjYdO2mOi6JM9Mst27E0vp68=
got: sha256-+B2+Fe4djPzHHcUXRx+m0cuyaopAhW0PcHsMgYfV5VE=
Standalone single-file fix so it can land fast and clear nix on
every other open PR.
* fix(nix): use --rebuild in fix-lockfiles to bypass cached FOD store paths
fix-lockfiles checked npm lockfile hashes by running
`nix build .#<attr>.npmDeps`, but fetchNpmDeps is a fixed-output
derivation — if the old store path exists locally, Nix returns it from
cache without re-fetching. This caused the script to report "ok" even
when hashes were stale, while CI (with no cache) failed with a hash
mismatch.
Adding --rebuild forces Nix to re-derive and verify the output hash
against the declared one, catching staleness regardless of local cache
state. Also updates the tui and web npm deps hashes that were stale.
* fix(nix): regenerate ui-tui lockfile to add missing @emnapi entries
npm ci was failing because @emnapi/core and @emnapi/runtime were
missing from ui-tui/package-lock.json despite being required as peer
deps by @napi-rs/wasm-runtime (via @rolldown/binding-wasm32-wasi).
Running npm install --package-lock-only adds the missing entries.
The npmDepsHash reverts to its previous value since fetchNpmDeps was
already fetching these packages as transitive dependencies.
The web dashboard (Vite/React frontend) is now built as a separate Nix
derivation and baked into the Hermes package. The build output is
installed to a standard location and exposed via the `HERMES_WEB_DIST`
environment variable, allowing the dashboard command to use pre-built
assets when available (e.g., in packaged releases) instead of rebuilding
on every invocation.