mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-07 20:00:37 -07:00
refactor(cli,core): foundational layout, identity management, and type safety (#23286)
This commit is contained in:
@@ -79,4 +79,28 @@ describe('colorizeCode', () => {
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('returns an array of lines when returnLines is true', () => {
|
||||
const code = 'line 1\nline 2\nline 3';
|
||||
const settings = new LoadedSettings(
|
||||
{ path: '', settings: {}, originalSettings: {} },
|
||||
{ path: '', settings: {}, originalSettings: {} },
|
||||
{ path: '', settings: {}, originalSettings: {} },
|
||||
{ path: '', settings: {}, originalSettings: {} },
|
||||
true,
|
||||
[],
|
||||
);
|
||||
|
||||
const result = colorizeCode({
|
||||
code,
|
||||
language: 'javascript',
|
||||
maxWidth: 80,
|
||||
settings,
|
||||
hideLineNumbers: true,
|
||||
returnLines: true,
|
||||
});
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
MaxSizedBox,
|
||||
MINIMUM_MAX_HEIGHT,
|
||||
} from '../components/shared/MaxSizedBox.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
// Configure theming and parsing utilities.
|
||||
const lowlight = createLowlight(common);
|
||||
@@ -117,7 +117,11 @@ export function colorizeLine(
|
||||
line: string,
|
||||
language: string | null,
|
||||
theme?: Theme,
|
||||
disableColor = false,
|
||||
): React.ReactNode {
|
||||
if (disableColor) {
|
||||
return <Text>{line}</Text>;
|
||||
}
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
return highlightAndRenderLine(line, language, activeTheme);
|
||||
}
|
||||
@@ -130,6 +134,8 @@ export interface ColorizeCodeOptions {
|
||||
theme?: Theme | null;
|
||||
settings: LoadedSettings;
|
||||
hideLineNumbers?: boolean;
|
||||
disableColor?: boolean;
|
||||
returnLines?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,6 +144,12 @@ export interface ColorizeCodeOptions {
|
||||
* @param options The options for colorizing the code.
|
||||
* @returns A React.ReactNode containing Ink <Text> elements for the highlighted code.
|
||||
*/
|
||||
export function colorizeCode(
|
||||
options: ColorizeCodeOptions & { returnLines: true },
|
||||
): React.ReactNode[];
|
||||
export function colorizeCode(
|
||||
options: ColorizeCodeOptions & { returnLines?: false },
|
||||
): React.ReactNode;
|
||||
export function colorizeCode({
|
||||
code,
|
||||
language = null,
|
||||
@@ -146,13 +158,16 @@ export function colorizeCode({
|
||||
theme = null,
|
||||
settings,
|
||||
hideLineNumbers = false,
|
||||
}: ColorizeCodeOptions): React.ReactNode {
|
||||
disableColor = false,
|
||||
returnLines = false,
|
||||
}: ColorizeCodeOptions): React.ReactNode | React.ReactNode[] {
|
||||
const codeToHighlight = code.replace(/\n$/, '');
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
const showLineNumbers = hideLineNumbers
|
||||
? false
|
||||
: settings.merged.ui.showLineNumbers;
|
||||
|
||||
const useMaxSizedBox = !settings.merged.ui.useAlternateBuffer && !returnLines;
|
||||
try {
|
||||
// Render the HAST tree using the adapted theme
|
||||
// Apply the theme's default foreground color to the top-level Text element
|
||||
@@ -162,7 +177,7 @@ export function colorizeCode({
|
||||
let hiddenLinesCount = 0;
|
||||
|
||||
// Optimization to avoid highlighting lines that cannot possibly be displayed.
|
||||
if (availableHeight !== undefined) {
|
||||
if (availableHeight !== undefined && useMaxSizedBox) {
|
||||
availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT);
|
||||
if (lines.length > availableHeight) {
|
||||
const sliceIndex = lines.length - availableHeight;
|
||||
@@ -172,11 +187,9 @@ export function colorizeCode({
|
||||
}
|
||||
|
||||
const renderedLines = lines.map((line, index) => {
|
||||
const contentToRender = highlightAndRenderLine(
|
||||
line,
|
||||
language,
|
||||
activeTheme,
|
||||
);
|
||||
const contentToRender = disableColor
|
||||
? line
|
||||
: highlightAndRenderLine(line, language, activeTheme);
|
||||
|
||||
return (
|
||||
<Box key={index} minHeight={1}>
|
||||
@@ -188,19 +201,26 @@ export function colorizeCode({
|
||||
alignItems="flex-start"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Text color={activeTheme.colors.Gray}>
|
||||
<Text color={disableColor ? undefined : activeTheme.colors.Gray}>
|
||||
{`${index + 1 + hiddenLinesCount}`}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Text color={activeTheme.defaultColor} wrap="wrap">
|
||||
<Text
|
||||
color={disableColor ? undefined : activeTheme.defaultColor}
|
||||
wrap="wrap"
|
||||
>
|
||||
{contentToRender}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
if (availableHeight !== undefined) {
|
||||
if (returnLines) {
|
||||
return renderedLines;
|
||||
}
|
||||
|
||||
if (useMaxSizedBox) {
|
||||
return (
|
||||
<MaxSizedBox
|
||||
maxHeight={availableHeight}
|
||||
@@ -237,14 +257,22 @@ export function colorizeCode({
|
||||
alignItems="flex-start"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Text color={activeTheme.defaultColor}>{`${index + 1}`}</Text>
|
||||
<Text color={disableColor ? undefined : activeTheme.defaultColor}>
|
||||
{`${index + 1}`}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Text color={activeTheme.colors.Gray}>{stripAnsi(line)}</Text>
|
||||
<Text color={disableColor ? undefined : activeTheme.colors.Gray}>
|
||||
{stripAnsi(line)}
|
||||
</Text>
|
||||
</Box>
|
||||
));
|
||||
|
||||
if (availableHeight !== undefined) {
|
||||
if (returnLines) {
|
||||
return fallbackLines;
|
||||
}
|
||||
|
||||
if (useMaxSizedBox) {
|
||||
return (
|
||||
<MaxSizedBox
|
||||
maxHeight={availableHeight}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type HistoryItemToolGroup,
|
||||
type HistoryItemWithoutId,
|
||||
type IndividualToolCallDisplay,
|
||||
} from '../types.js';
|
||||
import { getAllToolCalls } from './historyUtils.js';
|
||||
|
||||
export interface ConfirmingToolState {
|
||||
tool: IndividualToolCallDisplay;
|
||||
@@ -23,9 +23,7 @@ export interface ConfirmingToolState {
|
||||
export function getConfirmingToolState(
|
||||
pendingHistoryItems: HistoryItemWithoutId[],
|
||||
): ConfirmingToolState | null {
|
||||
const allPendingTools = pendingHistoryItems
|
||||
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
|
||||
.flatMap((group) => group.tools);
|
||||
const allPendingTools = getAllToolCalls(pendingHistoryItems);
|
||||
|
||||
const confirmingTools = allPendingTools.filter(
|
||||
(tool) => tool.status === CoreToolCallStatus.AwaitingApproval,
|
||||
|
||||
83
packages/cli/src/ui/utils/historyUtils.ts
Normal file
83
packages/cli/src/ui/utils/historyUtils.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CoreToolCallStatus } from '../types.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolGroup,
|
||||
IndividualToolCallDisplay,
|
||||
} from '../types.js';
|
||||
|
||||
export function getLastTurnToolCallIds(
|
||||
history: HistoryItem[],
|
||||
pendingHistoryItems: HistoryItemWithoutId[],
|
||||
): string[] {
|
||||
const targetToolCallIds: string[] = [];
|
||||
|
||||
// Find the boundary of the last user prompt
|
||||
let lastUserPromptIndex = -1;
|
||||
for (let i = history.length - 1; i >= 0; i--) {
|
||||
const type = history[i].type;
|
||||
if (type === 'user' || type === 'user_shell') {
|
||||
lastUserPromptIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect IDs from history after last user prompt
|
||||
history.forEach((item, index) => {
|
||||
if (index > lastUserPromptIndex && item.type === 'tool_group') {
|
||||
item.tools.forEach((t) => {
|
||||
if (t.callId) targetToolCallIds.push(t.callId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Collect IDs from pending items
|
||||
pendingHistoryItems.forEach((item) => {
|
||||
if (item.type === 'tool_group') {
|
||||
item.tools.forEach((t) => {
|
||||
if (t.callId) targetToolCallIds.push(t.callId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return targetToolCallIds;
|
||||
}
|
||||
|
||||
export function isToolExecuting(
|
||||
pendingHistoryItems: HistoryItemWithoutId[],
|
||||
): boolean {
|
||||
return pendingHistoryItems.some((item) => {
|
||||
if (item && item.type === 'tool_group') {
|
||||
return item.tools.some(
|
||||
(tool) => CoreToolCallStatus.Executing === tool.status,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export function isToolAwaitingConfirmation(
|
||||
pendingHistoryItems: HistoryItemWithoutId[],
|
||||
): boolean {
|
||||
return pendingHistoryItems
|
||||
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
|
||||
.some((item) =>
|
||||
item.tools.some(
|
||||
(tool) => CoreToolCallStatus.AwaitingApproval === tool.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getAllToolCalls(
|
||||
historyItems: HistoryItemWithoutId[],
|
||||
): IndividualToolCallDisplay[] {
|
||||
return historyItems
|
||||
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
|
||||
.flatMap((group) => group.tools);
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
calculateToolContentMaxLines,
|
||||
calculateShellMaxLines,
|
||||
SHELL_CONTENT_OVERHEAD,
|
||||
TOOL_RESULT_STATIC_HEIGHT,
|
||||
TOOL_RESULT_STANDARD_RESERVED_LINE_COUNT,
|
||||
TOOL_RESULT_ASB_RESERVED_LINE_COUNT,
|
||||
TOOL_RESULT_MIN_LINES_SHOWN,
|
||||
} from './toolLayoutUtils.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import {
|
||||
@@ -48,7 +52,7 @@ describe('toolLayoutUtils', () => {
|
||||
availableTerminalHeight: 2,
|
||||
isAlternateBuffer: false,
|
||||
},
|
||||
expected: 3,
|
||||
expected: TOOL_RESULT_MIN_LINES_SHOWN + 1,
|
||||
},
|
||||
{
|
||||
desc: 'returns available space directly in constrained terminal (ASB mode)',
|
||||
@@ -56,7 +60,7 @@ describe('toolLayoutUtils', () => {
|
||||
availableTerminalHeight: 4,
|
||||
isAlternateBuffer: true,
|
||||
},
|
||||
expected: 3,
|
||||
expected: TOOL_RESULT_MIN_LINES_SHOWN + 1,
|
||||
},
|
||||
{
|
||||
desc: 'returns remaining space if sufficient space exists (Standard mode)',
|
||||
@@ -64,7 +68,10 @@ describe('toolLayoutUtils', () => {
|
||||
availableTerminalHeight: 20,
|
||||
isAlternateBuffer: false,
|
||||
},
|
||||
expected: 17,
|
||||
expected:
|
||||
20 -
|
||||
TOOL_RESULT_STATIC_HEIGHT -
|
||||
TOOL_RESULT_STANDARD_RESERVED_LINE_COUNT,
|
||||
},
|
||||
{
|
||||
desc: 'returns remaining space if sufficient space exists (ASB mode)',
|
||||
@@ -72,7 +79,8 @@ describe('toolLayoutUtils', () => {
|
||||
availableTerminalHeight: 20,
|
||||
isAlternateBuffer: true,
|
||||
},
|
||||
expected: 13,
|
||||
expected:
|
||||
20 - TOOL_RESULT_STATIC_HEIGHT - TOOL_RESULT_ASB_RESERVED_LINE_COUNT,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -148,7 +156,7 @@ describe('toolLayoutUtils', () => {
|
||||
constrainHeight: true,
|
||||
isExpandable: false,
|
||||
},
|
||||
expected: 4,
|
||||
expected: 6 - TOOL_RESULT_STANDARD_RESERVED_LINE_COUNT,
|
||||
},
|
||||
{
|
||||
desc: 'handles negative availableTerminalHeight gracefully',
|
||||
@@ -172,7 +180,7 @@ describe('toolLayoutUtils', () => {
|
||||
constrainHeight: false,
|
||||
isExpandable: false,
|
||||
},
|
||||
expected: 28,
|
||||
expected: 30 - TOOL_RESULT_STANDARD_RESERVED_LINE_COUNT,
|
||||
},
|
||||
{
|
||||
desc: 'falls back to COMPLETED_SHELL_MAX_LINES - SHELL_CONTENT_OVERHEAD for completed shells if space allows',
|
||||
|
||||
@@ -17,7 +17,7 @@ import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
*/
|
||||
export const TOOL_RESULT_STATIC_HEIGHT = 1;
|
||||
export const TOOL_RESULT_ASB_RESERVED_LINE_COUNT = 6;
|
||||
export const TOOL_RESULT_STANDARD_RESERVED_LINE_COUNT = 2;
|
||||
export const TOOL_RESULT_STANDARD_RESERVED_LINE_COUNT = 3;
|
||||
export const TOOL_RESULT_MIN_LINES_SHOWN = 2;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user