Repository navigation
feat: many daemons in web and desktop, and notifications across them - #385
Merged
Merged
Conversation
web: daemons kept in Local's settings (Local plus SSH hosts, added or imported), reached by running `hmz web --attached` over ssh with the remote hmz installed at this version unless it holds running epics, proxied under /d/<name>/api/; the daemon bar and picker; only the current daemon shown, carried in every address; Settings grouped as App and Daemon, with a daemon picked per page and Batch apply; notices per machine and notifications gathered across daemons, filtered by kind, with a bell, cards and system notifications. runtime/doing: Host.status carries pending questions, the last end, and the epic and task. desktop: the page may invoke `notify` alone, shown as a native notification that opens its own-origin address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and app Document Local and ssh daemons (adding, importing from ~/.ssh/config, connecting, auto-install of the matching hmz, never under running epics), the daemon bar and picker, #/@<name>/ addresses, the App and Daemon settings groups with the daemon selector and Batch apply, and global notifications: kinds, bell, menu, toasts, browser and macOS notifications. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar names the daemon the page is of, its host, how it stands and how many epics are live on it. Its picker lists GET /api/daemons (Local alone on a 404), switches by the address's @<name>, offers Connect for a failed daemon and Manage for Settings -> Daemons, and keeps Update, Import, Settings, Usage and Download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the ssh daemons a page may switch between in settings.yaml under `daemons`, checked as an ssh runtime is: Local first and implicit, then each one added by hand or imported from the ssh config. Each carries how it stands from `site.remotes` where the site has one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Serve /api/notifications, its stream, read, clear and settings on the local interface. Notices are heard from site.notices and followed off each reached SSH daemon's /api/notices/stream, tagged with the daemon, filtered by the notifications kinds in settings.yaml, and the last 200 kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings gains a Daemons page -- Local and every SSH daemon with its state, an SSH form a part at a time, import from the SSH config, connect, and remove asked first -- and a Notifications page with the kinds, the system toggle and the browser's Allow. Both say Unavailable where the server does not serve their endpoints yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The address names the daemon first, #/@<name>/<view>, and Local where it names none. api.js asks everything of that daemon under /d/<name>/api/, save /api/daemons and /api/notifications, keeps @<name> in every link through hash(), and on a switch clears what polls read, closes the held streams and fires hmz:daemon, on which the view and the sidebar's lists are drawn again. The server answers /d/local/api/ as /api/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page in the app can call `notify` with {title, body, address}. The app
posts it through UNUserNotificationCenter, asking for permission the first
time. Clicking the notification brings the window forward. If the address is
a `#/...` hash, the app opens that hash on hmz web's own origin and goes
nowhere else. A click that launches the app opens the hash once the page is
there.
- The app's commands get an ACL manifest. The remote page
(http://127.0.0.1:*) is granted only `notify`, next to drag and zoom. The
app's own page keeps status, retry, keep and update_now.
- No new crates: objc2, objc2-foundation, block2 and
objc2-user-notifications are already in the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bell in the sidebar's top row with the unread count, a menu of the history newest first (kind, daemon, flow and what it said, how long ago) with Mark read and Clear, a card at the bottom right for each one heard on the stream, and -- while the page is hidden or unfocused -- the browser's Notification or the macOS app's `notify` command. Clicking any of them reads it and opens the epic going or the epic that ended. Built on the global /api/notifications endpoints; where the server has none, the bell is hidden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings now has two groups. App holds this page's own settings (Themes, and Daemons and Notifications once their pages exist). Daemon holds the rest, read from and written to the daemon picked above them: the one on screen by default, kept in the address as ?daemon=<name>. General, Fallback, Runtimes and Backup gain Batch apply, which sends what the page holds to each daemon ticked and says what each answered. Accounts say their secrets stay put. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host's status now says the questions it waits on (`pending`), how the last run to end ended (`ended`), and the run's `epic` and `task`. `hmz web` polls every workspace's host status every 2 s, without attaching, and turns what changed into notices (done, failed, stopped, asked, listening), served at `GET /api/notices?after=` and as `notice` events on `GET /api/notices/stream`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here
Each daemon under `daemons` in settings.yaml is reached by running
`hmz web --attached` over ssh, with a loopback port forwarded to it, and
proxied at /d/<name>/api/... (SSE and uploads streamed) with its own key;
/d/local/api/... is this machine. A missing or other hmz there is installed
at this version with uv tool install, never while its daemon may hold
running epics. Adds /api/daemons/{name}/state and /connect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…manfia/humanize into feat/multi-daemon-notifications
…feat/multi-daemon-notifications
…ze into feat/multi-daemon-notifications
…e into feat/multi-daemon-notifications # Conflicts: # src/hmz/web/site.py
… into feat/multi-daemon-notifications # Conflicts: # docs/reference/web.md # src/hmz/web/__init__.py # src/hmz/web/site.py
…e into feat/multi-daemon-notifications # Conflicts: # src/hmz/web/static/sidebar.js
…to feat/multi-daemon-notifications # Conflicts: # src/hmz/web/static/sidebar.js
…manize into feat/multi-daemon-notifications # Conflicts: # src/hmz/web/static/pickers.js # src/hmz/web/static/style.css # src/hmz/web/static/views/settings.js # src/hmz/web/static/views/settings/runtimes.js # src/hmz/web/static/views/settings/usage.js
…humanfia/humanize into feat/multi-daemon-notifications # Conflicts: # src/hmz/web/static/style.css # src/hmz/web/static/views/settings.js
…manize into feat/multi-daemon-notifications # Conflicts: # src/hmz/web/static/sidebar.js # src/hmz/web/static/style.css # tests/integration/doubles_web.py
…to feat/multi-daemon-notifications
…/humanize into feat/multi-daemon-notifications # Conflicts: # docs/user/web.md
- The daemon bar reads `{daemons: [...]}`, switches through `toDaemon`, and keeps `@<name>` on its links.
- Settings drops the optional-page guard now that both pages exist, and its links keep `@<name>`.
- The Daemons page saves `{name, fields, replace}`, and a rename removes the old entry.
- A view on a daemon that cannot be reached no longer throws: `places.js` catches the refusal.
- The notification settings are read only when a system notification is about to show.
- `style.css` is rebuilt from each unit's own block, with every brace closed, and the daemon bar no longer cuts "Local" short.
- The browser tests move under `tests/system/web`, as #369 moved them. Their fakes now follow the merged API, and `opened()` takes `routed=`.
- A new integration test covers a chat on an SSH daemon notifying Local.
- Docs: the proxy, the state and connect endpoints, the `daemons` key, and one Notifications section on the desktop page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futrime
enabled auto-merge
October 12, 2026 23:55
This was referenced Oct 12, 2026
This was referenced Oct 12, 2026
…z web never listened Also formats the daemons test, and has the remote tests list the stand-in's processes when a daemon never comes up, so CI says where it waits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… what a remote hmz web says as it comes up A notification sent between a stream's headers and its first read was lost, which hung the stream test on Linux. The stand-in remote now dumps its stack on SIGABRT, so CI says where a remote hmz web hangs on macOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HTTPServer.server_bind asks socket.getfqdn for a name the interface never uses, and on macOS runners that lookup hung long enough that a remote hmz web never said its address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands the 12 pull requests of the multi-daemon and notifications batch together, as #367 did. Each one was green alone; together they need the fixes in the last commit.
Multiple daemons
The page shows epics from Local and from any number of SSH daemons. It shows one at a time, and the daemon is part of the address (
#/@<name>/…).~/.ssh/config, edit, remove or connect one. The list is kept underdaemonsin~/.hmz/settings.yaml.hmz web --no-open --attachedon the host over ssh, forwards a port to it, and proxies/d/<name>/api/…to it, signing in with the remote key.hmzthere is missing or a different version, it is installed withuv tool install hmz==<this version>, installing uv first if needed.Notifications
hmz webserves notices for its machine: an epic done, failed or stopped, a question for a person, a chat waiting on its next turn. They come from polling host status every 2 s; it never attaches to a host./api/notifications, filtered by kind as set in Settings › Notifications.notifycommand. Clicking a native notification focuses the window and opens the epic.Fixes on top of the units
{daemons: [...]}, and{name, fields, replace}when saving).api.toDaemon, and links keep@<name>.style.cssis rebuilt from each unit's own block. The conflict resolution had left 5 blocks unclosed.tests/system/web, following test: move the web page's browser tests to tests/system, and run the web topic in tests #369.tests/integration/test_web_daemons_together.py: a chat on an SSH daemon (the stand-inssh) shows up as a notification on Local, tagged with that daemon.Supersedes #371, #372, #373, #374, #375, #376, #377, #378, #379, #381, #382 and #384.
Checks
pre-commit run --all-files: clean.pytest tests/unit tests/integration: 6241 passed.pytest tests/system/web: 158 passed, in Chromium and WebKit.#fragmentresolves.cargo fmt,clippyandtestpassed in feat(desktop): native notifications for hmz web's page #379. The native banner has not been seen on screen yet, because the permission prompt on the Mac it was tried on is still unanswered.🤖 Generated with Claude Code