mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-26 10:41:41 +00:00
fix(cli): guard gradient rendering without colors
This commit is contained in:
parent
a6b0b7e579
commit
cc9e65365d
5 changed files with 98 additions and 9 deletions
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Header, AuthDisplayType } from './Header.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
|
||||
|
|
@ -20,10 +20,21 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
describe('<Header />', () => {
|
||||
const originalNoColor = process.env['NO_COLOR'];
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env['NO_COLOR'];
|
||||
useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalNoColor === undefined) {
|
||||
delete process.env['NO_COLOR'];
|
||||
} else {
|
||||
process.env['NO_COLOR'] = originalNoColor;
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the ASCII logo on wide terminal', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('██╔═══██╗');
|
||||
|
|
@ -81,4 +92,12 @@ describe('<Header />', () => {
|
|||
expect(lastFrame()).toContain('┌');
|
||||
expect(lastFrame()).toContain('┐');
|
||||
});
|
||||
|
||||
it('renders plain text when NO_COLOR disables gradient colors', () => {
|
||||
process.env['NO_COLOR'] = '1';
|
||||
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
|
||||
expect(lastFrame()).toContain('██╔═══██╗');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { theme } from '../semantic-colors.js';
|
|||
import { shortAsciiLogo } from './AsciiArt.js';
|
||||
import { getAsciiArtWidth, getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { getRenderableGradientColors } from '../utils/gradientUtils.js';
|
||||
|
||||
/**
|
||||
* Auth display type for the Header component.
|
||||
|
|
@ -98,12 +99,11 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
? shortenedPath.slice(0, maxPathLength)
|
||||
: shortenedPath;
|
||||
|
||||
// Use theme gradient colors if available, otherwise use text colors (excluding primary)
|
||||
const gradientColors = theme.ui.gradient || [
|
||||
const gradientColors = getRenderableGradientColors(theme.ui.gradient, [
|
||||
theme.text.secondary,
|
||||
theme.text.link,
|
||||
theme.text.accent,
|
||||
];
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -116,9 +116,13 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
{showLogo && (
|
||||
<>
|
||||
<Box flexShrink={0}>
|
||||
<Gradient colors={gradientColors}>
|
||||
{gradientColors ? (
|
||||
<Gradient colors={gradientColors}>
|
||||
<Text>{displayLogo}</Text>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Text>{displayLogo}</Text>
|
||||
</Gradient>
|
||||
)}
|
||||
</Box>
|
||||
{/* Fixed gap between logo and info panel */}
|
||||
<Box width={logoGap} />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type {
|
||||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
SessionMetrics,
|
||||
} from '../contexts/SessionContext.js';
|
||||
import { MAIN_SOURCE } from '@qwen-code/qwen-code-core';
|
||||
import { DEFAULT_THEME, themeManager } from '../themes/theme-manager.js';
|
||||
|
||||
// Wraps a core metrics object as a ModelMetrics with a single `main` source
|
||||
// bucket, matching the shape produced by processing an API call with no
|
||||
|
|
@ -33,6 +34,21 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
|||
});
|
||||
|
||||
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
const originalNoColor = process.env['NO_COLOR'];
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env['NO_COLOR'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalNoColor === undefined) {
|
||||
delete process.env['NO_COLOR'];
|
||||
} else {
|
||||
process.env['NO_COLOR'] = originalNoColor;
|
||||
}
|
||||
themeManager.loadCustomThemes({});
|
||||
themeManager.setActiveTheme(DEFAULT_THEME.name);
|
||||
});
|
||||
|
||||
const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
|
|
@ -558,5 +574,35 @@ describe('<StatsDisplay />', () => {
|
|||
expect(output).not.toContain('Session Stats');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a custom title as plain text when the theme has too few gradient colors', () => {
|
||||
themeManager.loadCustomThemes({
|
||||
OneColorGradient: {
|
||||
name: 'OneColorGradient',
|
||||
type: 'custom',
|
||||
ui: { gradient: ['red'] },
|
||||
},
|
||||
});
|
||||
themeManager.setActiveTheme('OneColorGradient');
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics: zeroMetrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = render(
|
||||
<StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Agent powering down. Goodbye!');
|
||||
expect(output).not.toContain('Invalid number of stops');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
USER_AGREEMENT_RATE_HIGH,
|
||||
USER_AGREEMENT_RATE_MEDIUM,
|
||||
} from '../utils/displayUtils.js';
|
||||
import { getRenderableGradientColors } from '../utils/gradientUtils.js';
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import { flattenModelsBySource } from '../utils/modelsBySource.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
|
@ -194,8 +195,9 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
|||
|
||||
const renderTitle = () => {
|
||||
if (title) {
|
||||
return theme.ui.gradient && theme.ui.gradient.length > 0 ? (
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
const gradientColors = getRenderableGradientColors(theme.ui.gradient);
|
||||
return gradientColors ? (
|
||||
<Gradient colors={gradientColors}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{title}
|
||||
</Text>
|
||||
|
|
|
|||
18
packages/cli/src/ui/utils/gradientUtils.ts
Normal file
18
packages/cli/src/ui/utils/gradientUtils.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export function getRenderableGradientColors(
|
||||
...candidates: Array<string[] | undefined>
|
||||
): string[] | undefined {
|
||||
return candidates.find(
|
||||
(colors): colors is string[] =>
|
||||
Array.isArray(colors) &&
|
||||
colors.length >= 2 &&
|
||||
colors.every(
|
||||
(color) => typeof color === 'string' && color.trim().length > 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue