mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
topology make activity states visible
This commit is contained in:
1 parent
b4b93f4d69
commit
714932c491
6 files changed
+176
-9
No files matched your search
@@ -3,6 +3,7 @@ import { Handle, Position } from "@xyflow/react";
|
||||
import { PanelsTopLeft } from "lucide-react";
|
||||
import { copyText } from "../lib/copy-text.js";
|
||||
import { displayAgentName } from "../lib/display-name.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import {
|
||||
getActivityState,
|
||||
getActivityLabel,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
import type { AgentActivitySummary, CurrentQitemSummary } from "../hooks/useNodeInventory.js";
|
||||
import { ContextUsageRing } from "./ContextUsageRing.js";
|
||||
import { ActivityRing } from "./topology/ActivityRing.js";
|
||||
import { getActivityCardClasses, getActivityCardSignal } from "./topology/activity-card-visuals.js";
|
||||
import type { TopologyActivityVisual } from "../lib/topology-activity.js";
|
||||
|
||||
interface RigNodeData {
|
||||
@@ -93,6 +95,7 @@ export function RigNode({ data }: { data: RigNodeData }) {
|
||||
const activityBgClass = getActivityBgClass(activityState);
|
||||
const activityAnimClass = getActivityAnimationClass(activityState);
|
||||
const activityIsStale = isActivityStale(data.agentActivity);
|
||||
const activityCard = getActivityCardSignal({ activityRing: data.activityRing, activityState });
|
||||
|
||||
const placementChipLabel = data.placementState === "selected" ? "target" : data.placementState === "available" ? "avail" : null;
|
||||
|
||||
@@ -167,13 +170,21 @@ export function RigNode({ data }: { data: RigNodeData }) {
|
||||
|
||||
const card = (
|
||||
<div
|
||||
className={`group bg-white border min-w-[200px] hard-shadow relative ${
|
||||
className={cn(
|
||||
"group relative min-w-[200px] border hard-shadow transition-[background-color,border-color,box-shadow] duration-300",
|
||||
getActivityCardClasses({
|
||||
state: activityCard.state,
|
||||
flash: activityCard.flash,
|
||||
reducedMotion: data.reducedMotion,
|
||||
}),
|
||||
data.placementState === "selected"
|
||||
? "border-emerald-600 ring-2 ring-emerald-400/70 shadow-[0_0_0_3px_rgba(52,211,153,0.12)]"
|
||||
: data.placementState === "available"
|
||||
? "border-emerald-500 ring-1 ring-emerald-300/70"
|
||||
: "border-stone-900"
|
||||
}`}
|
||||
: "border-stone-900",
|
||||
)}
|
||||
data-activity-card-state={activityCard.state}
|
||||
data-activity-card-flash={activityCard.flash ?? "none"}
|
||||
data-testid="rig-node"
|
||||
title={hoverHint || undefined}
|
||||
>
|
||||
|
||||
@@ -50,7 +50,10 @@ export function HotPotatoEdge({
|
||||
data-reduced-motion="true"
|
||||
cx={targetX}
|
||||
cy={targetY}
|
||||
r={crossRig ? 2.5 : 3.25}
|
||||
r={crossRig ? 5 : 6.5}
|
||||
stroke="rgba(255,255,255,0.88)"
|
||||
strokeWidth={crossRig ? 1.75 : 2.25}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
className={crossRig ? "fill-stone-500" : "fill-emerald-600"}
|
||||
/>
|
||||
) : (
|
||||
@@ -58,7 +61,10 @@ export function HotPotatoEdge({
|
||||
key={packet.id}
|
||||
data-testid={`hot-potato-packet-${packet.id}`}
|
||||
data-reduced-motion="false"
|
||||
r={crossRig ? 2.25 : 3}
|
||||
r={crossRig ? 5 : 6.5}
|
||||
stroke="rgba(255,255,255,0.88)"
|
||||
strokeWidth={crossRig ? 1.75 : 2.25}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
className={crossRig ? "fill-stone-500 hot-potato-packet-cross" : "fill-emerald-600 hot-potato-packet"}
|
||||
>
|
||||
<animateMotion
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { AgentActivitySummary } from "../../hooks/useNodeInventory.js";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
import { useCmuxLaunch } from "../../hooks/useCmuxLaunch.js";
|
||||
import { ActivityRing } from "./ActivityRing.js";
|
||||
import { getActivityCardClasses, getActivityCardSignal } from "./activity-card-visuals.js";
|
||||
import type { TopologyActivityVisual } from "../../lib/topology-activity.js";
|
||||
|
||||
interface HybridPodGroupNodeData {
|
||||
@@ -89,6 +90,7 @@ export function HybridAgentNode({ data }: { data: HybridAgentNodeData }) {
|
||||
const activityBgClass = getActivityBgClass(activityState);
|
||||
const activityAnimClass = getActivityAnimationClass(activityState);
|
||||
const activityStale = isActivityStale(data.agentActivity);
|
||||
const activityCard = getActivityCardSignal({ activityRing: data.activityRing, activityState });
|
||||
const runtimeModel = [data.runtime, data.model].filter(Boolean).join(" / ");
|
||||
const contextKnown = data.contextAvailability === "known" && typeof data.contextUsedPercentage === "number";
|
||||
|
||||
@@ -100,8 +102,15 @@ export function HybridAgentNode({ data }: { data: HybridAgentNodeData }) {
|
||||
`activity: ${activityLabel}${activityStale ? " (stale)" : ""}`,
|
||||
runtimeModel || null,
|
||||
].filter(Boolean).join("\n")}
|
||||
data-activity-card-state={activityCard.state}
|
||||
data-activity-card-flash={activityCard.flash ?? "none"}
|
||||
className={cn(
|
||||
"group relative h-full w-full select-none border bg-white hard-shadow",
|
||||
"group relative h-full w-full select-none border hard-shadow transition-[background-color,border-color,box-shadow] duration-300",
|
||||
getActivityCardClasses({
|
||||
state: activityCard.state,
|
||||
flash: activityCard.flash,
|
||||
reducedMotion: data.reducedMotion,
|
||||
}),
|
||||
data.startupStatus === "failed"
|
||||
? "border-red-700"
|
||||
: data.startupStatus === "attention_required"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ActivityState } from "../../lib/activity-visuals.js";
|
||||
import type { ActivityFlash, ActivityRingState, TopologyActivityVisual } from "../../lib/topology-activity.js";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export function fallbackActivityCardState(activityState: ActivityState): ActivityRingState {
|
||||
if (activityState === "running") return "active";
|
||||
if (activityState === "needs_input") return "needs_input";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
export function getActivityCardSignal(input: {
|
||||
activityRing?: TopologyActivityVisual | null;
|
||||
activityState: ActivityState;
|
||||
}): { state: ActivityRingState; flash: ActivityFlash } {
|
||||
return {
|
||||
state: input.activityRing?.state ?? fallbackActivityCardState(input.activityState),
|
||||
flash: input.activityRing?.flash ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getActivityCardClasses(input: {
|
||||
state: ActivityRingState;
|
||||
flash?: ActivityFlash;
|
||||
reducedMotion?: boolean;
|
||||
}): string {
|
||||
return cn(
|
||||
"activity-card-surface",
|
||||
input.state === "active" && "activity-card-active",
|
||||
input.state === "needs_input" && "activity-card-needs-input",
|
||||
input.state === "blocked" && "activity-card-blocked",
|
||||
input.flash === "source" && "activity-card-source-flash",
|
||||
input.flash === "target" && "activity-card-target-flash",
|
||||
input.reducedMotion && "activity-card-reduced-motion",
|
||||
);
|
||||
}
|
||||
@@ -315,6 +315,84 @@
|
||||
0% { opacity: 1; box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.42); }
|
||||
100% { opacity: 0.82; box-shadow: 0 0 0 5px rgba(16, 185, 129, 0.0); }
|
||||
}
|
||||
@keyframes activity-card-active-frames {
|
||||
0%, 100% {
|
||||
background-color: rgba(220, 252, 231, 0.82);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(16, 185, 129, 0.16);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(187, 247, 208, 0.96);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(16, 185, 129, 0.30);
|
||||
}
|
||||
}
|
||||
@keyframes activity-card-needs-input-frames {
|
||||
0%, 100% {
|
||||
background-color: rgba(254, 243, 199, 0.88);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(245, 158, 11, 0.22);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(253, 230, 138, 0.98);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(245, 158, 11, 0.38);
|
||||
}
|
||||
}
|
||||
@keyframes activity-card-blocked-frames {
|
||||
0%, 100% {
|
||||
background-color: rgba(254, 226, 226, 0.88);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(220, 38, 38, 0.26);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(254, 202, 202, 0.96);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(220, 38, 38, 0.38);
|
||||
}
|
||||
}
|
||||
@keyframes activity-card-source-flash-frames {
|
||||
0% {
|
||||
background-color: rgba(167, 243, 208, 0.98);
|
||||
box-shadow: 2px 2px 0px #2e342e, 0 0 0 5px rgba(16, 185, 129, 0.34), 0 0 24px rgba(16, 185, 129, 0.30);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 2px 2px 0px #2e342e, 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
@keyframes activity-card-target-flash-frames {
|
||||
0% {
|
||||
background-color: rgba(110, 231, 183, 1);
|
||||
box-shadow: 2px 2px 0px #2e342e, 0 0 0 7px rgba(16, 185, 129, 0.42), 0 0 32px rgba(16, 185, 129, 0.38);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 2px 2px 0px #2e342e, 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
.activity-card-surface {
|
||||
background-color: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
.activity-card-active {
|
||||
background-color: rgba(220, 252, 231, 0.88);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(16, 185, 129, 0.18);
|
||||
}
|
||||
.activity-card-needs-input {
|
||||
background-color: rgba(254, 243, 199, 0.92);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(245, 158, 11, 0.24);
|
||||
}
|
||||
.activity-card-blocked {
|
||||
background-color: rgba(254, 226, 226, 0.92);
|
||||
box-shadow: 2px 2px 0px #2e342e, inset 0 0 0 1px rgba(220, 38, 38, 0.28);
|
||||
}
|
||||
.activity-card-active:not(.activity-card-reduced-motion) {
|
||||
animation: activity-card-active-frames 2.4s ease-in-out infinite;
|
||||
}
|
||||
.activity-card-needs-input:not(.activity-card-reduced-motion) {
|
||||
animation: activity-card-needs-input-frames 2.1s ease-in-out infinite;
|
||||
}
|
||||
.activity-card-blocked:not(.activity-card-reduced-motion) {
|
||||
animation: activity-card-blocked-frames 3.4s ease-in-out infinite;
|
||||
}
|
||||
.activity-card-source-flash:not(.activity-card-reduced-motion) {
|
||||
animation: activity-card-source-flash-frames 760ms ease-out 1;
|
||||
}
|
||||
.activity-card-target-flash:not(.activity-card-reduced-motion) {
|
||||
animation: activity-card-target-flash-frames 900ms ease-out 1;
|
||||
}
|
||||
.activity-ring-active {
|
||||
animation: activity-ring-active-frames 2.2s ease-in-out infinite;
|
||||
}
|
||||
@@ -331,15 +409,18 @@
|
||||
|
||||
.hot-potato-edge-active .react-flow__edge-path,
|
||||
.react-flow__edge.hot-potato-edge-active .react-flow__edge-path {
|
||||
filter: drop-shadow(0 0 6px rgba(16, 185, 129, 0.42)) !important;
|
||||
stroke-width: 4px !important;
|
||||
transition: stroke-width 180ms ease-out, filter 180ms ease-out;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.hot-potato-packet,
|
||||
.hot-potato-packet-cross {
|
||||
filter: drop-shadow(0 0 3px rgba(16, 185, 129, 0.32));
|
||||
filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.52));
|
||||
}
|
||||
.hot-potato-packet-cross {
|
||||
opacity: 0.72;
|
||||
filter: drop-shadow(0 0 2px rgba(120, 113, 108, 0.22));
|
||||
opacity: 0.86;
|
||||
filter: drop-shadow(0 0 6px rgba(120, 113, 108, 0.36));
|
||||
}
|
||||
|
||||
@keyframes rig-activity-frame-frames {
|
||||
@@ -368,6 +449,11 @@
|
||||
.activity-ring-blocked,
|
||||
.activity-ring-source-flash,
|
||||
.activity-ring-target-flash,
|
||||
.activity-card-active,
|
||||
.activity-card-needs-input,
|
||||
.activity-card-blocked,
|
||||
.activity-card-source-flash,
|
||||
.activity-card-target-flash,
|
||||
.rig-activity-frame-pulse {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { ActivityRing } from "../src/components/topology/ActivityRing.js";
|
||||
import { HotPotatoEdge } from "../src/components/topology/HotPotatoEdge.js";
|
||||
import { fallbackActivityCardState, getActivityCardClasses } from "../src/components/topology/activity-card-visuals.js";
|
||||
import {
|
||||
HOT_POTATO_WITHIN_RIG_DURATION_MS,
|
||||
type HotPotatoPacket,
|
||||
@@ -95,11 +96,23 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
|
||||
expect(screen.getByTestId("hot-potato-packet-packet-1").getAttribute("data-reduced-motion")).toBe("true");
|
||||
});
|
||||
|
||||
it("whole-card activity classes make active and handoff states visible", () => {
|
||||
expect(fallbackActivityCardState("running")).toBe("active");
|
||||
expect(fallbackActivityCardState("idle")).toBe("idle");
|
||||
expect(getActivityCardClasses({ state: "active" })).toContain("activity-card-active");
|
||||
expect(getActivityCardClasses({ state: "needs_input" })).toContain("activity-card-needs-input");
|
||||
expect(getActivityCardClasses({ state: "blocked" })).toContain("activity-card-blocked");
|
||||
expect(getActivityCardClasses({ state: "active", flash: "target" })).toContain("activity-card-target-flash");
|
||||
expect(getActivityCardClasses({ state: "active", flash: "source", reducedMotion: true })).toContain("activity-card-reduced-motion");
|
||||
});
|
||||
|
||||
it("source scan keeps activity and packet layers production-reachable", () => {
|
||||
const srcRoot = path.resolve(__dirname, "../src");
|
||||
const host = readFileSync(path.join(srcRoot, "components/topology/HostMultiRigGraph.tsx"), "utf8");
|
||||
const rigGraph = readFileSync(path.join(srcRoot, "components/RigGraph.tsx"), "utf8");
|
||||
const rigNode = readFileSync(path.join(srcRoot, "components/RigNode.tsx"), "utf8");
|
||||
const hybridNodes = readFileSync(path.join(srcRoot, "components/topology/HybridTopologyNodes.tsx"), "utf8");
|
||||
const activityCards = readFileSync(path.join(srcRoot, "components/topology/activity-card-visuals.ts"), "utf8");
|
||||
const table = readFileSync(path.join(srcRoot, "components/topology/TopologyTableView.tsx"), "utf8");
|
||||
const ring = readFileSync(path.join(srcRoot, "components/topology/ActivityRing.tsx"), "utf8");
|
||||
const edge = readFileSync(path.join(srcRoot, "components/topology/HotPotatoEdge.tsx"), "utf8");
|
||||
@@ -114,6 +127,10 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
|
||||
expect(rigGraph).toMatch(/applyHotPotatoEdges/);
|
||||
expect(rigGraph).toMatch(/edgeTypes=\{edgeTypes\}/);
|
||||
expect(hybridNodes).toMatch(/ActivityRing/);
|
||||
expect(hybridNodes).toMatch(/getActivityCardClasses/);
|
||||
expect(rigNode).toMatch(/getActivityCardClasses/);
|
||||
expect(activityCards).toMatch(/activity-card-active/);
|
||||
expect(rigGraph).toMatch(/activityRing/);
|
||||
expect(table).toMatch(/ActivityRing/);
|
||||
expect(table).toMatch(/group-hover:opacity-100/);
|
||||
expect(ring).not.toMatch(/StatusPip/);
|
||||
@@ -126,6 +143,9 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
|
||||
expect(sourceFiles.join("\n").match(/"6 7"/g) ?? []).toHaveLength(1);
|
||||
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/);
|
||||
expect(css).toMatch(/activity-ring-active/);
|
||||
expect(css).toMatch(/activity-card-active/);
|
||||
expect(css).toMatch(/activity-card-target-flash/);
|
||||
expect(edge).toMatch(/non-scaling-stroke/);
|
||||
expect(css).toMatch(/rig-activity-frame-pulse/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user