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:
Jack Wotherspoon
2026-03-04 21:21:48 -05:00
committed by GitHub
parent c5112cde46
commit 9dc6898d28
19 changed files with 1635 additions and 262 deletions
@@ -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',
]);
});
});
+132
View File
@@ -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),
};
}
+22 -2
View File
@@ -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: {