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 <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-28 14:59:56 +08:00
parent 00f9c56660
commit 7e4159569e
3 changed files with 77 additions and 85 deletions

View file

@ -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 (
<Box flexDirection="column">
{showBanner && (
<Header
version={version}
authType={authType}
authDisplayType={authDisplayType}
model={model}
workingDirectory={targetDir}
/>

View file

@ -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('<Header />', () => {
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(<Header {...defaultProps} />);
// 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(<Header {...defaultProps} />);
// 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(
<Header {...defaultProps} customAsciiArt={customArt} />,
);
expect(lastFrame()).toContain(customArt);
});
it('displays the version number', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('v1.0.0');
});
it('displays Qwen Code title with >_ prefix', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('>_ Qwen Code');
});
it('displays auth type and model', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('Qwen OAuth');
expect(lastFrame()).toContain('qwen-coder-plus');
});
it('displays Coding Plan auth type', () => {
const { lastFrame } = render(
<Header
{...defaultProps}
authDisplayType={AuthDisplayType.CODING_PLAN}
/>,
);
expect(lastFrame()).toContain('Coding Plan');
});
it('displays API Key auth type', () => {
const { lastFrame } = render(
<Header {...defaultProps} authDisplayType={AuthDisplayType.API_KEY} />,
);
expect(lastFrame()).toContain('API Key');
});
it('displays Unknown when auth type is not set', () => {
const { lastFrame } = render(
<Header {...defaultProps} authDisplayType={undefined} />,
);
expect(lastFrame()).toContain('Unknown');
});
it('displays working directory', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('/home/user/projects/test');
});
it('renders a custom working directory display', () => {
const { lastFrame } = render(
<Header {...defaultProps} workingDirectory="custom display" />,
);
expect(lastFrame()).toContain('custom display');
});
it('displays working directory without branch name', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
// 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(
<Header {...defaultProps} workingDirectory="/Users/testuser/projects" />,
);
// 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(<Header {...defaultProps} />);
// Check for border characters (round border style uses these)
expect(lastFrame()).toContain('╭');
expect(lastFrame()).toContain('╯');
});

View file

@ -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<HeaderProps> = ({
customAsciiArt,
version,
authType,
authDisplayType,
model,
workingDirectory,
}) => {
@ -67,7 +43,7 @@ export const Header: React.FC<HeaderProps> = ({
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<HeaderProps> = ({
? 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,