diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 85902eaf2..9cbbe0387 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` | @@ -273,7 +270,6 @@ If you are experiencing performance issues with file searching (e.g., with `@` c | `tools.enableToolOutputTruncation` | boolean | Enable truncation of large tool outputs. | `true` | Requires restart: Yes | | `tools.truncateToolOutputThreshold` | number | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `25000` | Requires restart: Yes | | `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes | -| `tools.autoAccept` | boolean | Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. | `false` | | #### mcp @@ -361,7 +357,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 aa3e8b3b3..07b474af2 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 818cc0122..018573393 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -103,7 +103,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; @@ -296,11 +295,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', @@ -503,10 +497,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.', @@ -1009,8 +999,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 9d6e7c8e3..e6d62b5c5 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..adbc162b5 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, ); @@ -171,17 +168,14 @@ describe('SettingsSchema', () => { ).toBe(true); expect( getSettingsSchema().ui.properties.hideWindowTitle.showInDialog, - ).toBe(true); + ).toBe(false); expect(getSettingsSchema().ui.properties.hideTips.showInDialog).toBe( true, ); - expect(getSettingsSchema().ui.properties.hideBanner.showInDialog).toBe( - true, - ); expect( getSettingsSchema().privacy.properties.usageStatisticsEnabled .showInDialog, - ).toBe(false); + ).toBe(true); // Check that advanced settings are hidden from dialog expect(getSettingsSchema().security.properties.auth.showInDialog).toBe( @@ -194,7 +188,7 @@ describe('SettingsSchema', () => { expect(getSettingsSchema().telemetry.showInDialog).toBe(false); // Check that some settings are appropriately hidden - expect(getSettingsSchema().ui.properties.theme.showInDialog).toBe(false); // Changed to false + expect(getSettingsSchema().ui.properties.theme.showInDialog).toBe(true); expect(getSettingsSchema().ui.properties.customThemes.showInDialog).toBe( false, ); // Managed via theme editor @@ -203,13 +197,13 @@ describe('SettingsSchema', () => { ).toBe(false); // Experimental feature expect(getSettingsSchema().ui.properties.accessibility.showInDialog).toBe( false, - ); // Changed to false + ); expect( getSettingsSchema().context.properties.fileFiltering.showInDialog, - ).toBe(false); // Changed to false + ).toBe(false); expect( getSettingsSchema().general.properties.preferredEditor.showInDialog, - ).toBe(false); // Changed to false + ).toBe(true); expect( getSettingsSchema().advanced.properties.autoConfigureMemory .showInDialog, @@ -287,7 +281,7 @@ describe('SettingsSchema', () => { expect( getSettingsSchema().security.properties.folderTrust.properties.enabled .showInDialog, - ).toBe(true); + ).toBe(false); }); it('should have debugKeystrokeLogging setting in schema', () => { @@ -310,7 +304,7 @@ describe('SettingsSchema', () => { expect( getSettingsSchema().general.properties.debugKeystrokeLogging .showInDialog, - ).toBe(true); + ).toBe(false); expect( getSettingsSchema().general.properties.debugKeystrokeLogging .description, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 3b0356ba8..116183216 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -132,7 +132,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: undefined as string | undefined, description: 'The preferred editor to open files in.', - showInDialog: false, + showInDialog: true, }, vimMode: { type: 'boolean', @@ -163,13 +163,13 @@ const SETTINGS_SCHEMA = { }, gitCoAuthor: { type: 'boolean', - label: 'Git Co-Author', + label: 'Attribution: commit', category: 'General', requiresRestart: false, default: true, description: 'Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code.', - showInDialog: false, + showInDialog: true, }, checkpointing: { type: 'object', @@ -198,13 +198,13 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: false, description: 'Enable debug logging of keystrokes to the console.', - showInDialog: true, + showInDialog: false, }, language: { type: 'enum', - label: 'Language', + label: 'Language: UI', category: 'General', - requiresRestart: false, + requiresRestart: true, default: 'auto', description: 'The language for the user interface. Use "auto" to detect from system settings. ' + @@ -219,9 +219,20 @@ const SETTINGS_SCHEMA = { { value: 'de', label: 'Deutsch (German)' }, ], }, + outputLanguage: { + type: 'string', + label: 'Language: Model', + category: 'General', + requiresRestart: true, + default: 'auto', + description: + 'The language for LLM output. Use "auto" to detect from system settings, ' + + 'or set a specific language (e.g., "English", "中文", "日本語").', + showInDialog: true, + }, terminalBell: { type: 'boolean', - label: 'Terminal Bell', + label: 'Terminal Bell Notification', category: 'General', requiresRestart: false, default: true, @@ -257,7 +268,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: 'text', description: 'The format of the CLI output.', - showInDialog: true, + showInDialog: false, options: [ { value: 'text', label: 'Text' }, { value: 'json', label: 'JSON' }, @@ -280,9 +291,9 @@ const SETTINGS_SCHEMA = { label: 'Theme', category: 'UI', requiresRestart: false, - default: undefined as string | undefined, + default: 'Qwen Dark' as string, description: 'The color theme for the UI.', - showInDialog: false, + showInDialog: true, }, customThemes: { type: 'object', @@ -300,7 +311,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: false, description: 'Hide the window title bar', - showInDialog: true, + showInDialog: false, }, showStatusInTitle: { type: 'boolean', @@ -310,7 +321,7 @@ const SETTINGS_SCHEMA = { default: false, description: 'Show Qwen Code status and thoughts in the terminal window title', - showInDialog: true, + showInDialog: false, }, hideTips: { type: 'boolean', @@ -321,89 +332,13 @@ 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', + label: 'Show Line Numbers in Code', category: 'UI', requiresRestart: false, default: false, - description: 'Show line numbers in the chat.', + description: 'Show line numbers in the code output.', showInDialog: true, }, showCitations: { @@ -413,7 +348,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: false, description: 'Show citations for generated text in the chat.', - showInDialog: true, + showInDialog: false, }, customWittyPhrases: { type: 'array', @@ -426,7 +361,7 @@ const SETTINGS_SCHEMA = { }, enableWelcomeBack: { type: 'boolean', - label: 'Enable Welcome Back', + label: 'Show Welcome Back Dialog', category: 'UI', requiresRestart: false, default: true, @@ -460,7 +395,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: false, description: 'Disable loading phrases for accessibility', - showInDialog: true, + showInDialog: false, }, screenReader: { type: 'boolean', @@ -470,7 +405,7 @@ const SETTINGS_SCHEMA = { default: undefined as boolean | undefined, description: 'Render output in plain-text to be more screen reader accessible', - showInDialog: true, + showInDialog: false, }, }, }, @@ -497,7 +432,7 @@ const SETTINGS_SCHEMA = { properties: { enabled: { type: 'boolean', - label: 'IDE Mode', + label: 'Auto-connect to IDE', category: 'IDE', requiresRestart: true, default: false, @@ -532,7 +467,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: true, description: 'Enable collection of usage statistics', - showInDialog: false, + showInDialog: true, }, }, }, @@ -573,7 +508,7 @@ const SETTINGS_SCHEMA = { default: -1, description: 'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.', - showInDialog: true, + showInDialog: false, }, summarizeToolOutput: { type: 'object', @@ -611,7 +546,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: true, description: 'Skip the next speaker check.', - showInDialog: true, + showInDialog: false, }, skipLoopDetection: { type: 'boolean', @@ -620,7 +555,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: false, description: 'Disable all loop detection checks (streaming and LLM).', - showInDialog: true, + showInDialog: false, }, skipStartupContext: { type: 'boolean', @@ -630,7 +565,7 @@ const SETTINGS_SCHEMA = { default: false, description: 'Avoid sending the workspace startup context at the beginning of each session.', - showInDialog: true, + showInDialog: false, }, enableOpenAILogging: { type: 'boolean', @@ -639,7 +574,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: false, description: 'Enable OpenAI logging.', - showInDialog: true, + showInDialog: false, }, openAILoggingDir: { type: 'string', @@ -649,7 +584,7 @@ const SETTINGS_SCHEMA = { default: undefined as string | undefined, description: 'Custom directory path for OpenAI API logs. If not specified, defaults to logs/openai in the current working directory.', - showInDialog: true, + showInDialog: false, }, generationConfig: { type: 'object', @@ -669,7 +604,7 @@ const SETTINGS_SCHEMA = { description: 'Request timeout in milliseconds.', parentKey: 'generationConfig', childKey: 'timeout', - showInDialog: true, + showInDialog: false, }, maxRetries: { type: 'number', @@ -680,7 +615,7 @@ const SETTINGS_SCHEMA = { description: 'Maximum number of retries for failed requests.', parentKey: 'generationConfig', childKey: 'maxRetries', - showInDialog: true, + showInDialog: false, }, disableCacheControl: { type: 'boolean', @@ -691,7 +626,7 @@ const SETTINGS_SCHEMA = { description: 'Disable cache control for DashScope providers.', parentKey: 'generationConfig', childKey: 'disableCacheControl', - showInDialog: true, + showInDialog: false, }, schemaCompliance: { type: 'enum', @@ -703,7 +638,7 @@ const SETTINGS_SCHEMA = { 'The compliance mode for tool schemas sent to the model. Use "openapi_30" for strict OpenAPI 3.0 compatibility (e.g., for Gemini).', parentKey: 'generationConfig', childKey: 'schemaCompliance', - showInDialog: true, + showInDialog: false, options: [ { value: 'auto', label: 'Auto (Default)' }, { value: 'openapi_30', label: 'OpenAPI 3.0 Strict' }, @@ -759,7 +694,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: false, description: 'Whether to load memory files from include directories.', - showInDialog: true, + showInDialog: false, }, fileFiltering: { type: 'object', @@ -795,7 +730,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: true, description: 'Enable recursive file search functionality', - showInDialog: true, + showInDialog: false, }, disableFuzzySearch: { type: 'boolean', @@ -804,7 +739,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: false, description: 'Disable fuzzy search when searching for files.', - showInDialog: true, + showInDialog: false, }, }, }, @@ -841,7 +776,7 @@ const SETTINGS_SCHEMA = { properties: { enableInteractiveShell: { type: 'boolean', - label: 'Enable Interactive Shell', + label: 'Interactive Shell (PTY)', category: 'Tools', requiresRestart: true, default: false, @@ -866,20 +801,10 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: false, description: 'Show color in shell output.', - showInDialog: true, + showInDialog: false, }, }, }, - autoAccept: { - type: 'boolean', - label: 'Auto Accept', - category: 'Tools', - requiresRestart: false, - default: false, - description: - 'Automatically accept and execute tool calls that are considered safe (e.g., read-only operations).', - showInDialog: true, - }, core: { type: 'array', label: 'Core Tools', @@ -911,7 +836,7 @@ const SETTINGS_SCHEMA = { }, approvalMode: { type: 'enum', - label: 'Approval Mode', + label: 'Tool Approval Mode', category: 'Tools', requiresRestart: false, default: ApprovalMode.DEFAULT, @@ -925,6 +850,16 @@ const SETTINGS_SCHEMA = { { value: ApprovalMode.YOLO, label: 'YOLO' }, ], }, + autoAccept: { + type: 'boolean', + label: 'Auto Accept', + category: 'Tools', + requiresRestart: false, + default: false, + description: + 'Automatically accept and execute tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation.', + showInDialog: false, + }, discoveryCommand: { type: 'string', label: 'Tool Discovery Command', @@ -951,7 +886,7 @@ const SETTINGS_SCHEMA = { default: true, description: 'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.', - showInDialog: true, + showInDialog: false, }, useBuiltinRipgrep: { type: 'boolean', @@ -961,7 +896,7 @@ const SETTINGS_SCHEMA = { default: true, description: 'Use the bundled ripgrep binary. When set to false, the system-level "rg" command will be used instead. This setting is only effective when useRipgrep is true.', - showInDialog: true, + showInDialog: false, }, enableToolOutputTruncation: { type: 'boolean', @@ -970,7 +905,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: true, description: 'Enable truncation of large tool outputs.', - showInDialog: true, + showInDialog: false, }, truncateToolOutputThreshold: { type: 'number', @@ -980,7 +915,7 @@ const SETTINGS_SCHEMA = { default: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, description: 'Truncate tool output if it is larger than this many characters. Set to -1 to disable.', - showInDialog: true, + showInDialog: false, }, truncateToolOutputLines: { type: 'number', @@ -989,7 +924,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, description: 'The number of lines to keep when truncating tool output.', - showInDialog: true, + showInDialog: false, }, }, }, @@ -1066,7 +1001,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: false, description: 'Setting to track whether Folder trust is enabled.', - showInDialog: true, + showInDialog: false, }, }, }, @@ -1219,7 +1154,7 @@ const SETTINGS_SCHEMA = { properties: { skills: { type: 'boolean', - label: 'Skills', + label: 'Experimental: Skills', category: 'Experimental', requiresRestart: true, default: false, @@ -1244,7 +1179,7 @@ const SETTINGS_SCHEMA = { default: true, description: 'Enable vision model support and auto-switching functionality. When disabled, vision models like qwen-vl-max-latest will be hidden and auto-switching will not occur.', - showInDialog: true, + showInDialog: false, }, vlmSwitchMode: { type: 'string', @@ -1312,9 +1247,3 @@ type InferSettings = { }; export type Settings = InferSettings; - -export interface FooterSettings { - hideCWD?: boolean; - hideSandboxStatus?: boolean; - hideModelInfo?: boolean; -} diff --git a/packages/cli/src/core/initializer.ts b/packages/cli/src/core/initializer.ts index 56f65b1c5..c21d637e3 100644 --- a/packages/cli/src/core/initializer.ts +++ b/packages/cli/src/core/initializer.ts @@ -15,7 +15,7 @@ import { type LoadedSettings, SettingScope } from '../config/settings.js'; import { performInitialAuth } from './auth.js'; import { validateTheme } from './theme.js'; import { initializeI18n } from '../i18n/index.js'; -import { initializeLlmOutputLanguage } from '../ui/commands/languageCommand.js'; +import { initializeLlmOutputLanguage } from '../utils/languageUtils.js'; export interface InitializationResult { authError: string | null; @@ -43,7 +43,7 @@ export async function initializeApp( await initializeI18n(languageSetting); // Auto-detect and set LLM output language on first use - initializeLlmOutputLanguage(); + initializeLlmOutputLanguage(settings.merged.general?.outputLanguage); // Use authType from modelsConfig which respects CLI --auth-type argument // over settings.security.auth.selectedType 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 d358811cf..cf383708b 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -60,10 +60,15 @@ export default { 'show version info': 'Versionsinformationen anzeigen', 'submit a bug report': 'Fehlerbericht einreichen', 'About Qwen Code': 'Über Qwen Code', + Status: 'Status', // ============================================================================ // System Information Fields // ============================================================================ + 'Qwen Code': 'Qwen Code', + Runtime: 'Laufzeit', + OS: 'Betriebssystem', + Auth: 'Authentifizierung', 'CLI Version': 'CLI-Version', 'Git Commit': 'Git-Commit', Model: 'Modell', @@ -76,6 +81,7 @@ export default { 'Session ID': 'Sitzungs-ID', 'Auth Method': 'Authentifizierungsmethode', 'Base URL': 'Basis-URL', + Proxy: 'Proxy', 'Memory Usage': 'Speichernutzung', 'IDE Client': 'IDE-Client', @@ -97,8 +103,8 @@ export default { Preview: 'Vorschau', '(Use Enter to select, Tab to configure scope)': '(Enter zum Auswählen, Tab zum Konfigurieren des Bereichs)', - '(Use Enter to apply scope, Tab to select theme)': - '(Enter zum Anwenden des Bereichs, Tab zum Auswählen des Designs)', + '(Use Enter to apply scope, Tab to go back)': + '(Enter zum Anwenden des Bereichs, Tab zum Zurückgehen)', 'Theme configuration unavailable due to NO_COLOR env variable.': 'Design-Konfiguration aufgrund der NO_COLOR-Umgebungsvariable nicht verfügbar.', 'Theme "{{themeName}}" not found.': 'Design "{{themeName}}" nicht gefunden.', @@ -260,8 +266,6 @@ export default { 'View and edit Qwen Code settings': 'Qwen Code Einstellungen anzeigen und bearbeiten', Settings: 'Einstellungen', - '(Use Enter to select{{tabText}})': '(Enter zum Auswählen{{tabText}})', - ', Tab to change focus': ', Tab zum Fokuswechsel', 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': 'Um Änderungen zu sehen, muss Qwen Code neu gestartet werden. Drücken Sie r, um jetzt zu beenden und Änderungen anzuwenden.', 'The command "/{{command}}" is not supported in non-interactive mode.': @@ -271,24 +275,24 @@ export default { // ============================================================================ 'Vim Mode': 'Vim-Modus', 'Disable Auto Update': 'Automatische Updates deaktivieren', + 'Attribution: commit': 'Attribution: Commit', + 'Terminal Bell Notification': 'Terminal-Signalton', + 'Enable Usage Statistics': 'Nutzungsstatistiken aktivieren', + Theme: 'Farbschema', + 'Preferred Editor': 'Bevorzugter Editor', + 'Auto-connect to IDE': 'Automatische Verbindung zur IDE', 'Enable Prompt Completion': 'Eingabevervollständigung aktivieren', 'Debug Keystroke Logging': 'Debug-Protokollierung von Tastatureingaben', - Language: 'Sprache', + 'Language: UI': 'Sprache: Benutzeroberfläche', + 'Language: Model': 'Sprache: Modell', 'Output Format': 'Ausgabeformat', '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 Line Numbers in Code': 'Zeilennummern im Code anzeigen', 'Show Citations': 'Quellenangaben anzeigen', 'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche', - 'Enable Welcome Back': 'Willkommen-zurück aktivieren', + 'Show Welcome Back Dialog': 'Willkommen-zurück-Dialog anzeigen', 'Enable User Feedback': 'Benutzerfeedback aktivieren', 'How is Qwen doing this session? (optional)': 'Wie macht sich Qwen in dieser Sitzung? (optional)', @@ -315,7 +319,7 @@ export default { 'Respect .qwenignore': '.qwenignore beachten', 'Enable Recursive File Search': 'Rekursive Dateisuche aktivieren', 'Disable Fuzzy Search': 'Unscharfe Suche deaktivieren', - 'Enable Interactive Shell': 'Interaktive Shell aktivieren', + 'Interactive Shell (PTY)': 'Interaktive Shell (PTY)', 'Show Color': 'Farbe anzeigen', 'Auto Accept': 'Automatisch akzeptieren', 'Use Ripgrep': 'Ripgrep verwenden', @@ -327,6 +331,7 @@ export default { 'Folder Trust': 'Ordnervertrauen', 'Vision Model Preview': 'Vision-Modell-Vorschau', 'Tool Schema Compliance': 'Werkzeug-Schema-Konformität', + 'Experimental: Skills': 'Experimentell: Fähigkeiten', // Settings enum options 'Auto (detect from system)': 'Automatisch (vom System erkennen)', Text: 'Text', @@ -351,6 +356,11 @@ export default { 'Show all directories in the workspace': 'Alle Verzeichnisse im Arbeitsbereich anzeigen', 'set external editor preference': 'Externen Editor festlegen', + 'Select Editor': 'Editor auswählen', + 'Editor Preference': 'Editor-Einstellung', + 'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.': + 'Diese Editoren werden derzeit unterstützt. Bitte beachten Sie, dass einige Editoren nicht im Sandbox-Modus verwendet werden können.', + 'Your preferred editor is:': 'Ihr bevorzugter Editor ist:', 'Manage extensions': 'Erweiterungen verwalten', 'List active extensions': 'Aktive Erweiterungen auflisten', 'Update extensions. Usage: update |--all': @@ -419,6 +429,8 @@ export default { 'Example: /language output English': 'Beispiel: /language output English', 'Example: /language output 日本語': 'Beispiel: /language output Japanisch', 'UI language changed to {{lang}}': 'UI-Sprache geändert zu {{lang}}', + 'LLM output language set to {{lang}}': + 'LLM-Ausgabesprache auf {{lang}} gesetzt', 'LLM output language rule file generated at {{path}}': 'LLM-Ausgabesprach-Regeldatei generiert unter {{path}}', 'Please restart the application for the changes to take effect.': @@ -441,7 +453,7 @@ export default { // ============================================================================ // Commands - Approval Mode // ============================================================================ - 'Approval Mode': 'Genehmigungsmodus', + 'Tool Approval Mode': 'Werkzeug-Genehmigungsmodus', 'Current approval mode: {{mode}}': 'Aktueller Genehmigungsmodus: {{mode}}', 'Available approval modes:': 'Verfügbare Genehmigungsmodi:', 'Approval mode changed to: {{mode}}': @@ -483,8 +495,6 @@ export default { 'Automatically approve all tools': 'Alle Werkzeuge automatisch genehmigen', 'Workspace approval mode exists and takes priority. User-level change will have no effect.': 'Arbeitsbereich-Genehmigungsmodus existiert und hat Vorrang. Benutzerebene-Änderung hat keine Wirkung.', - '(Use Enter to select, Tab to change focus)': - '(Enter zum Auswählen, Tab zum Fokuswechsel)', 'Apply To': 'Anwenden auf', 'User Settings': 'Benutzereinstellungen', 'Workspace Settings': 'Arbeitsbereich-Einstellungen', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index e3287731d..475d19d61 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)': @@ -59,10 +79,15 @@ export default { 'show version info': 'show version info', 'submit a bug report': 'submit a bug report', 'About Qwen Code': 'About Qwen Code', + Status: 'Status', // ============================================================================ // System Information Fields // ============================================================================ + 'Qwen Code': 'Qwen Code', + Runtime: 'Runtime', + OS: 'OS', + Auth: 'Auth', 'CLI Version': 'CLI Version', 'Git Commit': 'Git Commit', Model: 'Model', @@ -75,6 +100,7 @@ export default { 'Session ID': 'Session ID', 'Auth Method': 'Auth Method', 'Base URL': 'Base URL', + Proxy: 'Proxy', 'Memory Usage': 'Memory Usage', 'IDE Client': 'IDE Client', @@ -98,8 +124,8 @@ export default { Preview: 'Preview', '(Use Enter to select, Tab to configure scope)': '(Use Enter to select, Tab to configure scope)', - '(Use Enter to apply scope, Tab to select theme)': - '(Use Enter to apply scope, Tab to select theme)', + '(Use Enter to apply scope, Tab to go back)': + '(Use Enter to apply scope, Tab to go back)', 'Theme configuration unavailable due to NO_COLOR env variable.': 'Theme configuration unavailable due to NO_COLOR env variable.', 'Theme "{{themeName}}" not found.': 'Theme "{{themeName}}" not found.', @@ -257,8 +283,6 @@ export default { // ============================================================================ 'View and edit Qwen Code settings': 'View and edit Qwen Code settings', Settings: 'Settings', - '(Use Enter to select{{tabText}})': '(Use Enter to select{{tabText}})', - ', Tab to change focus': ', Tab to change focus', 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.', 'The command "/{{command}}" is not supported in non-interactive mode.': @@ -268,24 +292,24 @@ export default { // ============================================================================ 'Vim Mode': 'Vim Mode', 'Disable Auto Update': 'Disable Auto Update', + 'Attribution: commit': 'Attribution: commit', + 'Terminal Bell Notification': 'Terminal Bell Notification', + 'Enable Usage Statistics': 'Enable Usage Statistics', + Theme: 'Theme', + 'Preferred Editor': 'Preferred Editor', + 'Auto-connect to IDE': 'Auto-connect to IDE', 'Enable Prompt Completion': 'Enable Prompt Completion', 'Debug Keystroke Logging': 'Debug Keystroke Logging', - Language: 'Language', + 'Language: UI': 'Language: UI', + 'Language: Model': 'Language: Model', 'Output Format': 'Output Format', '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 Line Numbers in Code': 'Show Line Numbers in Code', 'Show Citations': 'Show Citations', 'Custom Witty Phrases': 'Custom Witty Phrases', - 'Enable Welcome Back': 'Enable Welcome Back', + 'Show Welcome Back Dialog': 'Show Welcome Back Dialog', 'Enable User Feedback': 'Enable User Feedback', 'How is Qwen doing this session? (optional)': 'How is Qwen doing this session? (optional)', @@ -312,7 +336,7 @@ export default { 'Respect .qwenignore': 'Respect .qwenignore', 'Enable Recursive File Search': 'Enable Recursive File Search', 'Disable Fuzzy Search': 'Disable Fuzzy Search', - 'Enable Interactive Shell': 'Enable Interactive Shell', + 'Interactive Shell (PTY)': 'Interactive Shell (PTY)', 'Show Color': 'Show Color', 'Auto Accept': 'Auto Accept', 'Use Ripgrep': 'Use Ripgrep', @@ -323,6 +347,7 @@ export default { 'Folder Trust': 'Folder Trust', 'Vision Model Preview': 'Vision Model Preview', 'Tool Schema Compliance': 'Tool Schema Compliance', + 'Experimental: Skills': 'Experimental: Skills', // Settings enum options 'Auto (detect from system)': 'Auto (detect from system)', Text: 'Text', @@ -347,6 +372,11 @@ export default { 'Show all directories in the workspace': 'Show all directories in the workspace', 'set external editor preference': 'set external editor preference', + 'Select Editor': 'Select Editor', + 'Editor Preference': 'Editor Preference', + 'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.': + 'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.', + 'Your preferred editor is:': 'Your preferred editor is:', 'Manage extensions': 'Manage extensions', 'List active extensions': 'List active extensions', 'Update extensions. Usage: update |--all': @@ -413,6 +443,7 @@ export default { 'Example: /language output English': 'Example: /language output English', 'Example: /language output 日本語': 'Example: /language output 日本語', 'UI language changed to {{lang}}': 'UI language changed to {{lang}}', + 'LLM output language set to {{lang}}': 'LLM output language set to {{lang}}', 'LLM output language rule file generated at {{path}}': 'LLM output language rule file generated at {{path}}', 'Please restart the application for the changes to take effect.': @@ -434,7 +465,7 @@ export default { // ============================================================================ // Commands - Approval Mode // ============================================================================ - 'Approval Mode': 'Approval Mode', + 'Tool Approval Mode': 'Tool Approval Mode', 'Current approval mode: {{mode}}': 'Current approval mode: {{mode}}', 'Available approval modes:': 'Available approval modes:', 'Approval mode changed to: {{mode}}': 'Approval mode changed to: {{mode}}', @@ -473,8 +504,6 @@ export default { 'Automatically approve all tools': 'Automatically approve all tools', 'Workspace approval mode exists and takes priority. User-level change will have no effect.': 'Workspace approval mode exists and takes priority. User-level change will have no effect.', - '(Use Enter to select, Tab to change focus)': - '(Use Enter to select, Tab to change focus)', 'Apply To': 'Apply To', 'User Settings': 'User Settings', 'Workspace Settings': 'Workspace Settings', @@ -898,14 +927,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 a91cd0d44..e20422474 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)': @@ -59,10 +67,28 @@ export default { 'show version info': 'Просмотр информации о версии', 'submit a bug report': 'Отправка отчёта об ошибке', 'About Qwen Code': 'Об Qwen Code', + Status: 'Статус', + + // 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': 'внешний редактор', // ============================================================================ // Поля системной информации // ============================================================================ + 'Qwen Code': 'Qwen Code', + Runtime: 'Среда выполнения', + OS: 'ОС', + Auth: 'Аутентификация', 'CLI Version': 'Версия CLI', 'Git Commit': 'Git-коммит', Model: 'Модель', @@ -75,6 +101,7 @@ export default { 'Session ID': 'ID сессии', 'Auth Method': 'Метод авторизации', 'Base URL': 'Базовый URL', + Proxy: 'Прокси', 'Memory Usage': 'Использование памяти', 'IDE Client': 'Клиент IDE', @@ -100,8 +127,8 @@ export default { Preview: 'Предпросмотр', '(Use Enter to select, Tab to configure scope)': '(Enter для выбора, Tab для настройки области)', - '(Use Enter to apply scope, Tab to select theme)': - '(Enter для применения области, Tab для выбора темы)', + '(Use Enter to apply scope, Tab to go back)': + '(Enter для применения области, Tab для возврата)', 'Theme configuration unavailable due to NO_COLOR env variable.': 'Настройка темы недоступна из-за переменной окружения NO_COLOR.', 'Theme "{{themeName}}" not found.': 'Тема "{{themeName}}" не найдена.', @@ -260,8 +287,6 @@ export default { // ============================================================================ 'View and edit Qwen Code settings': 'Просмотр и изменение настроек Qwen Code', Settings: 'Настройки', - '(Use Enter to select{{tabText}})': '(Enter для выбора{{tabText}})', - ', Tab to change focus': ', Tab для смены фокуса', 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': 'Для применения изменений необходимо перезапустить Qwen Code. Нажмите r для выхода и применения изменений.', 'The command "/{{command}}" is not supported in non-interactive mode.': @@ -271,24 +296,24 @@ export default { // ============================================================================ 'Vim Mode': 'Режим Vim', 'Disable Auto Update': 'Отключить автообновление', + 'Attribution: commit': 'Атрибуция: коммит', + 'Terminal Bell Notification': 'Звуковое уведомление терминала', + 'Enable Usage Statistics': 'Включить сбор статистики использования', + Theme: 'Тема', + 'Preferred Editor': 'Предпочтительный редактор', + 'Auto-connect to IDE': 'Автоподключение к IDE', 'Enable Prompt Completion': 'Включить автодополнение промптов', 'Debug Keystroke Logging': 'Логирование нажатий клавиш для отладки', - Language: 'Язык', + 'Language: UI': 'Язык: интерфейс', + 'Language: Model': 'Язык: модель', 'Output Format': 'Формат вывода', '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 Line Numbers in Code': 'Показывать номера строк в коде', 'Show Citations': 'Показывать цитаты', 'Custom Witty Phrases': 'Пользовательские остроумные фразы', - 'Enable Welcome Back': 'Включить приветствие при возврате', + 'Show Welcome Back Dialog': 'Показывать диалог приветствия', 'Enable User Feedback': 'Включить отзывы пользователей', 'How is Qwen doing this session? (optional)': 'Как дела у Qwen в этой сессии? (необязательно)', @@ -315,7 +340,7 @@ export default { 'Respect .qwenignore': 'Учитывать .qwenignore', 'Enable Recursive File Search': 'Включить рекурсивный поиск файлов', 'Disable Fuzzy Search': 'Отключить нечеткий поиск', - 'Enable Interactive Shell': 'Включить интерактивный терминал', + 'Interactive Shell (PTY)': 'Интерактивный терминал (PTY)', 'Show Color': 'Показывать цвета', 'Auto Accept': 'Автоподтверждение', 'Use Ripgrep': 'Использовать Ripgrep', @@ -326,6 +351,7 @@ export default { 'Folder Trust': 'Доверие к папке', 'Vision Model Preview': 'Визуальная модель (предпросмотр)', 'Tool Schema Compliance': 'Соответствие схеме инструмента', + 'Experimental: Skills': 'Экспериментальное: Навыки', // Варианты перечислений настроек 'Auto (detect from system)': 'Авто (определить из системы)', Text: 'Текст', @@ -352,6 +378,11 @@ export default { 'Показать все директории в рабочем пространстве', 'set external editor preference': 'Установка предпочитаемого внешнего редактора', + 'Select Editor': 'Выбрать редактор', + 'Editor Preference': 'Настройка редактора', + 'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.': + 'В настоящее время поддерживаются следующие редакторы. Обратите внимание, что некоторые редакторы нельзя использовать в режиме песочницы.', + 'Your preferred editor is:': 'Ваш предпочитаемый редактор:', 'Manage extensions': 'Управление расширениями', 'List active extensions': 'Показать активные расширения', 'Update extensions. Usage: update |--all': @@ -419,6 +450,8 @@ export default { 'Example: /language output English': 'Пример: /language output English', 'Example: /language output 日本語': 'Пример: /language output 日本語', 'UI language changed to {{lang}}': 'Язык интерфейса изменен на {{lang}}', + 'LLM output language set to {{lang}}': + 'Язык вывода LLM установлен на {{lang}}', 'LLM output language rule file generated at {{path}}': 'Файл правил языка вывода LLM создан в {{path}}', 'Please restart the application for the changes to take effect.': @@ -441,7 +474,7 @@ export default { // ============================================================================ // Команды - Режим подтверждения // ============================================================================ - 'Approval Mode': 'Режим подтверждения', + 'Tool Approval Mode': 'Режим подтверждения инструментов', 'Current approval mode: {{mode}}': 'Текущий режим подтверждения: {{mode}}', 'Available approval modes:': 'Доступные режимы подтверждения:', 'Approval mode changed to: {{mode}}': @@ -483,8 +516,6 @@ export default { 'Автоматически подтверждать все инструменты', 'Workspace approval mode exists and takes priority. User-level change will have no effect.': 'Режим подтверждения рабочего пространства существует и имеет приоритет. Изменение на уровне пользователя не будет иметь эффекта.', - '(Use Enter to select, Tab to change focus)': - '(Enter для выбора, Tab для смены фокуса)', 'Apply To': 'Применить к', 'User Settings': 'Настройки пользователя', 'Workspace Settings': 'Настройки рабочего пространства', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 466b163e3..2a0d5a368 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)': @@ -58,10 +78,15 @@ export default { 'show version info': '显示版本信息', 'submit a bug report': '提交错误报告', 'About Qwen Code': '关于 Qwen Code', + Status: '状态', // ============================================================================ // System Information Fields // ============================================================================ + 'Qwen Code': 'Qwen Code', + Runtime: '运行环境', + OS: '操作系统', + Auth: '认证', 'CLI Version': 'CLI 版本', 'Git Commit': 'Git 提交', Model: '模型', @@ -74,6 +99,7 @@ export default { 'Session ID': '会话 ID', 'Auth Method': '认证方式', 'Base URL': '基础 URL', + Proxy: '代理', 'Memory Usage': '内存使用', 'IDE Client': 'IDE 客户端', @@ -97,8 +123,8 @@ export default { Preview: '预览', '(Use Enter to select, Tab to configure scope)': '(使用 Enter 选择,Tab 配置作用域)', - '(Use Enter to apply scope, Tab to select theme)': - '(使用 Enter 应用作用域,Tab 选择主题)', + '(Use Enter to apply scope, Tab to go back)': + '(使用 Enter 应用作用域,Tab 返回)', 'Theme configuration unavailable due to NO_COLOR env variable.': '由于 NO_COLOR 环境变量,主题配置不可用。', 'Theme "{{themeName}}" not found.': '未找到主题 "{{themeName}}"。', @@ -248,8 +274,6 @@ export default { // ============================================================================ 'View and edit Qwen Code settings': '查看和编辑 Qwen Code 设置', Settings: '设置', - '(Use Enter to select{{tabText}})': '(使用 Enter 选择{{tabText}})', - ', Tab to change focus': ',Tab 切换焦点', 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': '要查看更改,必须重启 Qwen Code。按 r 退出并立即应用更改。', 'The command "/{{command}}" is not supported in non-interactive mode.': @@ -259,24 +283,24 @@ export default { // ============================================================================ 'Vim Mode': 'Vim 模式', 'Disable Auto Update': '禁用自动更新', + 'Attribution: commit': '署名:提交', + 'Terminal Bell Notification': '终端响铃通知', + 'Enable Usage Statistics': '启用使用统计', + Theme: '主题', + 'Preferred Editor': '首选编辑器', + 'Auto-connect to IDE': '自动连接到 IDE', 'Enable Prompt Completion': '启用提示补全', 'Debug Keystroke Logging': '调试按键记录', - Language: '语言', + 'Language: UI': '语言:界面', + 'Language: Model': '语言:模型', 'Output Format': '输出格式', '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 Line Numbers in Code': '在代码中显示行号', 'Show Citations': '显示引用', 'Custom Witty Phrases': '自定义诙谐短语', - 'Enable Welcome Back': '启用欢迎回来', + 'Show Welcome Back Dialog': '显示欢迎回来对话框', 'Enable User Feedback': '启用用户反馈', 'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)', Bad: '不满意', @@ -301,7 +325,7 @@ export default { 'Respect .qwenignore': '遵守 .qwenignore', 'Enable Recursive File Search': '启用递归文件搜索', 'Disable Fuzzy Search': '禁用模糊搜索', - 'Enable Interactive Shell': '启用交互式 Shell', + 'Interactive Shell (PTY)': '交互式 Shell (PTY)', 'Show Color': '显示颜色', 'Auto Accept': '自动接受', 'Use Ripgrep': '使用 Ripgrep', @@ -312,6 +336,7 @@ export default { 'Folder Trust': '文件夹信任', 'Vision Model Preview': '视觉模型预览', 'Tool Schema Compliance': '工具 Schema 兼容性', + 'Experimental: Skills': '实验性: 技能', // Settings enum options 'Auto (detect from system)': '自动(从系统检测)', Text: '文本', @@ -333,6 +358,11 @@ export default { '将目录添加到工作区。使用逗号分隔多个路径', 'Show all directories in the workspace': '显示工作区中的所有目录', 'set external editor preference': '设置外部编辑器首选项', + 'Select Editor': '选择编辑器', + 'Editor Preference': '编辑器首选项', + 'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.': + '当前支持以下编辑器。请注意,某些编辑器无法在沙箱模式下使用。', + 'Your preferred editor is:': '您的首选编辑器是:', 'Manage extensions': '管理扩展', 'List active extensions': '列出活动扩展', 'Update extensions. Usage: update |--all': @@ -396,6 +426,7 @@ export default { 'Example: /language output English': '示例:/language output English', 'Example: /language output 日本語': '示例:/language output 日本語', 'UI language changed to {{lang}}': 'UI 语言已更改为 {{lang}}', + 'LLM output language set to {{lang}}': 'LLM 输出语言已设置为 {{lang}}', 'LLM output language rule file generated at {{path}}': 'LLM 输出语言规则文件已生成于 {{path}}', 'Please restart the application for the changes to take effect.': @@ -416,7 +447,7 @@ export default { // ============================================================================ // Commands - Approval Mode // ============================================================================ - 'Approval Mode': '审批模式', + 'Tool Approval Mode': '工具审批模式', 'Current approval mode: {{mode}}': '当前审批模式:{{mode}}', 'Available approval modes:': '可用的审批模式:', 'Approval mode changed to: {{mode}}': '审批模式已更改为:{{mode}}', @@ -450,8 +481,6 @@ export default { 'Automatically approve all tools': '自动批准所有工具', 'Workspace approval mode exists and takes priority. User-level change will have no effect.': '工作区审批模式已存在并具有优先级。用户级别的更改将无效。', - '(Use Enter to select, Tab to change focus)': - '(使用 Enter 选择,Tab 切换焦点)', 'Apply To': '应用于', 'User Settings': '用户设置', 'Workspace Settings': '工作区设置', @@ -851,13 +880,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/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 7c8e6fc57..7d4f50421 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -8,7 +8,8 @@ vi.mock('../ui/commands/aboutCommand.js', async () => { const { CommandKind } = await import('../ui/commands/types.js'); return { aboutCommand: { - name: 'about', + name: 'status', + altNames: ['about'], description: 'About the CLI', kind: CommandKind.BUILT_IN, }, @@ -127,8 +128,8 @@ describe('BuiltinCommandLoader', () => { expect(ideCmd).toBeDefined(); // Other commands should still be present. - const aboutCmd = commands.find((c) => c.name === 'about'); - expect(aboutCmd).toBeDefined(); + const statusCmd = commands.find((c) => c.name === 'status'); + expect(statusCmd).toBeDefined(); }); it('should handle a null config gracefully when calling factories', async () => { @@ -143,9 +144,9 @@ describe('BuiltinCommandLoader', () => { const loader = new BuiltinCommandLoader(mockConfig); const commands = await loader.loadCommands(new AbortController().signal); - const aboutCmd = commands.find((c) => c.name === 'about'); - expect(aboutCmd).toBeDefined(); - expect(aboutCmd?.kind).toBe(CommandKind.BUILT_IN); + const statusCmd = commands.find((c) => c.name === 'status'); + expect(statusCmd).toBeDefined(); + expect(statusCmd?.kind).toBe(CommandKind.BUILT_IN); const approvalModeCmd = commands.find((c) => c.name === 'approval-mode'); expect(approvalModeCmd).toBeDefined(); 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 909d2beec..4384971d2 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -272,7 +272,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 => { @@ -1404,6 +1405,8 @@ export const AppContainer = (props: AppContainerProps) => { const uiActions: UIActions = useMemo( () => ({ + openThemeDialog, + openEditorDialog, handleThemeSelect, handleThemeHighlight, handleApprovalModeSelect, @@ -1445,6 +1448,8 @@ export const AppContainer = (props: AppContainerProps) => { submitFeedback, }), [ + openThemeDialog, + openEditorDialog, handleThemeSelect, handleThemeHighlight, handleApprovalModeSelect, diff --git a/packages/cli/src/ui/commands/aboutCommand.test.ts b/packages/cli/src/ui/commands/aboutCommand.test.ts index 8a1daaeb1..4920f2404 100644 --- a/packages/cli/src/ui/commands/aboutCommand.test.ts +++ b/packages/cli/src/ui/commands/aboutCommand.test.ts @@ -64,7 +64,8 @@ describe('aboutCommand', () => { }); it('should have the correct name and description', () => { - expect(aboutCommand.name).toBe('about'); + expect(aboutCommand.name).toBe('status'); + expect(aboutCommand.altNames).toEqual(['about']); expect(aboutCommand.description).toBe('show version info'); }); diff --git a/packages/cli/src/ui/commands/aboutCommand.ts b/packages/cli/src/ui/commands/aboutCommand.ts index 800b2b000..1570b9e3d 100644 --- a/packages/cli/src/ui/commands/aboutCommand.ts +++ b/packages/cli/src/ui/commands/aboutCommand.ts @@ -11,7 +11,8 @@ import { getExtendedSystemInfo } from '../../utils/systemInfo.js'; import { t } from '../../i18n/index.js'; export const aboutCommand: SlashCommand = { - name: 'about', + name: 'status', + altNames: ['about'], get description() { return t('show version info'); }, diff --git a/packages/cli/src/ui/commands/bugCommand.test.ts b/packages/cli/src/ui/commands/bugCommand.test.ts index 09c28aad9..eac703cfa 100644 --- a/packages/cli/src/ui/commands/bugCommand.test.ts +++ b/packages/cli/src/ui/commands/bugCommand.test.ts @@ -56,27 +56,22 @@ describe('bugCommand', () => { if (!bugCommand.action) throw new Error('Action is not defined'); await bugCommand.action(mockContext, 'A test bug'); - const gitCommitLine = + const qwenCodeLine = GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) - ? `* **Git Commit:** ${GIT_COMMIT_INFO}\n` - : ''; - const expectedInfo = ` -* **CLI Version:** 0.1.0 -${gitCommitLine}* **Model:** qwen3-coder-plus -* **Sandbox:** test -* **OS Platform:** test-platform -* **OS Arch:** x64 -* **OS Release:** 22.0.0 -* **Node.js Version:** v20.0.0 -* **NPM Version:** 10.0.0 -* **Session ID:** test-session-id -* **Auth Method:** -* **Memory Usage:** 100 MB -* **IDE Client:** VSCode -`; + ? `Qwen Code: 0.1.0 (${GIT_COMMIT_INFO})` + : 'Qwen Code: 0.1.0'; + const expectedInfo = `${qwenCodeLine} +Runtime: Node.js v20.0.0 / npm 10.0.0 +IDE Client: VSCode +OS: test-platform x64 (22.0.0) +Model: qwen3-coder-plus +Session ID: test-session-id +Sandbox: test +Proxy: no proxy +Memory Usage: 100 MB`; const expectedUrl = 'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title=A%20test%20bug&info=' + - encodeURIComponent(expectedInfo); + encodeURIComponent(`\n${expectedInfo}\n`); expect(open).toHaveBeenCalledWith(expectedUrl); }); @@ -95,27 +90,22 @@ ${gitCommitLine}* **Model:** qwen3-coder-plus if (!bugCommand.action) throw new Error('Action is not defined'); await bugCommand.action(mockContext, 'A custom bug'); - const gitCommitLine = + const qwenCodeLine = GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) - ? `* **Git Commit:** ${GIT_COMMIT_INFO}\n` - : ''; - const expectedInfo = ` -* **CLI Version:** 0.1.0 -${gitCommitLine}* **Model:** qwen3-coder-plus -* **Sandbox:** test -* **OS Platform:** test-platform -* **OS Arch:** x64 -* **OS Release:** 22.0.0 -* **Node.js Version:** v20.0.0 -* **NPM Version:** 10.0.0 -* **Session ID:** test-session-id -* **Auth Method:** -* **Memory Usage:** 100 MB -* **IDE Client:** VSCode -`; + ? `Qwen Code: 0.1.0 (${GIT_COMMIT_INFO})` + : 'Qwen Code: 0.1.0'; + const expectedInfo = `${qwenCodeLine} +Runtime: Node.js v20.0.0 / npm 10.0.0 +IDE Client: VSCode +OS: test-platform x64 (22.0.0) +Model: qwen3-coder-plus +Session ID: test-session-id +Sandbox: test +Proxy: no proxy +Memory Usage: 100 MB`; const expectedUrl = customTemplate .replace('{title}', encodeURIComponent('A custom bug')) - .replace('{info}', encodeURIComponent(expectedInfo)); + .replace('{info}', encodeURIComponent(`\n${expectedInfo}\n`)); expect(open).toHaveBeenCalledWith(expectedUrl); }); @@ -152,28 +142,23 @@ ${gitCommitLine}* **Model:** qwen3-coder-plus if (!bugCommand.action) throw new Error('Action is not defined'); await bugCommand.action(mockContext, 'OpenAI bug'); - const gitCommitLine = + const qwenCodeLine = GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) - ? `* **Git Commit:** ${GIT_COMMIT_INFO}\n` - : ''; - const expectedInfo = ` -* **CLI Version:** 0.1.0 -${gitCommitLine}* **Model:** qwen3-coder-plus -* **Sandbox:** test -* **OS Platform:** test-platform -* **OS Arch:** x64 -* **OS Release:** 22.0.0 -* **Node.js Version:** v20.0.0 -* **NPM Version:** 10.0.0 -* **Session ID:** test-session-id -* **Auth Method:** ${AuthType.USE_OPENAI} -* **Base URL:** https://api.openai.com/v1 -* **Memory Usage:** 100 MB -* **IDE Client:** VSCode -`; + ? `Qwen Code: 0.1.0 (${GIT_COMMIT_INFO})` + : 'Qwen Code: 0.1.0'; + const expectedInfo = `${qwenCodeLine} +Runtime: Node.js v20.0.0 / npm 10.0.0 +IDE Client: VSCode +OS: test-platform x64 (22.0.0) +Auth: ${AuthType.USE_OPENAI} (https://api.openai.com/v1) +Model: qwen3-coder-plus +Session ID: test-session-id +Sandbox: test +Proxy: no proxy +Memory Usage: 100 MB`; const expectedUrl = 'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title=OpenAI%20bug&info=' + - encodeURIComponent(expectedInfo); + encodeURIComponent(`\n${expectedInfo}\n`); expect(open).toHaveBeenCalledWith(expectedUrl); }); diff --git a/packages/cli/src/ui/commands/bugCommand.ts b/packages/cli/src/ui/commands/bugCommand.ts index 14cf37598..6bedbfebf 100644 --- a/packages/cli/src/ui/commands/bugCommand.ts +++ b/packages/cli/src/ui/commands/bugCommand.ts @@ -12,10 +12,7 @@ import { } from './types.js'; import { MessageType } from '../types.js'; import { getExtendedSystemInfo } from '../../utils/systemInfo.js'; -import { - getSystemInfoFields, - getFieldValue, -} from '../../utils/systemInfoFields.js'; +import { getSystemInfoFields } from '../../utils/systemInfoFields.js'; import { t } from '../../i18n/index.js'; export const bugCommand: SlashCommand = { @@ -30,11 +27,9 @@ export const bugCommand: SlashCommand = { const fields = getSystemInfoFields(systemInfo); - // Generate bug report info using the same field configuration - let info = '\n'; - for (const field of fields) { - info += `* **${field.label}:** ${getFieldValue(field, systemInfo)}\n`; - } + const info = fields + .map((field) => `${field.label}: ${field.value}`) + .join('\n'); let bugReportUrl = 'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title={title}&info={info}'; @@ -46,7 +41,7 @@ export const bugCommand: SlashCommand = { bugReportUrl = bugReportUrl .replace('{title}', encodeURIComponent(bugDescription)) - .replace('{info}', encodeURIComponent(info)); + .replace('{info}', encodeURIComponent(`\n${info}\n`)); context.ui.addItem( { diff --git a/packages/cli/src/ui/commands/languageCommand.test.ts b/packages/cli/src/ui/commands/languageCommand.test.ts index 719b1780c..9234773eb 100644 --- a/packages/cli/src/ui/commands/languageCommand.test.ts +++ b/packages/cli/src/ui/commands/languageCommand.test.ts @@ -8,6 +8,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'node:fs'; import { type CommandContext, CommandKind } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; +import type { LoadedSettings } from '../../config/settings.js'; // Mock i18n module vi.mock('../../i18n/index.js', () => ({ @@ -71,10 +72,8 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { // Import modules after mocking import * as i18n from '../../i18n/index.js'; -import { - languageCommand, - initializeLlmOutputLanguage, -} from './languageCommand.js'; +import { languageCommand } from './languageCommand.js'; +import { initializeLlmOutputLanguage } from '../../utils/languageUtils.js'; describe('languageCommand', () => { let mockContext: CommandContext; @@ -165,11 +164,13 @@ describe('languageCommand', () => { }); }); - it('should show LLM output language when set', async () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue( - '# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY', - ); + it('should show LLM output language when explicitly set', async () => { + // Set the outputLanguage setting explicitly + mockContext.services.settings = { + ...mockContext.services.settings, + merged: { general: { outputLanguage: 'Chinese' } }, + setValue: vi.fn(), + } as unknown as LoadedSettings; // Make t() function handle interpolation for this test vi.mocked(i18n.t).mockImplementation( @@ -192,7 +193,7 @@ describe('languageCommand', () => { messageType: 'info', content: expect.stringContaining('Current UI language:'), }); - // Verify it correctly parses "Chinese" from the template format + // Verify it shows "Chinese" for the explicitly set language expect(result).toEqual({ type: 'message', messageType: 'info', @@ -200,16 +201,14 @@ describe('languageCommand', () => { }); }); - it('should parse Unicode LLM output language from marker', async () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue( - [ - '# ⚠️ CRITICAL: 中文 Output Language Rule - HIGHEST PRIORITY ⚠️', - '', - '', - 'Some other content...', - ].join('\n'), - ); + it('should show auto-detected language when set to auto', async () => { + // Set the outputLanguage setting to 'auto' + mockContext.services.settings = { + ...mockContext.services.settings, + merged: { general: { outputLanguage: 'auto' } }, + setValue: vi.fn(), + } as unknown as LoadedSettings; + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh'); vi.mocked(i18n.t).mockImplementation( (key: string, params?: Record) => { @@ -226,10 +225,16 @@ describe('languageCommand', () => { const result = await languageCommand.action(mockContext, ''); + // Verify it shows "Auto (detect from system) → Chinese" expect(result).toEqual({ type: 'message', messageType: 'info', - content: expect.stringContaining('中文'), + content: expect.stringContaining('Auto (detect from system)'), + }); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('Chinese'), }); }); }); @@ -404,7 +409,7 @@ describe('languageCommand', () => { }); }); - it('should create LLM output language rule file', async () => { + it('should save LLM output language setting', async () => { if (!languageCommand.action) { throw new Error('The language command must have an action.'); } @@ -414,18 +419,16 @@ describe('languageCommand', () => { 'output Chinese', ); - expect(fs.mkdirSync).toHaveBeenCalled(); - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('output-language.md'), - expect.stringContaining('Chinese'), - 'utf-8', + // Verify setting was saved (rule file is updated on restart) + expect(mockContext.services.settings?.setValue).toHaveBeenCalledWith( + expect.anything(), // SettingScope.User + 'general.outputLanguage', + 'Chinese', ); expect(result).toEqual({ type: 'message', messageType: 'info', - content: expect.stringContaining( - 'LLM output language rule file generated', - ), + content: expect.stringContaining('LLM output language set to'), }); }); @@ -453,10 +456,11 @@ describe('languageCommand', () => { await languageCommand.action(mockContext, 'output ru'); - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('output-language.md'), - expect.stringContaining('Russian'), - 'utf-8', + // Verify setting was saved with normalized value + expect(mockContext.services.settings?.setValue).toHaveBeenCalledWith( + expect.anything(), + 'general.outputLanguage', + 'Russian', ); }); @@ -467,28 +471,36 @@ describe('languageCommand', () => { await languageCommand.action(mockContext, 'output de'); - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('output-language.md'), - expect.stringContaining('German'), - 'utf-8', + // Verify setting was saved with normalized value + expect(mockContext.services.settings?.setValue).toHaveBeenCalledWith( + expect.anything(), + 'general.outputLanguage', + 'German', ); }); - it('should handle file write errors gracefully', async () => { - vi.mocked(fs.writeFileSync).mockImplementation(() => { - throw new Error('Permission denied'); - }); - + it('should save setting without immediate rule file update', async () => { + // Even though rule file updates happen on restart, the setting should still be saved if (!languageCommand.action) { throw new Error('The language command must have an action.'); } - const result = await languageCommand.action(mockContext, 'output German'); + const result = await languageCommand.action( + mockContext, + 'output Spanish', + ); + // Verify setting was saved + expect(mockContext.services.settings?.setValue).toHaveBeenCalledWith( + expect.anything(), + 'general.outputLanguage', + 'Spanish', + ); + // Verify success message (no error about file generation) expect(result).toEqual({ type: 'message', - messageType: 'error', - content: expect.stringContaining('Failed to generate'), + messageType: 'info', + content: expect.stringContaining('LLM output language set to'), }); }); }); @@ -586,24 +598,23 @@ describe('languageCommand', () => { expect(outputSubcommand?.kind).toBe(CommandKind.BUILT_IN); }); - it('should have action that generates rule file', async () => { + it('should have action that saves setting', async () => { if (!outputSubcommand?.action) { throw new Error('Output subcommand must have an action.'); } - // Ensure mocks are properly set for this test - vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); - vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); - const result = await outputSubcommand.action(mockContext, 'French'); - expect(fs.writeFileSync).toHaveBeenCalled(); + // Verify setting was saved (rule file is updated on restart) + expect(mockContext.services.settings?.setValue).toHaveBeenCalledWith( + expect.anything(), + 'general.outputLanguage', + 'French', + ); expect(result).toEqual({ type: 'message', messageType: 'info', - content: expect.stringContaining( - 'LLM output language rule file generated', - ), + content: expect.stringContaining('LLM output language set to'), }); }); }); @@ -688,6 +699,7 @@ describe('languageCommand', () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); + vi.mocked(fs.readFileSync).mockImplementation(() => ''); }); it('should create file when it does not exist', () => { @@ -704,14 +716,50 @@ describe('languageCommand', () => { ); }); - it('should NOT overwrite existing file', () => { + it('should NOT overwrite existing file when content matches resolved language', () => { vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('en'); + vi.mocked(fs.readFileSync).mockReturnValue( + `# Output language preference: English + +`, + ); initializeLlmOutputLanguage(); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); + it('should overwrite existing file when output language setting differs', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue( + `# Output language preference: English + +`, + ); + + initializeLlmOutputLanguage('Japanese'); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('Japanese'), + 'utf-8', + ); + }); + + it('should resolve auto setting to detected system language', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh'); + + initializeLlmOutputLanguage('auto'); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('Chinese'), + 'utf-8', + ); + }); + it('should detect Chinese locale and create Chinese rule file', () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh'); diff --git a/packages/cli/src/ui/commands/languageCommand.ts b/packages/cli/src/ui/commands/languageCommand.ts index fff4a693a..e4158ce5c 100644 --- a/packages/cli/src/ui/commands/languageCommand.ts +++ b/packages/cli/src/ui/commands/languageCommand.ts @@ -15,25 +15,40 @@ import { SettingScope } from '../../config/settings.js'; import { setLanguageAsync, getCurrentLanguage, - detectSystemLanguage, - getLanguageNameFromLocale, type SupportedLanguage, t, } from '../../i18n/index.js'; +import { SUPPORTED_LANGUAGES } from '../../i18n/languages.js'; import { - SUPPORTED_LANGUAGES, - type LanguageDefinition, -} from '../../i18n/languages.js'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { Storage } from '@qwen-code/qwen-code-core'; + OUTPUT_LANGUAGE_AUTO, + isAutoLanguage, + resolveOutputLanguage, + updateOutputLanguageFile, +} from '../../utils/languageUtils.js'; -const LLM_OUTPUT_LANGUAGE_RULE_FILENAME = 'output-language.md'; -const LLM_OUTPUT_LANGUAGE_MARKER_PREFIX = 'qwen-code:llm-output-language:'; +/** + * Gets the current LLM output language setting and its resolved value. + * Returns an object with both the raw setting and the resolved language. + */ +function getCurrentOutputLanguage(context?: CommandContext): { + setting: string; + resolved: string; +} { + const settingValue = + context?.services?.settings?.merged?.general?.outputLanguage || + OUTPUT_LANGUAGE_AUTO; + const resolved = resolveOutputLanguage(settingValue); + return { setting: settingValue, resolved }; +} +/** + * Parses user input to find a matching supported UI language. + * Accepts locale codes (e.g., "zh"), IDs (e.g., "zh-CN"), or full names (e.g., "Chinese"). + */ function parseUiLanguageArg(input: string): SupportedLanguage | null { const lowered = input.trim().toLowerCase(); if (!lowered) return null; + for (const lang of SUPPORTED_LANGUAGES) { if ( lowered === lang.code || @@ -46,153 +61,22 @@ function parseUiLanguageArg(input: string): SupportedLanguage | null { return null; } +/** + * Formats a UI language code for display (e.g., "zh" -> "Chinese(zh-CN)"). + */ function formatUiLanguageDisplay(lang: SupportedLanguage): string { const option = SUPPORTED_LANGUAGES.find((o) => o.code === lang); return option ? `${option.fullName}(${option.id})` : lang; } -function sanitizeLanguageForMarker(language: string): string { - // HTML comments cannot contain "--" or end markers like "-->" or "--!>" safely. - // Also avoid newlines to keep the marker single-line and robust to parsing. - return language - .replace(/[\r\n]/g, ' ') - .replace(/--!?>/g, '') - .replace(/--/g, ''); -} - /** - * Generates the LLM output language rule template based on the language name. - */ -function generateLlmOutputLanguageRule(language: string): string { - const markerLanguage = sanitizeLanguageForMarker(language); - return `# Output language preference: ${language} - - -## Goal -Prefer responding in **${language}** for normal assistant messages and explanations. - -## Keep technical artifacts unchanged -Do **not** translate or rewrite: -- Code blocks, CLI commands, file paths, stack traces, logs, JSON keys, identifiers -- Exact quoted text from the user (keep quotes verbatim) - -## When a conflict exists -If higher-priority instructions (system/developer) require a different behavior, follow them. - -## Tool / system outputs -Raw tool/system outputs may contain fixed-format English. Preserve them verbatim, and if needed, add a short **${language}** explanation below. -`; -} - -/** - * Gets the path to the LLM output language rule file. - */ -function getLlmOutputLanguageRulePath(): string { - return path.join( - Storage.getGlobalQwenDir(), - LLM_OUTPUT_LANGUAGE_RULE_FILENAME, - ); -} - -/** - * Normalizes a language input to its full English name. - * If the input is a known locale code (e.g., "ru", "zh"), converts it to the full name. - * Otherwise, returns the input as-is (e.g., "Japanese" stays "Japanese"). - */ -function normalizeLanguageName(language: string): string { - const lowered = language.toLowerCase(); - // Check if it's a known locale code and convert to full name - const fullName = getLanguageNameFromLocale(lowered); - // If getLanguageNameFromLocale returned a different value, use it - // Otherwise, use the original input (preserves case for unknown languages) - if (fullName !== 'English' || lowered === 'en') { - return fullName; - } - return language; -} - -function extractLlmOutputLanguageFromRuleFileContent( - content: string, -): string | null { - // Preferred: machine-readable marker that supports Unicode and spaces. - // Example: - const markerMatch = content.match( - new RegExp( - String.raw``, - 'i', - ), - ); - if (markerMatch?.[1]) { - const lang = markerMatch[1].trim(); - if (lang) return lang; - } - - // Backward compatibility: parse the heading line. - // Example: "# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY" - // Example: "# ⚠️ CRITICAL: 日本語 Output Language Rule - HIGHEST PRIORITY ⚠️" - const headingMatch = content.match( - /^#.*?CRITICAL:\s*(.*?)\s+Output Language Rule\b/im, - ); - if (headingMatch?.[1]) { - const lang = headingMatch[1].trim(); - if (lang) return lang; - } - - return null; -} - -/** - * Initializes the LLM output language rule file on first startup. - * If the file already exists, it is not overwritten (respects user preference). - */ -export function initializeLlmOutputLanguage(): void { - const filePath = getLlmOutputLanguageRulePath(); - - // Skip if file already exists (user preference) - if (fs.existsSync(filePath)) { - return; - } - - // Detect system language and map to language name - const detectedLocale = detectSystemLanguage(); - const languageName = getLanguageNameFromLocale(detectedLocale); - - // Generate the rule file - const content = generateLlmOutputLanguageRule(languageName); - - // Ensure directory exists - const dir = path.dirname(filePath); - fs.mkdirSync(dir, { recursive: true }); - - // Write file - fs.writeFileSync(filePath, content, 'utf-8'); -} - -/** - * Gets the current LLM output language from the rule file if it exists. - */ -function getCurrentLlmOutputLanguage(): string | null { - const filePath = getLlmOutputLanguageRulePath(); - if (fs.existsSync(filePath)) { - try { - const content = fs.readFileSync(filePath, 'utf-8'); - return extractLlmOutputLanguageFromRuleFileContent(content); - } catch { - // Ignore errors - } - } - return null; -} - -/** - * Sets the UI language and persists it to settings. + * Sets the UI language and persists it to user settings. */ async function setUiLanguage( context: CommandContext, lang: SupportedLanguage, ): Promise { const { services } = context; - const { settings } = services; if (!services.config) { return { @@ -202,19 +86,19 @@ async function setUiLanguage( }; } - // Set language in i18n system (async to support JS translation files) + // Update i18n system await setLanguageAsync(lang); - // Persist to settings (user scope) - if (settings && typeof settings.setValue === 'function') { + // Persist to settings + if (services.settings?.setValue) { try { - settings.setValue(SettingScope.User, 'general.language', lang); + services.settings.setValue(SettingScope.User, 'general.language', lang); } catch (error) { console.warn('Failed to save language setting:', error); } } - // Reload commands to update their descriptions with the new language + // Reload commands to update localized descriptions context.ui.reloadCommands(); return { @@ -227,37 +111,51 @@ async function setUiLanguage( } /** - * Generates the LLM output language rule file. + * Handles the /language output command, updating both the setting and the rule file. + * 'auto' is preserved in settings but resolved to the detected language for the rule file. */ -function generateLlmOutputLanguageRuleFile( +async function setOutputLanguage( + context: CommandContext, language: string, ): Promise { try { - const filePath = getLlmOutputLanguageRulePath(); - // Normalize locale codes (e.g., "ru" -> "Russian") to full language names - const normalizedLanguage = normalizeLanguageName(language); - const content = generateLlmOutputLanguageRule(normalizedLanguage); + const isAuto = isAutoLanguage(language); + const resolved = resolveOutputLanguage(language); + // Save 'auto' as-is to settings, or normalize other values + const settingValue = isAuto ? OUTPUT_LANGUAGE_AUTO : resolved; - // Ensure directory exists - const dir = path.dirname(filePath); - fs.mkdirSync(dir, { recursive: true }); + // Update the rule file with the resolved language + updateOutputLanguageFile(settingValue); - // Write file (overwrite if exists) - fs.writeFileSync(filePath, content, 'utf-8'); + // Save to settings + if (context.services.settings?.setValue) { + try { + context.services.settings.setValue( + SettingScope.User, + 'general.outputLanguage', + settingValue, + ); + } catch (error) { + console.warn('Failed to save output language setting:', error); + } + } - return Promise.resolve({ + // Format display message + const displayLang = isAuto + ? `${t('Auto (detect from system)')} → ${resolved}` + : resolved; + + return { type: 'message', messageType: 'info', content: [ - t('LLM output language rule file generated at {{path}}', { - path: filePath, - }), + t('LLM output language set to {{lang}}', { lang: displayLang }), '', t('Please restart the application for the changes to take effect.'), ].join('\n'), - }); + }; } catch (error) { - return Promise.resolve({ + return { type: 'message', messageType: 'error', content: t( @@ -266,7 +164,7 @@ function generateLlmOutputLanguageRuleFile( error: error instanceof Error ? error.message : String(error), }, ), - }); + }; } } @@ -276,12 +174,12 @@ export const languageCommand: SlashCommand = { return t('View or change the language setting'); }, kind: CommandKind.BUILT_IN, + action: async ( context: CommandContext, args: string, ): Promise => { - const { services } = context; - if (!services.config) { + if (!context.services.config) { return { type: 'message', messageType: 'error', @@ -291,75 +189,83 @@ export const languageCommand: SlashCommand = { const trimmedArgs = args.trim(); - // Handle subcommands if called directly via action (for tests/backward compatibility) - const parts = trimmedArgs.split(/\s+/); - const firstArg = parts[0].toLowerCase(); - const subArgs = parts.slice(1).join(' '); + // Route to subcommands if specified + if (trimmedArgs) { + const [firstArg, ...rest] = trimmedArgs.split(/\s+/); + const subCommandName = firstArg.toLowerCase(); + const subArgs = rest.join(' '); - if (firstArg === 'ui' || firstArg === 'output') { - const subCommand = languageCommand.subCommands?.find( - (s) => s.name === firstArg, - ); - if (subCommand?.action) { - return subCommand.action( - context, - subArgs, - ) as Promise; + if (subCommandName === 'ui' || subCommandName === 'output') { + const subCommand = languageCommand.subCommands?.find( + (s) => s.name === subCommandName, + ); + if (subCommand?.action) { + return subCommand.action( + context, + subArgs, + ) as Promise; + } } + + // Backward compatibility: direct language code (e.g., /language zh) + const targetLang = parseUiLanguageArg(trimmedArgs); + if (targetLang) { + return setUiLanguage(context, targetLang); + } + + // Unknown argument + return { + type: 'message', + messageType: 'error', + content: [ + t('Invalid command. Available subcommands:'), + ` - /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`, + ` - /language output - ${t('Set LLM output language')}`, + ].join('\n'), + }; } - // If no arguments, show current language settings and usage - if (!trimmedArgs) { - const currentUiLang = getCurrentLanguage(); - const currentLlmLang = getCurrentLlmOutputLanguage(); - const message = [ + // No arguments: show current status + const currentUiLang = getCurrentLanguage(); + const { setting: outputSetting, resolved: outputResolved } = + getCurrentOutputLanguage(context); + + // Format output language display: show "Auto → English" or just "English" + const outputLangDisplay = isAutoLanguage(outputSetting) + ? `${t('Auto (detect from system)')} → ${outputResolved}` + : outputResolved; + + return { + type: 'message', + messageType: 'info', + content: [ t('Current UI language: {{lang}}', { lang: formatUiLanguageDisplay(currentUiLang as SupportedLanguage), }), - currentLlmLang - ? t('Current LLM output language: {{lang}}', { lang: currentLlmLang }) - : t('LLM output language not set'), + t('Current LLM output language: {{lang}}', { lang: outputLangDisplay }), '', t('Available subcommands:'), ` /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`, ` /language output - ${t('Set LLM output language')}`, - ].join('\n'); - - return { - type: 'message', - messageType: 'info', - content: message, - }; - } - - // Handle backward compatibility for /language [lang] - const targetLang = parseUiLanguageArg(trimmedArgs); - if (targetLang) { - return setUiLanguage(context, targetLang); - } - - return { - type: 'message', - messageType: 'error', - content: [ - t('Invalid command. Available subcommands:'), - ` - /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`, - ' - /language output - ' + t('Set LLM output language'), ].join('\n'), }; }, + subCommands: [ + // /language ui subcommand { name: 'ui', get description() { return t('Set UI language'); }, kind: CommandKind.BUILT_IN, + action: async ( context: CommandContext, args: string, ): Promise => { const trimmedArgs = args.trim(); + if (!trimmedArgs) { return { type: 'message', @@ -396,19 +302,45 @@ export const languageCommand: SlashCommand = { return setUiLanguage(context, targetLang); }, - subCommands: SUPPORTED_LANGUAGES.map(createUiLanguageSubCommand), + + // Nested subcommands for each supported language (e.g., /language ui zh-CN) + subCommands: SUPPORTED_LANGUAGES.map( + (lang): SlashCommand => ({ + name: lang.id, + get description() { + return t('Set UI language to {{name}}', { name: lang.fullName }); + }, + kind: CommandKind.BUILT_IN, + action: async (context, args) => { + if (args.trim()) { + return { + type: 'message', + messageType: 'error', + content: t( + 'Language subcommands do not accept additional arguments.', + ), + }; + } + return setUiLanguage(context, lang.code); + }, + }), + ), }, + + // /language output subcommand { name: 'output', get description() { return t('Set LLM output language'); }, kind: CommandKind.BUILT_IN, + action: async ( context: CommandContext, args: string, ): Promise => { const trimmedArgs = args.trim(); + if (!trimmedArgs) { return { type: 'message', @@ -424,33 +356,8 @@ export const languageCommand: SlashCommand = { }; } - return generateLlmOutputLanguageRuleFile(trimmedArgs); + return setOutputLanguage(context, trimmedArgs); }, }, ], }; - -/** - * Helper to create a UI language subcommand. - */ -function createUiLanguageSubCommand(option: LanguageDefinition): SlashCommand { - return { - name: option.id, - get description() { - return t('Set UI language to {{name}}', { name: option.fullName }); - }, - kind: CommandKind.BUILT_IN, - action: async (context, args) => { - if (args.trim().length > 0) { - return { - type: 'message', - messageType: 'error', - content: t( - 'Language subcommands do not accept additional arguments.', - ), - }; - } - return setUiLanguage(context, option.code); - }, - }; -} diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index e04fd42c8..70ef13711 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -8,16 +8,14 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import type { ExtendedSystemInfo } from '../../utils/systemInfo.js'; -import { - getSystemInfoFields, - getFieldValue, - type SystemInfoField, -} from '../../utils/systemInfoFields.js'; +import { getSystemInfoFields } 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,25 +24,26 @@ export const AboutBox: React.FC = (props) => { borderColor={theme.border.default} flexDirection="column" padding={1} - marginY={1} - width="100%" + width={width} > - {t('About Qwen Code')} + {t('Status')} - {fields.map((field: SystemInfoField) => ( - + {fields.map((field) => ( + {field.label} - - {getFieldValue(field, props)} - + {field.value} ))} 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/ApprovalModeDialog.tsx b/packages/cli/src/ui/components/ApprovalModeDialog.tsx index d81b6f4c0..48293ffdc 100644 --- a/packages/cli/src/ui/components/ApprovalModeDialog.tsx +++ b/packages/cli/src/ui/components/ApprovalModeDialog.tsx @@ -54,7 +54,7 @@ export function ApprovalModeDialog({ }: ApprovalModeDialogProps): React.JSX.Element { // Start with User scope by default const [selectedScope, setSelectedScope] = useState( - SettingScope.Workspace, + SettingScope.User, ); // Track the currently highlighted approval mode @@ -90,19 +90,17 @@ export function ApprovalModeDialog({ setSelectedScope(scope); }, []); - const handleScopeSelect = useCallback( - (scope: SettingScope) => { - onSelect(highlightedMode, scope); - }, - [onSelect, highlightedMode], - ); + const handleScopeSelect = useCallback((scope: SettingScope) => { + setSelectedScope(scope); + setMode('mode'); + }, []); - const [focusSection, setFocusSection] = useState<'mode' | 'scope'>('mode'); + const [mode, setMode] = useState<'mode' | 'scope'>('mode'); useKeypress( (key) => { if (key.name === 'tab') { - setFocusSection((prev) => (prev === 'mode' ? 'scope' : 'mode')); + setMode((prev) => (prev === 'mode' ? 'scope' : 'mode')); } if (key.name === 'escape') { onSelect(undefined, selectedScope); @@ -127,59 +125,56 @@ export function ApprovalModeDialog({ - - {/* Approval Mode Selection */} - - {focusSection === 'mode' ? '> ' : ' '} - {t('Approval Mode')}{' '} - {otherScopeModifiedMessage} - - - - - - - {/* Scope Selection */} - - - - - - - {/* Warning when workspace setting will override user setting */} - {showWorkspacePriorityWarning && ( - <> - - ⚠{' '} - {t( - 'Workspace approval mode exists and takes priority. User-level change will have no effect.', - )} + {mode === 'mode' ? ( + + {/* Approval Mode Selection */} + + {mode === 'mode' ? '> ' : ' '} + {t('Approval Mode')}{' '} + + {otherScopeModifiedMessage} - - - )} - - - {t('(Use Enter to select, Tab to change focus)')} + + + + {/* Warning when workspace setting will override user setting */} + {showWorkspacePriorityWarning && ( + + + ⚠{' '} + {t( + 'Workspace approval mode exists and takes priority. User-level change will have no effect.', + )} + + + )} + + ) : ( + + )} + + + {mode === 'mode' + ? t('(Use Enter to select, Tab to configure scope)') + : t('(Use Enter to apply scope, Tab to go back)')} 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 9052e4f4d..50b04a1d2 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -4,26 +4,20 @@ * 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 { FeedbackDialog } from '../FeedbackDialog.js'; @@ -31,16 +25,26 @@ 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( @@ -49,7 +53,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 && ( @@ -152,6 +107,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} @@ -163,7 +121,13 @@ export const Composer = () => { /> )} - {!settings.merged.ui?.hideFooter && !isScreenReaderEnabled &&