feat(protocol): deliver 'War Room' UI with Incursion Engine

We've transformed the Social-Dense Hub into a high-fidelity operational surface.
- BACKEND: Implemented Global Incursion Engine in agent-sessions.ts (N^2 overlap detection) and added the 60m 'Idle' state.
- API: Enriched the sessions payload with full metadata and active conflict arrays.
- HEADER: Delivered 4-state agent stations (Active/Stale/Evicted/Idle) with real-time 'time-ago' timers.
- FEED: Implemented the 'Fire Map' visuals:
  * Global Incursion Ticker: High-visibility alerts for agent collisions.
  * Local Conflict Badges: Pulsing pills on affected task cards.
- Refactored components for React-static compliance and strict TypeScript safety.

This commit completes the visibility track, allowing the human supervisor to monitor agent presence and friction in real-time.

OPERATIVE: silver-castle
SESSION: 2026-02-14-1430
This commit is contained in:
zenchantlive 2026-02-14 11:36:32 -08:00
parent e010e0b10b
commit eec1d6e28f
10 changed files with 224 additions and 41 deletions

View file

@ -1,16 +1,17 @@
'use client';
import { motion } from 'framer-motion';
import type { SessionTaskCard } from '../../lib/agent-sessions';
import type { SessionTaskCard, Incursion } from '../../lib/agent-sessions';
import { statusBorder, statusDotColor, statusGradient, sessionStateGlow } from '../shared/status-utils';
interface SessionFeedCardProps {
card: SessionTaskCard;
onSelect: (id: string) => void;
isHighlighted?: boolean;
incursion?: Incursion;
}
export function SessionFeedCard({ card, onSelect, isHighlighted }: SessionFeedCardProps) {
export function SessionFeedCard({ card, onSelect, isHighlighted, incursion }: SessionFeedCardProps) {
return (
<motion.article
layout
@ -19,8 +20,20 @@ export function SessionFeedCard({ card, onSelect, isHighlighted }: SessionFeedCa
isHighlighted
? 'border-sky-500 bg-sky-500/10 ring-1 ring-sky-500/50 scale-[1.02] shadow-[0_0_20px_rgba(56,189,248,0.15)]'
: `${statusBorder(card.status)} ${statusGradient(card.status)} hover:bg-white/[0.04]`
} ${sessionStateGlow(card.sessionState)}`}
} ${sessionStateGlow(card.sessionState)} ${incursion ? 'ring-1 ring-rose-500/30' : ''}`}
>
{incursion && (
<div className="absolute -top-2 right-4 z-10">
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[0.55rem] font-black uppercase tracking-[0.1em] border shadow-lg animate-pulse ${
incursion.severity === 'exact'
? 'bg-rose-500 text-white border-rose-400 shadow-rose-500/20'
: 'bg-amber-500 text-black border-amber-400 shadow-amber-500/20'
}`}>
Conflict
</span>
</div>
)}
<div className="flex gap-[0.75rem]">
{/* Compact Avatar */}
<div className="flex-none">

View file

@ -1,17 +1,41 @@
'use client';
import { useMemo } from 'react';
import type { EpicBucket } from '../../lib/agent-sessions';
import type { EpicBucket, Incursion } from '../../lib/agent-sessions';
import { SessionFeedCard } from './session-feed-card';
interface SessionTaskFeedProps {
feed: EpicBucket[];
incursions?: Incursion[];
selectedEpicId: string | null;
onSelectTask: (id: string) => void;
highlightTaskId?: string | null;
}
export function SessionTaskFeed({ feed, selectedEpicId, onSelectTask, highlightTaskId }: SessionTaskFeedProps) {
export function IncursionTicker({ incursions }: { incursions: Incursion[] }) {
return (
<div className="flex flex-col gap-2">
{incursions.map((inc, i) => (
<div
key={i}
className={`flex items-center gap-3 px-4 py-2 rounded-xl border border-rose-500/20 bg-rose-500/5 backdrop-blur-md animate-in slide-in-from-top-4 duration-500`}
>
<div className="flex-none">
<span className={`flex h-2 w-2 rounded-full ${inc.severity === 'exact' ? 'bg-rose-500 animate-pulse' : 'bg-amber-500'}`} />
</div>
<p className="ui-text text-[0.7rem] font-bold text-rose-200/80">
<span className="uppercase tracking-widest mr-2 opacity-50">Conflict Detected:</span>
<span className="text-white mr-2">{inc.agents.join(' & ')}</span>
<span className="opacity-40 font-medium">overlapping in</span>
<span className="ml-2 font-mono text-rose-300/90">{inc.scope}</span>
</p>
</div>
))}
</div>
);
}
export function SessionTaskFeed({ feed, incursions = [], selectedEpicId, onSelectTask, highlightTaskId }: SessionTaskFeedProps) {
const filteredFeed = useMemo(() => {
if (!selectedEpicId) return feed;
return feed.filter(b => b.epic.id === selectedEpicId);
@ -30,6 +54,12 @@ export function SessionTaskFeed({ feed, selectedEpicId, onSelectTask, highlightT
return (
<div className="space-y-16 pb-24">
{incursions.length > 0 && (
<div className="mb-8">
<IncursionTicker incursions={incursions} />
</div>
)}
{filteredFeed.map(bucket => (
<section key={bucket.epic.id} className="space-y-[1.5rem]">
<header className="flex items-center gap-[1rem] px-[0.5rem] group">
@ -53,17 +83,24 @@ export function SessionTaskFeed({ feed, selectedEpicId, onSelectTask, highlightT
</header>
<div className="grid grid-cols-[repeat(auto-fill,minmax(20rem,1fr))] gap-[1.5rem]">
{bucket.tasks.map(task => (
<SessionFeedCard
key={task.id}
card={task}
onSelect={onSelectTask}
isHighlighted={highlightTaskId === task.id}
/>
))}
{bucket.tasks.map(task => {
const taskIncursion = incursions.find(inc =>
task.owner && inc.agents.includes(task.owner)
);
return (
<SessionFeedCard
key={task.id}
card={task}
onSelect={onSelectTask}
isHighlighted={highlightTaskId === task.id}
incursion={taskIncursion}
/>
);
})}
</div>
</section>
))}
</div>
);
}
}

View file

@ -1,6 +1,7 @@
'use client';
import type { AgentRecord } from '../../lib/agent-registry';
import { useEffect, useState } from 'react';
import type { AgentRecord, AgentLiveness } from '../../lib/agent-registry';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import type { ProjectScopeOption } from '../../lib/project-scope';
@ -16,6 +17,7 @@ interface SessionsHeaderProps {
needsInput: number;
completed: number;
};
livenessMap?: Record<string, string>;
}
export function SessionsHeader({
@ -26,6 +28,7 @@ export function SessionsHeader({
projectScopeMode,
projectScopeOptions,
stats,
livenessMap = {},
}: SessionsHeaderProps) {
return (
<header className="sticky top-0 z-50 flex flex-col border-b border-white/5 bg-[#0b0c10]/60 backdrop-blur-3xl shadow-2xl">
@ -43,6 +46,7 @@ export function SessionsHeader({
agent={agent}
isSelected={activeAgentId === agent.agent_id}
onSelect={onSelectAgent}
liveness={(livenessMap[agent.agent_id] as AgentLiveness) || 'active'}
/>
))}
</div>
@ -74,25 +78,68 @@ export function SessionsHeader({
);
}
function useTimeAgo(isoTimestamp: string) {
const [timeAgo, setTimeAgo] = useState('');
useEffect(() => {
const update = () => {
const seconds = Math.floor((new Date().getTime() - new Date(isoTimestamp).getTime()) / 1000);
if (seconds < 60) setTimeAgo(`${seconds}s`);
else if (seconds < 3600) setTimeAgo(`${Math.floor(seconds / 60)}m`);
else setTimeAgo(`${Math.floor(seconds / 3600)}h`);
};
update();
const interval = setInterval(update, 10000);
return () => clearInterval(interval);
}, [isoTimestamp]);
return timeAgo;
}
function AgentStation({
agent,
isSelected,
onSelect
onSelect,
liveness
}: {
agent: AgentRecord,
isSelected: boolean,
onSelect: (id: string | null) => void
onSelect: (id: string | null) => void,
liveness: AgentLiveness
}) {
const isActive = agent.status !== 'idle';
const timeAgo = useTimeAgo(agent.last_seen_at);
const statusStyles = {
active: {
dot: 'bg-emerald-500 animate-pulse shadow-[0_0_10px_rgba(16,185,129,0.8)]',
label: 'On Mission',
color: 'text-emerald-400/60'
},
stale: {
dot: 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.5)]',
label: 'Lease Expiring',
color: 'text-amber-400/60'
},
evicted: {
dot: 'bg-rose-500/50 shadow-none',
label: 'Disconnected',
color: 'text-rose-400/40'
},
idle: {
dot: 'bg-zinc-700 shadow-none',
label: 'Idle',
color: 'text-zinc-500/30'
}
}[liveness];
return (
<button
onClick={() => onSelect(isSelected ? null : agent.agent_id)}
className={`flex-none group flex w-[9.5rem] items-center gap-2 rounded-lg border px-2 py-1.5 transition-all duration-300 ${
className={`flex-none group flex w-[10rem] items-center gap-2 rounded-lg border px-2 py-1.5 transition-all duration-300 ${
isSelected
? 'border-sky-500/50 bg-sky-500/10 shadow-[0_0_10px_rgba(14,165,233,0.1)]'
: 'border-white/5 bg-white/[0.01] hover:bg-white/5'
}`}
} ${liveness === 'idle' ? 'opacity-40 grayscale-[0.5]' : ''}`}
>
<div className="relative flex-none">
<div className={`h-7 w-7 rounded-md bg-gradient-to-br from-zinc-700 to-zinc-900 flex items-center justify-center border border-white/10 shadow-inner transition-transform duration-300 ${isSelected ? 'scale-90' : 'group-hover:scale-105'}`}>
@ -100,17 +147,20 @@ function AgentStation({
{agent.agent_id.slice(0, 2).toUpperCase()}
</span>
</div>
<span className={`absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border-2 border-[#0b0c10] ${
isActive ? 'bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.8)]' : 'bg-zinc-600'
}`} />
<span className={`absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border-2 border-[#0b0c10] ${statusStyles.dot}`} />
</div>
<div className="flex flex-col items-start min-w-0">
<span className={`ui-text text-[0.65rem] font-black truncate w-full transition-colors ${isSelected ? 'text-sky-300' : 'text-text-body'}`}>
{agent.agent_id}
</span>
<span className="system-data text-[0.5rem] font-bold text-text-muted/30 uppercase tracking-tighter">
{isActive ? 'On Mission' : 'Standby'}
<div className="flex flex-col items-start min-w-0 text-left">
<div className="flex items-center gap-1 w-full justify-between pr-1">
<span className={`ui-text text-[0.65rem] font-black truncate transition-colors ${isSelected ? 'text-sky-300' : 'text-text-body'}`}>
{agent.agent_id}
</span>
<span className="system-data text-[0.5rem] font-bold text-text-muted/40">
{timeAgo}
</span>
</div>
<span className={`system-data text-[0.5rem] font-bold uppercase tracking-tighter ${statusStyles.color}`}>
{statusStyles.label}
</span>
</div>
</button>

View file

@ -30,7 +30,7 @@ export function SessionsPage({
projectScopeMode,
}: SessionsPageProps) {
// 2. Session-specific feed
const { feed, loading, refresh: refreshFeed, stats } = useSessionFeed(projectRoot);
const { feed, incursions, livenessMap, loading, refresh: refreshFeed, stats } = useSessionFeed(projectRoot);
const {
selectedAgentId,
@ -73,6 +73,7 @@ export function SessionsPage({
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
stats={stats}
livenessMap={livenessMap}
/>
<div className="flex flex-1 overflow-hidden">
@ -97,6 +98,7 @@ export function SessionsPage({
) : (
<SessionTaskFeed
feed={feed}
incursions={incursions}
selectedEpicId={selectedEpicId}
onSelectTask={setSelectedTaskId}
highlightTaskId={selectedTaskId}