feat: add route control bar and fix replay protocol compliance
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user