diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml
index 70a413f13a..54c404c7c1 100644
--- a/.github/actions/publish-release/action.yml
+++ b/.github/actions/publish-release/action.yml
@@ -193,7 +193,7 @@ runs:
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
- name: '๐ฆ Prepare bundled CLI for npm release'
- if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
+ if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
diff --git a/README.md b/README.md
index 959b5a9534..2b25865179 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
[](https://codewiki.google/github.com/google-gemini/gemini-cli?utm_source=badge&utm_medium=github&utm_campaign=github.com/google-gemini/gemini-cli)
-
+
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
into your terminal. It provides lightweight access to Gemini, giving you the
diff --git a/docs/assets/theme-ansi-dark.png b/docs/assets/theme-ansi-dark.png
new file mode 100644
index 0000000000..10bcbd446e
Binary files /dev/null and b/docs/assets/theme-ansi-dark.png differ
diff --git a/docs/assets/theme-ansi-light.png b/docs/assets/theme-ansi-light.png
index 9766ae7820..8973ef2f99 100644
Binary files a/docs/assets/theme-ansi-light.png and b/docs/assets/theme-ansi-light.png differ
diff --git a/docs/assets/theme-ansi.png b/docs/assets/theme-ansi.png
deleted file mode 100644
index 5d46dacab8..0000000000
Binary files a/docs/assets/theme-ansi.png and /dev/null differ
diff --git a/docs/assets/theme-atom-one-dark.png b/docs/assets/theme-atom-one-dark.png
new file mode 100644
index 0000000000..f81ba24812
Binary files /dev/null and b/docs/assets/theme-atom-one-dark.png differ
diff --git a/docs/assets/theme-atom-one.png b/docs/assets/theme-atom-one.png
deleted file mode 100644
index c2787d6b62..0000000000
Binary files a/docs/assets/theme-atom-one.png and /dev/null differ
diff --git a/docs/assets/theme-ayu-dark.png b/docs/assets/theme-ayu-dark.png
new file mode 100644
index 0000000000..3f5d01d110
Binary files /dev/null and b/docs/assets/theme-ayu-dark.png differ
diff --git a/docs/assets/theme-ayu-light.png b/docs/assets/theme-ayu-light.png
index f177465679..a276a13c05 100644
Binary files a/docs/assets/theme-ayu-light.png and b/docs/assets/theme-ayu-light.png differ
diff --git a/docs/assets/theme-ayu.png b/docs/assets/theme-ayu.png
deleted file mode 100644
index 99391f8271..0000000000
Binary files a/docs/assets/theme-ayu.png and /dev/null differ
diff --git a/docs/assets/theme-default-dark.png b/docs/assets/theme-default-dark.png
new file mode 100644
index 0000000000..2f3e2d7534
Binary files /dev/null and b/docs/assets/theme-default-dark.png differ
diff --git a/docs/assets/theme-default-light.png b/docs/assets/theme-default-light.png
index 829d4ed5cc..e454211fdb 100644
Binary files a/docs/assets/theme-default-light.png and b/docs/assets/theme-default-light.png differ
diff --git a/docs/assets/theme-default.png b/docs/assets/theme-default.png
deleted file mode 100644
index 0b93a33433..0000000000
Binary files a/docs/assets/theme-default.png and /dev/null differ
diff --git a/docs/assets/theme-dracula-dark.png b/docs/assets/theme-dracula-dark.png
new file mode 100644
index 0000000000..e95183708e
Binary files /dev/null and b/docs/assets/theme-dracula-dark.png differ
diff --git a/docs/assets/theme-dracula.png b/docs/assets/theme-dracula.png
deleted file mode 100644
index 27213fbc5c..0000000000
Binary files a/docs/assets/theme-dracula.png and /dev/null differ
diff --git a/docs/assets/theme-github-dark.png b/docs/assets/theme-github-dark.png
new file mode 100644
index 0000000000..bcbd78ee29
Binary files /dev/null and b/docs/assets/theme-github-dark.png differ
diff --git a/docs/assets/theme-github-light.png b/docs/assets/theme-github-light.png
index 3cdc94aa49..35fbec5c8b 100644
Binary files a/docs/assets/theme-github-light.png and b/docs/assets/theme-github-light.png differ
diff --git a/docs/assets/theme-github.png b/docs/assets/theme-github.png
deleted file mode 100644
index a62961b650..0000000000
Binary files a/docs/assets/theme-github.png and /dev/null differ
diff --git a/docs/assets/theme-google-light.png b/docs/assets/theme-google-light.png
index 835ebc4bea..04f0aa8e46 100644
Binary files a/docs/assets/theme-google-light.png and b/docs/assets/theme-google-light.png differ
diff --git a/docs/assets/theme-holiday-dark.png b/docs/assets/theme-holiday-dark.png
new file mode 100644
index 0000000000..70416650d5
Binary files /dev/null and b/docs/assets/theme-holiday-dark.png differ
diff --git a/docs/assets/theme-shades-of-purple-dark.png b/docs/assets/theme-shades-of-purple-dark.png
new file mode 100644
index 0000000000..c3d2e50538
Binary files /dev/null and b/docs/assets/theme-shades-of-purple-dark.png differ
diff --git a/docs/assets/theme-solarized-dark.png b/docs/assets/theme-solarized-dark.png
new file mode 100644
index 0000000000..be57349283
Binary files /dev/null and b/docs/assets/theme-solarized-dark.png differ
diff --git a/docs/assets/theme-solarized-light.png b/docs/assets/theme-solarized-light.png
new file mode 100644
index 0000000000..838a3b6870
Binary files /dev/null and b/docs/assets/theme-solarized-light.png differ
diff --git a/docs/assets/theme-xcode-light.png b/docs/assets/theme-xcode-light.png
index eb056a5589..26f0a74314 100644
Binary files a/docs/assets/theme-xcode-light.png and b/docs/assets/theme-xcode-light.png differ
diff --git a/docs/cli/themes.md b/docs/cli/themes.md
index 08564a249a..adfe64d081 100644
--- a/docs/cli/themes.md
+++ b/docs/cli/themes.md
@@ -16,6 +16,8 @@ using the `/theme` command within Gemini CLI:
- `Default`
- `Dracula`
- `GitHub`
+ - `Holiday`
+ - `Shades Of Purple`
- `Solarized Dark`
- **Light themes:**
- `ANSI Light`
@@ -185,7 +187,7 @@ untrusted sources.
### Example custom theme
-
+
### Using your custom theme
@@ -212,58 +214,66 @@ identify their source, for example: `shades-of-green (green-extension)`.
### ANSI
-
+
-### Atom OneDark
+### Atom One
-
+
### Ayu
-
+
### Default
-
+
### Dracula
-
+
### GitHub
-
+
+
+### Holiday
+
+
+
+### Shades Of Purple
+
+
### Solarized Dark
-
+
## Light themes
### ANSI Light
-
+
### Ayu Light
-
+
### Default Light
-
+
### GitHub Light
-
+
### Google Code
-
+
### Solarized Light
-
+
### Xcode
-
+
diff --git a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx
index da8b43dd20..b8de6adb0b 100644
--- a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx
+++ b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx
@@ -103,7 +103,7 @@ describe('ApiAuthDialog', () => {
it.each([
{
- keyName: 'return',
+ keyName: 'enter',
sequence: '\r',
expectedCall: onSubmit,
args: ['submitted-key'],
diff --git a/packages/cli/src/ui/components/ConfigExtensionDialog.tsx b/packages/cli/src/ui/components/ConfigExtensionDialog.tsx
index b6fb8ce1b6..7f09d46491 100644
--- a/packages/cli/src/ui/components/ConfigExtensionDialog.tsx
+++ b/packages/cli/src/ui/components/ConfigExtensionDialog.tsx
@@ -210,7 +210,7 @@ export const ConfigExtensionDialog: React.FC = ({
useKeypress(
(key: Key) => {
if (state.type === 'ASK_CONFIRMATION') {
- if (key.name === 'y' || key.name === 'return') {
+ if (key.name === 'y' || key.name === 'enter') {
state.resolve(true);
return true;
}
@@ -220,7 +220,7 @@ export const ConfigExtensionDialog: React.FC = ({
}
}
if (state.type === 'DONE' || state.type === 'ERROR') {
- if (key.name === 'return' || key.name === 'escape') {
+ if (key.name === 'enter' || key.name === 'escape') {
onClose();
return true;
}
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/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 785641a556..1cfa2d4215 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -972,7 +972,7 @@ export const InputPrompt: React.FC = ({
if (targetIndex < completion.suggestions.length) {
const suggestion = completion.suggestions[targetIndex];
- const isEnterKey = key.name === 'return' && !key.ctrl;
+ const isEnterKey = key.name === 'enter' && !key.ctrl;
if (isEnterKey && shellModeActive) {
if (hasUserNavigatedSuggestions.current) {
diff --git a/packages/cli/src/ui/components/SessionBrowser.test.tsx b/packages/cli/src/ui/components/SessionBrowser.test.tsx
index 2e68cb6898..e97ae310bd 100644
--- a/packages/cli/src/ui/components/SessionBrowser.test.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.test.tsx
@@ -324,7 +324,7 @@ describe('SessionBrowser component', () => {
await waitUntilReady();
// Press Enter.
- triggerKey({ name: 'return', sequence: '\r' });
+ triggerKey({ name: 'enter', sequence: '\r' });
await waitUntilReady();
expect(onResumeSession).toHaveBeenCalledTimes(1);
@@ -367,7 +367,7 @@ describe('SessionBrowser component', () => {
await waitUntilReady();
// Active selection is at 0 (current session).
- triggerKey({ name: 'return', sequence: '\r' });
+ triggerKey({ name: 'enter', sequence: '\r' });
await waitUntilReady();
expect(onResumeSession).not.toHaveBeenCalled();
diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx
index 154ad62522..72eb5ef55c 100644
--- a/packages/cli/src/ui/components/SessionBrowser.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.tsx
@@ -873,7 +873,7 @@ export const useSessionBrowserInput = (
// Handling regardless of search mode.
if (
- key.name === 'return' &&
+ key.name === 'enter' &&
state.filteredAndSortedSessions[state.activeIndex]
) {
const selectedSession =
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[` > displays "Limit reached" message when remaining is 0 1`] = `
-" workspace (/directory) sandbox /model /stats
- ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro limit reached
+" workspace (/directory) sandbox /model /stats
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro limit reached
"
`;
exports[` > displays the usage indicator when usage is low 1`] = `
-" workspace (/directory) sandbox /model /stats
- ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 85%
+" workspace (/directory) sandbox /model /stats
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 85%
"
`;
exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
-" workspace (/directory) sandbox /model context
- ...me/more/directories/to/make/it/long no sandbox gemini-pro 14%
+" workspace (/directory) sandbox /model context
+ ...me/more/directories/to/make/it/long no sandbox gemini-pro 14%
"
`;
exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
-" workspace (/directory) sandbox /model context
- ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 14% used
+" workspace (/directory) sandbox /model context
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 14% used
"
`;
@@ -33,13 +33,13 @@ exports[` > footer configuration filtering (golden snapshots) > render
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`] = `
-" workspace (/directory) sandbox
+" workspace (/directory) sandbox
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox
"
`;
exports[` > hides the usage indicator when usage is not near limit 1`] = `
-" workspace (/directory) sandbox /model /stats
- ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 15%
+" workspace (/directory) sandbox /model /stats
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 15%
"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg
index 7cec49200d..d4da9c5fa0 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg
@@ -125,28 +125,27 @@
โ
โ
workspace (/directory)
- branch
- sandbox
- /model
- /stats
-
- diff
-
+ branch
+ sandbox
+ /model
+ /stats
+
+ diff
+
โ
โ
โ
โ
~/project/path
- main
- docker
- gemini-2.5-pro
- 97%
-
- +12
-
-
- -4
+ main
+ docker
+ gemini-2.5-pro
+ 97%
+
+ +12
+
+ -4
โ
โ
โ
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg
index ae9b8eecea..41b13e401b 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg
@@ -126,22 +126,21 @@
โ
workspace (/directory)
-
- branch
- sandbox
- /model
- /stats
+ branch
+ sandbox
+ /model
+ /stats
โ
โ
โ
โ
~/project/path
-
- main
- docker
- gemini-2.5-pro
- 97%
+
+ main
+ docker
+ gemini-2.5-pro
+ 97%
โ
โ
โ
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg
new file mode 100644
index 0000000000..08340ca8ac
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg
@@ -0,0 +1,143 @@
+
\ No newline at end of file
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap
index f2fee0a8c3..ce310c4ac9 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap
@@ -36,8 +36,8 @@ exports[` > highlights the active item in the preview 1`]
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Preview: โ โ
-โ โ workspace (/directory) branch sandbox /model /stats diff โ โ
-โ โ ~/project/path main docker gemini-2.5-pro 97% +12 -4 โ โ
+โ โ workspace (/directory) branch sandbox /model /stats diff โ โ
+โ โ ~/project/path main docker gemini-2.5-pro 97% +12 -4 โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
@@ -79,8 +79,8 @@ exports[` > renders correctly with default settings 1`] =
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Preview: โ โ
-โ โ workspace (/directory) branch sandbox /model /stats โ โ
-โ โ ~/project/path main docker gemini-2.5-pro 97% โ โ
+โ โ workspace (/directory) branch sandbox /model /stats โ โ
+โ โ ~/project/path main docker gemini-2.5-pro 97% โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
@@ -123,8 +123,50 @@ exports[` > renders correctly with default settings 2`] =
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Preview: โ โ
-โ โ workspace (/directory) branch sandbox /model /stats โ โ
-โ โ ~/project/path main docker gemini-2.5-pro 97% โ โ
+โ โ workspace (/directory) branch sandbox /model /stats โ โ
+โ โ ~/project/path main docker gemini-2.5-pro 97% โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > updates the preview when Show footer labels is toggled off 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Configure Footer โ
+โ โ
+โ Select which items to display in the footer. โ
+โ โ
+โ [โ] workspace โ
+โ Current working directory โ
+โ [โ] git-branch โ
+โ Current git branch name (not shown when unavailable) โ
+โ [โ] sandbox โ
+โ Sandbox type and trust indicator โ
+โ [โ] model-name โ
+โ Current model identifier โ
+โ [โ] quota โ
+โ Remaining usage on daily limit (not shown when unavailable) โ
+โ [ ] context-used โ
+โ Percentage of context window used โ
+โ [ ] memory-usage โ
+โ Memory used by the application โ
+โ [ ] session-id โ
+โ Unique identifier for the current session โ
+โ [ ] code-changes โ
+โ Lines added/removed in the session (not shown when zero) โ
+โ [ ] token-count โ
+โ Total tokens used in the session (not shown when zero) โ
+โ > [ ] Show footer labels โ
+โ โ
+โ Reset to default footer โ
+โ โ
+โ โ
+โ Enter to select ยท โ/โ to navigate ยท โ/โ to reorder ยท Esc to close โ
+โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ Preview: โ โ
+โ โ ~/project/path ยท main ยท docker ยท gemini-2.5-pro ยท 97% โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
diff --git a/packages/cli/src/ui/components/shared/TextInput.test.tsx b/packages/cli/src/ui/components/shared/TextInput.test.tsx
index 7e802bbbe3..a5bc79247c 100644
--- a/packages/cli/src/ui/components/shared/TextInput.test.tsx
+++ b/packages/cli/src/ui/components/shared/TextInput.test.tsx
@@ -287,7 +287,7 @@ describe('TextInput', () => {
await act(async () => {
keypressHandler({
- name: 'return',
+ name: 'enter',
shift: false,
alt: false,
ctrl: false,
@@ -314,7 +314,7 @@ describe('TextInput', () => {
await act(async () => {
keypressHandler({
- name: 'return',
+ name: 'enter',
shift: false,
alt: false,
ctrl: false,
@@ -339,7 +339,7 @@ describe('TextInput', () => {
await act(async () => {
keypressHandler({
- name: 'return',
+ name: 'enter',
shift: false,
alt: false,
ctrl: false,
diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts
index 34db346a6d..cf507335a0 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.test.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts
@@ -1534,7 +1534,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() => useTextBuffer({ viewport }));
act(() => {
result.current.handleInput({
- name: 'return',
+ name: 'enter',
shift: false,
alt: false,
ctrl: false,
@@ -1790,7 +1790,7 @@ describe('useTextBuffer', () => {
const { result } = renderHook(() => useTextBuffer({ viewport }));
act(() => {
result.current.handleInput({
- name: 'return',
+ name: 'enter',
shift: true,
alt: false,
ctrl: false,
@@ -2291,7 +2291,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
);
act(() => {
result.current.handleInput({
- name: 'return',
+ name: 'enter',
shift: false,
alt: false,
ctrl: false,
diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
index e353d3f23c..a0180b7186 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
@@ -109,7 +109,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
- name: 'return',
+ name: 'enter',
shift: false,
ctrl: false,
cmd: false,
@@ -124,7 +124,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
- name: 'return',
+ name: 'enter',
shift: true,
ctrl: false,
cmd: false,
@@ -157,7 +157,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
- name: 'return',
+ name: 'enter',
...expected,
}),
);
@@ -186,7 +186,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
- name: 'return',
+ name: 'enter',
shift: false,
alt: true,
ctrl: false,
@@ -225,7 +225,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenLastCalledWith(
expect.objectContaining({
- name: 'return',
+ name: 'enter',
sequence: '\r',
insertable: true,
shift: true,
@@ -247,7 +247,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenLastCalledWith(
expect.objectContaining({
- name: 'return',
+ name: 'enter',
shift: false,
alt: false,
ctrl: false,
@@ -647,8 +647,8 @@ describe('KeypressContext', () => {
describe('Parameterized functional keys', () => {
it.each([
// ModifyOtherKeys
- { sequence: `\x1b[27;2;13~`, expected: { name: 'return', shift: true } },
- { sequence: `\x1b[27;5;13~`, expected: { name: 'return', ctrl: true } },
+ { sequence: `\x1b[27;2;13~`, expected: { name: 'enter', shift: true } },
+ { sequence: `\x1b[27;5;13~`, expected: { name: 'enter', ctrl: true } },
{ sequence: `\x1b[27;5;9~`, expected: { name: 'tab', ctrl: true } },
{
sequence: `\x1b[27;6;9~`,
@@ -1133,7 +1133,7 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
- name: 'return',
+ name: 'enter',
}),
);
expect(keyHandler).toHaveBeenNthCalledWith(
diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx
index d3f9031ffe..7791872865 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.tsx
@@ -92,11 +92,11 @@ const KEY_INFO_MAP: Record<
'[[5~': { name: 'pageup' },
'[[6~': { name: 'pagedown' },
'[9u': { name: 'tab' },
- '[13u': { name: 'return' },
+ '[13u': { name: 'enter' },
'[27u': { name: 'escape' },
'[32u': { name: 'space' },
'[127u': { name: 'backspace' },
- '[57414u': { name: 'return' }, // Numpad Enter
+ '[57414u': { name: 'enter' }, // Numpad Enter
'[a': { name: 'up', shift: true },
'[b': { name: 'down', shift: true },
'[c': { name: 'right', shift: true },
@@ -186,10 +186,10 @@ function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler {
let lastKeyTime = 0;
return (key: Key) => {
const now = Date.now();
- if (key.name === 'return' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) {
+ if (key.name === 'enter' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) {
keypressHandler({
...key,
- name: 'return',
+ name: 'enter',
shift: true, // to make it a newline, not a submission
alt: false,
ctrl: false,
@@ -232,7 +232,7 @@ function bufferBackslashEnter(
if (nextKey === null) {
keypressHandler(key);
- } else if (nextKey.name === 'return') {
+ } else if (nextKey.name === 'enter') {
keypressHandler({
...nextKey,
shift: true,
@@ -582,11 +582,11 @@ function* emitKeys(
}
} else if (ch === '\r') {
// carriage return
- name = 'return';
+ name = 'enter';
alt = escaped;
} else if (escaped && ch === '\n') {
// Alt+Enter (linefeed), should be consistent with carriage return
- name = 'return';
+ name = 'enter';
alt = escaped;
} else if (ch === '\t') {
// tab
diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx
index 331d6a4bc4..947aa2354c 100644
--- a/packages/cli/src/ui/hooks/useKeypress.test.tsx
+++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx
@@ -119,7 +119,7 @@ describe(`useKeypress`, () => {
it('should correctly identify alt+enter (meta key)', () => {
renderKeypressHook(true);
- const key = { name: 'return', sequence: '\x1B\r' };
+ const key = { name: 'enter', sequence: '\x1B\r' };
act(() => stdin.write(key.sequence));
expect(onKeypress).toHaveBeenCalledWith(
expect.objectContaining({
diff --git a/packages/cli/src/ui/hooks/useSelectionList.test.tsx b/packages/cli/src/ui/hooks/useSelectionList.test.tsx
index 4151375280..6a1b82f77a 100644
--- a/packages/cli/src/ui/hooks/useSelectionList.test.tsx
+++ b/packages/cli/src/ui/hooks/useSelectionList.test.tsx
@@ -356,7 +356,7 @@ describe('useSelectionList', () => {
initialIndex: 2,
onSelect: mockOnSelect,
});
- pressKey('return');
+ pressKey('enter');
await waitUntilReady();
expect(mockOnSelect).toHaveBeenCalledTimes(1);
expect(mockOnSelect).toHaveBeenCalledWith('C');
@@ -371,7 +371,7 @@ describe('useSelectionList', () => {
act(() => result.current.setActiveIndex(1));
await waitUntilReady();
- pressKey('return');
+ pressKey('enter');
await waitUntilReady();
expect(mockOnSelect).not.toHaveBeenCalled();
});
@@ -415,7 +415,7 @@ describe('useSelectionList', () => {
await waitUntilReady();
// 3. Press Enter. Should select D.
act(() => {
- press('return');
+ press('enter');
});
await waitUntilReady();
@@ -459,7 +459,7 @@ describe('useSelectionList', () => {
// All presses happen in same render cycle - React batches the state updates
press('down'); // Should move 0 (A) -> 2 (C)
press('down'); // Should move 2 (C) -> 3 (D)
- press('return'); // Should select D
+ press('enter'); // Should select D
});
await waitUntilReady();
@@ -759,7 +759,7 @@ describe('useSelectionList', () => {
pressNumber('1');
await waitUntilReady();
- pressKey('return');
+ pressKey('enter');
await waitUntilReady();
expect(mockOnSelect).toHaveBeenCalledTimes(1);
diff --git a/packages/cli/src/ui/hooks/vim.ts b/packages/cli/src/ui/hooks/vim.ts
index 54de27496f..aa1388be9d 100644
--- a/packages/cli/src/ui/hooks/vim.ts
+++ b/packages/cli/src/ui/hooks/vim.ts
@@ -396,7 +396,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
// In INSERT mode, let InputPrompt handle completion keys and special commands
if (
normalizedKey.name === 'tab' ||
- (normalizedKey.name === 'return' && !normalizedKey.ctrl) ||
+ (normalizedKey.name === 'enter' && !normalizedKey.ctrl) ||
normalizedKey.name === 'up' ||
normalizedKey.name === 'down' ||
(normalizedKey.ctrl && normalizedKey.name === 'r')
@@ -424,7 +424,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
// Special handling for Enter key to allow command submission (lower priority than completion)
if (
- normalizedKey.name === 'return' &&
+ normalizedKey.name === 'enter' &&
!normalizedKey.alt &&
!normalizedKey.ctrl &&
!normalizedKey.cmd
diff --git a/packages/cli/src/ui/key/keyBindings.test.ts b/packages/cli/src/ui/key/keyBindings.test.ts
index b47e8d56b8..3057bf85b6 100644
--- a/packages/cli/src/ui/key/keyBindings.test.ts
+++ b/packages/cli/src/ui/key/keyBindings.test.ts
@@ -80,8 +80,8 @@ describe('KeyBinding', () => {
});
it('should handle named keys with modifiers', () => {
- const binding = new KeyBinding('ctrl+return');
- expect(binding.key).toBe('return');
+ const binding = new KeyBinding('ctrl+enter');
+ expect(binding.key).toBe('enter');
expect(binding.ctrl).toBe(true);
});
diff --git a/packages/cli/src/ui/key/keyBindings.ts b/packages/cli/src/ui/key/keyBindings.ts
index 209111b53c..b375d991c8 100644
--- a/packages/cli/src/ui/key/keyBindings.ts
+++ b/packages/cli/src/ui/key/keyBindings.ts
@@ -149,11 +149,9 @@ export class KeyBinding {
'numpad_subtract',
'numpad_decimal',
'numpad_divide',
- // Gemini CLI legacy/internal support
- 'return',
]);
- /** The key name (e.g., 'a', 'return', 'tab', 'escape') */
+ /** The key name (e.g., 'a', 'enter', 'tab', 'escape') */
readonly key: string;
readonly shift: boolean;
readonly alt: boolean;
@@ -238,7 +236,7 @@ export type KeyBindingConfig = {
*/
export const defaultKeyBindings: KeyBindingConfig = {
// Basic Controls
- [Command.RETURN]: [new KeyBinding('return')],
+ [Command.RETURN]: [new KeyBinding('enter')],
[Command.ESCAPE]: [new KeyBinding('escape'), new KeyBinding('ctrl+[')],
[Command.QUIT]: [new KeyBinding('ctrl+c')],
[Command.EXIT]: [new KeyBinding('ctrl+d')],
@@ -308,7 +306,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.HISTORY_UP]: [new KeyBinding('ctrl+p')],
[Command.HISTORY_DOWN]: [new KeyBinding('ctrl+n')],
[Command.REVERSE_SEARCH]: [new KeyBinding('ctrl+r')],
- [Command.SUBMIT_REVERSE_SEARCH]: [new KeyBinding('return')],
+ [Command.SUBMIT_REVERSE_SEARCH]: [new KeyBinding('enter')],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [new KeyBinding('tab')],
// Navigation
@@ -325,10 +323,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.DIALOG_PREV]: [new KeyBinding('shift+tab')],
// Suggestions & Completions
- [Command.ACCEPT_SUGGESTION]: [
- new KeyBinding('tab'),
- new KeyBinding('return'),
- ],
+ [Command.ACCEPT_SUGGESTION]: [new KeyBinding('tab'), new KeyBinding('enter')],
[Command.COMPLETION_UP]: [new KeyBinding('up'), new KeyBinding('ctrl+p')],
[Command.COMPLETION_DOWN]: [new KeyBinding('down'), new KeyBinding('ctrl+n')],
[Command.EXPAND_SUGGESTION]: [new KeyBinding('right')],
@@ -336,12 +331,12 @@ export const defaultKeyBindings: KeyBindingConfig = {
// Text Input
// Must also exclude shift to allow shift+enter for newline
- [Command.SUBMIT]: [new KeyBinding('return')],
+ [Command.SUBMIT]: [new KeyBinding('enter')],
[Command.NEWLINE]: [
- new KeyBinding('ctrl+return'),
- new KeyBinding('cmd+return'),
- new KeyBinding('alt+return'),
- new KeyBinding('shift+return'),
+ new KeyBinding('ctrl+enter'),
+ new KeyBinding('cmd+enter'),
+ new KeyBinding('alt+enter'),
+ new KeyBinding('shift+enter'),
new KeyBinding('ctrl+j'),
],
[Command.OPEN_EXTERNAL_EDITOR]: [new KeyBinding('ctrl+x')],
@@ -366,7 +361,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [new KeyBinding('tab')],
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [new KeyBinding('tab')],
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [new KeyBinding('tab')],
- [Command.BACKGROUND_SHELL_SELECT]: [new KeyBinding('return')],
+ [Command.BACKGROUND_SHELL_SELECT]: [new KeyBinding('enter')],
[Command.BACKGROUND_SHELL_ESCAPE]: [new KeyBinding('escape')],
[Command.SHOW_MORE_LINES]: [new KeyBinding('ctrl+o')],
[Command.EXPAND_PASTE]: [new KeyBinding('ctrl+o')],
diff --git a/packages/cli/src/ui/key/keyMatchers.test.ts b/packages/cli/src/ui/key/keyMatchers.test.ts
index 62766d1a0d..12e2f07bc2 100644
--- a/packages/cli/src/ui/key/keyMatchers.test.ts
+++ b/packages/cli/src/ui/key/keyMatchers.test.ts
@@ -31,7 +31,7 @@ describe('keyMatchers', () => {
// Basic bindings
{
command: Command.RETURN,
- positive: [createKey('return')],
+ positive: [createKey('enter')],
negative: [createKey('r')],
},
{
@@ -270,8 +270,8 @@ describe('keyMatchers', () => {
// Auto-completion
{
command: Command.ACCEPT_SUGGESTION,
- positive: [createKey('tab'), createKey('return')],
- negative: [createKey('return', { ctrl: true }), createKey('space')],
+ positive: [createKey('tab'), createKey('enter')],
+ negative: [createKey('enter', { ctrl: true }), createKey('space')],
},
{
command: Command.COMPLETION_UP,
@@ -287,21 +287,21 @@ describe('keyMatchers', () => {
// Text input
{
command: Command.SUBMIT,
- positive: [createKey('return')],
+ positive: [createKey('enter')],
negative: [
- createKey('return', { ctrl: true }),
- createKey('return', { cmd: true }),
- createKey('return', { alt: true }),
+ createKey('enter', { ctrl: true }),
+ createKey('enter', { cmd: true }),
+ createKey('enter', { alt: true }),
],
},
{
command: Command.NEWLINE,
positive: [
- createKey('return', { ctrl: true }),
- createKey('return', { cmd: true }),
- createKey('return', { alt: true }),
+ createKey('enter', { ctrl: true }),
+ createKey('enter', { cmd: true }),
+ createKey('enter', { alt: true }),
],
- negative: [createKey('return'), createKey('n')],
+ negative: [createKey('enter'), createKey('n')],
},
// External tools
@@ -382,14 +382,14 @@ describe('keyMatchers', () => {
},
{
command: Command.SUBMIT_REVERSE_SEARCH,
- positive: [createKey('return')],
- negative: [createKey('return', { ctrl: true }), createKey('tab')],
+ positive: [createKey('enter')],
+ negative: [createKey('enter', { ctrl: true }), createKey('tab')],
},
{
command: Command.ACCEPT_SUGGESTION_REVERSE_SEARCH,
positive: [createKey('tab')],
negative: [
- createKey('return'),
+ createKey('enter'),
createKey('space'),
createKey('tab', { ctrl: true }),
],
diff --git a/packages/cli/src/ui/key/keyToAnsi.ts b/packages/cli/src/ui/key/keyToAnsi.ts
index adb9874933..6d61c2e114 100644
--- a/packages/cli/src/ui/key/keyToAnsi.ts
+++ b/packages/cli/src/ui/key/keyToAnsi.ts
@@ -21,7 +21,7 @@ const SPECIAL_KEYS: Record = {
end: '\x1b[F',
pageup: '\x1b[5~',
pagedown: '\x1b[6~',
- return: '\r',
+ enter: '\r',
};
/**
diff --git a/packages/cli/src/ui/key/keybindingUtils.test.ts b/packages/cli/src/ui/key/keybindingUtils.test.ts
index 58a113f4de..633ebbedb2 100644
--- a/packages/cli/src/ui/key/keybindingUtils.test.ts
+++ b/packages/cli/src/ui/key/keybindingUtils.test.ts
@@ -27,7 +27,7 @@ describe('keybindingUtils', () => {
},
{
name: 'named key (return)',
- binding: new KeyBinding('return'),
+ binding: new KeyBinding('enter'),
expected: {
darwin: 'Enter',
win32: 'Enter',
diff --git a/packages/cli/src/ui/key/keybindingUtils.ts b/packages/cli/src/ui/key/keybindingUtils.ts
index c4f4c6b942..f0ec6e37bd 100644
--- a/packages/cli/src/ui/key/keybindingUtils.ts
+++ b/packages/cli/src/ui/key/keybindingUtils.ts
@@ -16,7 +16,7 @@ import {
* Maps internal key names to user-friendly display names.
*/
const KEY_NAME_MAP: Record = {
- return: 'Enter',
+ enter: 'Enter',
escape: 'Esc',
backspace: 'Backspace',
delete: 'Delete',
diff --git a/packages/core/src/agents/agent-scheduler.test.ts b/packages/core/src/agents/agent-scheduler.test.ts
index 451fb276a2..02d780128c 100644
--- a/packages/core/src/agents/agent-scheduler.test.ts
+++ b/packages/core/src/agents/agent-scheduler.test.ts
@@ -19,23 +19,24 @@ vi.mock('../scheduler/scheduler.js', () => ({
}));
describe('agent-scheduler', () => {
- let mockConfig: Mocked;
let mockToolRegistry: Mocked;
let mockMessageBus: Mocked;
beforeEach(() => {
+ vi.mocked(Scheduler).mockClear();
mockMessageBus = {} as Mocked;
mockToolRegistry = {
getTool: vi.fn(),
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
} as unknown as Mocked;
- mockConfig = {
- getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
- toolRegistry: mockToolRegistry,
- } as unknown as Mocked;
});
it('should create a scheduler with agent-specific config', async () => {
+ const mockConfig = {
+ getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
+ toolRegistry: mockToolRegistry,
+ } as unknown as Mocked;
+
const requests: ToolCallRequestInfo[] = [
{
callId: 'call-1',
@@ -68,8 +69,46 @@ describe('agent-scheduler', () => {
}),
);
- // Verify that the scheduler's config has the overridden tool registry
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config;
expect(schedulerConfig.toolRegistry).toBe(mockToolRegistry);
});
+
+ it('should override toolRegistry getter from prototype chain', async () => {
+ const mainRegistry = { _id: 'main' } as unknown as Mocked;
+ const agentRegistry = {
+ _id: 'agent',
+ getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
+ } as unknown as Mocked;
+
+ const config = {
+ getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
+ } as unknown as Mocked;
+ Object.defineProperty(config, 'toolRegistry', {
+ get: () => mainRegistry,
+ configurable: true,
+ });
+
+ await scheduleAgentTools(
+ config as unknown as Config,
+ [
+ {
+ callId: 'c1',
+ name: 'new_page',
+ args: {},
+ isClientInitiated: false,
+ prompt_id: 'p1',
+ },
+ ],
+ {
+ schedulerId: 'browser-1',
+ toolRegistry: agentRegistry as unknown as ToolRegistry,
+ signal: new AbortController().signal,
+ },
+ );
+
+ const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config;
+ expect(schedulerConfig.toolRegistry).toBe(agentRegistry);
+ expect(schedulerConfig.toolRegistry).not.toBe(mainRegistry);
+ expect(schedulerConfig.getToolRegistry()).toBe(agentRegistry);
+ });
});
diff --git a/packages/core/src/agents/agent-scheduler.ts b/packages/core/src/agents/agent-scheduler.ts
index 983f814b0f..3b78ec47ee 100644
--- a/packages/core/src/agents/agent-scheduler.ts
+++ b/packages/core/src/agents/agent-scheduler.ts
@@ -58,6 +58,11 @@ export async function scheduleAgentTools(
const agentConfig: Config = Object.create(config);
agentConfig.getToolRegistry = () => toolRegistry;
agentConfig.getMessageBus = () => toolRegistry.getMessageBus();
+ // Override toolRegistry property so AgentLoopContext reads the agent-specific registry.
+ Object.defineProperty(agentConfig, 'toolRegistry', {
+ get: () => toolRegistry,
+ configurable: true,
+ });
const scheduler = new Scheduler({
config: agentConfig,
diff --git a/packages/core/src/agents/browser/browserAgentFactory.test.ts b/packages/core/src/agents/browser/browserAgentFactory.test.ts
index a317f3a9ed..96fba50d4f 100644
--- a/packages/core/src/agents/browser/browserAgentFactory.test.ts
+++ b/packages/core/src/agents/browser/browserAgentFactory.test.ts
@@ -209,6 +209,45 @@ describe('browserAgentFactory', () => {
.map((t) => t.name) ?? [];
expect(toolNames).toContain('analyze_screenshot');
});
+
+ it('should include all MCP navigation tools (new_page, navigate_page) in definition', async () => {
+ mockBrowserManager.getDiscoveredTools.mockResolvedValue([
+ { name: 'take_snapshot', description: 'Take snapshot' },
+ { name: 'click', description: 'Click element' },
+ { name: 'fill', description: 'Fill form field' },
+ { name: 'navigate_page', description: 'Navigate to URL' },
+ { name: 'new_page', description: 'Open a new page/tab' },
+ { name: 'close_page', description: 'Close page' },
+ { name: 'select_page', description: 'Select page' },
+ { name: 'press_key', description: 'Press key' },
+ { name: 'hover', description: 'Hover element' },
+ ]);
+
+ const { definition } = await createBrowserAgentDefinition(
+ mockConfig,
+ mockMessageBus,
+ );
+
+ const toolNames =
+ definition.toolConfig?.tools
+ ?.filter(
+ (t): t is { name: string } => typeof t === 'object' && 'name' in t,
+ )
+ .map((t) => t.name) ?? [];
+
+ // All MCP tools must be present
+ expect(toolNames).toContain('new_page');
+ expect(toolNames).toContain('navigate_page');
+ expect(toolNames).toContain('close_page');
+ expect(toolNames).toContain('select_page');
+ expect(toolNames).toContain('click');
+ expect(toolNames).toContain('take_snapshot');
+ expect(toolNames).toContain('press_key');
+ // Custom composite tool must also be present
+ expect(toolNames).toContain('type_text');
+ // Total: 9 MCP + 1 type_text (no analyze_screenshot without visualModel)
+ expect(definition.toolConfig?.tools).toHaveLength(10);
+ });
});
describe('cleanupBrowserAgent', () => {
diff --git a/scripts/tests/generate-keybindings-doc.test.ts b/scripts/tests/generate-keybindings-doc.test.ts
index c669fed02e..19ba2e0f98 100644
--- a/scripts/tests/generate-keybindings-doc.test.ts
+++ b/scripts/tests/generate-keybindings-doc.test.ts
@@ -36,7 +36,7 @@ describe('generate-keybindings-doc', () => {
},
{
description: 'Submit with Enter if no modifiers are held.',
- bindings: [{ key: 'return', shift: false, ctrl: false }],
+ bindings: [{ key: 'enter', shift: false, ctrl: false }],
},
],
},