mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 23:42:03 +00:00
fix(cli): restore SubAgent shortcut focus (#3771)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
This commit is contained in:
parent
4cd9f0cbe4
commit
8b6b0d64f8
4 changed files with 247 additions and 5 deletions
|
|
@ -12,6 +12,7 @@ import { ToolGroupMessage } from './ToolGroupMessage.js';
|
|||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import type {
|
||||
AgentResultDisplay,
|
||||
Config,
|
||||
ToolCallConfirmationDetails,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
|
@ -26,12 +27,16 @@ vi.mock('./ToolMessage.js', () => ({
|
|||
description,
|
||||
status,
|
||||
emphasis,
|
||||
resultDisplay,
|
||||
isFocused,
|
||||
}: {
|
||||
callId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: ToolCallStatus;
|
||||
emphasis: string;
|
||||
resultDisplay?: unknown;
|
||||
isFocused?: boolean;
|
||||
}) {
|
||||
// Use the same constants as the real component
|
||||
const statusSymbolMap: Record<ToolCallStatus, string> = {
|
||||
|
|
@ -43,6 +48,18 @@ vi.mock('./ToolMessage.js', () => ({
|
|||
[ToolCallStatus.Error]: TOOL_STATUS.ERROR,
|
||||
};
|
||||
const statusSymbol = statusSymbolMap[status] || '?';
|
||||
if (
|
||||
resultDisplay &&
|
||||
typeof resultDisplay === 'object' &&
|
||||
(resultDisplay as { type?: string }).type === 'task_execution'
|
||||
) {
|
||||
return (
|
||||
<Text>
|
||||
MockSubagent[{callId}]: focused={String(isFocused)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text>
|
||||
MockTool[{callId}]: {statusSymbol} {name} - {description} ({emphasis})
|
||||
|
|
@ -258,6 +275,198 @@ describe('<ToolGroupMessage />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('SubAgent focus', () => {
|
||||
// Helper to build a running SubAgent result display
|
||||
const createRunningSubagentDisplay = (
|
||||
name: string,
|
||||
): AgentResultDisplay => ({
|
||||
type: 'task_execution',
|
||||
subagentName: name,
|
||||
taskDescription: `${name} task`,
|
||||
taskPrompt: `Run ${name}`,
|
||||
status: 'running',
|
||||
toolCalls: [
|
||||
{
|
||||
callId: `${name}-read-1`,
|
||||
name: 'read_file',
|
||||
status: 'success',
|
||||
description: 'Read file',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Helper to build a completed SubAgent result display
|
||||
const createCompletedSubagentDisplay = (
|
||||
name: string,
|
||||
): AgentResultDisplay => ({
|
||||
type: 'task_execution',
|
||||
subagentName: name,
|
||||
taskDescription: `${name} task`,
|
||||
taskPrompt: `Run ${name}`,
|
||||
status: 'completed',
|
||||
toolCalls: [
|
||||
{
|
||||
callId: `${name}-read-1`,
|
||||
name: 'read_file',
|
||||
status: 'success',
|
||||
description: 'Read file',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it('keeps a normal running subagent focused so Ctrl+E can expand it', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={[
|
||||
createToolCall({
|
||||
callId: 'agent-1',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: createRunningSubagentDisplay('reviewer'),
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('MockSubagent[agent-1]: focused=true');
|
||||
});
|
||||
|
||||
it('does not focus a running subagent when the parent group is not focused', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
isFocused={false}
|
||||
toolCalls={[
|
||||
createToolCall({
|
||||
callId: 'agent-1',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: createRunningSubagentDisplay('reviewer'),
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('MockSubagent[agent-1]: focused=false');
|
||||
});
|
||||
|
||||
it('gives focus to only the first running subagent when multiple are running', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={[
|
||||
createToolCall({
|
||||
callId: 'agent-1',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: createRunningSubagentDisplay('first'),
|
||||
}),
|
||||
createToolCall({
|
||||
callId: 'agent-2',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: createRunningSubagentDisplay('second'),
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('MockSubagent[agent-1]: focused=true');
|
||||
expect(lastFrame()).toContain('MockSubagent[agent-2]: focused=false');
|
||||
});
|
||||
|
||||
it('pending confirmation wins over running fallback', () => {
|
||||
const pendingDisplay: AgentResultDisplay = {
|
||||
...createRunningSubagentDisplay('pending-agent'),
|
||||
pendingConfirmation: {
|
||||
type: 'info',
|
||||
title: 'Approve?',
|
||||
prompt: 'Allow this action?',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={[
|
||||
createToolCall({
|
||||
callId: 'agent-running',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: createRunningSubagentDisplay('runner'),
|
||||
}),
|
||||
createToolCall({
|
||||
callId: 'agent-pending',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: pendingDisplay,
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The subagent with pending confirmation gets focus, not the first running one
|
||||
expect(lastFrame()).toContain(
|
||||
'MockSubagent[agent-running]: focused=false',
|
||||
);
|
||||
expect(lastFrame()).toContain(
|
||||
'MockSubagent[agent-pending]: focused=true',
|
||||
);
|
||||
});
|
||||
|
||||
it('direct tool-level confirmation blocks all subagent shortcut focus', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={[
|
||||
createToolCall({
|
||||
callId: 'tool-confirm',
|
||||
name: 'write_file',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'info',
|
||||
title: 'Write file?',
|
||||
prompt: 'Allow write?',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
}),
|
||||
createToolCall({
|
||||
callId: 'agent-running',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Executing,
|
||||
resultDisplay: createRunningSubagentDisplay('runner'),
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Direct tool confirmation active → subagent gets no shortcut focus
|
||||
expect(lastFrame()).toContain(
|
||||
'MockSubagent[agent-running]: focused=false',
|
||||
);
|
||||
});
|
||||
|
||||
it('completed subagent does not receive focus', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={[
|
||||
createToolCall({
|
||||
callId: 'agent-done',
|
||||
name: 'agent',
|
||||
status: ToolCallStatus.Success,
|
||||
resultDisplay: createCompletedSubagentDisplay('finished'),
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('MockSubagent[agent-done]: focused=false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Border Color Logic', () => {
|
||||
it('uses yellow border when tools are pending', () => {
|
||||
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
|
||||
|
|
|
|||
|
|
@ -30,6 +30,18 @@ function isAgentWithPendingConfirmation(
|
|||
);
|
||||
}
|
||||
|
||||
function isRunningAgent(
|
||||
rd: IndividualToolCallDisplay['resultDisplay'],
|
||||
): rd is AgentResultDisplay {
|
||||
return (
|
||||
typeof rd === 'object' &&
|
||||
rd !== null &&
|
||||
'type' in rd &&
|
||||
(rd as AgentResultDisplay).type === 'task_execution' &&
|
||||
(rd as AgentResultDisplay).status === 'running'
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
toolCalls: IndividualToolCallDisplay[];
|
||||
|
|
@ -128,6 +140,18 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
}
|
||||
|
||||
const focusedSubagentCallId = focusedSubagentRef.current;
|
||||
// When no subagent has a pending confirmation, fall back to the *first*
|
||||
// running subagent for Ctrl+E/Ctrl+F shortcut focus. "First" (array order)
|
||||
// is the oldest — the one most likely to have accumulated tool calls and
|
||||
// display the "+N more (ctrl+e to expand)" hint.
|
||||
const runningSubagentCallId = useMemo(
|
||||
() =>
|
||||
toolCalls.find((tc) => isRunningAgent(tc.resultDisplay))?.callId ?? null,
|
||||
[toolCalls],
|
||||
);
|
||||
// Pending confirmation takes strict priority over running fallback.
|
||||
const keyboardFocusedSubagentCallId =
|
||||
focusedSubagentCallId ?? runningSubagentCallId;
|
||||
|
||||
// Compact mode: entire group → single line summary
|
||||
// Force-expand when: user must interact (Confirming or subagent pending
|
||||
|
|
@ -256,12 +280,15 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
{toolCalls.map((tool) => {
|
||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||
// A subagent's inline confirmation should only receive keyboard focus
|
||||
// when (1) there is no direct tool-level confirmation active, and
|
||||
// (2) this tool currently holds the focus lock.
|
||||
// when (1) there is no direct tool-level confirmation active, and (2)
|
||||
// this tool currently holds the subagent keyboard focus. Pending
|
||||
// confirmations keep the existing first-come focus lock; otherwise the
|
||||
// first running subagent owns Ctrl+E/Ctrl+F so the compact hint remains
|
||||
// actionable without making parallel subagents toggle in lock-step.
|
||||
const isSubagentFocused =
|
||||
isFocused &&
|
||||
!toolAwaitingApproval &&
|
||||
focusedSubagentCallId === tool.callId;
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -342,7 +342,10 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
|||
embeddedShellFocused?: boolean;
|
||||
config?: Config;
|
||||
forceShowResult?: boolean;
|
||||
/** Whether this tool's subagent confirmation prompt should respond to keyboard input. */
|
||||
/**
|
||||
* Whether this subagent owns keyboard input for confirmations and
|
||||
* Ctrl+E/Ctrl+F display shortcuts.
|
||||
*/
|
||||
isFocused?: boolean;
|
||||
/** Whether another subagent's approval currently holds the focus lock, blocking this one. */
|
||||
isWaitingForOtherApproval?: boolean;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ export interface AgentExecutionDisplayProps {
|
|||
availableHeight?: number;
|
||||
childWidth: number;
|
||||
config: Config;
|
||||
/** Whether this display's confirmation prompt should respond to keyboard input. */
|
||||
/**
|
||||
* Whether this subagent owns keyboard input for confirmations and
|
||||
* Ctrl+E/Ctrl+F display shortcuts.
|
||||
*/
|
||||
isFocused?: boolean;
|
||||
/** Whether another subagent's approval currently holds the focus lock, blocking this one. */
|
||||
isWaitingForOtherApproval?: boolean;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue