diff --git a/src/__tests__/renderer/components/TabBar.test.tsx b/src/__tests__/renderer/components/TabBar.test.tsx
index f6cd3553..0da735b6 100644
--- a/src/__tests__/renderer/components/TabBar.test.tsx
+++ b/src/__tests__/renderer/components/TabBar.test.tsx
@@ -3994,3 +3994,388 @@ describe('File tab content and SSH support', () => {
expect(fileTab.editMode).toBe(true);
});
});
+
+// Extension badge styling tests for visual polish across themes
+describe('Extension badge styling across themes', () => {
+ const mockOnTabSelect = vi.fn();
+ const mockOnTabClose = vi.fn();
+ const mockOnNewTab = vi.fn();
+ const mockOnFileTabSelect = vi.fn();
+ const mockOnFileTabClose = vi.fn();
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.clearAllMocks();
+ Element.prototype.scrollTo = vi.fn();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ // Light theme for testing contrast
+ const lightTheme: Theme = {
+ id: 'github-light',
+ name: 'GitHub Light',
+ mode: 'light',
+ colors: {
+ bgMain: '#ffffff',
+ bgSidebar: '#f6f8fa',
+ bgActivity: '#eff2f5',
+ textMain: '#24292f',
+ textDim: '#57606a',
+ accent: '#0969da',
+ border: '#d0d7de',
+ error: '#cf222e',
+ success: '#1a7f37',
+ warning: '#9a6700',
+ },
+ };
+
+ // Dark theme for comparison
+ const darkTheme: Theme = {
+ id: 'dracula',
+ name: 'Dracula',
+ mode: 'dark',
+ colors: {
+ bgMain: '#282a36',
+ bgSidebar: '#21222c',
+ bgActivity: '#343746',
+ textMain: '#f8f8f2',
+ textDim: '#6272a4',
+ accent: '#bd93f9',
+ border: '#44475a',
+ error: '#ff5555',
+ success: '#50fa7b',
+ warning: '#ffb86c',
+ },
+ };
+
+ const createFileTab = (extension: string): FilePreviewTab => ({
+ id: `file-tab-${extension}`,
+ path: `/test/file${extension}`,
+ name: 'file',
+ extension: extension,
+ content: 'test content',
+ scrollTop: 0,
+ searchQuery: '',
+ editMode: false,
+ editContent: undefined,
+ createdAt: Date.now(),
+ lastModified: Date.now(),
+ });
+
+ it('renders extension badges for TypeScript files with appropriate styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.ts');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ // Extension badge should be rendered
+ const badge = screen.getByText('.ts');
+ expect(badge).toBeInTheDocument();
+ // Badge should have blue-ish background for TypeScript
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(59, 130, 246, 0.3)' });
+ });
+
+ it('renders extension badges for TypeScript files with light theme appropriate styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.tsx');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ // Extension badge should be rendered with light theme colors
+ const badge = screen.getByText('.tsx');
+ expect(badge).toBeInTheDocument();
+ // Badge should have darker blue for better contrast on light backgrounds
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(37, 99, 235, 0.15)' });
+ });
+
+ it('renders extension badges for Markdown files with dark theme styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.md');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.md');
+ expect(badge).toBeInTheDocument();
+ // Green tones for Markdown/Docs
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(34, 197, 94, 0.3)' });
+ });
+
+ it('renders extension badges for JSON files with dark theme styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.json');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.json');
+ expect(badge).toBeInTheDocument();
+ // Yellow tones for JSON/Config
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(234, 179, 8, 0.3)' });
+ });
+
+ it('renders extension badges for CSS files with dark theme styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.css');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.css');
+ expect(badge).toBeInTheDocument();
+ // Purple tones for CSS/Styles
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(168, 85, 247, 0.3)' });
+ });
+
+ it('renders extension badges for HTML files with dark theme styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.html');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.html');
+ expect(badge).toBeInTheDocument();
+ // Orange tones for HTML/Templates
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(249, 115, 22, 0.3)' });
+ });
+
+ it('renders extension badges for Python files with dark theme styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.py');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.py');
+ expect(badge).toBeInTheDocument();
+ // Teal/cyan tones for Python
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(20, 184, 166, 0.3)' });
+ });
+
+ it('renders extension badges for Rust files with dark theme styling', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.rs');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.rs');
+ expect(badge).toBeInTheDocument();
+ // Rust/red-orange tones for Rust
+ expect(badge).toHaveStyle({ backgroundColor: 'rgba(239, 68, 68, 0.3)' });
+ });
+
+ it('renders extension badges for unknown files using theme border color', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab = createFileTab('.xyz');
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ const badge = screen.getByText('.xyz');
+ expect(badge).toBeInTheDocument();
+ // Uses theme border color for unknown extensions
+ expect(badge).toHaveStyle({ backgroundColor: darkTheme.colors.border });
+ });
+
+ it('renders consistent tab name truncation for file tabs (max-w-[120px])', () => {
+ const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
+ const fileTab: FilePreviewTab = {
+ id: 'file-tab-1',
+ path: '/test/very-long-filename-that-should-be-truncated.ts',
+ name: 'very-long-filename-that-should-be-truncated',
+ extension: '.ts',
+ content: 'test',
+ scrollTop: 0,
+ searchQuery: '',
+ editMode: false,
+ editContent: undefined,
+ createdAt: Date.now(),
+ lastModified: Date.now(),
+ };
+
+ const unifiedTabs = [
+ { type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
+ { type: 'file' as const, id: fileTab.id, data: fileTab },
+ ];
+
+ render(
+
+ );
+
+ // File tab name span should have truncation class
+ const fileNameSpan = screen.getByText('very-long-filename-that-should-be-truncated');
+ expect(fileNameSpan).toHaveClass('truncate');
+ expect(fileNameSpan).toHaveClass('max-w-[120px]');
+ });
+});
diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx
index 74c4a2fb..479bb3cb 100644
--- a/src/renderer/components/TabBar.tsx
+++ b/src/renderer/components/TabBar.tsx
@@ -476,6 +476,9 @@ const Tab = memo(function Tab({
// Memoize display name to avoid recalculation on every render
const displayName = useMemo(() => getTabDisplayName(tab), [tab.name, tab.agentSessionId]);
+ // Hover background varies by theme mode for proper contrast
+ const hoverBgColor = theme.mode === 'light' ? 'rgba(0, 0, 0, 0.06)' : 'rgba(255, 255, 255, 0.08)';
+
// Memoize tab styles to avoid creating new object references on every render
const tabStyle = useMemo(
() =>
@@ -485,11 +488,7 @@ const Tab = memo(function Tab({
borderTopRightRadius: '6px',
// Active tab: bright background matching content area
// Inactive tabs: transparent with subtle hover
- backgroundColor: isActive
- ? theme.colors.bgMain
- : isHovered
- ? 'rgba(255, 255, 255, 0.08)'
- : 'transparent',
+ backgroundColor: isActive ? theme.colors.bgMain : isHovered ? hoverBgColor : 'transparent',
// Active tab has visible borders, inactive tabs have no borders (cleaner look)
borderTop: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
borderLeft: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
@@ -502,7 +501,7 @@ const Tab = memo(function Tab({
zIndex: isActive ? 1 : 0,
'--tw-ring-color': isDragOver ? theme.colors.accent : 'transparent',
}) as React.CSSProperties,
- [isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent]
+ [isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent, hoverBgColor]
);
// Browser-style tab: all tabs have borders, active tab "connects" to content
@@ -952,28 +951,65 @@ interface FileTabProps {
/**
* Get color for file extension badge.
* Returns a muted color based on file type for visual differentiation.
+ * Colors are adapted for both light and dark themes for good contrast.
*/
function getExtensionColor(extension: string, theme: Theme): { bg: string; text: string } {
const ext = extension.toLowerCase();
+ const isLightTheme = theme.mode === 'light';
+
// TypeScript/JavaScript - blue tones
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
- return { bg: 'rgba(59, 130, 246, 0.3)', text: 'rgba(147, 197, 253, 0.9)' };
+ return isLightTheme
+ ? { bg: 'rgba(37, 99, 235, 0.15)', text: 'rgba(29, 78, 216, 0.9)' }
+ : { bg: 'rgba(59, 130, 246, 0.3)', text: 'rgba(147, 197, 253, 0.9)' };
}
// Markdown/Docs - green tones
if (['.md', '.mdx', '.txt', '.rst'].includes(ext)) {
- return { bg: 'rgba(34, 197, 94, 0.3)', text: 'rgba(134, 239, 172, 0.9)' };
+ return isLightTheme
+ ? { bg: 'rgba(22, 163, 74, 0.15)', text: 'rgba(21, 128, 61, 0.9)' }
+ : { bg: 'rgba(34, 197, 94, 0.3)', text: 'rgba(134, 239, 172, 0.9)' };
}
- // JSON/Config - yellow tones
+ // JSON/Config - yellow/amber tones
if (['.json', '.yaml', '.yml', '.toml', '.ini', '.env'].includes(ext)) {
- return { bg: 'rgba(234, 179, 8, 0.3)', text: 'rgba(253, 224, 71, 0.9)' };
+ return isLightTheme
+ ? { bg: 'rgba(217, 119, 6, 0.15)', text: 'rgba(180, 83, 9, 0.9)' }
+ : { bg: 'rgba(234, 179, 8, 0.3)', text: 'rgba(253, 224, 71, 0.9)' };
}
// CSS/Styles - purple tones
if (['.css', '.scss', '.sass', '.less', '.styl'].includes(ext)) {
- return { bg: 'rgba(168, 85, 247, 0.3)', text: 'rgba(216, 180, 254, 0.9)' };
+ return isLightTheme
+ ? { bg: 'rgba(147, 51, 234, 0.15)', text: 'rgba(126, 34, 206, 0.9)' }
+ : { bg: 'rgba(168, 85, 247, 0.3)', text: 'rgba(216, 180, 254, 0.9)' };
}
// HTML/Templates - orange tones
if (['.html', '.htm', '.xml', '.svg'].includes(ext)) {
- return { bg: 'rgba(249, 115, 22, 0.3)', text: 'rgba(253, 186, 116, 0.9)' };
+ return isLightTheme
+ ? { bg: 'rgba(234, 88, 12, 0.15)', text: 'rgba(194, 65, 12, 0.9)' }
+ : { bg: 'rgba(249, 115, 22, 0.3)', text: 'rgba(253, 186, 116, 0.9)' };
+ }
+ // Python - teal/cyan tones
+ if (['.py', '.pyw', '.pyi'].includes(ext)) {
+ return isLightTheme
+ ? { bg: 'rgba(13, 148, 136, 0.15)', text: 'rgba(15, 118, 110, 0.9)' }
+ : { bg: 'rgba(20, 184, 166, 0.3)', text: 'rgba(94, 234, 212, 0.9)' };
+ }
+ // Rust - rust/orange-red tones
+ if (['.rs'].includes(ext)) {
+ return isLightTheme
+ ? { bg: 'rgba(185, 28, 28, 0.15)', text: 'rgba(153, 27, 27, 0.9)' }
+ : { bg: 'rgba(239, 68, 68, 0.3)', text: 'rgba(252, 165, 165, 0.9)' };
+ }
+ // Go - cyan tones
+ if (['.go'].includes(ext)) {
+ return isLightTheme
+ ? { bg: 'rgba(8, 145, 178, 0.15)', text: 'rgba(14, 116, 144, 0.9)' }
+ : { bg: 'rgba(6, 182, 212, 0.3)', text: 'rgba(103, 232, 249, 0.9)' };
+ }
+ // Shell scripts - gray/slate tones
+ if (['.sh', '.bash', '.zsh', '.fish'].includes(ext)) {
+ return isLightTheme
+ ? { bg: 'rgba(71, 85, 105, 0.15)', text: 'rgba(51, 65, 85, 0.9)' }
+ : { bg: 'rgba(100, 116, 139, 0.3)', text: 'rgba(203, 213, 225, 0.9)' };
}
// Default - use theme's dim colors
return { bg: theme.colors.border, text: theme.colors.textDim };
@@ -1211,6 +1247,9 @@ const FileTab = memo(function FileTab({
[tab.extension, theme]
);
+ // Hover background varies by theme mode for proper contrast
+ const hoverBgColor = theme.mode === 'light' ? 'rgba(0, 0, 0, 0.06)' : 'rgba(255, 255, 255, 0.08)';
+
// Memoize tab styles to avoid creating new object references on every render
const tabStyle = useMemo(
() =>
@@ -1220,11 +1259,7 @@ const FileTab = memo(function FileTab({
borderTopRightRadius: '6px',
// Active tab: bright background matching content area
// Inactive tabs: transparent with subtle hover
- backgroundColor: isActive
- ? theme.colors.bgMain
- : isHovered
- ? 'rgba(255, 255, 255, 0.08)'
- : 'transparent',
+ backgroundColor: isActive ? theme.colors.bgMain : isHovered ? hoverBgColor : 'transparent',
// Active tab has visible borders, inactive tabs have no borders (cleaner look)
borderTop: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
borderLeft: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
@@ -1237,7 +1272,7 @@ const FileTab = memo(function FileTab({
zIndex: isActive ? 1 : 0,
'--tw-ring-color': isDragOver ? theme.colors.accent : 'transparent',
}) as React.CSSProperties,
- [isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent]
+ [isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent, hoverBgColor]
);
// Check if tab has unsaved edits
@@ -1273,7 +1308,7 @@ const FileTab = memo(function FileTab({
{/* Tab name - filename without extension */}
{tab.name}