mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 16:10:59 -07:00
Show inline thoughts as individual bubbles
This commit is contained in:
@@ -380,7 +380,25 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Show model thinking summaries inline in the conversation.',
|
||||
'Show model thinking summaries inline in the conversation (deprecated; prefer the specific thinking modes).',
|
||||
showInDialog: true,
|
||||
},
|
||||
showInlineThinkingFull: {
|
||||
type: 'boolean',
|
||||
label: 'Show Inline Thinking (Full)',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Show full model thinking details inline.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showInlineThinkingSummary: {
|
||||
type: 'boolean',
|
||||
label: 'Show Inline Thinking (Summary)',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Show a short summary of model thinking inline.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showStatusInTitle: {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||
import { QuittingDisplay } from './QuittingDisplay.js';
|
||||
@@ -15,15 +16,18 @@ import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
|
||||
export const AlternateBufferQuittingDisplay = () => {
|
||||
const { version } = useAppContext();
|
||||
const uiState = useUIState();
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
|
||||
const confirmingTool = useConfirmingTool();
|
||||
const showPromptedTool =
|
||||
config.isEventDrivenSchedulerEnabled() && confirmingTool !== null;
|
||||
const inlineEnabled = getInlineThinkingMode(settings) !== 'off';
|
||||
|
||||
// We render the entire chat history and header here to ensure that the
|
||||
// conversation history is visible to the user after the app quits and the
|
||||
@@ -47,6 +51,7 @@ export const AlternateBufferQuittingDisplay = () => {
|
||||
item={h}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
inlineEnabled={inlineEnabled}
|
||||
/>
|
||||
))}
|
||||
{uiState.pendingHistoryItems.map((item, i) => (
|
||||
@@ -59,6 +64,7 @@ export const AlternateBufferQuittingDisplay = () => {
|
||||
isFocused={false}
|
||||
activeShellPtyId={uiState.activePtyId}
|
||||
embeddedShellFocused={uiState.embeddedShellFocused}
|
||||
inlineEnabled={inlineEnabled}
|
||||
/>
|
||||
))}
|
||||
{showPromptedTool && (
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('<HistoryItemDisplay />', () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'thinking',
|
||||
thoughts: [{ subject: 'Thinking', description: 'test' }],
|
||||
thought: { subject: 'Thinking', description: 'test' },
|
||||
};
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} inlineEnabled={true} />,
|
||||
@@ -250,7 +250,7 @@ describe('<HistoryItemDisplay />', () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'thinking',
|
||||
thoughts: [{ subject: 'Thinking', description: 'test' }],
|
||||
thought: { subject: 'Thinking', description: 'test' },
|
||||
};
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} inlineEnabled={false} />,
|
||||
|
||||
@@ -68,7 +68,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{/* Render standard message types */}
|
||||
{itemForDisplay.type === 'thinking' && inlineEnabled && (
|
||||
<ThinkingMessage
|
||||
thoughts={itemForDisplay.thoughts}
|
||||
thought={itemForDisplay.thought}
|
||||
terminalWidth={terminalWidth}
|
||||
availableTerminalHeight={
|
||||
isPending ? availableTerminalHeight : undefined
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
||||
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
|
||||
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
|
||||
const MemoizedAppHeader = memo(AppHeader);
|
||||
@@ -55,7 +56,7 @@ export const MainContent = () => {
|
||||
availableTerminalHeight,
|
||||
} = uiState;
|
||||
|
||||
const inlineEnabled = settings.merged.ui?.showInlineThinking;
|
||||
const inlineEnabled = getInlineThinkingMode(settings) !== 'off';
|
||||
|
||||
const historyItems = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -12,6 +12,17 @@ import { useUIState, type UIState } from '../contexts/UIStateContext.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js');
|
||||
vi.mock('../contexts/SettingsContext.js', () => ({
|
||||
useSettings: () => ({
|
||||
merged: {
|
||||
ui: {
|
||||
showInlineThinking: false,
|
||||
showInlineThinkingFull: false,
|
||||
showInlineThinkingSummary: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
vi.mock('./HistoryItemDisplay.js', async () => {
|
||||
const { Text } = await vi.importActual('ink');
|
||||
|
||||
@@ -6,14 +6,18 @@
|
||||
|
||||
import { Box } from 'ink';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
|
||||
export const QuittingDisplay = () => {
|
||||
const uiState = useUIState();
|
||||
const settings = useSettings();
|
||||
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
|
||||
|
||||
const availableTerminalHeight = terminalHeight;
|
||||
const inlineEnabled = getInlineThinkingMode(settings) !== 'off';
|
||||
|
||||
if (!uiState.quittingMessages) {
|
||||
return null;
|
||||
@@ -30,6 +34,7 @@ export const QuittingDisplay = () => {
|
||||
terminalWidth={terminalWidth}
|
||||
item={item}
|
||||
isPending={false}
|
||||
inlineEnabled={inlineEnabled}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -9,38 +9,35 @@ import { render } from '../../../test-utils/render.js';
|
||||
import { ThinkingMessage } from './ThinkingMessage.js';
|
||||
|
||||
describe('ThinkingMessage', () => {
|
||||
it('renders thinking header with count', () => {
|
||||
it('renders thinking header', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage
|
||||
thoughts={[
|
||||
{ subject: 'Planning', description: 'test' },
|
||||
{ subject: 'Analyzing', description: 'test' },
|
||||
]}
|
||||
thought={{ subject: 'Planning', description: 'test' }}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Thinking');
|
||||
expect(lastFrame()).toContain('(2)');
|
||||
});
|
||||
|
||||
it('renders with single thought', () => {
|
||||
it('renders with thought subject', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage
|
||||
thoughts={[{ subject: 'Processing', description: 'test' }]}
|
||||
thought={{ subject: 'Processing', description: 'test' }}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('(1)');
|
||||
expect(lastFrame()).toContain('Processing');
|
||||
});
|
||||
|
||||
it('renders thought content', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage
|
||||
thoughts={[
|
||||
{ subject: 'Planning', description: 'I am planning the solution.' },
|
||||
]}
|
||||
thought={{
|
||||
subject: 'Planning',
|
||||
description: 'I am planning the solution.',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
@@ -51,9 +48,12 @@ describe('ThinkingMessage', () => {
|
||||
|
||||
it('renders empty state gracefully', () => {
|
||||
const { lastFrame } = render(
|
||||
<ThinkingMessage thoughts={[]} terminalWidth={80} />,
|
||||
<ThinkingMessage
|
||||
thought={{ subject: '', description: '' }}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('(0)');
|
||||
expect(lastFrame()).toContain('Thinking');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,16 +10,18 @@ import type { ThoughtSummary } from '@google/gemini-cli-core';
|
||||
import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js';
|
||||
|
||||
interface ThinkingMessageProps {
|
||||
thoughts: ThoughtSummary[];
|
||||
thought: ThoughtSummary;
|
||||
terminalWidth: number;
|
||||
availableTerminalHeight?: number;
|
||||
}
|
||||
|
||||
export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
|
||||
thoughts,
|
||||
thought,
|
||||
terminalWidth,
|
||||
availableTerminalHeight,
|
||||
}) => {
|
||||
const subject = thought.subject.trim();
|
||||
const description = thought.description.trim();
|
||||
const contentMaxHeight =
|
||||
availableTerminalHeight !== undefined
|
||||
? Math.max(availableTerminalHeight - 4, MINIMUM_MAX_HEIGHT)
|
||||
@@ -39,23 +41,22 @@ export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
|
||||
<Text bold color="magenta">
|
||||
Thinking
|
||||
</Text>
|
||||
<Text dimColor> ({thoughts.length})</Text>
|
||||
</Box>
|
||||
<MaxSizedBox
|
||||
maxHeight={contentMaxHeight}
|
||||
maxWidth={terminalWidth - 2}
|
||||
overflowDirection="top"
|
||||
>
|
||||
{thoughts.map((thought, index) => (
|
||||
<Box key={index} marginTop={1} flexDirection="column">
|
||||
{thought.subject && (
|
||||
{(subject || description) && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{subject && (
|
||||
<Text bold color="magenta">
|
||||
{thought.subject}
|
||||
{subject}
|
||||
</Text>
|
||||
)}
|
||||
<Text>{thought.description || ' '}</Text>
|
||||
{description && <Text>{description}</Text>}
|
||||
</Box>
|
||||
))}
|
||||
)}
|
||||
</MaxSizedBox>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -63,6 +63,7 @@ import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { useShellCommandProcessor } from './shellCommandProcessor.js';
|
||||
import { handleAtCommand } from './atCommandProcessor.js';
|
||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { useStateAndRef } from './useStateAndRef.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { useLogger } from './useLogger.js';
|
||||
@@ -78,6 +79,29 @@ import {
|
||||
} from './useToolScheduler.js';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const MAX_THOUGHT_SUMMARY_LENGTH = 140;
|
||||
|
||||
function summarizeThought(thought: ThoughtSummary): ThoughtSummary {
|
||||
const subject = thought.subject.trim();
|
||||
if (subject) {
|
||||
return { subject, description: '' };
|
||||
}
|
||||
|
||||
const description = thought.description.trim();
|
||||
if (!description) {
|
||||
return { subject: '', description: '' };
|
||||
}
|
||||
|
||||
if (description.length <= MAX_THOUGHT_SUMMARY_LENGTH) {
|
||||
return { subject: description, description: '' };
|
||||
}
|
||||
|
||||
const trimmed = description
|
||||
.slice(0, MAX_THOUGHT_SUMMARY_LENGTH - 3)
|
||||
.trimEnd();
|
||||
return { subject: `${trimmed}...`, description: '' };
|
||||
}
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
@@ -762,7 +786,7 @@ export const useGeminiStream = (
|
||||
pendingHistoryItemRef.current?.type !== 'gemini' &&
|
||||
pendingHistoryItemRef.current?.type !== 'gemini_content'
|
||||
) {
|
||||
// Flush any pending item (including thinking items) before starting gemini content
|
||||
// Flush any pending item before starting gemini content
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
}
|
||||
@@ -810,34 +834,25 @@ export const useGeminiStream = (
|
||||
(eventValue: ThoughtSummary, userMessageTimestamp: number) => {
|
||||
setThought(eventValue);
|
||||
|
||||
// Only accumulate thoughts in history if inline thinking is enabled
|
||||
if (!settings.merged.ui?.showInlineThinking) {
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
if (inlineThinkingMode === 'off') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingHistoryItemRef.current?.type === 'thinking') {
|
||||
// Accumulate thoughts in the existing thinking item
|
||||
setPendingHistoryItem((prev) => ({
|
||||
const thoughtForDisplay =
|
||||
inlineThinkingMode === 'summary'
|
||||
? summarizeThought(eventValue)
|
||||
: eventValue;
|
||||
|
||||
addItem(
|
||||
{
|
||||
type: 'thinking',
|
||||
thoughts: [...(prev as HistoryItemThinking).thoughts, eventValue],
|
||||
}));
|
||||
} else {
|
||||
// Flush any existing pending item and start a new thinking item
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
}
|
||||
setPendingHistoryItem({
|
||||
type: 'thinking',
|
||||
thoughts: [eventValue],
|
||||
} as HistoryItemThinking);
|
||||
}
|
||||
thought: thoughtForDisplay,
|
||||
} as HistoryItemThinking,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
},
|
||||
[
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
settings.merged.ui?.showInlineThinking,
|
||||
],
|
||||
[addItem, settings],
|
||||
);
|
||||
|
||||
const handleUserCancelledEvent = useCallback(
|
||||
@@ -1279,10 +1294,6 @@ export const useGeminiStream = (
|
||||
}
|
||||
startNewPrompt();
|
||||
setThought(null); // Reset thought when starting a new prompt
|
||||
// Clear any pending thinking item from previous prompt
|
||||
if (pendingHistoryItemRef.current?.type === 'thinking') {
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
}
|
||||
|
||||
setIsResponding(true);
|
||||
|
||||
@@ -212,7 +212,7 @@ export interface ChatDetail {
|
||||
|
||||
export type HistoryItemThinking = HistoryItemBase & {
|
||||
type: 'thinking';
|
||||
thoughts: ThoughtSummary[];
|
||||
thought: ThoughtSummary;
|
||||
};
|
||||
|
||||
export type HistoryItemChatList = HistoryItemBase & {
|
||||
|
||||
Reference in New Issue
Block a user