fix(cli): serialize subagent confirmation focus to prevent concurrent input conflicts (#2930)

* fix: serialize subagent confirmation focus to prevent concurrent input conflicts

When multiple subagents run in parallel and each triggers a confirmation
prompt, all prompts previously received keyboard focus simultaneously,
causing a single keypress to be dispatched to every active confirmation.

This change introduces a first-come-first-served focus lock mechanism:
- Track subagents with pending confirmations via a type guard
- Use a useRef-based lock so only one confirmation is focused at a time
- Automatically promote focus to the next pending subagent on resolution
- Show a waiting indicator on non-focused confirmations

Fixes #2929

* fix(cli): use dedicated prop for subagent approval waiting state

---------

Co-authored-by: 思晗 <housihan.hsh@alibaba-inc.com>
This commit is contained in:
pikachu 2026-04-09 14:24:59 +08:00 committed by GitHub
parent 44c596cd14
commit 3e8b3c688f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 4 deletions

View file

@ -173,12 +173,23 @@ const SubagentExecutionRenderer: React.FC<{
availableHeight?: number;
childWidth: number;
config: Config;
}> = ({ data, availableHeight, childWidth, config }) => (
isFocused?: boolean;
isWaitingForOtherApproval?: boolean;
}> = ({
data,
availableHeight,
childWidth,
config,
isFocused,
isWaitingForOtherApproval,
}) => (
<AgentExecutionDisplay
data={data}
availableHeight={availableHeight}
childWidth={childWidth}
config={config}
isFocused={isFocused}
isWaitingForOtherApproval={isWaitingForOtherApproval}
/>
);
@ -249,6 +260,10 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
embeddedShellFocused?: boolean;
config?: Config;
forceShowResult?: boolean;
/** Whether this tool's subagent confirmation prompt should respond to keyboard input. */
isFocused?: boolean;
/** Whether another subagent's approval currently holds the focus lock, blocking this one. */
isWaitingForOtherApproval?: boolean;
}
export const ToolMessage: React.FC<ToolMessageProps> = ({
@ -265,6 +280,8 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
ptyId,
config,
forceShowResult,
isFocused,
isWaitingForOtherApproval,
}) => {
const settings = useSettings();
const isThisShellFocused =
@ -370,6 +387,8 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
availableHeight={availableHeight}
childWidth={innerWidth}
config={config}
isFocused={isFocused}
isWaitingForOtherApproval={isWaitingForOtherApproval}
/>
)}
{effectiveDisplayRenderer.type === 'diff' && (