first commit

This commit is contained in:
Dmitry Lyalin
2025-12-31 15:00:35 -05:00
parent 05049b5abf
commit 3aefc66b7b
8 changed files with 167 additions and 1 deletions

View File

@@ -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',

View File

@@ -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) => {

View File

@@ -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} />
)}

View File

@@ -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}
/>
));

View File

@@ -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)');
});
});

View 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>
);

View File

@@ -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);

View File

@@ -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 };