mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
Merge branch 'main' into feature/arena-agent-collaboration
This commit is contained in:
commit
edd8388b27
122 changed files with 3731 additions and 2201 deletions
|
|
@ -22,6 +22,7 @@ import {
|
|||
toJsonl,
|
||||
generateExportFilename,
|
||||
} from '../utils/export/index.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
/**
|
||||
* Action for the 'md' subcommand - exports session to markdown.
|
||||
|
|
@ -320,30 +321,40 @@ async function exportJsonlAction(
|
|||
*/
|
||||
export const exportCommand: SlashCommand = {
|
||||
name: 'export',
|
||||
description: 'Export current session message history to a file',
|
||||
get description() {
|
||||
return t('Export current session message history to a file');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'html',
|
||||
description: 'Export session to HTML format',
|
||||
get description() {
|
||||
return t('Export session to HTML format');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: exportHtmlAction,
|
||||
},
|
||||
{
|
||||
name: 'md',
|
||||
description: 'Export session to markdown format',
|
||||
get description() {
|
||||
return t('Export session to markdown format');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: exportMarkdownAction,
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
description: 'Export session to JSON format',
|
||||
get description() {
|
||||
return t('Export session to JSON format');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: exportJsonAction,
|
||||
},
|
||||
{
|
||||
name: 'jsonl',
|
||||
description: 'Export session to JSONL format (one message per line)',
|
||||
get description() {
|
||||
return t('Export session to JSONL format (one message per line)');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: exportJsonlAction,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
CommandKind,
|
||||
} from './types.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
async function restoreAction(
|
||||
context: CommandContext,
|
||||
|
|
@ -144,8 +145,11 @@ export const restoreCommand = (config: Config | null): SlashCommand | null => {
|
|||
|
||||
return {
|
||||
name: 'restore',
|
||||
description:
|
||||
'Restore a tool call. This will reset the conversation and file history to the state it was in when the tool call was suggested',
|
||||
get description() {
|
||||
return t(
|
||||
'Restore a tool call. This will reset the conversation and file history to the state it was in when the tool call was suggested',
|
||||
);
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: restoreAction,
|
||||
completion,
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ export enum CommandKind {
|
|||
BUILT_IN = 'built-in',
|
||||
FILE = 'file',
|
||||
MCP_PROMPT = 'mcp-prompt',
|
||||
SKILL = 'skill',
|
||||
}
|
||||
|
||||
export interface CommandCompletionItem {
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ export const PlanSummaryDisplay: React.FC<PlanSummaryDisplayProps> = ({
|
|||
availableHeight,
|
||||
childWidth,
|
||||
}) => {
|
||||
const { message, plan } = data;
|
||||
const { message, plan, rejected } = data;
|
||||
const messageColor = rejected ? Colors.AccentYellow : Colors.AccentGreen;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text color={Colors.AccentGreen} wrap="wrap">
|
||||
<Text color={messageColor} wrap="wrap">
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Box } from 'ink';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
|
|
@ -136,13 +136,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
contentWidth={innerWidth}
|
||||
/>
|
||||
)}
|
||||
{tool.outputFile && (
|
||||
<Box marginX={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
Output too long and was saved to: {tool.outputFile}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -300,4 +300,55 @@ describe('<ToolMessage />', () => {
|
|||
);
|
||||
expect(lastFrame()).toContain('MockAnsiOutput:hello');
|
||||
});
|
||||
|
||||
it('renders rejected plan content with plan text still visible', () => {
|
||||
const planResultDisplay = {
|
||||
type: 'plan_summary' as const,
|
||||
message: 'Plan was rejected. Remaining in plan mode.',
|
||||
plan: '# My Plan\n- Step 1: Do something\n- Step 2: Do another thing',
|
||||
rejected: true,
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
name="ExitPlanMode"
|
||||
description="Plan:"
|
||||
status={ToolCallStatus.Canceled}
|
||||
resultDisplay={planResultDisplay}
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Plan was rejected. Remaining in plan mode.');
|
||||
expect(output).toContain('MockMarkdown:# My Plan');
|
||||
expect(output).toContain('- Step 1: Do something');
|
||||
expect(output).toContain('- Step 2: Do another thing');
|
||||
});
|
||||
|
||||
it('renders approved plan content with approval message', () => {
|
||||
const planResultDisplay = {
|
||||
type: 'plan_summary' as const,
|
||||
message: 'User approved the plan.',
|
||||
plan: '# My Plan\n- Step 1\n- Step 2',
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
name="ExitPlanMode"
|
||||
description="Plan:"
|
||||
status={ToolCallStatus.Success}
|
||||
resultDisplay={planResultDisplay}
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('User approved the plan.');
|
||||
expect(output).toContain('MockMarkdown:# My Plan');
|
||||
expect(output).toContain('- Step 1');
|
||||
expect(output).toContain('- Step 2');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export function CreationSummary({
|
|||
}
|
||||
|
||||
// Check length warnings
|
||||
if (state.generatedDescription.length > 300) {
|
||||
if (state.generatedDescription.length > 1000) {
|
||||
allWarnings.push(
|
||||
t('Description is over {{length}} characters', {
|
||||
length: state.generatedDescription.length.toString(),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import type { LoadedSettings } from '../../config/settings.js';
|
|||
import { type CommandContext, type SlashCommand } from '../commands/types.js';
|
||||
import { CommandService } from '../../services/CommandService.js';
|
||||
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
|
||||
import { BundledSkillLoader } from '../../services/BundledSkillLoader.js';
|
||||
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
|
||||
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
|
||||
import { parseSlashCommand } from '../../utils/commands.js';
|
||||
|
|
@ -313,6 +314,7 @@ export const useSlashCommandProcessor = (
|
|||
const loaders = [
|
||||
new McpPromptLoader(config),
|
||||
new BuiltinCommandLoader(config),
|
||||
new BundledSkillLoader(config),
|
||||
new FileCommandLoader(config),
|
||||
];
|
||||
const commandService = await CommandService.create(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
ApprovalMode,
|
||||
AuthType,
|
||||
GeminiEventType as ServerGeminiEventType,
|
||||
SendMessageType,
|
||||
ToolErrorType,
|
||||
ToolConfirmationOutcome,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
|
@ -483,7 +484,7 @@ describe('useGeminiStream', () => {
|
|||
expectedMergedResponse,
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-2',
|
||||
{ isContinuation: true },
|
||||
{ type: SendMessageType.ToolResult },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -807,7 +808,7 @@ describe('useGeminiStream', () => {
|
|||
toolCallResponseParts,
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-4',
|
||||
{ isContinuation: true },
|
||||
{ type: SendMessageType.ToolResult },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -1123,7 +1124,7 @@ describe('useGeminiStream', () => {
|
|||
'This is the actual prompt from the command file.',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
|
||||
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
|
||||
|
|
@ -1150,7 +1151,7 @@ describe('useGeminiStream', () => {
|
|||
'',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1169,7 +1170,7 @@ describe('useGeminiStream', () => {
|
|||
'// This is a line comment',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1188,7 +1189,7 @@ describe('useGeminiStream', () => {
|
|||
'/* This is a block comment */',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -2092,7 +2093,7 @@ describe('useGeminiStream', () => {
|
|||
processedQueryParts, // Argument 1: The parts array directly
|
||||
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
||||
expect.any(String), // Argument 3: The prompt_id string
|
||||
undefined, // Argument 4: Options (undefined for normal prompts)
|
||||
{ type: SendMessageType.UserQuery }, // Argument 4: The options
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -2777,7 +2778,7 @@ describe('useGeminiStream', () => {
|
|||
'First query',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
|
||||
// Verify only the first query was added to history
|
||||
|
|
@ -2829,14 +2830,14 @@ describe('useGeminiStream', () => {
|
|||
'First query',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
expect(mockSendMessageStream).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'Second query',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -2859,7 +2860,7 @@ describe('useGeminiStream', () => {
|
|||
'Second query',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
undefined,
|
||||
{ type: SendMessageType.UserQuery },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,14 +19,17 @@ import type {
|
|||
} from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
GeminiEventType as ServerGeminiEventType,
|
||||
SendMessageType,
|
||||
createDebugLogger,
|
||||
getErrorMessage,
|
||||
isNodeError,
|
||||
MessageSenderType,
|
||||
logUserPrompt,
|
||||
logUserRetry,
|
||||
GitService,
|
||||
UnauthorizedError,
|
||||
UserPromptEvent,
|
||||
UserRetryEvent,
|
||||
logConversationFinishedEvent,
|
||||
ConversationFinishedEvent,
|
||||
ApprovalMode,
|
||||
|
|
@ -1088,19 +1091,22 @@ export const useGeminiStream = (
|
|||
const submitQuery = useCallback(
|
||||
async (
|
||||
query: PartListUnion,
|
||||
options?: { isContinuation: boolean; skipPreparation?: boolean },
|
||||
submitType: SendMessageType = SendMessageType.UserQuery,
|
||||
prompt_id?: string,
|
||||
) => {
|
||||
// Prevent concurrent executions of submitQuery, but allow continuations
|
||||
// which are part of the same logical flow (tool responses)
|
||||
if (isSubmittingQueryRef.current && !options?.isContinuation) {
|
||||
if (
|
||||
isSubmittingQueryRef.current &&
|
||||
submitType !== SendMessageType.ToolResult
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation) &&
|
||||
!options?.isContinuation
|
||||
submitType !== SendMessageType.ToolResult
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
@ -1110,7 +1116,7 @@ export const useGeminiStream = (
|
|||
const userMessageTimestamp = Date.now();
|
||||
|
||||
// Reset quota error flag when starting a new query (not a continuation)
|
||||
if (!options?.isContinuation) {
|
||||
if (submitType !== SendMessageType.ToolResult) {
|
||||
setModelSwitchedFromQuotaError(false);
|
||||
// Commit any pending retry error to history (without hint) since the
|
||||
// user is starting a new conversation turn.
|
||||
|
|
@ -1133,14 +1139,15 @@ export const useGeminiStream = (
|
|||
}
|
||||
|
||||
return promptIdContext.run(prompt_id, async () => {
|
||||
const { queryToSend, shouldProceed } = options?.skipPreparation
|
||||
? { queryToSend: query, shouldProceed: true }
|
||||
: await prepareQueryForGemini(
|
||||
query,
|
||||
userMessageTimestamp,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
);
|
||||
const { queryToSend, shouldProceed } =
|
||||
submitType === SendMessageType.Retry
|
||||
? { queryToSend: query, shouldProceed: true }
|
||||
: await prepareQueryForGemini(
|
||||
query,
|
||||
userMessageTimestamp,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
);
|
||||
|
||||
if (!shouldProceed || queryToSend === null) {
|
||||
isSubmittingQueryRef.current = false;
|
||||
|
|
@ -1148,7 +1155,7 @@ export const useGeminiStream = (
|
|||
}
|
||||
|
||||
// Check image format support for non-continuations
|
||||
if (!options?.isContinuation) {
|
||||
if (submitType === SendMessageType.UserQuery) {
|
||||
const formatCheck = checkImageFormatsSupport(queryToSend);
|
||||
if (formatCheck.hasUnsupportedFormats) {
|
||||
addItem(
|
||||
|
|
@ -1165,7 +1172,7 @@ export const useGeminiStream = (
|
|||
lastPromptRef.current = finalQueryToSend;
|
||||
lastPromptErroredRef.current = false;
|
||||
|
||||
if (!options?.isContinuation) {
|
||||
if (submitType === SendMessageType.UserQuery) {
|
||||
// trigger new prompt event for session stats in CLI
|
||||
startNewPrompt();
|
||||
|
||||
|
|
@ -1186,6 +1193,10 @@ export const useGeminiStream = (
|
|||
setThought(null);
|
||||
}
|
||||
|
||||
if (submitType === SendMessageType.Retry) {
|
||||
logUserRetry(config, new UserRetryEvent(prompt_id));
|
||||
}
|
||||
|
||||
setIsResponding(true);
|
||||
setInitError(null);
|
||||
|
||||
|
|
@ -1194,7 +1205,7 @@ export const useGeminiStream = (
|
|||
finalQueryToSend,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
options,
|
||||
{ type: submitType },
|
||||
);
|
||||
|
||||
const processingStatus = await processGeminiStreamEvents(
|
||||
|
|
@ -1282,7 +1293,7 @@ export const useGeminiStream = (
|
|||
*
|
||||
* When conditions are met:
|
||||
* - Clears any pending auto-retry countdown to avoid duplicate retries
|
||||
* - Re-submits the last query with skipPreparation: true for faster retry
|
||||
* - Re-submits the last query with isRetry: true, reusing the same prompt_id
|
||||
*
|
||||
* This function is exposed via UIActionsContext and triggered by InputPrompt
|
||||
* when the user presses Ctrl+Y (bound to Command.RETRY_LAST in keyBindings.ts).
|
||||
|
|
@ -1309,10 +1320,7 @@ export const useGeminiStream = (
|
|||
|
||||
clearRetryCountdown();
|
||||
|
||||
await submitQuery(lastPrompt, {
|
||||
isContinuation: false,
|
||||
skipPreparation: true,
|
||||
});
|
||||
await submitQuery(lastPrompt, SendMessageType.Retry);
|
||||
}, [streamingState, addItem, clearRetryCountdown, submitQuery]);
|
||||
|
||||
const handleApprovalModeChange = useCallback(
|
||||
|
|
@ -1461,13 +1469,7 @@ export const useGeminiStream = (
|
|||
return;
|
||||
}
|
||||
|
||||
submitQuery(
|
||||
responsesToSend,
|
||||
{
|
||||
isContinuation: true,
|
||||
},
|
||||
prompt_ids[0],
|
||||
);
|
||||
submitQuery(responsesToSend, SendMessageType.ToolResult, prompt_ids[0]);
|
||||
},
|
||||
[
|
||||
isResponding,
|
||||
|
|
|
|||
|
|
@ -252,7 +252,6 @@ export function mapToDisplay(
|
|||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
||||
resultDisplay: trackedCall.response.resultDisplay,
|
||||
confirmationDetails: undefined,
|
||||
outputFile: trackedCall.response.outputFile,
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ export interface IndividualToolCallDisplay {
|
|||
confirmationDetails: ToolCallConfirmationDetails | undefined;
|
||||
renderOutputAsMarkdown?: boolean;
|
||||
ptyId?: number;
|
||||
outputFile?: string;
|
||||
}
|
||||
|
||||
export interface CompressionProps {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue