-
-
-
+
+
+
+
+ {initials}
+
+
+
{role}
+
{label}
+
{model || 'not selected'}
-
{label}
- {reverse ? from : to}
+ {status === 'queued' ? 'Awaiting controlled hand-off' : formatReason(status)}
+
+ );
+}
+
+function ControlGate({ node, selected, onClick }: { node: HarnessGateNode; selected: boolean; onClick: () => void }) {
+ const tone = statusTone(node.status);
+ const gateNumber = node.id.slice(0, 2);
+ return (
+
+ {gateNumber}
+ {node.title.replace(`${gateNumber} · `, '')} {node.status}
+
+ );
+}
+
+function GateDetail({ node }: { node: HarnessGateNode }) {
+ const tone = statusTone(node.status);
+ const evidence = Object.entries(node.evidence).filter(([, value]) => value !== null && value !== undefined).slice(0, 3);
+ return (
+
+
Focused control
{node.title}
+
{node.description}
+
Observed impact
{formatReason(node.reason)}
+ {evidence.length > 0 &&
{evidence.map(([key, value]) => {key}: {typeof value === 'string' ? value : JSON.stringify(value)} )}
}
);
}
-export function ModelInteractionDiagram({ goal, localStage, cloudStage }: ModelInteractionDiagramProps) {
+export function GovernedOutcomePulse({ goal, trace }: { goal: GoalJob; trace?: HarnessTraceGraph }) {
+ const nodes = trace?.nodes ?? [];
+ const progressed = trace?.progress ?? nodes.filter((node) => node.status !== 'queued').length;
+ const current = [...nodes].reverse().find((node) => node.status !== 'queued') ?? null;
+ const outcome = nodes.find((node) => node.id === 'H7-orchestration');
+ const isFailure = goal.status === 'failed' || outcome?.status === 'blocked' || outcome?.status === 'error';
+ const label = isFailure ? 'Stopped with evidence retained' : goal.status === 'requires_approval' ? 'Awaiting human approval' : goal.status === 'completed' ? 'Certified governed outcome' : 'Governed outcome in progress';
+ const percentage = Math.min(100, Math.round((progressed / GATE_ORDER.length) * 100));
+ return (
+
+
Governed outcome · live
{label}
+
+
{progressed}/7 controls have emitted evidence {percentage}%
+
Now at
{current?.title ?? 'Waiting for H1'}
Decision
{outcome?.status === 'pass' ? 'Certified' : isFailure ? 'Safely stopped' : 'Not final yet'}
Latest evidence
{current ? formatReason(current.reason) : 'Trace stream connecting'}
+
+ );
+}
+
+export function ModelInteractionDiagram({ goal, localStage, cloudStage, trace }: ModelInteractionDiagramProps) {
+ const nodes = useMemo(() => trace?.nodes ?? GATE_ORDER.map((id) => ({ id, title: id.replace('-', ' · '), description: 'Waiting for trace evidence', status: 'queued' as const, reason: 'Waiting for evidence', updated_at: null, evidence: {}, events: [] })), [trace]);
+ const preferred = [...nodes].reverse().find((node) => node.status === 'running' || node.status === 'error' || node.status === 'blocked' || node.status === 'warning')?.id ?? [...nodes].reverse().find((node) => node.status !== 'queued')?.id ?? 'H1-context';
+ const [focusedGate, setFocusedGate] = useState(preferred);
+ const [focusedAgent, setFocusedAgent] = useState<'local' | 'cloud'>('local');
+ useEffect(() => setFocusedGate(preferred), [preferred, trace?.updated_at]);
+ const focusedNode = nodes.find((node) => node.id === focusedGate) ?? nodes[0];
const localStatus = localStage?.status ?? 'queued';
const cloudStatus = cloudStage?.status ?? 'queued';
+ const direction = focusedAgent === 'local' ? 'Local worker → security and review controls' : 'Cloud reviewer → governance and final certification';
+
return (
-
-
-
Live model exchange
Models trao đổi và kiểm chứng như thế nào
-
+
+
+
Interactive model exchange
Ai đang tác động vào outcome — và control nào đang quyết định Chọn avatar hoặc từng H-gate để khám phá tác động và evidence thực tế.
-
-
-
Local worker
{localStage?.model || goal.local_model}
-
Cloud reviewer
{cloudStage?.model || goal.cloud_model}
-
-
-
-
-
-
-
+
+
setFocusedAgent('local')} /> setFocusedAgent('cloud')} />Selected influence: {direction}
+
+
Control spine · H1 → H7
+
{nodes.map((node) => setFocusedGate(node.id)} />)}
+ {focusedNode &&
}
-
Mỗi mũi tên thể hiện một boundary có kiểm soát; màu sắc lấy từ trạng thái thực tế của goal và từng worker.
+
Màu, chuyển động và tiến độ lấy từ trace H1–H7. Nếu một gate dừng, governed outcome dừng đúng tại evidence đó — không diễn giải nó là lỗi mới ở các gate sau.
);
}
diff --git a/packages/casan-control-panel/frontend/src/index.css b/packages/casan-control-panel/frontend/src/index.css
index 10c9115..0fb16a6 100644
--- a/packages/casan-control-panel/frontend/src/index.css
+++ b/packages/casan-control-panel/frontend/src/index.css
@@ -58,6 +58,26 @@ button, input, select, textarea { font: inherit; }
animation: trace-flow 1.2s linear infinite;
}
+@keyframes goal-orb-breathe {
+ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 rgba(103, 232, 249, 0); }
+ 50% { transform: scale(1.055); box-shadow: 0 0 28px rgba(103, 232, 249, 0.28); }
+}
+
+@keyframes goal-core-pulse {
+ 0%, 100% { opacity: 0.45; transform: scale(0.7); }
+ 50% { opacity: 0.1; transform: scale(2.2); }
+}
+
+@keyframes goal-signal-ping {
+ 0% { box-shadow: 0 0 0 0 rgba(103, 232, 249, 0.68); }
+ 75%, 100% { box-shadow: 0 0 0 9px rgba(103, 232, 249, 0); }
+}
+
+.goal-agent-running { animation: goal-orb-breathe 1.8s ease-in-out infinite; }
+.goal-agent-running .goal-agent-core { position: absolute; animation: goal-core-pulse 1.8s ease-out infinite; }
+.goal-control-signal { animation: goal-signal-ping 1.4s ease-out infinite; }
+
@media (prefers-reduced-motion: reduce) {
.trace-edge-active::after { animation: none; }
+ .goal-agent-running, .goal-agent-running .goal-agent-core, .goal-control-signal { animation: none; }
}
diff --git a/packages/casan-control-panel/frontend/src/pages/Goals.tsx b/packages/casan-control-panel/frontend/src/pages/Goals.tsx
index a4f9646..5ff04cb 100644
--- a/packages/casan-control-panel/frontend/src/pages/Goals.tsx
+++ b/packages/casan-control-panel/frontend/src/pages/Goals.tsx
@@ -5,7 +5,7 @@ import { api, type GoalJob, type SettingsActor } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
import { TraceExplorer } from '../components/trace/TraceExplorer';
import { MarkdownText } from '../components/ui/MarkdownText';
-import { ModelInteractionDiagram } from '../components/goals/ModelInteractionDiagram';
+import { GovernedOutcomePulse, ModelInteractionDiagram } from '../components/goals/ModelInteractionDiagram';
const DEFAULT_ACTOR: SettingsActor = { actor: 'local-operator', role: 'org-admin', project: 'default', tenant: 'default' };
const TERMINAL = new Set
(['completed', 'degraded', 'failed', 'requires_approval']);
@@ -69,6 +69,12 @@ export function Goals() {
return current && TERMINAL.has(current.status) ? false : 1500;
},
});
+ const traceQuery = useQuery({
+ queryKey: ['goal-trace', selectedId],
+ queryFn: () => api.traceGraph(selectedQuery.data!.trace_id),
+ enabled: Boolean(selectedQuery.data?.trace_id),
+ refetchInterval: (query) => query.state.data?.terminal ? false : 1200,
+ });
const start = useMutation({
mutationFn: () => api.startGoal({ ...actor, project: effectiveProject }, goal.trim(), effectiveProject),
onSuccess: (job) => {
@@ -178,9 +184,10 @@ export function Goals() {
-
+
}>
+
{selected.workspace &&
{selected.workspace.domain} · {selected.workspace.project_id} {selected.context_manifest && <>· {selected.context_manifest.files} files / {selected.context_manifest.characters} chars {selected.context_manifest.truncated && }>}
}