qwen-code/packages/cli/src/ui/components/Footer.tsx
Shaojin Wen 746f67f436
refactor: rename verboseMode to compactMode for better UX clarity (#3075)
The "Compact Mode" label is more intuitive than "Verbose Mode" for users,
as it directly describes the default compact view experience. This change
inverts the boolean semantics (compactMode=false means show full output)
and exposes the setting in the /settings dialog (showInDialog: true).

- Rename ui.verboseMode → ui.compactMode with inverted default (false)
- Rename VerboseModeContext → CompactModeContext (file and exports)
- Rename TOGGLE_VERBOSE_MODE → TOGGLE_COMPACT_MODE in key bindings
- Update all consumer components with inverted logic
- Update i18n keys across 6 locales (verbose → compact)
- Update VS Code settings schema
- Add ui.compactMode documentation to settings.md
- Fix Ctrl+O description in keyboard-shortcuts.md
2026-04-10 11:55:50 +08:00

144 lines
4.9 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { useStatusLine } from '../hooks/useStatusLine.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { useCompactMode } from '../contexts/CompactModeContext.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
export const Footer: React.FC = () => {
const uiState = useUIState();
const config = useConfig();
const { vimEnabled, vimMode } = useVimMode();
const { text: statusLineText } = useStatusLine();
const { compactMode } = useCompactMode();
const { promptTokenCount, showAutoAcceptIndicator } = {
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
showAutoAcceptIndicator: uiState.showAutoAcceptIndicator,
};
const { columns: terminalWidth } = useTerminalSize();
const isNarrow = isNarrowWidth(terminalWidth);
// Determine sandbox info from environment
const sandboxEnv = process.env['SANDBOX'];
const sandboxInfo = sandboxEnv
? sandboxEnv === 'sandbox-exec'
? 'seatbelt'
: sandboxEnv.startsWith('qwen-code')
? 'docker'
: sandboxEnv
: null;
// Check if debug mode is enabled
const debugMode = config.getDebugMode();
const contextWindowSize =
config.getContentGeneratorConfig()?.contextWindowSize;
// Hide "? for shortcuts" when a custom status line is active (it already
// occupies the top row, so the hint is redundant). Matches upstream behavior.
const suppressHint = !!statusLineText;
// Left bottom row: high-priority messages > approval mode > hint.
const leftBottomContent = uiState.ctrlCPressedOnce ? (
<Text color={theme.status.warning}>{t('Press Ctrl+C again to exit.')}</Text>
) : uiState.ctrlDPressedOnce ? (
<Text color={theme.status.warning}>{t('Press Ctrl+D again to exit.')}</Text>
) : uiState.showEscapePrompt ? (
<Text color={theme.text.secondary}>{t('Press Esc again to clear.')}</Text>
) : vimEnabled && vimMode === 'INSERT' ? (
<Text color={theme.text.secondary}>-- INSERT --</Text>
) : uiState.shellModeActive ? (
<ShellModeIndicator />
) : showAutoAcceptIndicator !== undefined &&
showAutoAcceptIndicator !== ApprovalMode.DEFAULT ? (
<AutoAcceptIndicator approvalMode={showAutoAcceptIndicator} />
) : suppressHint ? null : (
<Text color={theme.text.secondary}>{t('? for shortcuts')}</Text>
);
const rightItems: Array<{ key: string; node: React.ReactNode }> = [];
if (sandboxInfo) {
rightItems.push({
key: 'sandbox',
node: <Text color={theme.status.success}>🔒 {sandboxInfo}</Text>,
});
}
if (debugMode) {
rightItems.push({
key: 'debug',
node: <Text color={theme.status.warning}>Debug Mode</Text>,
});
}
if (promptTokenCount > 0 && contextWindowSize) {
rightItems.push({
key: 'context',
node: (
<Text color={theme.text.accent}>
<ContextUsageDisplay
promptTokenCount={promptTokenCount}
terminalWidth={terminalWidth}
contextWindowSize={contextWindowSize}
/>
</Text>
),
});
}
if (compactMode) {
rightItems.push({
key: 'compact',
node: <Text color={theme.text.accent}>{t('compact')}</Text>,
});
}
// Layout matches upstream: left column has status line (top) + hints/mode
// (bottom), right section has indicators. Status line and hints coexist.
return (
<Box
flexDirection={isNarrow ? 'column' : 'row'}
justifyContent={isNarrow ? 'flex-start' : 'space-between'}
width="100%"
paddingX={2}
gap={isNarrow ? 0 : 1}
>
{/* Left column — status line on top, hints/mode on bottom */}
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
{statusLineText &&
!uiState.ctrlCPressedOnce &&
!uiState.ctrlDPressedOnce && (
<Text dimColor wrap="truncate">
{statusLineText}
</Text>
)}
<Text wrap="truncate">{leftBottomContent}</Text>
</Box>
{/* Right Section — never compressed */}
<Box flexShrink={0} gap={1}>
{rightItems.map(({ key, node }, index) => (
<Box key={key} alignItems="center">
{index > 0 && <Text color={theme.text.secondary}> | </Text>}
{node}
</Box>
))}
</Box>
</Box>
);
};