@@ -44,6 +44,13 @@ export type ActiveTurnSummary = {
4444 anchorAt : number ;
4545} ;
4646
47+ /** One channel with active agent work, aggregated across agents. */
48+ export type ActiveChannelTurnSummary = {
49+ channelId : string ;
50+ anchorAt : number ;
51+ agentCount : number ;
52+ } ;
53+
4754// Module-level state: agentPubkey → turnId → ActiveTurn
4855const activeTurnsByAgent = new Map < string , Map < string , ActiveTurn > > ( ) ;
4956const listeners = new Set < ( ) => void > ( ) ;
@@ -68,6 +75,7 @@ const clockOffsetByAgent = new Map<string, number>();
6875// Cached snapshots for useSyncExternalStore reference stability.
6976// Only regenerated when the underlying turn map for an agent actually changes.
7077const cachedTurnSummaries = new Map < string , ActiveTurnSummary [ ] > ( ) ;
78+ let cachedChannelTurnSummaries : ActiveChannelTurnSummary [ ] | null = null ;
7179
7280// Composite watermark per agent: the newest observer event processed, by
7381// (timestamp, seq) ordering. An event is processed only if it is strictly
@@ -87,6 +95,7 @@ let pruneInterval: ReturnType<typeof setInterval> | null = null;
8795
8896function invalidateCache ( agentKey : string ) {
8997 cachedTurnSummaries . delete ( agentKey ) ;
98+ cachedChannelTurnSummaries = null ;
9099}
91100
92101function notifyListeners ( ) {
@@ -427,6 +436,53 @@ export function getActiveTurnsForAgent(
427436}
428437
429438const EMPTY_TURNS : ActiveTurnSummary [ ] = [ ] ;
439+ const EMPTY_CHANNEL_TURNS : ActiveChannelTurnSummary [ ] = [ ] ;
440+
441+ /**
442+ * Returns active working channels across all tracked agents, sorted by
443+ * channelId and anchored to the earliest live turn in each channel.
444+ */
445+ export function getActiveTurnsByChannel ( ) : ActiveChannelTurnSummary [ ] {
446+ if ( cachedChannelTurnSummaries ) return cachedChannelTurnSummaries ;
447+ if ( activeTurnsByAgent . size === 0 ) return EMPTY_CHANNEL_TURNS ;
448+
449+ const summaries = new Map <
450+ string ,
451+ { anchorAt : number ; agentPubkeys : Set < string > }
452+ > ( ) ;
453+
454+ for ( const [ agentKey , agentTurns ] of activeTurnsByAgent ) {
455+ if ( agentTurns . size === 0 ) continue ;
456+ const offset = clockOffsetByAgent . get ( agentKey ) ?? 0 ;
457+
458+ for ( const turn of agentTurns . values ( ) ) {
459+ const anchorAt = turn . startedAt + offset ;
460+ const summary = summaries . get ( turn . channelId ) ;
461+ if ( ! summary ) {
462+ summaries . set ( turn . channelId , {
463+ anchorAt,
464+ agentPubkeys : new Set ( [ agentKey ] ) ,
465+ } ) ;
466+ continue ;
467+ }
468+
469+ summary . agentPubkeys . add ( agentKey ) ;
470+ if ( anchorAt < summary . anchorAt ) {
471+ summary . anchorAt = anchorAt ;
472+ }
473+ }
474+ }
475+
476+ const result = [ ...summaries . entries ( ) ]
477+ . map ( ( [ channelId , summary ] ) => ( {
478+ channelId,
479+ anchorAt : summary . anchorAt ,
480+ agentCount : summary . agentPubkeys . size ,
481+ } ) )
482+ . sort ( ( a , b ) => a . channelId . localeCompare ( b . channelId ) ) ;
483+ cachedChannelTurnSummaries = result ;
484+ return result ;
485+ }
430486
431487/**
432488 * Synchronize the active-turns store with the latest observer events for a
@@ -457,6 +513,17 @@ export function useActiveAgentTurns(
457513 return React . useSyncExternalStore ( subscribeActiveAgentTurns , getSnapshot ) ;
458514}
459515
516+ /**
517+ * Hook: returns channels with active agent work across all tracked agents.
518+ * Re-renders when the channel set changes — not when the clock ticks.
519+ */
520+ export function useActiveAgentTurnsByChannel ( ) : ActiveChannelTurnSummary [ ] {
521+ return React . useSyncExternalStore (
522+ subscribeActiveAgentTurns ,
523+ getActiveTurnsByChannel ,
524+ ) ;
525+ }
526+
460527/**
461528 * Bridge hook: processes observer events into the active-turns store.
462529 * Should be called by a parent component that has access to the observer events.
@@ -483,6 +550,7 @@ export function resetActiveAgentTurnsStore() {
483550 lastProcessed . clear ( ) ;
484551 clockOffsetByAgent . clear ( ) ;
485552 cachedTurnSummaries . clear ( ) ;
553+ cachedChannelTurnSummaries = null ;
486554 terminalAtByAgent . clear ( ) ;
487555 notifyListeners ( ) ;
488556}
0 commit comments