v0.3 scope:
- Founders Vault tab visible in inventory side panel
- locked-state placeholder
- badge on bag icon when unclaimed
- uses locked palette (GOLD #F1C56D, MIDNIGHT #0F1622)
- no real gift granting yet (v0.4)
From swarm11 (issue #9). Reads from FOUNDERS-EVENT-INVENTORY.md spec.
- moves long hair side locks behind head
- replaces face-covering cap/long-hair ellipses with forehead-only paths
- renders eyes last so face remains visible across hair/helmet states
- z-index ordering fixed for portrait camera
From swarm10 lane (issue #1).
Allow operators to hide the HermesWorld sidebar link by setting
VITE_HERMESWORLD_ENABLED=0 in .env. Default is enabled (1).
Closes the gap for users who don't want gamification/playground links
in their workspace sidebar.
Replace fetch-and-slice with proper offset/limit pagination so
the marketplace can show all results, not just the first 20.
Server (src/routes/api/mcp/hub-search.ts):
- accept ?offset=N query param (default 0, clamped to >=0)
- raise the limit cap from 100 to 500
Server (src/server/mcp-hub/index.ts):
- unifiedSearch now takes an offset parameter and returns
filtered.slice(offset, offset + limit). The dedup + filter
pipeline runs once per request (same as before); only the
final slice changed.
Client (src/screens/mcp/hooks/use-mcp-hub.ts):
- rewrite useMcpHub from useQuery to useInfiniteQuery
- page size 50; getNextPageParam returns undefined when
loaded >= total
- flatten data.pages[].results into a single data.results
array externally so existing callers keep working
UI (src/screens/mcp/mcp-screen.tsx):
- add Load more button under MarketplaceGrid
- shows current loaded count of total
- hides automatically when hasNextPage is false
Result: with the 11 sources I have configured, total dedup
returned ~301 results. Before: capped at 20. After: paginates
through all 301.
When HERMES_API_TOKEN and CLAUDE_API_TOKEN are not set, getBearerToken()
now falls back to reading the Codex OAuth access token from
~/.codex/auth.json. This fixes portable-mode (non-gateway) chat failing
with 401 invalid_api_key for users who authenticated via 'codex login'.
Fixes#329
The committed routeTree.gen.ts was missing entries for the
/api/gateway-reprobe route even though the route file exists.
The TanStack Router plugin regenerates the tree on every dev
server start, creating a permanent dirty working tree that
blocks git pull/merge operations.
Regenerate and commit the tree so it matches what the plugin
produces. Fixes#326.
Update banner:
- top-of-app update banners now only show when a one-click update is actually safe
- dirty checkouts, non-main branches, and blocked/conflicting repo states stay out of the global banner
- those states still belong in an advanced update center / dev-facing surface, but not in normal-user chrome
HermesWorld:
- add docs/hermesworld/visual-upgrade-spec.md
- locks the TinySkies-informed polish direction into a concrete execution spec
- covers lighting, landmarks, silhouettes, HUD cohesion, path readability, zone identity, phased rollout, and swarm/kanban-friendly task breakdown
This gives us a stable product target for an Opus-led visual polish pass while keeping mainline update UX calmer for normal users.
Two UX improvements in one pass:
#286 — update modal/right-click on Firefox/Linux
The update-center cards and release-notes modal lived inside motion/
backdrop layers with no explicit context-menu handling. On Firefox/Linux
this could make right-clicks feel swallowed or intermittently unresponsive.
There was no direct preventDefault in our code, the event was getting lost
in the wrapper stack.
Fix:
- Add onContextMenu stopPropagation to the update card and release-notes
modal container so the native context menu can open on the card itself
instead of bubbling into the backdrop/motion layers.
- Add select-text so copy/select interactions work naturally.
#295 — inline artifact rendering in chat (first slice)
The streaming pipeline already received a dedicated artifact event from
send-stream, but we flattened it into a generic tool-complete string like:
"Artifact created - /path/to/file"
That meant the chat renderer had no structured metadata and could only show
an ordinary tool row. This pass preserves the artifact fields and renders a
first-class inline artifact card in the message stream:
- use-streaming-message now keeps artifact metadata (title, kind, path,
preview) on the tool event instead of discarding it into a plain string.
- message-item detects tool sections whose type starts with artifact: and
renders a dedicated card with title, artifact kind badge, file path,
Open action, and preview text when provided by the stream.
- Generic tool input/output blocks are suppressed for artifact rows so the
card doesn't duplicate itself.
This is deliberately a thin first slice, enough to stop artifacts from
feeling invisible and generic in chat, while leaving room for a richer
multi-pane/Claude.ai-style artifact surface later.
Refs #295 and closes#286.
HTML files previously opened only in the code editor path, so users had
no way to render an .html file in-place and quickly inspect the actual
page. This was especially awkward for generated artifacts, landing pages,
and static exports.
Changes:
* New isHtmlFile() helper (html / htm)
* File header Preview/Raw toggle now applies to HTML as well as Markdown
and uses clearer button copy: 'Raw HTML' / 'Preview HTML'
* New HTML preview branch renders file content in a sandboxed iframe via
srcDoc, keeping it isolated from the workspace page while still letting
CSS/layout load naturally inside the preview
Sandbox mode is only — no scripts/forms/popups/nav.
That gives users a faithful layout preview without turning the file
browser into a script execution surface.
Closes#296
#275 reported workspace stuck on 'Disconnected' even though the agent
was reachable. Root cause: workspace boots before agent in docker
compose, every probe fails, capabilities cached as zero-state for the
full 120s TTL. By the time the agent comes up, the cache is still
stale and the UI looks broken.
Changes:
* effectiveProbeTtl(): 120s when healthy, 15s when disconnected. The
shorter window during 'mode=disconnected' state means a stack where
workspace lost the race to the agent recovers within ~15s of the
agent becoming reachable, instead of being stuck on the first failed
probe for two minutes.
* New POST /api/gateway-reprobe endpoint: forces a fresh probe
regardless of TTL. Useful for diagnostic scripts and a future UI
'Reconnect' button. Auth-gated (same as /api/gateway-status).
* New forceReprobeGateway() helper exported from gateway-capabilities.
* New docs/docker.md: comprehensive setup guide covering single-host,
multi-host (NAS/VPS), capability mismatches, and a step-by-step
diagnostic playbook for connection failures. Cross-references the
new /api/gateway-reprobe endpoint.
Foundation for #275 — the docs + faster recovery cover the most common
cases. Outstanding work: better startup ordering hint when probes fail
because the agent isn't up yet (toast + 'Reconnect' button in the UI)
and a CI test that boots both services in compose to catch regressions
in the connection contract.
The plain dark/light pill toggle was ambiguous — users couldn't tell at
a glance which side meant 'on' (especially in dark themes where the
unchecked grey and checked dark-blue tones read similarly).
Changes:
* Track is wider (2.4× thumb instead of 2×) to fit visible labels.
* Checked state uses emerald-600 instead of primary-900 — green is a
near-universal 'on' signal.
* 'ON' label appears on the left of the thumb when checked (white on
emerald, high contrast).
* 'OFF' label appears on the right of the thumb when unchecked
(muted-fg on neutral track).
* Labels are aria-hidden — the underlying SwitchPrimitive.Root already
exposes role/checked state to assistive tech, so duplicating it as
visible text would just create noise for screen-reader users.
Closes#284
Reported in #304: clicking Create Job displayed a toast that read
'[object Object]' instead of an actionable error.
Root cause: every error path in src/lib/jobs-api.ts coerced the
response body's .detail field directly into a template literal:
throw new Error(body.detail || `Failed to create job: ${res.status}`)
When the gateway returns a structured error (FastAPI/Pydantic
validation arrays, plain objects), .detail is not a string so the
Error message renders as the literal '[object Object]' once it goes
through React's toast.
Fix: a single errorMessageFromBody() helper that:
* Returns string detail as-is
* Joins arrays of validation errors using msg/message fields
* JSON-stringifies anything else
* Falls back to body.message, body.error, then the original status text
Wired into createJob, updateJob, deleteJob, pauseJob, resumeJob,
triggerJob \u2014 all six job mutation paths had the same bug.
Closes#304
Installs that were set up before the claude→hermes rename may have their
git remotes named claude-workspace and claude-agent rather than the new
hermes-workspace / hermes-agent names. Without these aliases the update
checker misidentifies the remote, reports no update URL match, and the
Update Center shows a misleading error.
Adds backward-compatible aliases so both old and new remote naming
conventions are recognised.
Co-authored-by: admin <admin@fattony.local>
When the workspace's swarm kanban is running in proxy mode against the
Hermes Dashboard kanban plugin (caps.kanban === true), the header badge
now:
* Renders in green ('Synced • Hermes ↗') instead of generic gray
* Becomes a clickable link to the dashboard's /kanban tab
* Tooltips include 'Open in Hermes Dashboard ↗'
Polling reduced from 30s → 5s so cards added/moved on the Hermes
dashboard show up in the workspace board within ~5s. The plugin also
exposes a WebSocket at /api/plugins/kanban/events for true live updates;
that's the next item on the kanban roadmap.
The 'hermes-proxy' badge tone is added alongside the existing 'claude'
(legacy direct-sqlite) and 'local' (file-backed) tones.
Builds on the kanban capability detection (commit 2526984fa). When the
upstream Hermes Agent dashboard exposes the kanban plugin
(/api/plugins/kanban/, caps.kanban === true), the workspace's /swarm
kanban surface now syncs with it as a single SQLite source of truth
instead of running a separate file-backed store.
Architecture:
┌─────────────────┐ ┌──────────────────┐
│ Hermes Workspace │ │ Hermes Dashboard │
│ /swarm kanban │──────▶│ /kanban │
│ (React UI) │ HTTP │ (React UI) │
└─────────────────┘ proxy└──────────────────┘
│ │
│ both read/write │
└────────┬─────────────────┘
▼
~/.hermes/kanban.db
(one SQLite, dispatcher-aware)
Why HTTP proxy and not direct SQLite (we still have that path too)?
Remote workspaces (Docker, VPS, separate machines from the agent)
can't share the SQLite file. Going through HTTP is the only viable
path for those deployments. The plugin's transactional helpers also
keep the workspace from racing the dispatcher on running/claimed
state — the dashboard rejects direct writes to 'running' for that
reason (only the dispatcher's claim path may transition into running).
Changes:
* New src/server/kanban-dashboard-proxy.ts: thin HTTP client for
/api/plugins/kanban/board, /tasks, /boards. Unwraps the {task: ...}
envelope the plugin returns.
* src/server/kanban-backend.ts: new dashboardProxyBackend (id:
'hermes-proxy'). resolveKanbanBackend() now picks proxy first when
caps.kanban is true, falling back to the legacy direct-SQLite
claudeBackend when only the DB is reachable, then to the local
file-backed store. Override via CLAUDE_KANBAN_BACKEND env var:
'local' | 'claude' | 'hermes-proxy' | 'auto' (default).
* lane↔dashboard status mapping handles two quirks:
- 'running' from the workspace UI is rewritten to 'ready' before
posting (dashboard rejects direct writes of running; dispatcher
will pick the task up on next tick).
- 'review' (workspace-only lane) maps to 'ready' for visibility.
* listKanbanCards / createKanbanCard / updateKanbanCard are now
async; /api/swarm-kanban awaits them. Tests updated.
Tested locally end-to-end against
/Users/aurora/hermes-dashboard-fresh/repo (upstream main with kanban
plugin). Cards created via /api/swarm-kanban appear in the dashboard
at http://127.0.0.1:9119/kanban and vice-versa, status changes
propagate, and dispatcher transitions are respected.
Foundation for the v2.3.0 kanban-sync user-visible work (UI badge,
dashboard deep-link, live WebSocket updates).
Adds capability detection for the upstream Hermes Agent kanban plugin
mounted at /api/plugins/kanban/. Lays the groundwork for the v2.3.0
work where the workspace's /swarm kanban surface syncs with the
dashboard's SQLite-backed kanban DB.
Changes:
* gateway-capabilities.ts: new probeKanban() probes
/api/plugins/kanban/board on the dashboard URL with a short timeout.
GatewayCapabilities now carries a 'kanban' boolean. Probed once per
PROBE_TTL_MS alongside conductor.
* connection-status.ts: surfaces caps.kanban so client-side feature
gates can react.
* use-feature-capability.ts + feature-gates.ts: 'kanban' is now a
recognized FeatureKey / EnhancedFeature so useFeatureCapability('kanban')
works.
* .env.example: documents HERMES_DASHBOARD_URL (default 127.0.0.1:9119
on current Hermes Agent v0.13+; the legacy 9120 is gone).
Tested locally with hermes-agent main pulled into
/Users/aurora/hermes-dashboard-fresh/repo. Workspace gateway-status
now reports kanban: true when the plugin is mounted, false otherwise.
The browser terminal periodically 'reset back to prompt' during normal
use because any transient SSE disconnect (network blip, browser tab
suspension, HMR reload, dev-server restart) tore down the user's PTY
and dropped them into a fresh shell.
Root cause: terminal-stream's request.signal abort handler called
session.close(), which SIGTERM'd the underlying Python PTY helper.
There was also no auto-reconnect on the client \u2014 a single dropped
read terminated the loop, called /api/terminal-close, cleared the
tab's sessionId, and left the user with an idle tab.
Fix in three parts:
1) terminal-sessions: TerminalSession gains markDetached() and
markAttached(). markDetached() starts a TTL timer (default 5 min,
override via HERMES_TERMINAL_DETACH_TTL_MS) that reaps the PTY only
if no client reattaches in time. The map keeps the session live in
the meantime.
2) terminal-stream: accepts an optional sessionId in the POST body. If
the id matches a still-alive session, the route reattaches to it
instead of spawning a fresh PTY. The 'session' event payload now
includes a 'reattach' flag. On SSE abort, we just detach listeners
and call session.markDetached() \u2014 the PTY stays running.
3) terminal-workspace: passes sessionId on every connect, so reconnect
reattaches automatically. When the read loop ends and the tab still
has a sessionId, we attempt a single quick reattach with a
'[reconnecting...]' nudge to the user instead of tearing the tab
down. /api/terminal-close is no longer called on stream end \u2014 the
server-side TTL handles abandoned sessions.
Fixes#298
Two related session-routing bugs that landed responses (or new-chat
clicks) into the wrong session.
#297 — cross-session response contamination
When the user navigated to a new chat while a previous chat was still
streaming, the previous chat's response chunks would land in the
**new** chat. Three fixes:
* useStreamingMessage now bumps a streamGenerationRef on every
startStreaming call. The fetch-reader loop captures that token at
start and re-checks it on every reader.read() and between events
in the same batch. If the token has changed (because the user
started a different stream), the loop cancels the reader and exits
without dispatching anything. This closes the brief race between
abortController.abort() and the underlying fetch reader actually
stopping, during which buffered chunks were silently writing into
activeSessionKeyRef.current (which had already been switched to
the new session).
* chat-screen now cancels the in-flight stream on session-key change
via a useEffect keyed on (activeCanonicalKey, activeFriendlyId,
isNewChat). Previously nothing cancelled the stream on navigation
\u2014 only the user clicking the explicit Stop button (handleStop)
called cancelStreaming().
#300 — /new slash command opens last chat instead of new session
/new was calling navigate({ to: '/chat' }), but the /chat index route
unconditionally redirects to localStorage('claude-last-session'), so
/new always landed in whichever chat was last active. Fixed at three
entry points so all 'new chat' actions go through the explicit 'new'
sentinel:
* /new in chat-screen.handleUiSlashCommand
* /new in command-palette.runSlashCommand
* 'New Chat' quick-action tile in the search modal
The 'Chat' nav link in the sidebar still goes to /chat (= last session)
\u2014 that's the correct behaviour for a screen-level nav target. Only
'new' actions are routed to the new sentinel.
Closes#297, #300
Reported in #244: a fresh hermes-workspace install on a host where
tmux lives outside of the hard-coded candidate list ("~/.local/bin",
"/opt/homebrew/bin", "/usr/local/bin", or PATH "tmux") shows
'can't find pane: swarm-<id>' for every dispatch because resolveTmuxBin()
returns null.
Adds:
* HERMES_TMUX_BIN (and CLAUDE_TMUX_BIN as legacy alias) env var that
takes precedence over the candidate list in resolveTmuxBin().
* Mirror in tmuxIsInstalled() so the swarm runtime UI doesn't show
'tmux not installed' on hosts that just have it elsewhere.
* /usr/bin/tmux added to the candidate list (typical Debian/Ubuntu
package layout).
Refs #244
The vite dev server intercepted /api/connection-status with a slim
inline shortcut handler that returned only {ok, mode, backend} \u2014
silently overriding the real route at src/routes/api/connection-status.ts
which returns the full ConnectionStatus payload.
Downstream feature gates (useFeatureCapability, useFeatureAvailable)
read .capabilities from the response. With the slim body, every
capability evaluated to undefined, so dev users got UI states that
looked like 'feature not available' even when the gateway was healthy
and exposing the API.
Fix: drop the inline shortcut entirely. The real route file already
caches via ensureGatewayProbed() (PROBE_TTL_MS), so the cost in dev
is negligible and the payload is correct everywhere.
Closes#285. Also drops the now-unused dashboard URL / token / cache
locals in vite.config.ts that were only used by the deleted handler;
the per-PR work that introduced them (#288, #289) was the
right diagnosis but the wrong fix \u2014 the real route was already
doing it correctly.
The update banner truncated its subtitle ('Hermes Agent checkout has
local c...') and never told the user which files were causing the
block. The update-system already detects dirty checkouts but did
not surface the file list to the UI.
Changes:
* Server: ProductUpdateStatus gains an optional blockingFiles array.
When isDirty(repoPath) is true, listDirtyFiles() runs git status
--porcelain and returns up to 24 paths.
* Client: UpdateCenterNotifier removes the truncate class on the
subtitle when blocked (so the full reason is visible), shows the
repo path so the user can tell which checkout is dirty, and lists
up to 8 blocking files with an overflow indicator.
* Reason copy mentions 'remove the listed files' so a user with
untracked files knows what action to take.
Closes#293
Reported in #291. The Search Modal's 'Chats' scope returned 'No
results found' even when sessions clearly existed.
Root cause: fetchSessions() was mapping the API response into
SearchSession with title = friendlyId (the raw cron-style id like
cron_b297a166a31e_20260503_122019), and the filter only searched
friendlyId/key/title. So a human query like 'github' or 'workflow'
never hit anything.
Changes:
* fetchSessions now reads derivedTitle and preview from the API,
prefers derivedTitle for title, and falls back to startedAt when
updatedAt is missing.
* filterResults call for chats now includes 'preview' alongside
friendlyId/key/title.
Closes#291
Recent Searches in the search modal previously displayed four
placeholder strings ('streaming fixes', 'session timeout', etc.)
that never updated.
Changes:
* Adds recentSearches + recordRecentSearch + clearRecentSearches to
the useSearchModal Zustand store, persisted to localStorage under
hermes-recent-searches-v1 (cap 6, dedupe by case-insensitive match).
* Records the trimmed query on result selection (mouse, Enter, or
numeric shortcut). Queries shorter than 2 chars are skipped.
* QuickActions hides the Recent Searches section entirely when there
is no history yet.
Closes#292
From @Interstellar-code's PR #287. Resolves merge conflicts with the
matrix theme (#279) and provider-card verified-status fix (#282) that
landed earlier in the batch. Closes#287.
* Adds 'Custom' provider card to the settings dialog \u2014 always clickable
(not gated by red dot)
* Adds editable Custom Endpoint section (Base URL + API key) in the
provider card flow
* Adds matching Custom Providers section to the full /settings page
with Base URL and CUSTOM_API_KEY support
Co-authored-by: Interstellar-code <Interstellar-code@users.noreply.github.com>
Combines two community contributions and one local correction:
* PR #289 (Sanjays2402): probe /api/sessions on the dashboard URL
(default :9119) instead of the agent URL (:8642). The agent does not
serve /api/sessions, which produced a 404 every 15s. Adds a small
cache so success/failure responses don't refetch every poll. Closes#276.
* PR #288 (Interstellar-code): send Authorization: Bearer
$HERMES_API_TOKEN on the connection-status probes so a gateway
secured with API_SERVER_KEY is detected correctly. Adds watch.ignored
for .runtime/.tanstack/.omc/.omx/coverage/dist/etc. so these noisy
internal paths don't fire spurious dev reloads.
* Local correction: did NOT add '**/routeTree.gen.ts' to watch.ignored.
An existing regression test (src/router-route-resolution.test.ts)
forbids that — ignoring the generated route tree breaks route HMR.
The real cause of routeTree.gen.ts thrash is multiple concurrent
vite dev servers writing to the same file (fixed earlier today).
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
Co-authored-by: Interstellar-code <Interstellar-code@users.noreply.github.com>