mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
Fix: Animated scrollbar renders black in NO_COLOR mode (#13188)
This commit is contained in:
@@ -10,7 +10,7 @@ import { theme } from '../semantic-colors.js';
|
||||
import { shortenPath, tildeifyPath } from '@google/gemini-cli-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { DebugProfiler } from './DebugProfiler.js';
|
||||
@@ -87,12 +87,10 @@ export const Footer: React.FC = () => {
|
||||
)}
|
||||
{!hideCWD &&
|
||||
(nightly ? (
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
<Text>
|
||||
{displayPath}
|
||||
{branchName && <Text> ({branchName}*)</Text>}
|
||||
</Text>
|
||||
</Gradient>
|
||||
<ThemedGradient>
|
||||
{displayPath}
|
||||
{branchName && <Text> ({branchName}*)</Text>}
|
||||
</ThemedGradient>
|
||||
) : (
|
||||
<Text color={theme.text.link}>
|
||||
{displayPath}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
|
||||
// Mock the theme module
|
||||
vi.mock('../semantic-colors.js', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('../semantic-colors.js')>();
|
||||
return {
|
||||
...original,
|
||||
theme: {
|
||||
...original.theme,
|
||||
ui: {
|
||||
...original.theme.ui,
|
||||
gradient: [], // Empty array to potentially trigger the crash
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SessionContext>();
|
||||
return {
|
||||
...actual,
|
||||
useSessionStats: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockSessionStats: SessionStatsState = {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 0,
|
||||
metrics: {
|
||||
models: {},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0, auto_accept: 0 },
|
||||
byName: {},
|
||||
},
|
||||
files: { totalLinesAdded: 0, totalLinesRemoved: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: mockSessionStats,
|
||||
getPromptCount: () => 0,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
describe('Gradient Crash Regression Tests', () => {
|
||||
it('<Footer /> should not crash when theme.ui.gradient has only one color (or empty) and nightly is true', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
nightly: true, // Enable nightly to trigger Gradient usage logic
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
});
|
||||
// If it crashes, this line won't be reached or lastFrame() will throw
|
||||
expect(lastFrame()).toBeDefined();
|
||||
// It should fall back to rendering text without gradient
|
||||
expect(lastFrame()).not.toContain('Gradient');
|
||||
});
|
||||
|
||||
it('<StatsDisplay /> should not crash when theme.ui.gradient is empty', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StatsDisplay duration="1s" title="My Stats" />,
|
||||
{
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toBeDefined();
|
||||
// Ensure title is rendered
|
||||
expect(lastFrame()).toContain('My Stats');
|
||||
});
|
||||
});
|
||||
@@ -5,9 +5,8 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { Box } from 'ink';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import {
|
||||
shortAsciiLogo,
|
||||
longAsciiLogo,
|
||||
@@ -26,26 +25,6 @@ interface HeaderProps {
|
||||
nightly: boolean;
|
||||
}
|
||||
|
||||
const ThemedGradient: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const gradient = theme.ui.gradient;
|
||||
|
||||
if (gradient && gradient.length >= 2) {
|
||||
return (
|
||||
<Gradient colors={gradient}>
|
||||
<Text>{children}</Text>
|
||||
</Gradient>
|
||||
);
|
||||
}
|
||||
|
||||
if (gradient && gradient.length === 1) {
|
||||
return <Text color={gradient[0]}>{children}</Text>;
|
||||
}
|
||||
|
||||
return <Text>{children}</Text>;
|
||||
};
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({
|
||||
customAsciiArt,
|
||||
version,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import type { ModelMetrics } from '../contexts/SessionContext.js';
|
||||
@@ -185,17 +185,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
|
||||
const renderTitle = () => {
|
||||
if (title) {
|
||||
return theme.ui.gradient && theme.ui.gradient.length > 0 ? (
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{title}
|
||||
</Text>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Text bold color={theme.text.accent}>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
return <ThemedGradient bold>{title}</ThemedGradient>;
|
||||
}
|
||||
return (
|
||||
<Text bold color={theme.text.accent}>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, type TextProps } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
export const ThemedGradient: React.FC<TextProps> = ({ children, ...props }) => {
|
||||
const gradient = theme.ui.gradient;
|
||||
|
||||
if (gradient && gradient.length >= 2) {
|
||||
return (
|
||||
<Gradient colors={gradient}>
|
||||
<Text {...props}>{children}</Text>
|
||||
</Gradient>
|
||||
);
|
||||
}
|
||||
|
||||
if (gradient && gradient.length === 1) {
|
||||
return (
|
||||
<Text color={gradient[0]} {...props}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text {...props}>{children}</Text>;
|
||||
};
|
||||
Reference in New Issue
Block a user