mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 08:01:02 -07:00
first commit
This commit is contained in:
@@ -374,6 +374,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Hide the window title bar',
|
||||
showInDialog: true,
|
||||
},
|
||||
showInlineThinking: {
|
||||
type: 'boolean',
|
||||
label: 'Show Inline Thinking',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Show model thinking summaries inline in the conversation.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showStatusInTitle: {
|
||||
type: 'boolean',
|
||||
label: 'Show Status in Title',
|
||||
|
||||
@@ -207,6 +207,34 @@ describe('<HistoryItemDisplay />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('thinking items', () => {
|
||||
it('renders thinking item when enabled', () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'thinking',
|
||||
thoughts: [{ subject: 'Thinking', description: 'test' }],
|
||||
};
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} inlineEnabled={true} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Thinking');
|
||||
});
|
||||
|
||||
it('does not render thinking item when disabled', () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'thinking',
|
||||
thoughts: [{ subject: 'Thinking', description: 'test' }],
|
||||
};
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} inlineEnabled={false} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([true, false])(
|
||||
'gemini items (alternateBuffer=%s)',
|
||||
(useAlternateBuffer) => {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { McpStatus } from './views/McpStatus.js';
|
||||
import { ChatList } from './views/ChatList.js';
|
||||
import { HooksList } from './views/HooksList.js';
|
||||
import { ModelMessage } from './messages/ModelMessage.js';
|
||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||
|
||||
interface HistoryItemDisplayProps {
|
||||
item: HistoryItem;
|
||||
@@ -44,6 +45,7 @@ interface HistoryItemDisplayProps {
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
availableTerminalHeightGemini?: number;
|
||||
inlineEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
@@ -56,12 +58,19 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
availableTerminalHeightGemini,
|
||||
inlineEnabled,
|
||||
}) => {
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={itemForDisplay.id} width={terminalWidth}>
|
||||
{/* Render standard message types */}
|
||||
{itemForDisplay.type === 'thinking' && inlineEnabled && (
|
||||
<ThinkingMessage
|
||||
thoughts={itemForDisplay.thoughts}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'user' && (
|
||||
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useAppContext } from '../contexts/AppContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { SCROLL_TO_ITEM_END } from './shared/VirtualizedList.js';
|
||||
@@ -27,6 +28,7 @@ const MemoizedAppHeader = memo(AppHeader);
|
||||
export const MainContent = () => {
|
||||
const { version } = useAppContext();
|
||||
const uiState = useUIState();
|
||||
const settings = useSettings();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const {
|
||||
@@ -36,6 +38,8 @@ export const MainContent = () => {
|
||||
availableTerminalHeight,
|
||||
} = uiState;
|
||||
|
||||
const inlineEnabled = settings.merged.ui?.showInlineThinking;
|
||||
|
||||
const historyItems = uiState.history.map((h) => (
|
||||
<HistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
@@ -45,6 +49,7 @@ export const MainContent = () => {
|
||||
item={h}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
inlineEnabled={inlineEnabled}
|
||||
/>
|
||||
));
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { ThinkingMessage } from './ThinkingMessage.js';
|
||||
|
||||
describe('ThinkingMessage', () => {
|
||||
it('renders thinking header with count', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage
|
||||
thoughts={[
|
||||
{ subject: 'Planning', description: 'test' },
|
||||
{ subject: 'Analyzing', description: 'test' },
|
||||
]}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Thinking');
|
||||
expect(lastFrame()).toContain('(2)');
|
||||
});
|
||||
|
||||
it('renders with single thought', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage
|
||||
thoughts={[{ subject: 'Processing', description: 'test' }]}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('(1)');
|
||||
});
|
||||
|
||||
it('renders empty state gracefully', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage thoughts={[]} terminalWidth={80} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('(0)');
|
||||
});
|
||||
});
|
||||
33
packages/cli/src/ui/components/messages/ThinkingMessage.tsx
Normal file
33
packages/cli/src/ui/components/messages/ThinkingMessage.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { ThoughtSummary } from '@google/gemini-cli-core';
|
||||
|
||||
interface ThinkingMessageProps {
|
||||
thoughts: ThoughtSummary[];
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
|
||||
thoughts,
|
||||
terminalWidth,
|
||||
}) => (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="magenta"
|
||||
width={terminalWidth}
|
||||
paddingX={1}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Text color="magenta">◆ </Text>
|
||||
<Text bold color="magenta">
|
||||
Thinking
|
||||
</Text>
|
||||
<Text dimColor> ({thoughts.length})</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -46,6 +46,7 @@ import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolGroup,
|
||||
HistoryItemThinking,
|
||||
SlashCommandProcessorResult,
|
||||
HistoryItemModel,
|
||||
} from '../types.js';
|
||||
@@ -118,8 +119,29 @@ export const useGeminiStream = (
|
||||
const activeQueryIdRef = useRef<string | null>(null);
|
||||
const [isResponding, setIsResponding] = useState<boolean>(false);
|
||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
const thoughtsBufferRef = useRef<ThoughtSummary[]>([]);
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
|
||||
const flushThoughts = useCallback(
|
||||
(userMessageTimestamp: number) => {
|
||||
if (
|
||||
thoughtsBufferRef.current.length > 0 &&
|
||||
settings.merged.ui?.showInlineThinking
|
||||
) {
|
||||
addItem(
|
||||
{
|
||||
type: 'thinking',
|
||||
thoughts: [...thoughtsBufferRef.current],
|
||||
} as HistoryItemThinking,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
thoughtsBufferRef.current = [];
|
||||
}
|
||||
},
|
||||
[addItem, settings],
|
||||
);
|
||||
|
||||
const processedMemoryToolsRef = useRef<Set<string>>(new Set());
|
||||
const { startNewPrompt, getPromptCount } = useSessionStats();
|
||||
const storage = config.storage;
|
||||
@@ -535,6 +557,7 @@ export const useGeminiStream = (
|
||||
currentGeminiMessageBuffer: string,
|
||||
userMessageTimestamp: number,
|
||||
): string => {
|
||||
flushThoughts(userMessageTimestamp);
|
||||
if (turnCancelledRef.current) {
|
||||
// Prevents additional output after a user initiated cancel.
|
||||
return '';
|
||||
@@ -584,7 +607,7 @@ export const useGeminiStream = (
|
||||
}
|
||||
return newGeminiMessageBuffer;
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem, flushThoughts],
|
||||
);
|
||||
|
||||
const handleUserCancelledEvent = useCallback(
|
||||
@@ -805,6 +828,7 @@ export const useGeminiStream = (
|
||||
switch (event.type) {
|
||||
case ServerGeminiEventType.Thought:
|
||||
setThought(event.value);
|
||||
thoughtsBufferRef.current.push(event.value);
|
||||
break;
|
||||
case ServerGeminiEventType.Content:
|
||||
geminiMessageBuffer = handleContentEvent(
|
||||
@@ -814,12 +838,15 @@ export const useGeminiStream = (
|
||||
);
|
||||
break;
|
||||
case ServerGeminiEventType.ToolCallRequest:
|
||||
flushThoughts(userMessageTimestamp);
|
||||
toolCallRequests.push(event.value);
|
||||
break;
|
||||
case ServerGeminiEventType.UserCancelled:
|
||||
flushThoughts(userMessageTimestamp);
|
||||
handleUserCancelledEvent(userMessageTimestamp);
|
||||
break;
|
||||
case ServerGeminiEventType.Error:
|
||||
flushThoughts(userMessageTimestamp);
|
||||
handleErrorEvent(event.value, userMessageTimestamp);
|
||||
break;
|
||||
case ServerGeminiEventType.ChatCompressed:
|
||||
@@ -839,6 +866,7 @@ export const useGeminiStream = (
|
||||
);
|
||||
break;
|
||||
case ServerGeminiEventType.Finished:
|
||||
flushThoughts(userMessageTimestamp);
|
||||
handleFinishedEvent(event, userMessageTimestamp);
|
||||
break;
|
||||
case ServerGeminiEventType.Citation:
|
||||
@@ -879,6 +907,7 @@ export const useGeminiStream = (
|
||||
handleContextWindowWillOverflowEvent,
|
||||
handleCitationEvent,
|
||||
handleChatModelEvent,
|
||||
flushThoughts,
|
||||
],
|
||||
);
|
||||
const submitQuery = useCallback(
|
||||
@@ -943,6 +972,7 @@ export const useGeminiStream = (
|
||||
}
|
||||
startNewPrompt();
|
||||
setThought(null); // Reset thought when starting a new prompt
|
||||
thoughtsBufferRef.current = [];
|
||||
}
|
||||
|
||||
setIsResponding(true);
|
||||
|
||||
@@ -189,6 +189,11 @@ export interface ChatDetail {
|
||||
mtime: string;
|
||||
}
|
||||
|
||||
export type HistoryItemThinking = HistoryItemBase & {
|
||||
type: 'thinking';
|
||||
thoughts: ThoughtSummary[];
|
||||
};
|
||||
|
||||
export type HistoryItemChatList = HistoryItemBase & {
|
||||
type: 'chat_list';
|
||||
chats: ChatDetail[];
|
||||
@@ -299,6 +304,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemSkillsList
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemChatList
|
||||
| HistoryItemThinking
|
||||
| HistoryItemHooksList;
|
||||
|
||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||
|
||||
Reference in New Issue
Block a user