import * as React from 'react' import { useAppState, useAppStateStore } from '../../state/AppState.js' import { getActiveAgentForInput, getViewedTeammateTask, } from '../../state/selectors.js' import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName, getAgentColor, } from '../../tools/AgentTool/agentColorManager.js' import { getStandaloneAgentName } from '../../utils/standaloneAgent.js' import { isInsideTmux } from '../../utils/swarm/backends/detection.js' import { getCachedDetectionResult, isInProcessEnabled, } from '../../utils/swarm/backends/registry.js' import { getSwarmSocketName } from '../../utils/swarm/constants.js' import { getAgentName, getTeammateColor, getTeamName, isTeammate, } from '../../utils/teammate.js' import { isInProcessTeammate } from '../../utils/teammateContext.js' import type { Theme } from '../../utils/theme.js' type SwarmBannerInfo = { text: string bgColor: keyof Theme } | null /** * Hook that returns banner information for swarm, standalone agent, or --agent CLI context. * - Leader (not in tmux): Returns "tmux -L ... attach" command with cyan background * - Leader (in tmux / in-process): Falls through to standalone-agent check — shows * /rename name + /color background if set, else null * - Teammate: Returns "teammate@team" format with their assigned color background * - Viewing a background agent (CoordinatorTaskPanel): Returns agent name with its color * - Standalone agent: Returns agent name with their color background (no @team) * - --agent CLI flag: Returns "@agentName" with cyan background */ export function useSwarmBanner(): SwarmBannerInfo { const teamContext = useAppState(s => s.teamContext) const standaloneAgentContext = useAppState(s => s.standaloneAgentContext) const agent = useAppState(s => s.agent) // Subscribe so the banner updates on enter/exit teammate view even though // getActiveAgentForInput reads it from store.getState(). useAppState(s => s.viewingAgentTaskId) const store = useAppStateStore() const [insideTmux, setInsideTmux] = React.useState(null) React.useEffect(() => { void isInsideTmux().then(setInsideTmux) }, []) const state = store.getState() // Teammate process: show @agentName with assigned color. // In-process teammates run headless — their banner shows in the leader UI instead. if (isTeammate() && !isInProcessTeammate()) { const agentName = getAgentName() if (agentName && getTeamName()) { return { text: `@${agentName}`, bgColor: toThemeColor( teamContext?.selfAgentColor ?? getTeammateColor(), ), } } } // Leader with spawned teammates: tmux-attach hint when external, else show // the viewed teammate's name when inside tmux / native panes / in-process. const hasTeammates = teamContext?.teamName && teamContext.teammates && Object.keys(teamContext.teammates).length > 0 if (hasTeammates) { const viewedTeammate = getViewedTeammateTask(state) const viewedColor = toThemeColor(viewedTeammate?.identity.color) const inProcessMode = isInProcessEnabled() const nativePanes = getCachedDetectionResult()?.isNative ?? false if (insideTmux === false && !inProcessMode && !nativePanes) { return { text: `View teammates: \`tmux -L ${getSwarmSocketName()} a\``, bgColor: viewedColor, } } if ( (insideTmux === true || inProcessMode || nativePanes) && viewedTeammate ) { return { text: `@${viewedTeammate.identity.agentName}`, bgColor: viewedColor, } } // insideTmux === null: still loading — fall through. // Not viewing a teammate: fall through so /rename and /color are honored. } // Viewing a background agent (CoordinatorTaskPanel): local_agent tasks aren't // InProcessTeammates, so getViewedTeammateTask misses them. Reverse-lookup the // name from agentNameRegistry the same way CoordinatorAgentStatus does. const active = getActiveAgentForInput(state) if (active.type === 'named_agent') { const task = active.task let name: string | undefined for (const [n, id] of state.agentNameRegistry) { if (id === task.id) { name = n break } } return { text: name ? `@${name}` : task.description, bgColor: getAgentColor(task.agentType) ?? 'cyan_FOR_SUBAGENTS_ONLY', } } // Standalone agent (/rename, /color): name and/or custom color, no @team. const standaloneName = getStandaloneAgentName(state) const standaloneColor = standaloneAgentContext?.color if (standaloneName || standaloneColor) { return { text: standaloneName ?? '', bgColor: toThemeColor(standaloneColor), } } // --agent CLI flag (when not handled above). if (agent) { const agentDef = state.agentDefinitions.activeAgents.find( a => a.agentType === agent, ) return { text: agent, bgColor: toThemeColor(agentDef?.color, 'promptBorder'), } } return null } function toThemeColor( colorName: string | undefined, fallback: keyof Theme = 'cyan_FOR_SUBAGENTS_ONLY', ): keyof Theme { return colorName && AGENT_COLORS.includes(colorName as AgentColorName) ? AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName] : fallback }