Merge branch 'main' into add-git-co-author

This commit is contained in:
tanzhenxin
2025-12-16 14:43:27 +08:00
129 changed files with 4822 additions and 4125 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.5.0",
"version": "0.5.1",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1"
},
"dependencies": {
"@google/genai": "1.16.0",

View File

@@ -130,6 +130,11 @@ export interface CliArgs {
inputFormat?: string | undefined;
outputFormat: string | undefined;
includePartialMessages?: boolean;
/**
* If chat recording is disabled, the chat history would not be recorded,
* so --continue and --resume would not take effect.
*/
chatRecording: boolean | undefined;
/** Resume the most recent session for the current project */
continue: boolean | undefined;
/** Resume a specific session by its ID */
@@ -233,6 +238,11 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
'proxy',
'Use the "proxy" setting in settings.json instead. This flag will be removed in a future version.',
)
.option('chat-recording', {
type: 'boolean',
description:
'Enable chat recording to disk. If false, chat history is not saved and --continue/--resume will not work.',
})
.command('$0 [query..]', 'Launch Qwen Code CLI', (yargsInstance: Argv) =>
yargsInstance
.positional('query', {
@@ -997,6 +1007,11 @@ export async function loadCliConfig(
format: outputSettingsFormat,
},
channel: argv.channel,
// Precedence: explicit CLI flag > settings file > default(true).
// NOTE: do NOT set a yargs default for `chat-recording`, otherwise argv will
// always be true and the settings file can never disable recording.
chatRecording:
argv.chatRecording ?? settings.general?.chatRecording ?? true,
});
}

View File

@@ -204,6 +204,16 @@ const SETTINGS_SCHEMA = {
'Play terminal bell sound when response completes or needs approval.',
showInDialog: true,
},
chatRecording: {
type: 'boolean',
label: 'Chat Recording',
category: 'General',
requiresRestart: true,
default: true,
description:
'Enable saving chat history to disk. Disabling this will also prevent --continue and --resume from working.',
showInDialog: false,
},
},
},
output: {

View File

@@ -486,6 +486,7 @@ describe('gemini.tsx main function kitty protocol', () => {
authType: undefined,
maxSessionTurns: undefined,
channel: undefined,
chatRecording: undefined,
});
await main();

View File

@@ -46,12 +46,15 @@ vi.mock('node:fs', async (importOriginal) => {
// Mock Storage from core
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual = await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
const actual =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...actual,
Storage: {
getGlobalQwenDir: vi.fn().mockReturnValue('/mock/.qwen'),
getGlobalSettingsPath: vi.fn().mockReturnValue('/mock/.qwen/settings.json'),
getGlobalSettingsPath: vi
.fn()
.mockReturnValue('/mock/.qwen/settings.json'),
},
};
});
@@ -360,7 +363,10 @@ describe('languageCommand', () => {
throw new Error('The language command must have an action.');
}
const result = await languageCommand.action(mockContext, 'output Chinese');
const result = await languageCommand.action(
mockContext,
'output Chinese',
);
expect(fs.mkdirSync).toHaveBeenCalled();
expect(fs.writeFileSync).toHaveBeenCalledWith(
@@ -371,7 +377,9 @@ describe('languageCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: expect.stringContaining('LLM output language rule file generated'),
content: expect.stringContaining(
'LLM output language rule file generated',
),
});
});
@@ -380,7 +388,10 @@ describe('languageCommand', () => {
throw new Error('The language command must have an action.');
}
const result = await languageCommand.action(mockContext, 'output Japanese');
const result = await languageCommand.action(
mockContext,
'output Japanese',
);
expect(result).toEqual({
type: 'message',
@@ -514,7 +525,9 @@ describe('languageCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: expect.stringContaining('LLM output language rule file generated'),
content: expect.stringContaining(
'LLM output language rule file generated',
),
});
});
});

View File

