merge main branch functionalities

This commit is contained in:
koalazf.99
2025-08-02 14:47:37 +08:00
parent b69b2ce376
commit 23f6ae8513
30 changed files with 1378 additions and 420 deletions

View File

@@ -6,7 +6,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as os from 'os';
import { loadCliConfig, parseArguments } from './config.js';
import { loadCliConfig, parseArguments, CliArgs } from './config.js';
import { Settings } from './settings.js';
import { Extension } from './extension.js';
import * as ServerConfig from '@qwen-code/qwen-code-core';
@@ -561,6 +561,68 @@ describe('mergeMcpServers', () => {
});
});
describe('loadCliConfig systemPromptMappings', () => {
it('should use default systemPromptMappings when not provided in settings', async () => {
const mockSettings: Settings = {
theme: 'dark',
};
const mockExtensions: Extension[] = [];
const mockSessionId = 'test-session';
const mockArgv: CliArgs = {
model: 'test-model',
} as CliArgs;
const config = await loadCliConfig(
mockSettings,
mockExtensions,
mockSessionId,
mockArgv,
);
expect(config.getSystemPromptMappings()).toEqual([
{
baseUrls: [
'https://dashscope.aliyuncs.com/compatible-mode/v1/',
'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/',
],
modelNames: ['qwen3-coder-plus'],
template:
'SYSTEM_TEMPLATE:{"name":"qwen3_coder","params":{"is_git_repository":{RUNTIME_VARS_IS_GIT_REPO},"sandbox":"{RUNTIME_VARS_SANDBOX}"}}',
},
]);
});
it('should use custom systemPromptMappings when provided in settings', async () => {
const customSystemPromptMappings = [
{
baseUrls: ['https://custom-api.com'],
modelNames: ['custom-model'],
template: 'Custom template',
},
];
const mockSettings: Settings = {
theme: 'dark',
systemPromptMappings: customSystemPromptMappings,
};
const mockExtensions: Extension[] = [];
const mockSessionId = 'test-session';
const mockArgv: CliArgs = {
model: 'test-model',
} as CliArgs;
const config = await loadCliConfig(
mockSettings,
mockExtensions,
mockSessionId,
mockArgv,
);
expect(config.getSystemPromptMappings()).toEqual(
customSystemPromptMappings,
);
});
});
describe('mergeExcludeTools', () => {
it('should merge excludeTools from settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };

View File

@@ -441,6 +441,8 @@ export async function loadCliConfig(
model: argv.model!,
extensionContextFilePaths,
maxSessionTurns: settings.maxSessionTurns ?? -1,
sessionTokenLimit: settings.sessionTokenLimit ?? 32000,
maxFolderItems: settings.maxFolderItems ?? 20,
experimentalAcp: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
extensions: allExtensions,
@@ -465,6 +467,7 @@ export async function loadCliConfig(
'SYSTEM_TEMPLATE:{"name":"qwen3_coder","params":{"is_git_repository":{RUNTIME_VARS_IS_GIT_REPO},"sandbox":"{RUNTIME_VARS_SANDBOX}"}}',
},
],
contentGenerator: settings.contentGenerator,
});
}

View File

@@ -95,6 +95,12 @@ export interface Settings {
// Setting for setting maximum number of user/model/tool turns in a session.
maxSessionTurns?: number;
// Setting for maximum token limit for conversation history before blocking requests
sessionTokenLimit?: number;
// Setting for maximum number of files and folders to show in folder structure
maxFolderItems?: number;
// A map of tool names to their summarization settings.
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
@@ -109,6 +115,10 @@ export interface Settings {
modelNames: string[];
template: string;
}>;
contentGenerator?: {
timeout?: number;
maxRetries?: number;
};
}
export interface SettingsError {

View File

@@ -69,7 +69,9 @@ export const createMockCommandContext = (
byName: {},
},
},
promptCount: 0,
} as SessionStatsState,
resetSession: vi.fn(),
},
};

View File

