2025-04-18 17:44:24 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* @license
|
2026-02-09 21:53:10 -05:00
|
|
|
|
* Copyright 2026 Google LLC
|
2025-04-18 17:44:24 -07:00
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type React from 'react';
|
2026-03-31 12:10:13 -04:00
|
|
|
|
import { useState, useEffect } from 'react';
|
2025-04-15 21:41:08 -07:00
|
|
|
|
import { Box, Text } from 'ink';
|
2025-08-07 16:11:35 -07:00
|
|
|
|
import { theme } from '../semantic-colors.js';
|
2025-12-17 09:43:21 -08:00
|
|
|
|
import {
|
|
|
|
|
|
shortenPath,
|
|
|
|
|
|
tildeifyPath,
|
|
|
|
|
|
getDisplayString,
|
2026-03-04 21:21:48 -05:00
|
|
|
|
checkExhaustive,
|
2026-03-31 12:10:13 -04:00
|
|
|
|
AuthType,
|
|
|
|
|
|
UserAccountManager,
|
2025-12-17 09:43:21 -08:00
|
|
|
|
} from '@google/gemini-cli-core';
|
2025-05-22 10:36:44 -07:00
|
|
|
|
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
2025-05-30 22:18:01 +00:00
|
|
|
|
import process from 'node:process';
|
|
|
|
|
|
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
2025-08-07 11:16:47 -07:00
|
|
|
|
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
2026-02-09 21:53:10 -05:00
|
|
|
|
import { QuotaDisplay } from './QuotaDisplay.js';
|
2025-07-30 17:43:11 -07:00
|
|
|
|
import { DebugProfiler } from './DebugProfiler.js';
|
2025-09-26 21:27:00 -04:00
|
|
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
|
|
|
|
|
import { useConfig } from '../contexts/ConfigContext.js';
|
|
|
|
|
|
import { useSettings } from '../contexts/SettingsContext.js';
|
|
|
|
|
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
2026-03-04 21:21:48 -05:00
|
|
|
|
import {
|
|
|
|
|
|
ALL_ITEMS,
|
|
|
|
|
|
type FooterItemId,
|
|
|
|
|
|
deriveItemsFromLegacySettings,
|
|
|
|
|
|
} from '../../config/footerItems.js';
|
|
|
|
|
|
import { isDevelopment } from '../../utils/installationInfo.js';
|
|
|
|
|
|
|
|
|
|
|
|
interface CwdIndicatorProps {
|
|
|
|
|
|
targetDir: string;
|
|
|
|
|
|
maxWidth: number;
|
|
|
|
|
|
debugMode?: boolean;
|
|
|
|
|
|
debugMessage?: string;
|
|
|
|
|
|
color?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const CwdIndicator: React.FC<CwdIndicatorProps> = ({
|
|
|
|
|
|
targetDir,
|
|
|
|
|
|
maxWidth,
|
|
|
|
|
|
debugMode,
|
|
|
|
|
|
debugMessage,
|
|
|
|
|
|
color = theme.text.primary,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const debugSuffix = debugMode ? ' ' + (debugMessage || '--debug') : '';
|
|
|
|
|
|
const availableForPath = Math.max(10, maxWidth - debugSuffix.length);
|
|
|
|
|
|
const displayPath = shortenPath(tildeifyPath(targetDir), availableForPath);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Text color={color}>
|
|
|
|
|
|
{displayPath}
|
|
|
|
|
|
{debugMode && <Text color={theme.status.error}>{debugSuffix}</Text>}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
interface SandboxIndicatorProps {
|
|
|
|
|
|
isTrustedFolder: boolean | undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SandboxIndicator: React.FC<SandboxIndicatorProps> = ({
|
|
|
|
|
|
isTrustedFolder,
|
|
|
|
|
|
}) => {
|
2026-04-02 22:22:21 -07:00
|
|
|
|
const config = useConfig();
|
|
|
|
|
|
const sandboxEnabled = config.getSandboxEnabled();
|
2026-03-04 21:21:48 -05:00
|
|
|
|
if (isTrustedFolder === false) {
|
|
|
|
|
|
return <Text color={theme.status.warning}>untrusted</Text>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sandbox = process.env['SANDBOX'];
|
2026-04-02 22:22:21 -07:00
|
|
|
|
if (sandbox) {
|
|
|
|
|
|
return <Text color={theme.status.warning}>current process</Text>;
|
2026-03-04 21:21:48 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 22:22:21 -07:00
|
|
|
|
if (sandboxEnabled) {
|
|
|
|
|
|
return <Text color={theme.status.warning}>all tools</Text>;
|
2026-03-04 21:21:48 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <Text color={theme.status.error}>no sandbox</Text>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const CorgiIndicator: React.FC = () => (
|
|
|
|
|
|
<Text>
|
|
|
|
|
|
<Text color={theme.status.error}>▼</Text>
|
|
|
|
|
|
<Text color={theme.text.primary}>(´</Text>
|
|
|
|
|
|
<Text color={theme.status.error}>ᴥ</Text>
|
|
|
|
|
|
<Text color={theme.text.primary}>`)</Text>
|
|
|
|
|
|
<Text color={theme.status.error}>▼</Text>
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
export interface FooterRowItem {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
header: string;
|
|
|
|
|
|
element: React.ReactNode;
|
2026-03-08 00:36:54 -08:00
|
|
|
|
flexGrow?: number;
|
|
|
|
|
|
flexShrink?: number;
|
|
|
|
|
|
isFocused?: boolean;
|
2026-03-10 01:07:26 -07:00
|
|
|
|
alignItems?: 'flex-start' | 'center' | 'flex-end';
|
2026-03-04 21:21:48 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const COLUMN_GAP = 3;
|
|
|
|
|
|
|
|
|
|
|
|
export const FooterRow: React.FC<{
|
|
|
|
|
|
items: FooterRowItem[];
|
|
|
|
|
|
showLabels: boolean;
|
|
|
|
|
|
}> = ({ items, showLabels }) => {
|
|
|
|
|
|
const elements: React.ReactNode[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item, idx) => {
|
2026-03-10 01:07:26 -07:00
|
|
|
|
if (idx > 0) {
|
2026-03-04 21:21:48 -05:00
|
|
|
|
elements.push(
|
2026-03-10 01:07:26 -07:00
|
|
|
|
<Box
|
|
|
|
|
|
key={`sep-${item.key}`}
|
|
|
|
|
|
flexGrow={1}
|
|
|
|
|
|
flexShrink={1}
|
|
|
|
|
|
minWidth={showLabels ? COLUMN_GAP : 3}
|
|
|
|
|
|
justifyContent="center"
|
|
|
|
|
|
alignItems="center"
|
|
|
|
|
|
>
|
|
|
|
|
|
{!showLabels && <Text color={theme.ui.comment}> · </Text>}
|
2026-03-04 21:21:48 -05:00
|
|
|
|
</Box>,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
elements.push(
|
2026-03-08 00:36:54 -08:00
|
|
|
|
<Box
|
|
|
|
|
|
key={item.key}
|
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
|
flexGrow={item.flexGrow ?? 0}
|
|
|
|
|
|
flexShrink={item.flexShrink ?? 1}
|
2026-03-10 01:07:26 -07:00
|
|
|
|
alignItems={item.alignItems}
|
2026-03-08 00:36:54 -08:00
|
|
|
|
backgroundColor={item.isFocused ? theme.background.focus : undefined}
|
|
|
|
|
|
>
|
2026-03-04 21:21:48 -05:00
|
|
|
|
{showLabels && (
|
|
|
|
|
|
<Box height={1}>
|
2026-03-08 00:36:54 -08:00
|
|
|
|
<Text
|
|
|
|
|
|
color={item.isFocused ? theme.text.primary : theme.ui.comment}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.header}
|
|
|
|
|
|
</Text>
|
2026-03-04 21:21:48 -05:00
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Box height={1}>{item.element}</Box>
|
|
|
|
|
|
</Box>,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-10 01:07:26 -07:00
|
|
|
|
<Box flexDirection="row" flexWrap="nowrap" width="100%">
|
2026-03-04 21:21:48 -05:00
|
|
|
|
{elements}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function isFooterItemId(id: string): id is FooterItemId {
|
|
|
|
|
|
return ALL_ITEMS.some((i) => i.id === id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface FooterColumn {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
header: string;
|
|
|
|
|
|
element: (maxWidth: number) => React.ReactNode;
|
|
|
|
|
|
width: number;
|
|
|
|
|
|
isHighPriority: boolean;
|
|
|
|
|
|
}
|
2025-09-26 21:27:00 -04:00
|
|
|
|
|
2026-04-02 02:54:51 -04:00
|
|
|
|
export const Footer: React.FC = () => {
|
2025-09-26 21:27:00 -04:00
|
|
|
|
const uiState = useUIState();
|
|
|
|
|
|
const config = useConfig();
|
|
|
|
|
|
const settings = useSettings();
|
|
|
|
|
|
const { vimEnabled, vimMode } = useVimMode();
|
|
|
|
|
|
|
2026-03-31 12:10:13 -04:00
|
|
|
|
const authType = config.getContentGeneratorConfig()?.authType;
|
|
|
|
|
|
const [email, setEmail] = useState<string | undefined>();
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (authType) {
|
|
|
|
|
|
const userAccountManager = new UserAccountManager();
|
|
|
|
|
|
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setEmail(undefined);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [authType]);
|
|
|
|
|
|
|
2025-09-26 21:27:00 -04:00
|
|
|
|
const {
|
|
|
|
|
|
model,
|
|
|
|
|
|
targetDir,
|
|
|
|
|
|
debugMode,
|
|
|
|
|
|
branchName,
|
|
|
|
|
|
debugMessage,
|
|
|
|
|
|
corgiMode,
|
|
|
|
|
|
errorCount,
|
|
|
|
|
|
showErrorDetails,
|
|
|
|
|
|
promptTokenCount,
|
|
|
|
|
|
isTrustedFolder,
|
2026-01-26 15:23:54 -08:00
|
|
|
|
terminalWidth,
|
2026-02-09 21:53:10 -05:00
|
|
|
|
quotaStats,
|
2025-09-26 21:27:00 -04:00
|
|
|
|
} = {
|
2025-10-27 15:33:12 -07:00
|
|
|
|
model: uiState.currentModel,
|
2025-09-26 21:27:00 -04:00
|
|
|
|
targetDir: config.getTargetDir(),
|
|
|
|
|
|
debugMode: config.getDebugMode(),
|
|
|
|
|
|
branchName: uiState.branchName,
|
|
|
|
|
|
debugMessage: uiState.debugMessage,
|
|
|
|
|
|
corgiMode: uiState.corgiMode,
|
|
|
|
|
|
errorCount: uiState.errorCount,
|
|
|
|
|
|
showErrorDetails: uiState.showErrorDetails,
|
|
|
|
|
|
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
|
|
|
|
|
isTrustedFolder: uiState.isTrustedFolder,
|
2026-01-26 15:23:54 -08:00
|
|
|
|
terminalWidth: uiState.terminalWidth,
|
2026-02-09 21:53:10 -05:00
|
|
|
|
quotaStats: uiState.quota.stats,
|
2025-09-26 21:27:00 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-27 14:15:10 -05:00
|
|
|
|
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
|
|
|
|
|
|
const showErrorSummary =
|
2026-03-02 20:32:50 -08:00
|
|
|
|
!showErrorDetails &&
|
|
|
|
|
|
errorCount > 0 &&
|
|
|
|
|
|
(isFullErrorVerbosity || debugMode || isDevelopment);
|
2026-03-04 21:21:48 -05:00
|
|
|
|
const displayVimMode = vimEnabled ? vimMode : undefined;
|
2026-03-31 12:10:13 -04:00
|
|
|
|
|
2026-03-04 21:21:48 -05:00
|
|
|
|
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;
|
2025-04-15 21:41:08 -07:00
|
|
|
|
|
2026-03-04 21:21:48 -05:00
|
|
|
|
const potentialColumns: FooterColumn[] = [];
|
2025-06-15 11:15:53 -07:00
|
|
|
|
|
2026-03-04 21:21:48 -05:00
|
|
|
|
const addCol = (
|
|
|
|
|
|
id: string,
|
|
|
|
|
|
header: string,
|
|
|
|
|
|
element: (maxWidth: number) => React.ReactNode,
|
|
|
|
|
|
dataWidth: number,
|
|
|
|
|
|
isHighPriority = false,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
potentialColumns.push({
|
|
|
|
|
|
id,
|
|
|
|
|
|
header: showLabels ? header : '',
|
|
|
|
|
|
element,
|
|
|
|
|
|
width: Math.max(dataWidth, showLabels ? header.length : 0),
|
|
|
|
|
|
isHighPriority,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-09-11 20:16:09 -04:00
|
|
|
|
|
2026-03-04 21:21:48 -05:00
|
|
|
|
// 1. System Indicators (Far Left, high priority)
|
|
|
|
|
|
if (uiState.showDebugProfiler) {
|
|
|
|
|
|
addCol('debug', '', () => <DebugProfiler />, 45, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (displayVimMode) {
|
|
|
|
|
|
const vimStr = `[${displayVimMode}]`;
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
'vim',
|
|
|
|
|
|
'',
|
|
|
|
|
|
() => <Text color={theme.text.accent}>{vimStr}</Text>,
|
|
|
|
|
|
vimStr.length,
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-10-07 10:28:35 -07:00
|
|
|
|
|
2026-03-04 21:21:48 -05:00
|
|
|
|
// 2. Main Configurable Items
|
|
|
|
|
|
for (const id of items) {
|
|
|
|
|
|
if (!isFooterItemId(id)) continue;
|
|
|
|
|
|
const itemConfig = ALL_ITEMS.find((i) => i.id === id);
|
|
|
|
|
|
const header = itemConfig?.header ?? id;
|
|
|
|
|
|
|
|
|
|
|
|
switch (id) {
|
|
|
|
|
|
case 'workspace': {
|
|
|
|
|
|
const fullPath = tildeifyPath(targetDir);
|
|
|
|
|
|
const debugSuffix = debugMode ? ' ' + (debugMessage || '--debug') : '';
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
(maxWidth) => (
|
|
|
|
|
|
<CwdIndicator
|
|
|
|
|
|
targetDir={targetDir}
|
|
|
|
|
|
maxWidth={maxWidth}
|
|
|
|
|
|
debugMode={debugMode}
|
|
|
|
|
|
debugMessage={debugMessage}
|
|
|
|
|
|
color={itemColor}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
fullPath.length + debugSuffix.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'git-branch': {
|
|
|
|
|
|
if (branchName) {
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => <Text color={itemColor}>{branchName}</Text>,
|
|
|
|
|
|
branchName.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'sandbox': {
|
|
|
|
|
|
let str = 'no sandbox';
|
|
|
|
|
|
const sandbox = process.env['SANDBOX'];
|
|
|
|
|
|
if (isTrustedFolder === false) str = 'untrusted';
|
2026-04-02 22:22:21 -07:00
|
|
|
|
else if (sandbox) str = 'current process';
|
|
|
|
|
|
else if (config.getSandboxEnabled()) str = 'all tools';
|
2026-03-04 21:21:48 -05:00
|
|
|
|
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => <SandboxIndicator isTrustedFolder={isTrustedFolder} />,
|
|
|
|
|
|
str.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'model-name': {
|
|
|
|
|
|
const str = getDisplayString(model);
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => <Text color={itemColor}>{str}</Text>,
|
|
|
|
|
|
str.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'context-used': {
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => (
|
|
|
|
|
|
<ContextUsageDisplay
|
|
|
|
|
|
promptTokenCount={promptTokenCount}
|
|
|
|
|
|
model={model}
|
|
|
|
|
|
terminalWidth={terminalWidth}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
10, // "100% used" is 9 chars
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'quota': {
|
|
|
|
|
|
if (quotaStats?.remaining !== undefined && quotaStats.limit) {
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => (
|
|
|
|
|
|
<QuotaDisplay
|
|
|
|
|
|
remaining={quotaStats.remaining}
|
|
|
|
|
|
limit={quotaStats.limit}
|
|
|
|
|
|
resetTime={quotaStats.resetTime}
|
|
|
|
|
|
terse={true}
|
|
|
|
|
|
forceShow={true}
|
|
|
|
|
|
lowercase={true}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
10, // "daily 100%" is 10 chars, but terse is "100%" (4 chars)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'memory-usage': {
|
2026-03-24 16:16:48 -07:00
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => (
|
|
|
|
|
|
<MemoryUsageDisplay
|
|
|
|
|
|
color={itemColor}
|
|
|
|
|
|
isActive={!uiState.copyModeEnabled}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
10,
|
|
|
|
|
|
);
|
2026-03-04 21:21:48 -05:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'session-id': {
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => (
|
|
|
|
|
|
<Text color={itemColor}>
|
|
|
|
|
|
{uiState.sessionStats.sessionId.slice(0, 8)}
|
2025-08-07 16:11:35 -07:00
|
|
|
|
</Text>
|
2026-03-04 21:21:48 -05:00
|
|
|
|
),
|
|
|
|
|
|
8,
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-31 12:10:13 -04:00
|
|
|
|
case 'auth': {
|
|
|
|
|
|
if (!settings.merged.ui.showUserIdentity) break;
|
|
|
|
|
|
if (!authType) break;
|
|
|
|
|
|
const displayStr =
|
|
|
|
|
|
authType === AuthType.LOGIN_WITH_GOOGLE
|
|
|
|
|
|
? (email ?? 'google')
|
|
|
|
|
|
: authType;
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => (
|
|
|
|
|
|
<Text color={itemColor} wrap="truncate-end">
|
|
|
|
|
|
{displayStr}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
),
|
|
|
|
|
|
displayStr.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-04 21:21:48 -05:00
|
|
|
|
case 'code-changes': {
|
|
|
|
|
|
const added = uiState.sessionStats.metrics.files.totalLinesAdded;
|
|
|
|
|
|
const removed = uiState.sessionStats.metrics.files.totalLinesRemoved;
|
|
|
|
|
|
if (added > 0 || removed > 0) {
|
|
|
|
|
|
const str = `+${added} -${removed}`;
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => (
|
|
|
|
|
|
<Text>
|
|
|
|
|
|
<Text color={theme.status.success}>+{added}</Text>{' '}
|
|
|
|
|
|
<Text color={theme.status.error}>-{removed}</Text>
|
2025-09-11 20:16:09 -04:00
|
|
|
|
</Text>
|
2026-03-04 21:21:48 -05:00
|
|
|
|
),
|
|
|
|
|
|
str.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'token-count': {
|
|
|
|
|
|
let total = 0;
|
|
|
|
|
|
for (const m of Object.values(uiState.sessionStats.metrics.models))
|
|
|
|
|
|
total += m.tokens.total;
|
|
|
|
|
|
if (total > 0) {
|
|
|
|
|
|
const formatter = new Intl.NumberFormat('en-US', {
|
|
|
|
|
|
notation: 'compact',
|
|
|
|
|
|
maximumFractionDigits: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
const formatted = formatter.format(total).toLowerCase();
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
id,
|
|
|
|
|
|
header,
|
|
|
|
|
|
() => <Text color={itemColor}>{formatted} tokens</Text>,
|
|
|
|
|
|
formatted.length + 7,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
checkExhaustive(id);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Transients
|
|
|
|
|
|
if (corgiMode) addCol('corgi', '', () => <CorgiIndicator />, 5);
|
|
|
|
|
|
if (showErrorSummary) {
|
|
|
|
|
|
addCol(
|
|
|
|
|
|
'error-count',
|
|
|
|
|
|
'',
|
|
|
|
|
|
() => <ConsoleSummaryDisplay errorCount={errorCount} />,
|
|
|
|
|
|
12,
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Width Fitting Logic ---
|
|
|
|
|
|
const columnsToRender: FooterColumn[] = [];
|
|
|
|
|
|
let droppedAny = false;
|
2026-03-08 00:36:54 -08:00
|
|
|
|
let currentUsedWidth = 2; // Initial padding
|
2026-03-04 21:21:48 -05:00
|
|
|
|
|
2026-03-08 00:36:54 -08:00
|
|
|
|
for (const col of potentialColumns) {
|
|
|
|
|
|
const gap = columnsToRender.length > 0 ? (showLabels ? COLUMN_GAP : 3) : 0;
|
2026-03-04 21:21:48 -05:00
|
|
|
|
const budgetWidth = col.id === 'workspace' ? 20 : col.width;
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
col.isHighPriority ||
|
2026-03-08 00:36:54 -08:00
|
|
|
|
currentUsedWidth + gap + budgetWidth <= terminalWidth - 2
|
2026-03-04 21:21:48 -05:00
|
|
|
|
) {
|
|
|
|
|
|
columnsToRender.push(col);
|
2026-03-08 00:36:54 -08:00
|
|
|
|
currentUsedWidth += gap + budgetWidth;
|
2026-03-04 21:21:48 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
droppedAny = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 01:07:26 -07:00
|
|
|
|
const rowItems: FooterRowItem[] = columnsToRender.map((col, index) => {
|
2026-03-08 00:36:54 -08:00
|
|
|
|
const isWorkspace = col.id === 'workspace';
|
2026-03-10 01:07:26 -07:00
|
|
|
|
const isLast = index === columnsToRender.length - 1;
|
2026-03-08 00:36:54 -08:00
|
|
|
|
|
|
|
|
|
|
// Calculate exact space available for growth to prevent over-estimation truncation
|
|
|
|
|
|
const otherItemsWidth = columnsToRender
|
|
|
|
|
|
.filter((c) => c.id !== 'workspace')
|
|
|
|
|
|
.reduce((sum, c) => sum + c.width, 0);
|
|
|
|
|
|
const numItems = columnsToRender.length + (droppedAny ? 1 : 0);
|
|
|
|
|
|
const numGaps = numItems > 1 ? numItems - 1 : 0;
|
|
|
|
|
|
const gapsWidth = numGaps * (showLabels ? COLUMN_GAP : 3);
|
|
|
|
|
|
const ellipsisWidth = droppedAny ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
const availableForWorkspace = Math.max(
|
|
|
|
|
|
20,
|
|
|
|
|
|
terminalWidth - 2 - gapsWidth - otherItemsWidth - ellipsisWidth,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const estimatedWidth = isWorkspace ? availableForWorkspace : col.width;
|
|
|
|
|
|
|
2026-03-04 21:21:48 -05:00
|
|
|
|
return {
|
|
|
|
|
|
key: col.id,
|
|
|
|
|
|
header: col.header,
|
2026-03-08 00:36:54 -08:00
|
|
|
|
element: col.element(estimatedWidth),
|
2026-03-10 01:07:26 -07:00
|
|
|
|
flexGrow: 0,
|
2026-03-08 00:36:54 -08:00
|
|
|
|
flexShrink: isWorkspace ? 1 : 0,
|
2026-03-10 01:07:26 -07:00
|
|
|
|
alignItems:
|
|
|
|
|
|
isLast && !droppedAny && index > 0 ? 'flex-end' : 'flex-start',
|
2026-03-04 21:21:48 -05:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (droppedAny) {
|
|
|
|
|
|
rowItems.push({
|
|
|
|
|
|
key: 'ellipsis',
|
|
|
|
|
|
header: '',
|
|
|
|
|
|
element: <Text color={theme.ui.comment}>…</Text>,
|
2026-03-08 00:36:54 -08:00
|
|
|
|
flexGrow: 0,
|
|
|
|
|
|
flexShrink: 0,
|
2026-03-10 01:07:26 -07:00
|
|
|
|
alignItems: 'flex-end',
|
2026-03-04 21:21:48 -05:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box width={terminalWidth} paddingX={1} overflow="hidden" flexWrap="nowrap">
|
|
|
|
|
|
<FooterRow items={rowItems} showLabels={showLabels} />
|
2025-04-21 14:43:43 -07:00
|
|
|
|
</Box>
|
2025-08-07 15:55:53 -07:00
|
|
|
|
);
|
|
|
|
|
|
};
|