From 17eecac886957a389afad5297e2a33342c8517d0 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:14:06 -0700 Subject: [PATCH] feat(ui): replace busy pulse animation with blue indicator Busy terminal icons now show a blue color instead of a pulsing animation, providing a cleaner, less distracting visual indicator. Fixes #160 --- src/components/TabItem.tsx | 8 ++++---- test/unit/client/components/TabBar.test.tsx | 21 ++++++++++---------- test/unit/client/components/TabItem.test.tsx | 17 +++++++++------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/components/TabItem.tsx b/src/components/TabItem.tsx index 40baf027..ae499119 100644 --- a/src/components/TabItem.tsx +++ b/src/components/TabItem.tsx @@ -10,7 +10,8 @@ import type { MouseEvent, KeyboardEvent } from 'react' import { ContextIds } from '@/components/context-menu/context-menu-constants' function StatusDot({ status, activityPulse }: { status: TerminalStatus; activityPulse?: boolean }) { - return + const isBusy = activityPulse && status === 'running' + return } const MAX_TAB_ICONS = 6 @@ -96,14 +97,13 @@ export default function TabItem({ content={content} className={cn( 'h-3 w-3 shrink-0', - getTerminalStatusIconClassName(status), - shouldPulse && 'animate-pulse', + shouldPulse ? 'text-blue-500' : getTerminalStatusIconClassName(status), )} /> ) })} {overflow > 0 && ( - +{overflow} + +{overflow} )} ) diff --git a/test/unit/client/components/TabBar.test.tsx b/test/unit/client/components/TabBar.test.tsx index 15d7b916..2c348436 100644 --- a/test/unit/client/components/TabBar.test.tsx +++ b/test/unit/client/components/TabBar.test.tsx @@ -279,7 +279,7 @@ describe('TabBar', () => { expect(scrollContainer).toBeNull() }) - it('pulses a tab when any exact terminal id in that tab is busy', () => { + it('shows blue color on a tab when any exact terminal id in that tab is busy', () => { const tab = createTab({ id: 'tab-codex', title: 'Codex Tab', @@ -307,11 +307,12 @@ describe('TabBar', () => { const busyIcon = icons.find((icon) => icon.getAttribute('data-terminal-id') === 'term-1') const idleIcon = icons.find((icon) => icon.getAttribute('data-terminal-id') === 'term-2') - expect(busyIcon?.getAttribute('class')).toContain('animate-pulse') - expect(idleIcon?.getAttribute('class') ?? '').not.toContain('animate-pulse') + expect(busyIcon?.getAttribute('class')).toContain('text-blue-500') + expect(busyIcon?.getAttribute('class')).not.toContain('animate-pulse') + expect(idleIcon?.getAttribute('class') ?? '').not.toContain('text-blue-500') }) - it('does not pulse a tab when the exact record is only pending', () => { + it('does not show blue on a tab when the exact record is only pending', () => { const tab = createTab({ id: 'tab-codex', title: 'Codex Pending', @@ -350,10 +351,10 @@ describe('TabBar', () => { renderWithStore(, store) const tabElement = screen.getByLabelText('Codex Pending') - const pulsingIcons = within(tabElement).getAllByTestId('pane-icon') - .filter((icon) => icon.getAttribute('class')?.includes('animate-pulse')) + const blueIcons = within(tabElement).getAllByTestId('pane-icon') + .filter((icon) => icon.getAttribute('class')?.includes('text-blue-500')) - expect(pulsingIcons).toHaveLength(0) + expect(blueIcons).toHaveLength(0) }) it('falls back to the exact tab terminal id for a single-pane rehydrate gap', () => { @@ -395,10 +396,10 @@ describe('TabBar', () => { renderWithStore(, store) const tabElement = screen.getByLabelText('Rehydrate Gap') - const pulsingIcons = within(tabElement).getAllByTestId('pane-icon') - .filter((icon) => icon.getAttribute('class')?.includes('animate-pulse')) + const blueIcons = within(tabElement).getAllByTestId('pane-icon') + .filter((icon) => icon.getAttribute('class')?.includes('text-blue-500')) - expect(pulsingIcons.length).toBeGreaterThan(0) + expect(blueIcons.length).toBeGreaterThan(0) }) }) diff --git a/test/unit/client/components/TabItem.test.tsx b/test/unit/client/components/TabItem.test.tsx index 9e1175d7..a3cfbeb7 100644 --- a/test/unit/client/components/TabItem.test.tsx +++ b/test/unit/client/components/TabItem.test.tsx @@ -155,7 +155,7 @@ describe('TabItem', () => { expect(screen.getByDisplayValue('Editing')).toBeInTheDocument() }) - it('pulses only the exact busy terminal icon in split tabs', () => { + it('shows blue color on the exact busy terminal icon in split tabs', () => { const paneContents: PaneContent[] = [ { kind: 'terminal', @@ -188,11 +188,12 @@ describe('TabItem', () => { const busyIcon = icons.find((icon) => icon.getAttribute('data-terminal-id') === 'term-1') const idleIcon = icons.find((icon) => icon.getAttribute('data-terminal-id') === 'term-2') - expect(busyIcon?.getAttribute('class')).toContain('animate-pulse') - expect(idleIcon?.getAttribute('class') ?? '').not.toContain('animate-pulse') + expect(busyIcon?.getAttribute('class')).toContain('text-blue-500') + expect(busyIcon?.getAttribute('class')).not.toContain('animate-pulse') + expect(idleIcon?.getAttribute('class') ?? '').not.toContain('text-blue-500') }) - it('pulses a single unnamed terminal icon during the exact tab-terminal fallback', () => { + it('shows blue color on a single unnamed terminal icon during the exact tab-terminal fallback', () => { const paneContents: PaneContent[] = [ { kind: 'terminal', @@ -213,10 +214,11 @@ describe('TabItem', () => { /> ) - expect(screen.getByTestId('pane-icon').getAttribute('class')).toContain('animate-pulse') + expect(screen.getByTestId('pane-icon').getAttribute('class')).toContain('text-blue-500') + expect(screen.getByTestId('pane-icon').getAttribute('class')).not.toContain('animate-pulse') }) - it('pulses the overflow indicator when the exact busy terminal is hidden beyond the visible icon cap', () => { + it('shows blue color on the overflow indicator when the exact busy terminal is hidden beyond the visible icon cap', () => { const paneContents: PaneContent[] = Array.from({ length: 7 }, (_, index) => ({ kind: 'terminal', mode: 'shell', @@ -235,7 +237,8 @@ describe('TabItem', () => { /> ) - expect(screen.getByText('+1').getAttribute('class')).toContain('animate-pulse') + expect(screen.getByText('+1').getAttribute('class')).toContain('text-blue-500') + expect(screen.getByText('+1').getAttribute('class')).not.toContain('animate-pulse') }) it('calls onClick when clicked', () => {