mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(cleanup): enable 30-day session retention by default (#18854)
This commit is contained in:
@@ -30,6 +30,7 @@ they appear in the UI.
|
|||||||
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
||||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
||||||
|
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
|
||||||
|
|
||||||
### Output
|
### Output
|
||||||
|
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
|
|
||||||
- **`general.sessionRetention.maxAge`** (string):
|
- **`general.sessionRetention.maxAge`** (string):
|
||||||
- **Description:** Maximum age of sessions to keep (e.g., "30d", "7d", "24h",
|
- **Description:** Automatically delete chats older than this time period
|
||||||
"1w")
|
(e.g., "30d", "7d", "24h", "1w")
|
||||||
- **Default:** `undefined`
|
- **Default:** `undefined`
|
||||||
|
|
||||||
- **`general.sessionRetention.maxCount`** (number):
|
- **`general.sessionRetention.maxCount`** (number):
|
||||||
@@ -170,6 +170,11 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Description:** Minimum retention period (safety limit, defaults to "1d")
|
- **Description:** Minimum retention period (safety limit, defaults to "1d")
|
||||||
- **Default:** `"1d"`
|
- **Default:** `"1d"`
|
||||||
|
|
||||||
|
- **`general.sessionRetention.warningAcknowledged`** (boolean):
|
||||||
|
- **Description:** INTERNAL: Whether the user has acknowledged the session
|
||||||
|
retention warning
|
||||||
|
- **Default:** `false`
|
||||||
|
|
||||||
#### `output`
|
#### `output`
|
||||||
|
|
||||||
- **`output.format`** (enum):
|
- **`output.format`** (enum):
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ export interface SessionRetentionSettings {
|
|||||||
|
|
||||||
/** Minimum retention period (safety limit, defaults to "1d") */
|
/** Minimum retention period (safety limit, defaults to "1d") */
|
||||||
minRetention?: string;
|
minRetention?: string;
|
||||||
|
|
||||||
|
/** INTERNAL: Whether the user has acknowledged the session retention warning */
|
||||||
|
warningAcknowledged?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsError {
|
export interface SettingsError {
|
||||||
|
|||||||
@@ -304,13 +304,13 @@ const SETTINGS_SCHEMA = {
|
|||||||
},
|
},
|
||||||
maxAge: {
|
maxAge: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
label: 'Max Session Age',
|
label: 'Keep chat history',
|
||||||
category: 'General',
|
category: 'General',
|
||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
default: undefined as string | undefined,
|
default: undefined as string | undefined,
|
||||||
description:
|
description:
|
||||||
'Maximum age of sessions to keep (e.g., "30d", "7d", "24h", "1w")',
|
'Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w")',
|
||||||
showInDialog: false,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
maxCount: {
|
maxCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -331,6 +331,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
description: `Minimum retention period (safety limit, defaults to "${DEFAULT_MIN_RETENTION}")`,
|
description: `Minimum retention period (safety limit, defaults to "${DEFAULT_MIN_RETENTION}")`,
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
warningAcknowledged: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Warning Acknowledged',
|
||||||
|
category: 'General',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: false,
|
||||||
|
showInDialog: false,
|
||||||
|
description:
|
||||||
|
'INTERNAL: Whether the user has acknowledged the session retention warning',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
description: 'Settings for automatic session cleanup.',
|
description: 'Settings for automatic session cleanup.',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ import { useSessionBrowser } from './hooks/useSessionBrowser.js';
|
|||||||
import { persistentState } from '../utils/persistentState.js';
|
import { persistentState } from '../utils/persistentState.js';
|
||||||
import { useSessionResume } from './hooks/useSessionResume.js';
|
import { useSessionResume } from './hooks/useSessionResume.js';
|
||||||
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||||
|
import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js';
|
||||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||||
import { useSettings } from './contexts/SettingsContext.js';
|
import { useSettings } from './contexts/SettingsContext.js';
|
||||||
@@ -1422,6 +1423,28 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
|
|
||||||
useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog);
|
useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog);
|
||||||
|
|
||||||
|
const handleAutoEnableRetention = useCallback(() => {
|
||||||
|
const userSettings = settings.forScope(SettingScope.User).settings;
|
||||||
|
const currentRetention = userSettings.general?.sessionRetention ?? {};
|
||||||
|
|
||||||
|
settings.setValue(SettingScope.User, 'general.sessionRetention', {
|
||||||
|
...currentRetention,
|
||||||
|
enabled: true,
|
||||||
|
maxAge: '30d',
|
||||||
|
warningAcknowledged: true,
|
||||||
|
});
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldShowWarning: shouldShowRetentionWarning,
|
||||||
|
checkComplete: retentionCheckComplete,
|
||||||
|
sessionsToDeleteCount,
|
||||||
|
} = useSessionRetentionCheck(
|
||||||
|
config,
|
||||||
|
settings.merged,
|
||||||
|
handleAutoEnableRetention,
|
||||||
|
);
|
||||||
|
|
||||||
const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1900,6 +1923,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
const nightly = props.version.includes('nightly');
|
const nightly = props.version.includes('nightly');
|
||||||
|
|
||||||
const dialogsVisible =
|
const dialogsVisible =
|
||||||
|
(shouldShowRetentionWarning && retentionCheckComplete) ||
|
||||||
shouldShowIdePrompt ||
|
shouldShowIdePrompt ||
|
||||||
isFolderTrustDialogOpen ||
|
isFolderTrustDialogOpen ||
|
||||||
adminSettingsChanged ||
|
adminSettingsChanged ||
|
||||||
@@ -2012,6 +2036,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
history: historyManager.history,
|
history: historyManager.history,
|
||||||
historyManager,
|
historyManager,
|
||||||
isThemeDialogOpen,
|
isThemeDialogOpen,
|
||||||
|
shouldShowRetentionWarning:
|
||||||
|
shouldShowRetentionWarning && retentionCheckComplete,
|
||||||
|
sessionsToDeleteCount: sessionsToDeleteCount ?? 0,
|
||||||
themeError,
|
themeError,
|
||||||
isAuthenticating,
|
isAuthenticating,
|
||||||
isConfigInitialized,
|
isConfigInitialized,
|
||||||
@@ -2125,6 +2152,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
isThemeDialogOpen,
|
isThemeDialogOpen,
|
||||||
|
shouldShowRetentionWarning,
|
||||||
|
retentionCheckComplete,
|
||||||
|
sessionsToDeleteCount,
|
||||||
themeError,
|
themeError,
|
||||||
isAuthenticating,
|
isAuthenticating,
|
||||||
isConfigInitialized,
|
isConfigInitialized,
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ import { AdminSettingsChangedDialog } from './AdminSettingsChangedDialog.js';
|
|||||||
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
|
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
|
||||||
import { NewAgentsNotification } from './NewAgentsNotification.js';
|
import { NewAgentsNotification } from './NewAgentsNotification.js';
|
||||||
import { AgentConfigDialog } from './AgentConfigDialog.js';
|
import { AgentConfigDialog } from './AgentConfigDialog.js';
|
||||||
|
import { SessionRetentionWarningDialog } from './SessionRetentionWarningDialog.js';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { SettingScope } from '../../config/settings.js';
|
||||||
|
|
||||||
interface DialogManagerProps {
|
interface DialogManagerProps {
|
||||||
addItem: UseHistoryManagerReturn['addItem'];
|
addItem: UseHistoryManagerReturn['addItem'];
|
||||||
@@ -55,8 +58,56 @@ export const DialogManager = ({
|
|||||||
terminalHeight,
|
terminalHeight,
|
||||||
staticExtraHeight,
|
staticExtraHeight,
|
||||||
terminalWidth: uiTerminalWidth,
|
terminalWidth: uiTerminalWidth,
|
||||||
|
shouldShowRetentionWarning,
|
||||||
|
sessionsToDeleteCount,
|
||||||
} = uiState;
|
} = uiState;
|
||||||
|
|
||||||
|
const handleKeep120Days = useCallback(() => {
|
||||||
|
settings.setValue(
|
||||||
|
SettingScope.User,
|
||||||
|
'general.sessionRetention.warningAcknowledged',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
settings.setValue(
|
||||||
|
SettingScope.User,
|
||||||
|
'general.sessionRetention.enabled',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
settings.setValue(
|
||||||
|
SettingScope.User,
|
||||||
|
'general.sessionRetention.maxAge',
|
||||||
|
'120d',
|
||||||
|
);
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
const handleKeep30Days = useCallback(() => {
|
||||||
|
settings.setValue(
|
||||||
|
SettingScope.User,
|
||||||
|
'general.sessionRetention.warningAcknowledged',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
settings.setValue(
|
||||||
|
SettingScope.User,
|
||||||
|
'general.sessionRetention.enabled',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
settings.setValue(
|
||||||
|
SettingScope.User,
|
||||||
|
'general.sessionRetention.maxAge',
|
||||||
|
'30d',
|
||||||
|
);
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
if (shouldShowRetentionWarning && sessionsToDeleteCount !== undefined) {
|
||||||
|
return (
|
||||||
|
<SessionRetentionWarningDialog
|
||||||
|
onKeep120Days={handleKeep120Days}
|
||||||
|
onKeep30Days={handleKeep30Days}
|
||||||
|
sessionsToDeleteCount={sessionsToDeleteCount ?? 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.adminSettingsChanged) {
|
if (uiState.adminSettingsChanged) {
|
||||||
return <AdminSettingsChangedDialog />;
|
return <AdminSettingsChangedDialog />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* @license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
|
import { SessionRetentionWarningDialog } from './SessionRetentionWarningDialog.js';
|
||||||
|
import { waitFor } from '../../test-utils/async.js';
|
||||||
|
import { act } from 'react';
|
||||||
|
|
||||||
|
// Helper to write to stdin
|
||||||
|
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
|
||||||
|
act(() => {
|
||||||
|
stdin.write(key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SessionRetentionWarningDialog', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly with warning message and session count', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<SessionRetentionWarningDialog
|
||||||
|
onKeep120Days={vi.fn()}
|
||||||
|
onKeep30Days={vi.fn()}
|
||||||
|
sessionsToDeleteCount={42}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Keep chat history');
|
||||||
|
expect(lastFrame()).toContain(
|
||||||
|
'introducing a limit on how long chat sessions are stored',
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toContain('Keep for 30 days (Recommended)');
|
||||||
|
expect(lastFrame()).toContain('42 sessions will be deleted');
|
||||||
|
expect(lastFrame()).toContain('Keep for 120 days');
|
||||||
|
expect(lastFrame()).toContain('No sessions will be deleted at this time');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles pluralization correctly for 1 session', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<SessionRetentionWarningDialog
|
||||||
|
onKeep120Days={vi.fn()}
|
||||||
|
onKeep30Days={vi.fn()}
|
||||||
|
sessionsToDeleteCount={1}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('1 session will be deleted');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to "Keep for 120 days" when there are sessions to delete', async () => {
|
||||||
|
const onKeep120Days = vi.fn();
|
||||||
|
const onKeep30Days = vi.fn();
|
||||||
|
|
||||||
|
const { stdin } = renderWithProviders(
|
||||||
|
<SessionRetentionWarningDialog
|
||||||
|
onKeep120Days={onKeep120Days}
|
||||||
|
onKeep30Days={onKeep30Days}
|
||||||
|
sessionsToDeleteCount={10}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial selection should be "Keep for 120 days" (index 1) because count > 0
|
||||||
|
// Pressing Enter immediately should select it.
|
||||||
|
writeKey(stdin, '\r');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onKeep120Days).toHaveBeenCalled();
|
||||||
|
expect(onKeep30Days).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onKeep30Days when "Keep for 30 days" is explicitly selected (from 120 days default)', async () => {
|
||||||
|
const onKeep120Days = vi.fn();
|
||||||
|
const onKeep30Days = vi.fn();
|
||||||
|
|
||||||
|
const { stdin } = renderWithProviders(
|
||||||
|
<SessionRetentionWarningDialog
|
||||||
|
onKeep120Days={onKeep120Days}
|
||||||
|
onKeep30Days={onKeep30Days}
|
||||||
|
sessionsToDeleteCount={10}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Default is index 1 (120 days). Move UP to index 0 (30 days).
|
||||||
|
writeKey(stdin, '\x1b[A'); // Up arrow
|
||||||
|
writeKey(stdin, '\r');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onKeep30Days).toHaveBeenCalled();
|
||||||
|
expect(onKeep120Days).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match snapshot', async () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<SessionRetentionWarningDialog
|
||||||
|
onKeep120Days={vi.fn()}
|
||||||
|
onKeep30Days={vi.fn()}
|
||||||
|
sessionsToDeleteCount={123}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { theme } from '../semantic-colors.js';
|
||||||
|
import {
|
||||||
|
RadioButtonSelect,
|
||||||
|
type RadioSelectItem,
|
||||||
|
} from './shared/RadioButtonSelect.js';
|
||||||
|
|
||||||
|
interface SessionRetentionWarningDialogProps {
|
||||||
|
onKeep120Days: () => void;
|
||||||
|
onKeep30Days: () => void;
|
||||||
|
sessionsToDeleteCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SessionRetentionWarningDialog = ({
|
||||||
|
onKeep120Days,
|
||||||
|
onKeep30Days,
|
||||||
|
sessionsToDeleteCount,
|
||||||
|
}: SessionRetentionWarningDialogProps) => {
|
||||||
|
const options: Array<RadioSelectItem<() => void>> = [
|
||||||
|
{
|
||||||
|
label: 'Keep for 30 days (Recommended)',
|
||||||
|
value: onKeep30Days,
|
||||||
|
key: '30days',
|
||||||
|
sublabel: `${sessionsToDeleteCount} session${
|
||||||
|
sessionsToDeleteCount === 1 ? '' : 's'
|
||||||
|
} will be deleted`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Keep for 120 days',
|
||||||
|
value: onKeep120Days,
|
||||||
|
key: '120days',
|
||||||
|
sublabel: 'No sessions will be deleted at this time',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={theme.border.default}
|
||||||
|
flexDirection="column"
|
||||||
|
width="100%"
|
||||||
|
padding={1}
|
||||||
|
>
|
||||||
|
<Box marginBottom={1} justifyContent="center" width="100%">
|
||||||
|
<Text bold>Keep chat history</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box flexDirection="column" gap={1} marginBottom={1}>
|
||||||
|
<Text>
|
||||||
|
To keep your workspace clean, we are introducing a limit on how long
|
||||||
|
chat sessions are stored. Please choose a retention period for your
|
||||||
|
existing chats:
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<RadioButtonSelect
|
||||||
|
items={options}
|
||||||
|
onSelect={(action) => action()}
|
||||||
|
initialIndex={1}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
Set a custom limit <Text color={theme.text.primary}>/settings</Text>{' '}
|
||||||
|
and change "Keep chat history".
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`SessionRetentionWarningDialog > should match snapshot 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ │
|
||||||
|
│ Keep chat history │
|
||||||
|
│ │
|
||||||
|
│ To keep your workspace clean, we are introducing a limit on how long chat sessions are stored. Please choose a │
|
||||||
|
│ retention period for your existing chats: │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ 1. Keep for 30 days (Recommended) │
|
||||||
|
│ 123 sessions will be deleted │
|
||||||
|
│ ● 2. Keep for 120 days │
|
||||||
|
│ No sessions will be deleted at this time │
|
||||||
|
│ │
|
||||||
|
│ Set a custom limit /settings and change "Keep chat history". │
|
||||||
|
│ │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
`;
|
||||||
@@ -28,12 +28,12 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -74,12 +74,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -120,12 +120,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -166,12 +166,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -212,12 +212,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -258,12 +258,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ > Apply To │
|
│ > Apply To │
|
||||||
@@ -304,12 +304,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -350,12 +350,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -396,12 +396,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
|
│ Keep chat history undefined │
|
||||||
|
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
||||||
|
│ │
|
||||||
│ Output Format Text │
|
│ Output Format Text │
|
||||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Auto Theme Switching true │
|
|
||||||
│ Automatically switch between default light and dark themes based on terminal backgro… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
|
import type { Text } from 'ink';
|
||||||
|
import { Box } from 'ink';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {
|
import {
|
||||||
RadioButtonSelect,
|
RadioButtonSelect,
|
||||||
@@ -144,9 +146,16 @@ describe('RadioButtonSelect', () => {
|
|||||||
|
|
||||||
const result = renderItem(item, mockContext);
|
const result = renderItem(item, mockContext);
|
||||||
|
|
||||||
expect(result?.props?.color).toBe(mockContext.titleColor);
|
expect(result.type).toBe(Box);
|
||||||
expect(result?.props?.children).toBe('Option 1');
|
const props = result.props as { children: React.ReactNode };
|
||||||
expect(result?.props?.wrap).toBe('truncate');
|
const textComponent = (props.children as React.ReactElement[])[0];
|
||||||
|
const textProps = textComponent?.props as React.ComponentProps<
|
||||||
|
typeof Text
|
||||||
|
>;
|
||||||
|
|
||||||
|
expect(textProps?.color).toBe(mockContext.titleColor);
|
||||||
|
expect(textProps?.children).toBe('Option 1');
|
||||||
|
expect(textProps?.wrap).toBe('truncate');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the special theme display when theme props are present', () => {
|
it('should render the special theme display when theme props are present', () => {
|
||||||
@@ -192,7 +201,13 @@ describe('RadioButtonSelect', () => {
|
|||||||
|
|
||||||
const result = renderItem(partialThemeItem, mockContext);
|
const result = renderItem(partialThemeItem, mockContext);
|
||||||
|
|
||||||
expect(result?.props?.children).toBe('Incomplete Theme');
|
expect(result.type).toBe(Box);
|
||||||
|
const props = result.props as { children: React.ReactNode };
|
||||||
|
const textComponent = (props.children as React.ReactElement[])[0];
|
||||||
|
const textProps = textComponent?.props as React.ComponentProps<
|
||||||
|
typeof Text
|
||||||
|
>;
|
||||||
|
expect(textProps?.children).toBe('Incomplete Theme');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Text } from 'ink';
|
import { Text, Box } from 'ink';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import {
|
import {
|
||||||
BaseSelectionList,
|
BaseSelectionList,
|
||||||
@@ -19,6 +19,7 @@ import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
|||||||
*/
|
*/
|
||||||
export interface RadioSelectItem<T> extends SelectionListItem<T> {
|
export interface RadioSelectItem<T> extends SelectionListItem<T> {
|
||||||
label: string;
|
label: string;
|
||||||
|
sublabel?: string;
|
||||||
themeNameDisplay?: string;
|
themeNameDisplay?: string;
|
||||||
themeTypeDisplay?: string;
|
themeTypeDisplay?: string;
|
||||||
}
|
}
|
||||||
@@ -98,9 +99,16 @@ export function RadioButtonSelect<T>({
|
|||||||
}
|
}
|
||||||
// Regular label display
|
// Regular label display
|
||||||
return (
|
return (
|
||||||
<Text color={titleColor} wrap="truncate">
|
<Box flexDirection="column">
|
||||||
{item.label}
|
<Text color={titleColor} wrap="truncate">
|
||||||
</Text>
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
{item.sublabel && (
|
||||||
|
<Text color={theme.text.secondary} wrap="truncate">
|
||||||
|
{item.sublabel}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export interface UIState {
|
|||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
historyManager: UseHistoryManagerReturn;
|
historyManager: UseHistoryManagerReturn;
|
||||||
isThemeDialogOpen: boolean;
|
isThemeDialogOpen: boolean;
|
||||||
|
shouldShowRetentionWarning: boolean;
|
||||||
|
sessionsToDeleteCount: number;
|
||||||
themeError: string | null;
|
themeError: string | null;
|
||||||
isAuthenticating: boolean;
|
isAuthenticating: boolean;
|
||||||
isConfigInitialized: boolean;
|
isConfigInitialized: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook } from '../../test-utils/render.js';
|
||||||
|
import { useSessionRetentionCheck } from './useSessionRetentionCheck.js';
|
||||||
|
import { type Config } from '@google/gemini-cli-core';
|
||||||
|
import type { Settings } from '../../config/settingsSchema.js';
|
||||||
|
import { waitFor } from '../../test-utils/async.js';
|
||||||
|
|
||||||
|
// Mock utils
|
||||||
|
const mockGetAllSessionFiles = vi.fn();
|
||||||
|
const mockIdentifySessionsToDelete = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../../utils/sessionUtils.js', () => ({
|
||||||
|
getAllSessionFiles: () => mockGetAllSessionFiles(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../utils/sessionCleanup.js', () => ({
|
||||||
|
identifySessionsToDelete: () => mockIdentifySessionsToDelete(),
|
||||||
|
DEFAULT_MIN_RETENTION: '30d',
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useSessionRetentionCheck', () => {
|
||||||
|
const mockConfig = {
|
||||||
|
storage: {
|
||||||
|
getProjectTempDir: () => '/mock/project/temp/dir',
|
||||||
|
},
|
||||||
|
getSessionId: () => 'mock-session-id',
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show warning if enabled is true but maxAge is undefined', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
enabled: true,
|
||||||
|
maxAge: undefined,
|
||||||
|
warningAcknowledged: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
mockGetAllSessionFiles.mockResolvedValue(['session1.json']);
|
||||||
|
mockIdentifySessionsToDelete.mockResolvedValue(['session1.json']);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(true);
|
||||||
|
expect(mockGetAllSessionFiles).toHaveBeenCalled();
|
||||||
|
expect(mockIdentifySessionsToDelete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show warning if warningAcknowledged is true', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
warningAcknowledged: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(false);
|
||||||
|
expect(mockGetAllSessionFiles).not.toHaveBeenCalled();
|
||||||
|
expect(mockIdentifySessionsToDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show warning if retention is already enabled', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
enabled: true,
|
||||||
|
maxAge: '30d', // Explicitly enabled with non-default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(false);
|
||||||
|
expect(mockGetAllSessionFiles).not.toHaveBeenCalled();
|
||||||
|
expect(mockIdentifySessionsToDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show warning if sessions to delete exist', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
enabled: false,
|
||||||
|
warningAcknowledged: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
mockGetAllSessionFiles.mockResolvedValue([
|
||||||
|
'session1.json',
|
||||||
|
'session2.json',
|
||||||
|
]);
|
||||||
|
mockIdentifySessionsToDelete.mockResolvedValue(['session1.json']); // 1 session to delete
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(true);
|
||||||
|
expect(result.current.sessionsToDeleteCount).toBe(1);
|
||||||
|
expect(mockGetAllSessionFiles).toHaveBeenCalled();
|
||||||
|
expect(mockIdentifySessionsToDelete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onAutoEnable if no sessions to delete and currently disabled', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
enabled: false,
|
||||||
|
warningAcknowledged: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
mockGetAllSessionFiles.mockResolvedValue(['session1.json']);
|
||||||
|
mockIdentifySessionsToDelete.mockResolvedValue([]); // 0 sessions to delete
|
||||||
|
|
||||||
|
const onAutoEnable = vi.fn();
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings, onAutoEnable),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(false);
|
||||||
|
expect(onAutoEnable).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show warning if no sessions to delete', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
enabled: false,
|
||||||
|
warningAcknowledged: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
mockGetAllSessionFiles.mockResolvedValue([
|
||||||
|
'session1.json',
|
||||||
|
'session2.json',
|
||||||
|
]);
|
||||||
|
mockIdentifySessionsToDelete.mockResolvedValue([]); // 0 sessions to delete
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(false);
|
||||||
|
expect(result.current.sessionsToDeleteCount).toBe(0);
|
||||||
|
expect(mockGetAllSessionFiles).toHaveBeenCalled();
|
||||||
|
expect(mockIdentifySessionsToDelete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully (assume no warning)', async () => {
|
||||||
|
const settings = {
|
||||||
|
general: {
|
||||||
|
sessionRetention: {
|
||||||
|
enabled: false,
|
||||||
|
warningAcknowledged: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Settings;
|
||||||
|
|
||||||
|
mockGetAllSessionFiles.mockRejectedValue(new Error('FS Error'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useSessionRetentionCheck(mockConfig, settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.checkComplete).toBe(true);
|
||||||
|
expect(result.current.shouldShowWarning).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { type Config } from '@google/gemini-cli-core';
|
||||||
|
import { type Settings } from '../../config/settings.js';
|
||||||
|
import { getAllSessionFiles } from '../../utils/sessionUtils.js';
|
||||||
|
import { identifySessionsToDelete } from '../../utils/sessionCleanup.js';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export function useSessionRetentionCheck(
|
||||||
|
config: Config,
|
||||||
|
settings: Settings,
|
||||||
|
onAutoEnable?: () => void,
|
||||||
|
) {
|
||||||
|
const [shouldShowWarning, setShouldShowWarning] = useState(false);
|
||||||
|
const [sessionsToDeleteCount, setSessionsToDeleteCount] = useState(0);
|
||||||
|
const [checkComplete, setCheckComplete] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If warning already acknowledged or retention already enabled, skip check
|
||||||
|
if (
|
||||||
|
settings.general?.sessionRetention?.warningAcknowledged ||
|
||||||
|
(settings.general?.sessionRetention?.enabled &&
|
||||||
|
settings.general?.sessionRetention?.maxAge !== undefined)
|
||||||
|
) {
|
||||||
|
setShouldShowWarning(false);
|
||||||
|
setCheckComplete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSessions = async () => {
|
||||||
|
try {
|
||||||
|
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
|
||||||
|
const allFiles = await getAllSessionFiles(
|
||||||
|
chatsDir,
|
||||||
|
config.getSessionId(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate how many sessions would be deleted if we applied a 30-day retention
|
||||||
|
const sessionsToDelete = await identifySessionsToDelete(allFiles, {
|
||||||
|
enabled: true,
|
||||||
|
maxAge: '30d',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionsToDelete.length > 0) {
|
||||||
|
setSessionsToDeleteCount(sessionsToDelete.length);
|
||||||
|
setShouldShowWarning(true);
|
||||||
|
} else {
|
||||||
|
setShouldShowWarning(false);
|
||||||
|
// If no sessions to delete, safe to auto-enable retention
|
||||||
|
onAutoEnable?.();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't check sessions, default to not showing the warning to be safe
|
||||||
|
setShouldShowWarning(false);
|
||||||
|
} finally {
|
||||||
|
setCheckComplete(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
checkSessions();
|
||||||
|
}, [config, settings.general?.sessionRetention, onAutoEnable]);
|
||||||
|
|
||||||
|
return { shouldShowWarning, checkComplete, sessionsToDeleteCount };
|
||||||
|
}
|
||||||
@@ -174,7 +174,10 @@ export async function cleanupExpiredSessions(
|
|||||||
/**
|
/**
|
||||||
* Identifies sessions that should be deleted (corrupted or expired based on retention policy)
|
* Identifies sessions that should be deleted (corrupted or expired based on retention policy)
|
||||||
*/
|
*/
|
||||||
async function identifySessionsToDelete(
|
/**
|
||||||
|
* Identifies sessions that should be deleted (corrupted or expired based on retention policy)
|
||||||
|
*/
|
||||||
|
export async function identifySessionsToDelete(
|
||||||
allFiles: SessionFileEntry[],
|
allFiles: SessionFileEntry[],
|
||||||
retentionConfig: SessionRetentionSettings,
|
retentionConfig: SessionRetentionSettings,
|
||||||
): Promise<SessionFileEntry[]> {
|
): Promise<SessionFileEntry[]> {
|
||||||
|
|||||||
@@ -133,9 +133,9 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"maxAge": {
|
"maxAge": {
|
||||||
"title": "Max Session Age",
|
"title": "Keep chat history",
|
||||||
"description": "Maximum age of sessions to keep (e.g., \"30d\", \"7d\", \"24h\", \"1w\")",
|
"description": "Automatically delete chats older than this time period (e.g., \"30d\", \"7d\", \"24h\", \"1w\")",
|
||||||
"markdownDescription": "Maximum age of sessions to keep (e.g., \"30d\", \"7d\", \"24h\", \"1w\")\n\n- Category: `General`\n- Requires restart: `no`",
|
"markdownDescription": "Automatically delete chats older than this time period (e.g., \"30d\", \"7d\", \"24h\", \"1w\")\n\n- Category: `General`\n- Requires restart: `no`",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"maxCount": {
|
"maxCount": {
|
||||||
@@ -150,6 +150,13 @@
|
|||||||
"markdownDescription": "Minimum retention period (safety limit, defaults to \"1d\")\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `1d`",
|
"markdownDescription": "Minimum retention period (safety limit, defaults to \"1d\")\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `1d`",
|
||||||
"default": "1d",
|
"default": "1d",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"warningAcknowledged": {
|
||||||
|
"title": "Warning Acknowledged",
|
||||||
|
"description": "INTERNAL: Whether the user has acknowledged the session retention warning",
|
||||||
|
"markdownDescription": "INTERNAL: Whether the user has acknowledged the session retention warning\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
Reference in New Issue
Block a user