mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge branch 'main' into feat/ide-companion-discovery
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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', {
|
||||
@@ -996,6 +1006,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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -486,6 +486,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
authType: undefined,
|
||||
maxSessionTurns: undefined,
|
||||
channel: undefined,
|
||||
chatRecording: undefined,
|
||||
});
|
||||
|
||||
await main();
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')}{' '}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -198,6 +198,52 @@ describe('GlobTool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should find files even if workspace path casing differs from glob results (Windows/macOS)', async () => {
|
||||
// Only relevant for Windows and macOS
|
||||
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
let mismatchedRootDir = tempRootDir;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// 1. Create a path with mismatched casing for the workspace root
|
||||
// e.g., if tempRootDir is "C:\Users\...", make it "c:\Users\..."
|
||||
const drive = path.parse(tempRootDir).root;
|
||||
if (!drive || !drive.match(/^[A-Z]:\\/)) {
|
||||
// Skip if we can't determine/manipulate the drive letter easily
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerDrive = drive.toLowerCase();
|
||||
mismatchedRootDir = lowerDrive + tempRootDir.substring(drive.length);
|
||||
} else {
|
||||
// macOS: change the casing of the path
|
||||
if (tempRootDir === tempRootDir.toLowerCase()) {
|
||||
mismatchedRootDir = tempRootDir.toUpperCase();
|
||||
} else {
|
||||
mismatchedRootDir = tempRootDir.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create a new GlobTool instance with this mismatched root
|
||||
const mismatchedConfig = {
|
||||
...mockConfig,
|
||||
getTargetDir: () => mismatchedRootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(mismatchedRootDir),
|
||||
} as unknown as Config;
|
||||
|
||||
const mismatchedGlobTool = new GlobTool(mismatchedConfig);
|
||||
|
||||
// 3. Execute search
|
||||
const params: GlobToolParams = { pattern: '*.txt' };
|
||||
const invocation = mismatchedGlobTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 2 file(s)');
|
||||
});
|
||||
|
||||
it('should return error if path is outside workspace', async () => {
|
||||
// Bypassing validation to test execute method directly
|
||||
vi.spyOn(globTool, 'validateToolParams').mockReturnValue(null);
|
||||
|
||||
@@ -134,12 +134,21 @@ class GlobToolInvocation extends BaseToolInvocation<
|
||||
this.getFileFilteringOptions(),
|
||||
);
|
||||
|
||||
const normalizePathForComparison = (p: string) =>
|
||||
process.platform === 'win32' || process.platform === 'darwin'
|
||||
? p.toLowerCase()
|
||||
: p;
|
||||
|
||||
const filteredAbsolutePaths = new Set(
|
||||
filteredPaths.map((p) => path.resolve(this.config.getTargetDir(), p)),
|
||||
filteredPaths.map((p) =>
|
||||
normalizePathForComparison(
|
||||
path.resolve(this.config.getTargetDir(), p),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const filteredEntries = allEntries.filter((entry) =>
|
||||
filteredAbsolutePaths.has(entry.fullpath()),
|
||||
filteredAbsolutePaths.has(normalizePathForComparison(entry.fullpath())),
|
||||
);
|
||||
|
||||
if (!filteredEntries || filteredEntries.length === 0) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user