Eric's call after living with iter 013 for a few minutes: removed
the Operator Tip and the dashboard reads cleaner without it.
Sessions Intelligence's `flex-1` stretch already absorbs the
bottom-of-column space, so an extra card was redundant.
- DEFAULT_HIDDEN now includes `operator_tip`.
- STORAGE_VERSION 3 \u2192 4 so the existing migration path applies the
new default to returning users while preserving any explicit hides
they had.
- Tip card is still registered in the catalog and reachable from the
edit-mode picker for users who want a contextual nudge.
Tests/build:
- 12/12 aggregator tests
- pnpm build clean
Iter 013 per Eric:
> put tip above session intelligence and make that longer to fill
> the gap at bottom
- Reordered main column: OperatorTip (compact) \u2192 Sessions Intelligence
(the bottom anchor that grows to fill).
- SessionsIntelligenceCard root is now `h-full flex-1` and its row
list is `flex-1 overflow-hidden` so the card consumes the
remaining vertical space in the column.
- Bumped row cap from 8 \u2192 14. The card now has the room.
- Main column wrapper is `min-h-full flex flex-col` and the sessions
WidgetShell is wrapped in a `min-h-0 flex-1 flex flex-col` so the
flex-1 actually expands rather than collapsing to content.
- WidgetShell edit-mode wrapper uses `h-full` so widgets that opted
into flex-1/h-full still expand correctly when in edit mode.
Tests/build:
- 12/12 aggregator tests
- pnpm build clean
- tsc clean for dashboard files
Iter 012 per Eric's iter-011 ask:
> small gap at the bottom by sessions intelligence \u00b7 what should we
> put there standard? tip of the day? or something?
New OperatorTipCard component: a smart 'tip of the moment' card that
sits below Sessions Intelligence in the main column.
Why contextual rather than static:
- A static rotating tip would feel like marketing filler.
- A scoring function per tip lets the dashboard surface the *most
relevant* tip given current state (low cache hit rate, stale
cron, config drift, restart pending, recent achievement, sudden
drop in sessions, top-model concentration risk, etc.).
- 11 tips total; context-specific ones score 40-80, evergreens
3-5 so they only surface when nothing better is relevant.
- Refresh icon cycles to the next-best tip; persists last-shown
index in localStorage so refreshes don't always snap to the top.
- Optional CTA per tip routes to the most relevant page (jobs /
settings / analytics / skills / etc).
Visual: matches the rail card chrome (rounded-xl, gradient bg,
top accent strip, soft glow), with a 36x36 lightbulb chip on the
left and a tip body + tone-colored 'Tip · X/N' eyebrow on the
right. Tone color tracks the relevance category (warn / info /
positive).
Catalog: registered as 'operator_tip', column 'main', visible by
default. Lives at the bottom of the left column under Sessions
Intelligence so it visually balances the side rail extending past.
Tests/build:
- 12/12 aggregator tests
- pnpm build clean
- tsc clean for dashboard files
Iter 011 polish per Eric:
> remove operator console v.12 \u00b7 lower workspace a bit cause the
> gateway version is under
Gateway version was already shipping in the OpsStrip ('\u2666 GATEWAY
V0.12.0'). The header eyebrow was duplicating it within ~30px on the
same screen, which read as visual clutter.
- Removed the 'Operator console \u00b7 v\u003cversion\u003e' subtitle entirely.
- Title row is now a single bold 'Hermes Workspace' lockup with
vertical-centered alignment so it sits visually centered against
the action cluster on the right (instead of biased to the top of
the row from the dropped subtitle).
- Logo + glow ring stay; nothing else moves.
Tests/build:
- 12/12 aggregator tests
- pnpm build clean
Iteration 010 per Eric's iteration-009 ask:
> kept cache its clean fits fine \u00b7 should we remove dashboard text and
> just make hermes workspace larger? there's missing space in middle
> \u00b7 maybe center hermes workspace? \u00b7 add any additional widgets in
> the menu
== Header rebrand ==
- Dropped the redundant 'Dashboard' eyebrow (the page IS the dashboard).
- Promoted 'Hermes Workspace' to the primary heading at text-2xl,
bold, tight-tracked. Now commands the left.
- Eyebrow becomes 'Operator console \u00b7 v\u003cgateway-version\u003e' wired
off `overview.status.version` so the build/version tag is live.
- Logo: bumped 36px \u2192 44px, wrapped in an accent-tinted square with
a soft outer glow ring. Reads as a real product mark, not a tiny
avatar.
- Kept anchored left rather than centering. Ops dashboards (Linear,
Vercel, Datadog) all anchor brand left + actions right because
that's the spatial hierarchy operators expect; centering wastes
the most premium real estate. Put the explanation in the section
comment so future changes know why.
== New menu-only widgets ==
VelocityCard (`velocity`):
- Big number: sessions/day average over the analytics window.
- Delta vs the prior half of the window with tone scaling
(green for positive, warning for >25% drop).
- Sub-stat: API calls/day.
- Tiny daily sparkline (bars).
CostLedgerCard (`cost_ledger`):
- Per-model cost table that splits paid providers from
subscription/included rows so the dollar figure is honest.
- Paid rows sorted by descending cost, included rows by descending
tokens.
- Header chip shows total billed across paid rows only.
- Subscription pattern set covers codex / anthropic-oauth / minimax /
ollama / lmstudio / pc1-* / pc2-* / gemma / llama / qwen.
Both default-hidden so the stock dashboard stays clean; they live
in the edit-mode menu. Fed off existing analytics payload so no
backend changes required.
== Storage migration v2 \u2192 v3 ==
DEFAULT_HIDDEN expanded to: logs_tail, provider_mix, velocity,
cost_ledger.
`readLayout` now does a real schema migration: when a stored
layout's version field is older than STORAGE_VERSION, union the
user's explicit hides with the new defaults so existing installs
don't suddenly sprout widgets they never asked for. Provider Mix
stays hidden post-upgrade, matching Eric's call to keep just Cache.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- tsc clean for dashboard files
- v2 \u2192 v3 migration verified: existing `{hidden:['logs_tail']}`
becomes `{hidden:['logs_tail','provider_mix','velocity','cost_ledger']}`
Iteration 009 per Eric's iteration-008 ask:
> theres just a space from top models to achievements maybe we can
> put another widget there or something? can we add any more graphs
> or charts on the dashboard?
== New widgets, both wired into the right-side stack of the top
analytics row ==
Provider Mix card (`ProviderMixCard`):
- Collapses `analytics.topModels[]` by provider family with a heuristic
(claude-* \u2192 anthropic, gpt-/o1/codex \u2192 openai, gemma/llama/qwen \u2192
local, gemini \u2192 google, grok \u2192 xai, minimax, etc.).
- Renders a CSS conic-gradient donut with the dominant family % +
label inside the hole, plus an inline legend table for the top 4
families and a '+N more' affordance when there are more.
- Family colors cycle through accent / accent-secondary / success /
warning / danger / purple / cyan / yellow so siblings are visually
distinct without us shipping a real palette.
Cache Efficiency card (`CacheEfficiencyCard`):
- Big % stat: cache_read / (cache_read + input).
- Sub-stat: total cache tokens / total input tokens, plus the ratio
multiplier (cache_read / input).
- Daily hit-rate sparkline (bars, not line) so a zero day pops.
- Pure derive from the existing analytics payload.
== Layout ==
The analytics chart row used to be: [chart 8 cols] [Top Models 4 cols].
The right side hosted a single short card next to a tall chart, which
left the dead vertical Eric flagged.
Now the right column is a flex stack of:
1. Top Models
2. Provider Mix
3. Cache Efficiency
This fills the chart-height, gives operators 3x the data density at
the top of the dashboard, and stops the gap between the top row and
the rail below it.
All three cards register in `useDashboardLayout` so they're toggleable
via edit mode just like everything else.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- tsc clean for dashboard files
Iteration 008 per Eric's iteration-007 feedback.
== Premium header icons ==
Replaced emoji glyphs in the action row with Hugeicons stroke
icons + better button chrome:
- New Chat: BubbleChatAddIcon on a real accent gradient button
(was translucent tinted background) with inset highlight, soft
drop shadow, and an animated overlay sheen on hover.
- Terminal: ConsoleIcon
- Skills: PuzzleIcon
- Edit: Edit02Icon \u2192 CheckmarkCircle02Icon when active
- Settings: Settings02Icon
SecondaryAction component now takes a Hugeicons icon (not an emoji
string), uses a subtle card gradient background, accent-colored
icon on hover, and matching uppercase tracking for visual unity
with the primary New Chat button.
Edit + Settings icon-only buttons get the same treatment so the
whole right-hand cluster reads as one premium control group.
== Side rail height/balance ==
- Achievements: query bumped to `achievements=5` (was 3) and the
card renders every unlock the aggregator returns. Switched from
compact \u2192 full-detail rows so the card has body. Together this
fills the gap between Top Models and the rest of the rail.
- Side rail container: `min-h-full` so the column stretches to
Sessions Intelligence height instead of collapsing to content.
- Mix & rhythm card: `flex-1 h-full` + `justify-between` so it
consumes the remaining vertical space at the bottom of the rail
and aligns flush with Sessions Intelligence's lower edge.
== Attention bar separated ==
Eric: 'is it cluttered or should be higher or lower / separate?'
- Lifted the AttentionMarquee out of OpsStrip into its own
dedicated row above the gateway strip. Now a self-contained
warning-tinted chamber with its own border + gradient. Clearly
reads as 'things to look at' separate from 'gateway is up'.
- OpsStrip reverted to its single-row layout (no more nested
vertical stack).
- When there are no incidents, the row simply doesn't render \u2014
no empty frame.
Build/tests:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- tsc clean for src/screens/dashboard/* and src/server/dashboard*
Iteration 007 per Eric's iteration-006 feedback:
== Attention marquee ==
- New `AttentionMarquee` component renders the existing
`incidents[]` payload as a right-to-left ticker. Lives inside
`OpsStrip` as a dedicated full-width row that only appears when
there is something to surface.
- Animates via CSS keyframes (`@keyframes oc-attention-marquee`,
32s linear infinite). Pauses on hover so the operator can read a
long item. Respects `prefers-reduced-motion` and disables the
animation in that case.
- Track is duplicated once for the seamless wrap-around. Soft fade
mask on the right edge for the 'ticker continues' affordance.
- Each item routes to the most context-appropriate page (cron \u2192
/jobs, config \u2192 /settings, log/gateway \u2192 /jobs as fallback) or
to the incident's own `href` when set.
- Glyph + severity color picked from existing `source` and
`severity` enums on `DashboardIncident` so the marquee stays
fully data-driven.
== Side rail rebalance ==
- AttentionCard removed from the side rail (its data is now in the
marquee). Import dropped.
- AchievementsCard moved to the *top* of the side rail. The right-of-
chart visual position effectively places it 'under Top Models'
which is what Eric asked for.
- New rail order:
1. Achievements
2. Skills usage
3. Mix & rhythm
- Mix & rhythm stays \u2014 it's the only chart left in this column and
the unique non-deep-route insight (token shape + 24h heatmap).
== Layout v2 ==
- WidgetId catalog drops `attention` (not a widget anymore).
- New `logs_tail` default state is HIDDEN. Eric's read: it's a
triage tool, not a dashboard staple. Power users can re-add via
the edit-mode panel.
- `useDashboardLayout` now writes a `version: 2` field to its
storage payload and seeds first-load state from `DEFAULT_HIDDEN`
when no localStorage entry exists, so the new defaults apply
cleanly to fresh installs without nuking returning users' explicit
hides.
- Reset button now returns to the iteration defaults rather than
show-everything, so first-time Reset doesn't surprise users with
Logs they never wanted.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- All edit-mode toggling round-trips cleanly with the v2 schema.
Iteration 006. Two product asks from Eric's iteration-005 feedback.
== Edit mode ==
New `useDashboardLayout` hook owns:
- which widgets are hidden (persisted to localStorage key
`dashboard.layout.v1`)
- whether the dashboard is in edit mode (session state)
Catalog of 8 toggleable widgets with column metadata (main vs rail):
analytics_chart, top_models, sessions_intelligence, logs_tail,
attention, skills_usage, achievements, mix_rhythm.
UI:
- New `WidgetShell` wrapper: zero-overhead passthrough when edit mode
is off; in edit mode adds a dashed accent outline + an X close
button in the top-right corner of the widget. Click X to hide.
- New `EditModePanel` banner: only renders when edit mode is active.
Shows widget toggle pills grouped by column (Main / Side rail) so
the operator can re-add hidden widgets. Includes Reset (show all)
and Done buttons. Visible-count chip shows '5 of 8 widgets shown'.
- New header pencil icon (\u270f\ufe0f) toggles edit mode; flips to checkmark
(\u2713) when active, with accent border to make state obvious.
Layout adaptiveness:
- Top row: Analytics chart and Top Models share a 12-col grid. If
one is hidden, the other expands to fill (col-span-12) so we don't
end up with a half-empty row.
- All sections check `layout.isVisible(id)` before rendering so
hidden widgets cost zero DOM nodes.
== Side rail visual cohesion ==
Image review of iteration-005 flagged the rail cards as having
ragged heights and inconsistent action-link styling. Fixes:
- AchievementsCard: replaced the legacy `rounded-md bg-card/40`
chrome with the same `rounded-xl border` + top gradient accent +
diagonal card gradient that AttentionCard / TokenMixHourCard /
SkillsUsageCard already use. The 'View all \u2192' button no longer
sits as a separate full-width row; merged into the title-row
microcopy ('48 unlocked \u00b7 view all \u2192') so the rail breathes.
- SkillsUsageCard: matches the same diagonal gradient background
(was flat var(--theme-card)). Title color promoted from muted to
text. 'manage \u2192' microcopy gets the accent color on hover so all
rail action affordances behave the same.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- localStorage key namespaced `dashboard.layout.v1` so future schema
changes can bump cleanly.
Iteration 005 per Eric's feedback after the iteration-004 screenshots.
Hero KPI row:
- Drop the Cost tile entirely. Even with the 'partial / included'
trust label it kept reading as misleading on a workload that's
almost entirely Codex/OAuth.
- New ActiveModelKpi tile in the 4th slot. Shows active model name +
Online/Offline pulse + share-of-calls chip + provider/sessions
microcopy + ctx length. Same gradient form factor as the other
three tiles so the row stays balanced.
- HeroMetrics now takes an optional extraTile slot so the dashboard
can compose the row without coupling the metrics widget to model
data.
Side rail (final order, top to bottom):
1. Attention
2. Skills usage
3. Achievements
4. Mix & rhythm (new)
- Drop the rail-side Active Model card (its data is now in the hero).
- Move Skills + Achievements up so the rail leads with the cards Eric
finds most useful.
- New TokenMixHourCard fuses the previously separate Token Mix and
Hour of Day cards into a single 'rhythm' card sharing chrome,
header, and typography rules. Halves still independent so a fresh
install with no analytics or sessions still hides cleanly.
Hero KPI row CSS bumped to grid-cols-1 sm:2 lg:4 so the new tile
stacks rather than overflowing on narrow viewports.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- /api/dashboard/overview shape unchanged \u2014 this is UI-only.
Iteration 004. UI-only polish + new charts based on the Hermes Agent
data audit. No new backend endpoints required.
Aggregator:
- Cost honesty: new `analytics.costLabel` with values `precise` /
`partial` / `included` / `unknown`. Computed from per-model
session counts split between priced providers and known
subscription-included ones (codex / anthropic-oauth / minimax /
ollama / lmstudio / pc1-* / pc2-*). Stops the dashboard from
showing '$0.052 for 247M tokens' as if that were precise.
- Insights tightened:
- Capped at 3 (was 4).
- Drops 'no active runs' line when activity peaked today (was
contradicting the peak callout in the same card).
- Skill names in callouts now strip the `namespace:` prefix.
- Model ids in callouts use the trailing slash segment instead of
raw provider/model strings.
- New presentational helpers `shortSkillName` / `shortModelName`
inside the aggregator so insight text is render-ready.
UI:
- HeroMetrics cost tile: reads `costLabel` and renders 'Included' /
'partial \u00b7 some included' / dollar figure / em-dash accordingly.
Hides delta + sparkline for non-precise variants.
- New `SkillsUsageCard`: replaces the lonely '60' tile. Top-5 used
skills as a horizontal bar chart with name (last segment), use
count, and percentage. Fades to '\u003cN\u003e installed' fallback when
no usage in the window.
- New `TokenMixCard`: stacked horizontal bar of input / output /
cache / reasoning split with hover tooltips and an out/in ratio
chip.
- New `HourOfDayCard`: 24-bucket activity strip computed
client-side from session `startedAt`. Highlights the peak hour
and labels the timeline at 12a / 6a / 12p / 6p.
- OpsStrip: appended `pulse Xm ago` microcopy from the new
`status.lastHeartbeatAt` field.
- SessionsIntelligenceCard:
- Better kind icon resolution. New `sessionGlyph` helper checks
`kind`, `source`, and the `cron_<jobId>_*` key heuristic so
cron sessions actually show a clock instead of a chat bubble.
- Adds api_server / signal / imessage / matrix / local mappings.
- New `formatSkillName` helper in lib/formatters.ts (strips colon
and slash namespaces).
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- Live: costLabel='partial' for our usage (codex + opus mix), insights
count = 3 (was 4), gpt-5.4 in callouts instead of openai/gpt-5.4.
Iteration 003. Drop the legacy 14d Activity chart; reclaim the space
with a sessions intelligence card. Adopts every Hermes Agent answer
on the source-of-truth fields.
Aggregator:
- /api/dashboard/overview now also probes /health/detailed via the new
gatewayFetch helper. status.activeAgents reads canonical
active_agents from the gateway runtime; status.activeSessions
preserves the /api/status heuristic for separate display.
- status.lastHeartbeatAt added (alias of gateway_updated_at).
- cron now exposes failed count + recentFailures[] (id, name,
last_error, last_run_at).
- skillsUsage section parses analytics 'skills' object: totalLoads,
totalEdits, totalActions, distinctSkills, topSkills[] with
percentage. (Hermes Agent confirmed schema.)
- New top-level insights[] computed server-side: peak day driver,
cache delta vs prior period, ops pulse (failed/stale crons +
no-active-runs + restart-pending), top-skill heat. UI no longer
recomputes.
- New top-level incidents[] aggregates cron failures + stale cron +
paused cron + platform errors + config drift + restart-pending +
log-tail errors into one triage list with hrefs.
UI:
- Drop ActivityChart and the legacy Recent Sessions list.
- New SessionsIntelligenceCard:
- Real human title from derivedTitle (falls back to short slug).
- Kind/source icon (chat / cron / cli / telegram / discord / api).
- Badges: hot (active <5m), tool-heavy (>=20 tools), high-token
(>=50k), error, stale (>7d).
- Hot row gets accent border so the operator sees what's running.
- Hierarchy: model chip · msgs · tools · tokens · recency.
- Click row -> /chat/<sessionKey>.
- AttentionCard now consumes overview.incidents directly. Source icon
per item. Click items navigate to /jobs / /settings.
- Skills tile: real 'top: <skill>' from skillsUsage.topSkills[0]; row
reads '<enabled> enabled · <distinct> used · top: <skill>'.
- AnalyticsChartCard switches to overview.insights so the UI is dumb.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12,
+3 new: failed-cron incidents, /health/detailed active_agents,
skills usage parsing)
- pnpm build (passes)
- Live: 31 distinct skills used in 30d, top
'autonomous-ai-agents:hermes-agent' (12 uses), 3 incidents surfaced
(stale cron, paused cron, 6 config diffs).
Iteration 002 - addresses the Hermes Agent product review.
New widgets:
- AttentionCard: prioritized 'what to look at right now' list. Pulls
stale cron, log errors/warns, config drift, restart-pending, and
platform error states from the existing overview payload. Shows an
explicit 'all clear' state when nothing needs eyes. Replaces the
scattered warning chips and gives the operator a single command line.
- AnalyticsChartCard: daily trend chart + 2-3 client-side insight
callouts ('Usage peaked Apr 17, driven by GPT-5.4', 'Cache reads up
X% vs prior period', 'no active runs · restart pending'). Period
switch (7d / 14d / 30d) at top-right; selection persists to
localStorage and feeds the same window into Hero KPIs and the rest of
the overview.
- TopModelsCard: standalone right-rail card so the model breakdown is
no longer cramped inside the analytics hero. Shows tokens bar plus
'% of calls' (proxy for routing share) and sessions per model.
- ModelInfoCard now adds an operational microcopy line
('66% of calls · 113 sessions · 30d') and a click-through 'Inventory'
modal that lists every model from /api/models grouped by provider,
with active-model highlight and live filter.
UX/microcopy fixes:
- OpsStrip: '0 ACTIVE' -> '0 active runs', 'CONFIG +6' -> '6 config
diffs', stale-cron pill now visually warns (warning border + tinted
background) instead of muted text only.
- Action row: New Chat is now a primary gradient button, Terminal +
Skills are secondary monochrome buttons, Settings is icon-only. Top
visual weight cut ~30%.
- SkillsWidget: replaced the 6-row mini list with a summary tile
('42 installed · 39 enabled · top: Airtable'). Click-through opens
/skills.
- Analytics card period-aware loading state: shows ' · refreshing…'
microcopy in the header while the period switch is in flight.
Plumbing:
- /api/dashboard/overview now respects ?days=N from the client; query
key includes period so React Query refetches when the operator
switches windows.
- Period default 30d preserved; valid options 7 / 14 / 30 only.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (9/9)
- pnpm build (passes)
- Live: /api/dashboard/overview?days=7 returns 7-day window with
56.5M tokens, top models gpt-5.4 + claude-opus-4-7.
Phase 2 iteration 001. The Workspace dashboard was already aggregating
`/api/analytics/usage` but parsing it for the wrong shape (legacy
`top_models`/`total_tokens`), so the analytics card stayed hidden
even though the gateway returned 247M tokens / 788 sessions.
Aggregator changes:
- normalizeAnalytics now reads native Hermes `{ totals, by_model, daily,
period_days }` and falls back to the legacy shape when present. New
fields exposed: inputTokens, outputTokens, cacheReadTokens,
reasoningTokens, totalSessions, totalApiCalls, daily[], plus a
`source` discriminator (`analytics` | `unavailable`).
- New logs section: pulls `/api/logs?lines=N`, classifies error/warn
counts, returns last N lines for tail UIs.
- Default analytics window bumped 7d → 30d to match native dashboard.
UI changes:
- HeroMetrics: 4 big tiles (Sessions, Tokens, API Calls, Cost) with
inline SVG sparklines + period-over-period delta chips. Replaces the
legacy 4 small MetricTile row.
- AnalyticsHeroCard: large daily area chart + top-5 models breakdown +
totals line. Click 'Expand' opens a modal with a stacked
input/output/reasoning bar chart and full per-model breakdown
(sessions / calls / cost).
- LogsTailCard: rolling tail with error/warn pulse and Expand modal.
Modal auto-refreshes every 3s and supports all/errors/warns filter.
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (9/9)
- pnpm build (passes)
- Live smoke: /api/dashboard/overview now returns analytics.source=
analytics with 247M totalTokens, 29 daily entries, 5 top models, and
log tail with 24 lines.
The README's feature list and roadmap were stale: missing the new MCP
page (#231), Operations dashboard, Agent View panel, Conductor screen,
and capability-gate work landed in the past 24-48h. Roadmap still
listed shipped features as 'In Development'.
Changes:
- Replace short 'Features' bullet list with a comprehensive 'What's
inside' that mentions every major surface, including the explicit
Conductor caveat with a link to #262.
- Roadmap restructured into Shipped / In progress / Coming sections.
Conductor gets its own 'in progress' row with the upstream-plugin
caveat. Native Desktop App moved to 'in progress' per spec status.
Multi-provider support called out explicitly so users know what
works on day one.
- Drop the in-house 'Agent W Managed Companion' subsection from public
README \u2014 it's internal-team-only deployment notes that don't apply
to anyone running upstream.
- Collapse the duplicate '## Features' section near the bottom into the
consolidated security section. Avoids saying the same thing twice.
- Tighten security headings: now '## Security & deployment env vars'
with two clean subsections: 'Built-in safeguards' and 'Env vars for
remote / Docker deployments'.
Build clean. No code changes.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
The 2026-05-01 codename rename swept most of the repo from 'Claude' to
'Hermes' but left a few env vars and README copy that still referenced
the old name. Users on fresh installs were configuring HERMES_PASSWORD
based on the docs but the auth middleware only read CLAUDE_PASSWORD,
silently bypassing the guard.
Changes:
- src/server/auth-middleware.ts: read HERMES_PASSWORD first, fall back
to CLAUDE_PASSWORD for back-compat with pre-rename setups.
- server-entry.js: same fallback for HERMES_PASSWORD +
HERMES_ALLOW_INSECURE_REMOTE. Error messages updated to point at the
new names.
- README.md:
- Drop the misleading 'v2 zero-fork = full feature parity' framing;
call out that Conductor specifically requires an upstream dashboard
plugin not yet shipped (per #262), with a link.
- Theme list updated to current names (Hermes / Nous / Bronze /
Slate / Mono \u2014 the v2.1 rename) instead of the old 'Official /
Classic' labels.
- Replace CLAUDE_PASSWORD / CLAUDE_ALLOW_INSECURE_REMOTE references
with HERMES_* primary names + back-compat note.
- Add HERMES_API_TOKEN to the security env-var list (previously
undocumented despite being honored by the gateway-capabilities probe).
- Inline note on the avatar PNG asset name being retained for cache
stability \u2014 the 'claude-avatar' filename is intentional.
- .env.example: HERMES_PASSWORD + HERMES_ALLOW_INSECURE_REMOTE primary
names with legacy notes.
- docker-compose.yml: HERMES_PASSWORD: ${HERMES_PASSWORD:-${CLAUDE_PASSWORD:-}}
so existing compose files that set CLAUDE_PASSWORD keep working while
new files use HERMES_PASSWORD.
Build + auth-middleware tests pass.
No public-facing breaking change: every previous CLAUDE_* env var still
resolves correctly through the back-compat fallbacks.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
Agents seeded by seedAgentPresets() (Sage, Trader, Builder, Scribe, Ops)
get system prompts on first load, but the profile dir's config.yaml has
no model configured by default. Dispatching into one hangs because
hermes-agent has nothing to call.
Add OperationsAgent.needsSetup boolean (true when agent.model is empty)
and surface it on OperationsAgentCard:
- Amber 'Needs setup — click to configure' button below the description
- Play button turns amber and routes to onOpenSettings instead of running
- Tooltip explains why on hover
Click either path \u2014 needs-setup banner OR amber play button \u2014 to open
the settings modal where the user can pick a model. Once a model is set,
needsSetup flips to false and the agent dispatches normally.
Refs #270.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
Conductor (#262):
- Add 'conductor' capability to gateway probe (probeConductor probes
/api/conductor/missions on the dashboard).
- Surface caps.conductor in /api/connection-status response.
- New <FeatureNotReady> component for graceful 'upstream not ready'
placeholders when a feature requires endpoints the agent doesn't have.
- New useFeatureCapability(key) hook polling /api/connection-status.
- /conductor route now wraps Conductor in a capability check: shows the
placeholder when the dashboard /api/conductor/missions endpoint isn't
there, instead of letting the UI 500 mid-action.
Swarm (#244):
- Toast error when /api/swarm-tmux-start returns 'tmux not installed'
with install instructions instead of silent console.error.
- Upgraded the in-screen 'tmux not installed' banner from grey muted text
to amber alert with the brew/apt install commands inline. Explains that
workers can't dispatch tasks without tmux ('can't find pane: swarm-X'
errors come from this missing dependency).
Build clean. Refs #244, #262.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
* feat(mcp): MCP server management page (Phase 1)
Implements the MCP management plan (.omc/plans/mcp-management.md) Phase 1
end-to-end on a single feature branch (PR1+PR2+PR3+PR4 collapsed):
- New `/mcp` route with capability gate + BackendUnavailableState fallback.
- New `/api/mcp` (GET list, POST create), `/api/mcp/test` (POST connection
probe), `/api/mcp/discover` (POST tool discovery for a draft config),
`/api/mcp/configure` (PUT enable/toolMode/include/exclude), and
`/api/mcp/$name` (DELETE).
- Strict `mcp` capability probe in gateway-capabilities: hits `GET /api/mcp`
directly and validates the body parses through `normalizeMcpList` —
dashboard-up-but-route-missing returns false (resolves Open Question #4).
- Type split: read shapes in `src/types/mcp.ts` (client+server), write
shapes in `src/types/mcp-input.ts` (server-only; secrets contained here).
- Runtime normalization layer `src/server/mcp-normalize.ts` mirrors the
Skills `asRecord`/`readString`/`normalizeSkill` defense — strips
unknown fields, coerces enums, masks secrets via `MASK_SENTINEL`,
re-applies via `maskSecretsInPlace` before every `json(...)`.
- All write endpoints CSRF-checked via `requireJsonContentType`.
- Capability-off responses use `createCapabilityUnavailablePayload('mcp')`
with `{ servers: [], total: 0, categories }` for GET (200) and 503 for
writes — feature gates fall open without throwing.
- Static preset catalog (`src/screens/mcp/presets.ts`) with GitHub,
Filesystem, Postgres, Slack, Linear; Catalog tab installs prefilled
drafts through the same dialog flow.
- Screens: `McpScreen` (Installed/Catalog/All tabs + search + category
filter), `McpServerCard` (status badge + Test/Edit/Delete + enable
toggle), `McpServerDialog` (HTTP/stdio + auth + Discover + Save with
bearer-token clear-on-submit).
- TanStack Query hooks (`useMcpServers`, `useTestMcpServer`,
`useDiscoverMcpTools`, `useUpsertMcpServer`, `useConfigureMcpServer`,
`useDeleteMcpServer`).
Tests (vitest):
- `src/server/mcp-normalize.test.ts` — 13 tests covering enum coercion,
list-shape variants, malformed-entry drop, presence flags without
echo, env/header masking by key hint, idempotency, test-result
normalization, payload-string scanner.
- `src/routes/api/-mcp.test.ts` — 8 tests covering input validation,
capability fall-open shape, CSRF gate (415 on non-JSON POST, pass on
JSON, pass on GET), and the **secret echo guard**: a worst-case agent
that echoes a submitted bearer token in body/env/headers must never
surface the original string in the workspace response.
Build, lint, and the new test files are clean. Pre-existing unrelated
test failures on `local` (router-route-resolution, context-usage,
markdown math, slash-command-menu, chat-message-list, gateway-capabilities
env-source) are unchanged by this PR.
Worked with Interstellar Code
* fix(mcp): strip secret fields from client-safe McpClientInput
Architect review flagged that `McpClientInput` in `src/types/mcp.ts` (the
file explicitly designated for client+server read shapes with no secrets)
contained `bearerToken` and `oauth.clientSecret`, allowing the browser
bundle to import a secret-bearing type via the dialog component.
Resolves the type-split violation:
- `src/types/mcp.ts`: drop `bearerToken` and `oauth` from `McpClientInput`.
Now strictly the browser-safe form payload, no secret fields.
- `src/screens/mcp/components/mcp-server-dialog.tsx`: hold `bearerToken`
in ephemeral component-local `useState<string>` typed inline. Cleared
on submit and on dialog open. No exported type carries the field.
- `src/screens/mcp/hooks/use-mcp-mutations.ts`: `useUpsertMcpServer`
accepts `McpClientInput & { bearerToken?: string }` inline at the
call-site, again with no exported secret-bearing type. Server route
`parseMcpServerInput` re-validates and forwards to the agent.
The full server-side write shape (`McpServerInput` with secrets) remains
in `src/types/mcp-input.ts`, server-only.
Worked with Interstellar Code
* fix(mcp): block client imports of server-only mcp-input types
Add no-restricted-imports rule scoped to src/screens/** and
src/components/** that blocks importing @/types/mcp-input. That
file may carry unmasked secrets and is server-only — clients should
import McpClientInput from @/types/mcp instead.
Worked with Interstellar Code
* feat(mcp): wire /mcp into all sidebar/nav surfaces
Mirror the existing /skills registration across every nav and
command surface so the MCP screen is reachable from the dashboard
overflow grid, command palette, mobile hamburger drawer, mobile tab
bar, slash menu, search modal quick actions, and workspace shell
(active-tab tracking + mobile page title). Inspector panel gets a
parallel MCP tab that lists configured servers via /api/mcp.
Worked with Interstellar Code
* feat(mcp): catalog tab search, category badges, and nav coverage tests
Catalog tab now reuses the screen's search state to filter presets
by name/description, surfaces an empty-state when no presets match,
and renders each preset as a card with an Official Presets category
badge styled to match the skills-screen design vocabulary.
Tests:
- src/components/-mcp-nav.test.tsx: each modified nav file references
the /mcp route (or registers an mcp tab id for inspector-panel)
- src/screens/mcp/-presets.test.ts: filtering MCP_PRESETS by query
narrows results by name and description, returns full catalog for
empty queries, and returns nothing for unknown queries
Worked with Interstellar Code
* feat(mcp): add MCP entry to chat-sidebar Knowledge group
The primary visible left rail (`chat-sidebar.tsx`) was missed by the
prior nav-coverage commit. Slot MCP between Skills and Profiles in
`knowledgeItems`, mirroring the McpServerIcon used elsewhere.
Worked with Interstellar Code
* feat(mcp): Phase 3 — live tool refresh, OAuth reauth, per-server SSE logs
- `useMcpServers`: enable refetchOnWindowFocus for live state.
- `McpServerCard`: per-card Refresh button (re-runs Test, updates
discoveredToolsCount), Reauth button when authType === 'oauth' (uses
new useMcpOAuth hook), Logs button (opens McpLogsDrawer).
- `use-mcp-oauth.ts`: opens auth URL in new tab, polls /api/mcp/test
every 2s until status === 'connected' or 60s timeout. Returns
mutation-style { start, isPending, isError, error, data }.
- `mcp-logs-drawer.tsx`: fixed-right slide-in drawer subscribing via
EventSource to /api/mcp/<name>/logs. Newest-first, max 500 lines,
auto-scroll, tear down on close (no zombie EventSource).
- `routes/api/mcp/$name.logs.ts`: SSE proxy with auth + capability
gates. Capability-off → 503. Pattern follows chat-events.ts.
- Tests: 3 new for logs route (input validation, capability-off,
auth gate); smoke test for useMcpOAuth shape.
Total tests: 24 passing (13 normalize + 8 mcp + 3 logs). Build clean.
Worked with Interstellar Code
* feat(mcp): localhost-only config-fallback transport (Phase 1.5)
Adds an `mcpFallback` capability that lets the workspace perform CRUD on
`config.mcp_servers` via the existing dashboard `/api/config` route when
the agent does not yet expose the new `/api/mcp*` runtime endpoints.
Gated to loopback-only deployments by `isLocalhostDeployment()` (both
URLs loopback AND HOST unset/loopback). Test/Discover/Logs return a
structured "not yet available" payload in fallback mode; the MCP screen
renders an amber banner so the limitation is visible.
Worked with Interstellar Code
* feat(mcp): full catalog + marketplace + sources manager (Phase 2-3.2)
Workspace-only end-to-end MCP catalog + marketplace replacing the static
presets.ts and the upstream /settings/mcp surfaces.
Phase 2 — File-backed catalog:
- assets/mcp-presets.seed.json + ~/.hermes/mcp-presets.json (atomic
bootstrap via tmp+linkSync, mtime+ino+ctime+size cache, malformed-file
preservation, schema validation: id regex, transport-specific fields,
env key regex, https URLs, category allowlist, duplicate-id rejection,
unknown-field warnings)
- src/server/mcp-input-validate.ts: shared parseMcpServerInput returning
per-field {path, message} errors; promoted from inline definition
- src/routes/api/mcp/presets.ts GET handler
Phase 3.0 — Federated marketplace:
- src/server/mcp-hub/{cache,trust,index,types}.ts + sources/{mcp-get,
local-file}.ts: Smithery registry adapter (replaces speculative
registry.mcp.run NXDOMAIN), ETag/If-Modified-Since with 304 reuse,
rate-limit handling, parallel Promise.allSettled across sources with
8s per-source timeout, dedupe by source+id+name, fallback to
local-file when remote degraded
- Trust hardening: shell metachar reject, transport allowlist, env-key
regex, control-char + absolute-path attack defenses, inline-exec
flag detection (-c, -lc, -e for sh/bash/python/node/perl/ruby)
- src/screens/mcp/components/install-confirmation-dialog.tsx: 2-click
commit with full template preview (command/args/env masked) and
AbortController on dismiss
- Disk persistence for tool-discovery cache (mcp-tools-cache.ts) +
hermes-mcp CLI bridge (mcp-cli-bridge.ts) for live test/tool
enumeration in fallback mode
Phase 3.2 — User-configurable sources:
- ~/.hermes/mcp-hub-sources.json schema (built-ins always present,
protected from mutation; user can add HTTPS-only generic-json
sources with trust+format)
- src/routes/api/mcp/hub-sources{,.$id}.ts CRUD with per-process mutex
(read-modify-write race protection)
- generic-json adapter: SSRF guard (private/loopback/link-local/IPv6
ULA all rejected after DNS resolution, redirects disabled), 5MB
response-size cap (streaming read), trust hard-cap at 'community'
for user-source entries, source field 'user:<id>' for dedupe
- src/screens/mcp/components/sources-manager-dialog.tsx UI
Polish:
- Placeholder detection at install confirmation (inline fill form
blocks commit until /path/to/, <your-...>, empty *_TOKEN/_KEY/etc
resolved)
- Test result UX hints when stdio Connection closed + placeholder args
or http fetch failed + placeholder url
- Env-ref preserved in normalize (${VAR_NAME} no longer masked) +
Edit dialog diagnostic
UI: Skills-pattern parity for /mcp screen (Tabs + Marketplace tab,
Switch primitive, Button primitives, DialogRoot/Content, primary-*
Tailwind classes matching skills-screen.tsx). Single-row toolbar
(tabs + search + filter). Removed All + Catalog tabs, kept Installed +
Marketplace.
Backend:
- gateway-capabilities probeMcp uses authenticated dashboardFetch
(Codex MAJOR fix); probeMcpConfigKey + isLocalhostDeployment for
mcpFallback capability
- routes/mcp.tsx route gate accepts mcp || mcpFallback
- mcp-normalize.ts headers.Authorization + env *_TOKEN/_KEY/_SECRET
/_AUTH/_APIKEY auth detection upgrades authType to 'bearer'
Removed (replaced by /mcp):
- src/screens/settings/mcp-settings-screen.tsx (759 LOC)
- src/routes/settings/mcp.tsx
- src/routes/api/mcp/{servers,reload}.ts (orphaned endpoints; reload
posted to gateway 404s)
- src/screens/mcp/presets.ts (static array, replaced by file-backed)
- settings-sidebar MCP nav entries (replaced by main /mcp route)
Tests: 263+ passing across 19+ MCP suites — input-validate, presets-
store, hub-cache/trust/unified-search, sources/{mcp-get,local-file,
generic-json}, hub-sources-store, mcp-tools-cache, ssrf-guard,
marketplace-install-confirmation, marketplace-placeholder-detection,
hub-search/-presets/-hub-sources route tests. Pre-existing 2
gateway-capabilities env-resolution failures unrelated.
Reviewers: Codex critic 4 passes (Phase 2 REJECTED → 8 fixes applied,
Phase 3.0 APPROVED-WITH-CHANGES → 4 fixes, Phase 3.2 REJECTED → 6
fixes including SSRF guard + response-size cap + concurrent-CRUD
mutex + trust cap). Architect approved final pass.
Worked with Interstellar Code
The composer workspace selector and file browser now resolve through one
profile-local workspace catalog, so ~/.hermes remains runtime state instead of
becoming the project root.
Changes:
- Rework /api/workspace into a profile-local workspace catalog
- Prevent ~/.hermes, Hermes profile dirs, and system roots from being
selected as workspaces
- Make /api/files use the same workspace catalog as the composer
- Update the composer workspace selector to list/switch backend workspaces
- Invalidate workspace state after profile switches
- Change the workspace menu action to open the in-place files sidebar
instead of navigating to /files
- Remove the "Add path manually..." button (used window.prompt; not suitable
for remote workspaces)
- Composer bottom-row layout: profile, workspace, reasoning, model, context
ring near send button; mic/attachment ordering aligned
- Add focused regression coverage for workspace resolution and composer
control wiring
Constraint: Hermes Web UI treats workspaces/spaces separately from
profile state.
Rejected: Keep localStorage saved workspace paths | stale saved paths could
keep showing ~/.hermes.
Rejected: Keep /api/files fallback to HERMES_HOME | files sidebar would still
browse runtime state.
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce HERMES_HOME/CLAUDE_HOME as project workspace
fallbacks; use /api/workspace catalog instead.
Tested: targeted vitest for workspace/files/composer controls; targeted
eslint; LSP diagnostics on key changed files; pnpm build green.
Not-tested: dedicated Spaces manager UI remains follow-up.
Worked with Interstellar Code
- Add zh-TW locale with Taiwan-specific terms (檔案, 終端機, 記憶體, 市集, 設定, 儲存, 搜尋, 載入中 etc.)
- Update getLocale() to match full navigator.language before splitting, ensuring zh-TW users are not routed to Simplified Chinese
- Rename zh label to 中文(简体)for clarity
Co-authored-by: 你的名字 <你的電子信箱>
* feat(skills): origin detection, source path, and category alias map in API
- Cross-reference .bundled_manifest + SKILL.md frontmatter to classify
each installed skill as built-in, agent-created, or marketplace
- Populate sourcePath with resolved local path (~/.hermes/skills/<cat>/<id>)
for installed skills (was previously empty)
- Add category alias map: gateway returns lowercase dir names (research,
productivity); map these to display labels for filter compatibility
Worked with Interstellar Code
* feat(skills): origin badges, filter, card layout, and security popup fix
- Add origin badge on each skill card (Built-in / Agent-created / Marketplace)
alongside existing Installed badge
- Add origin badge in skill detail dialog beside Source path field
- Add origin filter dropdown in Installed tab to filter by origin
- Fix card layout: skill icon now inline with name, author line hidden when empty
- Fix security popup transparency: stacking-context conflict with neighboring
cards resolved via inline background var + z-index promotion on card hover
Worked with Interstellar Code
* feat(mcp): align toolbar tab layout with Skills; frosted-glass tooltip
- MCP page toolbar: move Installed/Marketplace tabs to right side,
matching the Skills page toolbar layout for consistency
- Tooltip: translucent themed background with backdrop-blur for
frosted-glass appearance (affects chat sidebar icons, security
popups, and all tooltip usages globally)
Worked with Interstellar Code
* feat(chat): widen default content column to 1125px (25% wider)
Bumps `--chat-content-max-width` from 900px to 1125px so the
default ('comfortable') chat column is roomier without forcing
users to switch to the 'wide' or 'full' settings.
Worked with Interstellar Code
- Persist final assistant message to sessionStorage on 'done' event
- Merge recovery message into history if backend hasn't caught up yet
- Clear buffer once server history confirms the message
- Refactor: replace (msg as any) casts with type-safe bracket access
- Refactor: extractMsgId() helper in use-chat-history.ts
- Refactor: getMessageTimestamp() uses Array<unknown> + bracket access
The previous probe used a generic GET to /api/sessions/__probe__/chat/stream
and treated any non-404/403 status as 'available'. Vanilla hermes-agent
serves a router-level handler at sessions paths but doesn't actually
expose the streaming POST endpoint there, so it was returning 405 (or
similar) on the GET, which the probe interpreted as 'enhanced chat is
available'. Workspace then routed sends through streamChat() which
posts to /api/sessions/{id}/chat/stream, which 404s on vanilla agent,
and the bundle's error mapper surfaced it as a generic 'Authentication
error' to the user.
Replace the generic probe with probeEnhancedChatStream() that:
- POSTs (the real method) instead of GET
- Treats 404 (path missing) and 405 (path mismatch) as not-available
- Treats any other status as available (4xx structured errors / 401
auth gates / streaming start all imply the path is registered)
Result: getChatMode() correctly returns 'portable' on vanilla agent,
send-stream.ts takes the openaiChat path, chat works without patches.
Refs #261.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
* feat(agent-view): port AgentViewPanel from controlsuite into Workspace chat
- Copy 9 agent-view components + use-agent-spawn hook
- Copy use-agent-view zustand store + use-orchestrator-state hook
- Copy orchestrator-avatar (1490 LOC, 9 SVG avatars + animations)
- Copy agent-personas lib (Roger/Sally/Bill/Ada/Max/Luna/Kai/Nova)
- Rename storage keys controlsuite-* -> hermes-workspace-*
- Mount <AgentViewPanel /> in chat-screen.tsx after main, before terminal
- Demo data wired via createDemoActiveAgents in use-agent-view
Iteration 001 of goal 2026-05-02-agent-view-port-from-controlsuite.
All deps already present in Workspace. Greek persona swap + Hermes avatar art = next iter.
* feat(agent-view): demo fallback when live gateway returns zero agents
Hermes Workspace session status vocabulary (idle/active/etc) doesn't yet
map onto ControlSuite's running/queued/completed brackets, so live mode
was returning empty arrays and hiding the panel content.
Phase 1: fall through to demo data when classifier produces 0 agents,
so we can see the full UI visually before Phase 2 status mapping work.
* feat(agent-view): default avatar owl, drop lobster as first-run default
Per Eric: ditch lobster, default to owl. Lobster stays available in picker,
just no longer the first-run choice. Cleared user must clear localStorage
key 'hermes-workspace-orchestrator-avatar' to see new default if they had
old default cached.
* feat(agent-view): add Hermes PNG avatar as default
- Add 'hermes' to AvatarStyle union with PNG renderer (HermesPNG)
- HermesPNG: <img src='/avatars/hermes.png'> with state animations
(breathing scale loop on idle/responding, dotted ring on thinking)
- Avatar renders circular via objectFit cover + border-radius 50%
- Default first-run avatar = hermes (was owl, was lobster originally)
- New PNG asset at public/avatars/hermes.png (1.6MB anime portrait)
- Picker modal now shows Hermes as first tile
Phase 1.5 of agent-view port. 8 more Greek god PNGs to land same way.
* feat(agent-view): bump main agent avatar 52 -> 88 for more presence
Per Eric: avatar felt small relative to panel width and whitespace.
Now occupies ~30% panel width which gives the character actual presence
in the top-of-rail slot.
* feat(agent-view): two-tier avatar picker with 9 Greek god PNGs
- Add 8 Greek god PNG avatars: athena, apollo, artemis, iris, nike, eros, pan, chronos
- Refactor PNG renderer into makeGreekPNG factory (one fn per god)
- AvatarPicker now has two tiers:
* Standard tier: 9 emoji-styled SVG avatars (smaller tiles, default view)
* Greek tier: 9 anime PNG portraits (premium, opens via 'More ->' button)
- Picker remembers tier based on currently selected avatar
- Greek tile shows actual PNG thumb at 56x56, emoji tile shows 2xl emoji
- 'More ->' / '<- Standard' toggle button switches tiers in-place
- Default avatar remains 'hermes' (god of messengers, fits the brand)
* fix(agent-view): wire chat-activity-store so avatar reflects real state
Previously chat-screen.tsx had a no-op stub for setLocalActivity, so
the OrchestratorAvatar in the agent view never actually changed state
during user sends, thinking, tool calls, or responses.
Hook the actual zustand store so:
- send -> 'reading'
- waiting for first token -> 'thinking'
- streaming -> 'responding'
- tool call running -> 'tool-use'
- done -> 'idle'
Now the avatar's animation states (breathing, dotted ring, bob, glow)
react to live chat activity instead of staying frozen on idle.
* fix(agent-view): cleaner enterprise chrome, drop noisy stats line
Per Eric: panel had visible outline border + chatty '1 active · 0 queued · $0.000' line that felt noisy and unenterprise.
- Drop the left border on the outer aside, switch bg to --theme-sidebar
for cohesion with the rest of the dark surface
- Drop the bottom border on the header row
- Drop the inline stats line entirely (info available per-card)
- Soften the Agents section bg from /35 to /15, drop its border ring
Result: panel reads as a continuous dark surface flush with the chrome,
not a bordered floating widget.
* feat(agent-view): smooth slide+fade in/out for desktop panel
Was: instant render with no entrance animation (initial={false})
Now: panel slides in from right edge with opacity fade, custom cubic
bezier eases (0.32, 0.72, 0.24, 1) for a natural deceleration that
feels native and enterprise rather than CSS-default.
Open: 320ms slide, 220ms opacity fade-in
Close: same easing, panel slides off-canvas right
* feat(agent-view): port usage meter — real provider quota tracking
Port from controlsuite:
- src/server/provider-usage.ts (1026 LOC) — Claude OAuth, Codex JWT
decode, OpenAI billing, OpenRouter, Anthropic API key probes
- src/routes/api/provider-usage.ts — JSON endpoint
- src/components/usage-meter/* (5 files) — full meter, compact meter,
details modal, context alert, index re-exports
AgentViewPanel.OrchestratorCard already had the poller wired to
/api/provider-usage from the prior agent-view port; route was just
missing. Now live:
- Codex: Plus plan, session/weekly windows, JWT-decoded plan tier
- Claude: OAuth credentials read from ~/.claude or keychain, refresh
on demand, session+weekly+sonnet bars
- OpenAI: billing endpoint via API key
- OpenRouter, Anthropic API key paths included
Verified at /api/provider-usage returns real percentages.
* fix(agent-view): unify enterprise chrome — drop borders on inner sections, queue, history, agent cards
Per Eric: bottom subagent sections still had the old border-primary-300/70
outline ring + heavier bg/35. Top header was already cleaned but the rest
of the panel didn't match.
- Drop border on Agents/Queue/History sections (3 spots), bg /35 -> /15
- Section header pills (Agents/Queue/History): drop border + shadow,
use uppercase tracking-wider primary-500 for hierarchy without lines
- Drop border on individual agent card containers, swap gradient bg
for flat /15 to match parent section
Result: panel reads as one continuous dark surface from header to
bottom, no nested ring-on-ring outlines. Hierarchy comes from
typography + bg shading not strokes.
* fix(agent-view): kill demo data fallback — show real state only
Demo agents (Roger/Sally/Bill) were appearing as if they were real
spawned workers because we wired a fallback during Phase 1 visual
port. Per Eric: 'I noticed there's a few agents running but we didn't
spawn'.
Now:
- Empty results -> empty panel (no fake agents)
- Gateway error -> empty panel + error state, not fake agents
- Only real /api/sessions data populates Active/Queue/History sections
createDemoActiveAgents / createDemoQueue / createDemoHistory functions
remain in code for future test fixtures, just not invoked.
* feat(agent-view): swap lobster for wolf in emoji avatar tier
Per Eric: ditch lobster, add wolf instead.
- New WolfSVG component matching the same animation API as Fox/Owl/etc
- Gray palette (gray-200/400/500), yellow eyes with vertical pupils
- Sharp triangular ears, longer muzzle, faint snarl on orchestrating state
- Speech dots on responding, dotted ring on thinking
- AvatarStyle union: 'lobster' -> 'wolf'
- Picker tile: '🐺 Wolf' replaces '🦞 Lobster'
- Renderer map: wolf -> WolfSVG
LobsterSVG component left in code (unused) so no behaviour delta if
storage has stale 'lobster' key (will fall back to default 'hermes').
* fix(themes): rename theme labels — drop 'Claude' prefix, use Nous/Hermes/Bronze
Per Eric: theme picker showed 'Claude Nous', 'Claude Official', 'Claude Classic'
which was odd branding for Hermes Workspace. Themes are referenced by id
('claude-nous' etc) for backward storage compat, but the user-facing label
now reads cleanly:
- Claude Nous -> Nous
- Claude Nous Light -> Nous Light
- Claude Official -> Hermes (was the flagship navy/indigo)
- Claude Official L -> Hermes Light
- Claude Classic -> Bronze (bronze accents description)
- Classic Light -> Bronze Light
- Slate -> Slate (no change)
- Slate Light -> Slate Light (no change)
Storage keys + ThemeId union unchanged; no migration needed.
* fix(theme): darken sidebar so it reads as solid against dashboard bg
Eric reported the chat sidebar 'looks transparent.' Root cause: in
`claude-official` the sidebar (#0d1220) and main bg (#0a0e1a) were
nearly identical lightness, so the divide between them faded out
entirely.
Bumped `--theme-sidebar` to #060914 \u2014 deeper than the bg \u2014 so the
sidebar column reads as its own clearly-defined surface without
needing extra borders or backdrop blur.
Only the active theme is touched here; other themes already have
sufficient contrast between sidebar and bg.
Tests/build:
- 12/12 aggregator tests
- pnpm build clean
---------
Co-authored-by: Aurora release bot <release@outsourc-e.com>
The sortSwarmMembers function in swarm2-screen.tsx filtered crew members
with regex /^swarm\d+$/i that only matched IDs like swarm1, swarm2, etc.
Agent IDs like rocky, scotty, huyang, and margaret were silently filtered
out, causing the Swarm and Operations pages to show "No swarm workers
discovered from crew status yet." despite the /api/crew-status endpoint
returning all agents correctly.
Fix: change filter to accept any non-empty ID instead of requiring
the /^swarm\d+$/i pattern.
The slash-command autocomplete in the chat composer was missing the
/plugins command, even though there's a real /api/plugins backend route
and the existing test suite (src/components/slash-command-menu.test.tsx)
already specs the expected behavior:
import { DEFAULT_SLASH_COMMANDS } from './slash-command-menu'
describe('DEFAULT_SLASH_COMMANDS', () => {
it('includes /plugins in the slash autocomplete list', () => { ... })
})
That spec was failing because:
1. The local list was named SLASH_COMMANDS (not DEFAULT_SLASH_COMMANDS)
and was not exported.
2. The /plugins entry was simply missing.
Fixes:
- Rename SLASH_COMMANDS -> DEFAULT_SLASH_COMMANDS and export it (so
the test and any downstream code can introspect the list).
- Add { command: '/plugins', description: 'List installed plugins and
their status' }.
- Export the SlashCommandDefinition / SlashCommandMenuProps /
SlashCommandMenuHandle types that chat-composer.tsx already imports
(the import was working only because TypeScript exposes types
structurally even when not explicitly exported, but making them
public types is cleaner).
Extends the existing test with three additional regression specs:
- The list contains every core command (/new, /clear, /model, /save,
/skills, /plugins, /skin, /help).
- Every entry has a non-empty description and starts with '/'.
- No command label is duplicated.
Verification:
pnpm vitest run src/components/slash-command-menu.test.tsx
✓ 4 tests passed
Full suite: 19 failed -> 18 failed (the slash-menu test now passes;
remaining failures are pre-existing and unrelated).
Two related bugs in src/lib/connection-errors.ts caused the wrong UI
message when the gateway refused a device's auth token (issue #239,
'Hermes Agent rejected the connection token').
1) getConnectionErrorMessage had unreachable code: the
'gateway_auth_rejected' case fell through to a duplicated
'clawsuite_auth_required' label, which is already handled above.
That meant gateway-auth rejection always returned the ClawSuite
'Claude Login Required / Enter your password' UI, which is wrong
when the actual problem is the gateway refusing the device token.
Fix: give 'gateway_auth_rejected' its own message that points the
user at re-pairing the device or checking the gateway token, not
at typing a password.
2) classifyConnectionError matched on lower.includes('token'), which
misrouted benign network errors like
'failed to fetch token from /api/foo' to gateway_auth_rejected.
Fix: require 'token' to co-occur with an auth-failure marker
(unauthorized / forbidden / rejected / invalid / expired / 401 /
403 / etc.) before classifying as auth-rejected. Bare 'token'
strings now fall through to the appropriate network/unknown bucket.
Adds full unit-test coverage for the classifier and the message
table, including regression tests for both bugs.
Fixes#239
The Usage quick-action tile in the search modal (Ctrl/Cmd+K) emits an
'OPEN_USAGE' window event. UsageMeter listens for that event but was
never rendered in the app root, so the event fired into the void.
Mount it next to SearchModal in __root.tsx. UsageMeter renders no UI
when closed (just the listener), so this has no visual side effect.
Fixes#258.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
PR #185 (commit 8b45b632) added python3 to the runtime Dockerfile to
fix issue #161 — terminal broken in Docker because scripts/pty-helper.py
requires Python at runtime.
Commit efcb7d14 (2026-05-01 'migrate legacy Hermes codename bytes to
canonical Claude') reverted the Dockerfile to a pre-#185 state without
python3, regressing #161 silently. Issue #259 reports the regression.
This is a one-line restore. Inline comment added so the next rename
sweep doesn't trip over it again.
Co-authored-by: Aurora release bot <release@outsourc-e.com>
* feat(dashboard): aggregate /api/dashboard/overview + Hermes-native cards
Workspace dashboard now mirrors what the native Hermes Agent dashboard at
:9120 surfaces, on top of the existing sessions analytics, in a single
server-aggregated round trip.
Adds new server endpoint `GET /api/dashboard/overview` that fans out to:
- /api/status (gateway state, active sessions, platforms)
- /api/cron/jobs (cron summary)
- /api/plugins/hermes-achievements/recent-unlocks (recent ribbon)
- /api/plugins/hermes-achievements/achievements (totals)
- /api/model/info (provider, model, context, capabilities)
- /api/analytics/usage (token totals, top models, optional cost)
Per-section graceful fallbacks: each slice independently resolves to
null on auth failure / missing endpoint / unreachable dashboard, and
the corresponding card hides itself. Vanilla installs without the
achievements plugin or analytics auth still get a usable dashboard.
Adds 5 new dashboard cards:
- SystemStatusStrip: one-line gateway + active-agents pill at top,
warning chip when restart_requested.
- PlatformsCard: connected platforms with per-platform state pills
(api_server, telegram, discord, etc.).
- CronSummaryCard: scheduled / paused / running counts + next-run
countdown, click-through to /jobs.
- AchievementsCard: 3 most recent unlocks with tier badges, plus a
modal that fetches a wider window (?achievements=12) for the full
ribbon view.
- AnalyticsSummaryCard: top-3 models by tokens with proportional bars,
total tokens over the window, real cost from the dashboard (replaces
the old hardcoded ~$5/M estimate).
Other tweaks:
- Replace the hardcoded cost subline on the Tokens MetricTile with the
real estimated_cost_usd value from /api/analytics/usage when present.
- New section row between the chart row and Recent Sessions for
Platforms / Analytics / Achievements.
Tests: +7 for the aggregator covering the empty / mixed / full payload
shapes plus the field-rename quirks (gateway_platforms vs platforms,
active_sessions vs active_agents). All 31 swarm/dashboard tests green.
* fix(dashboard): use existing hugeicons names (Award01Icon, CancelIcon)
* feat(dashboard): consolidate ops strip + native model card polish
Polish pass on PR #242 (Workspace dashboard parity phase 1) before
merge. Tightens layout per the dashboard spec's '10-second status
read' goal.
Layout changes:
- Drop the centered logo hero. New header is a single row with title,
Hermes Workspace label, and inline QuickActions.
- Collapse the three stacked status rows (SystemStatusStrip,
CronSummaryCard, PlatformsCard) into one OpsStrip that surfaces
gateway state, version, active agents, restart-pending, config
drift, platform pills, and cron pulse in a single horizontal bar.
- Re-flow main content as 8/4 split: Activity + Analytics on the left,
Model + Skills + Achievements as a side rail.
Data parity:
- Aggregator now exposes status.version, releaseDate, configVersion,
latestConfigVersion, hermesHome from /api/status. OpsStrip uses these
for the version chip and config drift warning.
- New ModelInfoCard reads overview.modelInfo (i.e. /api/model/info, the
active model the gateway is using) instead of /api/claude-config
defaults. Surfaces context length and tools/vision/reasoning chips.
UX:
- AnalyticsSummaryCard now renders a stable 'No usage in last Nd'
empty state instead of disappearing, so layout doesn't reflow on
fresh installs.
- Cron stale next-run (>7 days overdue) downgrades to muted 'stale'
label so March overdue jobs don't look alarming.
Cleanup:
- Remove orphaned SystemStatusStrip, CronSummaryCard, PlatformsCard
components. Drop legacy ModelCard + dead SystemGlance helper from
dashboard-screen.tsx (-179 lines net).
Tests/build:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (7/7)
- pnpm build (passes)
---------
Co-authored-by: Aurora release bot <release@outsourc-e.com>
* fix(api): replace broken 'authResult as unknown as Response' cast with proper 401
isAuthenticated() returns boolean. The previous pattern:
const authResult = isAuthenticated(request)
if (authResult !== true) return authResult as unknown as Response
silenced the TypeScript error but threw HTTPError -> 500 at runtime
because the framework received `false` instead of a Response. This
broke /api/connection-status entirely on protected setups (causing
ONBOARDING_KEY to never persist on fresh installs) and would have
broken the just-merged /api/system-metrics in the same way.
Replace with the canonical pattern used by every other API route:
if (!isAuthenticated(request)) {
return json({ error: 'Unauthorized' }, { status: 401 })
}
Refs #261 (which spotted the pattern in connection-status), #246
(which copied the broken pattern into system-metrics).
* fix(claude-proxy): fall back to /v1/models for /api/available-models on vanilla agent
Vanilla hermes-agent (any version through 2026-05) does not expose
`/api/available-models` \u2014 that endpoint is legacy fork-only. The chat
composer + settings dialog hit `/api/claude-proxy/api/available-models`
expecting it to work, get 404, and fall through to broken UI states
where the model picker is empty.
Fix: when proxying GET /api/available-models and the upstream returns
404, synthesize a compatible `{ models: [...] }` response from
/v1/models filtered by ?provider= so the picker keeps working.
Also: read the bearer token at request time using the same precedence
as the rest of the codebase (HERMES_API_TOKEN || CLAUDE_API_TOKEN ||
module-level BEARER_TOKEN). PR #234 fixed this in openai-compat-api.ts;
this catches the proxy path that was missed.
Refs #261.
---------
Co-authored-by: Aurora release bot <release@outsourc-e.com>
src/server/openai-compat-api.ts has two issues that combine to break
the chat surface for any deployment whose Hermes Agent gateway has
`API_SERVER_KEY` set (i.e. anything that isn't an open loopback
gateway):
1. The local `BEARER_TOKEN` const reads only `CLAUDE_API_TOKEN`,
ignoring the documented `HERMES_API_TOKEN` env var that the README
tells users to set. Looks like a leftover from the Claude → Hermes
rename: the const in src/server/gateway-capabilities.ts:222 honors
both names, but this local one was never updated.
2. The const is evaluated at module-load time. Under vite-node SSR
(`pnpm dev`), the module can be loaded in a worker context where
`process.env` doesn't yet contain the values that systemd /
EnvironmentFile / .env populated for the parent node process.
That freezes the constant to '' permanently, even though the env
is correctly populated by the time `openaiChat` actually runs.
Symptom: chat UI loads, sessions/skills/memory all work, but every
message produces a run with `status: error` and:
errorMessage: "OpenAI-compatible chat: 401 {\"error\": {...,
\"code\": \"invalid_api_key\"}}"
…in `<HERMES_HOME>/webui-mvp/runs/<session>/<run>.json`. The error
format matches what the gateway returns for missing Authorization, not
an upstream provider error. Confirmed via instrumentation:
[DEBUG-AUTH] BEARER_TOKEN length: 0
env.HERMES_API_TOKEN length: 16
Fix: replace the const with a small `getBearerToken()` helper that
reads the env at call time and honors HERMES_API_TOKEN with a fallback
to CLAUDE_API_TOKEN. Three call sites updated (`getDefaultModel`,
`openaiChat` Authorization header, and the session-id guard).
No behavior change for setups that already worked (open loopback
gateway with no API_SERVER_KEY, or production builds where
process.env is fully populated before module load).
* fix: load kanban task assignees from Hermes profiles
* docs(swarm): remove dead reference to non-existent aurora-rotate-worker.sh
Fixes#255. Option A referenced a personal helper script that has never
existed in the repo. The Add Swarm dialog (former Option B) remains as
the single supported path for spawning tmux-backed workers.
---------
Co-authored-by: dontcallmejames <dontcallmejames@users.noreply.github.com>
TanStack Start's catch-all '$.tsx' route SSRs index.html for any missing
static file, returning 200 OK with content-type: text/html. The naive
r.ok check marked all GLB URLs 'present', then useGLTF tried to parse
HTML as a binary glTF and threw inside Suspense, killing the entire
3D world and forcing the lite fallback to render — exactly what Eric saw
('Athena · Agent' overlay text instead of the 3D scene).
Fix:
- Probe checks content-type explicitly and rejects text/html.
- Wrap GlbInner in a class-based GlbErrorBoundary so even a wrong probe
no longer crashes the world: errored GLBs cache as 'missing' and the
voxel body shows.
Verified on dev server: HEAD /avatars-3d/athena.glb returns 200 +
text/html, now correctly classified as missing.
- Add PlaygroundNpcGlb component using @react-three/drei useGLTF.
Loads /avatars-3d/<npcId>.glb on demand, with materials frozen and
raycasting disabled (parent group handles clicks).
- HEAD-probe per id with module-level cache so a missing GLB never
triggers GLTF parse errors. Voxel body is the seamless fallback.
- Wire useGlbAvailable() into the NPC component: when GLB present,
render it instead of the voxel torso/limbs/head; nameplate chip stays.
- public/avatars-3d/README.md with Meshy.ai prompts per Greek god.
Drop a GLB at public/avatars-3d/athena.glb and Athena upgrades. No code
change needed. Mix-and-match is supported (only some NPCs upgraded).
Client (use-playground-multiplayer.ts):
- Drop presence to 5 Hz (200ms, was 100ms). Halves bandwidth, identical look.
- Skip-send when player static (POS_EPSILON 0.04, YAW_EPSILON ~1.4°).
- Avatar config sent only on signature change, not every tick.
- Position-delta gate before re-render (RENDER_POS_EPSILON 0.03).
- World-scoped local rendering (visibleRemotes) — never see remotes from
other worlds.
- Connection state ('offline' | 'broadcast' | 'ws' | 'both') exposed for HUD.
- Server-pushed count consumed via 'count' wire kind (no /stats polling).
Worker (playground-ws-worker/src/worker.ts) v1:
- World-scoped fan-out: only broadcast to recipients in same world.
- Push 'count' messages on every join/leave (zero poll cost on clients).
- Per-socket token bucket rate limit: 30 msgs/sec.
- 50ms presence dedupe per player (drops floods).
- Stale prune at 5s (matches client).
- Send live count baseline on connect handshake.
HUD (playground-online-chip.tsx):
- Consume server-pushed 'hermes-playground-count' CustomEvent.
- /stats fetch only as 3s fallback if no push arrives.
- Connection-state dot: green (live), yellow (local-only), red (offline).
- Tooltip shows peak + per-world breakdown.
- Pulsing animation when fully connected.
World-3d:
- Coalesced position poll to 200ms (matches presence cadence).
- Surface transport + serverCount on window for HUD chip.
Verified: pnpm build clean, worker deploy clean, WS handshake + count
push + byWorld breakdown all working against the live worker.
- Replace floating PNG portraits with portrait-chip nameplates above heads
(NPCs, player, bots, remote players). Voxel body becomes the character;
PNG identifies them at a glance without the chimera-y face hover.
- Add Cloudflare Workers + Durable Objects port of scripts/playground-ws.mjs
in playground-ws-worker/ — same wire protocol so client connects unchanged.
Includes /stats endpoint with online + byWorld + peakToday.
- Add PlaygroundOnlineChip HUD component that polls /stats and renders a
live 'N agents online' badge. Hidden when VITE_PLAYGROUND_STATS_URL unset.
- Drop unused Billboard / useTexture imports from world-3d.
Build: pnpm build clean. No new deps. Worker deps install separately.