diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 7672b9e1c4..097caeeb18 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -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',
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
index 8488a78dfb..a22fcd20a6 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
@@ -207,6 +207,34 @@ describe('', () => {
);
});
+ describe('thinking items', () => {
+ it('renders thinking item when enabled', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: 'thinking',
+ thoughts: [{ subject: 'Thinking', description: 'test' }],
+ };
+ const { lastFrame } = renderWithProviders(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ expect(lastFrame()).toBe('');
+ });
+ });
+
describe.each([true, false])(
'gemini items (alternateBuffer=%s)',
(useAlternateBuffer) => {
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 5a7f769402..3f28b83535 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -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 = ({
@@ -56,12 +58,19 @@ export const HistoryItemDisplay: React.FC = ({
activeShellPtyId,
embeddedShellFocused,
availableTerminalHeightGemini,
+ inlineEnabled,
}) => {
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
return (
{/* Render standard message types */}
+ {itemForDisplay.type === 'thinking' && inlineEnabled && (
+
+ )}
{itemForDisplay.type === 'user' && (
)}
diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx
index a60f782d8f..11c97e53a8 100644
--- a/packages/cli/src/ui/components/MainContent.tsx
+++ b/packages/cli/src/ui/components/MainContent.tsx
@@ -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) => (
{
item={h}
isPending={false}
commands={uiState.slashCommands}
+ inlineEnabled={inlineEnabled}
/>
));
diff --git a/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx b/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx
new file mode 100644
index 0000000000..18ad73bd07
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ThinkingMessage.test.tsx
@@ -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(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Thinking');
+ expect(lastFrame()).toContain('(2)');
+ });
+
+ it('renders with single thought', () => {
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('(1)');
+ });
+
+ it('renders empty state gracefully', () => {
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('(0)');
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/ThinkingMessage.tsx b/packages/cli/src/ui/components/messages/ThinkingMessage.tsx
new file mode 100644
index 0000000000..a2eda6c375
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ThinkingMessage.tsx
@@ -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 = ({
+ thoughts,
+ terminalWidth,
+}) => (
+
+ ◆
+
+ Thinking
+
+ ({thoughts.length})
+
+);
diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts
index d36d9f57ed..7791541fea 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.ts
+++ b/packages/cli/src/ui/hooks/useGeminiStream.ts
@@ -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(null);
const [isResponding, setIsResponding] = useState(false);
const [thought, setThought] = useState(null);
+ const thoughtsBufferRef = useRef([]);
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
useStateAndRef(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>(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);
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index ede5ab5b84..d3c307978c 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -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 };