mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
feat(i18n): Add Internationalization Support for UI and LLM Output (#1058)
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
getFieldValue,
|
||||
type SystemInfoField,
|
||||
} from '../../utils/systemInfoFields.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
type AboutBoxProps = ExtendedSystemInfo;
|
||||
|
||||
@@ -30,7 +31,7 @@ export const AboutBox: React.FC<AboutBoxProps> = (props) => {
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
About Qwen Code
|
||||
{t('About Qwen Code')}
|
||||
</Text>
|
||||
</Box>
|
||||
{fields.map((field: SystemInfoField) => (
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SettingScope } from '../../config/settings.js';
|
||||
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { ScopeSelector } from './shared/ScopeSelector.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ApprovalModeDialogProps {
|
||||
/** Callback function when an approval mode is selected */
|
||||
@@ -33,15 +34,15 @@ interface ApprovalModeDialogProps {
|
||||
const formatModeDescription = (mode: ApprovalMode): string => {
|
||||
switch (mode) {
|
||||
case ApprovalMode.PLAN:
|
||||
return 'Analyze only, do not modify files or execute commands';
|
||||
return t('Analyze only, do not modify files or execute commands');
|
||||
case ApprovalMode.DEFAULT:
|
||||
return 'Require approval for file edits or shell commands';
|
||||
return t('Require approval for file edits or shell commands');
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
return 'Automatically approve file edits';
|
||||
return t('Automatically approve file edits');
|
||||
case ApprovalMode.YOLO:
|
||||
return 'Automatically approve all tools';
|
||||
return t('Automatically approve all tools');
|
||||
default:
|
||||
return `${mode} mode`;
|
||||
return t('{{mode}} mode', { mode });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,7 +135,8 @@ export function ApprovalModeDialog({
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{/* Approval Mode Selection */}
|
||||
<Text bold={focusSection === 'mode'} wrap="truncate">
|
||||
{focusSection === 'mode' ? '> ' : ' '}Approval Mode{' '}
|
||||
{focusSection === 'mode' ? '> ' : ' '}
|
||||
{t('Approval Mode')}{' '}
|
||||
<Text color={theme.text.secondary}>{otherScopeModifiedMessage}</Text>
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
@@ -167,15 +169,17 @@ export function ApprovalModeDialog({
|
||||
{showWorkspacePriorityWarning && (
|
||||
<>
|
||||
<Text color={theme.status.warning} wrap="wrap">
|
||||
⚠ Workspace approval mode exists and takes priority. User-level
|
||||
change will have no effect.
|
||||
⚠{' '}
|
||||
{t(
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.',
|
||||
)}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text color={theme.text.secondary}>
|
||||
(Use Enter to select, Tab to change focus)
|
||||
{t('(Use Enter to select, Tab to change focus)')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface AutoAcceptIndicatorProps {
|
||||
approvalMode: ApprovalMode;
|
||||
@@ -23,18 +24,18 @@ export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
|
||||
switch (approvalMode) {
|
||||
case ApprovalMode.PLAN:
|
||||
textColor = theme.status.success;
|
||||
textContent = 'plan mode';
|
||||
subText = ' (shift + tab to cycle)';
|
||||
textContent = t('plan mode');
|
||||
subText = ` ${t('(shift + tab to cycle)')}`;
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
textColor = theme.status.warning;
|
||||
textContent = 'auto-accept edits';
|
||||
subText = ' (shift + tab to cycle)';
|
||||
textContent = t('auto-accept edits');
|
||||
subText = ` ${t('(shift + tab to cycle)')}`;
|
||||
break;
|
||||
case ApprovalMode.YOLO:
|
||||
textColor = theme.status.error;
|
||||
textContent = 'YOLO mode';
|
||||
subText = ' (shift + tab to cycle)';
|
||||
textContent = t('YOLO mode');
|
||||
subText = ` ${t('(shift + tab to cycle)')}`;
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const Composer = () => {
|
||||
const config = useConfig();
|
||||
@@ -86,14 +87,16 @@ export const Composer = () => {
|
||||
)}
|
||||
{uiState.ctrlCPressedOnce ? (
|
||||
<Text color={theme.status.warning}>
|
||||
Press Ctrl+C again to exit.
|
||||
{t('Press Ctrl+C again to exit.')}
|
||||
</Text>
|
||||
) : uiState.ctrlDPressedOnce ? (
|
||||
<Text color={theme.status.warning}>
|
||||
Press Ctrl+D again to exit.
|
||||
{t('Press Ctrl+D again to exit.')}
|
||||
</Text>
|
||||
) : uiState.showEscapePrompt ? (
|
||||
<Text color={theme.text.secondary}>Press Esc again to clear.</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Press Esc again to clear.')}
|
||||
</Text>
|
||||
) : (
|
||||
!settings.merged.ui?.hideContextSummary && (
|
||||
<ContextSummaryDisplay
|
||||
@@ -151,8 +154,8 @@ export const Composer = () => {
|
||||
isEmbeddedShellFocused={uiState.embeddedShellFocused}
|
||||
placeholder={
|
||||
vimEnabled
|
||||
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
|
||||
: ' Type your message or @path/to/file'
|
||||
? ' ' + t("Press 'i' for INSERT mode and 'Esc' for NORMAL mode.")
|
||||
: ' ' + t('Type your message or @path/to/file')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SettingScope } from '../../config/settings.js';
|
||||
import type { EditorType } from '@qwen-code/qwen-code-core';
|
||||
import { isEditorAvailable } from '@qwen-code/qwen-code-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface EditorDialogProps {
|
||||
onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void;
|
||||
@@ -66,12 +67,16 @@ export function EditorSettingsDialog({
|
||||
|
||||
const scopeItems = [
|
||||
{
|
||||
label: 'User Settings',
|
||||
get label() {
|
||||
return t('User Settings');
|
||||
},
|
||||
value: SettingScope.User,
|
||||
key: SettingScope.User,
|
||||
},
|
||||
{
|
||||
label: 'Workspace Settings',
|
||||
get label() {
|
||||
return t('Workspace Settings');
|
||||
},
|
||||
value: SettingScope.Workspace,
|
||||
key: SettingScope.Workspace,
|
||||
},
|
||||
@@ -145,7 +150,8 @@ export function EditorSettingsDialog({
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold={focusedSection === 'scope'}>
|
||||
{focusedSection === 'scope' ? '> ' : ' '}Apply To
|
||||
{focusedSection === 'scope' ? '> ' : ' '}
|
||||
{t('Apply To')}
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -210,6 +210,7 @@ describe('InputPrompt', () => {
|
||||
inputWidth: 80,
|
||||
suggestionsWidth: 80,
|
||||
focus: true,
|
||||
placeholder: ' Type your message or @path/to/file',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
parseInputForHighlighting,
|
||||
buildSegmentsForVisualSlice,
|
||||
} from '../utils/highlight.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
clipboardHasImage,
|
||||
saveClipboardImage,
|
||||
@@ -88,7 +89,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
config,
|
||||
slashCommands,
|
||||
commandContext,
|
||||
placeholder = ' Type your message or @path/to/file',
|
||||
placeholder,
|
||||
focus = true,
|
||||
suggestionsWidth,
|
||||
shellModeActive,
|
||||
@@ -697,13 +698,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
let statusText = '';
|
||||
if (shellModeActive) {
|
||||
statusColor = theme.ui.symbol;
|
||||
statusText = 'Shell mode';
|
||||
statusText = t('Shell mode');
|
||||
} else if (showYoloStyling) {
|
||||
statusColor = theme.status.error;
|
||||
statusText = 'YOLO mode';
|
||||
statusText = t('YOLO mode');
|
||||
} else if (showAutoAcceptStyling) {
|
||||
statusColor = theme.status.warning;
|
||||
statusText = 'Accepting edits';
|
||||
statusText = t('Accepting edits');
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,6 +14,7 @@ import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
currentLoadingPhrase?: string;
|
||||
@@ -40,7 +41,12 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
|
||||
const cancelAndTimerContent =
|
||||
streamingState !== StreamingState.WaitingForConfirmation
|
||||
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`
|
||||
? t('(esc to cancel, {{time}})', {
|
||||
time:
|
||||
elapsedTime < 60
|
||||
? `${elapsedTime}s`
|
||||
: formatDuration(elapsedTime * 1000),
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../utils/computeStats.js';
|
||||
import type { ModelMetrics } from '../contexts/SessionContext.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
const METRIC_COL_WIDTH = 28;
|
||||
const MODEL_COL_WIDTH = 22;
|
||||
@@ -65,7 +66,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
paddingX={2}
|
||||
>
|
||||
<Text color={theme.text.primary}>
|
||||
No API calls have been made in this session.
|
||||
{t('No API calls have been made in this session.')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -94,7 +95,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
paddingX={2}
|
||||
>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Model Stats For Nerds
|
||||
{t('Model Stats For Nerds')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
|
||||
@@ -102,7 +103,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
<Box>
|
||||
<Box width={METRIC_COL_WIDTH}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Metric
|
||||
{t('Metric')}
|
||||
</Text>
|
||||
</Box>
|
||||
{modelNames.map((name) => (
|
||||
@@ -125,13 +126,13 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* API Section */}
|
||||
<StatRow title="API" values={[]} isSection />
|
||||
<StatRow title={t('API')} values={[]} isSection />
|
||||
<StatRow
|
||||
title="Requests"
|
||||
title={t('Requests')}
|
||||
values={getModelValues((m) => m.api.totalRequests.toLocaleString())}
|
||||
/>
|
||||
<StatRow
|
||||
title="Errors"
|
||||
title={t('Errors')}
|
||||
values={getModelValues((m) => {
|
||||
const errorRate = calculateErrorRate(m);
|
||||
return (
|
||||
@@ -146,7 +147,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
})}
|
||||
/>
|
||||
<StatRow
|
||||
title="Avg Latency"
|
||||
title={t('Avg Latency')}
|
||||
values={getModelValues((m) => {
|
||||
const avgLatency = calculateAverageLatency(m);
|
||||
return formatDuration(avgLatency);
|
||||
@@ -156,9 +157,9 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
<Box height={1} />
|
||||
|
||||
{/* Tokens Section */}
|
||||
<StatRow title="Tokens" values={[]} isSection />
|
||||
<StatRow title={t('Tokens')} values={[]} isSection />
|
||||
<StatRow
|
||||
title="Total"
|
||||
title={t('Total')}
|
||||
values={getModelValues((m) => (
|
||||
<Text color={theme.status.warning}>
|
||||
{m.tokens.total.toLocaleString()}
|
||||
@@ -166,13 +167,13 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
))}
|
||||
/>
|
||||
<StatRow
|
||||
title="Prompt"
|
||||
title={t('Prompt')}
|
||||
isSubtle
|
||||
values={getModelValues((m) => m.tokens.prompt.toLocaleString())}
|
||||
/>
|
||||
{hasCached && (
|
||||
<StatRow
|
||||
title="Cached"
|
||||
title={t('Cached')}
|
||||
isSubtle
|
||||
values={getModelValues((m) => {
|
||||
const cacheHitRate = calculateCacheHitRate(m);
|
||||
@@ -186,20 +187,20 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
)}
|
||||
{hasThoughts && (
|
||||
<StatRow
|
||||
title="Thoughts"
|
||||
title={t('Thoughts')}
|
||||
isSubtle
|
||||
values={getModelValues((m) => m.tokens.thoughts.toLocaleString())}
|
||||
/>
|
||||
)}
|
||||
{hasTool && (
|
||||
<StatRow
|
||||
title="Tool"
|
||||
title={t('Tool')}
|
||||
isSubtle
|
||||
values={getModelValues((m) => m.tokens.tool.toLocaleString())}
|
||||
/>
|
||||
)}
|
||||
<StatRow
|
||||
title="Output"
|
||||
title={t('Output')}
|
||||
isSubtle
|
||||
values={getModelValues((m) => m.tokens.candidates.toLocaleString())}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { z } from 'zod';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface OpenAIKeyPromptProps {
|
||||
onSubmit: (apiKey: string, baseUrl: string, model: string) => void;
|
||||
@@ -64,9 +65,11 @@ export function OpenAIKeyPrompt({
|
||||
const errorMessage = error.errors
|
||||
.map((e) => `${e.path.join('.')}: ${e.message}`)
|
||||
.join(', ');
|
||||
setValidationError(`Invalid credentials: ${errorMessage}`);
|
||||
setValidationError(
|
||||
t('Invalid credentials: {{errorMessage}}', { errorMessage }),
|
||||
);
|
||||
} else {
|
||||
setValidationError('Failed to validate credentials');
|
||||
setValidationError(t('Failed to validate credentials'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -205,7 +208,7 @@ export function OpenAIKeyPrompt({
|
||||
width="100%"
|
||||
>
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
OpenAI Configuration Required
|
||||
{t('OpenAI Configuration Required')}
|
||||
</Text>
|
||||
{validationError && (
|
||||
<Box marginTop={1}>
|
||||
@@ -214,7 +217,9 @@ export function OpenAIKeyPrompt({
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
Please enter your OpenAI configuration. You can get an API key from{' '}
|
||||
{t(
|
||||
'Please enter your OpenAI configuration. You can get an API key from',
|
||||
)}{' '}
|
||||
<Text color={Colors.AccentBlue}>
|
||||
https://bailian.console.aliyun.com/?tab=model#/api-key
|
||||
</Text>
|
||||
@@ -225,7 +230,7 @@ export function OpenAIKeyPrompt({
|
||||
<Text
|
||||
color={currentField === 'apiKey' ? Colors.AccentBlue : Colors.Gray}
|
||||
>
|
||||
API Key:
|
||||
{t('API Key:')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
@@ -240,7 +245,7 @@ export function OpenAIKeyPrompt({
|
||||
<Text
|
||||
color={currentField === 'baseUrl' ? Colors.AccentBlue : Colors.Gray}
|
||||
>
|
||||
Base URL:
|
||||
{t('Base URL:')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
@@ -255,7 +260,7 @@ export function OpenAIKeyPrompt({
|
||||
<Text
|
||||
color={currentField === 'model' ? Colors.AccentBlue : Colors.Gray}
|
||||
>
|
||||
Model:
|
||||
{t('Model:')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
@@ -267,7 +272,7 @@ export function OpenAIKeyPrompt({
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray}>
|
||||
Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel
|
||||
{t('Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ProQuotaDialogProps {
|
||||
failedModel: string;
|
||||
@@ -22,12 +23,12 @@ export function ProQuotaDialog({
|
||||
}: ProQuotaDialogProps): React.JSX.Element {
|
||||
const items = [
|
||||
{
|
||||
label: 'Change auth (executes the /auth command)',
|
||||
label: t('Change auth (executes the /auth command)'),
|
||||
value: 'auth' as const,
|
||||
key: 'auth',
|
||||
},
|
||||
{
|
||||
label: `Continue with ${fallbackModel}`,
|
||||
label: t('Continue with {{model}}', { model: fallbackModel }),
|
||||
value: 'continue' as const,
|
||||
key: 'continue',
|
||||
},
|
||||
@@ -40,7 +41,7 @@ export function ProQuotaDialog({
|
||||
return (
|
||||
<Box borderStyle="round" flexDirection="column" paddingX={1}>
|
||||
<Text bold color={theme.status.warning}>
|
||||
Pro quota limit reached for {failedModel}.
|
||||
{t('Pro quota limit reached for {{model}}.', { model: failedModel })}
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<RadioButtonSelect
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export enum QuitChoice {
|
||||
CANCEL = 'cancel',
|
||||
@@ -39,22 +40,22 @@ export const QuitConfirmationDialog: React.FC<QuitConfirmationDialogProps> = ({
|
||||
const options: Array<RadioSelectItem<QuitChoice>> = [
|
||||
{
|
||||
key: 'quit',
|
||||
label: 'Quit immediately (/quit)',
|
||||
label: t('Quit immediately (/quit)'),
|
||||
value: QuitChoice.QUIT,
|
||||
},
|
||||
{
|
||||
key: 'summary-and-quit',
|
||||
label: 'Generate summary and quit (/summary)',
|
||||
label: t('Generate summary and quit (/summary)'),
|
||||
value: QuitChoice.SUMMARY_AND_QUIT,
|
||||
},
|
||||
{
|
||||
key: 'save-and-quit',
|
||||
label: 'Save conversation and quit (/chat save)',
|
||||
label: t('Save conversation and quit (/chat save)'),
|
||||
value: QuitChoice.SAVE_AND_QUIT,
|
||||
},
|
||||
{
|
||||
key: 'cancel',
|
||||
label: 'Cancel (stay in application)',
|
||||
label: t('Cancel (stay in application)'),
|
||||
value: QuitChoice.CANCEL,
|
||||
},
|
||||
];
|
||||
@@ -69,7 +70,7 @@ export const QuitConfirmationDialog: React.FC<QuitConfirmationDialogProps> = ({
|
||||
marginLeft={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text>What would you like to do before exiting?</Text>
|
||||
<Text>{t('What would you like to do before exiting?')}</Text>
|
||||
</Box>
|
||||
|
||||
<RadioButtonSelect items={options} onSelect={onSelect} isFocused />
|
||||
|
||||
@@ -13,6 +13,7 @@ import qrcode from 'qrcode-terminal';
|
||||
import { Colors } from '../colors.js';
|
||||
import type { DeviceAuthorizationData } from '@qwen-code/qwen-code-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface QwenOAuthProgressProps {
|
||||
onTimeout: () => void;
|
||||
@@ -52,11 +53,11 @@ function QrCodeDisplay({
|
||||
width="100%"
|
||||
>
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
Qwen OAuth Authentication
|
||||
{t('Qwen OAuth Authentication')}
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>Please visit this URL to authorize:</Text>
|
||||
<Text>{t('Please visit this URL to authorize:')}</Text>
|
||||
</Box>
|
||||
|
||||
<Link url={verificationUrl} fallback={false}>
|
||||
@@ -66,7 +67,7 @@ function QrCodeDisplay({
|
||||
</Link>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>Or scan the QR code below:</Text>
|
||||
<Text>{t('Or scan the QR code below:')}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
@@ -103,15 +104,18 @@ function StatusDisplay({
|
||||
>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
<Spinner type="dots" /> Waiting for authorization{dots}
|
||||
<Spinner type="dots" /> {t('Waiting for authorization')}
|
||||
{dots}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1} justifyContent="space-between">
|
||||
<Text color={Colors.Gray}>
|
||||
Time remaining: {formatTime(timeRemaining)}
|
||||
{t('Time remaining:')} {formatTime(timeRemaining)}
|
||||
</Text>
|
||||
<Text color={Colors.AccentPurple}>
|
||||
{t('(Press ESC or CTRL+C to cancel)')}
|
||||
</Text>
|
||||
<Text color={Colors.AccentPurple}>(Press ESC or CTRL+C to cancel)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -215,19 +219,24 @@ export function QwenOAuthProgress({
|
||||
width="100%"
|
||||
>
|
||||
<Text bold color={Colors.AccentRed}>
|
||||
Qwen OAuth Authentication Timeout
|
||||
{t('Qwen OAuth Authentication Timeout')}
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
{authMessage ||
|
||||
`OAuth token expired (over ${defaultTimeout} seconds). Please select authentication method again.`}
|
||||
t(
|
||||
'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.',
|
||||
{
|
||||
seconds: defaultTimeout.toString(),
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray}>
|
||||
Press any key to return to authentication type selection.
|
||||
{t('Press any key to return to authentication type selection.')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -275,16 +284,17 @@ export function QwenOAuthProgress({
|
||||
>
|
||||
<Box>
|
||||
<Text>
|
||||
<Spinner type="dots" /> Waiting for Qwen OAuth authentication...
|
||||
<Spinner type="dots" />
|
||||
{t('Waiting for Qwen OAuth authentication...')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} justifyContent="space-between">
|
||||
<Text color={Colors.Gray}>
|
||||
Time remaining: {Math.floor(timeRemaining / 60)}:
|
||||
{t('Time remaining:')} {Math.floor(timeRemaining / 60)}:
|
||||
{(timeRemaining % 60).toString().padStart(2, '0')}
|
||||
</Text>
|
||||
<Text color={Colors.AccentPurple}>
|
||||
(Press ESC or CTRL+C to cancel)
|
||||
{t('(Press ESC or CTRL+C to cancel)')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface SessionSummaryDisplayProps {
|
||||
duration: string;
|
||||
@@ -14,5 +15,8 @@ interface SessionSummaryDisplayProps {
|
||||
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
|
||||
duration,
|
||||
}) => (
|
||||
<StatsDisplay title="Agent powering down. Goodbye!" duration={duration} />
|
||||
<StatsDisplay
|
||||
title={t('Agent powering down. Goodbye!')}
|
||||
duration={duration}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { LoadedSettings, Settings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
|
||||
import { ScopeSelector } from './shared/ScopeSelector.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
getDialogSettingKeys,
|
||||
setPendingSettingValue,
|
||||
@@ -124,7 +125,9 @@ export function SettingsDialog({
|
||||
const definition = getSettingDefinition(key);
|
||||
|
||||
return {
|
||||
label: definition?.label || key,
|
||||
label: definition?.label
|
||||
? t(definition.label) || definition.label
|
||||
: key,
|
||||
value: key,
|
||||
type: definition?.type,
|
||||
toggle: () => {
|
||||
@@ -779,7 +782,8 @@ export function SettingsDialog({
|
||||
>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Text bold={focusSection === 'settings'} wrap="truncate">
|
||||
{focusSection === 'settings' ? '> ' : ' '}Settings
|
||||
{focusSection === 'settings' ? '> ' : ' '}
|
||||
{t('Settings')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{showScrollUp && <Text color={theme.text.secondary}>▲</Text>}
|
||||
@@ -916,13 +920,15 @@ export function SettingsDialog({
|
||||
|
||||
<Box height={1} />
|
||||
<Text color={theme.text.secondary}>
|
||||
(Use Enter to select
|
||||
{showScopeSelection ? ', Tab to change focus' : ''})
|
||||
{t('(Use Enter to select{{tabText}})', {
|
||||
tabText: showScopeSelection ? t(', Tab to change focus') : '',
|
||||
})}
|
||||
</Text>
|
||||
{showRestartPrompt && (
|
||||
<Text color={theme.status.warning}>
|
||||
To see changes, Qwen Code must be restarted. Press r to exit and
|
||||
apply changes now.
|
||||
{t(
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RenderInline } from '../utils/InlineMarkdownRenderer.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export interface ShellConfirmationRequest {
|
||||
commands: string[];
|
||||
@@ -51,17 +52,17 @@ export const ShellConfirmationDialog: React.FC<
|
||||
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [
|
||||
{
|
||||
label: 'Yes, allow once',
|
||||
label: t('Yes, allow once'),
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
},
|
||||
{
|
||||
label: 'Yes, allow always for this session',
|
||||
label: t('Yes, allow always for this session'),
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always for this session',
|
||||
},
|
||||
{
|
||||
label: 'No (esc)',
|
||||
label: t('No (esc)'),
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No (esc)',
|
||||
},
|
||||
@@ -78,10 +79,10 @@ export const ShellConfirmationDialog: React.FC<
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Shell Command Execution
|
||||
{t('Shell Command Execution')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
A custom command wants to run the following shell commands:
|
||||
{t('A custom command wants to run the following shell commands:')}
|
||||
</Text>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
@@ -99,7 +100,7 @@ export const ShellConfirmationDialog: React.FC<
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.primary}>Do you want to proceed?</Text>
|
||||
<Text color={theme.text.primary}>{t('Do you want to proceed?')}</Text>
|
||||
</Box>
|
||||
|
||||
<RadioButtonSelect items={options} onSelect={handleSelect} isFocused />
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
USER_AGREEMENT_RATE_MEDIUM,
|
||||
} from '../utils/displayUtils.js';
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
// A more flexible and powerful StatRow component
|
||||
interface StatRowProps {
|
||||
@@ -85,22 +86,22 @@ const ModelUsageTable: React.FC<{
|
||||
<Box>
|
||||
<Box width={nameWidth}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model Usage
|
||||
{t('Model Usage')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={requestsWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Reqs
|
||||
{t('Reqs')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={inputTokensWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Input Tokens
|
||||
{t('Input Tokens')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={outputTokensWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Output Tokens
|
||||
{t('Output Tokens')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -141,13 +142,14 @@ const ModelUsageTable: React.FC<{
|
||||
{cacheEfficiency > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={theme.status.success}>Savings Highlight:</Text>{' '}
|
||||
<Text color={theme.status.success}>{t('Savings Highlight:')}</Text>{' '}
|
||||
{totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)}
|
||||
%) of input tokens were served from the cache, reducing costs.
|
||||
%){' '}
|
||||
{t('of input tokens were served from the cache, reducing costs.')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
<Text color={theme.text.secondary}>
|
||||
» Tip: For a full token breakdown, run `/stats model`.
|
||||
» {t('Tip: For a full token breakdown, run `/stats model`.')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -199,7 +201,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
}
|
||||
return (
|
||||
<Text bold color={theme.text.accent}>
|
||||
Session Stats
|
||||
{t('Session Stats')}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -215,33 +217,33 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
{renderTitle()}
|
||||
<Box height={1} />
|
||||
|
||||
<Section title="Interaction Summary">
|
||||
<StatRow title="Session ID:">
|
||||
<Section title={t('Interaction Summary')}>
|
||||
<StatRow title={t('Session ID:')}>
|
||||
<Text color={theme.text.primary}>{stats.sessionId}</Text>
|
||||
</StatRow>
|
||||
<StatRow title="Tool Calls:">
|
||||
<StatRow title={t('Tool Calls:')}>
|
||||
<Text color={theme.text.primary}>
|
||||
{tools.totalCalls} ({' '}
|
||||
<Text color={theme.status.success}>✓ {tools.totalSuccess}</Text>{' '}
|
||||
<Text color={theme.status.error}>x {tools.totalFail}</Text> )
|
||||
</Text>
|
||||
</StatRow>
|
||||
<StatRow title="Success Rate:">
|
||||
<StatRow title={t('Success Rate:')}>
|
||||
<Text color={successColor}>{computed.successRate.toFixed(1)}%</Text>
|
||||
</StatRow>
|
||||
{computed.totalDecisions > 0 && (
|
||||
<StatRow title="User Agreement:">
|
||||
<StatRow title={t('User Agreement:')}>
|
||||
<Text color={agreementColor}>
|
||||
{computed.agreementRate.toFixed(1)}%{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
({computed.totalDecisions} reviewed)
|
||||
({computed.totalDecisions} {t('reviewed')})
|
||||
</Text>
|
||||
</Text>
|
||||
</StatRow>
|
||||
)}
|
||||
{files &&
|
||||
(files.totalLinesAdded > 0 || files.totalLinesRemoved > 0) && (
|
||||
<StatRow title="Code Changes:">
|
||||
<StatRow title={t('Code Changes:')}>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={theme.status.success}>
|
||||
+{files.totalLinesAdded}
|
||||
@@ -254,16 +256,16 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title="Performance">
|
||||
<StatRow title="Wall Time:">
|
||||
<Section title={t('Performance')}>
|
||||
<StatRow title={t('Wall Time:')}>
|
||||
<Text color={theme.text.primary}>{duration}</Text>
|
||||
</StatRow>
|
||||
<StatRow title="Agent Active:">
|
||||
<StatRow title={t('Agent Active:')}>
|
||||
<Text color={theme.text.primary}>
|
||||
{formatDuration(computed.agentActiveTime)}
|
||||
</Text>
|
||||
</StatRow>
|
||||
<SubStatRow title="API Time:">
|
||||
<SubStatRow title={t('API Time:')}>
|
||||
<Text color={theme.text.primary}>
|
||||
{formatDuration(computed.totalApiTime)}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
@@ -271,7 +273,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
</Text>
|
||||
</Text>
|
||||
</SubStatRow>
|
||||
<SubStatRow title="Tool Time:">
|
||||
<SubStatRow title={t('Tool Time:')}>
|
||||
<Text color={theme.text.primary}>
|
||||
{formatDuration(computed.totalToolTime)}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { SettingScope } from '../../config/settings.js';
|
||||
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { ScopeSelector } from './shared/ScopeSelector.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ThemeDialogProps {
|
||||
/** Callback function when a theme is selected */
|
||||
@@ -198,7 +199,8 @@ export function ThemeDialog({
|
||||
{/* Left Column: Selection */}
|
||||
<Box flexDirection="column" width="45%" paddingRight={2}>
|
||||
<Text bold={mode === 'theme'} wrap="truncate">
|
||||
{mode === 'theme' ? '> ' : ' '}Select Theme{' '}
|
||||
{mode === 'theme' ? '> ' : ' '}
|
||||
{t('Select Theme')}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{otherScopeModifiedMessage}
|
||||
</Text>
|
||||
@@ -218,7 +220,7 @@ export function ThemeDialog({
|
||||
{/* Right Column: Preview */}
|
||||
<Box flexDirection="column" width="55%" paddingLeft={2}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Preview
|
||||
{t('Preview')}
|
||||
</Text>
|
||||
{/* Get the Theme object for the highlighted theme, fall back to default if not found */}
|
||||
{(() => {
|
||||
@@ -274,8 +276,9 @@ def fibonacci(n):
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
(Use Enter to {mode === 'theme' ? 'select' : 'apply scope'}, Tab to{' '}
|
||||
{mode === 'theme' ? 'configure scope' : 'select theme'})
|
||||
{mode === 'theme'
|
||||
? t('(Use Enter to select, Tab to configure scope)')
|
||||
: t('(Use Enter to apply scope, Tab to select theme)')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { type Config } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface TipsProps {
|
||||
config: Config;
|
||||
@@ -17,12 +18,12 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
|
||||
const geminiMdFileCount = config.getGeminiMdFileCount();
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.primary}>Tips for getting started:</Text>
|
||||
<Text color={theme.text.primary}>{t('Tips for getting started:')}</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
1. Ask questions, edit files, or run commands.
|
||||
{t('1. Ask questions, edit files, or run commands.')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
2. Be specific for the best results.
|
||||
{t('2. Be specific for the best results.')}
|
||||
</Text>
|
||||
{geminiMdFileCount === 0 && (
|
||||
<Text color={theme.text.primary}>
|
||||
@@ -30,7 +31,7 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
|
||||
<Text bold color={theme.text.accent}>
|
||||
QWEN.md
|
||||
</Text>{' '}
|
||||
files to customize your interactions with Qwen Code.
|
||||
{t('files to customize your interactions with Qwen Code.')}
|
||||
</Text>
|
||||
)}
|
||||
<Text color={theme.text.primary}>
|
||||
@@ -38,7 +39,7 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
|
||||
<Text bold color={theme.text.accent}>
|
||||
/help
|
||||
</Text>{' '}
|
||||
for more information.
|
||||
{t('for more information.')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../utils/displayUtils.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import type { ToolCallStats } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
const TOOL_NAME_COL_WIDTH = 25;
|
||||
const CALLS_COL_WIDTH = 8;
|
||||
@@ -68,7 +69,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
paddingX={2}
|
||||
>
|
||||
<Text color={theme.text.primary}>
|
||||
No tool calls have been made in this session.
|
||||
{t('No tool calls have been made in this session.')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -103,7 +104,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
width={70}
|
||||
>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Tool Stats For Nerds
|
||||
{t('Tool Stats For Nerds')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
|
||||
@@ -111,22 +112,22 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
<Box>
|
||||
<Box width={TOOL_NAME_COL_WIDTH}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Tool Name
|
||||
{t('Tool Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={CALLS_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Calls
|
||||
{t('Calls')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={SUCCESS_RATE_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Success Rate
|
||||
{t('Success Rate')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Avg Duration
|
||||
{t('Avg Duration')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -151,13 +152,15 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
|
||||
{/* User Decision Summary */}
|
||||
<Text bold color={theme.text.primary}>
|
||||
User Decision Summary
|
||||
{t('User Decision Summary')}
|
||||
</Text>
|
||||
<Box>
|
||||
<Box
|
||||
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
|
||||
>
|
||||
<Text color={theme.text.link}>Total Reviewed Suggestions:</Text>
|
||||
<Text color={theme.text.link}>
|
||||
{t('Total Reviewed Suggestions:')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text color={theme.text.primary}>{totalReviewed}</Text>
|
||||
@@ -167,7 +170,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
<Box
|
||||
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
|
||||
>
|
||||
<Text color={theme.text.primary}> » Accepted:</Text>
|
||||
<Text color={theme.text.primary}>{t(' » Accepted:')}</Text>
|
||||
</Box>
|
||||
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text color={theme.status.success}>{totalDecisions.accept}</Text>
|
||||
@@ -177,7 +180,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
<Box
|
||||
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
|
||||
>
|
||||
<Text color={theme.text.primary}> » Rejected:</Text>
|
||||
<Text color={theme.text.primary}>{t(' » Rejected:')}</Text>
|
||||
</Box>
|
||||
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text color={theme.status.error}>{totalDecisions.reject}</Text>
|
||||
@@ -187,7 +190,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
<Box
|
||||
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
|
||||
>
|
||||
<Text color={theme.text.primary}> » Modified:</Text>
|
||||
<Text color={theme.text.primary}>{t(' » Modified:')}</Text>
|
||||
</Box>
|
||||
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text color={theme.status.warning}>{totalDecisions.modify}</Text>
|
||||
@@ -209,7 +212,9 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
<Box
|
||||
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
|
||||
>
|
||||
<Text color={theme.text.primary}> Overall Agreement Rate:</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{t(' Overall Agreement Rate:')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
|
||||
<Text bold color={totalReviewed > 0 ? agreementColor : undefined}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface WelcomeBackDialogProps {
|
||||
welcomeBackInfo: ProjectSummaryInfo;
|
||||
@@ -36,12 +37,12 @@ export function WelcomeBackDialog({
|
||||
const options: Array<RadioSelectItem<'restart' | 'continue'>> = [
|
||||
{
|
||||
key: 'restart',
|
||||
label: 'Start new chat session',
|
||||
label: t('Start new chat session'),
|
||||
value: 'restart',
|
||||
},
|
||||
{
|
||||
key: 'continue',
|
||||
label: 'Continue previous conversation',
|
||||
label: t('Continue previous conversation'),
|
||||
value: 'continue',
|
||||
},
|
||||
];
|
||||
@@ -67,7 +68,9 @@ export function WelcomeBackDialog({
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={Colors.AccentBlue} bold>
|
||||
👋 Welcome back! (Last updated: {timeAgo})
|
||||
{t('👋 Welcome back! (Last updated: {{timeAgo}})', {
|
||||
timeAgo: timeAgo || '',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -75,7 +78,7 @@ export function WelcomeBackDialog({
|
||||
{goalContent && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={Colors.Foreground} bold>
|
||||
🎯 Overall Goal:
|
||||
{t('🎯 Overall Goal:')}
|
||||
</Text>
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text color={Colors.Gray}>{goalContent}</Text>
|
||||
@@ -87,19 +90,25 @@ export function WelcomeBackDialog({
|
||||
{totalTasks > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={Colors.Foreground} bold>
|
||||
📋 Current Plan:
|
||||
📋 {t('Current Plan:')}
|
||||
</Text>
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text color={Colors.Gray}>
|
||||
Progress: {doneCount}/{totalTasks} tasks completed
|
||||
{inProgressCount > 0 && `, ${inProgressCount} in progress`}
|
||||
{t('Progress: {{done}}/{{total}} tasks completed', {
|
||||
done: String(doneCount),
|
||||
total: String(totalTasks),
|
||||
})}
|
||||
{inProgressCount > 0 &&
|
||||
t(', {{inProgress}} in progress', {
|
||||
inProgress: String(inProgressCount),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{pendingTasks.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1} paddingLeft={2}>
|
||||
<Text color={Colors.Foreground} bold>
|
||||
Pending Tasks:
|
||||
{t('Pending Tasks:')}
|
||||
</Text>
|
||||
{pendingTasks.map((task: string, index: number) => (
|
||||
<Text key={index} color={Colors.Gray}>
|
||||
@@ -113,8 +122,8 @@ export function WelcomeBackDialog({
|
||||
|
||||
{/* Action Selection */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold>What would you like to do?</Text>
|
||||
<Text>Choose how to proceed with your session:</Text>
|
||||
<Text bold>{t('What would you like to do?')}</Text>
|
||||
<Text>{t('Choose how to proceed with your session:')}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -12,6 +12,8 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -20,8 +22,6 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -46,6 +46,8 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -54,8 +56,6 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -80,6 +80,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -88,8 +90,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -114,6 +114,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
|
||||
│ │
|
||||
│ Debug Keystroke Logging false* │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false* │
|
||||
@@ -122,8 +124,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
|
||||
│ │
|
||||
│ Hide Tips false* │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -148,6 +148,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -156,8 +158,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -182,6 +182,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
|
||||
│ │
|
||||
│ Debug Keystroke Logging (Modified in Workspace) false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -190,8 +192,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -216,6 +216,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -224,8 +226,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -250,6 +250,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false* │
|
||||
@@ -258,8 +260,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -284,6 +284,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
@@ -292,8 +294,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
|
||||
│ │
|
||||
│ Hide Tips false │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
@@ -318,6 +318,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
|
||||
│ │
|
||||
│ Debug Keystroke Logging true* │
|
||||
│ │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title true* │
|
||||
@@ -326,8 +328,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
|
||||
│ │
|
||||
│ Hide Tips true* │
|
||||
│ │
|
||||
│ Hide Banner false │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
@@ -10,6 +10,7 @@ import Spinner from 'ink-spinner';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
|
||||
import { CompressionStatus } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
export interface CompressionDisplayProps {
|
||||
compression: CompressionProps;
|
||||
@@ -30,22 +31,32 @@ export function CompressionMessage({
|
||||
|
||||
const getCompressionText = () => {
|
||||
if (isPending) {
|
||||
return 'Compressing chat history';
|
||||
return t('Compressing chat history');
|
||||
}
|
||||
|
||||
switch (compressionStatus) {
|
||||
case CompressionStatus.COMPRESSED:
|
||||
return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`;
|
||||
return t(
|
||||
'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.',
|
||||
{
|
||||
originalTokens: String(originalTokens),
|
||||
newTokens: String(newTokens),
|
||||
},
|
||||
);
|
||||
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
|
||||
// For smaller histories (< 50k tokens), compression overhead likely exceeds benefits
|
||||
if (originalTokens < 50000) {
|
||||
return 'Compression was not beneficial for this history size.';
|
||||
return t('Compression was not beneficial for this history size.');
|
||||
}
|
||||
// For larger histories where compression should work but didn't,
|
||||
// this suggests an issue with the compression process itself
|
||||
return 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.';
|
||||
return t(
|
||||
'Chat history compression did not reduce size. This may indicate issues with the compression prompt.',
|
||||
);
|
||||
case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
|
||||
return 'Could not compress chat history due to a token counting error.';
|
||||
return t(
|
||||
'Could not compress chat history due to a token counting error.',
|
||||
);
|
||||
case CompressionStatus.NOOP:
|
||||
return 'Nothing to compress.';
|
||||
default:
|
||||
|
||||
@@ -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)',
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
|
||||
import type { SettingScope } from '../../../config/settings.js';
|
||||
import { getScopeItems } from '../../../utils/dialogScopeUtils.js';
|
||||
import { RadioButtonSelect } from './RadioButtonSelect.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface ScopeSelectorProps {
|
||||
/** Callback function when a scope is selected */
|
||||
@@ -29,6 +30,7 @@ export function ScopeSelector({
|
||||
}: ScopeSelectorProps): React.JSX.Element {
|
||||
const scopeItems = getScopeItems().map((item) => ({
|
||||
...item,
|
||||
label: t(item.label),
|
||||
key: item.value,
|
||||
}));
|
||||
|
||||
@@ -40,7 +42,8 @@ export function ScopeSelector({
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold={isFocused} wrap="truncate">
|
||||
{isFocused ? '> ' : ' '}Apply To
|
||||
{isFocused ? '> ' : ' '}
|
||||
{t('Apply To')}
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { TextEntryStep } from './TextEntryStep.js';
|
||||
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface AgentCreationWizardProps {
|
||||
onClose: () => void;
|
||||
@@ -90,25 +91,25 @@ export function AgentCreationWizard({
|
||||
const n = state.currentStep;
|
||||
switch (kind) {
|
||||
case 'LOCATION':
|
||||
return `Step ${n}: Choose Location`;
|
||||
return t('Step {{n}}: Choose Location', { n: n.toString() });
|
||||
case 'GEN_METHOD':
|
||||
return `Step ${n}: Choose Generation Method`;
|
||||
return t('Step {{n}}: Choose Generation Method', { n: n.toString() });
|
||||
case 'LLM_DESC':
|
||||
return `Step ${n}: Describe Your Subagent`;
|
||||
return t('Step {{n}}: Describe Your Subagent', { n: n.toString() });
|
||||
case 'MANUAL_NAME':
|
||||
return `Step ${n}: Enter Subagent Name`;
|
||||
return t('Step {{n}}: Enter Subagent Name', { n: n.toString() });
|
||||
case 'MANUAL_PROMPT':
|
||||
return `Step ${n}: Enter System Prompt`;
|
||||
return t('Step {{n}}: Enter System Prompt', { n: n.toString() });
|
||||
case 'MANUAL_DESC':
|
||||
return `Step ${n}: Enter Description`;
|
||||
return t('Step {{n}}: Enter Description', { n: n.toString() });
|
||||
case 'TOOLS':
|
||||
return `Step ${n}: Select Tools`;
|
||||
return t('Step {{n}}: Select Tools', { n: n.toString() });
|
||||
case 'COLOR':
|
||||
return `Step ${n}: Choose Background Color`;
|
||||
return t('Step {{n}}: Choose Background Color', { n: n.toString() });
|
||||
case 'FINAL':
|
||||
return `Step ${n}: Confirm and Save`;
|
||||
return t('Step {{n}}: Confirm and Save', { n: n.toString() });
|
||||
default:
|
||||
return 'Unknown Step';
|
||||
return t('Unknown Step');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,11 +164,11 @@ export function AgentCreationWizard({
|
||||
// Special case: During generation in description input step, only show cancel option
|
||||
const kind = getStepKind(state.generationMethod, state.currentStep);
|
||||
if (kind === 'LLM_DESC' && state.isGenerating) {
|
||||
return 'Esc to cancel';
|
||||
return t('Esc to cancel');
|
||||
}
|
||||
|
||||
if (getStepKind(state.generationMethod, state.currentStep) === 'FINAL') {
|
||||
return 'Press Enter to save, e to save and edit, Esc to go back';
|
||||
return t('Press Enter to save, e to save and edit, Esc to go back');
|
||||
}
|
||||
|
||||
// Steps that have ↑↓ navigation (RadioButtonSelect components)
|
||||
@@ -177,14 +178,17 @@ export function AgentCreationWizard({
|
||||
kindForNav === 'GEN_METHOD' ||
|
||||
kindForNav === 'TOOLS' ||
|
||||
kindForNav === 'COLOR';
|
||||
const navigationPart = hasNavigation ? '↑↓ to navigate, ' : '';
|
||||
const navigationPart = hasNavigation ? t('↑↓ to navigate, ') : '';
|
||||
|
||||
const escAction =
|
||||
state.currentStep === WIZARD_STEPS.LOCATION_SELECTION
|
||||
? 'cancel'
|
||||
: 'go back';
|
||||
? t('cancel')
|
||||
: t('go back');
|
||||
|
||||
return `Press Enter to continue, ${navigationPart}Esc to ${escAction}`;
|
||||
return t('Press Enter to continue, {{navigation}}Esc to {{action}}', {
|
||||
navigation: navigationPart,
|
||||
action: escAction,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -210,16 +214,16 @@ export function AgentCreationWizard({
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onNext={handleNext}
|
||||
description="Enter a clear, unique name for this subagent."
|
||||
placeholder="e.g., Code Reviewer"
|
||||
description={t('Enter a clear, unique name for this subagent.')}
|
||||
placeholder={t('e.g., Code Reviewer')}
|
||||
height={1}
|
||||
initialText={state.generatedName}
|
||||
onChange={(t) => {
|
||||
const value = t; // keep raw, trim later when validating
|
||||
onChange={(text) => {
|
||||
const value = text; // keep raw, trim later when validating
|
||||
dispatch({ type: 'SET_GENERATED_NAME', name: value });
|
||||
}}
|
||||
validate={(t) =>
|
||||
t.trim().length === 0 ? 'Name cannot be empty.' : null
|
||||
validate={(text) =>
|
||||
text.trim().length === 0 ? t('Name cannot be empty.') : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -230,18 +234,22 @@ export function AgentCreationWizard({
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onNext={handleNext}
|
||||
description="Write the system prompt that defines this subagent's behavior. Be comprehensive for best results."
|
||||
placeholder="e.g., You are an expert code reviewer..."
|
||||
description={t(
|
||||
"Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.",
|
||||
)}
|
||||
placeholder={t('e.g., You are an expert code reviewer...')}
|
||||
height={10}
|
||||
initialText={state.generatedSystemPrompt}
|
||||
onChange={(t) => {
|
||||
onChange={(text) => {
|
||||
dispatch({
|
||||
type: 'SET_GENERATED_SYSTEM_PROMPT',
|
||||
systemPrompt: t,
|
||||
systemPrompt: text,
|
||||
});
|
||||
}}
|
||||
validate={(t) =>
|
||||
t.trim().length === 0 ? 'System prompt cannot be empty.' : null
|
||||
validate={(text) =>
|
||||
text.trim().length === 0
|
||||
? t('System prompt cannot be empty.')
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -252,15 +260,24 @@ export function AgentCreationWizard({
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onNext={handleNext}
|
||||
description="Describe when and how this subagent should be used."
|
||||
placeholder="e.g., Reviews code for best practices and potential bugs."
|
||||
description={t(
|
||||
'Describe when and how this subagent should be used.',
|
||||
)}
|
||||
placeholder={t(
|
||||
'e.g., Reviews code for best practices and potential bugs.',
|
||||
)}
|
||||
height={6}
|
||||
initialText={state.generatedDescription}
|
||||
onChange={(t) => {
|
||||
dispatch({ type: 'SET_GENERATED_DESCRIPTION', description: t });
|
||||
onChange={(text) => {
|
||||
dispatch({
|
||||
type: 'SET_GENERATED_DESCRIPTION',
|
||||
description: text,
|
||||
});
|
||||
}}
|
||||
validate={(t) =>
|
||||
t.trim().length === 0 ? 'Description cannot be empty.' : null
|
||||
validate={(text) =>
|
||||
text.trim().length === 0
|
||||
? t('Description cannot be empty.')
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -292,7 +309,9 @@ export function AgentCreationWizard({
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>
|
||||
Invalid step: {state.currentStep}
|
||||
{t('Invalid step: {{step}}', {
|
||||
step: state.currentStep.toString(),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { theme } from '../../../semantic-colors.js';
|
||||
import { shouldShowColor, getColorForDisplay } from '../utils.js';
|
||||
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
|
||||
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
/**
|
||||
* Step 6: Final confirmation and actions.
|
||||
@@ -62,15 +63,24 @@ export function CreationSummary({
|
||||
|
||||
if (conflictLevel === targetLevel) {
|
||||
allWarnings.push(
|
||||
`Name "${state.generatedName}" already exists at ${conflictLevel} level - will overwrite existing subagent`,
|
||||
t(
|
||||
'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent',
|
||||
{ name: state.generatedName, level: conflictLevel },
|
||||
),
|
||||
);
|
||||
} else if (targetLevel === 'project') {
|
||||
allWarnings.push(
|
||||
`Name "${state.generatedName}" exists at user level - project level will take precedence`,
|
||||
t(
|
||||
'Name "{{name}}" exists at user level - project level will take precedence',
|
||||
{ name: state.generatedName },
|
||||
),
|
||||
);
|
||||
} else {
|
||||
allWarnings.push(
|
||||
`Name "${state.generatedName}" exists at project level - existing subagent will take precedence`,
|
||||
t(
|
||||
'Name "{{name}}" exists at project level - existing subagent will take precedence',
|
||||
{ name: state.generatedName },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -83,12 +93,16 @@ export function CreationSummary({
|
||||
// Check length warnings
|
||||
if (state.generatedDescription.length > 300) {
|
||||
allWarnings.push(
|
||||
`Description is over ${state.generatedDescription.length} characters`,
|
||||
t('Description is over {{length}} characters', {
|
||||
length: state.generatedDescription.length.toString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (state.generatedSystemPrompt.length > 10000) {
|
||||
allWarnings.push(
|
||||
`System prompt is over ${state.generatedSystemPrompt.length} characters`,
|
||||
t('System prompt is over {{length}} characters', {
|
||||
length: state.generatedSystemPrompt.length.toString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +195,9 @@ export function CreationSummary({
|
||||
showSuccessAndClose();
|
||||
} catch (error) {
|
||||
setSaveError(
|
||||
`Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
t('Failed to save and edit subagent: {{error}}', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
@@ -215,13 +231,15 @@ export function CreationSummary({
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<Text bold color={theme.status.success}>
|
||||
✅ Subagent Created Successfully!
|
||||
{t('✅ Subagent Created Successfully!')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>
|
||||
Subagent "{state.generatedName}" has been saved to{' '}
|
||||
{state.location} level.
|
||||
{t('Subagent "{{name}}" has been saved to {{level}} level.', {
|
||||
name: state.generatedName,
|
||||
level: state.location,
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -232,35 +250,35 @@ export function CreationSummary({
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>Name: </Text>
|
||||
<Text color={theme.text.primary}>{t('Name: ')}</Text>
|
||||
<Text color={getColorForDisplay(state.color)}>
|
||||
{state.generatedName}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>Location: </Text>
|
||||
<Text color={theme.text.primary}>{t('Location: ')}</Text>
|
||||
<Text>
|
||||
{state.location === 'project'
|
||||
? 'Project Level (.qwen/agents/)'
|
||||
: 'User Level (~/.qwen/agents/)'}
|
||||
? t('Project Level (.qwen/agents/)')
|
||||
: t('User Level (~/.qwen/agents/)')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>Tools: </Text>
|
||||
<Text color={theme.text.primary}>{t('Tools: ')}</Text>
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(state.color) && (
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>Color: </Text>
|
||||
<Text color={theme.text.primary}>{t('Color: ')}</Text>
|
||||
<Text color={getColorForDisplay(state.color)}>{state.color}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.primary}>Description:</Text>
|
||||
<Text color={theme.text.primary}>{t('Description:')}</Text>
|
||||
</Box>
|
||||
<Box padding={1} paddingBottom={0}>
|
||||
<Text wrap="wrap">
|
||||
@@ -269,7 +287,7 @@ export function CreationSummary({
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.primary}>System Prompt:</Text>
|
||||
<Text color={theme.text.primary}>{t('System Prompt:')}</Text>
|
||||
</Box>
|
||||
<Box padding={1} paddingBottom={0}>
|
||||
<Text wrap="wrap">
|
||||
@@ -281,7 +299,7 @@ export function CreationSummary({
|
||||
{saveError && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.status.error}>
|
||||
❌ Error saving subagent:
|
||||
{t('❌ Error saving subagent:')}
|
||||
</Text>
|
||||
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||
<Text color={theme.status.error} wrap="wrap">
|
||||
@@ -294,7 +312,7 @@ export function CreationSummary({
|
||||
{warnings.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.status.warning}>
|
||||
Warnings:
|
||||
{t('Warnings:')}
|
||||
</Text>
|
||||
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||
{warnings.map((warning, index) => (
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useKeypress, type Key } from '../../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../../keyMatchers.js';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { TextInput } from '../../shared/TextInput.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
/**
|
||||
* Step 3: Description input with LLM generation.
|
||||
@@ -103,7 +104,9 @@ export function DescriptionInput({
|
||||
dispatch({
|
||||
type: 'SET_VALIDATION_ERRORS',
|
||||
errors: [
|
||||
`Failed to generate subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
t('Failed to generate subagent: {{error}}', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -135,15 +138,17 @@ export function DescriptionInput({
|
||||
isActive: state.isGenerating,
|
||||
});
|
||||
|
||||
const placeholder =
|
||||
'e.g., Expert code reviewer that reviews code based on best practices...';
|
||||
const placeholder = t(
|
||||
'e.g., Expert code reviewer that reviews code based on best practices...',
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>
|
||||
Describe what this subagent should do and when it should be used. (Be
|
||||
comprehensive for best results)
|
||||
{t(
|
||||
'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -153,7 +158,7 @@ export function DescriptionInput({
|
||||
<Spinner />
|
||||
</Box>
|
||||
<Text color={theme.text.accent}>
|
||||
Generating subagent configuration...
|
||||
{t('Generating subagent configuration...')}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Box } from 'ink';
|
||||
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||
import type { WizardStepProps } from '../types.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface GenerationOption {
|
||||
label: string;
|
||||
@@ -15,11 +16,15 @@ interface GenerationOption {
|
||||
|
||||
const generationOptions: GenerationOption[] = [
|
||||
{
|
||||
label: 'Generate with Qwen Code (Recommended)',
|
||||
get label() {
|
||||
return t('Generate with Qwen Code (Recommended)');
|
||||
},
|
||||
value: 'qwen',
|
||||
},
|
||||
{
|
||||
label: 'Manual Creation',
|
||||
get label() {
|
||||
return t('Manual Creation');
|
||||
},
|
||||
value: 'manual',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Box } from 'ink';
|
||||
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||
import type { WizardStepProps } from '../types.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface LocationOption {
|
||||
label: string;
|
||||
@@ -15,11 +16,15 @@ interface LocationOption {
|
||||
|
||||
const locationOptions: LocationOption[] = [
|
||||
{
|
||||
label: 'Project Level (.qwen/agents/)',
|
||||
get label() {
|
||||
return t('Project Level (.qwen/agents/)');
|
||||
},
|
||||
value: 'project',
|
||||
},
|
||||
{
|
||||
label: 'User Level (~/.qwen/agents/)',
|
||||
get label() {
|
||||
return t('User Level (~/.qwen/agents/)');
|
||||
},
|
||||
value: 'user',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||
import type { ToolCategory } from '../types.js';
|
||||
import { Kind, type Config } from '@qwen-code/qwen-code-core';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface ToolOption {
|
||||
label: string;
|
||||
@@ -45,7 +46,7 @@ export function ToolSelector({
|
||||
toolCategories: [
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All Tools (Default)',
|
||||
name: t('All Tools (Default)'),
|
||||
tools: [],
|
||||
},
|
||||
],
|
||||
@@ -89,22 +90,22 @@ export function ToolSelector({
|
||||
const toolCategories = [
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All Tools',
|
||||
name: t('All Tools'),
|
||||
tools: [],
|
||||
},
|
||||
{
|
||||
id: 'read',
|
||||
name: 'Read-only Tools',
|
||||
name: t('Read-only Tools'),
|
||||
tools: readTools,
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
name: 'Read & Edit Tools',
|
||||
name: t('Read & Edit Tools'),
|
||||
tools: [...readTools, ...editTools],
|
||||
},
|
||||
{
|
||||
id: 'execute',
|
||||
name: 'Read & Edit & Execution Tools',
|
||||
name: t('Read & Edit & Execution Tools'),
|
||||
tools: [...readTools, ...editTools, ...executeTools],
|
||||
},
|
||||
].filter((category) => category.id === 'all' || category.tools.length > 0);
|
||||
@@ -202,11 +203,11 @@ export function ToolSelector({
|
||||
<Box flexDirection="column">
|
||||
{currentCategory.id === 'all' ? (
|
||||
<Text color={theme.text.secondary}>
|
||||
All tools selected, including MCP tools
|
||||
{t('All tools selected, including MCP tools')}
|
||||
</Text>
|
||||
) : currentCategory.tools.length > 0 ? (
|
||||
<>
|
||||
<Text color={theme.text.secondary}>Selected tools:</Text>
|
||||
<Text color={theme.text.secondary}>{t('Selected tools:')}</Text>
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
{(() => {
|
||||
// Filter the already categorized tools to show only those in current category
|
||||
@@ -224,17 +225,19 @@ export function ToolSelector({
|
||||
<>
|
||||
{categoryReadTools.length > 0 && (
|
||||
<Text color={theme.text.secondary}>
|
||||
• Read-only tools: {categoryReadTools.join(', ')}
|
||||
• {t('Read-only tools:')}{' '}
|
||||
{categoryReadTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{categoryEditTools.length > 0 && (
|
||||
<Text color={theme.text.secondary}>
|
||||
• Edit tools: {categoryEditTools.join(', ')}
|
||||
• {t('Edit tools:')} {categoryEditTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{categoryExecuteTools.length > 0 && (
|
||||
<Text color={theme.text.secondary}>
|
||||
• Execution tools: {categoryExecuteTools.join(', ')}
|
||||
• {t('Execution tools:')}{' '}
|
||||
{categoryExecuteTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box } from 'ink';
|
||||
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||
import { MANAGEMENT_STEPS } from '../types.js';
|
||||
import { type SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface ActionSelectionStepProps {
|
||||
selectedAgent: SubagentConfig | null;
|
||||
@@ -27,10 +28,34 @@ export const ActionSelectionStep = ({
|
||||
|
||||
// Filter actions based on whether the agent is built-in
|
||||
const allActions = [
|
||||
{ key: 'view', label: 'View Agent', value: 'view' as const },
|
||||
{ key: 'edit', label: 'Edit Agent', value: 'edit' as const },
|
||||
{ key: 'delete', label: 'Delete Agent', value: 'delete' as const },
|
||||
{ key: 'back', label: 'Back', value: 'back' as const },
|
||||
{
|
||||
key: 'view',
|
||||
get label() {
|
||||
return t('View Agent');
|
||||
},
|
||||
value: 'view' as const,
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
get label() {
|
||||
return t('Edit Agent');
|
||||
},
|
||||
value: 'edit' as const,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
get label() {
|
||||
return t('Delete Agent');
|
||||
},
|
||||
value: 'delete' as const,
|
||||
},
|
||||
{
|
||||
key: 'back',
|
||||
get label() {
|
||||
return t('Back');
|
||||
},
|
||||
value: 'back' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const actions = selectedAgent?.isBuiltin
|
||||
|
||||
@@ -9,6 +9,7 @@ import { type SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
import type { StepNavigationProps } from '../types.js';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface AgentDeleteStepProps extends StepNavigationProps {
|
||||
selectedAgent: SubagentConfig | null;
|
||||
@@ -41,7 +42,7 @@ export function AgentDeleteStep({
|
||||
if (!selectedAgent) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>No agent selected</Text>
|
||||
<Text color={theme.status.error}>{t('No agent selected')}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -49,8 +50,9 @@ export function AgentDeleteStep({
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color={theme.status.error}>
|
||||
Are you sure you want to delete agent “{selectedAgent.name}
|
||||
”?
|
||||
{t('Are you sure you want to delete agent "{{name}}"?', {
|
||||
name: selectedAgent.name,
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MANAGEMENT_STEPS } from '../types.js';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
|
||||
import { type SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface EditOption {
|
||||
id: string;
|
||||
@@ -20,15 +21,21 @@ interface EditOption {
|
||||
const editOptions: EditOption[] = [
|
||||
{
|
||||
id: 'editor',
|
||||
label: 'Open in editor',
|
||||
get label() {
|
||||
return t('Open in editor');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
label: 'Edit tools',
|
||||
get label() {
|
||||
return t('Edit tools');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'color',
|
||||
label: 'Edit color',
|
||||
get label() {
|
||||
return t('Edit color');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -65,7 +72,9 @@ export function EditOptionsStep({
|
||||
await launchEditor(selectedAgent?.filePath);
|
||||
} catch (err) {
|
||||
setError(
|
||||
`Failed to launch editor: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
||||
t('Failed to launch editor: {{error}}', {
|
||||
error: err instanceof Error ? err.message : 'Unknown error',
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (selectedValue === 'tools') {
|
||||
@@ -98,7 +107,7 @@ export function EditOptionsStep({
|
||||
{error && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.status.error}>
|
||||
❌ Error:
|
||||
{t('❌ Error:')}
|
||||
</Text>
|
||||
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||
<Text color={theme.status.error} wrap="wrap">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||
import { type SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface NavigationState {
|
||||
currentBlock: 'project' | 'user' | 'builtin';
|
||||
@@ -205,9 +206,9 @@ export const AgentSelectionStep = ({
|
||||
if (availableAgents.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>No subagents found.</Text>
|
||||
<Text color={theme.text.secondary}>{t('No subagents found.')}</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Use '/agents create' to create your first subagent.
|
||||
{t("Use '/agents create' to create your first subagent.")}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -237,7 +238,7 @@ export const AgentSelectionStep = ({
|
||||
{agent.isBuiltin && (
|
||||
<Text color={isSelected ? theme.text.accent : theme.text.secondary}>
|
||||
{' '}
|
||||
(built-in)
|
||||
{t('(built-in)')}
|
||||
</Text>
|
||||
)}
|
||||
{agent.level === 'user' && projectNames.has(agent.name) && (
|
||||
@@ -245,7 +246,7 @@ export const AgentSelectionStep = ({
|
||||
color={isSelected ? theme.status.warning : theme.text.secondary}
|
||||
>
|
||||
{' '}
|
||||
(overridden by project level agent)
|
||||
{t('(overridden by project level agent)')}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
@@ -265,7 +266,9 @@ export const AgentSelectionStep = ({
|
||||
{projectAgents.length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.text.primary} bold>
|
||||
Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')})
|
||||
{t('Project Level ({{path}})', {
|
||||
path: projectAgents[0].filePath.replace(/\/[^/]+$/, ''),
|
||||
})}
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{projectAgents.map((agent, index) => {
|
||||
@@ -285,7 +288,9 @@ export const AgentSelectionStep = ({
|
||||
marginBottom={builtinAgents.length > 0 ? 1 : 0}
|
||||
>
|
||||
<Text color={theme.text.primary} bold>
|
||||
User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')})
|
||||
{t('User Level ({{path}})', {
|
||||
path: userAgents[0].filePath.replace(/\/[^/]+$/, ''),
|
||||
})}
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{userAgents.map((agent, index) => {
|
||||
@@ -302,7 +307,7 @@ export const AgentSelectionStep = ({
|
||||
{builtinAgents.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.primary} bold>
|
||||
Built-in Agents
|
||||
{t('Built-in Agents')}
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{builtinAgents.map((agent, index) => {
|
||||
@@ -321,7 +326,9 @@ export const AgentSelectionStep = ({
|
||||
builtinAgents.length > 0) && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
Using: {enabledAgentsCount} agents
|
||||
{t('Using: {{count}} agents', {
|
||||
count: enabledAgentsCount.toString(),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Box, Text } from 'ink';
|
||||
import { theme } from '../../../semantic-colors.js';
|
||||
import { shouldShowColor, getColorForDisplay } from '../utils.js';
|
||||
import { type SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface AgentViewerStepProps {
|
||||
selectedAgent: SubagentConfig | null;
|
||||
@@ -17,7 +18,7 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
|
||||
if (!selectedAgent) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>No agent selected</Text>
|
||||
<Text color={theme.status.error}>{t('No agent selected')}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -30,31 +31,31 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>File Path: </Text>
|
||||
<Text color={theme.text.primary}>{t('File Path: ')}</Text>
|
||||
<Text>{agent.filePath}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>Tools: </Text>
|
||||
<Text color={theme.text.primary}>{t('Tools: ')}</Text>
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(agent.color) && (
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>Color: </Text>
|
||||
<Text color={theme.text.primary}>{t('Color: ')}</Text>
|
||||
<Text color={getColorForDisplay(agent.color)}>{agent.color}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.primary}>Description:</Text>
|
||||
<Text color={theme.text.primary}>{t('Description:')}</Text>
|
||||
</Box>
|
||||
<Box padding={1} paddingBottom={0}>
|
||||
<Text wrap="wrap">{agent.description}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.primary}>System Prompt:</Text>
|
||||
<Text color={theme.text.primary}>{t('System Prompt:')}</Text>
|
||||
</Box>
|
||||
<Box padding={1} paddingBottom={0}>
|
||||
<Text wrap="wrap">{agent.systemPrompt}</Text>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { theme } from '../../../semantic-colors.js';
|
||||
import { getColorForDisplay, shouldShowColor } from '../utils.js';
|
||||
import type { SubagentConfig, Config } from '@qwen-code/qwen-code-core';
|
||||
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||
import { t } from '../../../../i18n/index.js';
|
||||
|
||||
interface AgentsManagerDialogProps {
|
||||
onClose: () => void;
|
||||
@@ -143,21 +144,21 @@ export function AgentsManagerDialog({
|
||||
const getStepHeaderText = () => {
|
||||
switch (currentStep) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return 'Agents';
|
||||
return t('Agents');
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return 'Choose Action';
|
||||
return t('Choose Action');
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return selectedAgent?.name;
|
||||
case MANAGEMENT_STEPS.EDIT_OPTIONS:
|
||||
return `Edit ${selectedAgent?.name}`;
|
||||
return t('Edit {{name}}', { name: selectedAgent?.name || '' });
|
||||
case MANAGEMENT_STEPS.EDIT_TOOLS:
|
||||
return `Edit Tools: ${selectedAgent?.name}`;
|
||||
return t('Edit Tools: {{name}}', { name: selectedAgent?.name || '' });
|
||||
case MANAGEMENT_STEPS.EDIT_COLOR:
|
||||
return `Edit Color: ${selectedAgent?.name}`;
|
||||
return t('Edit Color: {{name}}', { name: selectedAgent?.name || '' });
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return `Delete ${selectedAgent?.name}`;
|
||||
return t('Delete {{name}}', { name: selectedAgent?.name || '' });
|
||||
default:
|
||||
return 'Unknown Step';
|
||||
return t('Unknown Step');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -183,20 +184,20 @@ export function AgentsManagerDialog({
|
||||
const getNavigationInstructions = () => {
|
||||
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
if (availableAgents.length === 0) {
|
||||
return 'Esc to close';
|
||||
return t('Esc to close');
|
||||
}
|
||||
return 'Enter to select, ↑↓ to navigate, Esc to close';
|
||||
return t('Enter to select, ↑↓ to navigate, Esc to close');
|
||||
}
|
||||
|
||||
if (currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
|
||||
return 'Esc to go back';
|
||||
return t('Esc to go back');
|
||||
}
|
||||
|
||||
if (currentStep === MANAGEMENT_STEPS.DELETE_CONFIRMATION) {
|
||||
return 'Enter to confirm, Esc to cancel';
|
||||
return t('Enter to confirm, Esc to cancel');
|
||||
}
|
||||
|
||||
return 'Enter to select, ↑↓ to navigate, Esc to go back';
|
||||
return t('Enter to select, ↑↓ to navigate, Esc to go back');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -295,7 +296,9 @@ export function AgentsManagerDialog({
|
||||
default:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>Invalid step: {currentStep}</Text>
|
||||
<Text color={theme.status.error}>
|
||||
{t('Invalid step: {{step}}', { step: currentStep })}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type ToolDefinition } from '../../types.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface ToolsListProps {
|
||||
tools: readonly ToolDefinition[];
|
||||
@@ -23,7 +24,7 @@ export const ToolsList: React.FC<ToolsListProps> = ({
|
||||
}) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Available Qwen Code CLI tools:
|
||||
{t('Available Qwen Code CLI tools:')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{tools.length > 0 ? (
|
||||
@@ -46,7 +47,7 @@ export const ToolsList: React.FC<ToolsListProps> = ({
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Text color={theme.text.primary}> No tools available</Text>
|
||||
<Text color={theme.text.primary}> {t('No tools available')}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user