feat(arena): improve cancellation handling and simplify to in-process mode

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

- Track user-initiated cancellation separately from failures

- Cancel round immediately when user denies a tool call

- Add message queue to handle input during streaming

- Add info messages during Arena operations (apply, stop, cleanup)

- Disable tmux/iTerm2 backends (only in-process mode supported)

- Polish UI: green tool count, updated warning prefix

This improves the Arena UX by providing clearer feedback and

properly handling user cancellations without treating them as failures.
This commit is contained in:
tanzhenxin 2026-03-12 16:57:44 +08:00
parent 3233d16b5c
commit 4ee94715df
13 changed files with 153 additions and 65 deletions

View file

@ -18,9 +18,10 @@
*/
import { Box, Text, useStdin } from 'ink';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
AgentStatus,
isTerminalStatus,
ApprovalMode,
APPROVAL_MODES,
} from '@qwen-code/qwen-code-core';
@ -38,6 +39,7 @@ import { useTextBuffer } from '../shared/text-buffer.js';
import { calculatePromptWidths } from '../../utils/layoutUtils.js';
import { BaseTextInput } from '../BaseTextInput.js';
import { LoadingIndicator } from '../LoadingIndicator.js';
import { QueuedMessageDisplay } from '../QueuedMessageDisplay.js';
import { AgentFooter } from './AgentFooter.js';
import { keyMatchers, Command } from '../../keyMatchers.js';
import { theme } from '../../semantic-colors.js';
@ -182,13 +184,35 @@ export const AgentComposer: React.FC<AgentComposerProps> = ({ agentId }) => {
[buffer, agentTabBarFocused, setAgentTabBarFocused],
);
// ── Message queue (accumulate while streaming, flush as one prompt on idle) ──
const [messageQueue, setMessageQueue] = useState<string[]>([]);
// When agent becomes idle (and not terminal), flush queued messages.
useEffect(() => {
if (
streamingState === StreamingState.Idle &&
messageQueue.length > 0 &&
status !== undefined &&
!isTerminalStatus(status)
) {
const combined = messageQueue.join('\n');
setMessageQueue([]);
interactiveAgent?.enqueueMessage(combined);
}
}, [streamingState, messageQueue, interactiveAgent, status]);
const handleSubmit = useCallback(
(text: string) => {
const trimmed = text.trim();
if (!trimmed || !interactiveAgent) return;
interactiveAgent.enqueueMessage(trimmed);
if (streamingState === StreamingState.Idle) {
interactiveAgent.enqueueMessage(trimmed);
} else {
setMessageQueue((prev) => [...prev, trimmed]);
}
},
[interactiveAgent],
[interactiveAgent, streamingState],
);
// ── Render ──
@ -255,6 +279,8 @@ export const AgentComposer: React.FC<AgentComposerProps> = ({ agentId }) => {
</Box>
)}
<QueuedMessageDisplay messageQueue={messageQueue} />
{/* Input prompt — always visible, like the main Composer */}
<BaseTextInput
buffer={buffer}