diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 54684a8c2c..ea8482a161 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -5,15 +5,34 @@
*/
import { useIsScreenReaderEnabled } from 'ink';
+import { useTerminalSize } from './hooks/useTerminalSize.js';
+import { lerp } from '../utils/math.js';
import { useUIState } from './contexts/UIStateContext.js';
import { StreamingContext } from './contexts/StreamingContext.js';
import { QuittingDisplay } from './components/QuittingDisplay.js';
import { ScreenReaderAppLayout } from './layouts/ScreenReaderAppLayout.js';
import { DefaultAppLayout } from './layouts/DefaultAppLayout.js';
+const getContainerWidth = (terminalWidth: number): string => {
+ if (terminalWidth <= 80) {
+ return '98%';
+ }
+ if (terminalWidth >= 132) {
+ return '90%';
+ }
+
+ // Linearly interpolate between 80 columns (98%) and 132 columns (90%).
+ const t = (terminalWidth - 80) / (132 - 80);
+ const percentage = lerp(98, 90, t);
+
+ return `${Math.round(percentage)}%`;
+};
+
export const App = () => {
const uiState = useUIState();
const isScreenReaderEnabled = useIsScreenReaderEnabled();
+ const { columns } = useTerminalSize();
+ const containerWidth = getContainerWidth(columns);
if (uiState.quittingMessages) {
return ;
@@ -21,7 +40,11 @@ export const App = () => {
return (
- {isScreenReaderEnabled ? : }
+ {isScreenReaderEnabled ? (
+
+ ) : (
+
+ )}
);
};
diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.tsx
index d7d8c063f0..25dad9c7e3 100644
--- a/packages/cli/src/ui/components/ContextUsageDisplay.tsx
+++ b/packages/cli/src/ui/components/ContextUsageDisplay.tsx
@@ -11,15 +11,21 @@ import { tokenLimit } from '@google/gemini-cli-core';
export const ContextUsageDisplay = ({
promptTokenCount,
model,
+ terminalWidth,
}: {
promptTokenCount: number;
model: string;
+ terminalWidth: number;
}) => {
const percentage = promptTokenCount / tokenLimit(model);
+ const percentageLeft = ((1 - percentage) * 100).toFixed(0);
+
+ const label = terminalWidth < 100 ? '%' : '% context left';
return (
- ({((1 - percentage) * 100).toFixed(0)}% context left)
+ ({percentageLeft}
+ {label})
);
};
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index 3c0a4d9b96..31f256d5c6 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -9,7 +9,6 @@ import { describe, it, expect, vi } from 'vitest';
import { Footer } from './Footer.js';
import * as useTerminalSize from '../hooks/useTerminalSize.js';
import { tildeifyPath } from '@google/gemini-cli-core';
-import path from 'node:path';
import { type UIState, UIStateContext } from '../contexts/UIStateContext.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
import { SettingsContext } from '../contexts/SettingsContext.js';
@@ -103,34 +102,22 @@ describe('', () => {
});
describe('path display', () => {
- it('should display shortened path on a wide terminal', () => {
- const { lastFrame } = renderWithWidth(120, createMockUIState());
- const tildePath = tildeifyPath(defaultProps.targetDir);
- const expectedPath = '...' + tildePath.slice(tildePath.length - 48 + 3);
- expect(lastFrame()).toContain(expectedPath);
- });
-
- it('should display only the base directory name on a narrow terminal', () => {
+ it('should display a shortened path on a narrow terminal', () => {
const { lastFrame } = renderWithWidth(79, createMockUIState());
- const expectedPath = path.basename(defaultProps.targetDir);
+ const tildePath = tildeifyPath(defaultProps.targetDir);
+ const pathLength = Math.max(20, Math.floor(79 * 0.25));
+ const expectedPath =
+ '...' + tildePath.slice(tildePath.length - pathLength + 3);
expect(lastFrame()).toContain(expectedPath);
});
it('should use wide layout at 80 columns', () => {
const { lastFrame } = renderWithWidth(80, createMockUIState());
const tildePath = tildeifyPath(defaultProps.targetDir);
- const expectedPath = '...' + tildePath.slice(tildePath.length - 32 + 3);
+ const expectedPath =
+ '...' + tildePath.slice(tildePath.length - 80 * 0.25 + 3);
expect(lastFrame()).toContain(expectedPath);
});
-
- it('should use narrow layout at 79 columns', () => {
- const { lastFrame } = renderWithWidth(79, createMockUIState());
- const expectedPath = path.basename(defaultProps.targetDir);
- expect(lastFrame()).toContain(expectedPath);
- const tildePath = tildeifyPath(defaultProps.targetDir);
- const unexpectedPath = '...' + tildePath.slice(tildePath.length - 31 + 3);
- expect(lastFrame()).not.toContain(unexpectedPath);
- });
});
it('displays the branch name when provided', () => {
@@ -151,7 +138,13 @@ describe('', () => {
it('displays the model name and context percentage', () => {
const { lastFrame } = renderWithWidth(120, createMockUIState());
expect(lastFrame()).toContain(defaultProps.model);
- expect(lastFrame()).toMatch(/\(\d+% context[\s\S]*left\)/);
+ expect(lastFrame()).toMatch(/\(\d+% context left\)/);
+ });
+
+ it('displays the model name and abbreviated context percentage', () => {
+ const { lastFrame } = renderWithWidth(99, createMockUIState());
+ expect(lastFrame()).toContain(defaultProps.model);
+ expect(lastFrame()).toMatch(/\(\d+%\)/);
});
describe('sandbox and trust info', () => {
diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx
index f35eb6abad..7bbe5ef52d 100644
--- a/packages/cli/src/ui/components/Footer.tsx
+++ b/packages/cli/src/ui/components/Footer.tsx
@@ -10,14 +10,11 @@ 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 path from 'node:path';
import Gradient from 'ink-gradient';
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { DebugProfiler } from './DebugProfiler.js';
-
import { useTerminalSize } from '../hooks/useTerminalSize.js';
-import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
@@ -65,13 +62,8 @@ export const Footer: React.FC = () => {
const { columns: terminalWidth } = useTerminalSize();
- const isNarrow = isNarrowWidth(terminalWidth);
-
- // Adjust path length based on terminal width
- const pathLength = Math.max(20, Math.floor(terminalWidth * 0.4));
- const displayPath = isNarrow
- ? path.basename(tildeifyPath(targetDir))
- : shortenPath(tildeifyPath(targetDir), pathLength);
+ const pathLength = Math.max(20, Math.floor(terminalWidth * 0.25));
+ const displayPath = shortenPath(tildeifyPath(targetDir), pathLength);
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
const displayVimMode = vimEnabled ? vimMode : undefined;
@@ -80,8 +72,8 @@ export const Footer: React.FC = () => {
{(debugMode || displayVimMode || !hideCWD) && (
@@ -116,12 +108,10 @@ export const Footer: React.FC = () => {
{/* Middle Section: Centered Trust/Sandbox Info */}
{!hideSandboxStatus && (
{isTrustedFolder === false ? (
untrusted
@@ -139,35 +129,33 @@ export const Footer: React.FC = () => {
) : (
- no sandbox (see /docs)
+ no sandbox
+ {terminalWidth >= 100 && (
+ (see /docs)
+ )}
)}
)}
{/* Right Section: Gemini Label and Console Summary */}
- {(!hideModelInfo ||
- showMemoryUsage ||
- corgiMode ||
- (!showErrorDetails && errorCount > 0)) && (
-
- {!hideModelInfo && (
-
-
- {isNarrow ? '' : ' '}
- {model}{' '}
-
-
- {showMemoryUsage && }
-
- )}
+ {!hideModelInfo && (
+
+
+
+ {model}{' '}
+
+
+ {showMemoryUsage && }
+
{corgiMode && (
- {!hideModelInfo && | }
+ |
▼
(´
ᴥ
@@ -177,7 +165,7 @@ export const Footer: React.FC = () => {
)}
{!showErrorDetails && errorCount > 0 && (
- {!hideModelInfo && | }
+ |
)}
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 8c6553c33d..23b14903d5 100644
--- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
@@ -1,20 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
-"long (main*)
+exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `"...s/to/make/it/long (main*) no sandbox gemini-pro (100%)"`;
-no sandbox (see /docs)
-
-gemini-pro (100% context left)"
-`;
-
-exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
-"...bar/and/some/more/directories/to/make/it/long no sandbox (see gemini-pro (100% context
-(main*) /docs) left)"
-`;
+exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `"...directories/to/make/it/long (main*) no sandbox (see /docs) gemini-pro (100% context left)"`;
exports[` > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `" no sandbox (see /docs)"`;
exports[` > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
-exports[` > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `"...bar/and/some/more/directories/to/make/it/long (main*) no sandbox (see /docs)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `"...directories/to/make/it/long (main*) no sandbox (see /docs)"`;
diff --git a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx
index 1fd980e387..37a617250d 100644
--- a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx
+++ b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx
@@ -13,11 +13,13 @@ import { Composer } from '../components/Composer.js';
import { ExitWarning } from '../components/ExitWarning.js';
import { useUIState } from '../contexts/UIStateContext.js';
-export const DefaultAppLayout: React.FC = () => {
+export const DefaultAppLayout: React.FC<{ width?: string }> = ({
+ width = '90%',
+}) => {
const uiState = useUIState();
return (
-
+
diff --git a/packages/cli/src/utils/math.ts b/packages/cli/src/utils/math.ts
new file mode 100644
index 0000000000..e1297590f6
--- /dev/null
+++ b/packages/cli/src/utils/math.ts
@@ -0,0 +1,14 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Linearly interpolates between two values.
+ *
+ * @param start The start value.
+ * @param end The end value.
+ * @param t The interpolation amount (typically between 0 and 1).
+ */
+export const lerp = (start: number, end: number, t: number): number => start + (end - start) * t;