diff --git a/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx b/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx index 611cb6408..3e22afa23 100644 --- a/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx +++ b/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx @@ -279,9 +279,30 @@ export const LiveAgentPanel: React.FC = ({ // in `bgTasksDialogOpen`), but we keep the internal gate so callers // mounting the panel outside that layout still get the right // behavior. + // + // The early-return is the LAST statement of this component on + // purpose — pure rendering moves to LiveAgentPanelBody so that + // future refactors which add a hook can't accidentally drop it + // below the `dialogOpen` guard (`Rendered fewer hooks than + // expected` is the canonical bug shape this guards against). if (dialogOpen) return null; + return ( + + ); +}; - const visibleAgents: LivePanelEntry[] = liveAgentSnapshots +const LiveAgentPanelBody: React.FC<{ + snapshots: AgentDialogEntry[]; + now: number; + maxRows: number; + width: number | undefined; +}> = ({ snapshots, now, maxRows, width }) => { + const visibleAgents: LivePanelEntry[] = snapshots .map((entry) => ({ ...entry, expired: diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 4a78ccdb5..bf8527ff0 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -51,9 +51,9 @@ interface ToolGroupMessageProps { /** * True when this tool group is being rendered live (in * `pendingHistoryItems`). False once it commits to Ink's ``. - * The subagent renderer uses this to suppress the live frame for - * foreground subagents (the pill+dialog handle live drill-down) while - * keeping the committed scrollback render unchanged. + * Currently consumed by upstream callers but not by the group body + * itself — the subagent renderer used to gate its live frame on + * this; that gating moved to LiveAgentPanel + BackgroundTasksDialog. */ isPending?: boolean; activeShellPtyId?: number | null; @@ -78,7 +78,10 @@ export const ToolGroupMessage: React.FC = ({ availableTerminalHeight, contentWidth, isFocused = true, - isPending = false, + // `isPending` stays on the props interface for upstream compat + // (HistoryItemDisplay et al. forward it) but the group body no + // longer reads it. Skip the destructure so TS catches accidental + // re-introductions of dead state. activeShellPtyId, embeddedShellFocused, memoryWriteCount, @@ -298,12 +301,6 @@ export const ToolGroupMessage: React.FC = ({ isFocused && !toolAwaitingApproval && keyboardFocusedSubagentCallId === tool.callId; - // Show the waiting indicator only when this subagent genuinely has a - // pending confirmation AND another subagent holds the focus lock. - const isWaitingForOtherApproval = - isAgentWithPendingConfirmation(tool.resultDisplay) && - focusedSubagentCallId !== null && - focusedSubagentCallId !== tool.callId; return ( @@ -328,8 +325,6 @@ export const ToolGroupMessage: React.FC = ({ isAgentWithPendingConfirmation(tool.resultDisplay) } isFocused={isSubagentFocused} - isPending={isPending} - isWaitingForOtherApproval={isWaitingForOtherApproval} /> {tool.status === ToolCallStatus.Confirming && diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index f1b2df54a..f55f9b165 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -103,8 +103,8 @@ vi.mock('../../utils/MarkdownDisplay.js', () => ({ })); vi.mock('./ToolConfirmationMessage.js', () => ({ ToolConfirmationMessage: function MockToolConfirmationMessage() { - // Sentinel string lets `isPending && pendingConfirmation` tests - // assert the banner renders (instead of being suppressed). + // Sentinel string lets the focus-routed approval tests assert + // the banner renders (instead of being suppressed). return MockApprovalPrompt; }, })); @@ -315,9 +315,7 @@ describe('', () => { status: 'running' | 'completed'; pendingConfirmation?: object; }; - isPending?: boolean; isFocused?: boolean; - isWaitingForOtherApproval?: boolean; }): ToolMessageProps => { const resultDisplay = { type: 'task_execution' as const, @@ -331,9 +329,7 @@ describe('', () => { status: ToolCallStatus.Executing, callId: 'gated-task-call', forceShowResult: true, // mirror ToolGroupMessage's forceShowResult - isPending: overrides.isPending, isFocused: overrides.isFocused, - isWaitingForOtherApproval: overrides.isWaitingForOtherApproval, }; }; @@ -347,7 +343,6 @@ describe('', () => { taskPrompt: 'Search', status: 'running', }, - isPending: true, })} />, StreamingState.Responding, @@ -371,7 +366,6 @@ describe('', () => { taskPrompt: 'Already done', status: 'completed', }, - isPending: false, })} />, StreamingState.Idle, @@ -392,7 +386,6 @@ describe('', () => { status: 'running', pendingConfirmation: {} as object, }, - isPending: true, isFocused: true, })} />, @@ -419,7 +412,6 @@ describe('', () => { status: 'running', pendingConfirmation: {} as object, }, - isPending: true, isFocused: false, })} />, diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index c5a6d7db0..df7e9e13d 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -406,29 +406,8 @@ export interface ToolMessageProps extends IndividualToolCallDisplay { * surface — when true the focus-holder banner renders and the * underlying ToolConfirmationMessage receives keystrokes; when false * sibling subagents render a dim "Queued approval" marker instead. - * (The legacy Ctrl+E / Ctrl+F display shortcuts were retired with - * the inline AgentExecutionDisplay frame; LiveAgentPanel owns the - * live progress surface and BackgroundTasksDialog owns drill-down.) */ isFocused?: boolean; - /** - * True when rendering inside `pendingHistoryItems` (live area), false once - * committed to ``. Subagents no longer paint an inline frame in - * either phase — `LiveAgentPanel` (always-on roster) and - * `BackgroundTasksDialog` (Down-arrow detail) own that surface — so this - * flag is purely informational at this layer; ToolGroupMessage still - * forwards it for non-subagent message types that key off live vs. - * committed rendering. - */ - isPending?: boolean; - /** - * Whether another subagent's approval currently holds the focus lock, - * blocking this one. Routed by `ToolGroupMessage`; vestigial for the - * subagent renderer (the queued marker reads the absence of `isFocused` - * directly), retained on the prop bag for call-site compatibility and - * future signaling needs. - */ - isWaitingForOtherApproval?: boolean; } export const ToolMessage: React.FC = ({ @@ -446,11 +425,6 @@ export const ToolMessage: React.FC = ({ config, forceShowResult, isFocused, - // isPending / isWaitingForOtherApproval flow into ToolMessage from - // ToolGroupMessage but no longer drive the subagent render path - // (LiveAgentPanel + BackgroundTasksDialog own that surface). They stay - // on the props interface so callers don't churn, but skipping the - // destructure here keeps the implementation honest about what's read. executionStartTime, }) => { const settings = useSettings();