/ components / PromptInput / useSwarmBanner.ts
useSwarmBanner.ts
  1  import * as React from 'react'
  2  import { useAppState, useAppStateStore } from '../../state/AppState.js'
  3  import {
  4    getActiveAgentForInput,
  5    getViewedTeammateTask,
  6  } from '../../state/selectors.js'
  7  import {
  8    AGENT_COLOR_TO_THEME_COLOR,
  9    AGENT_COLORS,
 10    type AgentColorName,
 11    getAgentColor,
 12  } from '../../tools/AgentTool/agentColorManager.js'
 13  import { getStandaloneAgentName } from '../../utils/standaloneAgent.js'
 14  import { isInsideTmux } from '../../utils/swarm/backends/detection.js'
 15  import {
 16    getCachedDetectionResult,
 17    isInProcessEnabled,
 18  } from '../../utils/swarm/backends/registry.js'
 19  import { getSwarmSocketName } from '../../utils/swarm/constants.js'
 20  import {
 21    getAgentName,
 22    getTeammateColor,
 23    getTeamName,
 24    isTeammate,
 25  } from '../../utils/teammate.js'
 26  import { isInProcessTeammate } from '../../utils/teammateContext.js'
 27  import type { Theme } from '../../utils/theme.js'
 28  
 29  type SwarmBannerInfo = {
 30    text: string
 31    bgColor: keyof Theme
 32  } | null
 33  
 34  /**
 35   * Hook that returns banner information for swarm, standalone agent, or --agent CLI context.
 36   * - Leader (not in tmux): Returns "tmux -L ... attach" command with cyan background
 37   * - Leader (in tmux / in-process): Falls through to standalone-agent check — shows
 38   *   /rename name + /color background if set, else null
 39   * - Teammate: Returns "teammate@team" format with their assigned color background
 40   * - Viewing a background agent (CoordinatorTaskPanel): Returns agent name with its color
 41   * - Standalone agent: Returns agent name with their color background (no @team)
 42   * - --agent CLI flag: Returns "@agentName" with cyan background
 43   */
 44  export function useSwarmBanner(): SwarmBannerInfo {
 45    const teamContext = useAppState(s => s.teamContext)
 46    const standaloneAgentContext = useAppState(s => s.standaloneAgentContext)
 47    const agent = useAppState(s => s.agent)
 48    // Subscribe so the banner updates on enter/exit teammate view even though
 49    // getActiveAgentForInput reads it from store.getState().
 50    useAppState(s => s.viewingAgentTaskId)
 51    const store = useAppStateStore()
 52    const [insideTmux, setInsideTmux] = React.useState<boolean | null>(null)
 53  
 54    React.useEffect(() => {
 55      void isInsideTmux().then(setInsideTmux)
 56    }, [])
 57  
 58    const state = store.getState()
 59  
 60    // Teammate process: show @agentName with assigned color.
 61    // In-process teammates run headless — their banner shows in the leader UI instead.
 62    if (isTeammate() && !isInProcessTeammate()) {
 63      const agentName = getAgentName()
 64      if (agentName && getTeamName()) {
 65        return {
 66          text: `@${agentName}`,
 67          bgColor: toThemeColor(
 68            teamContext?.selfAgentColor ?? getTeammateColor(),
 69          ),
 70        }
 71      }
 72    }
 73  
 74    // Leader with spawned teammates: tmux-attach hint when external, else show
 75    // the viewed teammate's name when inside tmux / native panes / in-process.
 76    const hasTeammates =
 77      teamContext?.teamName &&
 78      teamContext.teammates &&
 79      Object.keys(teamContext.teammates).length > 0
 80    if (hasTeammates) {
 81      const viewedTeammate = getViewedTeammateTask(state)
 82      const viewedColor = toThemeColor(viewedTeammate?.identity.color)
 83      const inProcessMode = isInProcessEnabled()
 84      const nativePanes = getCachedDetectionResult()?.isNative ?? false
 85  
 86      if (insideTmux === false && !inProcessMode && !nativePanes) {
 87        return {
 88          text: `View teammates: \`tmux -L ${getSwarmSocketName()} a\``,
 89          bgColor: viewedColor,
 90        }
 91      }
 92      if (
 93        (insideTmux === true || inProcessMode || nativePanes) &&
 94        viewedTeammate
 95      ) {
 96        return {
 97          text: `@${viewedTeammate.identity.agentName}`,
 98          bgColor: viewedColor,
 99        }
100      }
101      // insideTmux === null: still loading — fall through.
102      // Not viewing a teammate: fall through so /rename and /color are honored.
103    }
104  
105    // Viewing a background agent (CoordinatorTaskPanel): local_agent tasks aren't
106    // InProcessTeammates, so getViewedTeammateTask misses them. Reverse-lookup the
107    // name from agentNameRegistry the same way CoordinatorAgentStatus does.
108    const active = getActiveAgentForInput(state)
109    if (active.type === 'named_agent') {
110      const task = active.task
111      let name: string | undefined
112      for (const [n, id] of state.agentNameRegistry) {
113        if (id === task.id) {
114          name = n
115          break
116        }
117      }
118      return {
119        text: name ? `@${name}` : task.description,
120        bgColor: getAgentColor(task.agentType) ?? 'cyan_FOR_SUBAGENTS_ONLY',
121      }
122    }
123  
124    // Standalone agent (/rename, /color): name and/or custom color, no @team.
125    const standaloneName = getStandaloneAgentName(state)
126    const standaloneColor = standaloneAgentContext?.color
127    if (standaloneName || standaloneColor) {
128      return {
129        text: standaloneName ?? '',
130        bgColor: toThemeColor(standaloneColor),
131      }
132    }
133  
134    // --agent CLI flag (when not handled above).
135    if (agent) {
136      const agentDef = state.agentDefinitions.activeAgents.find(
137        a => a.agentType === agent,
138      )
139      return {
140        text: agent,
141        bgColor: toThemeColor(agentDef?.color, 'promptBorder'),
142      }
143    }
144  
145    return null
146  }
147  
148  function toThemeColor(
149    colorName: string | undefined,
150    fallback: keyof Theme = 'cyan_FOR_SUBAGENTS_ONLY',
151  ): keyof Theme {
152    return colorName && AGENT_COLORS.includes(colorName as AgentColorName)
153      ? AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
154      : fallback
155  }