{who}
+ {card.grant.principal.kind === "ai_agent" ? (
+
+ {card.grant.principal.declarationStatus === "unregistered"
+ ? "Unregistered agent"
+ : "AI agent"}
+
+ ) : null}
{card.irreversible ? (
Permanent
@@ -103,6 +120,12 @@ export function ApprovalCardView({
{" on "}
{card.grant.resource.displayName}
+ {card.grant.principal.kind === "ai_agent" ? (
+
+ Runtime: {card.grant.principal.runtime ?? "unknown"} · Reachable:{" "}
+ {card.grant.principal.reachableTools?.join(", ") || "not reported"}
+
+ ) : null}
diff --git a/apps/web/src/components/ApprovalQueue.tsx b/apps/web/src/components/ApprovalQueue.tsx
index 7193fd4..c9c230e 100644
--- a/apps/web/src/components/ApprovalQueue.tsx
+++ b/apps/web/src/components/ApprovalQueue.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { postDecision } from "../api/client.js";
import type { ApiCard } from "../api/types.js";
@@ -7,6 +7,28 @@ import { ApprovalCardView } from "./ApprovalCardView.js";
import { ExecutePanel } from "./ExecutePanel.js";
import { HoldDialog } from "./HoldDialog.js";
+interface RingRect {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+}
+
+interface TravellingRing {
+ key: string;
+ rect: RingRect;
+ travelling: boolean;
+}
+
+function toRingRect(rect: DOMRect): RingRect {
+ return {
+ top: rect.top,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ };
+}
+
export function ApprovalQueue({
cards,
systemIds,
@@ -33,6 +55,8 @@ export function ApprovalQueue({
const [holdTarget, setHoldTarget] = useState
(null);
const [executeOpen, setExecuteOpen] = useState(false);
const [busy, setBusy] = useState(false);
+ const [travellingRing, setTravellingRing] = useState(null);
+ const previousGuidedCardId = useRef(null);
const focusable = ordered.filter((c) => c.status === "pending");
const focused = focusable[Math.min(focusIndex, Math.max(0, focusable.length - 1))];
@@ -42,11 +66,44 @@ export function ApprovalQueue({
}, [scanId]);
useEffect(() => {
- if (!guidedCardId) return;
- document.getElementById(`approval-card-${guidedCardId}`)?.scrollIntoView({
- behavior: "smooth",
- block: "center",
+ if (!guidedCardId) {
+ previousGuidedCardId.current = null;
+ setTravellingRing(null);
+ return;
+ }
+ const target = document.getElementById(`approval-card-${guidedCardId}`);
+ if (!target) return;
+ const reducedMotion =
+ typeof window === "undefined" ||
+ !window.matchMedia ||
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "center" });
+
+ const targetRect = toRingRect(target.getBoundingClientRect());
+ const previous = previousGuidedCardId.current
+ ? document.getElementById(`approval-card-${previousGuidedCardId.current}`)
+ : null;
+ const startRect = previous ? toRingRect(previous.getBoundingClientRect()) : targetRect;
+ previousGuidedCardId.current = guidedCardId;
+
+ if (reducedMotion) {
+ setTravellingRing(null);
+ return;
+ }
+ const key = `${guidedCardId}-${Date.now()}`;
+ setTravellingRing({ key, rect: startRect, travelling: false });
+ const animationFrame = requestAnimationFrame(() => {
+ setTravellingRing((current) =>
+ current?.key === key ? { key, rect: targetRect, travelling: true } : current,
+ );
});
+ const cleanup = window.setTimeout(() => {
+ setTravellingRing((current) => (current?.key === key ? null : current));
+ }, 360);
+ return () => {
+ cancelAnimationFrame(animationFrame);
+ window.clearTimeout(cleanup);
+ };
}, [guidedCardId]);
useEffect(() => {
@@ -139,6 +196,22 @@ export function ApprovalQueue({
return (
+ {travellingRing ? (
+
+ ) : null}
diff --git a/apps/web/src/components/GuidedDemoStatusStrip.tsx b/apps/web/src/components/GuidedDemoStatusStrip.tsx
new file mode 100644
index 0000000..ac29fda
--- /dev/null
+++ b/apps/web/src/components/GuidedDemoStatusStrip.tsx
@@ -0,0 +1,65 @@
+import type { GuidedDemoPhase, GuidedDemoState } from "../hooks/useGuidedDemo.js";
+
+export function GuidedDemoStatusStrip({
+ state,
+ systemCount,
+}: {
+ state: GuidedDemoState;
+ systemCount: number;
+}) {
+ if (state.phase === "idle") return null;
+
+ const waiting = state.phase === "waiting";
+ return (
+