From b804b1f48a45a004ec24c28593e5ff0efcf9a63e Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Fri, 16 Jan 2026 11:48:31 +0800 Subject: [PATCH 01/13] feat: Redesign CLI welcome screen and improve visual consistency --- docs/users/configuration/settings.md | 4 - docs/users/reference/keyboard-shortcuts.md | 2 + packages/cli/src/config/config.test.ts | 64 ----- packages/cli/src/config/config.ts | 12 - packages/cli/src/config/settings.test.ts | 4 +- packages/cli/src/config/settings.ts | 7 - .../cli/src/config/settingsSchema.test.ts | 6 - packages/cli/src/config/settingsSchema.ts | 82 ------ packages/cli/src/gemini.test.tsx | 1 - packages/cli/src/i18n/locales/de.js | 7 - packages/cli/src/i18n/locales/en.js | 52 ++-- packages/cli/src/i18n/locales/ru.js | 28 +- packages/cli/src/i18n/locales/zh.js | 50 +++- packages/cli/src/ui/App.tsx | 25 +- packages/cli/src/ui/AppContainer.test.tsx | 28 -- packages/cli/src/ui/AppContainer.tsx | 3 +- packages/cli/src/ui/components/AboutBox.tsx | 9 +- .../cli/src/ui/components/AppHeader.test.tsx | 93 +++++++ packages/cli/src/ui/components/AppHeader.tsx | 22 +- packages/cli/src/ui/components/AsciiArt.ts | 23 +- .../cli/src/ui/components/Composer.test.tsx | 115 +++----- packages/cli/src/ui/components/Composer.tsx | 90 ++----- .../src/ui/components/ConsentPrompt.test.tsx | 2 +- .../cli/src/ui/components/ConsentPrompt.tsx | 2 +- .../src/ui/components/ContextUsageDisplay.tsx | 8 +- .../cli/src/ui/components/Footer.test.tsx | 218 ++------------- packages/cli/src/ui/components/Footer.tsx | 188 ++++++------- .../cli/src/ui/components/Header.test.tsx | 91 +++++-- packages/cli/src/ui/components/Header.tsx | 133 +++++++--- packages/cli/src/ui/components/Help.tsx | 5 +- .../src/ui/components/HistoryItemDisplay.tsx | 42 ++- .../cli/src/ui/components/InputPrompt.tsx | 38 ++- .../src/ui/components/KeyboardShortcuts.tsx | 118 +++++++++ .../cli/src/ui/components/MainContent.tsx | 7 +- .../src/ui/components/ModelStatsDisplay.tsx | 10 +- .../src/ui/components/PlanSummaryDisplay.tsx | 2 +- .../cli/src/ui/components/QuittingDisplay.tsx | 2 + .../cli/src/ui/components/SessionPicker.tsx | 10 +- .../ui/components/SessionSummaryDisplay.tsx | 3 + .../src/ui/components/SettingsDialog.test.tsx | 8 +- .../cli/src/ui/components/StatsDisplay.tsx | 3 + .../src/ui/components/SuggestionsDisplay.tsx | 4 +- .../cli/src/ui/components/ThemeDialog.tsx | 2 +- packages/cli/src/ui/components/Tips.tsx | 51 ++-- .../src/ui/components/ToolStatsDisplay.tsx | 11 +- .../__snapshots__/Footer.test.tsx.snap | 10 +- .../HistoryItemDisplay.test.tsx.snap | 248 +++++++++--------- .../__snapshots__/InputPrompt.test.tsx.snap | 16 +- .../ToolStatsDisplay.test.tsx.snap | 140 +++++----- .../components/messages/DiffRenderer.test.tsx | 22 +- .../ui/components/messages/DiffRenderer.tsx | 14 +- .../ui/components/messages/ErrorMessage.tsx | 2 +- .../ui/components/messages/GeminiMessage.tsx | 6 +- .../messages/GeminiMessageContent.tsx | 6 +- .../messages/GeminiThoughtMessage.tsx | 8 +- .../messages/GeminiThoughtMessageContent.tsx | 8 +- .../ui/components/messages/InfoMessage.tsx | 2 +- .../messages/ToolConfirmationMessage.test.tsx | 14 +- .../messages/ToolConfirmationMessage.tsx | 13 +- .../messages/ToolGroupMessage.test.tsx | 4 +- .../components/messages/ToolGroupMessage.tsx | 16 +- .../components/messages/ToolMessage.test.tsx | 4 +- .../ui/components/messages/ToolMessage.tsx | 19 +- .../ui/components/messages/WarningMessage.tsx | 2 +- .../ToolGroupMessage.test.tsx.snap | 127 ++++----- .../runtime/AgentExecutionDisplay.tsx | 4 +- .../ui/components/views/ToolsList.test.tsx | 10 +- .../cli/src/ui/components/views/ToolsList.tsx | 8 +- .../__snapshots__/ToolsList.test.tsx.snap | 9 +- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 1 - packages/cli/src/ui/hooks/useTerminalSize.ts | 14 +- .../cli/src/ui/layouts/DefaultAppLayout.tsx | 18 +- .../src/ui/layouts/ScreenReaderAppLayout.tsx | 10 +- .../cli/src/ui/utils/MarkdownDisplay.test.tsx | 2 +- packages/cli/src/ui/utils/MarkdownDisplay.tsx | 28 +- packages/cli/src/ui/utils/TableRenderer.tsx | 7 +- packages/core/src/config/config.test.ts | 19 ++ packages/core/src/config/config.ts | 7 - packages/core/src/subagents/subagent.test.ts | 22 ++ packages/core/src/utils/paths.test.ts | 173 ++++++++++++ packages/core/src/utils/paths.ts | 118 +++++---- 81 files changed, 1474 insertions(+), 1342 deletions(-) create mode 100644 packages/cli/src/ui/components/AppHeader.test.tsx create mode 100644 packages/cli/src/ui/components/KeyboardShortcuts.tsx diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 58f0543ef..613b819a4 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -74,9 +74,6 @@ Settings are organized into categories. All settings should be placed within the | `ui.customThemes` | object | Custom theme definitions. | `{}` | | `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` | | `ui.hideTips` | boolean | Hide helpful tips in the UI. | `false` | -| `ui.hideBanner` | boolean | Hide the application banner. | `false` | -| `ui.hideFooter` | boolean | Hide the footer from the UI. | `false` | -| `ui.showMemoryUsage` | boolean | Display memory usage information in the UI. | `false` | | `ui.showLineNumbers` | boolean | Show line numbers in code blocks in the CLI output. | `true` | | `ui.showCitations` | boolean | Show citations for generated text in the chat. | `true` | | `enableWelcomeBack` | boolean | Show welcome back dialog when returning to a project with conversation history. When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. This feature integrates with the `/summary` command and quit confirmation dialog. | `true` | @@ -356,7 +353,6 @@ Here is an example of a `settings.json` file with the nested structure, new as o }, "ui": { "theme": "GitHub", - "hideBanner": true, "hideTips": false, "customWittyPhrases": [ "You forget a thousand things every day. Make sure this is one of 'em", diff --git a/docs/users/reference/keyboard-shortcuts.md b/docs/users/reference/keyboard-shortcuts.md index b029c4a03..46f3c8c42 100644 --- a/docs/users/reference/keyboard-shortcuts.md +++ b/docs/users/reference/keyboard-shortcuts.md @@ -20,6 +20,7 @@ This document lists the available keyboard shortcuts in Qwen Code. | Shortcut | Description | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `!` | Toggle shell mode when the input is empty. | +| `?` | Toggle keyboard shortcuts display when the input is empty. | | `\` (at end of line) + `Enter` | Insert a newline. | | `Down Arrow` | Navigate down through the input history. | | `Enter` | Submit the current prompt. | @@ -38,6 +39,7 @@ This document lists the available keyboard shortcuts in Qwen Code. | `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B` | Move the cursor one word to the left. | | `Ctrl+N` | Navigate down through the input history. | | `Ctrl+P` | Navigate up through the input history. | +| `Ctrl+R` | Reverse search through input/shell history. | | `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F` | Move the cursor one word to the right. | | `Ctrl+U` | Delete from the cursor to the beginning of the line. | | `Ctrl+V` | Paste clipboard content. If the clipboard contains an image, it will be saved and a reference to it will be inserted in the prompt. | diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 850d4a822..ebdabfee4 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -553,70 +553,6 @@ describe('loadCliConfig', () => { expect(config.getIncludePartialMessages()).toBe(true); }); - it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => { - process.argv = ['node', 'script.js', '--show-memory-usage']; - const argv = await parseArguments({} as Settings); - const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - argv, - ); - expect(config.getShowMemoryUsage()).toBe(true); - }); - - it('should set showMemoryUsage to false when --memory flag is not present', async () => { - process.argv = ['node', 'script.js']; - const argv = await parseArguments({} as Settings); - const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - argv, - ); - expect(config.getShowMemoryUsage()).toBe(false); - }); - - it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => { - process.argv = ['node', 'script.js']; - const argv = await parseArguments({} as Settings); - const settings: Settings = { ui: { showMemoryUsage: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - argv, - ); - expect(config.getShowMemoryUsage()).toBe(false); - }); - - it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => { - process.argv = ['node', 'script.js', '--show-memory-usage']; - const argv = await parseArguments({} as Settings); - const settings: Settings = { ui: { showMemoryUsage: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - argv, - ); - expect(config.getShowMemoryUsage()).toBe(true); - }); - describe('Proxy configuration', () => { const originalProxyEnv: { [key: string]: string | undefined } = {}; const proxyEnvVars = [ diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 8dd09a238..42ed430d5 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -105,7 +105,6 @@ export interface CliArgs { prompt: string | undefined; promptInteractive: string | undefined; allFiles: boolean | undefined; - showMemoryUsage: boolean | undefined; yolo: boolean | undefined; approvalMode: string | undefined; telemetry: boolean | undefined; @@ -298,11 +297,6 @@ export async function parseArguments(settings: Settings): Promise { description: 'Include ALL files in context?', default: false, }) - .option('show-memory-usage', { - type: 'boolean', - description: 'Show memory usage in status bar', - default: false, - }) .option('yolo', { alias: 'y', type: 'boolean', @@ -498,10 +492,6 @@ export async function parseArguments(settings: Settings): Promise { ], description: 'Authentication type', }) - .deprecateOption( - 'show-memory-usage', - 'Use the "ui.showMemoryUsage" setting in settings.json instead. This flag will be removed in a future version.', - ) .deprecateOption( 'sandbox-image', 'Use the "tools.sandbox" setting in settings.json instead. This flag will be removed in a future version.', @@ -1014,8 +1004,6 @@ export async function loadCliConfig( userMemory: memoryContent, geminiMdFileCount: fileCount, approvalMode, - showMemoryUsage: - argv.showMemoryUsage || settings.ui?.showMemoryUsage || false, accessibility: { ...settings.ui?.accessibility, screenReader, diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 6549f6f71..25a898393 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2260,7 +2260,7 @@ describe('Settings Loading and Merging', () => { disableAutoUpdate: true, }, ui: { - hideBanner: true, + hideTips: true, customThemes: { myTheme: {}, }, @@ -2283,7 +2283,7 @@ describe('Settings Loading and Merging', () => { const v1Settings = migrateSettingsToV1(v2Settings); expect(v1Settings).toEqual({ disableAutoUpdate: true, - hideBanner: true, + hideTips: true, customThemes: { myTheme: {}, }, diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index c9b845cd8..6f2f4fcd9 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -90,13 +90,6 @@ const MIGRATION_MAP: Record = { hideWindowTitle: 'ui.hideWindowTitle', showStatusInTitle: 'ui.showStatusInTitle', hideTips: 'ui.hideTips', - hideBanner: 'ui.hideBanner', - hideFooter: 'ui.hideFooter', - hideCWD: 'ui.footer.hideCWD', - hideSandboxStatus: 'ui.footer.hideSandboxStatus', - hideModelInfo: 'ui.footer.hideModelInfo', - hideContextSummary: 'ui.hideContextSummary', - showMemoryUsage: 'ui.showMemoryUsage', showLineNumbers: 'ui.showLineNumbers', showCitations: 'ui.showCitations', ideMode: 'ide.enabled', diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 407d54414..68c60759b 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -157,9 +157,6 @@ describe('SettingsSchema', () => { it('should have showInDialog property configured', () => { // Check that user-facing settings are marked for dialog display - expect( - getSettingsSchema().ui.properties.showMemoryUsage.showInDialog, - ).toBe(true); expect(getSettingsSchema().general.properties.vimMode.showInDialog).toBe( true, ); @@ -175,9 +172,6 @@ describe('SettingsSchema', () => { expect(getSettingsSchema().ui.properties.hideTips.showInDialog).toBe( true, ); - expect(getSettingsSchema().ui.properties.hideBanner.showInDialog).toBe( - true, - ); expect( getSettingsSchema().privacy.properties.usageStatisticsEnabled .showInDialog, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 74b63a7b9..6df10b547 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -321,82 +321,6 @@ const SETTINGS_SCHEMA = { description: 'Hide helpful tips in the UI', showInDialog: true, }, - hideBanner: { - type: 'boolean', - label: 'Hide Banner', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide the application banner', - showInDialog: true, - }, - hideContextSummary: { - type: 'boolean', - label: 'Hide Context Summary', - category: 'UI', - requiresRestart: false, - default: false, - description: - 'Hide the context summary (QWEN.md, MCP servers) above the input.', - showInDialog: true, - }, - footer: { - type: 'object', - label: 'Footer', - category: 'UI', - requiresRestart: false, - default: {}, - description: 'Settings for the footer.', - showInDialog: false, - properties: { - hideCWD: { - type: 'boolean', - label: 'Hide CWD', - category: 'UI', - requiresRestart: false, - default: false, - description: - 'Hide the current working directory path in the footer.', - showInDialog: true, - }, - hideSandboxStatus: { - type: 'boolean', - label: 'Hide Sandbox Status', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide the sandbox status indicator in the footer.', - showInDialog: true, - }, - hideModelInfo: { - type: 'boolean', - label: 'Hide Model Info', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide the model name and context usage in the footer.', - showInDialog: true, - }, - }, - }, - hideFooter: { - type: 'boolean', - label: 'Hide Footer', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide the footer from the UI', - showInDialog: true, - }, - showMemoryUsage: { - type: 'boolean', - label: 'Show Memory Usage', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Display memory usage information in the UI', - showInDialog: true, - }, showLineNumbers: { type: 'boolean', label: 'Show Line Numbers', @@ -1292,9 +1216,3 @@ type InferSettings = { }; export type Settings = InferSettings; - -export interface FooterSettings { - hideCWD?: boolean; - hideSandboxStatus?: boolean; - hideModelInfo?: boolean; -} diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index c2f971ec4..84a1b175d 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -456,7 +456,6 @@ describe('gemini.tsx main function kitty protocol', () => { promptInteractive: undefined, query: undefined, allFiles: undefined, - showMemoryUsage: undefined, yolo: undefined, approvalMode: undefined, telemetry: undefined, diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index fa4221854..2383ef7df 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -278,13 +278,6 @@ export default { 'Hide Window Title': 'Fenstertitel ausblenden', 'Show Status in Title': 'Status im Titel anzeigen', 'Hide Tips': 'Tipps ausblenden', - 'Hide Banner': 'Banner ausblenden', - 'Hide Context Summary': 'Kontextzusammenfassung ausblenden', - 'Hide CWD': 'Arbeitsverzeichnis ausblenden', - 'Hide Sandbox Status': 'Sandbox-Status ausblenden', - 'Hide Model Info': 'Modellinformationen ausblenden', - 'Hide Footer': 'Fußzeile ausblenden', - 'Show Memory Usage': 'Speichernutzung anzeigen', 'Show Line Numbers': 'Zeilennummern anzeigen', 'Show Citations': 'Quellenangaben anzeigen', 'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 51461f4cb..7db9aec56 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -33,6 +33,25 @@ export default { 'Model Context Protocol command (from external servers)': 'Model Context Protocol command (from external servers)', 'Keyboard Shortcuts:': 'Keyboard Shortcuts:', + 'Toggle this help display': 'Toggle this help display', + 'Toggle shell mode': 'Toggle shell mode', + 'Open command menu': 'Open command menu', + 'Add file context': 'Add file context', + 'Accept suggestion / Autocomplete': 'Accept suggestion / Autocomplete', + 'Reverse search history': 'Reverse search history', + 'Press ? again to close': 'Press ? again to close', + // Keyboard shortcuts panel descriptions + 'for shell mode': 'for shell mode', + 'for commands': 'for commands', + 'for file paths': 'for file paths', + 'to clear input': 'to clear input', + 'to cycle approvals': 'to cycle approvals', + 'to quit': 'to quit', + 'for newline': 'for newline', + 'to clear screen': 'to clear screen', + 'to search history': 'to search history', + 'to paste images': 'to paste images', + 'for external editor': 'for external editor', 'Jump through words in the input': 'Jump through words in the input', 'Close dialogs, cancel requests, or quit application': 'Close dialogs, cancel requests, or quit application', @@ -46,6 +65,7 @@ export default { 'Connecting to MCP servers... ({{connected}}/{{total}})': 'Connecting to MCP servers... ({{connected}}/{{total}})', 'Type your message or @path/to/file': 'Type your message or @path/to/file', + '? for shortcuts': '? for shortcuts', "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.", 'Cancel operation / Clear input (double press)': @@ -275,13 +295,6 @@ export default { 'Hide Window Title': 'Hide Window Title', 'Show Status in Title': 'Show Status in Title', 'Hide Tips': 'Hide Tips', - 'Hide Banner': 'Hide Banner', - 'Hide Context Summary': 'Hide Context Summary', - 'Hide CWD': 'Hide CWD', - 'Hide Sandbox Status': 'Hide Sandbox Status', - 'Hide Model Info': 'Hide Model Info', - 'Hide Footer': 'Hide Footer', - 'Show Memory Usage': 'Show Memory Usage', 'Show Line Numbers': 'Show Line Numbers', 'Show Citations': 'Show Citations', 'Custom Witty Phrases': 'Custom Witty Phrases', @@ -891,14 +904,23 @@ export default { // ============================================================================ // Startup Tips // ============================================================================ - 'Tips for getting started:': 'Tips for getting started:', - '1. Ask questions, edit files, or run commands.': - '1. Ask questions, edit files, or run commands.', - '2. Be specific for the best results.': - '2. Be specific for the best results.', - 'files to customize your interactions with Qwen Code.': - 'files to customize your interactions with Qwen Code.', - 'for more information.': 'for more information.', + 'Tips:': 'Tips:', + 'Use /compress when the conversation gets long to summarize history and free up context.': + '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.': + '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.': + 'Use /bug to submit issues to the maintainers when something goes off.', + 'Switch auth type quickly with /auth.': + 'Switch auth type quickly with /auth.', + 'You can run any shell commands from Qwen Code using ! (e.g. !ls).': + 'You can run any shell commands from Qwen Code using ! (e.g. !ls).', + 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.': + 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.', + 'You can resume a previous conversation by running qwen --continue or qwen --resume.': + 'You can resume a previous conversation by running qwen --continue or qwen --resume.', + 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': + 'You can switch permission mode quickly with Shift+Tab or /approval-mode.', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 82f2436ef..8a5e7362d 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -33,6 +33,13 @@ export default { 'Model Context Protocol command (from external servers)': 'Команда Model Context Protocol (из внешних серверов)', 'Keyboard Shortcuts:': 'Горячие клавиши:', + 'Toggle this help display': 'Показать/скрыть эту справку', + 'Toggle shell mode': 'Переключить режим оболочки', + 'Open command menu': 'Открыть меню команд', + 'Add file context': 'Добавить файл в контекст', + 'Accept suggestion / Autocomplete': 'Принять подсказку / Автодополнение', + 'Reverse search history': 'Обратный поиск по истории', + 'Press ? again to close': 'Нажмите ? ещё раз, чтобы закрыть', 'Jump through words in the input': 'Переход по словам во вводе', 'Close dialogs, cancel requests, or quit application': 'Закрыть диалоги, отменить запросы или выйти из приложения', @@ -46,6 +53,7 @@ export default { 'Connecting to MCP servers... ({{connected}}/{{total}})': 'Подключение к MCP-серверам... ({{connected}}/{{total}})', 'Type your message or @path/to/file': 'Введите сообщение или @путь/к/файлу', + '? for shortcuts': '? — горячие клавиши', "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": "Нажмите 'i' для режима ВСТАВКА и 'Esc' для ОБЫЧНОГО режима.", 'Cancel operation / Clear input (double press)': @@ -60,6 +68,19 @@ export default { 'submit a bug report': 'Отправка отчёта об ошибке', 'About Qwen Code': 'Об Qwen Code', + // Keyboard shortcuts panel descriptions + 'for shell mode': 'режим оболочки', + 'for commands': 'меню команд', + 'for file paths': 'пути к файлам', + 'to clear input': 'очистить ввод', + 'to cycle approvals': 'переключить режим', + 'to quit': 'выход', + 'for newline': 'новая строка', + 'to clear screen': 'очистить экран', + 'to search history': 'поиск в истории', + 'to paste images': 'вставить изображения', + 'for external editor': 'внешний редактор', + // ============================================================================ // Поля системной информации // ============================================================================ @@ -278,13 +299,6 @@ export default { 'Hide Window Title': 'Скрыть заголовок окна', 'Show Status in Title': 'Показывать статус в заголовке', 'Hide Tips': 'Скрыть подсказки', - 'Hide Banner': 'Скрыть баннер', - 'Hide Context Summary': 'Скрыть сводку контекста', - 'Hide CWD': 'Скрыть текущую директорию', - 'Hide Sandbox Status': 'Скрыть статус песочницы', - 'Hide Model Info': 'Скрыть информацию о модели', - 'Hide Footer': 'Скрыть нижний колонтитул', - 'Show Memory Usage': 'Показывать использование памяти', 'Show Line Numbers': 'Показывать номера строк', 'Show Citations': 'Показывать цитаты', 'Custom Witty Phrases': 'Пользовательские остроумные фразы', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index a1b9c2033..4ee3fc220 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -32,6 +32,25 @@ export default { 'Model Context Protocol command (from external servers)': '模型上下文协议命令(来自外部服务器)', 'Keyboard Shortcuts:': '键盘快捷键:', + 'Toggle this help display': '切换此帮助显示', + 'Toggle shell mode': '切换命令行模式', + 'Open command menu': '打开命令菜单', + 'Add file context': '添加文件上下文', + 'Accept suggestion / Autocomplete': '接受建议 / 自动补全', + 'Reverse search history': '反向搜索历史', + 'Press ? again to close': '再次按 ? 关闭', + // Keyboard shortcuts panel descriptions + 'for shell mode': '命令行模式', + 'for commands': '命令菜单', + 'for file paths': '文件路径', + 'to clear input': '清空输入', + 'to cycle approvals': '切换审批模式', + 'to quit': '退出', + 'for newline': '换行', + 'to clear screen': '清屏', + 'to search history': '搜索历史', + 'to paste images': '粘贴图片', + 'for external editor': '外部编辑器', 'Jump through words in the input': '在输入中按单词跳转', 'Close dialogs, cancel requests, or quit application': '关闭对话框、取消请求或退出应用程序', @@ -45,6 +64,7 @@ export default { 'Connecting to MCP servers... ({{connected}}/{{total}})': '正在连接到 MCP 服务器... ({{connected}}/{{total}})', 'Type your message or @path/to/file': '输入您的消息或 @ 文件路径', + '? for shortcuts': '按 ? 查看快捷键', "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": "按 'i' 进入插入模式,按 'Esc' 进入普通模式", 'Cancel operation / Clear input (double press)': @@ -266,13 +286,6 @@ export default { 'Hide Window Title': '隐藏窗口标题', 'Show Status in Title': '在标题中显示状态', 'Hide Tips': '隐藏提示', - 'Hide Banner': '隐藏横幅', - 'Hide Context Summary': '隐藏上下文摘要', - 'Hide CWD': '隐藏当前工作目录', - 'Hide Sandbox Status': '隐藏沙箱状态', - 'Hide Model Info': '隐藏模型信息', - 'Hide Footer': '隐藏页脚', - 'Show Memory Usage': '显示内存使用', 'Show Line Numbers': '显示行号', 'Show Citations': '显示引用', 'Custom Witty Phrases': '自定义诙谐短语', @@ -845,13 +858,22 @@ export default { // ============================================================================ // Startup Tips // ============================================================================ - 'Tips for getting started:': '入门提示:', - '1. Ask questions, edit files, or run commands.': - '1. 提问、编辑文件或运行命令', - '2. Be specific for the best results.': '2. 具体描述以获得最佳结果', - 'files to customize your interactions with Qwen Code.': - '文件以自定义您与 Qwen Code 的交互', - 'for more information.': '获取更多信息', + '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 中使用 ! 可运行任意 shell 命令(例如 !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 可快速切换权限模式。', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index ea8482a16..54684a8c2 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -5,34 +5,15 @@ */ import { useIsScreenReaderEnabled } from 'ink'; -import { useTerminalSize } from './hooks/useTerminalSize.js'; -import { lerp } from '../utils/math.js'; import { useUIState } from './contexts/UIStateContext.js'; import { StreamingContext } from './contexts/StreamingContext.js'; import { QuittingDisplay } from './components/QuittingDisplay.js'; import { ScreenReaderAppLayout } from './layouts/ScreenReaderAppLayout.js'; import { DefaultAppLayout } from './layouts/DefaultAppLayout.js'; -const getContainerWidth = (terminalWidth: number): string => { - if (terminalWidth <= 80) { - return '98%'; - } - if (terminalWidth >= 132) { - return '90%'; - } - - // Linearly interpolate between 80 columns (98%) and 132 columns (90%). - const t = (terminalWidth - 80) / (132 - 80); - const percentage = lerp(98, 90, t); - - return `${Math.round(percentage)}%`; -}; - export const App = () => { const uiState = useUIState(); const isScreenReaderEnabled = useIsScreenReaderEnabled(); - const { columns } = useTerminalSize(); - const containerWidth = getContainerWidth(columns); if (uiState.quittingMessages) { return ; @@ -40,11 +21,7 @@ export const App = () => { return ( - {isScreenReaderEnabled ? ( - - ) : ( - - )} + {isScreenReaderEnabled ? : } ); }; diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 60426f1dd..faf5ddb83 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -294,10 +294,7 @@ describe('AppContainer State Management', () => { // Mock LoadedSettings mockSettings = { merged: { - hideBanner: false, - hideFooter: false, hideTips: false, - showMemoryUsage: false, theme: 'default', ui: { showStatusInTitle: false, @@ -445,10 +442,7 @@ describe('AppContainer State Management', () => { it('handles settings with all display options disabled', () => { const settingsAllHidden = { merged: { - hideBanner: true, - hideFooter: true, hideTips: true, - showMemoryUsage: false, }, } as unknown as LoadedSettings; @@ -463,28 +457,6 @@ describe('AppContainer State Management', () => { ); }).not.toThrow(); }); - - it('handles settings with memory usage enabled', () => { - const settingsWithMemory = { - merged: { - hideBanner: false, - hideFooter: false, - hideTips: false, - showMemoryUsage: true, - }, - } as unknown as LoadedSettings; - - expect(() => { - render( - , - ); - }).not.toThrow(); - }); }); describe('Version Handling', () => { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index b10bbe1e7..6cf2ea940 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -271,7 +271,8 @@ export const AppContainer = (props: AppContainerProps) => { calculatePromptWidths(terminalWidth); return { inputWidth, suggestionsWidth }; }, [terminalWidth]); - const mainAreaWidth = Math.floor(terminalWidth * 0.9); + // Uniform width for bordered box components: accounts for margins and caps at 100 + const mainAreaWidth = Math.min(terminalWidth - 4, 100); const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100); const isValidPath = useCallback((filePath: string): boolean => { diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index e04fd42c8..dc2ac3827 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -15,9 +15,11 @@ import { } from '../../utils/systemInfoFields.js'; import { t } from '../../i18n/index.js'; -type AboutBoxProps = ExtendedSystemInfo; +type AboutBoxProps = ExtendedSystemInfo & { + width?: number; +}; -export const AboutBox: React.FC = (props) => { +export const AboutBox: React.FC = ({ width, ...props }) => { const fields = getSystemInfoFields(props); return ( @@ -26,8 +28,7 @@ export const AboutBox: React.FC = (props) => { borderColor={theme.border.default} flexDirection="column" padding={1} - marginY={1} - width="100%" + width={width} > diff --git a/packages/cli/src/ui/components/AppHeader.test.tsx b/packages/cli/src/ui/components/AppHeader.test.tsx new file mode 100644 index 000000000..c125bd977 --- /dev/null +++ b/packages/cli/src/ui/components/AppHeader.test.tsx @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from 'ink-testing-library'; +import { describe, expect, it, vi } from 'vitest'; +import { AppHeader } from './AppHeader.js'; +import { ConfigContext } from '../contexts/ConfigContext.js'; +import { SettingsContext } from '../contexts/SettingsContext.js'; +import { type UIState, UIStateContext } from '../contexts/UIStateContext.js'; +import { VimModeProvider } from '../contexts/VimModeContext.js'; +import * as useTerminalSize from '../hooks/useTerminalSize.js'; +import type { LoadedSettings } from '../../config/settings.js'; + +vi.mock('../hooks/useTerminalSize.js'); +const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize); + +const createSettings = (options?: { hideTips?: boolean }): LoadedSettings => + ({ + merged: { + ui: { + hideTips: options?.hideTips ?? true, + }, + }, + }) as never; + +const createMockConfig = (overrides = {}) => ({ + getContentGeneratorConfig: vi.fn(() => ({ authType: undefined })), + getModel: vi.fn(() => 'gemini-pro'), + getTargetDir: vi.fn(() => '/projects/qwen-code'), + getMcpServers: vi.fn(() => ({})), + getBlockedMcpServers: vi.fn(() => []), + getDebugMode: vi.fn(() => false), + getScreenReader: vi.fn(() => false), + ...overrides, +}); + +const createMockUIState = (overrides: Partial = {}): UIState => + ({ + branchName: 'main', + nightly: false, + debugMessage: '', + sessionStats: { + lastPromptTokenCount: 0, + }, + ...overrides, + }) as UIState; + +const renderWithProviders = ( + uiState: UIState, + settings = createSettings(), + config = createMockConfig(), +) => { + useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 }); + return render( + + + + + + + + + , + ); +}; + +describe('', () => { + it('shows the working directory', () => { + const { lastFrame } = renderWithProviders(createMockUIState()); + expect(lastFrame()).toContain('/projects/qwen-code'); + }); + + it('hides the header when screen reader is enabled', () => { + const { lastFrame } = renderWithProviders( + createMockUIState(), + createSettings(), + createMockConfig({ getScreenReader: vi.fn(() => true) }), + ); + // When screen reader is enabled, header is not rendered + expect(lastFrame()).not.toContain('/projects/qwen-code'); + expect(lastFrame()).not.toContain('Qwen Code'); + }); + + it('shows the header with all info when banner is visible', () => { + const { lastFrame } = renderWithProviders(createMockUIState()); + expect(lastFrame()).toContain('>_ Qwen Code'); + expect(lastFrame()).toContain('gemini-pro'); + expect(lastFrame()).toContain('/projects/qwen-code'); + }); +}); diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index 9b6c5fc3b..e37d4aef1 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -9,7 +9,6 @@ import { Header } from './Header.js'; import { Tips } from './Tips.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; -import { useUIState } from '../contexts/UIStateContext.js'; interface AppHeaderProps { version: string; @@ -18,16 +17,25 @@ interface AppHeaderProps { export const AppHeader = ({ version }: AppHeaderProps) => { const settings = useSettings(); const config = useConfig(); - const { nightly } = useUIState(); + + const contentGeneratorConfig = config.getContentGeneratorConfig(); + const authType = contentGeneratorConfig?.authType; + const model = config.getModel(); + const targetDir = config.getTargetDir(); + const showBanner = !config.getScreenReader(); + const showTips = !(settings.merged.ui?.hideTips || config.getScreenReader()); return ( - {!(settings.merged.ui?.hideBanner || config.getScreenReader()) && ( -
- )} - {!(settings.merged.ui?.hideTips || config.getScreenReader()) && ( - + {showBanner && ( +
)} + {showTips && } ); }; diff --git a/packages/cli/src/ui/components/AsciiArt.ts b/packages/cli/src/ui/components/AsciiArt.ts index 8a37a5df3..c70a38f4c 100644 --- a/packages/cli/src/ui/components/AsciiArt.ts +++ b/packages/cli/src/ui/components/AsciiArt.ts @@ -5,29 +5,10 @@ */ export const shortAsciiLogo = ` - ██████╗ ██╗ ██╗███████╗███╗ ██╗ + ▄▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄ ██╔═══██╗██║ ██║██╔════╝████╗ ██║ ██║ ██║██║ █╗ ██║█████╗ ██╔██╗ ██║ ██║▄▄ ██║██║███╗██║██╔══╝ ██║╚██╗██║ ╚██████╔╝╚███╔███╔╝███████╗██║ ╚████║ - ╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝ -`; -export const longAsciiLogo = ` -██╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗ -╚██╗ ██╔═══██╗██║ ██║██╔════╝████╗ ██║ - ╚██╗ ██║ ██║██║ █╗ ██║█████╗ ██╔██╗ ██║ - ██╔╝ ██║▄▄ ██║██║███╗██║██╔══╝ ██║╚██╗██║ -██╔╝ ╚██████╔╝╚███╔███╔╝███████╗██║ ╚████║ -╚═╝ ╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝ -`; - -export const tinyAsciiLogo = ` - ███ █████████ -░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ - ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝ `; diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index d660d7040..a12855e4c 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -14,7 +14,6 @@ import { type UIActions, } from '../contexts/UIActionsContext.js'; import { ConfigContext } from '../contexts/ConfigContext.js'; -import { SettingsContext } from '../contexts/SettingsContext.js'; // Mock VimModeContext hook vi.mock('../contexts/VimModeContext.js', () => ({ useVimMode: vi.fn(() => ({ @@ -146,92 +145,33 @@ const createMockConfig = (overrides = {}) => ({ ...overrides, }); -const createMockSettings = (merged = {}) => ({ - merged: { - hideFooter: false, - showMemoryUsage: false, - ...merged, - }, -}); - /* eslint-disable @typescript-eslint/no-explicit-any */ const renderComposer = ( uiState: UIState, - settings = createMockSettings(), config = createMockConfig(), uiActions = createMockUIActions(), ) => render( - - - - - - - + + + + + , ); /* eslint-enable @typescript-eslint/no-explicit-any */ describe('Composer', () => { - describe('Footer Display Settings', () => { - it('renders Footer by default when hideFooter is false', () => { + describe('Footer Display', () => { + it('renders Footer by default', () => { const uiState = createMockUIState(); - const settings = createMockSettings({ hideFooter: false }); - const { lastFrame } = renderComposer(uiState, settings); + const { lastFrame } = renderComposer(uiState); - // Smoke check that the Footer renders when enabled. + // Smoke check that the Footer renders expect(lastFrame()).toContain('Footer'); }); - - it('does NOT render Footer when hideFooter is true', () => { - const uiState = createMockUIState(); - const settings = createMockSettings({ hideFooter: true }); - - const { lastFrame } = renderComposer(uiState, settings); - - // Check for content that only appears IN the Footer component itself - expect(lastFrame()).not.toContain('[NORMAL]'); // Vim mode indicator - expect(lastFrame()).not.toContain('(main'); // Branch name with parentheses - }); - - it('passes correct props to Footer including vim mode when enabled', async () => { - const uiState = createMockUIState({ - branchName: 'feature-branch', - errorCount: 2, - sessionStats: { - sessionId: 'test-session', - sessionStartTime: new Date(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - metrics: {} as any, - lastPromptTokenCount: 150, - promptCount: 5, - }, - }); - const config = createMockConfig({ - getModel: vi.fn(() => 'gemini-1.5-flash'), - getTargetDir: vi.fn(() => '/project/path'), - getDebugMode: vi.fn(() => true), - }); - const settings = createMockSettings({ - hideFooter: false, - showMemoryUsage: true, - }); - // Mock vim mode for this test - const { useVimMode } = await import('../contexts/VimModeContext.js'); - vi.mocked(useVimMode).mockReturnValueOnce({ - vimEnabled: true, - vimMode: 'INSERT', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - - const { lastFrame } = renderComposer(uiState, settings, config); - - expect(lastFrame()).toContain('Footer'); - // Footer should be rendered with all the state passed through - }); }); describe('Loading Indicator', () => { @@ -261,7 +201,7 @@ describe('Composer', () => { getAccessibility: vi.fn(() => ({ disableLoadingPhrases: true })), }); - const { lastFrame } = renderComposer(uiState, undefined, config); + const { lastFrame } = renderComposer(uiState, config); const output = lastFrame(); expect(output).toContain('LoadingIndicator'); @@ -318,7 +258,8 @@ describe('Composer', () => { }); describe('Context and Status Display', () => { - it('shows ContextSummaryDisplay in normal state', () => { + // Note: ContextSummaryDisplay and status prompts are now rendered in Footer, not Composer + it('shows empty space in normal state (ContextSummaryDisplay moved to Footer)', () => { const uiState = createMockUIState({ ctrlCPressedOnce: false, ctrlDPressedOnce: false, @@ -327,37 +268,43 @@ describe('Composer', () => { const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toContain('ContextSummaryDisplay'); + // ContextSummaryDisplay is now in Footer, so we just verify normal state renders + expect(lastFrame()).toBeDefined(); }); - it('shows Ctrl+C exit prompt when ctrlCPressedOnce is true', () => { + // Note: Ctrl+C, Ctrl+D, and Escape prompts are now rendered in Footer component + // These are tested in Footer.test.tsx + it('renders Footer which handles Ctrl+C exit prompt', () => { const uiState = createMockUIState({ ctrlCPressedOnce: true, }); const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toContain('Press Ctrl+C again to exit'); + // Ctrl+C prompt is now inside Footer, verify Footer renders + expect(lastFrame()).toContain('Footer'); }); - it('shows Ctrl+D exit prompt when ctrlDPressedOnce is true', () => { + it('renders Footer which handles Ctrl+D exit prompt', () => { const uiState = createMockUIState({ ctrlDPressedOnce: true, }); const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toContain('Press Ctrl+D again to exit'); + // Ctrl+D prompt is now inside Footer, verify Footer renders + expect(lastFrame()).toContain('Footer'); }); - it('shows escape prompt when showEscapePrompt is true', () => { + it('renders Footer which handles escape prompt', () => { const uiState = createMockUIState({ showEscapePrompt: true, }); const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toContain('Press Esc again to clear'); + // Escape prompt is now inside Footer, verify Footer renders + expect(lastFrame()).toContain('Footer'); }); }); @@ -382,7 +329,9 @@ describe('Composer', () => { expect(lastFrame()).not.toContain('InputPrompt'); }); - it('shows AutoAcceptIndicator when approval mode is not default and shell mode is inactive', () => { + // Note: AutoAcceptIndicator and ShellModeIndicator are now rendered inside Footer component + // These are tested in Footer.test.tsx + it('renders Footer which contains AutoAcceptIndicator when approval mode is not default', () => { const uiState = createMockUIState({ showAutoAcceptIndicator: ApprovalMode.YOLO, shellModeActive: false, @@ -390,17 +339,19 @@ describe('Composer', () => { const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toContain('AutoAcceptIndicator'); + // AutoAcceptIndicator is now inside Footer, verify Footer renders + expect(lastFrame()).toContain('Footer'); }); - it('shows ShellModeIndicator when shell mode is active', () => { + it('renders Footer which contains ShellModeIndicator when shell mode is active', () => { const uiState = createMockUIState({ shellModeActive: true, }); const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toContain('ShellModeIndicator'); + // ShellModeIndicator is now inside Footer, verify Footer renders + expect(lastFrame()).toContain('Footer'); }); }); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 1b51227a1..292273ca5 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -4,42 +4,46 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Box, Text, useIsScreenReaderEnabled } from 'ink'; -import { useMemo } from 'react'; +import { Box, useIsScreenReaderEnabled } from 'ink'; +import { useCallback, useMemo, useState } from 'react'; import { LoadingIndicator } from './LoadingIndicator.js'; -import { ContextSummaryDisplay } from './ContextSummaryDisplay.js'; -import { AutoAcceptIndicator } from './AutoAcceptIndicator.js'; -import { ShellModeIndicator } from './ShellModeIndicator.js'; import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js'; import { InputPrompt, calculatePromptWidths } from './InputPrompt.js'; import { Footer } from './Footer.js'; import { ShowMoreLines } from './ShowMoreLines.js'; import { QueuedMessageDisplay } from './QueuedMessageDisplay.js'; +import { KeyboardShortcuts } from './KeyboardShortcuts.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; -import { theme } from '../semantic-colors.js'; -import { isNarrowWidth } from '../utils/isNarrowWidth.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js'; import { useVimMode } from '../contexts/VimModeContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; -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(); - const settings = useSettings(); const isScreenReaderEnabled = useIsScreenReaderEnabled(); const uiState = useUIState(); const uiActions = useUIActions(); const { vimEnabled } = useVimMode(); const terminalWidth = process.stdout.columns; - const isNarrow = isNarrowWidth(terminalWidth); const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5)); - const { contextFileNames, showAutoAcceptIndicator } = uiState; + const { showAutoAcceptIndicator } = uiState; + + // State for keyboard shortcuts display toggle + const [showShortcuts, setShowShortcuts] = useState(false); + const handleToggleShortcuts = useCallback(() => { + setShowShortcuts((prev) => !prev); + }, []); + + // State for suggestions visibility + const [showSuggestions, setShowSuggestions] = useState(false); + const handleSuggestionsVisibilityChange = useCallback((visible: boolean) => { + setShowSuggestions(visible); + }, []); // Use the container width of InputPrompt for width of DetailedMessagesDisplay const { containerWidth } = useMemo( @@ -48,7 +52,7 @@ export const Composer = () => { ); return ( - + {!uiState.embeddedShellFocused && ( { - - - {process.env['GEMINI_SYSTEM_MD'] && ( - |⌐■_■| - )} - {uiState.ctrlCPressedOnce ? ( - - {t('Press Ctrl+C again to exit.')} - - ) : uiState.ctrlDPressedOnce ? ( - - {t('Press Ctrl+D again to exit.')} - - ) : uiState.showEscapePrompt ? ( - - {t('Press Esc again to clear.')} - - ) : ( - !settings.merged.ui?.hideContextSummary && ( - - ) - )} - - - {showAutoAcceptIndicator !== ApprovalMode.DEFAULT && - !uiState.shellModeActive && ( - - )} - {uiState.shellModeActive && } - - - {uiState.showErrorDetails && ( @@ -149,6 +104,9 @@ export const Composer = () => { setShellModeActive={uiActions.setShellModeActive} approvalMode={showAutoAcceptIndicator} onEscapePromptChange={uiActions.onEscapePromptChange} + onToggleShortcuts={handleToggleShortcuts} + showShortcuts={showShortcuts} + onSuggestionsVisibilityChange={handleSuggestionsVisibilityChange} focus={true} vimHandleInput={uiActions.vimHandleInput} isEmbeddedShellFocused={uiState.embeddedShellFocused} @@ -160,7 +118,13 @@ export const Composer = () => { /> )} - {!settings.merged.ui?.hideFooter && !isScreenReaderEnabled &&