jenkins b66c762f5d hermes(webui): add HUX card UI models with contract-locked suites
Standalone per-card browser model/security/view modules for HUX-01..10
plus node+pytest suites that read the hux.v1 contract schemas directly.
Reconciled drift found on integration: the activity model now accepts
all 32 hux.event.v1 kinds (delegation.*, memory.suppressed,
memory.retrieval_removed, budget.exhausted, side_effect.*), the
autonomy model carries the external_side_effect capability, and the
foundation boundary test now asserts the shipped static HUX surface
exists on disk and that images never bake activated HUX_FLAGS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 04:12:03 -03:00

261 lines
7.5 KiB
TypeScript

/** Compact, accessible HUX-01 activity timeline. The feature is default-off. */
import {
Activity,
AlertTriangle,
Check,
GitBranch,
ListChecks,
LoaderCircle,
} from "lucide-react";
import { useMemo, useState } from "react";
import {
activityTimelineEnabled,
mergeActivityEvents,
normalizeCancellationReceipt,
surfacePolicy,
} from "./model.ts";
import type {
ActivityItem,
CancellationReceipt,
HuxSurface,
RawActivityEvent,
RawCancellationReceipt,
} from "./types.ts";
interface ActivityTimelineProps {
conversationId: string;
events: readonly RawActivityEvent[];
flags?: Iterable<string>;
surface: HuxSurface;
status?: "connecting" | "live" | "reconnecting" | "complete";
cancellationReceipts?: readonly RawCancellationReceipt[];
onRetry?: () => void;
}
const PHASE_LABELS: Readonly<Record<ActivityItem["phase"], string>> = {
intent: "Intent",
action: "Action",
evidence: "Evidence",
delegation: "Delegation",
decision: "Decision",
completion: "Complete",
failure: "Failed",
};
function PhaseIcon({ item }: { item: ActivityItem }) {
if (item.phase === "failure")
return <AlertTriangle className="h-3.5 w-3.5" aria-hidden />;
if (item.phase === "completion")
return <Check className="h-3.5 w-3.5" aria-hidden />;
if (item.phase === "delegation")
return <GitBranch className="h-3.5 w-3.5" aria-hidden />;
if (item.phase === "decision")
return <ListChecks className="h-3.5 w-3.5" aria-hidden />;
return <Activity className="h-3.5 w-3.5" aria-hidden />;
}
function TimelineRow({
item,
expanded,
}: {
item: ActivityItem;
expanded: boolean;
}) {
return (
<li
className={`hux-activity-row hux-tone-${item.tone}`}
data-kind={item.kind}
data-seq={item.seq}
>
<div className="hux-activity-marker">
<PhaseIcon item={item} />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<span className="text-[0.6875rem] font-semibold uppercase tracking-[0.12em] text-current/65">
{PHASE_LABELS[item.phase]}
</span>
<time
className="text-[0.625rem] text-current/45"
dateTime={item.timestamp}
>
{new Date(item.timestamp).toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
})}
</time>
</div>
<p className="mt-0.5 text-xs leading-relaxed text-current/90">
{item.summary}
</p>
{expanded && item.evidence.length > 0 && (
<ul
className="mt-1.5 flex flex-wrap gap-1"
aria-label="Bounded evidence references"
>
{item.evidence.map((evidence, index) => (
<li
key={`${evidence.kind}-${index}`}
className="hux-evidence-chip"
>
{evidence.label}
</li>
))}
</ul>
)}
</div>
</li>
);
}
function CancellationCard({ receipt }: { receipt: CancellationReceipt }) {
const complete = receipt.outcome !== "failed_to_cancel";
return (
<aside
className="hux-cancellation-receipt"
aria-label="Cancellation receipt"
>
<div className="flex items-center gap-2 text-xs font-medium">
{complete ? (
<Check className="h-3.5 w-3.5" aria-hidden />
) : (
<AlertTriangle className="h-3.5 w-3.5" aria-hidden />
)}
<span>
{complete
? "Stop request handled"
: "Hermes could not confirm the stop"}
</span>
</div>
{receipt.sideEffects.length > 0 && (
<ul className="mt-1 list-inside list-disc text-[0.6875rem] text-current/70">
{receipt.sideEffects.map((effect, index) => (
<li key={index}>
{effect.description} {" "}
{effect.reverted ? "reverted" : "left in place"}
</li>
))}
</ul>
)}
{receipt.omittedEffects > 0 && (
<p className="mt-1 text-[0.625rem] text-current/55">
{receipt.omittedEffects} additional side effect
{receipt.omittedEffects === 1 ? "" : "s"} recorded.
</p>
)}
</aside>
);
}
/** Render nothing unless both HUX-11 and HUX-01 have been explicitly enabled. */
export function ActivityTimeline({
conversationId,
events,
flags,
surface,
status = "live",
cancellationReceipts = [],
onRetry,
}: ActivityTimelineProps) {
const [showAll, setShowAll] = useState(false);
const enabled = activityTimelineEnabled(flags);
const policy = surfacePolicy(surface);
const result = useMemo(
() => mergeActivityEvents([], events, conversationId, surface),
[conversationId, events, surface],
);
const receipts = useMemo(
() =>
cancellationReceipts
.map((receipt) => normalizeCancellationReceipt(receipt, surface))
.filter((receipt): receipt is CancellationReceipt => receipt !== null)
.slice(-3),
[cancellationReceipts, surface],
);
if (!enabled) return null;
const hidden = Math.max(0, result.items.length - policy.initialItems);
const visible = showAll
? result.items
: result.items.slice(-policy.initialItems);
const reconnecting = status === "connecting" || status === "reconnecting";
return (
<section
className={`hux-activity hux-activity-${surface}`}
aria-label="Hermes activity timeline"
>
<header className="flex items-center justify-between gap-3">
<div>
<h2 className="text-sm font-semibold">What Hermes is doing</h2>
<p className="text-[0.6875rem] text-current/55">
{reconnecting
? "Reconnecting to durable activity…"
: `${result.items.length} recorded update${result.items.length === 1 ? "" : "s"}`}
</p>
</div>
{reconnecting && (
<LoaderCircle className="h-4 w-4 animate-spin" aria-hidden />
)}
</header>
{(result.conflicts > 0 || result.invalid > 0) && (
<div className="hux-activity-warning" role="status">
Some unsafe or conflicting activity was ignored. Visible history was
not rewritten.
</div>
)}
<ol
className="mt-3 space-y-1.5"
role="log"
aria-live="polite"
aria-relevant="additions"
aria-busy={reconnecting}
>
{visible.map((item) => (
<TimelineRow
key={item.id}
item={item}
expanded={policy.expandRoutineEvents || !item.routine}
/>
))}
</ol>
{result.items.length === 0 && (
<p className="mt-3 text-xs text-current/55">
No activity has been recorded yet.
</p>
)}
{hidden > 0 && !showAll && (
<button
className="hux-activity-more"
type="button"
onClick={() => setShowAll(true)}
>
Show {hidden} earlier update{hidden === 1 ? "" : "s"}
</button>
)}
{showAll && hidden > 0 && (
<button
className="hux-activity-more"
type="button"
onClick={() => setShowAll(false)}
>
Show recent activity only
</button>
)}
{reconnecting && onRetry && (
<button className="hux-activity-more" type="button" onClick={onRetry}>
Retry now
</button>
)}
{receipts.map((receipt) => (
<CancellationCard key={receipt.id} receipt={receipt} />
))}
</section>
);
}