Merge remote-tracking branch 'origin/main' into feat/npm-extension-installation

This commit is contained in:
tanzhenxin 2026-03-30 19:16:08 +08:00
commit 2c1432a23a
47 changed files with 1056 additions and 253 deletions

View file

@ -54,7 +54,7 @@ export function generateCodingPlanTemplate(
return [
{
id: 'qwen3.5-plus',
name: '[Bailian Coding Plan] qwen3.5-plus',
name: '[ModelStudio Coding Plan] qwen3.5-plus',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -66,7 +66,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'glm-5',
name: '[Bailian Coding Plan] glm-5',
name: '[ModelStudio Coding Plan] glm-5',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -78,7 +78,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'kimi-k2.5',
name: '[Bailian Coding Plan] kimi-k2.5',
name: '[ModelStudio Coding Plan] kimi-k2.5',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -90,7 +90,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'MiniMax-M2.5',
name: '[Bailian Coding Plan] MiniMax-M2.5',
name: '[ModelStudio Coding Plan] MiniMax-M2.5',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -102,7 +102,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'qwen3-coder-plus',
name: '[Bailian Coding Plan] qwen3-coder-plus',
name: '[ModelStudio Coding Plan] qwen3-coder-plus',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -111,7 +111,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'qwen3-coder-next',
name: '[Bailian Coding Plan] qwen3-coder-next',
name: '[ModelStudio Coding Plan] qwen3-coder-next',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -120,7 +120,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'qwen3-max-2026-01-23',
name: '[Bailian Coding Plan] qwen3-max-2026-01-23',
name: '[ModelStudio Coding Plan] qwen3-max-2026-01-23',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -132,7 +132,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'glm-4.7',
name: '[Bailian Coding Plan] glm-4.7',
name: '[ModelStudio Coding Plan] glm-4.7',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -145,11 +145,11 @@ export function generateCodingPlanTemplate(
];
}
// Global region uses Bailian Coding Plan branding for Global/Intl
// Global region uses ModelStudio Coding Plan branding for Global/Intl
return [
{
id: 'qwen3.5-plus',
name: '[Bailian Coding Plan for Global/Intl] qwen3.5-plus',
name: '[ModelStudio Coding Plan for Global/Intl] qwen3.5-plus',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -161,7 +161,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'qwen3-coder-plus',
name: '[Bailian Coding Plan for Global/Intl] qwen3-coder-plus',
name: '[ModelStudio Coding Plan for Global/Intl] qwen3-coder-plus',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -170,7 +170,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'qwen3-coder-next',
name: '[Bailian Coding Plan for Global/Intl] qwen3-coder-next',
name: '[ModelStudio Coding Plan for Global/Intl] qwen3-coder-next',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -179,7 +179,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'qwen3-max-2026-01-23',
name: '[Bailian Coding Plan for Global/Intl] qwen3-max-2026-01-23',
name: '[ModelStudio Coding Plan for Global/Intl] qwen3-max-2026-01-23',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -191,7 +191,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'glm-4.7',
name: '[Bailian Coding Plan for Global/Intl] glm-4.7',
name: '[ModelStudio Coding Plan for Global/Intl] glm-4.7',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -203,7 +203,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'glm-5',
name: '[Bailian Coding Plan for Global/Intl] glm-5',
name: '[ModelStudio Coding Plan for Global/Intl] glm-5',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -215,7 +215,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'MiniMax-M2.5',
name: '[Bailian Coding Plan for Global/Intl] MiniMax-M2.5',
name: '[ModelStudio Coding Plan for Global/Intl] MiniMax-M2.5',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -227,7 +227,7 @@ export function generateCodingPlanTemplate(
},
{
id: 'kimi-k2.5',
name: '[Bailian Coding Plan for Global/Intl] kimi-k2.5',
name: '[ModelStudio Coding Plan for Global/Intl] kimi-k2.5',
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {

View file

@ -1784,8 +1784,8 @@ export default {
// Auth Dialog - View Titles and Labels
// ============================================================================
'Coding Plan': 'Coding Plan',
"Paste your api key of Bailian Coding Plan and you're all set!":
'Fügen Sie Ihren Bailian Coding Plan API-Schlüssel ein und Sie sind bereit!',
"Paste your api key of ModelStudio Coding Plan and you're all set!":
'Fügen Sie Ihren ModelStudio Coding Plan API-Schlüssel ein und Sie sind bereit!',
Custom: 'Benutzerdefiniert',
'More instructions about configuring `modelProviders` manually.':
'Weitere Anweisungen zur manuellen Konfiguration von `modelProviders`.',

View file

@ -1833,8 +1833,8 @@ export default {
// Auth Dialog - View Titles and Labels
// ============================================================================
'Coding Plan': 'Coding Plan',
"Paste your api key of Bailian Coding Plan and you're all set!":
"Paste your api key of Bailian Coding Plan and you're all set!",
"Paste your api key of ModelStudio Coding Plan and you're all set!":
"Paste your api key of ModelStudio Coding Plan and you're all set!",
Custom: 'Custom',
'More instructions about configuring `modelProviders` manually.':
'More instructions about configuring `modelProviders` manually.',

View file

@ -1285,8 +1285,8 @@ export default {
// Auth Dialog - View Titles and Labels
// ============================================================================
'Coding Plan': 'Coding Plan',
"Paste your api key of Bailian Coding Plan and you're all set!":
'Bailian Coding PlanのAPIキーを貼り付けるだけで準備完了です',
"Paste your api key of ModelStudio Coding Plan and you're all set!":
'ModelStudio Coding PlanのAPIキーを貼り付けるだけで準備完了です',
Custom: 'カスタム',
'More instructions about configuring `modelProviders` manually.':
'`modelProviders`を手動で設定する方法の詳細はこちら。',

View file

@ -1777,8 +1777,8 @@ export default {
// Auth Dialog - View Titles and Labels
// ============================================================================
'Coding Plan': 'Coding Plan',
"Paste your api key of Bailian Coding Plan and you're all set!":
'Cole sua chave de API do Bailian Coding Plan e pronto!',
"Paste your api key of ModelStudio Coding Plan and you're all set!":
'Cole sua chave de API do ModelStudio Coding Plan e pronto!',
Custom: 'Personalizado',
'More instructions about configuring `modelProviders` manually.':
'Mais instruções sobre como configurar `modelProviders` manualmente.',

View file

@ -1711,8 +1711,8 @@ export default {
// Auth Dialog - View Titles and Labels
// ============================================================================
'Coding Plan': 'Coding Plan',
"Paste your api key of Bailian Coding Plan and you're all set!":
'Вставьте ваш API-ключ Bailian Coding Plan и всё готово!',
"Paste your api key of ModelStudio Coding Plan and you're all set!":
'Вставьте ваш API-ключ ModelStudio Coding Plan и всё готово!',
Custom: 'Пользовательский',
'More instructions about configuring `modelProviders` manually.':
'Дополнительные инструкции по ручной настройке `modelProviders`.',

View file

@ -1650,7 +1650,7 @@ export default {
// ============================================================================
'API-KEY': 'API-KEY',
'Coding Plan': 'Coding Plan',
"Paste your api key of Bailian Coding Plan and you're all set!":
"Paste your api key of ModelStudio Coding Plan and you're all set!":
'粘贴您的百炼 Coding Plan API Key即可完成设置',
Custom: '自定义',
'More instructions about configuring `modelProviders` manually.':

View file

@ -558,121 +558,4 @@ describe('AuthDialog', () => {
expect(handleAuthSelect).toHaveBeenCalledWith(undefined);
unmount();
});
it('shows API Key subtype menu and opens custom info', async () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
originalSettings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
originalSettings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { stdin, lastFrame, unmount } = renderAuthDialog(settings);
await wait();
// Move from Qwen OAuth -> Coding Plan -> API Key, then enter
stdin.write('\u001B[B');
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
expect(lastFrame()).toContain('Select API Key Type');
expect(lastFrame()).toContain('Alibaba Cloud ModelStudio Standard API Key');
expect(lastFrame()).toContain('Custom API Key');
// Move to Custom API Key and enter
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
expect(lastFrame()).toContain('Custom Configuration');
unmount();
});
it('shows Alibaba Cloud ModelStudio Standard API Key region endpoint', async () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
originalSettings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
originalSettings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { stdin, lastFrame, unmount } = renderAuthDialog(settings, {}, {});
await wait();
// Main -> API Key
stdin.write('\u001B[B');
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
// API Key type -> Alibaba Cloud ModelStudio Standard API Key (default)
stdin.write('\r');
await wait();
// Region -> Singapore
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
expect(lastFrame()).toContain(
'Enter Alibaba Cloud ModelStudio Standard API Key',
);
expect(lastFrame()).toContain(
'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
);
unmount();
});
});

View file

@ -140,6 +140,71 @@ describe('clearCommand', () => {
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
});
it('should clear UI before resetChat for immediate responsiveness', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}
const callOrder: string[] = [];
(mockContext.ui.clear as ReturnType<typeof vi.fn>).mockImplementation(
() => {
callOrder.push('ui.clear');
},
);
mockResetChat.mockImplementation(async () => {
callOrder.push('resetChat');
});
await clearCommand.action(mockContext, '');
// ui.clear should be called before resetChat for immediate UI feedback
const clearIndex = callOrder.indexOf('ui.clear');
const resetIndex = callOrder.indexOf('resetChat');
expect(clearIndex).toBeGreaterThanOrEqual(0);
expect(resetIndex).toBeGreaterThanOrEqual(0);
expect(clearIndex).toBeLessThan(resetIndex);
});
it('should not await hook events (fire-and-forget)', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}
// Make hooks take a long time - they should not block
let sessionEndResolved = false;
let sessionStartResolved = false;
mockFireSessionEndEvent.mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => {
sessionEndResolved = true;
resolve(undefined);
}, 5000);
}),
);
mockFireSessionStartEvent.mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => {
sessionStartResolved = true;
resolve(undefined);
}, 5000);
}),
);
await clearCommand.action(mockContext, '');
// The action should complete immediately without waiting for hooks
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
expect(mockResetChat).toHaveBeenCalledTimes(1);
// Hooks should have been called but not necessarily resolved
expect(mockFireSessionEndEvent).toHaveBeenCalled();
expect(mockFireSessionStartEvent).toHaveBeenCalled();
// Hooks should NOT have resolved yet since they have 5s timeouts
expect(sessionEndResolved).toBe(false);
expect(sessionStartResolved).toBe(false);
});
it('should not attempt to reset chat if config service is not available', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');

View file

@ -27,14 +27,13 @@ export const clearCommand: SlashCommand = {
const { config } = context.services;
if (config) {
// Fire SessionEnd event before clearing (current session ends)
try {
await config
.getHookSystem()
?.fireSessionEndEvent(SessionEndReason.Clear);
} catch (err) {
config.getDebugLogger().warn(`SessionEnd hook failed: ${err}`);
}
// Fire SessionEnd event (non-blocking to avoid UI lag)
config
.getHookSystem()
?.fireSessionEndEvent(SessionEndReason.Clear)
.catch((err) => {
config.getDebugLogger().warn(`SessionEnd hook failed: ${err}`);
});
const newSessionId = config.startNewSession();
@ -54,6 +53,9 @@ export const clearCommand: SlashCommand = {
context.session.startNewSession(newSessionId);
}
// Clear UI first for immediate responsiveness
context.ui.clear();
const geminiClient = config.getGeminiClient();
if (geminiClient) {
context.ui.setDebugMessage(
@ -66,22 +68,20 @@ export const clearCommand: SlashCommand = {
context.ui.setDebugMessage(t('Starting a new session and clearing.'));
}
// Fire SessionStart event after clearing (new session starts)
try {
await config
.getHookSystem()
?.fireSessionStartEvent(
SessionStartSource.Clear,
config.getModel() ?? '',
String(config.getApprovalMode()) as PermissionMode,
);
} catch (err) {
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
}
// Fire SessionStart event (non-blocking to avoid UI lag)
config
.getHookSystem()
?.fireSessionStartEvent(
SessionStartSource.Clear,
config.getModel() ?? '',
String(config.getApprovalMode()) as PermissionMode,
)
.catch((err) => {
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
});
} else {
context.ui.setDebugMessage(t('Starting a new session and clearing.'));
context.ui.clear();
}
context.ui.clear();
},
};

View file

@ -540,7 +540,16 @@ export function KeypressProvider({
}
};
// Matches terminal query responses (DA1, DA2, Kitty protocol query)
// that may arrive late from startup detection in kittyProtocolDetector.
// These are never valid user input.
// eslint-disable-next-line no-control-regex
const TERMINAL_RESPONSE_RE = /^\x1b\[[?>][\d;]*[uc]$/;
const handleKeypress = async (_: unknown, key: Key) => {
if (TERMINAL_RESPONSE_RE.test(key.sequence)) {
return;
}
if (key.sequence === FOCUS_IN || key.sequence === FOCUS_OUT) {
return;
}

View file

@ -37,11 +37,20 @@ export async function detectAndEnableKittyProtocol(): Promise<boolean> {
const onTimeout = () => {
timeoutId = undefined;
process.stdin.removeListener('data', handleData);
if (!originalRawMode) {
process.stdin.setRawMode(false);
}
detectionComplete = true;
resolve(false);
// Keep a drain handler briefly to consume any late-arriving terminal
// responses that would otherwise leak into the application input.
const drainHandler = () => {};
process.stdin.on('data', drainHandler);
setTimeout(() => {
process.stdin.removeListener('data', drainHandler);
if (!originalRawMode) {
process.stdin.setRawMode(false);
}
detectionComplete = true;
resolve(false);
}, 100);
};
const handleData = (data: Buffer) => {