From 5a819f91f77ea9b127d36b9ff7af441a3540587d Mon Sep 17 00:00:00 2001 From: Renato Aparecido da Silva Date: Tue, 29 Sep 2026 22:44:30 -0300 Subject: [PATCH 1/5] fix(prompt): prevent color-only banner layout regression --- src/components/PromptInput/PromptInput.tsx | 8 +++ src/components/PromptInput/useSwarmBanner.ts | 3 +- src/components/PromptInput/utils.test.ts | 63 +++++++++++++++++++- src/components/PromptInput/utils.ts | 10 ++++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index f2f2a14f6d..49dd9c2af9 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -314,6 +314,7 @@ function PromptInput({ // WebBrowser pill — visible when a browser is open const bagelFooterVisible = useAppState(s => false); const teamContext = useAppState(s => s.teamContext); + const standaloneAgentContext = useAppState(s => s.standaloneAgentContext); const queuedCommands = useCommandQueue(); const promptSuggestionState = useAppState(s => s.promptSuggestion); const speculation = useAppState(s => s.speculation); @@ -2309,6 +2310,13 @@ function PromptInput({ return AGENT_COLOR_TO_THEME_COLOR[teammateColorName as AgentColorName]; } + // Standalone color is a prompt identity indicator, but does not add a + // separate banner row when no standalone name is set. + const standaloneColor = standaloneAgentContext?.color; + if (standaloneColor && AGENT_COLORS.includes(standaloneColor as AgentColorName)) { + return AGENT_COLOR_TO_THEME_COLOR[standaloneColor as AgentColorName]; + } + // Ambient ultracode indicator: cyan-blue border whenever ultracode is the // active effort. Ranks below bash mode and teammate identity (explicit // contextual overrides) but above the default border. diff --git a/src/components/PromptInput/useSwarmBanner.ts b/src/components/PromptInput/useSwarmBanner.ts index 2ce6ba9f65..093903460f 100644 --- a/src/components/PromptInput/useSwarmBanner.ts +++ b/src/components/PromptInput/useSwarmBanner.ts @@ -24,6 +24,7 @@ import { isTeammate, } from '../../utils/teammate.js' import { isInProcessTeammate } from '../../utils/teammateContext.js' +import { shouldShowStandaloneAgentBanner } from './utils.js' import type { Theme } from '../../utils/theme.js' type SwarmBannerInfo = { @@ -124,7 +125,7 @@ export function useSwarmBanner(): SwarmBannerInfo { // Standalone agent (/rename, /color): name and/or custom color, no @team. const standaloneName = getStandaloneAgentName(state) const standaloneColor = standaloneAgentContext?.color - if (standaloneName || standaloneColor) { + if (shouldShowStandaloneAgentBanner(standaloneName)) { return { text: standaloneName ?? '', bgColor: toThemeColor(standaloneColor), diff --git a/src/components/PromptInput/utils.test.ts b/src/components/PromptInput/utils.test.ts index 98a2fb950a..3b21c90e39 100644 --- a/src/components/PromptInput/utils.test.ts +++ b/src/components/PromptInput/utils.test.ts @@ -1,10 +1,16 @@ +import { PassThrough } from 'node:stream' + import { expect, test } from 'bun:test' +import React from 'react' -import type { Key } from '../../ink.js' +import { createRoot, type Key } from '../../ink.js' +import { AppStateProvider, getDefaultAppState } from '../../state/AppState.js' +import { useSwarmBanner } from './useSwarmBanner.js' import { canAcceptPromptSuggestion, isNonSpacePrintable, normalizePromptInputChunk, + shouldShowStandaloneAgentBanner, resolveHelpToggleChange, resolveCoalescedModeSubmission, } from './utils.js' @@ -86,6 +92,61 @@ test('preserves input and rendered mode without a pending mode entry', () => { }) }) +test('does not create a banner for a color-only standalone context', () => { + expect(shouldShowStandaloneAgentBanner(undefined)).toBe(false) + expect(shouldShowStandaloneAgentBanner('')).toBe(false) + expect(shouldShowStandaloneAgentBanner(' ')).toBe(false) +}) + +test('creates a banner when a standalone agent has a usable name', () => { + expect(shouldShowStandaloneAgentBanner('renato')).toBe(true) +}) + +test('useSwarmBanner omits a banner for color-only standalone context', async () => { + let observedBanner: ReturnType | undefined + + function HookProbe() { + observedBanner = useSwarmBanner() + return null + } + + const stdout = new PassThrough() + const stdin = new PassThrough() as PassThrough & { + isTTY: boolean + setRawMode: (mode: boolean) => void + ref: () => void + unref: () => void + } + stdin.isTTY = true + stdin.setRawMode = () => {} + stdin.ref = () => {} + stdin.unref = () => {} + ;(stdout as unknown as { columns: number }).columns = 120 + + const root = await createRoot({ + stdout: stdout as unknown as NodeJS.WriteStream, + stdin: stdin as unknown as NodeJS.ReadStream, + patchConsole: false, + }) + + root.render( + React.createElement( + AppStateProvider, + { + initialState: { + ...getDefaultAppState(), + standaloneAgentContext: { name: '', color: 'blue' }, + }, + }, + React.createElement(HookProbe), + ), + ) + + await Bun.sleep(10) + expect(observedBanner).toBeNull() + root.unmount() +}) + test('only prompt submissions can accept prompt suggestions', () => { expect(canAcceptPromptSuggestion('prompt')).toBe(true) expect(canAcceptPromptSuggestion('bash')).toBe(false) diff --git a/src/components/PromptInput/utils.ts b/src/components/PromptInput/utils.ts index 5102597e4c..d2870d3986 100644 --- a/src/components/PromptInput/utils.ts +++ b/src/components/PromptInput/utils.ts @@ -126,3 +126,13 @@ export function resolveCoalescedModeSubmission( export function canAcceptPromptSuggestion(mode: PromptInputMode): boolean { return mode === 'prompt' } + +/** + * A standalone color without a usable name should tint the prompt border, + * not create an empty banner that changes the prompt layout. + */ +export function shouldShowStandaloneAgentBanner( + standaloneName: string | undefined, +): boolean { + return standaloneName !== undefined && standaloneName.trim().length > 0 +} From ea2adf2f9dea2d06f3e87f78c799d8852849e37d Mon Sep 17 00:00:00 2001 From: Renato Aparecido da Silva Date: Thu, 1 Oct 2026 11:10:41 -0300 Subject: [PATCH 2/5] test: fix standalone banner regression cleanup --- src/components/PromptInput/utils.test.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/PromptInput/utils.test.ts b/src/components/PromptInput/utils.test.ts index 3b21c90e39..3b706747b8 100644 --- a/src/components/PromptInput/utils.test.ts +++ b/src/components/PromptInput/utils.test.ts @@ -129,22 +129,22 @@ test('useSwarmBanner omits a banner for color-only standalone context', async () patchConsole: false, }) - root.render( - React.createElement( - AppStateProvider, - { + try { + root.render( + React.createElement(AppStateProvider, { initialState: { ...getDefaultAppState(), standaloneAgentContext: { name: '', color: 'blue' }, }, - }, - React.createElement(HookProbe), - ), - ) - - await Bun.sleep(10) - expect(observedBanner).toBeNull() - root.unmount() + children: React.createElement(HookProbe), + }), + ) + + await Bun.sleep(10) + expect(observedBanner).toBeNull() + } finally { + root.unmount() + } }) test('only prompt submissions can accept prompt suggestions', () => { From daa781fd943121cb8e9a2110a09ac679da38f99b Mon Sep 17 00:00:00 2001 From: Renato Aparecido da Silva Date: Thu, 1 Oct 2026 12:11:40 -0300 Subject: [PATCH 3/5] fix: preserve team border identity and validate color-only prompts --- src/components/PromptInput/PromptInput.tsx | 48 +++++----------------- src/components/PromptInput/utils.test.ts | 34 +++++++++++++-- src/components/PromptInput/utils.ts | 36 ++++++++++++++++ 3 files changed, 77 insertions(+), 41 deletions(-) diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 49dd9c2af9..053523fd2b 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -87,7 +87,7 @@ import { findSlackChannelPositions, getKnownChannelsVersion, hasSlackMcpServer, import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'; import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js'; import type { TeamSummary } from '../../utils/teamDiscovery.js'; -import { getTeammateColor } from '../../utils/teammate.js'; +import { getTeammateColor, getTeamName } from '../../utils/teammate.js'; import { isInProcessTeammate } from '../../utils/teammateContext.js'; import { writeToMailbox } from '../../utils/teammateMailbox.js'; import type { TextHighlight } from '../../utils/textHighlighting.js'; @@ -123,7 +123,7 @@ import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'; import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'; import { useShowFastIconHint } from './useShowFastIconHint.js'; import { useSwarmBanner } from './useSwarmBanner.js'; -import { canAcceptPromptSuggestion, isVimModeEnabled, normalizePromptInputChunk, resolveCoalescedModeSubmission, resolveHelpToggleChange } from './utils.js'; +import { canAcceptPromptSuggestion, isVimModeEnabled, normalizePromptInputChunk, resolveCoalescedModeSubmission, resolveHelpToggleChange, resolvePromptBorderColor } from './utils.js'; type Props = { debug: boolean; ideSelection: IDESelection | undefined; @@ -2289,42 +2289,14 @@ function PromptInput({ inlineGhostText, inputFilter: lazySpaceInputFilter }; - const getBorderColor = (): keyof Theme => { - const modeColors: Record = { - bash: 'bashBorder' - }; - - // Mode colors take priority, then teammate color, then default - if (modeColors[mode]) { - return modeColors[mode]; - } - - // In-process teammates run headless - don't apply teammate colors to leader UI - if (isInProcessTeammate()) { - return 'promptBorder'; - } - - // Check for teammate color from environment - const teammateColorName = getTeammateColor(); - if (teammateColorName && AGENT_COLORS.includes(teammateColorName as AgentColorName)) { - return AGENT_COLOR_TO_THEME_COLOR[teammateColorName as AgentColorName]; - } - - // Standalone color is a prompt identity indicator, but does not add a - // separate banner row when no standalone name is set. - const standaloneColor = standaloneAgentContext?.color; - if (standaloneColor && AGENT_COLORS.includes(standaloneColor as AgentColorName)) { - return AGENT_COLOR_TO_THEME_COLOR[standaloneColor as AgentColorName]; - } - - // Ambient ultracode indicator: cyan-blue border whenever ultracode is the - // active effort. Ranks below bash mode and teammate identity (explicit - // contextual overrides) but above the default border. - if (ultracodeActive) { - return 'ultracode'; - } - return 'promptBorder'; - }; + const getBorderColor = (): keyof Theme => resolvePromptBorderColor({ + mode, + inProcessTeammate: isInProcessTeammate(), + teammateColor: teamContext?.selfAgentColor ?? getTeammateColor(), + teamName: getTeamName(teamContext), + standaloneColor: standaloneAgentContext?.color, + ultracodeActive, + }); if (isExternalEditorActive) { return diff --git a/src/components/PromptInput/utils.test.ts b/src/components/PromptInput/utils.test.ts index 3b706747b8..a552e5b456 100644 --- a/src/components/PromptInput/utils.test.ts +++ b/src/components/PromptInput/utils.test.ts @@ -11,6 +11,7 @@ import { isNonSpacePrintable, normalizePromptInputChunk, shouldShowStandaloneAgentBanner, + resolvePromptBorderColor, resolveHelpToggleChange, resolveCoalescedModeSubmission, } from './utils.js' @@ -102,11 +103,14 @@ test('creates a banner when a standalone agent has a usable name', () => { expect(shouldShowStandaloneAgentBanner('renato')).toBe(true) }) -test('useSwarmBanner omits a banner for color-only standalone context', async () => { +test.each(['', ' '])('useSwarmBanner omits a color-only banner with name %j', async name => { let observedBanner: ReturnType | undefined + let notifyRendered!: () => void + const rendered = new Promise(resolve => { notifyRendered = resolve }) function HookProbe() { observedBanner = useSwarmBanner() + React.useEffect(() => { notifyRendered() }, []) return null } @@ -128,25 +132,49 @@ test('useSwarmBanner omits a banner for color-only standalone context', async () stdin: stdin as unknown as NodeJS.ReadStream, patchConsole: false, }) + const exited = root.waitUntilExit() try { root.render( React.createElement(AppStateProvider, { initialState: { ...getDefaultAppState(), - standaloneAgentContext: { name: '', color: 'blue' }, + standaloneAgentContext: { name, color: 'blue' }, }, children: React.createElement(HookProbe), }), ) - await Bun.sleep(10) + await rendered expect(observedBanner).toBeNull() } finally { root.unmount() + await exited + stdout.destroy() + stdin.destroy() } }) +test('standalone border color respects mode and team identity', () => { + const standalone = { + mode: 'prompt' as const, + inProcessTeammate: false, + standaloneColor: 'blue', + ultracodeActive: false, + } + expect(resolvePromptBorderColor(standalone)).toBe('blue_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...standalone, mode: 'bash' })).toBe('bashBorder') + expect(resolvePromptBorderColor({ ...standalone, inProcessTeammate: true })).toBe('promptBorder') + expect(resolvePromptBorderColor({ ...standalone, teammateColor: 'red' })).toBe('red_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...standalone, teammateColor: 'invalid' })).toBe('blue_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...standalone, teamName: 'team', teammateColor: 'red' })).toBe('red_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...standalone, teamName: 'team' })).toBe('promptBorder') + expect(resolvePromptBorderColor({ ...standalone, teamName: 'team', ultracodeActive: true })).toBe('ultracode') + expect(resolvePromptBorderColor({ ...standalone, standaloneColor: 'invalid' })).toBe('promptBorder') + expect(resolvePromptBorderColor({ ...standalone, standaloneColor: undefined, ultracodeActive: true })).toBe('ultracode') + expect(resolvePromptBorderColor({ ...standalone, ultracodeActive: true })).toBe('blue_FOR_SUBAGENTS_ONLY') +}) + test('only prompt submissions can accept prompt suggestions', () => { expect(canAcceptPromptSuggestion('prompt')).toBe(true) expect(canAcceptPromptSuggestion('bash')).toBe(false) diff --git a/src/components/PromptInput/utils.ts b/src/components/PromptInput/utils.ts index d2870d3986..9c97d0b280 100644 --- a/src/components/PromptInput/utils.ts +++ b/src/components/PromptInput/utils.ts @@ -3,6 +3,12 @@ import { isShiftEnterKeyBindingInstalled, } from '../../commands/terminalSetup/terminalSetup.js' import type { Key } from '../../ink.js' +import { + AGENT_COLORS, + AGENT_COLOR_TO_THEME_COLOR, + type AgentColorName, +} from '../../tools/AgentTool/agentColorManager.js' +import type { Theme } from '../../utils/theme.js' import type { PromptInputMode, TextInputChangeContext, @@ -136,3 +142,33 @@ export function shouldShowStandaloneAgentBanner( ): boolean { return standaloneName !== undefined && standaloneName.trim().length > 0 } + +export function resolvePromptBorderColor({ + mode, + inProcessTeammate, + teammateColor, + teamName, + standaloneColor, + ultracodeActive, +}: { + mode: PromptInputMode + inProcessTeammate: boolean + teammateColor?: string + teamName?: string + standaloneColor?: string + ultracodeActive?: boolean +}): keyof Theme { + if (mode === 'bash') return 'bashBorder' + if (inProcessTeammate) return 'promptBorder' + + // Team identity takes precedence over a saved standalone color. + for (const identityColor of [ + teammateColor, + teamName ? undefined : standaloneColor, + ]) { + if (identityColor && AGENT_COLORS.includes(identityColor as AgentColorName)) { + return AGENT_COLOR_TO_THEME_COLOR[identityColor as AgentColorName] + } + } + return ultracodeActive ? 'ultracode' : 'promptBorder' +} From 6b50e997bb4f543d36cd929f8410de3e2b7624f4 Mon Sep 17 00:00:00 2001 From: Renato Aparecido da Silva Date: Fri, 2 Oct 2026 10:42:39 -0300 Subject: [PATCH 4/5] fix: suppress standalone banners while team identity is active --- src/components/PromptInput/PromptInput.tsx | 2 ++ src/components/PromptInput/useSwarmBanner.ts | 14 ++++++++----- src/components/PromptInput/utils.test.ts | 21 ++++++++++++++++++-- src/components/PromptInput/utils.ts | 5 +++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 053523fd2b..15ca22525f 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -198,6 +198,7 @@ type Props = { // Bottom slot has maxHeight="50%"; reserve lines for footer, border, status. const PROMPT_FOOTER_LINES = 5; const MIN_INPUT_VIEWPORT_LINES = 3; +/** Renders the prompt editor, agent banner, and mode-aware input controls. */ function PromptInput({ debug, ideSelection, @@ -2289,6 +2290,7 @@ function PromptInput({ inlineGhostText, inputFilter: lazySpaceInputFilter }; + /** Resolves the current mode and team/standalone identity for the input border. */ const getBorderColor = (): keyof Theme => resolvePromptBorderColor({ mode, inProcessTeammate: isInProcessTeammate(), diff --git a/src/components/PromptInput/useSwarmBanner.ts b/src/components/PromptInput/useSwarmBanner.ts index 093903460f..25b479e193 100644 --- a/src/components/PromptInput/useSwarmBanner.ts +++ b/src/components/PromptInput/useSwarmBanner.ts @@ -35,8 +35,8 @@ type SwarmBannerInfo = { /** * 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 + * - Leader (in tmux / in-process): Shows the viewed teammate, otherwise suppresses + * saved standalone identity while the team remains active * - 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) @@ -100,7 +100,7 @@ export function useSwarmBanner(): SwarmBannerInfo { } } // insideTmux === null: still loading — fall through. - // Not viewing a teammate: fall through so /rename and /color are honored. + // Not viewing a teammate: fall through to background-agent and CLI checks. } // Viewing a background agent (CoordinatorTaskPanel): local_agent tasks aren't @@ -122,10 +122,13 @@ export function useSwarmBanner(): SwarmBannerInfo { } } - // Standalone agent (/rename, /color): name and/or custom color, no @team. + // Standalone agent (/rename, /color): a usable name outside any active team. const standaloneName = getStandaloneAgentName(state) const standaloneColor = standaloneAgentContext?.color - if (shouldShowStandaloneAgentBanner(standaloneName)) { + if ( + !getTeamName(teamContext) && + shouldShowStandaloneAgentBanner(standaloneName) + ) { return { text: standaloneName ?? '', bgColor: toThemeColor(standaloneColor), @@ -146,6 +149,7 @@ export function useSwarmBanner(): SwarmBannerInfo { return null } +/** Maps a recognized agent color to its theme token, otherwise uses the fallback. */ function toThemeColor( colorName: string | undefined, fallback: keyof Theme = 'cyan_FOR_SUBAGENTS_ONLY', diff --git a/src/components/PromptInput/utils.test.ts b/src/components/PromptInput/utils.test.ts index a552e5b456..c9b2010092 100644 --- a/src/components/PromptInput/utils.test.ts +++ b/src/components/PromptInput/utils.test.ts @@ -103,11 +103,20 @@ test('creates a banner when a standalone agent has a usable name', () => { expect(shouldShowStandaloneAgentBanner('renato')).toBe(true) }) -test.each(['', ' '])('useSwarmBanner omits a color-only banner with name %j', async name => { +test.each([ + { name: '', teamName: undefined, expected: null }, + { name: ' ', teamName: undefined, expected: null }, + { name: 'saved-agent', teamName: 'active-team', expected: null }, + { + name: 'saved-agent', teamName: undefined, + expected: { text: 'saved-agent', bgColor: 'blue_FOR_SUBAGENTS_ONLY' }, + }, +])('useSwarmBanner respects standalone and AppState team identity: %j', async ({ name, teamName, expected }) => { let observedBanner: ReturnType | undefined let notifyRendered!: () => void const rendered = new Promise(resolve => { notifyRendered = resolve }) + /** Observes the real hook after mounting its AppState provider. */ function HookProbe() { observedBanner = useSwarmBanner() React.useEffect(() => { notifyRendered() }, []) @@ -140,13 +149,21 @@ test.each(['', ' '])('useSwarmBanner omits a color-only banner with name %j', initialState: { ...getDefaultAppState(), standaloneAgentContext: { name, color: 'blue' }, + teamContext: teamName ? { + teamName, + teamFilePath: '/test/team.json', + leadAgentId: 'team-lead', + isLeader: true, + selfAgentColor: 'red', + teammates: {}, + } : undefined, }, children: React.createElement(HookProbe), }), ) await rendered - expect(observedBanner).toBeNull() + expect(observedBanner).toEqual(expected) } finally { root.unmount() await exited diff --git a/src/components/PromptInput/utils.ts b/src/components/PromptInput/utils.ts index 9c97d0b280..241c2b5d36 100644 --- a/src/components/PromptInput/utils.ts +++ b/src/components/PromptInput/utils.ts @@ -143,6 +143,11 @@ export function shouldShowStandaloneAgentBanner( return standaloneName !== undefined && standaloneName.trim().length > 0 } +/** + * Resolves the border token with mode overrides before agent identity. + * Active teams suppress saved standalone colors; invalid colors fall through + * to the next eligible identity, then ultracode or the default prompt border. + */ export function resolvePromptBorderColor({ mode, inProcessTeammate, From bfefd18c244841bab7518872cb2c515b7fee5a81 Mon Sep 17 00:00:00 2001 From: Renato Aparecido da Silva Date: Fri, 2 Oct 2026 11:14:10 -0300 Subject: [PATCH 5/5] fix: resolve prompt color from AppState team members Resolve explicit self color, matching self/leader member color, then dynamic teammate color. Preserve mode priority and suppress saved standalone colors for active teams. Validation (Windows, Node 24.21.0, Bun 1.4.2): - PromptInput tests: 21 passed, 0 failed, 42 assertions. - Frozen install, typecheck, type tests (10 files), build/smoke/deadcode, both CLI version checks, diff check and PR intent scan passed. - Full check incomplete: hung at repo-map timeout cancellation; isolated PR test passed, isolated unchanged base 9a2910da also hung. Both stuck test processes were terminated. Seven launcher failures preceding the hang match recorded base failures. - Provider: 1701 passed, 1 failed; watchdog failure reproduced on base in prior comparison. - Recommendation: 164 passed, 1 failed; default-profile fallback failure reproduced on base. No provider, launcher, or repo-map code changed. Clean-runner CI remains required. --- src/components/PromptInput/PromptInput.tsx | 3 ++- src/components/PromptInput/utils.test.ts | 25 ++++++++++++++++++++++ src/components/PromptInput/utils.ts | 12 ++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 15ca22525f..3ac42fcce2 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -2294,7 +2294,8 @@ function PromptInput({ const getBorderColor = (): keyof Theme => resolvePromptBorderColor({ mode, inProcessTeammate: isInProcessTeammate(), - teammateColor: teamContext?.selfAgentColor ?? getTeammateColor(), + teammateColor: getTeammateColor(), + teamContext, teamName: getTeamName(teamContext), standaloneColor: standaloneAgentContext?.color, ultracodeActive, diff --git a/src/components/PromptInput/utils.test.ts b/src/components/PromptInput/utils.test.ts index c9b2010092..39f35d630d 100644 --- a/src/components/PromptInput/utils.test.ts +++ b/src/components/PromptInput/utils.test.ts @@ -192,6 +192,31 @@ test('standalone border color respects mode and team identity', () => { expect(resolvePromptBorderColor({ ...standalone, ultracodeActive: true })).toBe('blue_FOR_SUBAGENTS_ONLY') }) +test('border color reads production AppState member identity before dynamic fallback', () => { + const leader = { + name: 'team-lead', color: 'red', tmuxSessionName: '', tmuxPaneId: '', + cwd: '/test', spawnedAt: 0, + } + const teamContext = { + teamName: 'active-team', teamFilePath: '/test/team.json', leadAgentId: 'leader', + teammates: { leader, member: { ...leader, name: 'member', color: 'green' } }, + } + const input = { + mode: 'prompt' as const, inProcessTeammate: false, teamContext, + standaloneColor: 'blue', ultracodeActive: true, + } + // TeamCreateTool stores the leader color in teammates, without selfAgentColor. + expect(resolvePromptBorderColor(input)).toBe('red_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...input, teammateColor: 'yellow' })).toBe('red_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...input, teamContext: { ...teamContext, selfAgentId: 'member' } })).toBe('green_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...input, teamContext: { ...teamContext, selfAgentColor: 'purple' } })).toBe('purple_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...input, teamContext: { ...teamContext, selfAgentId: 'missing' }, teammateColor: 'yellow' })).toBe('yellow_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...input, teamContext: { ...teamContext, teammates: {} } })).toBe('ultracode') + expect(resolvePromptBorderColor({ ...input, teamContext: { ...teamContext, teammates: {}, selfAgentColor: 'invalid' }, teammateColor: 'yellow' })).toBe('yellow_FOR_SUBAGENTS_ONLY') + expect(resolvePromptBorderColor({ ...input, mode: 'bash' })).toBe('bashBorder') + expect(resolvePromptBorderColor({ ...input, inProcessTeammate: true })).toBe('promptBorder') +}) + test('only prompt submissions can accept prompt suggestions', () => { expect(canAcceptPromptSuggestion('prompt')).toBe(true) expect(canAcceptPromptSuggestion('bash')).toBe(false) diff --git a/src/components/PromptInput/utils.ts b/src/components/PromptInput/utils.ts index 241c2b5d36..cfefd2dc3f 100644 --- a/src/components/PromptInput/utils.ts +++ b/src/components/PromptInput/utils.ts @@ -3,6 +3,7 @@ import { isShiftEnterKeyBindingInstalled, } from '../../commands/terminalSetup/terminalSetup.js' import type { Key } from '../../ink.js' +import type { AppState } from '../../state/AppState.js' import { AGENT_COLORS, AGENT_COLOR_TO_THEME_COLOR, @@ -147,11 +148,14 @@ export function shouldShowStandaloneAgentBanner( * Resolves the border token with mode overrides before agent identity. * Active teams suppress saved standalone colors; invalid colors fall through * to the next eligible identity, then ultracode or the default prompt border. + * AppState color priority is explicit self color, matching member color, then + * dynamic teammate color. Leaders without a self ID use their leadAgentId. */ export function resolvePromptBorderColor({ mode, inProcessTeammate, teammateColor, + teamContext, teamName, standaloneColor, ultracodeActive, @@ -159,6 +163,7 @@ export function resolvePromptBorderColor({ mode: PromptInputMode inProcessTeammate: boolean teammateColor?: string + teamContext?: AppState['teamContext'] teamName?: string standaloneColor?: string ultracodeActive?: boolean @@ -166,10 +171,15 @@ export function resolvePromptBorderColor({ if (mode === 'bash') return 'bashBorder' if (inProcessTeammate) return 'promptBorder' + const memberId = teamContext?.selfAgentId ?? teamContext?.leadAgentId + const memberColor = memberId ? teamContext?.teammates[memberId]?.color : undefined + // Team identity takes precedence over a saved standalone color. for (const identityColor of [ + teamContext?.selfAgentColor, + memberColor, teammateColor, - teamName ? undefined : standaloneColor, + teamName || teamContext?.teamName ? undefined : standaloneColor, ]) { if (identityColor && AGENT_COLORS.includes(identityColor as AgentColorName)) { return AGENT_COLOR_TO_THEME_COLOR[identityColor as AgentColorName]