Skip to content

Commit d012929

Browse files
authored
Code review comments as a pr (#21209)
1 parent 97dfbd4 commit d012929

11 files changed

Lines changed: 847 additions & 271 deletions

packages/cli/src/ui/components/Footer.test.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ describe('<Footer />', () => {
132132
const output = lastFrame();
133133
expect(output).toBeDefined();
134134
// Should contain some part of the path, likely shortened
135-
expect(output).toContain(
136-
path.join('directories', 'to', 'make', 'it', 'long'),
137-
);
135+
expect(output).toContain(path.join('make', 'it'));
138136
unmount();
139137
});
140138

@@ -149,9 +147,38 @@ describe('<Footer />', () => {
149147
await waitUntilReady();
150148
const output = lastFrame();
151149
expect(output).toBeDefined();
152-
expect(output).toContain(
153-
path.join('directories', 'to', 'make', 'it', 'long'),
150+
expect(output).toContain(path.join('make', 'it'));
151+
unmount();
152+
});
153+
154+
it('should not truncate high-priority items on narrow terminals (regression)', async () => {
155+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
156+
<Footer />,
157+
{
158+
width: 60,
159+
uiState: {
160+
sessionStats: mockSessionStats,
161+
},
162+
settings: createMockSettings({
163+
general: {
164+
vimMode: true,
165+
},
166+
ui: {
167+
footer: {
168+
showLabels: true,
169+
items: ['workspace', 'model-name'],
170+
},
171+
},
172+
}),
173+
},
154174
);
175+
await waitUntilReady();
176+
const output = lastFrame();
177+
// [INSERT] is high priority and should be fully visible
178+
// (Note: VimModeProvider defaults to 'INSERT' mode when enabled)
179+
expect(output).toContain('[INSERT]');
180+
// Other items should be present but might be shortened
181+
expect(output).toContain('gemini-pro');
155182
unmount();
156183
});
157184
});

packages/cli/src/ui/components/Footer.tsx

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ export interface FooterRowItem {
103103
key: string;
104104
header: string;
105105
element: React.ReactNode;
106+
flexGrow?: number;
107+
flexShrink?: number;
108+
isFocused?: boolean;
106109
}
107110

108111
const COLUMN_GAP = 3;
@@ -123,10 +126,20 @@ export const FooterRow: React.FC<{
123126
}
124127

