From 6ae6c810ba155e8eb049595e77e9a63e92d3d6eb Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Tue, 10 Mar 2026 01:07:26 -0700 Subject: [PATCH] fix(cli): make footer items equally spaced (#21843) --- packages/cli/src/ui/components/Footer.tsx | 30 ++-- .../ui/components/FooterConfigDialog.test.tsx | 57 +++++++ .../src/ui/components/FooterConfigDialog.tsx | 2 +- .../__snapshots__/Footer.test.tsx.snap | 22 +-- ...ts-the-active-item-in-the-preview.snap.svg | 31 ++-- ...s-correctly-with-default-settings.snap.svg | 19 ++- ...Show-footer-labels-is-toggled-off.snap.svg | 143 ++++++++++++++++++ .../FooterConfigDialog.test.tsx.snap | 54 ++++++- 8 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index e5a1f9e8b6..c6816339f5 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -106,6 +106,7 @@ export interface FooterRowItem { flexGrow?: number; flexShrink?: number; isFocused?: boolean; + alignItems?: 'flex-start' | 'center' | 'flex-end'; } const COLUMN_GAP = 3; @@ -117,10 +118,17 @@ export const FooterRow: React.FC<{ const elements: React.ReactNode[] = []; items.forEach((item, idx) => { - if (idx > 0 && !showLabels) { + if (idx > 0) { elements.push( - - · + + {!showLabels && · } , ); } @@ -131,6 +139,7 @@ export const FooterRow: React.FC<{ flexDirection="column" flexGrow={item.flexGrow ?? 0} flexShrink={item.flexShrink ?? 1} + alignItems={item.alignItems} backgroundColor={item.isFocused ? theme.background.focus : undefined} > {showLabels && ( @@ -148,12 +157,7 @@ export const FooterRow: React.FC<{ }); return ( - + {elements} ); @@ -441,8 +445,9 @@ export const Footer: React.FC = () => { } } - const rowItems: FooterRowItem[] = columnsToRender.map((col) => { + const rowItems: FooterRowItem[] = columnsToRender.map((col, index) => { const isWorkspace = col.id === 'workspace'; + const isLast = index === columnsToRender.length - 1; // Calculate exact space available for growth to prevent over-estimation truncation const otherItemsWidth = columnsToRender @@ -464,8 +469,10 @@ export const Footer: React.FC = () => { key: col.id, header: col.header, element: col.element(estimatedWidth), - flexGrow: isWorkspace ? 1 : 0, + flexGrow: 0, flexShrink: isWorkspace ? 1 : 0, + alignItems: + isLast && !droppedAny && index > 0 ? 'flex-end' : 'flex-start', }; }); @@ -476,6 +483,7 @@ export const Footer: React.FC = () => { element: , flexGrow: 0, flexShrink: 0, + alignItems: 'flex-end', }); } diff --git a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx index 3141c3a1d7..9d3688e17a 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx @@ -9,6 +9,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { FooterConfigDialog } from './FooterConfigDialog.js'; import { createMockSettings } from '../../test-utils/settings.js'; +import { ALL_ITEMS } from '../../config/footerItems.js'; import { act } from 'react'; describe('', () => { @@ -213,4 +214,60 @@ describe('', () => { expect(bIdxAfter).toBeLessThan(wIdxAfter); }); }); + + it('updates the preview when Show footer labels is toggled off', async () => { + const settings = createMockSettings(); + const renderResult = renderWithProviders( + , + { settings }, + ); + + const { lastFrame, stdin, waitUntilReady } = renderResult; + await waitUntilReady(); + + // By default labels are on + expect(lastFrame()).toContain('workspace (/directory)'); + expect(lastFrame()).toContain('sandbox'); + expect(lastFrame()).toContain('/model'); + + // Move to "Show footer labels" (which is the second to last item) + for (let i = 0; i < ALL_ITEMS.length; i++) { + act(() => { + stdin.write('\u001b[B'); // Down arrow + }); + } + + await waitFor(() => { + expect(lastFrame()).toMatch(/> \[✓\] Show footer labels/); + }); + + // Toggle it off + act(() => { + stdin.write('\r'); + }); + + await waitFor(() => { + expect(lastFrame()).toMatch(/> \[ \] Show footer labels/); + // The headers should no longer be in the preview + expect(lastFrame()).not.toContain('workspace (/directory)'); + expect(lastFrame()).not.toContain('/model'); + + // We can't strictly search for "sandbox" because the menu item also says "sandbox". + // Let's assert that the spacer dots are now present in the preview instead. + const previewLine = + lastFrame() + .split('\n') + .find((line) => line.includes('Preview:')) || ''; + const nextLine = + lastFrame().split('\n')[ + lastFrame().split('\n').indexOf(previewLine) + 1 + ] || ''; + expect(nextLine).toContain('·'); + expect(nextLine).toContain('~/project/path'); + expect(nextLine).toContain('docker'); + expect(nextLine).toContain('97%'); + }); + + await expect(renderResult).toMatchSvgSnapshot(); + }); }); diff --git a/packages/cli/src/ui/components/FooterConfigDialog.tsx b/packages/cli/src/ui/components/FooterConfigDialog.tsx index cda58574a3..562bbabd81 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.tsx @@ -266,7 +266,7 @@ export const FooterConfigDialog: React.FC = ({ key: id, header: ALL_ITEMS.find((i) => i.id === id)?.header ?? id, element: mockData[id], - flexGrow: 1, + flexGrow: 0, isFocused: id === focusKey, })); diff --git a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap index 2d98d66f03..3980ddbd0a 100644 --- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap @@ -1,26 +1,26 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`