mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
Merge remote-tracking branch 'origin/main' into feat/review-skill-improvements
This commit is contained in:
commit
3364cf880f
72 changed files with 2442 additions and 432 deletions
|
|
@ -1069,6 +1069,7 @@ export async function loadCliConfig(
|
|||
telemetry: telemetrySettings,
|
||||
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
|
||||
fileFiltering: settings.context?.fileFiltering,
|
||||
thinkingIdleThresholdMinutes: settings.context?.gapThresholdMinutes,
|
||||
checkpointing:
|
||||
argv.checkpointing || settings.general?.checkpointing?.enabled,
|
||||
proxy:
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ const SETTINGS_SCHEMA = {
|
|||
label: 'Enable Follow-up Suggestions',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
default: false,
|
||||
description:
|
||||
'Show context-aware follow-up suggestions after task completion. Press Tab or Right Arrow to accept, Enter to accept and submit.',
|
||||
showInDialog: true,
|
||||
|
|
@ -924,6 +924,16 @@ const SETTINGS_SCHEMA = {
|
|||
},
|
||||
},
|
||||
},
|
||||
gapThresholdMinutes: {
|
||||
type: 'number',
|
||||
label: 'Thinking Block Idle Threshold (minutes)',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: 5,
|
||||
description:
|
||||
'Minutes of inactivity after which retained thinking blocks are cleared to free context tokens. Aligns with provider prompt-cache TTL.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,18 @@ export function generateCodingPlanTemplate(
|
|||
// China region uses legacy fields to maintain backward compatibility
|
||||
// This ensures existing users don't get prompted for unnecessary updates
|
||||
return [
|
||||
{
|
||||
id: 'qwen3.6-plus',
|
||||
name: '[ModelStudio Coding Plan] qwen3.6-plus',
|
||||
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
|
||||
envKey: CODING_PLAN_ENV_KEY,
|
||||
generationConfig: {
|
||||
extra_body: {
|
||||
enable_thinking: true,
|
||||
},
|
||||
contextWindowSize: 1000000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'qwen3.5-plus',
|
||||
name: '[ModelStudio Coding Plan] qwen3.5-plus',
|
||||
|
|
@ -147,6 +159,18 @@ export function generateCodingPlanTemplate(
|
|||
|
||||
// Global region uses ModelStudio Coding Plan branding for Global/Intl
|
||||
return [
|
||||
{
|
||||
id: 'qwen3.6-plus',
|
||||
name: '[ModelStudio Coding Plan for Global/Intl] qwen3.6-plus',
|
||||
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
|
||||
envKey: CODING_PLAN_ENV_KEY,
|
||||
generationConfig: {
|
||||
extra_body: {
|
||||
enable_thinking: true,
|
||||
},
|
||||
contextWindowSize: 1000000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'qwen3.5-plus',
|
||||
name: '[ModelStudio Coding Plan for Global/Intl] qwen3.5-plus',
|
||||
|
|
|
|||
|
|
@ -1973,4 +1973,15 @@ export default {
|
|||
'Vollständige Tool-Ausgabe und Denkprozess im ausführlichen Modus anzeigen (mit Strg+O umschalten).',
|
||||
'Press Ctrl+O to show full tool output':
|
||||
'Strg+O für vollständige Tool-Ausgabe drücken',
|
||||
|
||||
'Switch to plan mode or exit plan mode':
|
||||
'Switch to plan mode or exit plan mode',
|
||||
'Exited plan mode. Previous approval mode restored.':
|
||||
'Exited plan mode. Previous approval mode restored.',
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.':
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.':
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.',
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.':
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2013,4 +2013,15 @@ export default {
|
|||
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).',
|
||||
'Press Ctrl+O to show full tool output':
|
||||
'Press Ctrl+O to show full tool output',
|
||||
|
||||
'Switch to plan mode or exit plan mode':
|
||||
'Switch to plan mode or exit plan mode',
|
||||
'Exited plan mode. Previous approval mode restored.':
|
||||
'Exited plan mode. Previous approval mode restored.',
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.':
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.':
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.',
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.':
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1464,4 +1464,15 @@ export default {
|
|||
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
|
||||
'詳細モードで完全なツール出力と思考を表示します(Ctrl+O で切り替え)。',
|
||||
'Press Ctrl+O to show full tool output': 'Ctrl+O で完全なツール出力を表示',
|
||||
|
||||
'Switch to plan mode or exit plan mode':
|
||||
'Switch to plan mode or exit plan mode',
|
||||
'Exited plan mode. Previous approval mode restored.':
|
||||
'Exited plan mode. Previous approval mode restored.',
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.':
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.':
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.',
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.':
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1963,4 +1963,15 @@ export default {
|
|||
'Mostrar saída completa da ferramenta e raciocínio no modo detalhado (alternar com Ctrl+O).',
|
||||
'Press Ctrl+O to show full tool output':
|
||||
'Pressione Ctrl+O para exibir a saída completa da ferramenta',
|
||||
|
||||
'Switch to plan mode or exit plan mode':
|
||||
'Switch to plan mode or exit plan mode',
|
||||
'Exited plan mode. Previous approval mode restored.':
|
||||
'Exited plan mode. Previous approval mode restored.',
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.':
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.':
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.',
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.':
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1970,4 +1970,15 @@ export default {
|
|||
'Показывать полный вывод инструментов и процесс рассуждений в подробном режиме (переключить с помощью Ctrl+O).',
|
||||
'Press Ctrl+O to show full tool output':
|
||||
'Нажмите Ctrl+O для показа полного вывода инструментов',
|
||||
|
||||
'Switch to plan mode or exit plan mode':
|
||||
'Switch to plan mode or exit plan mode',
|
||||
'Exited plan mode. Previous approval mode restored.':
|
||||
'Exited plan mode. Previous approval mode restored.',
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.':
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.':
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.',
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.':
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1817,4 +1817,14 @@ export default {
|
|||
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
|
||||
'详细模式下显示完整工具输出和思考过程(Ctrl+O 切换)。',
|
||||
'Press Ctrl+O to show full tool output': '按 Ctrl+O 查看详细工具调用结果',
|
||||
|
||||
'Switch to plan mode or exit plan mode': '切换到计划模式或退出计划模式',
|
||||
'Exited plan mode. Previous approval mode restored.':
|
||||
'已退出计划模式,已恢复之前的审批模式。',
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.':
|
||||
'启用计划模式。智能体将只分析和规划,而不执行工具。',
|
||||
'Already in plan mode. Use "/plan exit" to exit plan mode.':
|
||||
'已处于计划模式。使用 "/plan exit" 退出计划模式。',
|
||||
'Not in plan mode. Use "/plan" to enter plan mode first.':
|
||||
'未处于计划模式。请先使用 "/plan" 进入计划模式。',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { languageCommand } from '../ui/commands/languageCommand.js';
|
|||
import { mcpCommand } from '../ui/commands/mcpCommand.js';
|
||||
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||
import { modelCommand } from '../ui/commands/modelCommand.js';
|
||||
import { planCommand } from '../ui/commands/planCommand.js';
|
||||
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
|
||||
import { trustCommand } from '../ui/commands/trustCommand.js';
|
||||
import { quitCommand } from '../ui/commands/quitCommand.js';
|
||||
|
|
@ -103,6 +104,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
|||
mcpCommand,
|
||||
memoryCommand,
|
||||
modelCommand,
|
||||
planCommand,
|
||||
permissionsCommand,
|
||||
...(this.config?.getFolderTrust() ? [trustCommand] : []),
|
||||
quitCommand,
|
||||
|
|
|
|||
|
|
@ -1113,7 +1113,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
|
||||
// Generate prompt suggestions when streaming completes
|
||||
const followupSuggestionsEnabled =
|
||||
settings.merged.ui?.enableFollowupSuggestions !== false;
|
||||
settings.merged.ui?.enableFollowupSuggestions === true;
|
||||
|
||||
useEffect(() => {
|
||||
// Clear suggestion when feature is disabled at runtime
|
||||
|
|
|
|||
159
packages/cli/src/ui/commands/planCommand.test.ts
Normal file
159
packages/cli/src/ui/commands/planCommand.test.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { planCommand } from './planCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
|
||||
describe('planCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPrePlanMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as import('@qwen-code/qwen-code-core').Config,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should switch to plan mode if not in plan mode', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
const result = await planCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return submit prompt if arguments are provided when switching to plan mode', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
const result = await planCommand.action(mockContext, 'refactor the code');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: 'refactor the code' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return already in plan mode if mode is already plan', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
|
||||
const result = await planCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Already in plan mode. Use "/plan exit" to exit plan mode.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return submit prompt if arguments are provided and already in plan mode', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
|
||||
const result = await planCommand.action(mockContext, 'keep planning');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: 'keep planning' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should exit plan mode when exit argument is passed', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
|
||||
const result = await planCommand.action(mockContext, 'exit');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Exited plan mode. Previous approval mode restored.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should restore pre-plan mode when executing from plan mode', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
(mockContext.services.config?.getPrePlanMode as Mock).mockReturnValue(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
|
||||
const result = await planCommand.action(mockContext, 'exit');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Exited plan mode. Previous approval mode restored.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when execute is used but not in plan mode', async () => {
|
||||
if (!planCommand.action) {
|
||||
throw new Error('The plan command must have an action.');
|
||||
}
|
||||
|
||||
// Default mock returns ApprovalMode.DEFAULT (not PLAN)
|
||||
const result = await planCommand.action(mockContext, 'exit');
|
||||
|
||||
expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Not in plan mode. Use "/plan" to enter plan mode first.',
|
||||
});
|
||||
});
|
||||
});
|
||||
104
packages/cli/src/ui/commands/planCommand.ts
Normal file
104
packages/cli/src/ui/commands/planCommand.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
type CommandContext,
|
||||
CommandKind,
|
||||
type SlashCommand,
|
||||
type MessageActionReturn,
|
||||
type SubmitPromptActionReturn,
|
||||
} from './types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export const planCommand: SlashCommand = {
|
||||
name: 'plan',
|
||||
get description() {
|
||||
return t('Switch to plan mode or exit plan mode');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn | SubmitPromptActionReturn> => {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Configuration is not available.'),
|
||||
};
|
||||
}
|
||||
|
||||
const trimmedArgs = args.trim();
|
||||
const currentMode = config.getApprovalMode();
|
||||
|
||||
if (trimmedArgs === 'exit') {
|
||||
if (currentMode !== ApprovalMode.PLAN) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Not in plan mode. Use "/plan" to enter plan mode first.'),
|
||||
};
|
||||
}
|
||||
try {
|
||||
config.setApprovalMode(config.getPrePlanMode());
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: (e as Error).message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t('Exited plan mode. Previous approval mode restored.'),
|
||||
};
|
||||
}
|
||||
|
||||
if (currentMode !== ApprovalMode.PLAN) {
|
||||
try {
|
||||
config.setApprovalMode(ApprovalMode.PLAN);
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: (e as Error).message,
|
||||
};
|
||||
}
|
||||
|
||||
if (trimmedArgs) {
|
||||
return {
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: trimmedArgs }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t(
|
||||
'Enabled plan mode. The agent will analyze and plan without executing tools.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Already in plan mode
|
||||
if (trimmedArgs) {
|
||||
return {
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: trimmedArgs }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t('Already in plan mode. Use "/plan exit" to exit plan mode.'),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -849,7 +849,6 @@ describe('InputPrompt', () => {
|
|||
// Verify useCompletion was called with correct signature
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -878,7 +877,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -907,7 +905,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -936,7 +933,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -965,7 +961,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -995,7 +990,6 @@ describe('InputPrompt', () => {
|
|||
// Verify useCompletion was called with the buffer
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1024,7 +1018,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1054,7 +1047,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1084,7 +1076,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1114,7 +1105,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1144,7 +1134,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1176,7 +1165,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1206,7 +1194,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
@ -1238,7 +1225,6 @@ describe('InputPrompt', () => {
|
|||
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
|
|
|||
|
|
@ -168,15 +168,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const [dirs, setDirs] = useState<readonly string[]>(
|
||||
config.getWorkspaceContext().getDirectories(),
|
||||
);
|
||||
const dirsChanged = config.getWorkspaceContext().getDirectories();
|
||||
useEffect(() => {
|
||||
if (dirs.length !== dirsChanged.length) {
|
||||
setDirs(dirsChanged);
|
||||
}
|
||||
}, [dirs.length, dirsChanged]);
|
||||
const [reverseSearchActive, setReverseSearchActive] = useState(false);
|
||||
const [commandSearchActive, setCommandSearchActive] = useState(false);
|
||||
const [textBeforeReverseSearch, setTextBeforeReverseSearch] = useState('');
|
||||
|
|
@ -190,7 +181,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
|
||||
const completion = useCommandCompletion(
|
||||
buffer,
|
||||
dirs,
|
||||
config.getTargetDir(),
|
||||
slashCommands,
|
||||
commandContext,
|
||||
|
|
|
|||
|
|
@ -189,13 +189,35 @@ export function KeypressProvider({
|
|||
clearKittyTimeout();
|
||||
kittySequenceTimeout = setTimeout(() => {
|
||||
if (kittySequenceBufferRef.current) {
|
||||
if (debugKeystrokeLogging) {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Kitty buffer timeout, clearing:',
|
||||
kittySequenceBufferRef.current,
|
||||
);
|
||||
// Before discarding, try to salvage any parseable sequences
|
||||
// that may have been missed (e.g., due to chunked input).
|
||||
while (kittySequenceBufferRef.current) {
|
||||
const parsed = parseKittyPrefix(kittySequenceBufferRef.current);
|
||||
if (parsed) {
|
||||
kittySequenceBufferRef.current =
|
||||
kittySequenceBufferRef.current.slice(parsed.length);
|
||||
broadcast(parsed.key);
|
||||
continue;
|
||||
}
|
||||
const plain = parsePlainTextPrefix(kittySequenceBufferRef.current);
|
||||
if (plain) {
|
||||
kittySequenceBufferRef.current =
|
||||
kittySequenceBufferRef.current.slice(plain.length);
|
||||
broadcast(plain.key);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Clear any remaining unparseable content
|
||||
if (kittySequenceBufferRef.current) {
|
||||
if (debugKeystrokeLogging) {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Kitty buffer timeout, clearing:',
|
||||
kittySequenceBufferRef.current,
|
||||
);
|
||||
}
|
||||
kittySequenceBufferRef.current = '';
|
||||
}
|
||||
kittySequenceBufferRef.current = '';
|
||||
}
|
||||
}, KITTY_SEQUENCE_TIMEOUT_MS);
|
||||
};
|
||||
|
|
@ -331,14 +353,19 @@ export function KeypressProvider({
|
|||
};
|
||||
}
|
||||
|
||||
// 3) CSI-u form: ESC [ <code> ; <mods> (u|~)
|
||||
// 3) CSI-u and tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
|
||||
// 3) CSI-u form: ESC [ <code>[:<shifted>][:<base>] ; <mods>[:<event>] [; <text>] (u|~)
|
||||
// 3) CSI-u and tilde-coded functional keys with optional kitty extensions:
|
||||
// Full kitty format: ESC [ code:shifted:base ; mods:event ; text u
|
||||
// 'u' terminator: Kitty CSI-u; '~' terminator: tilde-coded function keys.
|
||||
const csiUPrefix = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])`);
|
||||
// The colon-separated fields (shifted key, base key, event type, text)
|
||||
// are optional extensions that some terminals send.
|
||||
const csiUPrefix = new RegExp(
|
||||
`^${ESC}\\[(\\d+)(?::\\d+)*(?:;(\\d+)(?::\\d+)*)?(?:;\\d+)?([u~])`,
|
||||
);
|
||||
m = buffer.match(csiUPrefix);
|
||||
if (m) {
|
||||
const keyCode = parseInt(m[1], 10);
|
||||
let modifiers = m[3] ? parseInt(m[3], 10) : KITTY_MODIFIER_BASE;
|
||||
let modifiers = m[2] ? parseInt(m[2], 10) : KITTY_MODIFIER_BASE;
|
||||
if (modifiers >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||||
modifiers -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||||
}
|
||||
|
|
@ -347,7 +374,7 @@ export function KeypressProvider({
|
|||
(modifierBits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
|
||||
const alt = (modifierBits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||||
const ctrl = (modifierBits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||||
const terminator = m[4];
|
||||
const terminator = m[3];
|
||||
|
||||
// Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End)
|
||||
if (terminator === '~') {
|
||||
|
|
|
|||
|
|
@ -391,9 +391,9 @@ describe('useCodingPlanUpdates', () => {
|
|||
>;
|
||||
|
||||
// Should have new China configs + custom config only (global config removed since regions are mutually exclusive)
|
||||
// The China template has 8 models, so we expect 8 (from template) + 1 (custom) = 9
|
||||
// The China template has 9 models, so we expect 9 (from template) + 1 (custom) = 10
|
||||
// Note: description field has been removed, only name field contains the branding
|
||||
expect(updatedConfigs.length).toBe(9);
|
||||
expect(updatedConfigs.length).toBe(10);
|
||||
|
||||
// Should NOT contain the Global config (mutually exclusive)
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ const setupMocks = ({
|
|||
describe('useCommandCompletion', () => {
|
||||
const mockCommandContext = {} as CommandContext;
|
||||
const mockConfig = {} as Config;
|
||||
const testDirs: string[] = [];
|
||||
const testRootDir = '/';
|
||||
|
||||
// Helper to create real TextBuffer objects within renderHook
|
||||
|
|
@ -114,7 +113,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(''),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -139,7 +137,6 @@ describe('useCommandCompletion', () => {
|
|||
const textBuffer = useTextBufferForTest('@file');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -172,7 +169,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('@files'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -200,7 +196,6 @@ describe('useCommandCompletion', () => {
|
|||
renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(text),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -226,7 +221,6 @@ describe('useCommandCompletion', () => {
|
|||
renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(text, cursorOffset),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -265,7 +259,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -286,7 +279,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -306,7 +298,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -332,7 +323,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -361,7 +351,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -398,7 +387,6 @@ describe('useCommandCompletion', () => {
|
|||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -427,7 +415,6 @@ describe('useCommandCompletion', () => {
|
|||
renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(text),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -455,7 +442,6 @@ describe('useCommandCompletion', () => {
|
|||
renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(text),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -484,7 +470,6 @@ describe('useCommandCompletion', () => {
|
|||
const textBuffer = useTextBufferForTest(text);
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -517,7 +502,6 @@ describe('useCommandCompletion', () => {
|
|||
const textBuffer = useTextBufferForTest('/mem');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -547,7 +531,6 @@ describe('useCommandCompletion', () => {
|
|||
const textBuffer = useTextBufferForTest('@src/fi');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
@ -580,7 +563,6 @@ describe('useCommandCompletion', () => {
|
|||
const textBuffer = useTextBufferForTest(text, cursorOffset);
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export interface UseCommandCompletionReturn {
|
|||
|
||||
export function useCommandCompletion(
|
||||
buffer: TextBuffer,
|
||||
dirs: readonly string[],
|
||||
cwd: string,
|
||||
slashCommands: readonly SlashCommand[],
|
||||
commandContext: CommandContext,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue