diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx index 304833326..5d3efd905 100644 --- a/packages/cli/src/ui/components/Header.tsx +++ b/packages/cli/src/ui/components/Header.tsx @@ -10,7 +10,7 @@ import Gradient from 'ink-gradient'; import { AuthType, shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core'; import { theme } from '../semantic-colors.js'; import { shortAsciiLogo } from './AsciiArt.js'; -import { getAsciiArtWidth } from '../utils/textUtils.js'; +import { getAsciiArtWidth, getCachedStringWidth } from '../utils/textUtils.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; interface HeaderProps { @@ -21,15 +21,38 @@ interface HeaderProps { workingDirectory: string; } +function titleizeAuthType(value: string): string { + return value + .split(/[-_]/g) + .filter(Boolean) + .map((part) => { + if (part.toLowerCase() === 'ai') { + return 'AI'; + } + return part.charAt(0).toUpperCase() + part.slice(1); + }) + .join(' '); +} + // Format auth type for display function formatAuthType(authType?: AuthType): string { + if (!authType) { + return 'Unknown'; + } + switch (authType) { case AuthType.QWEN_OAUTH: return 'Qwen OAuth'; case AuthType.USE_OPENAI: return 'OpenAI'; + case AuthType.USE_GEMINI: + return 'Gemini'; + case AuthType.USE_VERTEX_AI: + return 'Vertex AI'; + case AuthType.USE_ANTHROPIC: + return 'Anthropic'; default: - return 'Unknown'; + return titleizeAuthType(String(authType)); } } @@ -48,30 +71,53 @@ export const Header: React.FC = ({ // Calculate available space properly: // First determine if logo can be shown, then use remaining space for path - const logoGap = 4; // Gap between logo and info panel - const infoPanelPadding = 4; // paddingX={1} on each side + borders = ~4 chars + const containerMarginX = 2; // marginLeft + marginRight on the outer container + const logoGap = 2; // Gap between logo and info panel + const infoPanelPaddingX = 1; + const infoPanelBorderWidth = 2; // left + right border + const infoPanelChromeWidth = infoPanelBorderWidth + infoPanelPaddingX * 2; const minPathLength = 40; // Minimum readable path length - const minInfoPanelWidth = minPathLength + infoPanelPadding; + const minInfoPanelWidth = minPathLength + infoPanelChromeWidth; + + const availableTerminalWidth = Math.max( + 0, + terminalWidth - containerMarginX * 2, + ); // Check if we have enough space for logo + gap + minimum info panel - const showLogo = terminalWidth >= logoWidth + logoGap + minInfoPanelWidth; + const showLogo = + availableTerminalWidth >= logoWidth + logoGap + minInfoPanelWidth; // Calculate available width for info panel (use all remaining space) const availableInfoPanelWidth = showLogo - ? terminalWidth - logoWidth - logoGap - : terminalWidth; + ? availableTerminalWidth - logoWidth - logoGap + : availableTerminalWidth; // Calculate max path length (subtract padding/borders from available space) const maxPathLength = Math.max( - minPathLength, - availableInfoPanelWidth - infoPanelPadding, + 0, + availableInfoPanelWidth - infoPanelChromeWidth, ); - // Now shorten the path to fit the available space - const displayPath = shortenPath( - tildeifyPath(workingDirectory), - maxPathLength, + const infoPanelContentWidth = Math.max( + 0, + availableInfoPanelWidth - infoPanelChromeWidth, ); + const authModelText = `${formattedAuthType} | ${model}`; + const authHintText = ' (/auth to change)'; + const showAuthHint = + infoPanelContentWidth > 0 && + getCachedStringWidth(authModelText + authHintText) <= infoPanelContentWidth; + + // Now shorten the path to fit the available space + const tildeifiedPath = tildeifyPath(workingDirectory); + const shortenedPath = shortenPath(tildeifiedPath, Math.max(3, maxPathLength)); + const displayPath = + maxPathLength <= 0 + ? '' + : shortenedPath.length > maxPathLength + ? shortenedPath.slice(0, maxPathLength) + : shortenedPath; // Use theme gradient colors if available, otherwise use text colors (excluding primary) const gradientColors = theme.ui.gradient || [ @@ -81,7 +127,12 @@ export const Header: React.FC = ({ ]; return ( - + {/* Left side: ASCII logo (only if enough space) */} {showLogo && ( <> @@ -91,7 +142,7 @@ export const Header: React.FC = ({ {/* Fixed gap between logo and info panel */} - + )} @@ -100,7 +151,7 @@ export const Header: React.FC = ({ flexDirection="column" borderStyle="round" borderColor={theme.border.default} - paddingX={1} + paddingX={infoPanelPaddingX} flexGrow={1} > {/* Title line: >_ Qwen Code (v{version}) */} @@ -114,10 +165,10 @@ export const Header: React.FC = ({ {/* Auth and Model line */} - - {formattedAuthType} | {model} - - (/auth to change) + {authModelText} + {showAuthHint && ( + {authHintText} + )} {/* Directory line */} {displayPath}