summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/gemini.tsx4
-rw-r--r--packages/cli/src/ui/App.test.tsx2
-rw-r--r--packages/cli/src/ui/App.tsx9
-rw-r--r--packages/cli/src/ui/contexts/SessionContext.test.tsx29
-rw-r--r--packages/cli/src/ui/contexts/SessionContext.tsx38
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.test.ts37
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts31
7 files changed, 146 insertions, 4 deletions
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index b7df958e..4f30e3da 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { render } from 'ink';
-import { App } from './ui/App.js';
+import { AppWrapper } from './ui/App.js';
import { loadCliConfig } from './config/config.js';
import { readStdin } from './utils/readStdin.js';
import { sandbox_command, start_sandbox } from './utils/sandbox.js';
@@ -95,7 +95,7 @@ export async function main() {
if (process.stdin.isTTY && input?.length === 0) {
render(
<React.StrictMode>
- <App
+ <AppWrapper
config={config}
settings={settings}
startupWarnings={startupWarnings}
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index eeb5bbe6..e3cff7a4 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -6,7 +6,7 @@
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { render } from 'ink-testing-library';
-import { App } from './App.js';
+import { AppWrapper as App } from './App.js';
import {
Config as ServerConfig,
MCPServerConfig,
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 365266f8..b458a822 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -48,6 +48,7 @@ import {
} from '@gemini-cli/core';
import { useLogger } from './hooks/useLogger.js';
import { StreamingContext } from './contexts/StreamingContext.js';
+import { SessionProvider } from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
const CTRL_C_PROMPT_DURATION_MS = 1000;
@@ -58,7 +59,13 @@ interface AppProps {
startupWarnings?: string[];
}
-export const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
+export const AppWrapper = (props: AppProps) => (
+ <SessionProvider>
+ <App {...props} />
+ </SessionProvider>
+);
+
+const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
const { history, addItem, clearItems } = useHistory();
const {
consoleMessages,
diff --git a/packages/cli/src/ui/contexts/SessionContext.test.tsx b/packages/cli/src/ui/contexts/SessionContext.test.tsx
new file mode 100644
index 00000000..3b5454cf
--- /dev/null
+++ b/packages/cli/src/ui/contexts/SessionContext.test.tsx
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { Text } from 'ink';
+import { SessionProvider, useSession } from './SessionContext.js';
+import { describe, it, expect } from 'vitest';
+
+const TestComponent = () => {
+ const { startTime } = useSession();
+ return <Text>{startTime.toISOString()}</Text>;
+};
+
+describe('SessionContext', () => {
+ it('should provide a start time', () => {
+ const { lastFrame } = render(
+ <SessionProvider>
+ <TestComponent />
+ </SessionProvider>,
+ );
+
+ const frameText = lastFrame();
+ // Check if the output is a valid ISO string, which confirms it's a Date object.
+ expect(new Date(frameText!).toString()).not.toBe('Invalid Date');
+ });
+});
diff --git a/packages/cli/src/ui/contexts/SessionContext.tsx b/packages/cli/src/ui/contexts/SessionContext.tsx
new file mode 100644
index 00000000..c511aa46
--- /dev/null
+++ b/packages/cli/src/ui/contexts/SessionContext.tsx
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { createContext, useContext, useState, useMemo } from 'react';
+
+interface SessionContextType {
+ startTime: Date;
+}
+
+const SessionContext = createContext<SessionContextType | null>(null);
+
+export const SessionProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [startTime] = useState(new Date());
+
+ const value = useMemo(
+ () => ({
+ startTime,
+ }),
+ [startTime],
+ );
+
+ return (
+ <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
+ );
+};
+
+export const useSession = () => {
+ const context = useContext(SessionContext);
+ if (!context) {
+ throw new Error('useSession must be used within a SessionProvider');
+ }
+ return context;
+};
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
index 221893a2..3fcdff97 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
@@ -61,10 +61,15 @@ import {
MCPServerStatus,
getMCPServerStatus,
} from '@gemini-cli/core';
+import { useSession } from '../contexts/SessionContext.js';
import * as ShowMemoryCommandModule from './useShowMemoryCommand.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
+vi.mock('../contexts/SessionContext.js', () => ({
+ useSession: vi.fn(),
+}));
+
vi.mock('./useShowMemoryCommand.js', () => ({
SHOW_MEMORY_COMMAND_NAME: '/memory show',
createShowMemoryAction: vi.fn(() => vi.fn()),
@@ -84,6 +89,7 @@ describe('useSlashCommandProcessor', () => {
let mockPerformMemoryRefresh: ReturnType<typeof vi.fn>;
let mockConfig: Config;
let mockCorgiMode: ReturnType<typeof vi.fn>;
+ const mockUseSession = useSession as Mock;
beforeEach(() => {
mockAddItem = vi.fn();
@@ -99,6 +105,9 @@ describe('useSlashCommandProcessor', () => {
getModel: vi.fn(() => 'test-model'),
} as unknown as Config;
mockCorgiMode = vi.fn();
+ mockUseSession.mockReturnValue({
+ startTime: new Date('2025-01-01T00:00:00.000Z'),
+ });
(open as Mock).mockClear();
mockProcessExit.mockClear();
@@ -230,6 +239,34 @@ describe('useSlashCommandProcessor', () => {
});
});
+ describe('/stats command', () => {
+ it('should show the session duration', async () => {
+ const { handleSlashCommand } = getProcessor();
+ let commandResult: SlashCommandActionReturn | boolean = false;
+
+ // Mock current time
+ const mockDate = new Date('2025-01-01T00:01:05.000Z');
+ vi.setSystemTime(mockDate);
+
+ await act(async () => {
+ commandResult = handleSlashCommand('/stats');
+ });
+
+ expect(mockAddItem).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ type: MessageType.INFO,
+ text: 'Session duration: 1m 5s',
+ }),
+ expect.any(Number),
+ );
+ expect(commandResult).toBe(true);
+
+ // Restore system time
+ vi.useRealTimers();
+ });
+ });
+
describe('Other commands', () => {
it('/help should open help and return true', async () => {
const { handleSlashCommand } = getProcessor();
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index 38fdddba..85ae825e 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -11,6 +11,7 @@ import process from 'node:process';
import { UseHistoryManagerReturn } from './useHistoryManager.js';
import { Config, MCPServerStatus, getMCPServerStatus } from '@gemini-cli/core';
import { Message, MessageType, HistoryItemWithoutId } from '../types.js';
+import { useSession } from '../contexts/SessionContext.js';
import { createShowMemoryAction } from './useShowMemoryCommand.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
@@ -49,6 +50,8 @@ export const useSlashCommandProcessor = (
toggleCorgiMode: () => void,
showToolDescriptions: boolean = false,
) => {
+ const session = useSession();
+
const addMessage = useCallback(
(message: Message) => {
// Convert Message to HistoryItemWithoutId
@@ -139,6 +142,33 @@ export const useSlashCommandProcessor = (
},
},
{
+ name: 'stats',
+ altName: 'usage',
+ description: 'check session stats',
+ action: (_mainCommand, _subCommand, _args) => {
+ const now = new Date();
+ const duration = now.getTime() - session.startTime.getTime();
+ const durationInSeconds = Math.floor(duration / 1000);
+ const hours = Math.floor(durationInSeconds / 3600);
+ const minutes = Math.floor((durationInSeconds % 3600) / 60);
+ const seconds = durationInSeconds % 60;
+
+ const durationString = [
+ hours > 0 ? `${hours}h` : '',
+ minutes > 0 ? `${minutes}m` : '',
+ `${seconds}s`,
+ ]
+ .filter(Boolean)
+ .join(' ');
+
+ addMessage({
+ type: MessageType.INFO,
+ content: `Session duration: ${durationString}`,
+ timestamp: new Date(),
+ });
+ },
+ },
+ {
name: 'mcp',
description: 'list configured MCP servers and tools',
action: async (_mainCommand, _subCommand, _args) => {
@@ -447,6 +477,7 @@ Add any other context about the problem here.
toggleCorgiMode,
config,
showToolDescriptions,
+ session.startTime,
],
);