mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: update description
This commit is contained in:
@@ -19,7 +19,8 @@
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:ci": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check-i18n": "tsx ../../scripts/check-i18n.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -109,6 +109,8 @@ export default {
|
||||
'install required IDE companion for {{ideName}}',
|
||||
'enable IDE integration': 'enable IDE integration',
|
||||
'disable IDE integration': 'disable IDE integration',
|
||||
'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.':
|
||||
'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.',
|
||||
'Set up GitHub Actions': 'Set up GitHub Actions',
|
||||
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)':
|
||||
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)',
|
||||
@@ -220,6 +222,12 @@ export default {
|
||||
'Project memory is currently empty.': 'Project memory is currently empty.',
|
||||
'Refreshing memory from source files...':
|
||||
'Refreshing memory from source files...',
|
||||
'Add content to the memory. Use --global for global memory or --project for project memory.':
|
||||
'Add content to the memory. Use --global for global memory or --project for project memory.',
|
||||
'Usage: /memory add [--global|--project] <text to remember>':
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
'Attempting to save to memory {{scope}}: "{{fact}}"':
|
||||
'Attempting to save to memory {{scope}}: "{{fact}}"',
|
||||
|
||||
// ============================================================================
|
||||
// Commands - MCP
|
||||
@@ -287,6 +295,8 @@ export default {
|
||||
'Error sharing conversation: {{error}}',
|
||||
'Conversation shared to {{filePath}}': 'Conversation shared to {{filePath}}',
|
||||
'No conversation found to share.': 'No conversation found to share.',
|
||||
'Share the current conversation to a markdown or json file. Usage: /chat share <file>':
|
||||
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
|
||||
|
||||
// ============================================================================
|
||||
// Commands - Summary
|
||||
@@ -344,6 +354,27 @@ export default {
|
||||
No: 'No',
|
||||
'No (esc)': 'No (esc)',
|
||||
'Yes, allow always for this session': 'Yes, allow always for this session',
|
||||
'Modify in progress:': 'Modify in progress:',
|
||||
'Save and close external editor to continue':
|
||||
'Save and close external editor to continue',
|
||||
'Apply this change?': 'Apply this change?',
|
||||
'Yes, allow always': 'Yes, allow always',
|
||||
'Modify with external editor': 'Modify with external editor',
|
||||
'No, suggest changes (esc)': 'No, suggest changes (esc)',
|
||||
"Allow execution of: '{{command}}'?": "Allow execution of: '{{command}}'?",
|
||||
'Yes, allow always ...': 'Yes, allow always ...',
|
||||
'Yes, and auto-accept edits': 'Yes, and auto-accept edits',
|
||||
'Yes, and manually approve edits': 'Yes, and manually approve edits',
|
||||
'No, keep planning (esc)': 'No, keep planning (esc)',
|
||||
'URLs to fetch:': 'URLs to fetch:',
|
||||
'MCP Server: {{server}}': 'MCP Server: {{server}}',
|
||||
'Tool: {{tool}}': 'Tool: {{tool}}',
|
||||
'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?':
|
||||
'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?',
|
||||
'Yes, always allow tool "{{tool}}" from server "{{server}}"':
|
||||
'Yes, always allow tool "{{tool}}" from server "{{server}}"',
|
||||
'Yes, always allow all tools from server "{{server}}"':
|
||||
'Yes, always allow all tools from server "{{server}}"',
|
||||
|
||||
// ============================================================================
|
||||
// Dialogs - Shell Confirmation
|
||||
@@ -427,6 +458,16 @@ export default {
|
||||
'Waiting for Qwen OAuth authentication...':
|
||||
'Waiting for Qwen OAuth authentication...',
|
||||
|
||||
// ============================================================================
|
||||
// Dialogs - Model
|
||||
// ============================================================================
|
||||
'Select Model': 'Select Model',
|
||||
'(Press Esc to close)': '(Press Esc to close)',
|
||||
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)':
|
||||
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)':
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
|
||||
|
||||
// ============================================================================
|
||||
// Dialogs - Permissions
|
||||
// ============================================================================
|
||||
@@ -471,6 +512,29 @@ export default {
|
||||
'{{count}} prompt': '{{count}} prompt',
|
||||
'{{count}} prompts': '{{count}} prompts',
|
||||
'(from {{extensionName}})': '(from {{extensionName}})',
|
||||
OAuth: 'OAuth',
|
||||
'OAuth expired': 'OAuth expired',
|
||||
'OAuth not authenticated': 'OAuth not authenticated',
|
||||
'tools and prompts will appear when ready':
|
||||
'tools and prompts will appear when ready',
|
||||
'{{count}} tools cached': '{{count}} tools cached',
|
||||
'Tools:': 'Tools:',
|
||||
'Parameters:': 'Parameters:',
|
||||
'Prompts:': 'Prompts:',
|
||||
Blocked: 'Blocked',
|
||||
'💡 Tips:': '💡 Tips:',
|
||||
Use: 'Use',
|
||||
'to show server and tool descriptions':
|
||||
'to show server and tool descriptions',
|
||||
'to show tool parameter schemas': 'to show tool parameter schemas',
|
||||
'to hide descriptions': 'to hide descriptions',
|
||||
'to authenticate with OAuth-enabled servers':
|
||||
'to authenticate with OAuth-enabled servers',
|
||||
Press: 'Press',
|
||||
'to toggle tool descriptions on/off': 'to toggle tool descriptions on/off',
|
||||
"Starting OAuth authentication for MCP server '{{name}}'...":
|
||||
"Starting OAuth authentication for MCP server '{{name}}'...",
|
||||
'Restarting MCP servers...': 'Restarting MCP servers...',
|
||||
|
||||
// ============================================================================
|
||||
// Startup Tips
|
||||
|
||||
@@ -103,6 +103,8 @@ export default {
|
||||
'安装 {{ideName}} 所需的 IDE 配套工具',
|
||||
'enable IDE integration': '启用 IDE 集成',
|
||||
'disable IDE integration': '禁用 IDE 集成',
|
||||
'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.':
|
||||
'您当前环境不支持 IDE 集成。要使用此功能,请在以下支持的 IDE 之一中运行 Qwen Code:VS Code 或 VS Code 分支版本。',
|
||||
'Set up GitHub Actions': '设置 GitHub Actions',
|
||||
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)':
|
||||
'配置终端按键绑定以支持多行输入(VS Code、Cursor、Windsurf)',
|
||||
@@ -204,6 +206,12 @@ export default {
|
||||
'项目记忆内容来自 {{path}}:\n\n---\n{{content}}\n---',
|
||||
'Project memory is currently empty.': '项目记忆当前为空',
|
||||
'Refreshing memory from source files...': '正在从源文件刷新记忆...',
|
||||
'Add content to the memory. Use --global for global memory or --project for project memory.':
|
||||
'添加内容到记忆。使用 --global 表示全局记忆,使用 --project 表示项目记忆',
|
||||
'Usage: /memory add [--global|--project] <text to remember>':
|
||||
'用法:/memory add [--global|--project] <要记住的文本>',
|
||||
'Attempting to save to memory {{scope}}: "{{fact}}"':
|
||||
'正在尝试保存到记忆 {{scope}}:"{{fact}}"',
|
||||
|
||||
// ============================================================================
|
||||
// Commands - MCP
|
||||
@@ -266,6 +274,8 @@ export default {
|
||||
'Error sharing conversation: {{error}}': '分享对话时出错:{{error}}',
|
||||
'Conversation shared to {{filePath}}': '对话已分享到 {{filePath}}',
|
||||
'No conversation found to share.': '未找到要分享的对话',
|
||||
'Share the current conversation to a markdown or json file. Usage: /chat share <file>':
|
||||
'将当前对话分享到 markdown 或 json 文件。用法:/chat share <file>',
|
||||
|
||||
// ============================================================================
|
||||
// Commands - Summary
|
||||
@@ -320,6 +330,26 @@ export default {
|
||||
No: '否',
|
||||
'No (esc)': '否 (esc)',
|
||||
'Yes, allow always for this session': '是,本次会话总是允许',
|
||||
'Modify in progress:': '正在修改:',
|
||||
'Save and close external editor to continue': '保存并关闭外部编辑器以继续',
|
||||
'Apply this change?': '是否应用此更改?',
|
||||
'Yes, allow always': '是,总是允许',
|
||||
'Modify with external editor': '使用外部编辑器修改',
|
||||
'No, suggest changes (esc)': '否,建议更改 (esc)',
|
||||
"Allow execution of: '{{command}}'?": "允许执行:'{{command}}'?",
|
||||
'Yes, allow always ...': '是,总是允许 ...',
|
||||
'Yes, and auto-accept edits': '是,并自动接受编辑',
|
||||
'Yes, and manually approve edits': '是,并手动批准编辑',
|
||||
'No, keep planning (esc)': '否,继续规划 (esc)',
|
||||
'URLs to fetch:': '要获取的 URL:',
|
||||
'MCP Server: {{server}}': 'MCP 服务器:{{server}}',
|
||||
'Tool: {{tool}}': '工具:{{tool}}',
|
||||
'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?':
|
||||
'允许执行来自服务器 "{{server}}" 的 MCP 工具 "{{tool}}"?',
|
||||
'Yes, always allow tool "{{tool}}" from server "{{server}}"':
|
||||
'是,总是允许来自服务器 "{{server}}" 的工具 "{{tool}}"',
|
||||
'Yes, always allow all tools from server "{{server}}"':
|
||||
'是,总是允许来自服务器 "{{server}}" 的所有工具',
|
||||
|
||||
// ============================================================================
|
||||
// Dialogs - Shell Confirmation
|
||||
@@ -394,6 +424,16 @@ export default {
|
||||
'按任意键返回认证类型选择',
|
||||
'Waiting for Qwen OAuth authentication...': '正在等待 Qwen OAuth 认证...',
|
||||
|
||||
// ============================================================================
|
||||
// Dialogs - Model
|
||||
// ============================================================================
|
||||
'Select Model': '选择模型',
|
||||
'(Press Esc to close)': '(按 Esc 关闭)',
|
||||
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)':
|
||||
'来自阿里云 ModelStudio 的最新 Qwen Coder 模型(版本:qwen3-coder-plus-2025-09-23)',
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)':
|
||||
'来自阿里云 ModelStudio 的最新 Qwen Vision 模型(版本:qwen3-vl-plus-2025-09-23)',
|
||||
|
||||
// ============================================================================
|
||||
// Dialogs - Permissions
|
||||
// ============================================================================
|
||||
@@ -438,6 +478,27 @@ export default {
|
||||
'{{count}} prompt': '{{count}} 个提示',
|
||||
'{{count}} prompts': '{{count}} 个提示',
|
||||
'(from {{extensionName}})': '(来自 {{extensionName}})',
|
||||
OAuth: 'OAuth',
|
||||
'OAuth expired': 'OAuth 已过期',
|
||||
'OAuth not authenticated': 'OAuth 未认证',
|
||||
'tools and prompts will appear when ready': '工具和提示将在就绪时显示',
|
||||
'{{count}} tools cached': '{{count}} 个工具已缓存',
|
||||
'Tools:': '工具:',
|
||||
'Parameters:': '参数:',
|
||||
'Prompts:': '提示:',
|
||||
Blocked: '已阻止',
|
||||
'💡 Tips:': '💡 提示:',
|
||||
Use: '使用',
|
||||
'to show server and tool descriptions': '显示服务器和工具描述',
|
||||
'to show tool parameter schemas': '显示工具参数架构',
|
||||
'to hide descriptions': '隐藏描述',
|
||||
'to authenticate with OAuth-enabled servers':
|
||||
'使用支持 OAuth 的服务器进行认证',
|
||||
Press: '按',
|
||||
'to toggle tool descriptions on/off': '切换工具描述开关',
|
||||
"Starting OAuth authentication for MCP server '{{name}}'...":
|
||||
"正在为 MCP 服务器 '{{name}}' 启动 OAuth 认证...",
|
||||
'Restarting MCP servers...': '正在重启 MCP 服务器...',
|
||||
|
||||
// ============================================================================
|
||||
// Startup Tips
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type {
|
||||
CommandContext,
|
||||
SlashCommand,
|
||||
@@ -20,6 +19,7 @@ import path from 'node:path';
|
||||
import type { HistoryItemWithoutId } from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ChatDetail {
|
||||
name: string;
|
||||
@@ -67,7 +67,9 @@ const getSavedChatTags = async (
|
||||
|
||||
const listCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
description: 'List saved conversation checkpoints',
|
||||
get description() {
|
||||
return t('List saved conversation checkpoints');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context): Promise<MessageActionReturn> => {
|
||||
const chatDetails = await getSavedChatTags(context, false);
|
||||
@@ -75,7 +77,7 @@ const listCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No saved conversation checkpoints found.',
|
||||
content: t('No saved conversation checkpoints found.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ const listCommand: SlashCommand = {
|
||||
...chatDetails.map((chat) => chat.name.length),
|
||||
);
|
||||
|
||||
let message = 'List of saved conversations:\n\n';
|
||||
let message = t('List of saved conversations:') + '\n\n';
|
||||
for (const chat of chatDetails) {
|
||||
const paddedName = chat.name.padEnd(maxNameLength, ' ');
|
||||
const isoString = chat.mtime.toISOString();
|
||||
@@ -91,7 +93,7 @@ const listCommand: SlashCommand = {
|
||||
const formattedDate = match ? `${match[1]} ${match[2]}` : 'Invalid Date';
|
||||
message += ` - ${paddedName} (saved on ${formattedDate})\n`;
|
||||
}
|
||||
message += `\nNote: Newest last, oldest first`;
|
||||
message += `\n${t('Note: Newest last, oldest first')}`;
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -102,8 +104,11 @@ const listCommand: SlashCommand = {
|
||||
|
||||
const saveCommand: SlashCommand = {
|
||||
name: 'save',
|
||||
description:
|
||||
get description() {
|
||||
return t(
|
||||
'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
|
||||
);
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context, args): Promise<SlashCommandActionReturn | void> => {
|
||||
const tag = args.trim();
|
||||
@@ -111,7 +116,7 @@ const saveCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Missing tag. Usage: /chat save <tag>',
|
||||
content: t('Missing tag. Usage: /chat save <tag>'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,9 +131,12 @@ const saveCommand: SlashCommand = {
|
||||
prompt: React.createElement(
|
||||
Text,
|
||||
null,
|
||||
'A checkpoint with the tag ',
|
||||
React.createElement(Text, { color: theme.text.accent }, tag),
|
||||
' already exists. Do you want to overwrite it?',
|
||||
t(
|
||||
'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?',
|
||||
{
|
||||
tag,
|
||||
},
|
||||
),
|
||||
),
|
||||
originalInvocation: {
|
||||
raw: context.invocation?.raw || `/chat save ${tag}`,
|
||||
@@ -142,7 +150,7 @@ const saveCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No chat client available to save conversation.',
|
||||
content: t('No chat client available to save conversation.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,13 +160,15 @@ const saveCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation checkpoint saved with tag: ${decodeTagName(tag)}.`,
|
||||
content: t('Conversation checkpoint saved with tag: {{tag}}.', {
|
||||
tag: decodeTagName(tag),
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No conversation found to save.',
|
||||
content: t('No conversation found to save.'),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -167,8 +177,11 @@ const saveCommand: SlashCommand = {
|
||||
const resumeCommand: SlashCommand = {
|
||||
name: 'resume',
|
||||
altNames: ['load'],
|
||||
description:
|
||||
get description() {
|
||||
return t(
|
||||
'Resume a conversation from a checkpoint. Usage: /chat resume <tag>',
|
||||
);
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context, args) => {
|
||||
const tag = args.trim();
|
||||
@@ -176,7 +189,7 @@ const resumeCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Missing tag. Usage: /chat resume <tag>',
|
||||
content: t('Missing tag. Usage: /chat resume <tag>'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,7 +201,9 @@ const resumeCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `No saved checkpoint found with tag: ${decodeTagName(tag)}.`,
|
||||
content: t('No saved checkpoint found with tag: {{tag}}.', {
|
||||
tag: decodeTagName(tag),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,7 +252,9 @@ const resumeCommand: SlashCommand = {
|
||||
|
||||
const deleteCommand: SlashCommand = {
|
||||
name: 'delete',
|
||||
description: 'Delete a conversation checkpoint. Usage: /chat delete <tag>',
|
||||
get description() {
|
||||
return t('Delete a conversation checkpoint. Usage: /chat delete <tag>');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context, args): Promise<MessageActionReturn> => {
|
||||
const tag = args.trim();
|
||||
@@ -245,7 +262,7 @@ const deleteCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Missing tag. Usage: /chat delete <tag>',
|
||||
content: t('Missing tag. Usage: /chat delete <tag>'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,13 +274,17 @@ const deleteCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation checkpoint '${decodeTagName(tag)}' has been deleted.`,
|
||||
content: t("Conversation checkpoint '{{tag}}' has been deleted.", {
|
||||
tag: decodeTagName(tag),
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Error: No checkpoint found with tag '${decodeTagName(tag)}'.`,
|
||||
content: t("Error: No checkpoint found with tag '{{tag}}'.", {
|
||||
tag: decodeTagName(tag),
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -309,8 +330,11 @@ export function serializeHistoryToMarkdown(history: Content[]): string {
|
||||
|
||||
const shareCommand: SlashCommand = {
|
||||
name: 'share',
|
||||
description:
|
||||
get description() {
|
||||
return t(
|
||||
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
|
||||
);
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context, args): Promise<MessageActionReturn> => {
|
||||
let filePathArg = args.trim();
|
||||
@@ -324,7 +348,7 @@ const shareCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Invalid file format. Only .md and .json are supported.',
|
||||
content: t('Invalid file format. Only .md and .json are supported.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -333,7 +357,7 @@ const shareCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No chat client available to share conversation.',
|
||||
content: t('No chat client available to share conversation.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -346,7 +370,7 @@ const shareCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No conversation found to share.',
|
||||
content: t('No conversation found to share.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -362,14 +386,18 @@ const shareCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation shared to ${filePath}`,
|
||||
content: t('Conversation shared to {{filePath}}', {
|
||||
filePath,
|
||||
}),
|
||||
};
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Error sharing conversation: ${errorMessage}`,
|
||||
content: t('Error sharing conversation: {{error}}', {
|
||||
error: errorMessage,
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -377,7 +405,9 @@ const shareCommand: SlashCommand = {
|
||||
|
||||
export const chatCommand: SlashCommand = {
|
||||
name: 'chat',
|
||||
description: 'Manage conversation history.',
|
||||
get description() {
|
||||
return t('Manage conversation history.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
listCommand,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type SlashCommand,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
async function listAction(context: CommandContext) {
|
||||
context.ui.addItem(
|
||||
@@ -131,14 +132,18 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
|
||||
const listExtensionsCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
description: 'List active extensions',
|
||||
get description() {
|
||||
return t('List active extensions');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: listAction,
|
||||
};
|
||||
|
||||
const updateExtensionsCommand: SlashCommand = {
|
||||
name: 'update',
|
||||
description: 'Update extensions. Usage: update <extension-names>|--all',
|
||||
get description() {
|
||||
return t('Update extensions. Usage: update <extension-names>|--all');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: updateAction,
|
||||
completion: async (context, partialArg) => {
|
||||
@@ -158,7 +163,9 @@ const updateExtensionsCommand: SlashCommand = {
|
||||
|
||||
export const extensionsCommand: SlashCommand = {
|
||||
name: 'extensions',
|
||||
description: 'Manage extensions',
|
||||
get description() {
|
||||
return t('Manage extensions');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [listExtensionsCommand, updateExtensionsCommand],
|
||||
action: (context, args) =>
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
} from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
function getIdeStatusMessage(ideClient: IdeClient): {
|
||||
messageType: 'info' | 'error';
|
||||
@@ -138,27 +139,35 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
if (!currentIDE) {
|
||||
return {
|
||||
name: 'ide',
|
||||
description: 'manage IDE integration',
|
||||
get description() {
|
||||
return t('manage IDE integration');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (): SlashCommandActionReturn =>
|
||||
({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.`,
|
||||
content: t(
|
||||
'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.',
|
||||
),
|
||||
}) as const,
|
||||
};
|
||||
}
|
||||
|
||||
const ideSlashCommand: SlashCommand = {
|
||||
name: 'ide',
|
||||
description: 'manage IDE integration',
|
||||
get description() {
|
||||
return t('manage IDE integration');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [],
|
||||
};
|
||||
|
||||
const statusCommand: SlashCommand = {
|
||||
name: 'status',
|
||||
description: 'check status of IDE integration',
|
||||
get description() {
|
||||
return t('check status of IDE integration');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (): Promise<SlashCommandActionReturn> => {
|
||||
const { messageType, content } =
|
||||
@@ -173,7 +182,12 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
|
||||
const installCommand: SlashCommand = {
|
||||
name: 'install',
|
||||
description: `install required IDE companion for ${ideClient.getDetectedIdeDisplayName()}`,
|
||||
get description() {
|
||||
const ideName = ideClient.getDetectedIdeDisplayName() ?? 'IDE';
|
||||
return t('install required IDE companion for {{ideName}}', {
|
||||
ideName,
|
||||
});
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
const installer = getIdeInstaller(currentIDE);
|
||||
@@ -246,7 +260,9 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
|
||||
const enableCommand: SlashCommand = {
|
||||
name: 'enable',
|
||||
description: 'enable IDE integration',
|
||||
get description() {
|
||||
return t('enable IDE integration');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext) => {
|
||||
context.services.settings.setValue(
|
||||
@@ -268,7 +284,9 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
|
||||
const disableCommand: SlashCommand = {
|
||||
name: 'disable',
|
||||
description: 'disable IDE integration',
|
||||
get description() {
|
||||
return t('disable IDE integration');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext) => {
|
||||
context.services.settings.setValue(
|
||||
|
||||
@@ -92,7 +92,12 @@ const authCommand: SlashCommand = {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: `Starting OAuth authentication for MCP server '${serverName}'...`,
|
||||
text: t(
|
||||
"Starting OAuth authentication for MCP server '{{name}}'...",
|
||||
{
|
||||
name: serverName,
|
||||
},
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -208,7 +213,7 @@ const listCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Could not retrieve tool registry.',
|
||||
content: t('Could not retrieve tool registry.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -320,14 +325,14 @@ const refreshCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Could not retrieve tool registry.',
|
||||
content: t('Could not retrieve tool registry.'),
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Restarting MCP servers...',
|
||||
text: t('Restarting MCP servers...'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -15,15 +15,20 @@ import fs from 'fs/promises';
|
||||
import { MessageType } from '../types.js';
|
||||
import type { SlashCommand, SlashCommandActionReturn } from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const memoryCommand: SlashCommand = {
|
||||
name: 'memory',
|
||||
description: 'Commands for interacting with memory.',
|
||||
get description() {
|
||||
return t('Commands for interacting with memory.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'show',
|
||||
description: 'Show the current memory contents.',
|
||||
get description() {
|
||||
return t('Show the current memory contents.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
const memoryContent = context.services.config?.getUserMemory() || '';
|
||||
@@ -31,8 +36,8 @@ export const memoryCommand: SlashCommand = {
|
||||
|
||||
const messageContent =
|
||||
memoryContent.length > 0
|
||||
? `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---`
|
||||
: 'Memory is currently empty.';
|
||||
? `${t('Current memory content from {{count}} file(s):', { count: String(fileCount) })}\n\n---\n${memoryContent}\n---`
|
||||
: t('Memory is currently empty.');
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
@@ -45,7 +50,9 @@ export const memoryCommand: SlashCommand = {
|
||||
subCommands: [
|
||||
{
|
||||
name: '--project',
|
||||
description: 'Show project-level memory contents.',
|
||||
get description() {
|
||||
return t('Show project-level memory contents.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
try {
|
||||
@@ -57,8 +64,14 @@ export const memoryCommand: SlashCommand = {
|
||||
|
||||
const messageContent =
|
||||
memoryContent.trim().length > 0
|
||||
? `Project memory content from ${projectMemoryPath}:\n\n---\n${memoryContent}\n---`
|
||||
: 'Project memory is currently empty.';
|
||||
? t(
|
||||
'Project memory content from {{path}}:\n\n---\n{{content}}\n---',
|
||||
{
|
||||
path: projectMemoryPath,
|
||||
content: memoryContent,
|
||||
},
|
||||
)
|
||||
: t('Project memory is currently empty.');
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
@@ -71,7 +84,9 @@ export const memoryCommand: SlashCommand = {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Project memory file not found or is currently empty.',
|
||||
text: t(
|
||||
'Project memory file not found or is currently empty.',
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -80,7 +95,9 @@ export const memoryCommand: SlashCommand = {
|
||||
},
|
||||
{
|
||||
name: '--global',
|
||||
description: 'Show global memory contents.',
|
||||
get description() {
|
||||
return t('Show global memory contents.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
try {
|
||||
@@ -96,8 +113,10 @@ export const memoryCommand: SlashCommand = {
|
||||
|
||||
const messageContent =
|
||||
globalMemoryContent.trim().length > 0
|
||||
? `Global memory content:\n\n---\n${globalMemoryContent}\n---`
|
||||
: 'Global memory is currently empty.';
|
||||
? t('Global memory content:\n\n---\n{{content}}\n---', {
|
||||
content: globalMemoryContent,
|
||||
})
|
||||
: t('Global memory is currently empty.');
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
@@ -110,7 +129,9 @@ export const memoryCommand: SlashCommand = {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Global memory file not found or is currently empty.',
|
||||
text: t(
|
||||
'Global memory file not found or is currently empty.',
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -121,16 +142,20 @@ export const memoryCommand: SlashCommand = {
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
description:
|
||||
get description() {
|
||||
return t(
|
||||
'Add content to the memory. Use --global for global memory or --project for project memory.',
|
||||
);
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
content: t(
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,8 +175,9 @@ export const memoryCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
content: t(
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
),
|
||||
};
|
||||
} else {
|
||||
// No scope specified, will be handled by the tool
|
||||
@@ -162,8 +188,9 @@ export const memoryCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
content: t(
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,7 +198,10 @@ export const memoryCommand: SlashCommand = {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory ${scopeText}: "${fact}"`,
|
||||
text: t('Attempting to save to memory {{scope}}: "{{fact}}"', {
|
||||
scope: scopeText,
|
||||
fact,
|
||||
}),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -185,21 +215,25 @@ export const memoryCommand: SlashCommand = {
|
||||
subCommands: [
|
||||
{
|
||||
name: '--project',
|
||||
description: 'Add content to project-level memory.',
|
||||
get description() {
|
||||
return t('Add content to project-level memory.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add --project <text to remember>',
|
||||
content: t('Usage: /memory add --project <text to remember>'),
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to project memory: "${args.trim()}"`,
|
||||
text: t('Attempting to save to project memory: "{{text}}"', {
|
||||
text: args.trim(),
|
||||
}),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -213,21 +247,25 @@ export const memoryCommand: SlashCommand = {
|
||||
},
|
||||
{
|
||||
name: '--global',
|
||||
description: 'Add content to global memory.',
|
||||
get description() {
|
||||
return t('Add content to global memory.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add --global <text to remember>',
|
||||
content: t('Usage: /memory add --global <text to remember>'),
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to global memory: "${args.trim()}"`,
|
||||
text: t('Attempting to save to global memory: "{{text}}"', {
|
||||
text: args.trim(),
|
||||
}),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -243,13 +281,15 @@ export const memoryCommand: SlashCommand = {
|
||||
},
|
||||
{
|
||||
name: 'refresh',
|
||||
description: 'Refresh the memory from the source.',
|
||||
get description() {
|
||||
return t('Refresh the memory from the source.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Refreshing memory from source files...',
|
||||
text: t('Refreshing memory from source files...'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -12,10 +12,13 @@ import type {
|
||||
} from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { getAvailableModelsForAuthType } from '../models/availableModels.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const modelCommand: SlashCommand = {
|
||||
name: 'model',
|
||||
description: 'Switch the model for this session',
|
||||
get description() {
|
||||
return t('Switch the model for this session');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
@@ -36,7 +39,7 @@ export const modelCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Content generator configuration not available.',
|
||||
content: t('Content generator configuration not available.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ export const modelCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Authentication type not available.',
|
||||
content: t('Authentication type not available.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +58,12 @@ export const modelCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `No models available for the current authentication type (${authType}).`,
|
||||
content: t(
|
||||
'No models available for the current authentication type ({{authType}}).',
|
||||
{
|
||||
authType,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,16 @@ import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { type McpClient, MCPServerStatus } from '@qwen-code/qwen-code-core';
|
||||
import { GeminiSpinner } from './GeminiRespondingSpinner.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const ConfigInitDisplay = () => {
|
||||
const config = useConfig();
|
||||
const [message, setMessage] = useState('Initializing...');
|
||||
const [message, setMessage] = useState(t('Initializing...'));
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (clients?: Map<string, McpClient>) => {
|
||||
if (!clients || clients.size === 0) {
|
||||
setMessage(`Initializing...`);
|
||||
setMessage(t('Initializing...'));
|
||||
return;
|
||||
}
|
||||
let connected = 0;
|
||||
@@ -28,7 +29,12 @@ export const ConfigInitDisplay = () => {
|
||||
connected++;
|
||||
}
|
||||
}
|
||||
setMessage(`Connecting to MCP servers... (${connected}/${clients.size})`);
|
||||
setMessage(
|
||||
t('Connecting to MCP servers... ({{connected}}/{{total}})', {
|
||||
connected: String(connected),
|
||||
total: String(clients.size),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
appEvents.on('mcp-client-update', onChange);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ContextSummaryDisplayProps {
|
||||
geminiMdFileCount: number;
|
||||
@@ -50,9 +51,11 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
if (openFileCount === 0) {
|
||||
return '';
|
||||
}
|
||||
return `${openFileCount} open file${
|
||||
openFileCount > 1 ? 's' : ''
|
||||
} (ctrl+g to view)`;
|
||||
const fileText =
|
||||
openFileCount === 1
|
||||
? t('{{count}} open file', { count: String(openFileCount) })
|
||||
: t('{{count}} open files', { count: String(openFileCount) });
|
||||
return `${fileText} ${t('(ctrl+g to view)')}`;
|
||||
})();
|
||||
|
||||
const geminiMdText = (() => {
|
||||
@@ -61,9 +64,15 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
}
|
||||
const allNamesTheSame = new Set(contextFileNames).size < 2;
|
||||
const name = allNamesTheSame ? contextFileNames[0] : 'context';
|
||||
return `${geminiMdFileCount} ${name} file${
|
||||
geminiMdFileCount > 1 ? 's' : ''
|
||||
}`;
|
||||
return geminiMdFileCount === 1
|
||||
? t('{{count}} {{name}} file', {
|
||||
count: String(geminiMdFileCount),
|
||||
name,
|
||||
})
|
||||
: t('{{count}} {{name}} files', {
|
||||
count: String(geminiMdFileCount),
|
||||
name,
|
||||
});
|
||||
})();
|
||||
|
||||
const mcpText = (() => {
|
||||
@@ -73,15 +82,27 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
|
||||
const parts = [];
|
||||
if (mcpServerCount > 0) {
|
||||
parts.push(
|
||||
`${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}`,
|
||||
);
|
||||
const serverText =
|
||||
mcpServerCount === 1
|
||||
? t('{{count}} MCP server', { count: String(mcpServerCount) })
|
||||
: t('{{count}} MCP servers', { count: String(mcpServerCount) });
|
||||
parts.push(serverText);
|
||||
}
|
||||
|
||||
if (blockedMcpServerCount > 0) {
|
||||
let blockedText = `${blockedMcpServerCount} Blocked`;
|
||||
let blockedText = t('{{count}} Blocked', {
|
||||
count: String(blockedMcpServerCount),
|
||||
});
|
||||
if (mcpServerCount === 0) {
|
||||
blockedText += ` MCP server${blockedMcpServerCount > 1 ? 's' : ''}`;
|
||||
const serverText =
|
||||
blockedMcpServerCount === 1
|
||||
? t('{{count}} MCP server', {
|
||||
count: String(blockedMcpServerCount),
|
||||
})
|
||||
: t('{{count}} MCP servers', {
|
||||
count: String(blockedMcpServerCount),
|
||||
});
|
||||
blockedText += ` ${serverText}`;
|
||||
}
|
||||
parts.push(blockedText);
|
||||
}
|
||||
@@ -89,9 +110,9 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
// Add ctrl+t hint when MCP servers are available
|
||||
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
||||
if (showToolDescriptions) {
|
||||
text += ' (ctrl+t to toggle)';
|
||||
text += ` ${t('(ctrl+t to toggle)')}`;
|
||||
} else {
|
||||
text += ' (ctrl+t to view)';
|
||||
text += ` ${t('(ctrl+t to view)')}`;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
@@ -102,7 +123,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
if (isNarrow) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>Using:</Text>
|
||||
<Text color={theme.text.secondary}>{t('Using:')}</Text>
|
||||
{summaryParts.map((part, index) => (
|
||||
<Text key={index} color={theme.text.secondary}>
|
||||
{' '}- {part}
|
||||
@@ -115,7 +136,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>
|
||||
Using: {summaryParts.join(' | ')}
|
||||
{t('Using:')} {summaryParts.join(' | ')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { type SlashCommand, CommandKind } from '../commands/types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface Help {
|
||||
commands: readonly SlashCommand[];
|
||||
@@ -23,46 +24,41 @@ export const Help: React.FC<Help> = ({ commands }) => (
|
||||
>
|
||||
{/* Basics */}
|
||||
<Text bold color={theme.text.primary}>
|
||||
Basics:
|
||||
{t('Basics:')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Add context
|
||||
{t('Add context')}
|
||||
</Text>
|
||||
: Use{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
@
|
||||
</Text>{' '}
|
||||
to specify files for context (e.g.,{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
@src/myFile.ts
|
||||
</Text>
|
||||
) to target specific files or folders.
|
||||
:{' '}
|
||||
{t(
|
||||
'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.',
|
||||
{
|
||||
symbol: t('@'),
|
||||
example: t('@src/myFile.ts'),
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Shell mode
|
||||
{t('Shell mode')}
|
||||
</Text>
|
||||
: Execute shell commands via{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
!
|
||||
</Text>{' '}
|
||||
(e.g.,{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
!npm run start
|
||||
</Text>
|
||||
) or use natural language (e.g.{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
start server
|
||||
</Text>
|
||||
).
|
||||
:{' '}
|
||||
{t(
|
||||
'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).',
|
||||
{
|
||||
symbol: t('!'),
|
||||
example1: t('!npm run start'),
|
||||
example2: t('start server'),
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Commands */}
|
||||
<Text bold color={theme.text.primary}>
|
||||
Commands:
|
||||
{t('Commands:')}
|
||||
</Text>
|
||||
{commands
|
||||
.filter((command) => command.description && !command.hidden)
|
||||
@@ -97,81 +93,81 @@ export const Help: React.FC<Help> = ({ commands }) => (
|
||||
{' '}
|
||||
!{' '}
|
||||
</Text>
|
||||
- shell command
|
||||
- {t('shell command')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={theme.text.secondary}>[MCP]</Text> - Model Context Protocol
|
||||
command (from external servers)
|
||||
<Text color={theme.text.secondary}>[MCP]</Text> -{' '}
|
||||
{t('Model Context Protocol command (from external servers)')}
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Shortcuts */}
|
||||
<Text bold color={theme.text.primary}>
|
||||
Keyboard Shortcuts:
|
||||
{t('Keyboard Shortcuts:')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Alt+Left/Right
|
||||
</Text>{' '}
|
||||
- Jump through words in the input
|
||||
- {t('Jump through words in the input')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+C
|
||||
</Text>{' '}
|
||||
- Close dialogs, cancel requests, or quit application
|
||||
- {t('Close dialogs, cancel requests, or quit application')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
|
||||
</Text>{' '}
|
||||
-{' '}
|
||||
{process.platform === 'linux'
|
||||
? '- New line (Alt+Enter works for certain linux distros)'
|
||||
: '- New line'}
|
||||
? t('New line (Alt+Enter works for certain linux distros)')
|
||||
: t('New line')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+L
|
||||
</Text>{' '}
|
||||
- Clear the screen
|
||||
- {t('Clear the screen')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'}
|
||||
</Text>{' '}
|
||||
- Open input in external editor
|
||||
- {t('Open input in external editor')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Enter
|
||||
</Text>{' '}
|
||||
- Send message
|
||||
- {t('Send message')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Esc
|
||||
</Text>{' '}
|
||||
- Cancel operation / Clear input (double press)
|
||||
- {t('Cancel operation / Clear input (double press)')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Shift+Tab
|
||||
</Text>{' '}
|
||||
- Cycle approval modes
|
||||
- {t('Cycle approval modes')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Up/Down
|
||||
</Text>{' '}
|
||||
- Cycle through your prompt history
|
||||
- {t('Cycle through your prompt history')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
<Text color={theme.text.primary}>
|
||||
For a full list of shortcuts, see{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
docs/keyboard-shortcuts.md
|
||||
</Text>
|
||||
{t('For a full list of shortcuts, see {{docPath}}', {
|
||||
docPath: t('docs/keyboard-shortcuts.md'),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getAvailableModelsForAuthType,
|
||||
MAINLINE_CODER,
|
||||
} from '../models/availableModels.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ModelDialogProps {
|
||||
onClose: () => void;
|
||||
@@ -87,7 +88,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
padding={1}
|
||||
width="100%"
|
||||
>
|
||||
<Text bold>Select Model</Text>
|
||||
<Text bold>{t('Select Model')}</Text>
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={MODEL_OPTIONS}
|
||||
@@ -97,7 +98,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>(Press Esc to close)</Text>
|
||||
<Text color={theme.text.secondary}>{t('(Press Esc to close)')}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
confirmationDetails: ToolCallConfirmationDetails;
|
||||
@@ -105,17 +106,17 @@ export const ToolConfirmationMessage: React.FC<
|
||||
const compactOptions: Array<RadioSelectItem<ToolConfirmationOutcome>> = [
|
||||
{
|
||||
key: 'proceed-once',
|
||||
label: 'Yes, allow once',
|
||||
label: t('Yes, allow once'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
{
|
||||
key: 'proceed-always',
|
||||
label: 'Allow always',
|
||||
label: t('Allow always'),
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
},
|
||||
{
|
||||
key: 'cancel',
|
||||
label: 'No',
|
||||
label: t('No'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
},
|
||||
];
|
||||
@@ -123,7 +124,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text wrap="truncate">Do you want to proceed?</Text>
|
||||
<Text wrap="truncate">{t('Do you want to proceed?')}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<RadioButtonSelect
|
||||
@@ -185,37 +186,37 @@ export const ToolConfirmationMessage: React.FC<
|
||||
padding={1}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Text color={theme.text.primary}>Modify in progress: </Text>
|
||||
<Text color={theme.text.primary}>{t('Modify in progress:')} </Text>
|
||||
<Text color={theme.status.success}>
|
||||
Save and close external editor to continue
|
||||
{t('Save and close external editor to continue')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
question = `Apply this change?`;
|
||||
question = t('Apply this change?');
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
label: t('Yes, allow once'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Yes, allow always',
|
||||
label: t('Yes, allow always'),
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always',
|
||||
});
|
||||
}
|
||||
if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) {
|
||||
options.push({
|
||||
label: 'Modify with external editor',
|
||||
label: t('Modify with external editor'),
|
||||
value: ToolConfirmationOutcome.ModifyWithEditor,
|
||||
key: 'Modify with external editor',
|
||||
});
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
label: t('No, suggest changes (esc)'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
@@ -232,21 +233,23 @@ export const ToolConfirmationMessage: React.FC<
|
||||
const executionProps =
|
||||
confirmationDetails as ToolExecuteConfirmationDetails;
|
||||
|
||||
question = `Allow execution of: '${executionProps.rootCommand}'?`;
|
||||
question = t("Allow execution of: '{{command}}'?", {
|
||||
command: executionProps.rootCommand,
|
||||
});
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
label: t('Yes, allow once'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: `Yes, allow always ...`,
|
||||
label: t('Yes, allow always ...'),
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: `Yes, allow always ...`,
|
||||
key: 'Yes, allow always ...',
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
label: t('No, suggest changes (esc)'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
@@ -275,17 +278,17 @@ export const ToolConfirmationMessage: React.FC<
|
||||
question = planProps.title;
|
||||
options.push({
|
||||
key: 'proceed-always',
|
||||
label: 'Yes, and auto-accept edits',
|
||||
label: t('Yes, and auto-accept edits'),
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
});
|
||||
options.push({
|
||||
key: 'proceed-once',
|
||||
label: 'Yes, and manually approve edits',
|
||||
label: t('Yes, and manually approve edits'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
});
|
||||
options.push({
|
||||
key: 'cancel',
|
||||
label: 'No, keep planning (esc)',
|
||||
label: t('No, keep planning (esc)'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
});
|
||||
|
||||
@@ -305,21 +308,21 @@ export const ToolConfirmationMessage: React.FC<
|
||||
infoProps.urls &&
|
||||
!(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
|
||||
|
||||
question = `Do you want to proceed?`;
|
||||
question = t('Do you want to proceed?');
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
label: t('Yes, allow once'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Yes, allow always',
|
||||
label: t('Yes, allow always'),
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always',
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
label: t('No, suggest changes (esc)'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
@@ -331,7 +334,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
</Text>
|
||||
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.primary}>URLs to fetch:</Text>
|
||||
<Text color={theme.text.primary}>{t('URLs to fetch:')}</Text>
|
||||
{infoProps.urls.map((url) => (
|
||||
<Text key={url}>
|
||||
{' '}
|
||||
@@ -348,31 +351,46 @@ export const ToolConfirmationMessage: React.FC<
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
||||
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text>
|
||||
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text>
|
||||
<Text color={theme.text.link}>
|
||||
{t('MCP Server: {{server}}', { server: mcpProps.serverName })}
|
||||
</Text>
|
||||
<Text color={theme.text.link}>
|
||||
{t('Tool: {{tool}}', { tool: mcpProps.toolName })}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
||||
question = t(
|
||||
'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?',
|
||||
{
|
||||
tool: mcpProps.toolName,
|
||||
server: mcpProps.serverName,
|
||||
},
|
||||
);
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
label: t('Yes, allow once'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||
label: t('Yes, always allow tool "{{tool}}" from server "{{server}}"', {
|
||||
tool: mcpProps.toolName,
|
||||
server: mcpProps.serverName,
|
||||
}),
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
|
||||
key: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||
});
|
||||
options.push({
|
||||
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||
label: t('Yes, always allow all tools from server "{{server}}"', {
|
||||
server: mcpProps.serverName,
|
||||
}),
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
key: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
label: t('No, suggest changes (esc)'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
JsonMcpPrompt,
|
||||
JsonMcpTool,
|
||||
} from '../../types.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface McpStatusProps {
|
||||
servers: Record<string, MCPServerConfig>;
|
||||
@@ -47,13 +48,13 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
if (serverNames.length === 0 && blockedServers.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>No MCP servers configured.</Text>
|
||||
<Text>{t('No MCP servers configured.')}</Text>
|
||||
<Text>
|
||||
Please view MCP documentation in your browser:{' '}
|
||||
{t('Please view MCP documentation in your browser:')}{' '}
|
||||
<Text color={theme.text.link}>
|
||||
https://goo.gle/gemini-cli-docs-mcp
|
||||
</Text>{' '}
|
||||
or use the cli /docs command
|
||||
{t('or use the cli /docs command')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -64,17 +65,19 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
{discoveryInProgress && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
⏳ MCP servers are starting up ({connectingServers.length}{' '}
|
||||
initializing)...
|
||||
{t('⏳ MCP servers are starting up ({{count}} initializing)...', {
|
||||
count: String(connectingServers.length),
|
||||
})}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
Note: First startup may take longer. Tool availability will update
|
||||
automatically.
|
||||
{t(
|
||||
'Note: First startup may take longer. Tool availability will update automatically.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Text bold>Configured MCP servers:</Text>
|
||||
<Text bold>{t('Configured MCP servers:')}</Text>
|
||||
<Box height={1} />
|
||||
|
||||
{serverNames.map((serverName) => {
|
||||
@@ -100,50 +103,61 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
switch (status) {
|
||||
case MCPServerStatus.CONNECTED:
|
||||
statusIndicator = '🟢';
|
||||
statusText = 'Ready';
|
||||
statusText = t('Ready');
|
||||
statusColor = theme.status.success;
|
||||
break;
|
||||
case MCPServerStatus.CONNECTING:
|
||||
statusIndicator = '🔄';
|
||||
statusText = 'Starting... (first startup may take longer)';
|
||||
statusText = t('Starting... (first startup may take longer)');
|
||||
statusColor = theme.status.warning;
|
||||
break;
|
||||
case MCPServerStatus.DISCONNECTED:
|
||||
default:
|
||||
statusIndicator = '🔴';
|
||||
statusText = 'Disconnected';
|
||||
statusText = t('Disconnected');
|
||||
statusColor = theme.status.error;
|
||||
break;
|
||||
}
|
||||
|
||||
let serverDisplayName = serverName;
|
||||
if (server.extensionName) {
|
||||
serverDisplayName += ` (from ${server.extensionName})`;
|
||||
serverDisplayName += ` ${t('(from {{extensionName}})', {
|
||||
extensionName: server.extensionName,
|
||||
})}`;
|
||||
}
|
||||
|
||||
const toolCount = serverTools.length;
|
||||
const promptCount = serverPrompts.length;
|
||||
const parts = [];
|
||||
if (toolCount > 0) {
|
||||
parts.push(`${toolCount} ${toolCount === 1 ? 'tool' : 'tools'}`);
|
||||
parts.push(
|
||||
toolCount === 1
|
||||
? t('{{count}} tool', { count: String(toolCount) })
|
||||
: t('{{count}} tools', { count: String(toolCount) }),
|
||||
);
|
||||
}
|
||||
if (promptCount > 0) {
|
||||
parts.push(
|
||||
`${promptCount} ${promptCount === 1 ? 'prompt' : 'prompts'}`,
|
||||
promptCount === 1
|
||||
? t('{{count}} prompt', { count: String(promptCount) })
|
||||
: t('{{count}} prompts', { count: String(promptCount) }),
|
||||
);
|
||||
}
|
||||
|
||||
const serverAuthStatus = authStatus[serverName];
|
||||
let authStatusNode: React.ReactNode = null;
|
||||
if (serverAuthStatus === 'authenticated') {
|
||||
authStatusNode = <Text> (OAuth)</Text>;
|
||||
authStatusNode = <Text> ({t('OAuth')})</Text>;
|
||||
} else if (serverAuthStatus === 'expired') {
|
||||
authStatusNode = (
|
||||
<Text color={theme.status.error}> (OAuth expired)</Text>
|
||||
<Text color={theme.status.error}> ({t('OAuth expired')})</Text>
|
||||
);
|
||||
} else if (serverAuthStatus === 'unauthenticated') {
|
||||
authStatusNode = (
|
||||
<Text color={theme.status.warning}> (OAuth not authenticated)</Text>
|
||||
<Text color={theme.status.warning}>
|
||||
{' '}
|
||||
({t('OAuth not authenticated')})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,10 +176,12 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
{authStatusNode}
|
||||
</Box>
|
||||
{status === MCPServerStatus.CONNECTING && (
|
||||
<Text> (tools and prompts will appear when ready)</Text>
|
||||
<Text> ({t('tools and prompts will appear when ready')})</Text>
|
||||
)}
|
||||
{status === MCPServerStatus.DISCONNECTED && toolCount > 0 && (
|
||||
<Text> ({toolCount} tools cached)</Text>
|
||||
<Text>
|
||||
({t('{{count}} tools cached', { count: String(toolCount) })})
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{showDescriptions && server?.description && (
|
||||
@@ -176,7 +192,7 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
|
||||
{serverTools.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
<Text color={theme.text.primary}>Tools:</Text>
|
||||
<Text color={theme.text.primary}>{t('Tools:')}</Text>
|
||||
{serverTools.map((tool) => {
|
||||
const schemaContent =
|
||||
showSchema &&
|
||||
@@ -204,7 +220,9 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
)}
|
||||
{schemaContent && (
|
||||
<Box flexDirection="column" marginLeft={4}>
|
||||
<Text color={theme.text.secondary}>Parameters:</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Parameters:')}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{schemaContent}
|
||||
</Text>
|
||||
@@ -218,7 +236,7 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
|
||||
{serverPrompts.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
<Text color={theme.text.primary}>Prompts:</Text>
|
||||
<Text color={theme.text.primary}>{t('Prompts:')}</Text>
|
||||
{serverPrompts.map((prompt) => (
|
||||
<Box key={prompt.name} flexDirection="column">
|
||||
<Text>
|
||||
@@ -244,35 +262,41 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
<Text color={theme.status.error}>🔴 </Text>
|
||||
<Text bold>
|
||||
{server.name}
|
||||
{server.extensionName ? ` (from ${server.extensionName})` : ''}
|
||||
{server.extensionName
|
||||
? ` ${t('(from {{extensionName}})', {
|
||||
extensionName: server.extensionName,
|
||||
})}`
|
||||
: ''}
|
||||
</Text>
|
||||
<Text> - Blocked</Text>
|
||||
<Text> - {t('Blocked')}</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{showTips && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.accent}>💡 Tips:</Text>
|
||||
<Text color={theme.text.accent}>{t('💡 Tips:')}</Text>
|
||||
<Text>
|
||||
{' '}- Use <Text color={theme.text.accent}>/mcp desc</Text> to show
|
||||
server and tool descriptions
|
||||
{' '}- {t('Use')} <Text color={theme.text.accent}>/mcp desc</Text>{' '}
|
||||
{t('to show server and tool descriptions')}
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Use <Text color={theme.text.accent}>/mcp schema</Text> to
|
||||
show tool parameter schemas
|
||||
{' '}- {t('Use')}{' '}
|
||||
<Text color={theme.text.accent}>/mcp schema</Text>{' '}
|
||||
{t('to show tool parameter schemas')}
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Use <Text color={theme.text.accent}>/mcp nodesc</Text> to
|
||||
hide descriptions
|
||||
{' '}- {t('Use')}{' '}
|
||||
<Text color={theme.text.accent}>/mcp nodesc</Text>{' '}
|
||||
{t('to hide descriptions')}
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Use{' '}
|
||||
{' '}- {t('Use')}{' '}
|
||||
<Text color={theme.text.accent}>/mcp auth <server-name></Text>{' '}
|
||||
to authenticate with OAuth-enabled servers
|
||||
{t('to authenticate with OAuth-enabled servers')}
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Press <Text color={theme.text.accent}>Ctrl+T</Text> to
|
||||
toggle tool descriptions on/off
|
||||
{' '}- {t('Press')} <Text color={theme.text.accent}>Ctrl+T</Text>{' '}
|
||||
{t('to toggle tool descriptions on/off')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { AuthType, DEFAULT_QWEN_MODEL } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export type AvailableModel = {
|
||||
id: string;
|
||||
@@ -20,14 +21,20 @@ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
|
||||
{
|
||||
id: MAINLINE_CODER,
|
||||
label: MAINLINE_CODER,
|
||||
description:
|
||||
get description() {
|
||||
return t(
|
||||
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: MAINLINE_VLM,
|
||||
label: MAINLINE_VLM,
|
||||
description:
|
||||
get description() {
|
||||
return t(
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
|
||||
);
|
||||
},
|
||||
isVision: true,
|
||||
},
|
||||
];
|
||||
|
||||
329
scripts/check-i18n.ts
Normal file
329
scripts/check-i18n.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { glob } from 'glob';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
interface CheckResult {
|
||||
success: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
stats: {
|
||||
totalKeys: number;
|
||||
translatedKeys: number;
|
||||
unusedKeys: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations from JS file
|
||||
*/
|
||||
async function loadTranslationsFile(
|
||||
filePath: string,
|
||||
): Promise<Record<string, string>> {
|
||||
try {
|
||||
// Dynamic import for ES modules
|
||||
const module = await import(filePath);
|
||||
return module.default || module;
|
||||
} catch (error) {
|
||||
// Fallback: try reading as JSON if JS import fails
|
||||
try {
|
||||
const content = fs.readFileSync(
|
||||
filePath.replace('.js', '.json'),
|
||||
'utf-8',
|
||||
);
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract string literal from code, handling escaped quotes
|
||||
*/
|
||||
function extractStringLiteral(
|
||||
content: string,
|
||||
startPos: number,
|
||||
quote: string,
|
||||
): { value: string; endPos: number } | null {
|
||||
let pos = startPos + 1; // Skip opening quote
|
||||
let value = '';
|
||||
let escaped = false;
|
||||
|
||||
while (pos < content.length) {
|
||||
const char = content[pos];
|
||||
|
||||
if (escaped) {
|
||||
if (char === '\\') {
|
||||
value += '\\';
|
||||
} else if (char === quote) {
|
||||
value += quote;
|
||||
} else if (char === 'n') {
|
||||
value += '\n';
|
||||
} else if (char === 't') {
|
||||
value += '\t';
|
||||
} else if (char === 'r') {
|
||||
value += '\r';
|
||||
} else {
|
||||
value += char;
|
||||
}
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === quote) {
|
||||
return { value, endPos: pos };
|
||||
} else {
|
||||
value += char;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
return null; // String not closed
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all t() calls from source files
|
||||
*/
|
||||
async function extractUsedKeys(sourceDir: string): Promise<Set<string>> {
|
||||
const usedKeys = new Set<string>();
|
||||
|
||||
// Find all TypeScript/TSX files
|
||||
const files = await glob('**/*.{ts,tsx}', {
|
||||
cwd: sourceDir,
|
||||
ignore: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
],
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(sourceDir, file);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Find all t( calls
|
||||
const tCallRegex = /t\s*\(/g;
|
||||
let match;
|
||||
while ((match = tCallRegex.exec(content)) !== null) {
|
||||
const startPos = match.index + match[0].length;
|
||||
let pos = startPos;
|
||||
|
||||
// Skip whitespace
|
||||
while (pos < content.length && /\s/.test(content[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= content.length) continue;
|
||||
|
||||
const char = content[pos];
|
||||
if (char === "'" || char === '"') {
|
||||
const result = extractStringLiteral(content, pos, char);
|
||||
if (result) {
|
||||
usedKeys.add(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check key-value consistency in en.js
|
||||
*/
|
||||
function checkKeyValueConsistency(
|
||||
enTranslations: Record<string, string>,
|
||||
): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(enTranslations)) {
|
||||
if (key !== value) {
|
||||
errors.push(`Key-value mismatch: "${key}" !== "${value}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if en.js and zh.js have matching keys
|
||||
*/
|
||||
function checkKeyMatching(
|
||||
enTranslations: Record<string, string>,
|
||||
zhTranslations: Record<string, string>,
|
||||
): string[] {
|
||||
const errors: string[] = [];
|
||||
const enKeys = new Set(Object.keys(enTranslations));
|
||||
const zhKeys = new Set(Object.keys(zhTranslations));
|
||||
|
||||
// Check for keys in en but not in zh
|
||||
for (const key of enKeys) {
|
||||
if (!zhKeys.has(key)) {
|
||||
errors.push(`Missing translation in zh.js: "${key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keys in zh but not in en
|
||||
for (const key of zhKeys) {
|
||||
if (!enKeys.has(key)) {
|
||||
errors.push(`Extra key in zh.js (not in en.js): "${key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find unused translation keys
|
||||
*/
|
||||
function findUnusedKeys(allKeys: Set<string>, usedKeys: Set<string>): string[] {
|
||||
const unused: string[] = [];
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (!usedKeys.has(key)) {
|
||||
unused.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return unused.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main check function
|
||||
*/
|
||||
async function checkI18n(): Promise<CheckResult> {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const localesDir = path.join(__dirname, '../packages/cli/src/i18n/locales');
|
||||
const sourceDir = path.join(__dirname, '../packages/cli/src');
|
||||
|
||||
const enPath = path.join(localesDir, 'en.js');
|
||||
const zhPath = path.join(localesDir, 'zh.js');
|
||||
|
||||
// Load translation files
|
||||
let enTranslations: Record<string, string>;
|
||||
let zhTranslations: Record<string, string>;
|
||||
|
||||
try {
|
||||
enTranslations = await loadTranslationsFile(enPath);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Failed to load en.js: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
errors,
|
||||
warnings,
|
||||
stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
zhTranslations = await loadTranslationsFile(zhPath);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Failed to load zh.js: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
errors,
|
||||
warnings,
|
||||
stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] },
|
||||
};
|
||||
}
|
||||
|
||||
// Check key-value consistency in en.js
|
||||
const consistencyErrors = checkKeyValueConsistency(enTranslations);
|
||||
errors.push(...consistencyErrors);
|
||||
|
||||
// Check key matching between en and zh
|
||||
const matchingErrors = checkKeyMatching(enTranslations, zhTranslations);
|
||||
errors.push(...matchingErrors);
|
||||
|
||||
// Extract used keys from source code
|
||||
const usedKeys = await extractUsedKeys(sourceDir);
|
||||
|
||||
// Find unused keys
|
||||
const enKeys = new Set(Object.keys(enTranslations));
|
||||
const unusedKeys = findUnusedKeys(enKeys, usedKeys);
|
||||
|
||||
if (unusedKeys.length > 0) {
|
||||
warnings.push(`Found ${unusedKeys.length} unused translation keys`);
|
||||
}
|
||||
|
||||
const totalKeys = Object.keys(enTranslations).length;
|
||||
const translatedKeys = Object.keys(zhTranslations).length;
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
stats: {
|
||||
totalKeys,
|
||||
translatedKeys,
|
||||
unusedKeys,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Run checks
|
||||
checkI18n()
|
||||
.then((result) => {
|
||||
console.log('\n=== i18n Check Results ===\n');
|
||||
|
||||
console.log(`Total keys: ${result.stats.totalKeys}`);
|
||||
console.log(`Translated keys: ${result.stats.translatedKeys}`);
|
||||
console.log(
|
||||
`Translation coverage: ${((result.stats.translatedKeys / result.stats.totalKeys) * 100).toFixed(1)}%\n`,
|
||||
);
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log('⚠️ Warnings:');
|
||||
result.warnings.forEach((warning) => console.log(` - ${warning}`));
|
||||
if (
|
||||
result.stats.unusedKeys.length > 0 &&
|
||||
result.stats.unusedKeys.length <= 10
|
||||
) {
|
||||
console.log('\nUnused keys:');
|
||||
result.stats.unusedKeys.forEach((key) => console.log(` - "${key}"`));
|
||||
} else if (result.stats.unusedKeys.length > 10) {
|
||||
console.log(
|
||||
`\nUnused keys (showing first 10 of ${result.stats.unusedKeys.length}):`,
|
||||
);
|
||||
result.stats.unusedKeys
|
||||
.slice(0, 10)
|
||||
.forEach((key) => console.log(` - "${key}"`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.log('❌ Errors:');
|
||||
result.errors.forEach((error) => console.log(` - ${error}`));
|
||||
console.log();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ All checks passed!\n');
|
||||
process.exit(0);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -28,7 +28,7 @@ const targetDir = path.join('dist', 'src');
|
||||
|
||||
const extensionsToCopy = ['.md', '.json', '.sb'];
|
||||
|
||||
function copyFilesRecursive(source, target) {
|
||||
function copyFilesRecursive(source, target, rootSourceDir) {
|
||||
if (!fs.existsSync(target)) {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
@@ -40,14 +40,15 @@ function copyFilesRecursive(source, target) {
|
||||
const targetPath = path.join(target, item.name);
|
||||
|
||||
if (item.isDirectory()) {
|
||||
copyFilesRecursive(sourcePath, targetPath);
|
||||
copyFilesRecursive(sourcePath, targetPath, rootSourceDir);
|
||||
} else {
|
||||
const ext = path.extname(item.name);
|
||||
// Copy standard extensions, or .js files in i18n/locales directory
|
||||
// Use path.relative for precise matching to avoid false positives
|
||||
const relativePath = path.relative(rootSourceDir, sourcePath);
|
||||
const normalizedPath = relativePath.replace(/\\/g, '/');
|
||||
const isLocaleJs =
|
||||
ext === '.js' &&
|
||||
(sourcePath.includes('i18n/locales') ||
|
||||
sourcePath.includes(path.join('i18n', 'locales')));
|
||||
ext === '.js' && normalizedPath.startsWith('i18n/locales/');
|
||||
if (extensionsToCopy.includes(ext) || isLocaleJs) {
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
@@ -60,7 +61,7 @@ if (!fs.existsSync(sourceDir)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
copyFilesRecursive(sourceDir, targetDir);
|
||||
copyFilesRecursive(sourceDir, targetDir, sourceDir);
|
||||
|
||||
// Copy example extensions into the bundle.
|
||||
const packageName = path.basename(process.cwd());
|
||||
|
||||
Reference in New Issue
Block a user