diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md
index 1777d22f5f..da0b4c9171 100644
--- a/docs/get-started/configuration.md
+++ b/docs/get-started/configuration.md
@@ -243,6 +243,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
+- **`ui.compact`** (boolean):
+ - **Description:** Enable a more compact UI layout.
+ - **Default:** `false`
+
- **`ui.incrementalRendering`** (boolean):
- **Description:** Enable incremental rendering for the UI. This option will
reduce flickering but may cause rendering artifacts. Only supported when
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 4dea5d0a99..3ae9806340 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -524,6 +524,15 @@ const SETTINGS_SCHEMA = {
'Use an alternate screen buffer for the UI, preserving shell history.',
showInDialog: true,
},
+ compact: {
+ type: 'boolean',
+ label: 'Compact UI',
+ category: 'UI',
+ requiresRestart: false,
+ default: false,
+ description: 'Enable a more compact UI layout.',
+ showInDialog: true,
+ },
incrementalRendering: {
type: 'boolean',
label: 'Incremental Rendering',
diff --git a/packages/cli/src/ui/components/AsciiArt.ts b/packages/cli/src/ui/components/AsciiArt.ts
index e1cb5e058b..b004fad479 100644
--- a/packages/cli/src/ui/components/AsciiArt.ts
+++ b/packages/cli/src/ui/components/AsciiArt.ts
@@ -37,6 +37,27 @@ export const tinyAsciiLogo = `
░░░ ░░░░░░░░░
`;
+export const shortAsciiLogoCompact = `
+ ▟▛▀▀█▖▜█▀▀▜▝██▙▗██▛▝█▛▝██▙ ▜█▘▜█▘
+▐█ ▐█▄▌ █▌▜█▘█▌ █▌ █▌▜▙▐█ ▐█
+▝█▖ ▜█▘▐█ ▘▗ █▌ █▌ █▌ █▌ ▜██ ▐█
+ ▝▀▀▀▀ ▀▀▀▀▀▝▀▀ ▝▀▀▝▀▀▝▀▀ ▀▀▘▀▀▘
+`;
+
+export const longAsciiLogoCompact = `
+▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
+ ▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
+ ▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
+▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
+`;
+
+export const tinyAsciiLogoCompact = `
+▝▜▄ ▟▛▀▀█▖
+ ▝▜▄▐█
+ ▗▟▀ ▝█▖ ▜█▘
+▝▀ ▝▀▀▀▀
+`;
+
export const shortAsciiLogoIde = `
░░░░░░░░░ ░░░░░░░░░░ ░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░
░░░ ░░░ ░░░ ░░░░░░ ░░░░░░ ░░░ ░░░░░░ ░░░░░ ░░░
diff --git a/packages/cli/src/ui/components/Header.test.tsx b/packages/cli/src/ui/components/Header.test.tsx
index 5200db17d4..0c4c76c51b 100644
--- a/packages/cli/src/ui/components/Header.test.tsx
+++ b/packages/cli/src/ui/components/Header.test.tsx
@@ -4,11 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render } from '../../test-utils/render.js';
+import {
+ renderWithProviders,
+ createMockSettings,
+} from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { Header } from './Header.js';
import * as useTerminalSize from '../hooks/useTerminalSize.js';
-import { longAsciiLogo, longAsciiLogoIde } from './AsciiArt.js';
+import {
+ longAsciiLogo,
+ longAsciiLogoIde,
+ longAsciiLogoCompact,
+} from './AsciiArt.js';
import * as semanticColors from '../semantic-colors.js';
import * as terminalSetup from '../utils/terminalSetup.js';
import { Text } from 'ink';
@@ -49,7 +56,7 @@ describe('', () => {
columns: 120,
rows: 20,
});
- render();
+ renderWithProviders();
expect(Text).toHaveBeenCalledWith(
expect.objectContaining({
children: longAsciiLogo,
@@ -65,7 +72,7 @@ describe('', () => {
});
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue('vscode');
- render();
+ renderWithProviders();
expect(Text).toHaveBeenCalledWith(
expect.objectContaining({
children: longAsciiLogoIde,
@@ -76,7 +83,7 @@ describe('', () => {
it('renders custom ASCII art when provided', () => {
const customArt = 'CUSTOM ART';
- render(
+ renderWithProviders(
,
);
expect(Text).toHaveBeenCalledWith(
@@ -90,7 +97,7 @@ describe('', () => {
it('renders custom ASCII art as is when running in an IDE', () => {
const customArt = 'CUSTOM ART';
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue('vscode');
- render(
+ renderWithProviders(
,
);
expect(Text).toHaveBeenCalledWith(
@@ -102,13 +109,13 @@ describe('', () => {
});
it('displays the version number when nightly is true', () => {
- render();
+ renderWithProviders();
const textCalls = (Text as Mock).mock.calls;
expect(textCalls[1][0].children.join('')).toBe('v1.0.0');
});
it('does not display the version number when nightly is false', () => {
- render();
+ renderWithProviders();
expect(Text).not.toHaveBeenCalledWith(
expect.objectContaining({
children: 'v1.0.0',
@@ -117,6 +124,45 @@ describe('', () => {
);
});
+ it('renders the compact logo when ui.compact is true', () => {
+ vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
+ columns: 120,
+ rows: 20,
+ });
+ renderWithProviders(, {
+ settings: createMockSettings({
+ ui: {
+ compact: true,
+ },
+ }),
+ });
+ expect(Text).toHaveBeenCalledWith(
+ expect.objectContaining({
+ children: longAsciiLogoCompact,
+ }),
+ undefined,
+ );
+ });
+
+ it('renders the version to the right in compact mode when nightly is true', () => {
+ vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
+ columns: 120,
+ rows: 20,
+ });
+ const { lastFrame } = renderWithProviders(
+ ,
+ {
+ settings: createMockSettings({
+ ui: {
+ compact: true,
+ },
+ }),
+ },
+ );
+ expect(lastFrame()).toContain('v1.0.0');
+ // In compact mode, logo and version are in the same Box with flexDirection="row"
+ });
+
it('renders with no gradient when theme.ui.gradient is undefined', async () => {
vi.spyOn(semanticColors, 'theme', 'get').mockReturnValue({
text: {
@@ -147,10 +193,13 @@ describe('', () => {
},
});
const Gradient = await import('ink-gradient');
- render();
+ renderWithProviders();
expect(Gradient.default).not.toHaveBeenCalled();
const textCalls = (Text as Mock).mock.calls;
- expect(textCalls[0][0]).toHaveProperty('color', '#123456');
+ expect(textCalls[textCalls.length - 1][0]).toHaveProperty(
+ 'color',
+ '#123456',
+ );
});
it('renders with a single color when theme.ui.gradient has one color', async () => {
@@ -159,11 +208,14 @@ describe('', () => {
ui: { gradient: [singleColor] },
} as typeof semanticColors.theme);
const Gradient = await import('ink-gradient');
- render();
+ renderWithProviders();
expect(Gradient.default).not.toHaveBeenCalled();
const textCalls = (Text as Mock).mock.calls;
- expect(textCalls.length).toBe(1);
- expect(textCalls[0][0]).toHaveProperty('color', singleColor);
+ expect(textCalls.length).toBeGreaterThanOrEqual(1);
+ expect(textCalls[textCalls.length - 1][0]).toHaveProperty(
+ 'color',
+ singleColor,
+ );
});
it('renders with a gradient when theme.ui.gradient has two or more colors', async () => {
@@ -172,7 +224,7 @@ describe('', () => {
ui: { gradient: gradientColors },
} as typeof semanticColors.theme);
const Gradient = await import('ink-gradient');
- render();
+ renderWithProviders();
expect(Gradient.default).toHaveBeenCalledWith(
expect.objectContaining({
colors: gradientColors,
diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx
index 52fd0175c5..f8dfce335e 100644
--- a/packages/cli/src/ui/components/Header.tsx
+++ b/packages/cli/src/ui/components/Header.tsx
@@ -14,11 +14,15 @@ import {
shortAsciiLogoIde,
longAsciiLogoIde,
tinyAsciiLogoIde,
+ shortAsciiLogoCompact,
+ longAsciiLogoCompact,
+ tinyAsciiLogoCompact,
} from './AsciiArt.js';
import { getAsciiArtWidth } from '../utils/textUtils.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { getTerminalProgram } from '../utils/terminalSetup.js';
import { useSnowfall } from '../hooks/useSnowfall.js';
+import { useSettings } from '../contexts/SettingsContext.js';
interface HeaderProps {
customAsciiArt?: string; // For user-defined ASCII art
@@ -31,25 +35,62 @@ export const Header: React.FC = ({
version,
nightly,
}) => {
+ const { merged: settings } = useSettings();
+ const compact = settings.ui?.compact;
const { columns: terminalWidth } = useTerminalSize();
const isIde = getTerminalProgram();
let displayTitle;
- const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo);
- const widthOfShortLogo = getAsciiArtWidth(shortAsciiLogo);
+
+ const longLogo = compact
+ ? longAsciiLogoCompact
+ : isIde
+ ? longAsciiLogoIde
+ : longAsciiLogo;
+ const shortLogo = compact
+ ? shortAsciiLogoCompact
+ : isIde
+ ? shortAsciiLogoIde
+ : shortAsciiLogo;
+ const tinyLogo = compact
+ ? tinyAsciiLogoCompact
+ : isIde
+ ? tinyAsciiLogoIde
+ : tinyAsciiLogo;
+
+ const widthOfLongLogo = getAsciiArtWidth(longLogo);
+ const widthOfShortLogo = getAsciiArtWidth(shortLogo);
if (customAsciiArt) {
displayTitle = customAsciiArt;
} else if (terminalWidth >= widthOfLongLogo) {
- displayTitle = isIde ? longAsciiLogoIde : longAsciiLogo;
+ displayTitle = longLogo;
} else if (terminalWidth >= widthOfShortLogo) {
- displayTitle = isIde ? shortAsciiLogoIde : shortAsciiLogo;
+ displayTitle = shortLogo;
} else {
- displayTitle = isIde ? tinyAsciiLogoIde : tinyAsciiLogo;
+ displayTitle = tinyLogo;
}
const artWidth = getAsciiArtWidth(displayTitle);
const title = useSnowfall(displayTitle);
+ if (compact) {
+ return (
+
+ {title}
+ {nightly && (
+
+ v{version}
+
+ )}
+
+ );
+ }
+
return (