mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
first commit
This commit is contained in:
@@ -374,6 +374,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
description: 'Hide the window title bar',
|
description: 'Hide the window title bar',
|
||||||
showInDialog: true,
|
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: {
|
showStatusInTitle: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Show Status in Title',
|
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])(
|
describe.each([true, false])(
|
||||||
'gemini items (alternateBuffer=%s)',
|
'gemini items (alternateBuffer=%s)',
|
||||||
(useAlternateBuffer) => {
|
(useAlternateBuffer) => {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { McpStatus } from './views/McpStatus.js';
|
|||||||
import { ChatList } from './views/ChatList.js';
|
import { ChatList } from './views/ChatList.js';
|
||||||
import { HooksList } from './views/HooksList.js';
|
import { HooksList } from './views/HooksList.js';
|
||||||
import { ModelMessage } from './messages/ModelMessage.js';
|
import { ModelMessage } from './messages/ModelMessage.js';
|
||||||
|
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||||
|
|
||||||
interface HistoryItemDisplayProps {
|
interface HistoryItemDisplayProps {
|
||||||
item: HistoryItem;
|
item: HistoryItem;
|
||||||
@@ -44,6 +45,7 @@ interface HistoryItemDisplayProps {
|
|||||||
activeShellPtyId?: number | null;
|
activeShellPtyId?: number | null;
|
||||||
embeddedShellFocused?: boolean;
|
embeddedShellFocused?: boolean;
|
||||||
availableTerminalHeightGemini?: number;
|
availableTerminalHeightGemini?: number;
|
||||||
|
inlineEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||||
@@ -56,12 +58,19 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
activeShellPtyId,
|
activeShellPtyId,
|
||||||
embeddedShellFocused,
|
embeddedShellFocused,
|
||||||
availableTerminalHeightGemini,
|
availableTerminalHeightGemini,
|
||||||
|
inlineEnabled,
|
||||||
}) => {
|
}) => {
|
||||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" key={itemForDisplay.id} width={terminalWidth}>
|
<Box flexDirection="column" key={itemForDisplay.id} width={terminalWidth}>
|
||||||
{/* Render standard message types */}
|
{/* Render standard message types */}
|
||||||
|
{itemForDisplay.type === 'thinking' && inlineEnabled && (
|
||||||
|
<ThinkingMessage
|
||||||
|
thoughts={itemForDisplay.thoughts}
|
||||||
|
terminalWidth={terminalWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{itemForDisplay.type === 'user' && (
|
{itemForDisplay.type === 'user' && (
|
||||||
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
|
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ShowMoreLines } from './ShowMoreLines.js';
|
|||||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
import { useAppContext } from '../contexts/AppContext.js';
|
import { useAppContext } from '../contexts/AppContext.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
import { AppHeader } from './AppHeader.js';
|
import { AppHeader } from './AppHeader.js';
|
||||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
import { SCROLL_TO_ITEM_END } from './shared/VirtualizedList.js';
|
import { SCROLL_TO_ITEM_END } from './shared/VirtualizedList.js';
|
||||||
@@ -27,6 +28,7 @@ const MemoizedAppHeader = memo(AppHeader);
|
|||||||
export const MainContent = () => {
|
export const MainContent = () => {
|
||||||
const { version } = useAppContext();
|
const { version } = useAppContext();
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
|
const settings = useSettings();
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -36,6 +38,8 @@ export const MainContent = () => {
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
} = uiState;
|
} = uiState;
|
||||||
|
|
||||||
|
const inlineEnabled = settings.merged.ui?.showInlineThinking;
|
||||||
|
|
||||||
const historyItems = uiState.history.map((h) => (
|
const historyItems = uiState.history.map((h) => (
|
||||||
<HistoryItemDisplay
|
<HistoryItemDisplay
|
||||||
terminalWidth={mainAreaWidth}
|
terminalWidth={mainAreaWidth}
|
||||||
@@ -45,6 +49,7 @@ export const MainContent = () => {
|
|||||||
item={h}
|
item={h}
|
||||||
isPending={false}
|
isPending={false}
|
||||||
commands={uiState.slashCommands}
|
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)');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
HistoryItem,
|
||||||
HistoryItemWithoutId,
|
HistoryItemWithoutId,
|
||||||
HistoryItemToolGroup,
|
HistoryItemToolGroup,
|
||||||
|
HistoryItemThinking,
|
||||||
SlashCommandProcessorResult,
|
SlashCommandProcessorResult,
|
||||||
HistoryItemModel,
|
HistoryItemModel,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
@@ -118,8 +119,29 @@ export const useGeminiStream = (
|
|||||||
const activeQueryIdRef = useRef<string | null>(null);
|
const activeQueryIdRef = useRef<string | null>(null);
|
||||||
const [isResponding, setIsResponding] = useState<boolean>(false);
|
const [isResponding, setIsResponding] = useState<boolean>(false);
|
||||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||||
|
const thoughtsBufferRef = useRef<ThoughtSummary[]>([]);
|
||||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
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 processedMemoryToolsRef = useRef<Set<string>>(new Set());
|
||||||
const { startNewPrompt, getPromptCount } = useSessionStats();
|
const { startNewPrompt, getPromptCount } = useSessionStats();
|
||||||
const storage = config.storage;
|
const storage = config.storage;
|
||||||
@@ -535,6 +557,7 @@ export const useGeminiStream = (
|
|||||||
currentGeminiMessageBuffer: string,
|
currentGeminiMessageBuffer: string,
|
||||||
userMessageTimestamp: number,
|
userMessageTimestamp: number,
|
||||||
): string => {
|
): string => {
|
||||||
|
flushThoughts(userMessageTimestamp);
|
||||||
if (turnCancelledRef.current) {
|
if (turnCancelledRef.current) {
|
||||||
// Prevents additional output after a user initiated cancel.
|
// Prevents additional output after a user initiated cancel.
|
||||||
return '';
|
return '';
|
||||||
@@ -584,7 +607,7 @@ export const useGeminiStream = (
|
|||||||
}
|
}
|
||||||
return newGeminiMessageBuffer;
|
return newGeminiMessageBuffer;
|
||||||
},
|
},
|
||||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
[addItem, pendingHistoryItemRef, setPendingHistoryItem, flushThoughts],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUserCancelledEvent = useCallback(
|
const handleUserCancelledEvent = useCallback(
|
||||||
@@ -805,6 +828,7 @@ export const useGeminiStream = (
|
|||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case ServerGeminiEventType.Thought:
|
case ServerGeminiEventType.Thought:
|
||||||
setThought(event.value);
|
setThought(event.value);
|
||||||
|
thoughtsBufferRef.current.push(event.value);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.Content:
|
case ServerGeminiEventType.Content:
|
||||||
geminiMessageBuffer = handleContentEvent(
|
geminiMessageBuffer = handleContentEvent(
|
||||||
@@ -814,12 +838,15 @@ export const useGeminiStream = (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.ToolCallRequest:
|
case ServerGeminiEventType.ToolCallRequest:
|
||||||
|
flushThoughts(userMessageTimestamp);
|
||||||
toolCallRequests.push(event.value);
|
toolCallRequests.push(event.value);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.UserCancelled:
|
case ServerGeminiEventType.UserCancelled:
|
||||||
|
flushThoughts(userMessageTimestamp);
|
||||||
handleUserCancelledEvent(userMessageTimestamp);
|
handleUserCancelledEvent(userMessageTimestamp);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.Error:
|
case ServerGeminiEventType.Error:
|
||||||
|
flushThoughts(userMessageTimestamp);
|
||||||
handleErrorEvent(event.value, userMessageTimestamp);
|
handleErrorEvent(event.value, userMessageTimestamp);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.ChatCompressed:
|
case ServerGeminiEventType.ChatCompressed:
|
||||||
@@ -839,6 +866,7 @@ export const useGeminiStream = (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.Finished:
|
case ServerGeminiEventType.Finished:
|
||||||
|
flushThoughts(userMessageTimestamp);
|
||||||
handleFinishedEvent(event, userMessageTimestamp);
|
handleFinishedEvent(event, userMessageTimestamp);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.Citation:
|
case ServerGeminiEventType.Citation:
|
||||||
@@ -879,6 +907,7 @@ export const useGeminiStream = (
|
|||||||
handleContextWindowWillOverflowEvent,
|
handleContextWindowWillOverflowEvent,
|
||||||
handleCitationEvent,
|
handleCitationEvent,
|
||||||
handleChatModelEvent,
|
handleChatModelEvent,
|
||||||
|
flushThoughts,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const submitQuery = useCallback(
|
const submitQuery = useCallback(
|
||||||
@@ -943,6 +972,7 @@ export const useGeminiStream = (
|
|||||||
}
|
}
|
||||||
startNewPrompt();
|
startNewPrompt();
|
||||||
setThought(null); // Reset thought when starting a new prompt
|
setThought(null); // Reset thought when starting a new prompt
|
||||||
|
thoughtsBufferRef.current = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsResponding(true);
|
setIsResponding(true);
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ export interface ChatDetail {
|
|||||||
mtime: string;
|
mtime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HistoryItemThinking = HistoryItemBase & {
|
||||||
|
type: 'thinking';
|
||||||
|
thoughts: ThoughtSummary[];
|
||||||
|
};
|
||||||
|
|
||||||
export type HistoryItemChatList = HistoryItemBase & {
|
export type HistoryItemChatList = HistoryItemBase & {
|
||||||
type: 'chat_list';
|
type: 'chat_list';
|
||||||
chats: ChatDetail[];
|
chats: ChatDetail[];
|
||||||
@@ -299,6 +304,7 @@ export type HistoryItemWithoutId =
|
|||||||
| HistoryItemSkillsList
|
| HistoryItemSkillsList
|
||||||
| HistoryItemMcpStatus
|
| HistoryItemMcpStatus
|
||||||
| HistoryItemChatList
|
| HistoryItemChatList
|
||||||
|
| HistoryItemThinking
|
||||||
| HistoryItemHooksList;
|
| HistoryItemHooksList;
|
||||||
|
|
||||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||||
|
|||||||
Reference in New Issue
Block a user