Fix: Animated scrollbar renders black in NO_COLOR mode (#13188)

This commit is contained in:
Jacob Richman
2025-11-16 20:45:07 -08:00
committed by GitHub
parent cf8de02c68
commit 78a28bfc01
9 changed files with 202 additions and 44 deletions
+5 -7
View File
@@ -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');
});
});
+2 -23
View File
@@ -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>;
};