Closes Phase 2. Every entity in `web/src/lib/hermes` now carries an
`instanceId: 'vijay' | 'bheem'` (with `'all'` allowed for cross-cutting
agents like Hermes Core / GitHub link), and a global instance switcher
above every Mission Control pane filters them.
Library changes (`web/src/lib/hermes.ts`):
- New `HermesInstanceId` / `HermesInstanceFilter` types + `HERMES_INSTANCES`
metadata array.
- `instanceId` added to `HermesProduct`, `HermesTask`, `HermesEvent`,
`HermesRun`, `HermesAgentStatus`. Seed data deterministically split
~50/50 across instances; agents tagged per-scope (Local VM runner →
bheem, CLI runner / Scheduler → vijay, Hermes Core / GitHub /
OpenClaw / deployment / notifications → all).
- `getHermesTasks({instance})`, `getHermesProducts(view, instance)`,
`getHermesAgents(instance)`, `getHermesHistory(instance)`,
`getHermesOverview(instance)` all accept the filter; helper
`instanceMatches(scope, filter)` keeps the semantics consistent
(always-match for `'all'` on either side).
UI changes:
- New `HermesInstanceProvider` (React context, localStorage-backed
under `hermes.instanceFilter.v1`, SSR-safe default to avoid
hydration mismatch) mounted in `app/hermes/layout.tsx`.
- New `HermesInstanceSwitcher` segmented control (radiogroup with
aria-checked) rendered in the layout header above every pane.
- New `HermesInstanceBadge` shown on task rows (Active Missions +
Task Ledger), product cards (overview minicards + portfolio
cards), and agent cards.
- `/hermes` overview gains a "Per-instance roll-up" section that
always shows Vijay vs Bheem side-by-side regardless of the active
filter — that's the always-cross-instance comparison view, while
the eight metric cards above it are filtered by the switcher.
Tests:
- 2 new unit tests in `lib/hermes.test.ts` (instance tagging on seed
data + filter semantics across tasks/products/agents/overview).
- 1 new E2E test asserting the switcher's radiogroup, default
selection, and persistence-friendly state change.
- All green: 13/13 web unit tests, 7/7 E2E.
`web/test-results/` and `web/playwright-report/` added to `.gitignore`
since they're regenerated per run.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
'use client';
|
|
|
|
import { Badge } from '@/components/ui/Primitives';
|
|
import { cn } from '@/lib/utils';
|
|
import { HERMES_INSTANCES, type HermesInstanceFilter, type HermesInstanceId } from '@/lib/hermes';
|
|
import { useHermesInstance } from '@/lib/hermes-instance-context';
|
|
|
|
const OPTIONS: Array<{ id: HermesInstanceFilter; label: string }> = [
|
|
{ id: 'all', label: 'All' },
|
|
...HERMES_INSTANCES.map((inst) => ({ id: inst.id, label: inst.label })),
|
|
];
|
|
|
|
interface HermesInstanceSwitcherProps {
|
|
className?: string;
|
|
}
|
|
|
|
// Segmented control mounted in the Hermes layout header. Persists its
|
|
// selection via `useHermesInstance` (localStorage-backed); every Mission
|
|
// Control pane reads from the same hook.
|
|
export function HermesInstanceSwitcher({ className }: HermesInstanceSwitcherProps) {
|
|
const { selectedInstance, setSelectedInstance } = useHermesInstance();
|
|
|
|
return (
|
|
<div
|
|
role="radiogroup"
|
|
aria-label="Hermes instance filter"
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-1 text-sm shadow-[var(--bl-shadow-sm)]',
|
|
className,
|
|
)}
|
|
>
|
|
{OPTIONS.map((option) => {
|
|
const active = option.id === selectedInstance;
|
|
return (
|
|
<button
|
|
key={option.id}
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={active}
|
|
onClick={() => setSelectedInstance(option.id)}
|
|
className={cn(
|
|
'rounded-full px-3 py-1 transition-colors',
|
|
active
|
|
? 'bg-[var(--bl-accent)] text-[var(--bl-text-on-accent,white)]'
|
|
: 'text-[var(--bl-text-secondary)] hover:bg-[var(--bl-surface-muted)]',
|
|
)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Small per-row badge so table cells can show which instance owns the entity.
|
|
// Use `'all'` for entities scoped to both instances (e.g. cross-cutting agents
|
|
// like Hermes Core, GitHub link).
|
|
const INSTANCE_LABEL: Record<HermesInstanceId | 'all', string> = {
|
|
vijay: 'Vijay',
|
|
bheem: 'Bheem',
|
|
all: 'Both',
|
|
};
|
|
|
|
export function HermesInstanceBadge({ instanceId }: { instanceId: HermesInstanceId | 'all' }) {
|
|
return <Badge variant="info">{INSTANCE_LABEL[instanceId]}</Badge>;
|
|
}
|