125128
elements.push(
126-
<Box key={item.key} flexDirection="column">
129+
<Box
130+
key={item.key}
131+
flexDirection="column"
132+
flexGrow={item.flexGrow ?? 0}
133+
flexShrink={item.flexShrink ?? 1}
134+
backgroundColor={item.isFocused ? theme.background.focus : undefined}
135+
>
127136
{showLabels && (
128137
<Box height={1}>
129-
<Text color={theme.ui.comment}>{item.header}</Text>
138+
<Text
139+
color={item.isFocused ? theme.text.primary : theme.ui.comment}
140+
>
141+
{item.header}
142+
</Text>
130143
</Box>
131144
)}
132145
<Box height={1}>{item.element}</Box>
@@ -138,6 +151,7 @@ export const FooterRow: React.FC<{
138151
<Box
139152
flexDirection="row"
140153
flexWrap="nowrap"
154+
width="100%"
141155
columnGap={showLabels ? COLUMN_GAP : 0}
142156
>
143157
{elements}
@@ -408,41 +422,50 @@ export const Footer: React.FC = () => {
408422
}
409423

410424
// --- Width Fitting Logic ---
411-
let currentWidth = 2; // Initial padding
412425
const columnsToRender: FooterColumn[] = [];
413426
let droppedAny = false;
427+
let currentUsedWidth = 2; // Initial padding
414428

415-
for (let i = 0; i < potentialColumns.length; i++) {
416-
const col = potentialColumns[i];
417-
const gap = columnsToRender.length > 0 ? (showLabels ? COLUMN_GAP : 3) : 0; // Use 3 for dot separator width
429+
for (const col of potentialColumns) {
430+
const gap = columnsToRender.length > 0 ? (showLabels ? COLUMN_GAP : 3) : 0;
418431
const budgetWidth = col.id === 'workspace' ? 20 : col.width;
419432

420433
if (
421434
col.isHighPriority ||
422-
currentWidth + gap + budgetWidth <= terminalWidth - 2
435+
currentUsedWidth + gap + budgetWidth <= terminalWidth - 2
423436
) {
424437
columnsToRender.push(col);
425-
currentWidth += gap + budgetWidth;
438+
currentUsedWidth += gap + budgetWidth;
426439
} else {
427440
droppedAny = true;
428441
}
429442
}
430443

431-
const totalBudgeted = columnsToRender.reduce(
432-
(sum, c, idx) =>
433-
sum +
434-
(c.id === 'workspace' ? 20 : c.width) +
435-
(idx > 0 ? (showLabels ? COLUMN_GAP : 3) : 0),
436-
2,
437-
);
438-
const excessSpace = Math.max(0, terminalWidth - totalBudgeted);
439-
440444
const rowItems: FooterRowItem[] = columnsToRender.map((col) => {
441-
const maxWidth = col.id === 'workspace' ? 20 + excessSpace : col.width;
445+
const isWorkspace = col.id === 'workspace';
446+
447+
// Calculate exact space available for growth to prevent over-estimation truncation
448+
const otherItemsWidth = columnsToRender
449+
.filter((c) => c.id !== 'workspace')
450+
.reduce((sum, c) => sum + c.width, 0);
451+
const numItems = columnsToRender.length + (droppedAny ? 1 : 0);
452+
const numGaps = numItems > 1 ? numItems - 1 : 0;
453+
const gapsWidth = numGaps * (showLabels ? COLUMN_GAP : 3);
454+
const ellipsisWidth = droppedAny ? 1 : 0;
455+
456+
const availableForWorkspace = Math.max(
457+
20,
458+
terminalWidth - 2 - gapsWidth - otherItemsWidth - ellipsisWidth,
459+
);
460+
461+
const estimatedWidth = isWorkspace ? availableForWorkspace : col.width;
462+
442463
return {
443464
key: col.id,
444465
header: col.header,
445-
element: col.element(maxWidth),
466+
element: col.element(estimatedWidth),
467+
flexGrow: isWorkspace ? 1 : 0,
468+
flexShrink: isWorkspace ? 1 : 0,
446469
};
447470
});
448471

@@ -451,6 +474,8 @@ export const Footer: React.FC = () => {
451474
key: 'ellipsis',
452475
header: '',
453476
element: <Text color={theme.ui.comment}></Text>,
477+
flexGrow: 0,
478+
flexShrink: 0,
454479
});
455480
}
456481

packages/cli/src/ui/components/FooterConfigDialog.test.tsx

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ describe('<FooterConfigDialog />', () => {
2424

2525
it('renders correctly with default settings', async () => {
2626
const settings = createMockSettings();
27-
const { lastFrame, waitUntilReady } = renderWithProviders(
27+
const renderResult = renderWithProviders(
2828
<FooterConfigDialog onClose={mockOnClose} />,
2929
{ settings },
3030
);
3131

32-
await waitUntilReady();
33-
expect(lastFrame()).toMatchSnapshot();
32+
await renderResult.waitUntilReady();
33+
expect(renderResult.lastFrame()).toMatchSnapshot();
34+
await expect(renderResult).toMatchSvgSnapshot();
3435
});
3536

3637
it('toggles an item when enter is pressed', async () => {
@@ -66,7 +67,7 @@ describe('<FooterConfigDialog />', () => {
6667
);
6768

6869
await waitUntilReady();
69-
// Initial order: workspace, branch, ...
70+
// Initial order: workspace, git-branch, ...
7071
const output = lastFrame();
7172
const cwdIdx = output.indexOf('] workspace');
7273
const branchIdx = output.indexOf('] git-branch');
@@ -108,22 +109,40 @@ describe('<FooterConfigDialog />', () => {
108109

109110
it('highlights the active item in the preview', async () => {
110111
const settings = createMockSettings();
111-
const { lastFrame, stdin, waitUntilReady } = renderWithProviders(
112+
const renderResult = renderWithProviders(
112113
<FooterConfigDialog onClose={mockOnClose} />,
113114
{ settings },
114115
);
115116

117+
const { lastFrame, stdin, waitUntilReady } = renderResult;
118+
116119
await waitUntilReady();
117120
expect(lastFrame()).toContain('~/project/path');
118121

119-
// Move focus down to 'git-branch'
122+
// Move focus down to 'code-changes' (which has colored elements)
123+
for (let i = 0; i < 8; i++) {
124+
act(() => {
125+
stdin.write('\u001b[B'); // Down arrow
126+
});
127+
}
128+
129+
await waitFor(() => {
130+
// The selected indicator should be next to 'code-changes'
131+
expect(lastFrame()).toMatch(/> \[ \] code-changes/);
132+
});
133+
134+
// Toggle it on
120135
act(() => {
121-
stdin.write('\u001b[B'); // Down arrow
136+
stdin.write('\r');
122137
});
123138

124139
await waitFor(() => {
125-
expect(lastFrame()).toContain('main');
140+
// It should now be checked and appear in the preview
141+
expect(lastFrame()).toMatch(/> \[\] code-changes/);
142+
expect(lastFrame()).toContain('+12 -4');
126143
});
144+
145+
await expect(renderResult).toMatchSvgSnapshot();
127146
});
128147

129148
it('shows an empty preview when all items are deselected', async () => {
@@ -134,20 +153,64 @@ describe('<FooterConfigDialog />', () => {
134153
);
135154

136155
await waitUntilReady();
137-
for (let i = 0; i < 10; i++) {
156+
157+
// Default items are the first 5. We toggle them off.
158+
for (let i = 0; i < 5; i++) {
159+
act(() => {
160+
stdin.write('\r'); // Toggle off
161+
});
138162
act(() => {
139-
stdin.write('\r'); // Toggle (deselect)
140163
stdin.write('\u001b[B'); // Down arrow
141164
});
142165
}
143166

167+
await waitFor(
168+
() => {
169+
const output = lastFrame();
170+
expect(output).toContain('Preview:');
171+
expect(output).not.toContain('~/project/path');
172+
expect(output).not.toContain('docker');
173+
},
174+
{ timeout: 2000 },
175+
);
176+
});
177+
178+
it('moves item correctly after trying to move up at the top', async () => {
179+
const settings = createMockSettings();
180+
const { lastFrame, stdin, waitUntilReady } = renderWithProviders(
181+
<FooterConfigDialog onClose={mockOnClose} />,
182+
{ settings },
183+
);
184+
await waitUntilReady();
185+
186+
// Default initial items in mock settings are 'git-branch', 'workspace', ...
144187
await waitFor(() => {
145188
const output = lastFrame();
146-
expect(output).toContain('Preview:');
147-
expect(output).not.toContain('~/project/path');
148-
expect(output).not.toContain('docker');
149-
expect(output).not.toContain('gemini-2.5-pro');
150-
expect(output).not.toContain('1.2k left');
189+
expect(output).toContain('] git-branch');
190+
expect(output).toContain('] workspace');
191+
});
192+
193+
const output = lastFrame();
194+
const branchIdx = output.indexOf('] git-branch');
195+
const workspaceIdx = output.indexOf('] workspace');
196+
expect(workspaceIdx).toBeLessThan(branchIdx);
197+
198+
// Try to move workspace up (left arrow) while it's at the top
199+
act(() => {
200+
stdin.write('\u001b[D'); // Left arrow
201+
});
202+
203+
// Move workspace down (right arrow)
204+
act(() => {
205+
stdin.write('\u001b[C'); // Right arrow
206+
});
207+
208+
await waitFor(() => {
209+
const outputAfter = lastFrame();
210+
const bIdxAfter = outputAfter.indexOf('] git-branch');
211+
const wIdxAfter = outputAfter.indexOf('] workspace');
212+
// workspace should now be after git-branch
213+
expect(bIdxAfter).toBeLessThan(wIdxAfter);
151214
});
152215
});
153216
});

0 commit comments

Comments
 (0)