diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 8ae18e16e4..adf73cf09e 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1380,10 +1380,26 @@ export default { 'Erweiterungsseite wird im Browser geöffnet: {{url}}', 'Failed to open browser. Check out the extensions gallery at {{url}}': 'Browser konnte nicht geöffnet werden. Besuchen Sie die Erweiterungsgalerie unter {{url}}', + 'Use /compress when the conversation gets long to summarize history and free up context.': + 'Verwenden Sie /compress, wenn die Unterhaltung lang wird, um den Verlauf zusammenzufassen und Kontext freizugeben.', + 'Start a fresh idea with /clear or /new; the previous session stays available in history.': + 'Starten Sie eine neue Idee mit /clear oder /new; die vorherige Sitzung bleibt im Verlauf verfügbar.', + 'Use /bug to submit issues to the maintainers when something goes off.': + 'Verwenden Sie /bug, um Probleme an die Betreuer zu melden, wenn etwas schiefgeht.', + 'Switch auth type quickly with /auth.': + 'Wechseln Sie den Authentifizierungstyp schnell mit /auth.', + 'You can run any shell commands from Qwen Code using ! (e.g. !ls).': + 'Sie können beliebige Shell-Befehle in Qwen Code mit ! ausführen (z. B. !ls).', + 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.': + 'Geben Sie / ein, um das Befehlsmenü zu öffnen; Tab vervollständigt Slash-Befehle und gespeicherte Prompts.', + 'You can resume a previous conversation by running qwen --continue or qwen --resume.': + 'Sie können eine frühere Unterhaltung mit qwen --continue oder qwen --resume fortsetzen.', 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': 'Sie können den Berechtigungsmodus schnell mit Shift+Tab oder /approval-mode wechseln.', 'You can switch permission mode quickly with Tab or /approval-mode.': 'Sie können den Berechtigungsmodus schnell mit Tab oder /approval-mode wechseln.', + 'Try /insight to generate personalized insights from your chat history.': + 'Probieren Sie /insight, um personalisierte Erkenntnisse aus Ihrem Chatverlauf zu erstellen.', // ============================================================================ // Custom API-KEY Configuration diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 0d3d422a70..c9cf2bb938 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1115,6 +1115,8 @@ export default { 'You can switch permission mode quickly with Shift+Tab or /approval-mode.', 'You can switch permission mode quickly with Tab or /approval-mode.': 'You can switch permission mode quickly with Tab or /approval-mode.', + 'Try /insight to generate personalized insights from your chat history.': + 'Try /insight to generate personalized insights from your chat history.', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index 9632d5675f..c0eb9d64f3 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -783,6 +783,27 @@ export default { "Starting OAuth authentication for MCP server '{{name}}'...": "MCPサーバー '{{name}}' のOAuth認証を開始中...", // Startup Tips + 'Tips:': 'ヒント:', + 'Use /compress when the conversation gets long to summarize history and free up context.': + '会話が長くなったら /compress で履歴を要約し、コンテキストを解放できます。', + 'Start a fresh idea with /clear or /new; the previous session stays available in history.': + '/clear または /new で新しいアイデアを始められます。前のセッションは履歴に残ります。', + 'Use /bug to submit issues to the maintainers when something goes off.': + '問題が発生したら /bug でメンテナーに報告できます。', + 'Switch auth type quickly with /auth.': + '/auth で認証タイプをすばやく切り替えられます。', + 'You can run any shell commands from Qwen Code using ! (e.g. !ls).': + 'Qwen Code から ! を使って任意のシェルコマンドを実行できます(例: !ls)。', + 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.': + '/ を入力してコマンドポップアップを開きます。Tab でスラッシュコマンドと保存済みプロンプトを補完できます。', + 'You can resume a previous conversation by running qwen --continue or qwen --resume.': + 'qwen --continue または qwen --resume で前の会話を再開できます。', + 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': + 'Shift+Tab または /approval-mode で権限モードをすばやく切り替えられます。', + 'You can switch permission mode quickly with Tab or /approval-mode.': + 'Tab または /approval-mode で権限モードをすばやく切り替えられます。', + 'Try /insight to generate personalized insights from your chat history.': + '/insight でチャット履歴からパーソナライズされたインサイトを生成できます。', 'Tips for getting started:': '始めるためのヒント:', '1. Ask questions, edit files, or run commands.': '1. 質問したり、ファイルを編集したり、コマンドを実行したりできます', diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index d630879d1a..8e1e8267d6 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1132,6 +1132,8 @@ export default { 'Você pode retomar uma conversa anterior executando qwen --continue ou qwen --resume.', 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': 'Você pode alternar o modo de permissão rapidamente com Shift+Tab ou /approval-mode.', + 'Try /insight to generate personalized insights from your chat history.': + 'Experimente /insight para gerar insights personalizados do seu histórico de conversas.', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index b8b332b769..a9ddd19459 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1384,10 +1384,26 @@ export default { 'Открываем страницу расширений в браузере: {{url}}', 'Failed to open browser. Check out the extensions gallery at {{url}}': 'Не удалось открыть браузер. Посетите галерею расширений по адресу {{url}}', + 'Use /compress when the conversation gets long to summarize history and free up context.': + 'Используйте /compress, когда разговор становится длинным, чтобы подвести итог и освободить контекст.', + 'Start a fresh idea with /clear or /new; the previous session stays available in history.': + 'Начните новую идею с /clear или /new; предыдущая сессия останется в истории.', + 'Use /bug to submit issues to the maintainers when something goes off.': + 'Используйте /bug, чтобы сообщить о проблемах разработчикам.', + 'Switch auth type quickly with /auth.': + 'Быстро переключите тип аутентификации с помощью /auth.', + 'You can run any shell commands from Qwen Code using ! (e.g. !ls).': + 'Вы можете выполнять любые shell-команды в Qwen Code с помощью ! (например, !ls).', + 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.': + 'Введите /, чтобы открыть меню команд; Tab автодополняет слэш-команды и сохранённые промпты.', + 'You can resume a previous conversation by running qwen --continue or qwen --resume.': + 'Вы можете продолжить предыдущий разговор, запустив qwen --continue или qwen --resume.', 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': 'Вы можете быстро переключать режим разрешений с помощью Shift+Tab или /approval-mode.', 'You can switch permission mode quickly with Tab or /approval-mode.': 'Вы можете быстро переключать режим разрешений с помощью Tab или /approval-mode.', + 'Try /insight to generate personalized insights from your chat history.': + 'Попробуйте /insight, чтобы получить персонализированные выводы из истории чатов.', // ============================================================================ // Custom API-KEY Configuration diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 02ae707b63..7dba55bafc 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1052,6 +1052,8 @@ export default { '按 Shift+Tab 或输入 /approval-mode 可快速切换权限模式。', 'You can switch permission mode quickly with Tab or /approval-mode.': '按 Tab 或输入 /approval-mode 可快速切换权限模式。', + 'Try /insight to generate personalized insights from your chat history.': + '试试 /insight,从聊天记录中生成个性化洞察。', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/ui/components/Tips.test.ts b/packages/cli/src/ui/components/Tips.test.ts new file mode 100644 index 0000000000..dd2c25ea9d --- /dev/null +++ b/packages/cli/src/ui/components/Tips.test.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi } from 'vitest'; +import { selectWeightedTip } from './Tips.js'; + +describe('selectWeightedTip', () => { + const tips = [ + { text: 'tip-a', weight: 1 }, + { text: 'tip-b', weight: 3 }, + { text: 'tip-c', weight: 1 }, + ]; + + it('returns a valid tip text', () => { + const result = selectWeightedTip(tips); + expect(['tip-a', 'tip-b', 'tip-c']).toContain(result); + }); + + it('selects the first tip when random is near zero', () => { + vi.spyOn(Math, 'random').mockReturnValue(0); + expect(selectWeightedTip(tips)).toBe('tip-a'); + vi.restoreAllMocks(); + }); + + it('selects the weighted tip when random falls in its range', () => { + // Total weight = 5. tip-a covers [0,1), tip-b covers [1,4), tip-c covers [4,5) + // Math.random() * 5 = 2.0 falls in tip-b's range + vi.spyOn(Math, 'random').mockReturnValue(0.4); // 0.4 * 5 = 2.0 + expect(selectWeightedTip(tips)).toBe('tip-b'); + vi.restoreAllMocks(); + }); + + it('selects the last tip when random is near max', () => { + vi.spyOn(Math, 'random').mockReturnValue(0.99); + expect(selectWeightedTip(tips)).toBe('tip-c'); + vi.restoreAllMocks(); + }); + + it('respects weight distribution over many samples', () => { + const counts: Record = { + 'tip-a': 0, + 'tip-b': 0, + 'tip-c': 0, + }; + const iterations = 10000; + for (let i = 0; i < iterations; i++) { + const result = selectWeightedTip(tips); + counts[result]!++; + } + // tip-b (weight 3) should appear roughly 3x as often as tip-a or tip-c (weight 1) + // With 10k iterations, we expect: tip-a ~2000, tip-b ~6000, tip-c ~2000 + expect(counts['tip-b']!).toBeGreaterThan(counts['tip-a']! * 2); + expect(counts['tip-b']!).toBeGreaterThan(counts['tip-c']! * 2); + }); + + it('handles single tip', () => { + expect(selectWeightedTip([{ text: 'only', weight: 1 }])).toBe('only'); + }); +}); diff --git a/packages/cli/src/ui/components/Tips.tsx b/packages/cli/src/ui/components/Tips.tsx index d1b6a71bfd..f85184a194 100644 --- a/packages/cli/src/ui/components/Tips.tsx +++ b/packages/cli/src/ui/components/Tips.tsx @@ -9,7 +9,9 @@ import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { t } from '../../i18n/index.js'; -const startupTips = [ +type Tip = string | { text: string; weight: number }; + +const startupTips: Tip[] = [ 'Use /compress when the conversation gets long to summarize history and free up context.', 'Start a fresh idea with /clear or /new; the previous session stays available in history.', 'Use /bug to submit issues to the maintainers when something goes off.', @@ -20,13 +22,34 @@ const startupTips = [ process.platform === 'win32' ? 'You can switch permission mode quickly with Tab or /approval-mode.' : 'You can switch permission mode quickly with Shift+Tab or /approval-mode.', -] as const; + { + text: 'Try /insight to generate personalized insights from your chat history.', + weight: 3, + }, +]; + +function tipText(tip: Tip): string { + return typeof tip === 'string' ? tip : tip.text; +} + +function tipWeight(tip: Tip): number { + return typeof tip === 'string' ? 1 : tip.weight; +} + +export function selectWeightedTip(tips: Tip[]): string { + const totalWeight = tips.reduce((sum, tip) => sum + tipWeight(tip), 0); + let random = Math.random() * totalWeight; + for (const tip of tips) { + random -= tipWeight(tip); + if (random <= 0) { + return tipText(tip); + } + } + return tipText(tips[tips.length - 1]!); +} export const Tips: React.FC = () => { - const selectedTip = useMemo(() => { - const randomIndex = Math.floor(Math.random() * startupTips.length); - return startupTips[randomIndex]; - }, []); + const selectedTip = useMemo(() => selectWeightedTip(startupTips), []); return (