diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
index a3bd8d47..3c4270d7 100644
--- a/packages/cli/src/config/settings.ts
+++ b/packages/cli/src/config/settings.ts
@@ -141,6 +141,7 @@ export interface Settings {
loadMemoryFromIncludeDirectories?: boolean;
chatCompression?: ChatCompressionSettings;
+ showLineNumbers?: boolean;
}
export interface SettingsError {
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 771fcacb..cba18047 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -41,6 +41,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from './utils/events.js';
+import { SettingsContext } from './ui/contexts/SettingsContext.js';
export function validateDnsResolutionOrder(
order: string | undefined,
@@ -257,12 +258,14 @@ export async function main() {
setWindowTitle(basename(workspaceRoot), settings);
const instance = render(
-
+
+
+
,
{ exitOnCtrlC: false },
);
diff --git a/packages/cli/src/ui/contexts/SettingsContext.tsx b/packages/cli/src/ui/contexts/SettingsContext.tsx
new file mode 100644
index 00000000..130ba66e
--- /dev/null
+++ b/packages/cli/src/ui/contexts/SettingsContext.tsx
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useContext } from 'react';
+import { LoadedSettings } from '../../config/settings.js';
+
+export const SettingsContext = React.createContext(
+ undefined,
+);
+
+export const useSettings = () => {
+ const context = useContext(SettingsContext);
+ if (context === undefined) {
+ throw new Error('useSettings must be used within a SettingsProvider');
+ }
+ return context;
+};
diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx
index 58b32c7e..b183d556 100644
--- a/packages/cli/src/ui/utils/CodeColorizer.tsx
+++ b/packages/cli/src/ui/utils/CodeColorizer.tsx
@@ -20,6 +20,7 @@ import {
MaxSizedBox,
MINIMUM_MAX_HEIGHT,
} from '../components/shared/MaxSizedBox.js';
+import { LoadedSettings } from '../../config/settings.js';
// Configure theming and parsing utilities.
const lowlight = createLowlight(common);
@@ -129,9 +130,11 @@ export function colorizeCode(
availableHeight?: number,
maxWidth?: number,
theme?: Theme,
+ settings?: LoadedSettings,
): React.ReactNode {
const codeToHighlight = code.replace(/\n$/, '');
const activeTheme = theme || themeManager.getActiveTheme();
+ const showLineNumbers = settings?.merged.showLineNumbers ?? true;
try {
// Render the HAST tree using the adapted theme
@@ -167,12 +170,14 @@ export function colorizeCode(
return (
-
- {`${String(index + 1 + hiddenLinesCount).padStart(
- padWidth,
- ' ',
- )} `}
-
+ {showLineNumbers && (
+
+ {`${String(index + 1 + hiddenLinesCount).padStart(
+ padWidth,
+ ' ',
+ )} `}
+
+ )}
{contentToRender}
@@ -198,9 +203,11 @@ export function colorizeCode(
>
{lines.map((line, index) => (
-
- {`${String(index + 1).padStart(padWidth, ' ')} `}
-
+ {showLineNumbers && (
+
+ {`${String(index + 1).padStart(padWidth, ' ')} `}
+
+ )}
{line}
))}
diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx
index 312c1b5b..dba6bb6d 100644
--- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx
+++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx
@@ -7,6 +7,8 @@
import { render } from 'ink-testing-library';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MarkdownDisplay } from './MarkdownDisplay.js';
+import { LoadedSettings } from '../../config/settings.js';
+import { SettingsContext } from '../contexts/SettingsContext.js';
describe('', () => {
const baseProps = {
@@ -15,19 +17,32 @@ describe('', () => {
availableTerminalHeight: 40,
};
+ const mockSettings = new LoadedSettings(
+ { path: '', settings: {} },
+ { path: '', settings: {} },
+ { path: '', settings: {} },
+ [],
+ );
+
beforeEach(() => {
vi.clearAllMocks();
});
it('renders nothing for empty text', () => {
- const { lastFrame } = render();
+ const { lastFrame } = render(
+
+
+ ,
+ );
expect(lastFrame()).toMatchSnapshot();
});
it('renders a simple paragraph', () => {
const text = 'Hello, world.';
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -40,7 +55,9 @@ describe('', () => {
#### Header 4
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -48,7 +65,9 @@ describe('', () => {
it('renders a fenced code block with a language', () => {
const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```';
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -56,7 +75,9 @@ describe('', () => {
it('renders a fenced code block without a language', () => {
const text = '```\nplain text\n```';
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -64,7 +85,9 @@ describe('', () => {
it('handles unclosed (pending) code blocks', () => {
const text = '```typescript\nlet y = 2;';
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -76,7 +99,9 @@ describe('', () => {
+ item C
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -88,7 +113,9 @@ describe('', () => {
* Level 3
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -99,7 +126,9 @@ describe('', () => {
2. Second item
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -113,7 +142,9 @@ World
Test
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -126,7 +157,9 @@ Test
| Cell 3 | Cell 4 |
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -138,7 +171,9 @@ Some text before.
|---|
| 1 | 2 |`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -148,7 +183,9 @@ Some text before.
Paragraph 2.`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -169,8 +206,39 @@ some code
Another paragraph.
`;
const { lastFrame } = render(
- ,
+
+
+ ,
);
expect(lastFrame()).toMatchSnapshot();
});
+
+ it('hides line numbers in code blocks when showLineNumbers is false', () => {
+ const text = '```javascript\nconst x = 1;\n```';
+ const settings = new LoadedSettings(
+ { path: '', settings: {} },
+ { path: '', settings: { showLineNumbers: false } },
+ { path: '', settings: {} },
+ [],
+ );
+
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ expect(lastFrame()).not.toContain(' 1 ');
+ });
+
+ it('shows line numbers in code blocks by default', () => {
+ const text = '```javascript\nconst x = 1;\n```';
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ expect(lastFrame()).toContain(' 1 ');
+ });
});
diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx
index eb5dc453..7568e1f8 100644
--- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx
+++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx
@@ -10,6 +10,7 @@ import { Colors } from '../colors.js';
import { colorizeCode } from './CodeColorizer.js';
import { TableRenderer } from './TableRenderer.js';
import { RenderInline } from './InlineMarkdownRenderer.js';
+import { useSettings } from '../contexts/SettingsContext.js';
interface MarkdownDisplayProps {
text: string;
@@ -298,6 +299,7 @@ const RenderCodeBlockInternal: React.FC = ({
availableTerminalHeight,
terminalWidth,
}) => {
+ const settings = useSettings();
const MIN_LINES_FOR_MESSAGE = 1; // Minimum lines to show before the "generating more" message
const RESERVED_LINES = 2; // Lines reserved for the message itself and potential padding
@@ -322,6 +324,8 @@ const RenderCodeBlockInternal: React.FC = ({
lang,
availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING,
+ undefined,
+ settings,
);
return (
@@ -338,6 +342,8 @@ const RenderCodeBlockInternal: React.FC = ({
lang,
availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING,
+ undefined,
+ settings,
);
return (
diff --git a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap
index 66436bbd..223c293b 100644
--- a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap
@@ -23,6 +23,8 @@ exports[` > handles a table at the end of the input 1`] = `
exports[` > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`;
+exports[` > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`;
+
exports[` > inserts a single space between paragraphs 1`] = `
"Paragraph 1.
@@ -87,3 +89,5 @@ exports[` > renders unordered lists with different markers 1`
+ item C
"
`;
+
+exports[` > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;