@@ -55,6 +55,7 @@ describe('clearCommand', () => {
expect(mockContext.ui.setDebugMessage).toHaveBeenCalledTimes(1);
expect(mockResetChat).toHaveBeenCalledTimes(1);
expect(mockContext.session.resetSession).toHaveBeenCalledTimes(1);
expect(uiTelemetryService.resetLastPromptTokenCount).toHaveBeenCalledTimes(
1,
);
@@ -64,6 +65,8 @@ describe('clearCommand', () => {
const setDebugMessageOrder = (mockContext.ui.setDebugMessage as Mock).mock
.invocationCallOrder[0];
const resetChatOrder = mockResetChat.mock.invocationCallOrder[0];
const resetSessionOrder = (mockContext.session.resetSession as Mock).mock
.invocationCallOrder[0];
const resetTelemetryOrder = (
uiTelemetryService.resetLastPromptTokenCount as Mock
).mock.invocationCallOrder[0];
@@ -73,6 +76,8 @@ describe('clearCommand', () => {
expect(setDebugMessageOrder).toBeLessThan(resetChatOrder);
expect(resetChatOrder).toBeLessThan(resetTelemetryOrder);
expect(resetTelemetryOrder).toBeLessThan(clearOrder);
expect(resetChatOrder).toBeLessThan(resetSessionOrder);
expect(resetSessionOrder).toBeLessThan(clearOrder);
});
it('should not attempt to reset chat if config service is not available', async () => {
@@ -92,6 +97,7 @@ describe('clearCommand', () => {
'Clearing terminal.',
);
expect(mockResetChat).not.toHaveBeenCalled();
expect(nullConfigContext.session.resetSession).toHaveBeenCalledTimes(1);
expect(uiTelemetryService.resetLastPromptTokenCount).toHaveBeenCalledTimes(
1,
);

View File

@@ -24,6 +24,7 @@ export const clearCommand: SlashCommand = {
}
uiTelemetryService.resetLastPromptTokenCount();
context.session.resetSession();
context.ui.clear();
},
};

View File

@@ -15,10 +15,10 @@ import { MessageType } from '../types.js';
export const docsCommand: SlashCommand = {
name: 'docs',
description: 'open full Gemini CLI documentation in your browser',
description: 'open full Qwen Code documentation in your browser',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext): Promise<void> => {
const docsUrl = 'https://goo.gle/gemini-cli-docs';
const docsUrl = 'https://github.com/QwenLM/qwen-code';
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
context.ui.addItem(

View File

@@ -13,7 +13,7 @@ import { MessageType } from '../types.js';
export const toolsCommand: SlashCommand = {
name: 'tools',
description: 'list available Gemini CLI tools',
description: 'list available Qwen Codetools',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext, args?: string): Promise<void> => {
const subCommand = args?.trim();
@@ -40,7 +40,7 @@ export const toolsCommand: SlashCommand = {
// Filter out MCP tools by checking for the absence of a serverName property
const geminiTools = tools.filter((tool) => !('serverName' in tool));
let message = 'Available Gemini CLI tools:\n\n';
let message = 'Available Qwen Code tools:\n\n';
if (geminiTools.length > 0) {
geminiTools.forEach((tool) => {

View File

@@ -63,6 +63,7 @@ export interface CommandContext {
// Session-specific data
session: {
stats: SessionStatsState;
resetSession: () => void;
/** A transient list of shell commands the user has approved for this session. */
sessionShellAllowlist: Set<string>;
};

View File

@@ -50,6 +50,7 @@ interface SessionStatsContextValue {
stats: SessionStatsState;
startNewPrompt: () => void;
getPromptCount: () => number;
resetSession: () => void;
}
// --- Context Definition ---
@@ -109,13 +110,23 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
[stats.promptCount],
);
const resetSession = useCallback(() => {
setStats({
sessionStartTime: new Date(),
metrics: uiTelemetryService.getMetrics(),
lastPromptTokenCount: uiTelemetryService.getLastPromptTokenCount(),
promptCount: 0,
});
}, []);
const value = useMemo(
() => ({
stats,
startNewPrompt,
getPromptCount,
resetSession,
}),
[stats, startNewPrompt, getPromptCount],
[stats, startNewPrompt, getPromptCount, resetSession],
);
return (

View File

@@ -162,6 +162,7 @@ export const useSlashCommandProcessor = (
session: {
stats: session.stats,
sessionShellAllowlist,
resetSession: session.resetSession,
},
}),
[
@@ -174,6 +175,7 @@ export const useSlashCommandProcessor = (
clearItems,
refreshStatic,
session.stats,
session.resetSession,
onDebugMessage,
pendingCompressionItemRef,
setPendingCompressionItem,

View File

@@ -511,6 +511,23 @@ export const useGeminiStream = (
[addItem, config],
);
const handleSessionTokenLimitExceededEvent = useCallback(
(value: { currentTokens: number; limit: number; message: string }) =>
addItem(
{
type: 'error',
text:
`🚫 Session token limit exceeded: ${value.currentTokens.toLocaleString()} tokens > ${value.limit.toLocaleString()} limit.\n\n` +
`💡 Solutions:\n` +
` • Start a new session: Use /clear command\n` +
` • Increase limit: Add "sessionTokenLimit": (e.g., 128000) to your settings.json\n` +
` • Compress history: Use /compress command to compress history`,
},
Date.now(),
),
[addItem],
);
const handleLoopDetectedEvent = useCallback(() => {
addItem(
{
@@ -560,6 +577,9 @@ export const useGeminiStream = (
case ServerGeminiEventType.MaxSessionTurns:
handleMaxSessionTurnsEvent();
break;
case ServerGeminiEventType.SessionTokenLimitExceeded:
handleSessionTokenLimitExceededEvent(event.value);
break;
case ServerGeminiEventType.Finished:
handleFinishedEvent(
event as ServerGeminiFinishedEvent,
@@ -591,6 +611,7 @@ export const useGeminiStream = (
handleChatCompressionEvent,
handleFinishedEvent,
handleMaxSessionTurnsEvent,
handleSessionTokenLimitExceededEvent,
],
);

View File

@@ -34,7 +34,7 @@ export async function checkForUpdates(): Promise<string | null> {
notifier.update &&
semver.gt(notifier.update.latest, notifier.update.current)
) {
return `Gemini CLI update available! ${notifier.update.current}${notifier.update.latest}\nRun npm install -g ${packageJson.name} to update`;
return `Qwen Code update available! ${notifier.update.current}${notifier.update.latest}\nRun npm install -g ${packageJson.name} to update`;
}
return null;

View File

@@ -24,7 +24,7 @@ const homeDirectoryCheck: WarningCheck = {
]);
if (workspaceRealPath === homeRealPath) {
return 'You are running Gemini CLI in your home directory. It is recommended to run in a project-specific directory.';
return 'You are running Qwen Code in your home directory. It is recommended to run in a project-specific directory.';
}
return null;
} catch (_err: unknown) {