Compare commits

..

3 Commits

Author SHA1 Message Date
Christine Betts 675b3d7834 test docs change 2026-02-02 17:20:03 -05:00
Jack Wotherspoon ae672881d1 docs: update clearContext to hookSpecificOutput (#18024) 2026-02-02 17:32:37 +00:00
Jerop Kipruto 9e7c10ad88 feat(plan): use placeholder for choice question "Other" option (#18101) 2026-02-02 17:00:13 +00:00
30 changed files with 135 additions and 472 deletions
-2
View File
@@ -42,8 +42,6 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| ------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Show Inline Thinking (Full) | `ui.showInlineThinkingFull` | Show full model thinking details inline. | `false` |
| Show Inline Thinking (Summary) | `ui.showInlineThinkingSummary` | Show a short summary of model thinking inline. | `false` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
+1 -1
View File
@@ -21,7 +21,7 @@ the full instructions and resources required to complete the task using the
## Key Benefits
- **Shared Expertise:** Package complex workflows (like a specific team's PR
- **Shared Expertise:** Package complex workflows (like a a specific team's PR
review process) into a folder that anyone can use.
- **Repeatable Workflows:** Ensure complex multi-step tasks are performed
consistently by providing a procedural framework.
-8
View File
@@ -179,14 +179,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`ui.showInlineThinkingFull`** (boolean):
- **Description:** Show full model thinking details inline.
- **Default:** `false`
- **`ui.showInlineThinkingSummary`** (boolean):
- **Description:** Show a short summary of model thinking inline.
- **Default:** `false`
- **`ui.showStatusInTitle`** (boolean):
- **Description:** Show Gemini CLI model thoughts in the terminal window title
during the working phase
+2 -2
View File
@@ -167,8 +167,8 @@ case is response validation and automatic retries.
- `reason`: Required if denied. This text is sent **to the agent as a new
prompt** to request a correction.
- `continue`: Set to `false` to **stop the session** without retrying.
- `clearContext`: If `true`, clears conversation history (LLM memory) while
preserving UI display.
- `hookSpecificOutput.clearContext`: If `true`, clears conversation history
(LLM memory) while preserving UI display.
- **Exit Code 2 (Retry)**: Rejects the response and triggers an automatic retry
turn using `stderr` as the feedback prompt.
-18
View File
@@ -373,24 +373,6 @@ const SETTINGS_SCHEMA = {
description: 'Hide the window title bar',
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: {
type: 'boolean',
label: 'Show Thoughts in Title',
-1
View File
@@ -42,7 +42,6 @@ vi.mock('../ui/utils/terminalUtils.js', () => ({
isLowColorDepth: vi.fn(() => false),
getColorDepth: vi.fn(() => 24),
isITerm2: vi.fn(() => false),
shouldUseEmoji: vi.fn(() => true),
}));
// Wrapper around ink-testing-library's render that ensures act() is called
@@ -6,7 +6,6 @@
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';
@@ -16,18 +15,15 @@ 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
@@ -51,7 +47,6 @@ export const AlternateBufferQuittingDisplay = () => {
item={h}
isPending={false}
commands={uiState.slashCommands}
inlineEnabled={inlineEnabled}
/>
))}
{uiState.pendingHistoryItems.map((item, i) => (
@@ -64,7 +59,6 @@ export const AlternateBufferQuittingDisplay = () => {
isFocused={false}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
inlineEnabled={inlineEnabled}
/>
))}
{showPromptedTool && (
@@ -1008,4 +1008,71 @@ describe('AskUserDialog', () => {
// Should contain the full long question (or at least its parts)
expect(lastFrame()).toContain('This is a very long question');
});
describe('Choice question placeholder', () => {
it('uses placeholder for "Other" option when provided', async () => {
const questions: Question[] = [
{
question: 'Select your preferred language:',
header: 'Language',
options: [
{ label: 'TypeScript', description: '' },
{ label: 'JavaScript', description: '' },
],
placeholder: 'Type another language...',
multiSelect: false,
},
];
const { stdin, lastFrame } = renderWithProviders(
<AskUserDialog
questions={questions}
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={80}
/>,
{ width: 80 },
);
// Navigate to the "Other" option
writeKey(stdin, '\x1b[B'); // Down
writeKey(stdin, '\x1b[B'); // Down to Other
await waitFor(() => {
expect(lastFrame()).toMatchSnapshot();
});
});
it('uses default placeholder when not provided', async () => {
const questions: Question[] = [
{
question: 'Select your preferred language:',
header: 'Language',
options: [
{ label: 'TypeScript', description: '' },
{ label: 'JavaScript', description: '' },
],
multiSelect: false,
},
];
const { stdin, lastFrame } = renderWithProviders(
<AskUserDialog
questions={questions}
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={80}
/>,
{ width: 80 },
);
// Navigate to the "Other" option
writeKey(stdin, '\x1b[B'); // Down
writeKey(stdin, '\x1b[B'); // Down to Other
await waitFor(() => {
expect(lastFrame()).toMatchSnapshot();
});
});
});
});
@@ -780,7 +780,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
// Render inline text input for custom option
if (optionItem.type === 'other') {
const placeholder = 'Enter a custom value';
const placeholder = question.placeholder || 'Enter a custom value';
return (
<Box flexDirection="row">
{showCheck && (
@@ -232,34 +232,6 @@ describe('<HistoryItemDisplay />', () => {
);
});
describe('thinking items', () => {
it('renders thinking item when enabled', () => {
const item: HistoryItem = {
...baseItem,
type: 'thinking',
thought: { 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',
thought: { 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) => {
@@ -34,7 +34,6 @@ 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;
@@ -46,7 +45,6 @@ interface HistoryItemDisplayProps {
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
availableTerminalHeightGemini?: number;
inlineEnabled?: boolean;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
@@ -59,22 +57,12 @@ 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
thought={itemForDisplay.thought}
terminalWidth={terminalWidth}
availableTerminalHeight={
isPending ? availableTerminalHeight : undefined
}
/>
)}
{itemForDisplay.type === 'user' && (
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
)}
@@ -12,21 +12,6 @@ import { Box, Text } from 'ink';
import type React from 'react';
// Mock dependencies
vi.mock('../contexts/SettingsContext.js', async () => {
const actual = await vi.importActual('../contexts/SettingsContext.js');
return {
...actual,
useSettings: () => ({
merged: {
ui: {
showInlineThinkingFull: false,
showInlineThinkingSummary: false,
},
},
}),
};
});
vi.mock('../contexts/AppContext.js', async () => {
const actual = await vi.importActual('../contexts/AppContext.js');
return {
+1 -17
View File
@@ -8,7 +8,6 @@ import { Box, Static } from 'ink';
import { HistoryItemDisplay } from './HistoryItemDisplay.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 {
@@ -21,7 +20,6 @@ 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);
@@ -33,7 +31,6 @@ const MemoizedAppHeader = memo(AppHeader);
export const MainContent = () => {
const { version } = useAppContext();
const uiState = useUIState();
const settings = useSettings();
const config = useConfig();
const isAlternateBuffer = useAlternateBuffer();
@@ -56,8 +53,6 @@ export const MainContent = () => {
availableTerminalHeight,
} = uiState;
const inlineEnabled = getInlineThinkingMode(settings) !== 'off';
const historyItems = useMemo(
() =>
uiState.history.map((h) => (
@@ -69,7 +64,6 @@ export const MainContent = () => {
item={h}
isPending={false}
commands={uiState.slashCommands}
inlineEnabled={inlineEnabled}
/>
)),
[
@@ -77,7 +71,6 @@ export const MainContent = () => {
mainAreaWidth,
staticAreaMaxItemHeight,
uiState.slashCommands,
inlineEnabled,
],
);
@@ -98,7 +91,6 @@ export const MainContent = () => {
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
inlineEnabled={inlineEnabled}
/>
))}
{showConfirmationQueue && confirmingTool && (
@@ -112,7 +104,6 @@ export const MainContent = () => {
isAlternateBuffer,
availableTerminalHeight,
mainAreaWidth,
inlineEnabled,
uiState.isEditorDialogOpen,
uiState.activePtyId,
uiState.embeddedShellFocused,
@@ -144,20 +135,13 @@ export const MainContent = () => {
item={item.item}
isPending={false}
commands={uiState.slashCommands}
inlineEnabled={inlineEnabled}
/>
);
} else {
return pendingItems;
}
},
[
version,
mainAreaWidth,
uiState.slashCommands,
inlineEnabled,
pendingItems,
],
[version, mainAreaWidth, uiState.slashCommands, pendingItems],
);
if (isAlternateBuffer) {
@@ -12,16 +12,6 @@ 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: {
showInlineThinkingFull: false,
showInlineThinkingSummary: false,
},
},
}),
}));
vi.mock('../hooks/useTerminalSize.js');
vi.mock('./HistoryItemDisplay.js', async () => {
const { Text } = await vi.importActual('ink');
@@ -6,18 +6,14 @@
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;
@@ -34,7 +30,6 @@ export const QuittingDisplay = () => {
terminalWidth={terminalWidth}
item={item}
isPending={false}
inlineEnabled={inlineEnabled}
/>
))}
</Box>
@@ -137,7 +137,7 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
/>
</Box>
<Box
height={1}
height={0}
width={mainAreaWidth}
borderLeft={true}
borderRight={true}
@@ -1,5 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 1`] = `
"Select your preferred language:
1. TypeScript
2. JavaScript
● 3. Enter a custom value
Enter to submit · Esc to cancel"
`;
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = `
"Select your preferred language:
1. TypeScript
2. JavaScript
● 3. Type another language...
Enter to submit · Esc to cancel"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
"Choose an option
@@ -1,59 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { render } from '../../../test-utils/render.js';
import { ThinkingMessage } from './ThinkingMessage.js';
describe('ThinkingMessage', () => {
it('renders thinking subject', () => {
const { lastFrame } = render(
<ThinkingMessage
thought={{ subject: 'Planning', description: 'test' }}
terminalWidth={80}
/>,
);
expect(lastFrame()).toContain('Planning');
});
it('renders with thought subject', () => {
const { lastFrame } = render(
<ThinkingMessage
thought={{ subject: 'Processing', description: 'test' }}
terminalWidth={80}
/>,
);
expect(lastFrame()).toContain('Processing');
});
it('renders thought content', () => {
const { lastFrame } = render(
<ThinkingMessage
thought={{
subject: 'Planning',
description: 'I am planning the solution.',
}}
terminalWidth={80}
/>,
);
expect(lastFrame()).toContain('Planning');
expect(lastFrame()).toContain('I am planning the solution.');
});
it('renders empty state gracefully', () => {
const { lastFrame } = render(
<ThinkingMessage
thought={{ subject: '', description: '' }}
terminalWidth={80}
/>,
);
expect(lastFrame()).not.toContain('Planning');
});
});
@@ -1,62 +0,0 @@
/**
* @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';
import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js';
import { IconText } from '../shared/IconText.js';
interface ThinkingMessageProps {
thought: ThoughtSummary;
terminalWidth: number;
availableTerminalHeight?: number;
}
export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
thought,
terminalWidth,
availableTerminalHeight,
}) => {
const subject = thought.subject.trim();
const description = thought.description.trim();
const headerText = subject || description;
const bodyText = subject ? description : '';
const contentMaxWidth = Math.max(terminalWidth - 4, 1);
const contentMaxHeight =
availableTerminalHeight !== undefined
? Math.max(availableTerminalHeight - 4, MINIMUM_MAX_HEIGHT)
: undefined;
return (
<Box
borderStyle="round"
borderColor="magenta"
width={terminalWidth}
paddingX={1}
marginBottom={1}
flexDirection="column"
>
<MaxSizedBox
maxHeight={contentMaxHeight}
maxWidth={contentMaxWidth}
overflowDirection="top"
>
{headerText && (
<Box flexDirection="column">
<IconText
icon="💬"
fallbackIcon="◆"
text={headerText}
color="magenta"
bold
/>
{bodyText && <Text>{bodyText}</Text>}
</Box>
)}
</MaxSizedBox>
</Box>
);
};
@@ -239,7 +239,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
*/
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
<Box
height={1}
height={0}
width={terminalWidth}
borderLeft={true}
borderRight={true}
@@ -1,32 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Text } from 'ink';
import { shouldUseEmoji } from '../../utils/terminalUtils.js';
interface IconTextProps {
icon: string;
fallbackIcon?: string;
text: string;
color?: string;
bold?: boolean;
}
export const IconText: React.FC<IconTextProps> = ({
icon,
fallbackIcon,
text,
color,
bold,
}) => {
const resolvedIcon = shouldUseEmoji() ? icon : (fallbackIcon ?? icon);
return (
<Text color={color} bold={bold}>
{resolvedIcon} {text}
</Text>
);
};
+1 -67
View File
@@ -53,7 +53,6 @@ import type {
HistoryItem,
HistoryItemWithoutId,
HistoryItemToolGroup,
HistoryItemThinking,
IndividualToolCallDisplay,
SlashCommandProcessorResult,
HistoryItemModel,
@@ -63,7 +62,6 @@ 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';
@@ -79,42 +77,6 @@ import {
} from './useToolScheduler.js';
import { promises as fs } from 'node:fs';
import path from 'node:path';
const MAX_THOUGHT_SUMMARY_LENGTH = 140;
function splitGraphemes(value: string): string[] {
if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
const segmenter = new Intl.Segmenter(undefined, {
granularity: 'grapheme',
});
return Array.from(segmenter.segment(value), (segment) => segment.segment);
}
return Array.from(value);
}
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: '' };
}
const descriptionGraphemes = splitGraphemes(description);
if (descriptionGraphemes.length <= MAX_THOUGHT_SUMMARY_LENGTH) {
return { subject: description, description: '' };
}
const trimmed = descriptionGraphemes
.slice(0, MAX_THOUGHT_SUMMARY_LENGTH - 3)
.join('')
.trimEnd();
return { subject: `${trimmed}...`, description: '' };
}
import { useSessionStats } from '../contexts/SessionContext.js';
import { useKeypress } from './useKeypress.js';
import type { LoadedSettings } from '../../config/settings.js';
@@ -234,7 +196,6 @@ export const useGeminiStream = (
const [thought, setThought] = useState<ThoughtSummary | null>(null);
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
useStateAndRef<HistoryItemWithoutId | null>(null);
const [lastGeminiActivityTime, setLastGeminiActivityTime] =
useState<number>(0);
const [pushedToolCallIds, pushedToolCallIdsRef, setPushedToolCallIds] =
@@ -799,7 +760,6 @@ export const useGeminiStream = (
pendingHistoryItemRef.current?.type !== 'gemini' &&
pendingHistoryItemRef.current?.type !== 'gemini_content'
) {
// Flush any pending item before starting gemini content
if (pendingHistoryItemRef.current) {
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
}
@@ -843,31 +803,6 @@ export const useGeminiStream = (
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
);
const handleThoughtEvent = useCallback(
(eventValue: ThoughtSummary, userMessageTimestamp: number) => {
setThought(eventValue);
const inlineThinkingMode = getInlineThinkingMode(settings);
if (inlineThinkingMode === 'off') {
return;
}
const thoughtForDisplay =
inlineThinkingMode === 'summary'
? summarizeThought(eventValue)
: eventValue;
addItem(
{
type: 'thinking',
thought: thoughtForDisplay,
} as HistoryItemThinking,
userMessageTimestamp,
);
},
[addItem, settings],
);
const handleUserCancelledEvent = useCallback(
(userMessageTimestamp: number) => {
if (turnCancelledRef.current) {
@@ -1140,7 +1075,7 @@ export const useGeminiStream = (
switch (event.type) {
case ServerGeminiEventType.Thought:
setLastGeminiActivityTime(Date.now());
handleThoughtEvent(event.value, userMessageTimestamp);
setThought(event.value);
break;
case ServerGeminiEventType.Content:
setLastGeminiActivityTime(Date.now());
@@ -1227,7 +1162,6 @@ export const useGeminiStream = (
},
[
handleContentEvent,
handleThoughtEvent,
handleUserCancelledEvent,
handleErrorEvent,
scheduleToolCalls,
-6
View File
@@ -210,11 +210,6 @@ export interface ChatDetail {
mtime: string;
}
export type HistoryItemThinking = HistoryItemBase & {
type: 'thinking';
thought: ThoughtSummary;
};
export type HistoryItemChatList = HistoryItemBase & {
type: 'chat_list';
chats: ChatDetail[];
@@ -338,7 +333,6 @@ export type HistoryItemWithoutId =
| HistoryItemAgentsList
| HistoryItemMcpStatus
| HistoryItemChatList
| HistoryItemThinking
| HistoryItemHooksList;
export type HistoryItem = HistoryItemWithoutId & { id: number };
@@ -1,25 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { LoadedSettings } from '../../config/settings.js';
export type InlineThinkingMode = 'off' | 'summary' | 'full';
export function getInlineThinkingMode(
settings: LoadedSettings,
): InlineThinkingMode {
const ui = settings.merged.ui;
if (ui?.showInlineThinkingFull) {
return 'full';
}
if (ui?.showInlineThinkingSummary) {
return 'summary';
}
return 'off';
}
+20 -55
View File
@@ -5,16 +5,12 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { isITerm2, resetITerm2Cache, shouldUseEmoji } from './terminalUtils.js';
import { isITerm2, resetITerm2Cache } from './terminalUtils.js';
describe('terminalUtils', () => {
beforeEach(() => {
vi.stubEnv('TERM_PROGRAM', '');
vi.stubEnv('ITERM_SESSION_ID', '');
vi.stubEnv('LC_ALL', '');
vi.stubEnv('LC_CTYPE', '');
vi.stubEnv('LANG', '');
vi.stubEnv('TERM', '');
resetITerm2Cache();
});
@@ -23,61 +19,30 @@ describe('terminalUtils', () => {
vi.restoreAllMocks();
});
describe('isITerm2', () => {
it('should detect iTerm2 via TERM_PROGRAM', () => {
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
expect(isITerm2()).toBe(true);
});
it('should detect iTerm2 via ITERM_SESSION_ID', () => {
vi.stubEnv('ITERM_SESSION_ID', 'w0t0p0:6789...');
expect(isITerm2()).toBe(true);
});
it('should return false if not iTerm2', () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
expect(isITerm2()).toBe(false);
});
it('should cache the result', () => {
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
expect(isITerm2()).toBe(true);
// Change env but should still be true due to cache
vi.stubEnv('TERM_PROGRAM', 'vscode');
expect(isITerm2()).toBe(true);
resetITerm2Cache();
expect(isITerm2()).toBe(false);
});
it('should detect iTerm2 via TERM_PROGRAM', () => {
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
expect(isITerm2()).toBe(true);
});
describe('shouldUseEmoji', () => {
it('should return true when UTF-8 is supported', () => {
vi.stubEnv('LANG', 'en_US.UTF-8');
expect(shouldUseEmoji()).toBe(true);
});
it('should detect iTerm2 via ITERM_SESSION_ID', () => {
vi.stubEnv('ITERM_SESSION_ID', 'w0t0p0:6789...');
expect(isITerm2()).toBe(true);
});
it('should return true when utf8 (no hyphen) is supported', () => {
vi.stubEnv('LANG', 'en_US.utf8');
expect(shouldUseEmoji()).toBe(true);
});
it('should return false if not iTerm2', () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
expect(isITerm2()).toBe(false);
});
it('should check LC_ALL first', () => {
vi.stubEnv('LC_ALL', 'en_US.UTF-8');
vi.stubEnv('LANG', 'C');
expect(shouldUseEmoji()).toBe(true);
});
it('should cache the result', () => {
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
expect(isITerm2()).toBe(true);
it('should return false when UTF-8 is not supported', () => {
vi.stubEnv('LANG', 'C');
expect(shouldUseEmoji()).toBe(false);
});
// Change env but should still be true due to cache
vi.stubEnv('TERM_PROGRAM', 'vscode');
expect(isITerm2()).toBe(true);
it('should return false on linux console (TERM=linux)', () => {
vi.stubEnv('LANG', 'en_US.UTF-8');
vi.stubEnv('TERM', 'linux');
expect(shouldUseEmoji()).toBe(false);
});
resetITerm2Cache();
expect(isITerm2()).toBe(false);
});
});
@@ -45,25 +45,3 @@ export function isITerm2(): boolean {
export function resetITerm2Cache(): void {
cachedIsITerm2 = undefined;
}
/**
* Returns true if the terminal likely supports emoji.
*/
export function shouldUseEmoji(): boolean {
const locale = (
process.env['LC_ALL'] ||
process.env['LC_CTYPE'] ||
process.env['LANG'] ||
''
).toLowerCase();
const supportsUtf8 = locale.includes('utf-8') || locale.includes('utf8');
if (!supportsUtf8) {
return false;
}
if (process.env['TERM'] === 'linux') {
return false;
}
return true;
}
+1 -1
View File
@@ -148,7 +148,7 @@ export interface Question {
options?: QuestionOption[];
/** Allow multiple selections. Only applies when type='choice'. */
multiSelect?: boolean;
/** Placeholder hint text. Only applies when type='text'. */
/** Placeholder hint text. For type='text', shown in the input field. For type='choice', shown in the "Other" custom input. */
placeholder?: string;
}
+18
View File
@@ -177,6 +177,24 @@ describe('AskUserTool', () => {
expect(result).toBeNull();
});
it('should accept placeholder for choice type', () => {
const result = tool.validateToolParams({
questions: [
{
question: 'Which language?',
header: 'Language',
type: QuestionType.CHOICE,
options: [
{ label: 'TypeScript', description: 'Typed JavaScript' },
{ label: 'JavaScript', description: 'Dynamic language' },
],
placeholder: 'Type another language...',
},
],
});
expect(result).toBeNull();
});
it('should return error if option has empty label', () => {
const result = tool.validateToolParams({
questions: [
+1 -1
View File
@@ -90,7 +90,7 @@ export class AskUserTool extends BaseDeclarativeTool<
placeholder: {
type: 'string',
description:
"Only applies when type='text'. Hint text shown in the input field.",
"Hint text shown in the input field. For type='text', shown in the main input. For type='choice', shown in the 'Other' custom input.",
},
},
},
-14
View File
@@ -187,20 +187,6 @@
"default": false,
"type": "boolean"
},
"showInlineThinkingFull": {
"title": "Show Inline Thinking (Full)",
"description": "Show full model thinking details inline.",
"markdownDescription": "Show full model thinking details inline.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"showInlineThinkingSummary": {
"title": "Show Inline Thinking (Summary)",
"description": "Show a short summary of model thinking inline.",
"markdownDescription": "Show a short summary of model thinking inline.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"showStatusInTitle": {
"title": "Show Thoughts in Title",
"description": "Show Gemini CLI model thoughts in the terminal window title during the working phase",