From 7e4159569e6aed7dad2c901b9cea49d18f2783fb Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Sat, 28 Feb 2026 14:59:56 +0800 Subject: [PATCH] refactor(cli): simplify auth type display in Header - Add AuthDisplayType enum and helper for Coding Plan detection - Remove formatAuthType/titleizeAuthType functions - Update tests for new auth types Co-authored-by: Qwen-Coder --- packages/cli/src/ui/components/AppHeader.tsx | 37 +++++++++- .../cli/src/ui/components/Header.test.tsx | 69 +++++++------------ packages/cli/src/ui/components/Header.tsx | 56 +++++---------- 3 files changed, 77 insertions(+), 85 deletions(-) diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index ba044d10d..0254a2012 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -5,16 +5,43 @@ */ import { Box } from 'ink'; -import { Header } from './Header.js'; +import { AuthType } from '@qwen-code/qwen-code-core'; +import { Header, AuthDisplayType } 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'; +import { isCodingPlanConfig } from '../../constants/codingPlan.js'; interface AppHeaderProps { version: string; } +/** + * Determine the auth display type based on auth type and configuration. + */ +function getAuthDisplayType( + authType?: AuthType, + baseUrl?: string, + apiKeyEnvKey?: string, +): AuthDisplayType { + if (!authType) { + return AuthDisplayType.UNKNOWN; + } + + // Check if it's a Coding Plan config + if (isCodingPlanConfig(baseUrl, apiKeyEnvKey)) { + return AuthDisplayType.CODING_PLAN; + } + + switch (authType) { + case AuthType.QWEN_OAUTH: + return AuthDisplayType.QWEN_OAUTH; + default: + return AuthDisplayType.API_KEY; + } +} + export const AppHeader = ({ version }: AppHeaderProps) => { const settings = useSettings(); const config = useConfig(); @@ -27,12 +54,18 @@ export const AppHeader = ({ version }: AppHeaderProps) => { const showBanner = !config.getScreenReader(); const showTips = !(settings.merged.ui?.hideTips || config.getScreenReader()); + const authDisplayType = getAuthDisplayType( + authType, + contentGeneratorConfig?.baseUrl, + contentGeneratorConfig?.apiKeyEnvKey, + ); + return ( {showBanner && (
diff --git a/packages/cli/src/ui/components/Header.test.tsx b/packages/cli/src/ui/components/Header.test.tsx index 1d3a4d7f1..99bb053da 100644 --- a/packages/cli/src/ui/components/Header.test.tsx +++ b/packages/cli/src/ui/components/Header.test.tsx @@ -6,8 +6,7 @@ import { render } from 'ink-testing-library'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AuthType } from '@qwen-code/qwen-code-core'; -import { Header } from './Header.js'; +import { Header, AuthDisplayType } from './Header.js'; import * as useTerminalSize from '../hooks/useTerminalSize.js'; vi.mock('../hooks/useTerminalSize.js'); @@ -15,86 +14,70 @@ const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize); const defaultProps = { version: '1.0.0', - authType: AuthType.QWEN_OAUTH, + authDisplayType: AuthDisplayType.QWEN_OAUTH, model: 'qwen-coder-plus', workingDirectory: '/home/user/projects/test', }; describe('
', () => { beforeEach(() => { - // Default to wide terminal (shows both logo and info panel) useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 }); }); it('renders the ASCII logo on wide terminal', () => { const { lastFrame } = render(
); - // Check that parts of the shortAsciiLogo are rendered expect(lastFrame()).toContain('██╔═══██╗'); }); it('hides the ASCII logo on narrow terminal', () => { useTerminalSizeMock.mockReturnValue({ columns: 60, rows: 24 }); const { lastFrame } = render(
); - // Should not contain the logo but still show the info panel expect(lastFrame()).not.toContain('██╔═══██╗'); expect(lastFrame()).toContain('>_ Qwen Code'); }); - it('renders custom ASCII art when provided on wide terminal', () => { - const customArt = 'CUSTOM ART'; - const { lastFrame } = render( -
, - ); - expect(lastFrame()).toContain(customArt); - }); - it('displays the version number', () => { const { lastFrame } = render(
); expect(lastFrame()).toContain('v1.0.0'); }); - it('displays Qwen Code title with >_ prefix', () => { - const { lastFrame } = render(
); - expect(lastFrame()).toContain('>_ Qwen Code'); - }); - it('displays auth type and model', () => { const { lastFrame } = render(
); expect(lastFrame()).toContain('Qwen OAuth'); expect(lastFrame()).toContain('qwen-coder-plus'); }); + it('displays Coding Plan auth type', () => { + const { lastFrame } = render( +
, + ); + expect(lastFrame()).toContain('Coding Plan'); + }); + + it('displays API Key auth type', () => { + const { lastFrame } = render( +
, + ); + expect(lastFrame()).toContain('API Key'); + }); + + it('displays Unknown when auth type is not set', () => { + const { lastFrame } = render( +
, + ); + expect(lastFrame()).toContain('Unknown'); + }); + it('displays working directory', () => { const { lastFrame } = render(
); expect(lastFrame()).toContain('/home/user/projects/test'); }); - it('renders a custom working directory display', () => { - const { lastFrame } = render( -
, - ); - expect(lastFrame()).toContain('custom display'); - }); - - it('displays working directory without branch name', () => { - const { lastFrame } = render(
); - // Branch name is no longer shown in header - expect(lastFrame()).toContain('/home/user/projects/test'); - expect(lastFrame()).not.toContain('(main*)'); - }); - - it('formats home directory with tilde', () => { - const { lastFrame } = render( -
, - ); - // The actual home dir replacement depends on os.homedir() - // Just verify the path is shown - expect(lastFrame()).toContain('projects'); - }); - it('renders with border around info panel', () => { const { lastFrame } = render(
); - // Check for border characters (round border style uses these) expect(lastFrame()).toContain('╭'); expect(lastFrame()).toContain('╯'); }); diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx index adbe13071..45fce4385 100644 --- a/packages/cli/src/ui/components/Header.tsx +++ b/packages/cli/src/ui/components/Header.tsx @@ -7,59 +7,35 @@ import type React from 'react'; import { Box, Text } from 'ink'; import Gradient from 'ink-gradient'; -import { AuthType, shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core'; +import { shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core'; import { theme } from '../semantic-colors.js'; import { shortAsciiLogo } from './AsciiArt.js'; import { getAsciiArtWidth, getCachedStringWidth } from '../utils/textUtils.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; +/** + * Auth display type for the Header component. + * Simplified representation of authentication method shown to users. + */ +export enum AuthDisplayType { + QWEN_OAUTH = 'Qwen OAuth', + CODING_PLAN = 'Coding Plan', + API_KEY = 'API Key', + UNKNOWN = 'Unknown', +} + interface HeaderProps { customAsciiArt?: string; // For user-defined ASCII art version: string; - authType?: AuthType; + authDisplayType?: AuthDisplayType; model: string; 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 titleizeAuthType(String(authType)); - } -} - export const Header: React.FC = ({ customAsciiArt, version, - authType, + authDisplayType, model, workingDirectory, }) => { @@ -67,7 +43,7 @@ export const Header: React.FC = ({ const displayLogo = customAsciiArt ?? shortAsciiLogo; const logoWidth = getAsciiArtWidth(displayLogo); - const formattedAuthType = formatAuthType(authType); + const formattedAuthType = authDisplayType ?? AuthDisplayType.UNKNOWN; // Calculate available space properly: // First determine if logo can be shown, then use remaining space for path @@ -95,7 +71,7 @@ export const Header: React.FC = ({ ? Math.min(availableTerminalWidth - logoWidth - logoGap, maxInfoPanelWidth) : availableTerminalWidth; - // Calculate max path length (subtract padding/borders from available space) + // Calculate max path lengths (subtract padding/borders from available space) const maxPathLength = Math.max( 0, availableInfoPanelWidth - infoPanelChromeWidth,