@@ -15,6 +15,7 @@ import type {
} from '@qwen-code/qwen-code-core';
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
// Mock child components
vi.mock('./messages/ToolGroupMessage.js', () => ({
@@ -22,7 +23,9 @@ vi.mock('./messages/ToolGroupMessage.js', () => ({
}));
describe('<HistoryItemDisplay />', () => {
const mockConfig = {} as unknown as Config;
const mockConfig = {
getChatRecordingService: () => undefined,
} as unknown as Config;
const baseItem = {
id: 1,
timestamp: 12345,
@@ -133,9 +136,11 @@ describe('<HistoryItemDisplay />', () => {
duration: '1s',
};
const { lastFrame } = renderWithProviders(
<SessionStatsProvider>
<HistoryItemDisplay {...baseItem} item={item} />
</SessionStatsProvider>,
<ConfigContext.Provider value={mockConfig as never}>
<SessionStatsProvider>
<HistoryItemDisplay {...baseItem} item={item} />
</SessionStatsProvider>
</ConfigContext.Provider>,
);
expect(lastFrame()).toContain('Agent powering down. Goodbye!');
});

View File

@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
const actual = await importOriginal<typeof SessionContext>();
@@ -24,6 +25,7 @@ const renderWithMockedStats = (
metrics: SessionMetrics,
sessionId: string = 'test-session-id-12345',
promptCount: number = 5,
chatRecordingEnabled: boolean = true,
) => {
useSessionStatsMock.mockReturnValue({
stats: {
@@ -38,7 +40,17 @@ const renderWithMockedStats = (
startNewPrompt: vi.fn(),
});
return render(<SessionSummaryDisplay duration="1h 23m 45s" />);
const mockConfig = {
getChatRecordingService: vi.fn(() =>
chatRecordingEnabled ? ({} as never) : undefined,
),
};
return render(
<ConfigContext.Provider value={mockConfig as never}>
<SessionSummaryDisplay duration="1h 23m 45s" />
</ConfigContext.Provider>,
);
};
describe('<SessionSummaryDisplay />', () => {
@@ -109,4 +121,34 @@ describe('<SessionSummaryDisplay />', () => {
expect(output).not.toContain('To continue this session, run');
expect(output).not.toContain('qwen --resume');
});
it('does not show resume message when chat recording is disabled', () => {
const metrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0 },
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
const { lastFrame } = renderWithMockedStats(
metrics,
'test-session-id-12345',
5,
false,
);
const output = lastFrame();
expect(output).toContain('Agent powering down. Goodbye!');
expect(output).not.toContain('To continue this session, run');
expect(output).not.toContain('qwen --resume');
});
});

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Box, Text } from 'ink';
import { StatsDisplay } from './StatsDisplay.js';
import { useSessionStats } from '../contexts/SessionContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { theme } from '../semantic-colors.js';
import { t } from '../../i18n/index.js';
@@ -18,10 +19,13 @@ interface SessionSummaryDisplayProps {
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
duration,
}) => {
const config = useConfig();
const { stats } = useSessionStats();
// Only show the resume message if there were messages in the session
// Only show the resume message if there were messages in the session AND
// chat recording is enabled (otherwise there is nothing to resume).
const hasMessages = stats.promptCount > 0;
const canResume = !!config.getChatRecordingService();
return (
<>
@@ -29,7 +33,7 @@ export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
title={t('Agent powering down. Goodbye!')}
duration={duration}
/>
{hasMessages && (
{hasMessages && canResume && (
<Box marginTop={1}>
<Text color={theme.text.secondary}>
{t('To continue this session, run')}{' '}

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.5.0",
"version": "0.5.1",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -318,6 +318,7 @@ export interface ConfigParameters {
generationConfig?: Partial<ContentGeneratorConfig>;
cliVersion?: string;
loadMemoryFromIncludeDirectories?: boolean;
chatRecording?: boolean;
// Web search providers
webSearch?: {
provider: Array<{
@@ -457,6 +458,7 @@ export class Config {
| undefined;
private readonly cliVersion?: string;
private readonly experimentalZedIntegration: boolean = false;
private readonly chatRecordingEnabled: boolean;
private readonly loadMemoryFromIncludeDirectories: boolean = false;
private readonly webSearch?: {
provider: Array<{
@@ -572,6 +574,8 @@ export class Config {
._generationConfig as ContentGeneratorConfig;
this.cliVersion = params.cliVersion;
this.chatRecordingEnabled = params.chatRecording ?? true;
this.loadMemoryFromIncludeDirectories =
params.loadMemoryFromIncludeDirectories ?? false;
this.chatCompression = params.chatCompression;
@@ -618,7 +622,9 @@ export class Config {
setGlobalDispatcher(new ProxyAgent(this.getProxy() as string));
}
this.geminiClient = new GeminiClient(this);
this.chatRecordingService = new ChatRecordingService(this);
this.chatRecordingService = this.chatRecordingEnabled
? new ChatRecordingService(this)
: undefined;
}
/**
@@ -738,7 +744,9 @@ export class Config {
startNewSession(sessionId?: string): string {
this.sessionId = sessionId ?? randomUUID();
this.sessionData = undefined;
this.chatRecordingService = new ChatRecordingService(this);
this.chatRecordingService = this.chatRecordingEnabled
? new ChatRecordingService(this)
: undefined;
if (this.initialized) {
logStartSession(this, new StartSessionEvent(this));
}
@@ -1267,7 +1275,10 @@ export class Config {
/**
* Returns the chat recording service.
*/
getChatRecordingService(): ChatRecordingService {
getChatRecordingService(): ChatRecordingService | undefined {
if (!this.chatRecordingEnabled) {
return undefined;
}
if (!this.chatRecordingService) {
this.chatRecordingService = new ChatRecordingService(this);
}

View File

@@ -338,10 +338,7 @@ describe('OpenAIContentConverter', () => {
});
it('should handle tools without functionDeclarations', async () => {
const emptyTools: Tool[] = [
{} as Tool,
{ functionDeclarations: [] },
];
const emptyTools: Tool[] = [{} as Tool, { functionDeclarations: [] }];
const result = await converter.convertGeminiToolsToOpenAI(emptyTools);
@@ -489,7 +486,10 @@ describe('OpenAIContentConverter', () => {
const result = converter.convertGeminiToolParametersToOpenAI(params);
const properties = result?.['properties'] as Record<string, unknown>;
const nested = properties?.['nested'] as Record<string, unknown>;
const nestedProperties = nested?.['properties'] as Record<string, unknown>;
const nestedProperties = nested?.['properties'] as Record<
string,
unknown
>;
expect(nestedProperties?.['deep']).toEqual({
type: 'integer',

View File

@@ -69,6 +69,8 @@ async function createMockConfig(
targetDir: '.',
debugMode: false,
cwd: process.cwd(),
// Avoid writing any chat recording records from tests (e.g. via tool-call telemetry).
chatRecording: false,
};
const config = new Config(configParams);
await config.initialize();

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/sdk",
"version": "0.5.0",
"version": "0.5.1",
"description": "TypeScript SDK for programmatic access to qwen-code CLI",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.5.0",
"version": "0.5.1",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {