@@ -177,17 +177,23 @@ export function ChatPanel({ state, onChatUsage }: ChatPanelProps) {
177177 const messages = selectedId ? ( threads . get ( selectedId ) ?? [ ] ) : [ ] ;
178178 const history = selectedId ? ( historyByAgent . get ( selectedId ) ?? [ ] ) : [ ] ;
179179
180- const agents = useMemo ( ( ) => {
181- const map = new Map < string , AgentInfo > ( ) ;
180+ // Group reactions by the lead actor that produced them so cohort runs
181+ // can dropdown-filter to one actor's swarm at a time. Same character
182+ // name appearing under two different leaders (e.g. shared scenario
183+ // archetypes) gets two entries, one per actor, so the chat-thread
184+ // memory stays isolated per leader instead of cross-contaminating.
185+ const agentsByActor = useMemo ( ( ) => {
186+ const out = new Map < string , AgentInfo [ ] > ( ) ;
182187 for ( const actorName of state . actorIds ) {
183188 const sideState = state . actors [ actorName ] ;
184189 if ( ! sideState ) continue ;
190+ const perActorMap = new Map < string , AgentInfo > ( ) ;
185191 for ( const evt of sideState . events ) {
186192 if ( evt . type === 'agent_reactions' ) {
187193 const reactions = evt . data . reactions as Array < Record < string , unknown > > || [ ] ;
188194 for ( const r of reactions ) {
189195 if ( r . name ) {
190- map . set ( r . name as string , {
196+ perActorMap . set ( r . name as string , {
191197 name : r . name as string , role : r . role as string || '' ,
192198 department : r . department as string || '' , mood : r . mood as string || 'neutral' ,
193199 age : r . age as number , marsborn : r . marsborn as boolean ,
@@ -201,10 +207,35 @@ export function ChatPanel({ state, onChatUsage }: ChatPanelProps) {
201207 }
202208 }
203209 }
210+ if ( perActorMap . size > 0 ) out . set ( actorName , Array . from ( perActorMap . values ( ) ) ) ;
204211 }
205- return Array . from ( map . values ( ) ) ;
212+ return out ;
206213 } , [ state ] ) ;
207214
215+ // Cohort filter: when 3+ actors, the sidebar shows a dropdown to
216+ // scope the agent list to a single lead actor's swarm. Default for
217+ // cohorts is the first actor with any agents (so the sidebar isn't
218+ // overwhelmed by ~100 × N characters). Pair runs (2 actors) keep
219+ // the original combined view by defaulting to null = "all leaders".
220+ const isCohort = state . actorIds . length > 2 ;
221+ const [ filterActorId , setFilterActorId ] = useState < string | null > ( null ) ;
222+ useEffect ( ( ) => {
223+ if ( ! isCohort ) {
224+ if ( filterActorId !== null ) setFilterActorId ( null ) ;
225+ return ;
226+ }
227+ if ( filterActorId && agentsByActor . has ( filterActorId ) ) return ;
228+ const firstWithAgents = state . actorIds . find ( id => ( agentsByActor . get ( id ) ?. length ?? 0 ) > 0 ) ;
229+ setFilterActorId ( firstWithAgents ?? null ) ;
230+ } , [ isCohort , agentsByActor , state . actorIds , filterActorId ] ) ;
231+
232+ const agents = useMemo ( ( ) => {
233+ if ( filterActorId ) return agentsByActor . get ( filterActorId ) ?? [ ] ;
234+ const all : AgentInfo [ ] = [ ] ;
235+ for ( const ids of agentsByActor . values ( ) ) all . push ( ...ids ) ;
236+ return all ;
237+ } , [ agentsByActor , filterActorId ] ) ;
238+
208239 const selected = agents . find ( c => c . name === selectedId ) ;
209240
210241 useEffect ( ( ) => {
@@ -334,6 +365,35 @@ export function ChatPanel({ state, onChatUsage }: ChatPanelProps) {
334365 : `Chat becomes available after the first turn completes. Start a simulation and come back once agents have reacted to the first crisis. Each agent has persistent memory, personality, and relationships shaped by the crises they experience.`
335366 }
336367 </ p >
368+
369+ { /* Cohort filter: pick one lead actor's swarm at a time. Hidden
370+ for solo + pair runs so the existing 2-actor combined view
371+ stays unchanged. Built off `agentsByActor` so the count next
372+ to each name updates live as reactions stream in. */ }
373+ { isCohort && agentsByActor . size > 0 && (
374+ < div className = { styles . actorFilter } >
375+ < label className = { styles . actorFilterLabel } htmlFor = "chat-actor-filter" >
376+ Lead actor
377+ </ label >
378+ < select
379+ id = "chat-actor-filter"
380+ className = { styles . actorFilterSelect }
381+ value = { filterActorId ?? '' }
382+ onChange = { ( e ) => setFilterActorId ( e . target . value || null ) }
383+ aria-label = "Filter chat agents by lead actor"
384+ >
385+ { state . actorIds . map ( ( id ) => {
386+ const count = agentsByActor . get ( id ) ?. length ?? 0 ;
387+ const name = state . actors [ id ] ?. leader ?. name || id ;
388+ return (
389+ < option key = { id } value = { id } disabled = { count === 0 } >
390+ { name } ({ count } )
391+ </ option >
392+ ) ;
393+ } ) }
394+ </ select >
395+ </ div >
396+ ) }
337397 { agents . map ( c => (
338398 < button
339399 key = { c . name }
0 commit comments