mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
feat: add custom footer configuration via /footer (#19001)
Co-authored-by: Keith Guerin <keithguerin@gmail.com> Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { deriveItemsFromLegacySettings } from './footerItems.js';
|
||||
import { createMockSettings } from '../test-utils/settings.js';
|
||||
|
||||
describe('deriveItemsFromLegacySettings', () => {
|
||||
it('returns defaults when no legacy settings are customized', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toEqual([
|
||||
'workspace',
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'quota',
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes workspace when hideCWD is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideCWD: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('workspace');
|
||||
});
|
||||
|
||||
it('removes sandbox when hideSandboxStatus is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideSandboxStatus: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('sandbox');
|
||||
});
|
||||
|
||||
it('removes model-name, context-used, and quota when hideModelInfo is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideModelInfo: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('model-name');
|
||||
expect(items).not.toContain('context-used');
|
||||
expect(items).not.toContain('quota');
|
||||
});
|
||||
|
||||
it('includes context-used when hideContextPercentage is false', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideContextPercentage: false } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toContain('context-used');
|
||||
// Should be after model-name
|
||||
const modelIdx = items.indexOf('model-name');
|
||||
const contextIdx = items.indexOf('context-used');
|
||||
expect(contextIdx).toBe(modelIdx + 1);
|
||||
});
|
||||
|
||||
it('includes memory-usage when showMemoryUsage is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { showMemoryUsage: true, footer: { hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toContain('memory-usage');
|
||||
});
|
||||
|
||||
it('handles combination of settings', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
showMemoryUsage: true,
|
||||
footer: {
|
||||
hideCWD: true,
|
||||
hideModelInfo: true,
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
},
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toEqual([
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'context-used',
|
||||
'memory-usage',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { MergedSettings } from './settings.js';
|
||||
|
||||
export const ALL_ITEMS = [
|
||||
{
|
||||
id: 'workspace',
|
||||
header: 'workspace (/directory)',
|
||||
description: 'Current working directory',
|
||||
},
|
||||
{
|
||||
id: 'git-branch',
|
||||
header: 'branch',
|
||||
description: 'Current git branch name (not shown when unavailable)',
|
||||
},
|
||||
{
|
||||
id: 'sandbox',
|
||||
header: 'sandbox',
|
||||
description: 'Sandbox type and trust indicator',
|
||||
},
|
||||
{
|
||||
id: 'model-name',
|
||||
header: '/model',
|
||||
description: 'Current model identifier',
|
||||
},
|
||||
{
|
||||
id: 'context-used',
|
||||
header: 'context',
|
||||
description: 'Percentage of context window used',
|
||||
},
|
||||
{
|
||||
id: 'quota',
|
||||
header: '/stats',
|
||||
description: 'Remaining usage on daily limit (not shown when unavailable)',
|
||||
},
|
||||
{
|
||||
id: 'memory-usage',
|
||||
header: 'memory',
|
||||
description: 'Memory used by the application',
|
||||
},
|
||||
{
|
||||
id: 'session-id',
|
||||
header: 'session',
|
||||
description: 'Unique identifier for the current session',
|
||||
},
|
||||
{
|
||||
id: 'code-changes',
|
||||
header: 'diff',
|
||||
description: 'Lines added/removed in the session (not shown when zero)',
|
||||
},
|
||||
{
|
||||
id: 'token-count',
|
||||
header: 'tokens',
|
||||
description: 'Total tokens used in the session (not shown when zero)',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type FooterItemId = (typeof ALL_ITEMS)[number]['id'];
|
||||
|
||||
export const DEFAULT_ORDER = [
|
||||
'workspace',
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'context-used',
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'session-id',
|
||||
'code-changes',
|
||||
'token-count',
|
||||
];
|
||||
|
||||
export function deriveItemsFromLegacySettings(
|
||||
settings: MergedSettings,
|
||||
): string[] {
|
||||
const defaults = [
|
||||
'workspace',
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'quota',
|
||||
];
|
||||
const items = [...defaults];
|
||||
|
||||
const remove = (arr: string[], id: string) => {
|
||||
const idx = arr.indexOf(id);
|
||||
if (idx !== -1) arr.splice(idx, 1);
|
||||
};
|
||||
|
||||
if (settings.ui.footer.hideCWD) remove(items, 'workspace');
|
||||
if (settings.ui.footer.hideSandboxStatus) remove(items, 'sandbox');
|
||||
if (settings.ui.footer.hideModelInfo) {
|
||||
remove(items, 'model-name');
|
||||
remove(items, 'context-used');
|
||||
remove(items, 'quota');
|
||||
}
|
||||
if (
|
||||
!settings.ui.footer.hideContextPercentage &&
|
||||
!items.includes('context-used')
|
||||
) {
|
||||
const modelIdx = items.indexOf('model-name');
|
||||
if (modelIdx !== -1) items.splice(modelIdx + 1, 0, 'context-used');
|
||||
else items.push('context-used');
|
||||
}
|
||||
if (settings.ui.showMemoryUsage) items.push('memory-usage');
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const VALID_IDS: Set<string> = new Set(ALL_ITEMS.map((i) => i.id));
|
||||
|
||||
/**
|
||||
* Resolves the ordered list and selected set of footer items from settings.
|
||||
* Used by FooterConfigDialog to initialize and reset state.
|
||||
*/
|
||||
export function resolveFooterState(settings: MergedSettings): {
|
||||
orderedIds: string[];
|
||||
selectedIds: Set<string>;
|
||||
} {
|
||||
const source = (
|
||||
settings.ui?.footer?.items ?? deriveItemsFromLegacySettings(settings)
|
||||
).filter((id: string) => VALID_IDS.has(id));
|
||||
const others = DEFAULT_ORDER.filter((id) => !source.includes(id));
|
||||
return {
|
||||
orderedIds: [...source, ...others],
|
||||
selectedIds: new Set(source),
|
||||
};
|
||||
}
|
||||
@@ -565,14 +565,34 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Settings for the footer.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
items: {
|
||||
type: 'array',
|
||||
label: 'Footer Items',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: undefined as string[] | undefined,
|
||||
description:
|
||||
'List of item IDs to display in the footer. Rendered in order',
|
||||
showInDialog: false,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
showLabels: {
|
||||
type: 'boolean',
|
||||
label: 'Show Footer Labels',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Display a second line above the footer items with descriptive headers (e.g., /model).',
|
||||
showInDialog: false,
|
||||
},
|
||||
hideCWD: {
|
||||
type: 'boolean',
|
||||
label: 'Hide CWD',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Hide the current working directory path in the footer.',
|
||||
description: 'Hide the current working directory in the footer.',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideSandboxStatus: {
|
||||
|
||||
Reference in New Issue
Block a user