diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 300338f991..21c7f96a1b 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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: { diff --git a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx index fec35d46c3..abe68ebbf9 100644 --- a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx +++ b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx @@ -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 && ( diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx index d213b489cb..e2938be0b8 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx @@ -237,7 +237,7 @@ describe('', () => { const item: HistoryItem = { ...baseItem, type: 'thinking', - thoughts: [{ subject: 'Thinking', description: 'test' }], + thought: { subject: 'Thinking', description: 'test' }, }; const { lastFrame } = renderWithProviders( , @@ -250,7 +250,7 @@ describe('', () => { const item: HistoryItem = { ...baseItem, type: 'thinking', - thoughts: [{ subject: 'Thinking', description: 'test' }], + thought: { subject: 'Thinking', description: 'test' }, }; const { lastFrame } = renderWithProviders( , diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index f83c2d34d9..8b13a004a4 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -68,7 +68,7 @@ export const HistoryItemDisplay: React.FC = ({ {/* Render standard message types */} {itemForDisplay.type === 'thinking' && inlineEnabled && ( { availableTerminalHeight, } = uiState; - const inlineEnabled = settings.merged.ui?.showInlineThinking; + const inlineEnabled = getInlineThinkingMode(settings) !== 'off'; const historyItems = useMemo( () => diff --git a/packages/cli/src/ui/components/QuittingDisplay.test.tsx b/packages/cli/src/ui/components/QuittingDisplay.test.tsx index 79cc7e5d7b..ab20a12d83 100644 --- a/packages/cli/src/ui/components/QuittingDisplay.test.tsx +++ b/packages/cli/src/ui/components/QuittingDisplay.test.tsx @@ -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'); diff --git a/packages/cli/src/ui/components/QuittingDisplay.tsx b/packages/cli/src/ui/components/QuittingDisplay.tsx index ee81f92012..f2770b998d 100644 --- a/packages/cli/src/ui/components/QuittingDisplay.tsx +++ b/packages/cli/src/ui/components/QuittingDisplay.tsx @@ -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} /> ))} diff --git a/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx b/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx index 582776bb82..59c62b3cb0 100644 --- a/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx @@ -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( , ); expect(lastFrame()).toContain('Thinking'); - expect(lastFrame()).toContain('(2)'); }); - it('renders with single thought', () => { + it('renders with thought subject', () => { const { lastFrame } = render( , ); - expect(lastFrame()).toContain('(1)'); + expect(lastFrame()).toContain('Processing'); }); it('renders thought content', () => { const { lastFrame } = render( , ); @@ -51,9 +48,12 @@ describe('ThinkingMessage', () => { it('renders empty state gracefully', () => { const { lastFrame } = render( - , + , ); - expect(lastFrame()).toContain('(0)'); + expect(lastFrame()).toContain('Thinking'); }); }); diff --git a/packages/cli/src/ui/components/messages/ThinkingMessage.tsx b/packages/cli/src/ui/components/messages/ThinkingMessage.tsx index e1453add44..3c2426d918 100644 --- a/packages/cli/src/ui/components/messages/ThinkingMessage.tsx +++ b/packages/cli/src/ui/components/messages/ThinkingMessage.tsx @@ -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 = ({ - 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 = ({ Thinking - ({thoughts.length}) - {thoughts.map((thought, index) => ( - - {thought.subject && ( + {(subject || description) && ( + + {subject && ( - {thought.subject} + {subject} )} - {thought.description || ' '} + {description && {description}} - ))} + )} ); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 66ffa35799..aaa8d9c440 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -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); diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index df9600705a..9f99af1f0c 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -212,7 +212,7 @@ export interface ChatDetail { export type HistoryItemThinking = HistoryItemBase & { type: 'thinking'; - thoughts: ThoughtSummary[]; + thought: ThoughtSummary; }; export type HistoryItemChatList = HistoryItemBase & {