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
261 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|