feat: add route control bar and fix replay protocol compliance
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 1m0s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled

Add ROUTE_CONTROL command type and route-control mapping in
AgentCommandController. New RouteControlBar component in the exchange
header shows Start/Stop/Suspend/Resume actions (grouped pill bar) and
a Replay button, gated by agent capabilities and OPERATOR/ADMIN role.

Fix useReplayExchange hook to match protocol section 16: payload now
uses { routeId, exchange: { body, headers }, originalExchangeId, nonce }
instead of the flat { headers, body } format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-30 21:42:06 +02:00
parent 30e9b55379
commit 8b0d473fcd
6 changed files with 258 additions and 9 deletions

View File

@@ -4,8 +4,10 @@ import { GitBranch, Server } from 'lucide-react';
import { StatusDot, MonoText, Badge } from '@cameleer/design-system';
import { useCorrelationChain } from '../../api/queries/correlation';
import { useAgents } from '../../api/queries/agents';
import { useAuthStore } from '../../auth/auth-store';
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
import { attributeBadgeColor } from '../../utils/attribute-color';
import { RouteControlBar } from './RouteControlBar';
import styles from './ExchangeHeader.module.css';
interface ExchangeHeaderProps {
@@ -47,14 +49,22 @@ export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }:
const showChain = chain && chain.length > 1;
const attrs = Object.entries(detail.attributes ?? {});
// Look up agent state for icon coloring
// Look up agent state for icon coloring + route control capability
const { data: agents } = useAgents(undefined, detail.applicationName);
const agentState = useMemo(() => {
if (!agents || !detail.agentId) return undefined;
const agent = (agents as any[]).find((a: any) => a.id === detail.agentId);
return agent?.state?.toLowerCase() as 'live' | 'stale' | 'dead' | undefined;
const { agentState, hasRouteControl, hasReplay } = useMemo(() => {
if (!agents) return { agentState: undefined, hasRouteControl: false, hasReplay: false };
const agentList = agents as any[];
const agent = detail.agentId ? agentList.find((a: any) => a.id === detail.agentId) : undefined;
return {
agentState: agent?.state?.toLowerCase() as 'live' | 'stale' | 'dead' | undefined,
hasRouteControl: agentList.some((a: any) => a.capabilities?.routeControl === true),
hasReplay: agentList.some((a: any) => a.capabilities?.replay === true),
};
}, [agents, detail.agentId]);
const roles = useAuthStore((s) => s.roles);
const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN');
return (
<div className={styles.header}>
{/* Exchange info — always shown */}
@@ -92,6 +102,20 @@ export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }:
<span className={styles.duration}>{formatDuration(detail.durationMs)}</span>
</div>
{/* Route control / replay — only if agent supports it AND user has operator+ role */}
{canControl && (hasRouteControl || hasReplay) && (
<RouteControlBar
application={detail.applicationName}
routeId={detail.routeId}
hasRouteControl={hasRouteControl}
hasReplay={hasReplay}
agentId={detail.agentId}
exchangeId={detail.exchangeId}
inputHeaders={detail.inputHeaders}
inputBody={detail.inputBody}
/>
)}
{/* Correlation chain */}
<div className={styles.chain}>
<span className={styles.chainLabel}>Correlated</span>