mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
chore: refactor based on feedback
This commit is contained in:
committed by
Keith Guerin
parent
39be41d6ae
commit
686998c1c8
@@ -9,60 +9,70 @@ import type { MergedSettings } from './settings.js';
|
|||||||
export const ALL_ITEMS = [
|
export const ALL_ITEMS = [
|
||||||
{
|
{
|
||||||
id: 'cwd',
|
id: 'cwd',
|
||||||
|
header: 'Path',
|
||||||
label: 'cwd',
|
label: 'cwd',
|
||||||
description: 'Current directory path',
|
description: 'Current directory path',
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'git-branch',
|
id: 'git-branch',
|
||||||
|
header: 'Branch',
|
||||||
label: 'git-branch',
|
label: 'git-branch',
|
||||||
description: 'Current git branch name',
|
description: 'Current git branch name',
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sandbox-status',
|
id: 'sandbox-status',
|
||||||
|
header: '/docs',
|
||||||
label: 'sandbox-status',
|
label: 'sandbox-status',
|
||||||
description: 'Sandbox type and trust indicator',
|
description: 'Sandbox type and trust indicator',
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'model-name',
|
id: 'model-name',
|
||||||
|
header: '/model',
|
||||||
label: 'model-name',
|
label: 'model-name',
|
||||||
description: 'Current model identifier',
|
description: 'Current model identifier',
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'context-remaining',
|
id: 'context-remaining',
|
||||||
|
header: 'Context',
|
||||||
label: 'context-remaining',
|
label: 'context-remaining',
|
||||||
description: 'Percentage of context window remaining',
|
description: 'Percentage of context window remaining',
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quota',
|
id: 'quota',
|
||||||
|
header: '/stats',
|
||||||
label: 'quota',
|
label: 'quota',
|
||||||
description: 'Remaining usage on daily limit',
|
description: 'Remaining usage on daily limit',
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'memory-usage',
|
id: 'memory-usage',
|
||||||
|
header: 'Memory',
|
||||||
label: 'memory-usage',
|
label: 'memory-usage',
|
||||||
description: 'Node.js heap memory usage',
|
description: 'Node.js heap memory usage',
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'session-id',
|
id: 'session-id',
|
||||||
|
header: 'Session',
|
||||||
label: 'session-id',
|
label: 'session-id',
|
||||||
description: 'Unique identifier for the current session',
|
description: 'Unique identifier for the current session',
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'code-changes',
|
id: 'code-changes',
|
||||||
|
header: 'Diff',
|
||||||
label: 'code-changes',
|
label: 'code-changes',
|
||||||
description: 'Lines added/removed in the session',
|
description: 'Lines added/removed in the session',
|
||||||
defaultEnabled: true,
|
defaultEnabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'token-count',
|
id: 'token-count',
|
||||||
|
header: 'Tokens',
|
||||||
label: 'token-count',
|
label: 'token-count',
|
||||||
description: 'Total tokens used in the session',
|
description: 'Total tokens used in the session',
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
@@ -73,6 +83,7 @@ export type FooterItemId = (typeof ALL_ITEMS)[number]['id'];
|
|||||||
|
|
||||||
export interface FooterItem {
|
export interface FooterItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
header: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
defaultEnabled: boolean;
|
defaultEnabled: boolean;
|
||||||
|
|||||||
@@ -582,6 +582,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
items: { type: 'string' },
|
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: {
|
hideCWD: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Hide CWD',
|
label: 'Hide CWD',
|
||||||
|
|||||||
@@ -27,46 +27,40 @@ vi.mock('../../config/settings.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('ContextUsageDisplay', () => {
|
describe('ContextUsageDisplay', () => {
|
||||||
it('renders correct percentage left', async () => {
|
it('renders correct percentage left', () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame } = render(
|
||||||
<ContextUsageDisplay
|
<ContextUsageDisplay
|
||||||
promptTokenCount={5000}
|
promptTokenCount={5000}
|
||||||
model="gemini-pro"
|
model="gemini-pro"
|
||||||
terminalWidth={120}
|
terminalWidth={120}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('50% context left');
|
expect(output).toContain('50% left');
|
||||||
unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders short label when terminal width is small', async () => {
|
it('renders short label when terminal width is small', () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame } = render(
|
||||||
<ContextUsageDisplay
|
<ContextUsageDisplay
|
||||||
promptTokenCount={2000}
|
promptTokenCount={2000}
|
||||||
model="gemini-pro"
|
model="gemini-pro"
|
||||||
terminalWidth={80}
|
terminalWidth={80}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('80%');
|
expect(output).toContain('80%');
|
||||||
expect(output).not.toContain('context left');
|
expect(output).not.toContain('context left');
|
||||||
unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders 0% when full', async () => {
|
it('renders 0% when full', () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame } = render(
|
||||||
<ContextUsageDisplay
|
<ContextUsageDisplay
|
||||||
promptTokenCount={10000}
|
promptTokenCount={10000}
|
||||||
model="gemini-pro"
|
model="gemini-pro"
|
||||||
terminalWidth={120}
|
terminalWidth={120}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('0% context left');
|
expect(output).toContain('0% left');
|
||||||
unmount();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,18 +12,20 @@ export const ContextUsageDisplay = ({
|
|||||||
promptTokenCount,
|
promptTokenCount,
|
||||||
model,
|
model,
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
|
color = theme.text.primary,
|
||||||
}: {
|
}: {
|
||||||
promptTokenCount: number;
|
promptTokenCount: number;
|
||||||
model: string;
|
model: string;
|
||||||
terminalWidth: number;
|
terminalWidth: number;
|
||||||
|
color?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const percentage = getContextUsagePercentage(promptTokenCount, model);
|
const percentage = getContextUsagePercentage(promptTokenCount, model);
|
||||||
const percentageLeft = ((1 - percentage) * 100).toFixed(0);
|
const percentageLeft = ((1 - percentage) * 100).toFixed(0);
|
||||||
|
|
||||||
const label = terminalWidth < 100 ? '%' : '% context left';
|
const label = terminalWidth < 100 ? '%' : '% left';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={color}>
|
||||||
{percentageLeft}
|
{percentageLeft}
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,23 +17,24 @@ import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||||
import { QuotaDisplay } from './QuotaDisplay.js';
|
|
||||||
import {
|
import {
|
||||||
getStatusColor,
|
|
||||||
QUOTA_THRESHOLD_HIGH,
|
QUOTA_THRESHOLD_HIGH,
|
||||||
QUOTA_THRESHOLD_MEDIUM,
|
QUOTA_THRESHOLD_MEDIUM,
|
||||||
} from '../utils/displayUtils.js';
|
} from '../utils/displayUtils.js';
|
||||||
import { DebugProfiler } from './DebugProfiler.js';
|
import { DebugProfiler } from './DebugProfiler.js';
|
||||||
import { isDevelopment } from '../../utils/installationInfo.js';
|
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
import { useConfig } from '../contexts/ConfigContext.js';
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||||
import { ALL_ITEMS, type FooterItemId } from '../../config/footerItems.js';
|
import {
|
||||||
|
ALL_ITEMS,
|
||||||
|
type FooterItemId,
|
||||||
|
deriveItemsFromLegacySettings,
|
||||||
|
} from '../../config/footerItems.js';
|
||||||
|
|
||||||
interface CwdIndicatorProps {
|
interface CwdIndicatorProps {
|
||||||
targetDir: string;
|
targetDir: string;
|
||||||
terminalWidth: number;
|
maxWidth: number;
|
||||||
debugMode?: boolean;
|
debugMode?: boolean;
|
||||||
debugMessage?: string;
|
debugMessage?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
@@ -41,21 +42,19 @@ interface CwdIndicatorProps {
|
|||||||
|
|
||||||
const CwdIndicator: React.FC<CwdIndicatorProps> = ({
|
const CwdIndicator: React.FC<CwdIndicatorProps> = ({
|
||||||
targetDir,
|
targetDir,
|
||||||
terminalWidth,
|
maxWidth,
|
||||||
debugMode,
|
debugMode,
|
||||||
debugMessage,
|
debugMessage,
|
||||||
color = theme.text.primary,
|
color = theme.text.primary,
|
||||||
}) => {
|
}) => {
|
||||||
const pathLength = Math.max(20, Math.floor(terminalWidth * 0.25));
|
const debugSuffix = debugMode ? ' ' + (debugMessage || '--debug') : '';
|
||||||
const displayPath = shortenPath(tildeifyPath(targetDir), pathLength);
|
const availableForPath = Math.max(10, maxWidth - debugSuffix.length);
|
||||||
|
const displayPath = shortenPath(tildeifyPath(targetDir), availableForPath);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text color={color}>
|
<Text color={color}>
|
||||||
{displayPath}
|
{displayPath}
|
||||||
{debugMode && (
|
{debugMode && <Text color={theme.status.error}>{debugSuffix}</Text>}
|
||||||
<Text color={theme.status.error}>
|
|
||||||
{' ' + (debugMessage || '--debug')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -63,13 +62,15 @@ const CwdIndicator: React.FC<CwdIndicatorProps> = ({
|
|||||||
interface BranchIndicatorProps {
|
interface BranchIndicatorProps {
|
||||||
branchName: string;
|
branchName: string;
|
||||||
showParentheses?: boolean;
|
showParentheses?: boolean;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BranchIndicator: React.FC<BranchIndicatorProps> = ({
|
const BranchIndicator: React.FC<BranchIndicatorProps> = ({
|
||||||
branchName,
|
branchName,
|
||||||
showParentheses = true,
|
showParentheses = true,
|
||||||
|
color = theme.text.primary,
|
||||||
}) => (
|
}) => (
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={color}>
|
||||||
{showParentheses ? `(${branchName}*)` : `${branchName}*`}
|
{showParentheses ? `(${branchName}*)` : `${branchName}*`}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -100,7 +101,7 @@ const SandboxIndicator: React.FC<SandboxIndicatorProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Text color={theme.status.warning}>
|
<Text color={theme.status.warning}>
|
||||||
macOS Seatbelt{' '}
|
macOS Seatbelt{' '}
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.ui.comment}>
|
||||||
({process.env['SEATBELT_PROFILE']})
|
({process.env['SEATBELT_PROFILE']})
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -141,6 +142,14 @@ function isFooterItemId(id: string): id is FooterItemId {
|
|||||||
return ALL_ITEMS.some((i) => i.id === id);
|
return ALL_ITEMS.some((i) => i.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FooterColumn {
|
||||||
|
id: string;
|
||||||
|
header: string;
|
||||||
|
element: (maxWidth: number) => React.ReactNode;
|
||||||
|
width: number;
|
||||||
|
isHighPriority: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const Footer: React.FC = () => {
|
export const Footer: React.FC = () => {
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
@@ -176,264 +185,213 @@ export const Footer: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const displayVimMode = vimEnabled ? vimMode : undefined;
|
const displayVimMode = vimEnabled ? vimMode : undefined;
|
||||||
|
const items =
|
||||||
|
settings.merged.ui.footer.items ??
|
||||||
|
deriveItemsFromLegacySettings(settings.merged);
|
||||||
|
const showLabels = settings.merged.ui.footer.showLabels !== false;
|
||||||
|
const itemColor = showLabels ? theme.text.primary : theme.ui.comment;
|
||||||
|
|
||||||
const hasCustomItems = settings.merged.ui.footer.items != null;
|
const potentialColumns: FooterColumn[] = [];
|
||||||
|
|
||||||
if (!hasCustomItems) {
|
const addCol = (
|
||||||
const showMemoryUsage =
|
id: string,
|
||||||
config.getDebugMode() || settings.merged.ui.showMemoryUsage;
|
header: string,
|
||||||
const hideCWD = settings.merged.ui.footer.hideCWD;
|
element: (maxWidth: number) => React.ReactNode,
|
||||||
const hideSandboxStatus = settings.merged.ui.footer.hideSandboxStatus;
|
dataWidth: number,
|
||||||
const hideModelInfo = settings.merged.ui.footer.hideModelInfo;
|
isHighPriority = false,
|
||||||
const hideContextPercentage =
|
) => {
|
||||||
settings.merged.ui.footer.hideContextPercentage;
|
potentialColumns.push({
|
||||||
|
id,
|
||||||
const justifyContent =
|
header: showLabels ? header : '',
|
||||||
hideCWD && hideModelInfo ? 'center' : 'space-between';
|
element,
|
||||||
|
width: Math.max(dataWidth, showLabels ? header.length : 0),
|
||||||
const showDebugProfiler = debugMode || isDevelopment;
|
isHighPriority,
|
||||||
|
});
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
justifyContent={justifyContent}
|
|
||||||
width={terminalWidth}
|
|
||||||
flexDirection="row"
|
|
||||||
alignItems="center"
|
|
||||||
paddingX={1}
|
|
||||||
>
|
|
||||||
{(showDebugProfiler || displayVimMode || !hideCWD) && (
|
|
||||||
<Box>
|
|
||||||
{showDebugProfiler && <DebugProfiler />}
|
|
||||||
{displayVimMode && (
|
|
||||||
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
|
|
||||||
)}
|
|
||||||
{!hideCWD && (
|
|
||||||
<Box flexDirection="row">
|
|
||||||
<CwdIndicator
|
|
||||||
targetDir={targetDir}
|
|
||||||
terminalWidth={terminalWidth}
|
|
||||||
debugMode={debugMode}
|
|
||||||
debugMessage={debugMessage}
|
|
||||||
/>
|
|
||||||
{branchName && (
|
|
||||||
<>
|
|
||||||
<Text> </Text>
|
|
||||||
<BranchIndicator branchName={branchName} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Middle Section: Centered Trust/Sandbox Info */}
|
|
||||||
{!hideSandboxStatus && (
|
|
||||||
<Box
|
|
||||||
flexGrow={1}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
display="flex"
|
|
||||||
>
|
|
||||||
<SandboxIndicator
|
|
||||||
isTrustedFolder={isTrustedFolder}
|
|
||||||
terminalWidth={terminalWidth}
|
|
||||||
showDocsHint={true}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Right Section: Gemini Label and Console Summary */}
|
|
||||||
{!hideModelInfo && (
|
|
||||||
<Box alignItems="center" justifyContent="flex-end">
|
|
||||||
<Box alignItems="center">
|
|
||||||
<Text color={theme.text.primary}>
|
|
||||||
<Text color={theme.text.secondary}>/model </Text>
|
|
||||||
{getDisplayString(model)}
|
|
||||||
{!hideContextPercentage && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<ContextUsageDisplay
|
|
||||||
promptTokenCount={promptTokenCount}
|
|
||||||
model={model}
|
|
||||||
terminalWidth={terminalWidth}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{quotaStats && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<QuotaDisplay
|
|
||||||
remaining={quotaStats.remaining}
|
|
||||||
limit={quotaStats.limit}
|
|
||||||
resetTime={quotaStats.resetTime}
|
|
||||||
terse={true}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
{showMemoryUsage && (
|
|
||||||
<>
|
|
||||||
<Text color={theme.text.secondary}> | </Text>
|
|
||||||
<MemoryUsageDisplay />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box alignItems="center">
|
|
||||||
{corgiMode && (
|
|
||||||
<Box paddingLeft={1} flexDirection="row">
|
|
||||||
<Text color={theme.ui.symbol}>| </Text>
|
|
||||||
<CorgiIndicator />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{!showErrorDetails && errorCount > 0 && (
|
|
||||||
<Box paddingLeft={1} flexDirection="row">
|
|
||||||
<Text color={theme.ui.comment}>| </Text>
|
|
||||||
<ErrorIndicator errorCount={errorCount} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items-based rendering path
|
|
||||||
const items = settings.merged.ui.footer.items ?? [];
|
|
||||||
const elements: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
const addElement = (id: string, element: React.ReactNode) => {
|
|
||||||
if (elements.length > 0) {
|
|
||||||
elements.push(
|
|
||||||
<Text key={`sep-${id}`} color={theme.text.secondary}>
|
|
||||||
{' | '}
|
|
||||||
</Text>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
elements.push(<Box key={id}>{element}</Box>);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepend Vim mode if enabled
|
// 1. System Indicators (Far Left, high priority)
|
||||||
|
if (uiState.showDebugProfiler) {
|
||||||
|
addCol('debug', '', () => <DebugProfiler />, 45, true);
|
||||||
|
}
|
||||||
if (displayVimMode) {
|
if (displayVimMode) {
|
||||||
elements.push(
|
const vimStr = `[${displayVimMode}]`;
|
||||||
<Box key="vim-mode-static">
|
addCol(
|
||||||
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
|
'vim',
|
||||||
</Box>,
|
'',
|
||||||
|
() => <Text color={theme.text.accent}>{vimStr}</Text>,
|
||||||
|
vimStr.length,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Main Configurable Items
|
||||||
for (const id of items) {
|
for (const id of items) {
|
||||||
if (!isFooterItemId(id)) {
|
if (!isFooterItemId(id)) continue;
|
||||||
continue;
|
const itemConfig = ALL_ITEMS.find((i) => i.id === id);
|
||||||
}
|
const header = itemConfig?.header ?? id;
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'cwd': {
|
case 'cwd': {
|
||||||
addElement(
|
const fullPath = tildeifyPath(targetDir);
|
||||||
|
const debugSuffix = debugMode ? ' ' + (debugMessage || '--debug') : '';
|
||||||
|
addCol(
|
||||||
id,
|
id,
|
||||||
<CwdIndicator
|
header,
|
||||||
targetDir={targetDir}
|
(maxWidth) => (
|
||||||
terminalWidth={terminalWidth}
|
<CwdIndicator
|
||||||
debugMode={debugMode}
|
targetDir={targetDir}
|
||||||
debugMessage={debugMessage}
|
maxWidth={maxWidth}
|
||||||
color={theme.text.secondary}
|
debugMode={debugMode}
|
||||||
/>,
|
debugMessage={debugMessage}
|
||||||
|
color={itemColor}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
fullPath.length + debugSuffix.length,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'git-branch': {
|
case 'git-branch': {
|
||||||
if (branchName) {
|
if (branchName) {
|
||||||
addElement(
|
const str = `${branchName}*`;
|
||||||
|
addCol(
|
||||||
id,
|
id,
|
||||||
<BranchIndicator branchName={branchName} showParentheses={false} />,
|
header,
|
||||||
|
() => (
|
||||||
|
<BranchIndicator
|
||||||
|
branchName={branchName}
|
||||||
|
showParentheses={false}
|
||||||
|
color={itemColor}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
str.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'sandbox-status': {
|
case 'sandbox-status': {
|
||||||
addElement(
|
let str = 'no sandbox';
|
||||||
|
const sandbox = process.env['SANDBOX'];
|
||||||
|
if (isTrustedFolder === false) str = 'untrusted';
|
||||||
|
else if (sandbox === 'sandbox-exec')
|
||||||
|
str = `macOS Seatbelt (${process.env['SEATBELT_PROFILE']})`;
|
||||||
|
else if (sandbox) str = sandbox.replace(/^gemini-(?:cli-)?/, '');
|
||||||
|
|
||||||
|
addCol(
|
||||||
id,
|
id,
|
||||||
<SandboxIndicator
|
header,
|
||||||
isTrustedFolder={isTrustedFolder}
|
() => (
|
||||||
terminalWidth={terminalWidth}
|
<SandboxIndicator
|
||||||
/>,
|
isTrustedFolder={isTrustedFolder}
|
||||||
|
terminalWidth={terminalWidth}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
str.length,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'model-name': {
|
case 'model-name': {
|
||||||
addElement(
|
const str = getDisplayString(model);
|
||||||
|
addCol(
|
||||||
id,
|
id,
|
||||||
<Text color={theme.text.secondary}>{getDisplayString(model)}</Text>,
|
header,
|
||||||
|
() => <Text color={itemColor}>{str}</Text>,
|
||||||
|
str.length,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'context-remaining': {
|
case 'context-remaining': {
|
||||||
addElement(
|
if (!settings.merged.ui.footer.hideContextPercentage) {
|
||||||
id,
|
addCol(
|
||||||
<ContextUsageDisplay
|
id,
|
||||||
promptTokenCount={promptTokenCount}
|
header,
|
||||||
model={model}
|
() => (
|
||||||
terminalWidth={terminalWidth}
|
<ContextUsageDisplay
|
||||||
/>,
|
promptTokenCount={promptTokenCount}
|
||||||
);
|
model={model}
|
||||||
|
terminalWidth={terminalWidth}
|
||||||
|
color={itemColor}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
10, // "100% left" is 9 chars
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'quota': {
|
case 'quota': {
|
||||||
if (
|
if (quotaStats?.remaining !== undefined && quotaStats.limit) {
|
||||||
quotaStats &&
|
|
||||||
quotaStats.remaining !== undefined &&
|
|
||||||
quotaStats.limit
|
|
||||||
) {
|
|
||||||
const percentage = (quotaStats.remaining / quotaStats.limit) * 100;
|
const percentage = (quotaStats.remaining / quotaStats.limit) * 100;
|
||||||
const color = getStatusColor(percentage, {
|
let color = itemColor;
|
||||||
green: QUOTA_THRESHOLD_HIGH,
|
if (percentage < QUOTA_THRESHOLD_MEDIUM) {
|
||||||
yellow: QUOTA_THRESHOLD_MEDIUM,
|
color = theme.status.error;
|
||||||
});
|
} else if (percentage < QUOTA_THRESHOLD_HIGH) {
|
||||||
|
color = theme.status.warning;
|
||||||
|
}
|
||||||
const text =
|
const text =
|
||||||
quotaStats.remaining === 0
|
quotaStats.remaining === 0
|
||||||
? 'limit reached'
|
? 'limit reached'
|
||||||
: `daily ${percentage.toFixed(0)}%`;
|
: `daily ${percentage.toFixed(0)}%`;
|
||||||
addElement(id, <Text color={color}>{text}</Text>);
|
addCol(
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
() => <Text color={color}>{text}</Text>,
|
||||||
|
text.length,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'memory-usage': {
|
case 'memory-usage': {
|
||||||
addElement(id, <MemoryUsageDisplay />);
|
addCol(id, header, () => <MemoryUsageDisplay color={itemColor} />, 10);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'session-id': {
|
case 'session-id': {
|
||||||
const idShort = uiState.sessionStats.sessionId.slice(0, 8);
|
addCol(
|
||||||
addElement(id, <Text color={theme.text.secondary}>{idShort}</Text>);
|
id,
|
||||||
|
header,
|
||||||
|
() => (
|
||||||
|
<Text color={itemColor}>
|
||||||
|
{uiState.sessionStats.sessionId.slice(0, 8)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
8,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'code-changes': {
|
case 'code-changes': {
|
||||||
const added = uiState.sessionStats.metrics.files.totalLinesAdded;
|
const added = uiState.sessionStats.metrics.files.totalLinesAdded;
|
||||||
const removed = uiState.sessionStats.metrics.files.totalLinesRemoved;
|
const removed = uiState.sessionStats.metrics.files.totalLinesRemoved;
|
||||||
if (added > 0 || removed > 0) {
|
if (added > 0 || removed > 0) {
|
||||||
addElement(
|
const str = `+${added} -${removed}`;
|
||||||
|
addCol(
|
||||||
id,
|
id,
|
||||||
<Text>
|
header,
|
||||||
<Text color={theme.status.success}>+{added}</Text>{' '}
|
() => (
|
||||||
<Text color={theme.status.error}>-{removed}</Text>
|
<Text>
|
||||||
</Text>,
|
<Text color={theme.status.success}>+{added}</Text>{' '}
|
||||||
|
<Text color={theme.status.error}>-{removed}</Text>
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
str.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'token-count': {
|
case 'token-count': {
|
||||||
let totalTokens = 0;
|
let total = 0;
|
||||||
for (const m of Object.values(uiState.sessionStats.metrics.models)) {
|
for (const m of Object.values(uiState.sessionStats.metrics.models))
|
||||||
totalTokens += m.tokens.total;
|
total += m.tokens.total;
|
||||||
}
|
if (total > 0) {
|
||||||
if (totalTokens > 0) {
|
const formatted =
|
||||||
const formatter = new Intl.NumberFormat('en-US', {
|
new Intl.NumberFormat('en-US', {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
});
|
})
|
||||||
const formatted = formatter.format(totalTokens).toLowerCase();
|
.format(total)
|
||||||
addElement(
|
.toLowerCase() + ' tokens';
|
||||||
|
addCol(
|
||||||
id,
|
id,
|
||||||
<Text color={theme.text.secondary}>{formatted} tokens</Text>,
|
header,
|
||||||
|
() => <Text color={itemColor}>{formatted}</Text>,
|
||||||
|
formatted.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -444,14 +402,88 @@ export const Footer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (corgiMode) {
|
// 3. Transients
|
||||||
addElement('corgi-transient', <CorgiIndicator />);
|
if (corgiMode) addCol('corgi', '', () => <CorgiIndicator />, 5);
|
||||||
|
if (!showErrorDetails && errorCount > 0) {
|
||||||
|
addCol(
|
||||||
|
'error-count',
|
||||||
|
'',
|
||||||
|
() => <ErrorIndicator errorCount={errorCount} />,
|
||||||
|
12,
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showErrorDetails && errorCount > 0) {
|
// --- Width Fitting Logic ---
|
||||||
addElement(
|
const COLUMN_GAP = 3;
|
||||||
'error-count-transient',
|
let currentWidth = 2; // Initial padding
|
||||||
<ErrorIndicator errorCount={errorCount} />,
|
const columnsToRender: FooterColumn[] = [];
|
||||||
|
let droppedAny = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < potentialColumns.length; i++) {
|
||||||
|
const col = potentialColumns[i];
|
||||||
|
const gap = columnsToRender.length > 0 ? (showLabels ? COLUMN_GAP : 3) : 0; // Use 3 for dot separator width
|
||||||
|
const budgetWidth = col.id === 'cwd' ? 20 : col.width;
|
||||||
|
|
||||||
|
if (
|
||||||
|
col.isHighPriority ||
|
||||||
|
currentWidth + gap + budgetWidth <= terminalWidth - 2
|
||||||
|
) {
|
||||||
|
columnsToRender.push(col);
|
||||||
|
currentWidth += gap + budgetWidth;
|
||||||
|
} else {
|
||||||
|
droppedAny = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBudgeted = columnsToRender.reduce(
|
||||||
|
(sum, c, idx) =>
|
||||||
|
sum +
|
||||||
|
(c.id === 'cwd' ? 20 : c.width) +
|
||||||
|
(idx > 0 ? (showLabels ? COLUMN_GAP : 3) : 0),
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
const excessSpace = Math.max(0, terminalWidth - totalBudgeted);
|
||||||
|
|
||||||
|
const finalElements: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
columnsToRender.forEach((col, idx) => {
|
||||||
|
if (idx > 0 && !showLabels) {
|
||||||
|
finalElements.push(
|
||||||
|
<Box key={`sep-${col.id}`} height={1}>
|
||||||
|
<Text color={theme.ui.comment}> · </Text>
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidth = col.id === 'cwd' ? 20 + excessSpace : col.width;
|
||||||
|
finalElements.push(
|
||||||
|
<Box key={col.id} flexDirection="column">
|
||||||
|
{showLabels && (
|
||||||
|
<Box height={1}>
|
||||||
|
<Text color={theme.ui.comment}>{col.header}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box height={1}>{col.element(maxWidth)}</Box>
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (droppedAny) {
|
||||||
|
if (!showLabels) {
|
||||||
|
finalElements.push(
|
||||||
|
<Box key="sep-ellipsis" height={1}>
|
||||||
|
<Text color={theme.ui.comment}> · </Text>
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finalElements.push(
|
||||||
|
<Box key="ellipsis" flexDirection="column">
|
||||||
|
{showLabels && <Box height={1} />}
|
||||||
|
<Box height={1}>
|
||||||
|
<Text color={theme.ui.comment}>…</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,12 +491,12 @@ export const Footer: React.FC = () => {
|
|||||||
<Box
|
<Box
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
alignItems="center"
|
|
||||||
paddingX={1}
|
paddingX={1}
|
||||||
flexWrap="nowrap"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
|
flexWrap="nowrap"
|
||||||
|
columnGap={showLabels ? COLUMN_GAP : 0}
|
||||||
>
|
>
|
||||||
{elements}
|
{finalElements}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ describe('<FooterConfigDialog />', () => {
|
|||||||
|
|
||||||
// Initial order: cwd, git-branch, ...
|
// Initial order: cwd, git-branch, ...
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
const cwdIdx = output!.indexOf('cwd');
|
const cwdIdx = output!.indexOf('] cwd');
|
||||||
const branchIdx = output!.indexOf('git-branch');
|
const branchIdx = output!.indexOf('] git-branch');
|
||||||
|
expect(cwdIdx).toBeGreaterThan(-1);
|
||||||
|
expect(branchIdx).toBeGreaterThan(-1);
|
||||||
expect(cwdIdx).toBeLessThan(branchIdx);
|
expect(cwdIdx).toBeLessThan(branchIdx);
|
||||||
|
|
||||||
// Move cwd down (right arrow)
|
// Move cwd down (right arrow)
|
||||||
@@ -94,8 +96,10 @@ describe('<FooterConfigDialog />', () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const outputAfter = lastFrame();
|
const outputAfter = lastFrame();
|
||||||
const cwdIdxAfter = outputAfter!.indexOf('cwd');
|
const cwdIdxAfter = outputAfter!.indexOf('] cwd');
|
||||||
const branchIdxAfter = outputAfter!.indexOf('git-branch');
|
const branchIdxAfter = outputAfter!.indexOf('] git-branch');
|
||||||
|
expect(cwdIdxAfter).toBeGreaterThan(-1);
|
||||||
|
expect(branchIdxAfter).toBeGreaterThan(-1);
|
||||||
expect(branchIdxAfter).toBeLessThan(cwdIdxAfter);
|
expect(branchIdxAfter).toBeLessThan(cwdIdxAfter);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -142,7 +146,7 @@ describe('<FooterConfigDialog />', () => {
|
|||||||
{ settings },
|
{ settings },
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
act(() => {
|
act(() => {
|
||||||
stdin.write('\r'); // Toggle (deselect)
|
stdin.write('\r'); // Toggle (deselect)
|
||||||
stdin.write('\u001b[B'); // Down arrow
|
stdin.write('\u001b[B'); // Down arrow
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
|||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||||
import { TextInput } from './shared/TextInput.js';
|
import { TextInput } from './shared/TextInput.js';
|
||||||
import { useFuzzyList } from '../hooks/useFuzzyList.js';
|
import { useFuzzyList } from '../hooks/useFuzzyList.js';
|
||||||
|
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||||
import {
|
import {
|
||||||
ALL_ITEMS,
|
ALL_ITEMS,
|
||||||
DEFAULT_ORDER,
|
DEFAULT_ORDER,
|
||||||
@@ -55,7 +56,7 @@ function footerConfigReducer(
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'MOVE_UP': {
|
case 'MOVE_UP': {
|
||||||
const { filteredCount, maxToShow } = action;
|
const { filteredCount, maxToShow } = action;
|
||||||
const totalSlots = filteredCount + 1;
|
const totalSlots = filteredCount + 2; // +1 for showLabels, +1 for reset
|
||||||
const newIndex =
|
const newIndex =
|
||||||
state.activeIndex > 0 ? state.activeIndex - 1 : totalSlots - 1;
|
state.activeIndex > 0 ? state.activeIndex - 1 : totalSlots - 1;
|
||||||
let newOffset = state.scrollOffset;
|
let newOffset = state.scrollOffset;
|
||||||
@@ -71,7 +72,7 @@ function footerConfigReducer(
|
|||||||
}
|
}
|
||||||
case 'MOVE_DOWN': {
|
case 'MOVE_DOWN': {
|
||||||
const { filteredCount, maxToShow } = action;
|
const { filteredCount, maxToShow } = action;
|
||||||
const totalSlots = filteredCount + 1;
|
const totalSlots = filteredCount + 2;
|
||||||
const newIndex =
|
const newIndex =
|
||||||
state.activeIndex < totalSlots - 1 ? state.activeIndex + 1 : 0;
|
state.activeIndex < totalSlots - 1 ? state.activeIndex + 1 : 0;
|
||||||
let newOffset = state.scrollOffset;
|
let newOffset = state.scrollOffset;
|
||||||
@@ -108,8 +109,8 @@ function footerConfigReducer(
|
|||||||
return { ...state, orderedIds: newOrderedIds, activeIndex: newIndex };
|
return { ...state, orderedIds: newOrderedIds, activeIndex: newIndex };
|
||||||
}
|
}
|
||||||
case 'TOGGLE_ITEM': {
|
case 'TOGGLE_ITEM': {
|
||||||
const isResetFocused = state.activeIndex === action.filteredItems.length;
|
const isSystemFocused = state.activeIndex >= action.filteredItems.length;
|
||||||
if (isResetFocused) return state; // Handled by separate effect/callback if needed, or we can add a RESET_DEFAULTS action
|
if (isSystemFocused) return state;
|
||||||
|
|
||||||
const item = action.filteredItems[state.activeIndex];
|
const item = action.filteredItems[state.activeIndex];
|
||||||
if (!item) return state;
|
if (!item) return state;
|
||||||
@@ -209,7 +210,8 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
dispatch({ type: 'RESET_INDEX' });
|
dispatch({ type: 'RESET_INDEX' });
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
const isResetFocused = activeIndex === filteredItems.length;
|
const isResetFocused = activeIndex === filteredItems.length + 1;
|
||||||
|
const isShowLabelsFocused = activeIndex === filteredItems.length;
|
||||||
|
|
||||||
const handleResetToDefaults = useCallback(() => {
|
const handleResetToDefaults = useCallback(() => {
|
||||||
setSetting(SettingScope.User, 'ui.footer.items', undefined);
|
setSetting(SettingScope.User, 'ui.footer.items', undefined);
|
||||||
@@ -231,6 +233,11 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
});
|
});
|
||||||
}, [setSetting, settings.merged]);
|
}, [setSetting, settings.merged]);
|
||||||
|
|
||||||
|
const handleToggleLabels = useCallback(() => {
|
||||||
|
const current = settings.merged.ui.footer.showLabels !== false;
|
||||||
|
setSetting(SettingScope.User, 'ui.footer.showLabels', !current);
|
||||||
|
}, [setSetting, settings.merged.ui.footer.showLabels]);
|
||||||
|
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key: Key) => {
|
(key: Key) => {
|
||||||
if (keyMatchers[Command.ESCAPE](key)) {
|
if (keyMatchers[Command.ESCAPE](key)) {
|
||||||
@@ -269,6 +276,8 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
if (keyMatchers[Command.RETURN](key)) {
|
if (keyMatchers[Command.RETURN](key)) {
|
||||||
if (isResetFocused) {
|
if (isResetFocused) {
|
||||||
handleResetToDefaults();
|
handleResetToDefaults();
|
||||||
|
} else if (isShowLabelsFocused) {
|
||||||
|
handleToggleLabels();
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: 'TOGGLE_ITEM', filteredItems });
|
dispatch({ type: 'TOGGLE_ITEM', filteredItems });
|
||||||
}
|
}
|
||||||
@@ -286,12 +295,13 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const activeId = filteredItems[activeIndex]?.key;
|
const activeId = filteredItems[activeIndex]?.key;
|
||||||
|
const showLabels = settings.merged.ui.footer.showLabels !== false;
|
||||||
|
|
||||||
// Preview logic
|
// Preview logic
|
||||||
const previewText = useMemo(() => {
|
const previewContent = useMemo(() => {
|
||||||
if (isResetFocused) {
|
if (isResetFocused) {
|
||||||
return (
|
return (
|
||||||
<Text color={theme.text.secondary} italic>
|
<Text color={theme.ui.comment} italic>
|
||||||
Default footer (uses legacy settings)
|
Default footer (uses legacy settings)
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -302,53 +312,103 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
);
|
);
|
||||||
if (itemsToPreview.length === 0) return null;
|
if (itemsToPreview.length === 0) return null;
|
||||||
|
|
||||||
|
const itemColor = showLabels ? theme.text.primary : theme.ui.comment;
|
||||||
const getColor = (id: string, defaultColor?: string) =>
|
const getColor = (id: string, defaultColor?: string) =>
|
||||||
id === activeId ? 'white' : defaultColor || theme.text.secondary;
|
id === activeId ? 'white' : defaultColor || itemColor;
|
||||||
|
|
||||||
// Mock values for preview
|
// Mock values for preview
|
||||||
const mockValues: Record<string, React.ReactNode> = {
|
const mockValues: Record<
|
||||||
cwd: <Text color={getColor('cwd')}>~/project/path</Text>,
|
string,
|
||||||
'git-branch': <Text color={getColor('git-branch')}>main*</Text>,
|
{ header: string; data: React.ReactNode }
|
||||||
'sandbox-status': (
|
> = {
|
||||||
<Text color={getColor('sandbox-status', 'green')}>docker</Text>
|
cwd: {
|
||||||
),
|
header: 'Path',
|
||||||
'model-name': (
|
data: <Text color={getColor('cwd', itemColor)}>~/project/path</Text>,
|
||||||
<Box flexDirection="row">
|
},
|
||||||
<Text color={getColor('model-name')}>gemini-2.5-pro</Text>
|
'git-branch': {
|
||||||
</Box>
|
header: 'Branch',
|
||||||
),
|
data: <Text color={getColor('git-branch', itemColor)}>main*</Text>,
|
||||||
'context-remaining': (
|
},
|
||||||
<Text color={getColor('context-remaining')}>85% context left</Text>
|
'sandbox-status': {
|
||||||
),
|
header: '/docs',
|
||||||
quota: <Text color={getColor('quota')}>daily 97%</Text>,
|
data: <Text color={getColor('sandbox-status', 'green')}>docker</Text>,
|
||||||
'memory-usage': <Text color={getColor('memory-usage')}>124MB</Text>,
|
},
|
||||||
'session-id': <Text color={getColor('session-id')}>769992f9</Text>,
|
'model-name': {
|
||||||
'code-changes': (
|
header: '/model',
|
||||||
<Box flexDirection="row">
|
data: (
|
||||||
<Text color={getColor('code-changes', theme.status.success)}>
|
<Text color={getColor('model-name', itemColor)}>gemini-2.5-pro</Text>
|
||||||
+12
|
),
|
||||||
</Text>
|
},
|
||||||
<Text color={getColor('code-changes')}> </Text>
|
'context-remaining': {
|
||||||
<Text color={getColor('code-changes', theme.status.error)}>-4</Text>
|
header: 'Context',
|
||||||
</Box>
|
data: (
|
||||||
),
|
<Text color={getColor('context-remaining', itemColor)}>85% left</Text>
|
||||||
'token-count': <Text color={getColor('token-count')}>1.5k tokens</Text>,
|
),
|
||||||
|
},
|
||||||
|
quota: {
|
||||||
|
header: '/stats',
|
||||||
|
data: <Text color={getColor('quota', itemColor)}>daily 97%</Text>,
|
||||||
|
},
|
||||||
|
'memory-usage': {
|
||||||
|
header: 'Memory',
|
||||||
|
data: <MemoryUsageDisplay color={itemColor} />,
|
||||||
|
},
|
||||||
|
'session-id': {
|
||||||
|
header: 'Session',
|
||||||
|
data: <Text color={getColor('session-id', itemColor)}>769992f9</Text>,
|
||||||
|
},
|
||||||
|
'code-changes': {
|
||||||
|
header: 'Diff',
|
||||||
|
data: (
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Text color={getColor('code-changes', theme.status.success)}>
|
||||||
|
+12
|
||||||
|
</Text>
|
||||||
|
<Text color={getColor('code-changes')}> </Text>
|
||||||
|
<Text color={getColor('code-changes', theme.status.error)}>-4</Text>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'token-count': {
|
||||||
|
header: 'Tokens',
|
||||||
|
data: (
|
||||||
|
<Text color={getColor('token-count', itemColor)}>1.5k tokens</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const elements: React.ReactNode[] = [];
|
const previewElements: React.ReactNode[] = [];
|
||||||
|
|
||||||
itemsToPreview.forEach((id: string, idx: number) => {
|
itemsToPreview.forEach((id: string, idx: number) => {
|
||||||
if (idx > 0) {
|
const mock = mockValues[id];
|
||||||
elements.push(
|
if (!mock) return;
|
||||||
<Text key={`sep-${id}`} color={theme.text.secondary}>
|
|
||||||
{' | '}
|
if (idx > 0 && !showLabels) {
|
||||||
</Text>,
|
previewElements.push(
|
||||||
|
<Box key={`sep-${id}`} height={1}>
|
||||||
|
<Text color={theme.ui.comment}> · </Text>
|
||||||
|
</Box>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
elements.push(<Box key={id}>{mockValues[id] || id}</Box>);
|
|
||||||
|
previewElements.push(
|
||||||
|
<Box key={id} flexDirection="column">
|
||||||
|
{showLabels && (
|
||||||
|
<Box height={1}>
|
||||||
|
<Text color={theme.ui.comment}>{mock.header}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box height={1}>{mock.data}</Box>
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return elements;
|
return (
|
||||||
}, [orderedIds, selectedIds, activeId, isResetFocused]);
|
<Box flexDirection="row" flexWrap="nowrap" columnGap={showLabels ? 3 : 0}>
|
||||||
|
{previewElements}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}, [orderedIds, selectedIds, activeId, isResetFocused, showLabels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -403,13 +463,25 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1} flexDirection="column">
|
||||||
<Text
|
<Box flexDirection="row">
|
||||||
color={isResetFocused ? theme.status.warning : theme.text.secondary}
|
<Text color={isShowLabelsFocused ? theme.status.success : undefined}>
|
||||||
>
|
{isShowLabelsFocused ? '> ' : ' '}
|
||||||
{isResetFocused ? '> ' : ' '}
|
</Text>
|
||||||
Reset to default footer
|
<Text color={isShowLabelsFocused ? theme.status.success : undefined}>
|
||||||
</Text>
|
[{showLabels ? '✓' : ' '}] Show footer labels
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Text color={isResetFocused ? theme.status.warning : undefined}>
|
||||||
|
{isResetFocused ? '> ' : ' '}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
color={isResetFocused ? theme.status.warning : theme.text.secondary}
|
||||||
|
>
|
||||||
|
Reset to default footer
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
<Box marginTop={1} flexDirection="column">
|
||||||
@@ -431,7 +503,7 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
>
|
>
|
||||||
<Text bold>Preview:</Text>
|
<Text bold>Preview:</Text>
|
||||||
<Box flexDirection="row">{previewText}</Box>
|
<Box flexDirection="row">{previewContent}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,26 +11,24 @@ import { theme } from '../semantic-colors.js';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { formatBytes } from '../utils/formatters.js';
|
import { formatBytes } from '../utils/formatters.js';
|
||||||
|
|
||||||
export const MemoryUsageDisplay: React.FC = () => {
|
export const MemoryUsageDisplay: React.FC<{ color?: string }> = ({
|
||||||
|
color = theme.text.primary,
|
||||||
|
}) => {
|
||||||
const [memoryUsage, setMemoryUsage] = useState<string>('');
|
const [memoryUsage, setMemoryUsage] = useState<string>('');
|
||||||
const [memoryUsageColor, setMemoryUsageColor] = useState<string>(
|
const [memoryUsageColor, setMemoryUsageColor] = useState<string>(color);
|
||||||
theme.text.secondary,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateMemory = () => {
|
const updateMemory = () => {
|
||||||
const usage = process.memoryUsage().rss;
|
const usage = process.memoryUsage().rss;
|
||||||
setMemoryUsage(formatBytes(usage));
|
setMemoryUsage(formatBytes(usage));
|
||||||
setMemoryUsageColor(
|
setMemoryUsageColor(
|
||||||
usage >= 2 * 1024 * 1024 * 1024
|
usage >= 2 * 1024 * 1024 * 1024 ? theme.status.error : color,
|
||||||
? theme.status.error
|
|
||||||
: theme.text.secondary,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const intervalId = setInterval(updateMemory, 2000);
|
const intervalId = setInterval(updateMemory, 2000);
|
||||||
updateMemory(); // Initial update
|
updateMemory(); // Initial update
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, []);
|
}, [color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<Footer /> > displays "Limit reached" message when remaining is 0 1`] = `
|
exports[`<Footer /> > displays "Limit reached" message when remaining is 0 1`] = `
|
||||||
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro Limit reached
|
" Path /docs /model /stats
|
||||||
"
|
/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro limit reached"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
|
exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
|
||||||
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 15%
|
" Path /docs /model /stats
|
||||||
"
|
/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro daily 15%"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
|
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
|
||||||
" ...s/to/make/it/long no sandbox /model gemini-pro 100%
|
" Path /docs /model Context
|
||||||
"
|
/Users/.../directories/to/make/it/long no sandbox gemini-pro 100%"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
|
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
|
||||||
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 100% context left
|
" Path /docs /model Context
|
||||||
"
|
/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 100% left"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `
|
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `
|
||||||
" no sandbox (see /docs)
|
" /docs
|
||||||
"
|
no sandbox"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
|
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
|
||||||
|
|
||||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `
|
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `
|
||||||
" ...directories/to/make/it/long no sandbox (see /docs)
|
" Path /docs
|
||||||
"
|
/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Footer /> > hides the usage indicator when usage is not near limit 1`] = `
|
exports[`<Footer /> > hides the usage indicator when usage is not near limit 1`] = `
|
||||||
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro
|
" Path /docs /model /stats
|
||||||
"
|
/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro daily 85%"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -22,13 +22,15 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 1`] =
|
|||||||
│ [ ] code-changes Lines added/removed in the session │
|
│ [ ] code-changes Lines added/removed in the session │
|
||||||
│ [ ] token-count Total tokens used in the session │
|
│ [ ] token-count Total tokens used in the session │
|
||||||
│ │
|
│ │
|
||||||
|
│ [✓] Show footer labels │
|
||||||
│ Reset to default footer │
|
│ Reset to default footer │
|
||||||
│ │
|
│ │
|
||||||
│ ↑/↓ navigate · ←/→ reorder · enter select · esc close │
|
│ ↑/↓ navigate · ←/→ reorder · enter select · esc close │
|
||||||
│ │
|
│ │
|
||||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ Preview: │ │
|
│ │ Preview: │ │
|
||||||
│ │ ~/project/path | main* | docker | gemini-2.5-pro | daily 97% │ │
|
│ │ Path Branch /docs /model /stats │ │
|
||||||
|
│ │ ~/project/path main* docker gemini-2.5-pro daily 97% │ │
|
||||||
│ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
|||||||
Reference in New Issue
Block a user