mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 11:34:44 -07:00
Support ink scrolling final pr (#12567)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
@@ -21,6 +21,7 @@ import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
confirmationDetails: ToolCallConfirmationDetails;
|
||||
@@ -42,6 +43,8 @@ export const ToolConfirmationMessage: React.FC<
|
||||
const { onConfirm } = confirmationDetails;
|
||||
const childWidth = terminalWidth - 2; // 2 for padding
|
||||
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const [ideClient, setIdeClient] = useState<IdeClient | null>(null);
|
||||
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
|
||||
|
||||
@@ -90,42 +93,230 @@ export const ToolConfirmationMessage: React.FC<
|
||||
|
||||
const handleSelect = (item: ToolConfirmationOutcome) => handleConfirm(item);
|
||||
|
||||
let bodyContent: React.ReactNode | null = null; // Removed contextDisplay here
|
||||
let question: string;
|
||||
const { question, bodyContent, options } = useMemo(() => {
|
||||
let bodyContent: React.ReactNode | null = null;
|
||||
let question = '';
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [];
|
||||
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = new Array<
|
||||
RadioSelectItem<ToolConfirmationOutcome>
|
||||
>();
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
if (!confirmationDetails.isModifying) {
|
||||
question = `Apply this change?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Yes, allow always',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always',
|
||||
});
|
||||
}
|
||||
if (!config.getIdeMode() || !isDiffingEnabled) {
|
||||
options.push({
|
||||
label: 'Modify with external editor',
|
||||
value: ToolConfirmationOutcome.ModifyWithEditor,
|
||||
key: 'Modify with external editor',
|
||||
});
|
||||
}
|
||||
|
||||
// Body content is now the DiffRenderer, passing filename to it
|
||||
// The bordered box is removed from here and handled within DiffRenderer
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
}
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const executionProps =
|
||||
confirmationDetails as ToolExecuteConfirmationDetails;
|
||||
|
||||
function availableBodyContentHeight() {
|
||||
if (options.length === 0) {
|
||||
// This should not happen in practice as options are always added before this is called.
|
||||
throw new Error('Options not provided for confirmation message');
|
||||
question = `Allow execution of: '${executionProps.rootCommand}'?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: `Yes, allow always ...`,
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: `Yes, allow always ...`,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
question = `Do you want to proceed?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Yes, allow always',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always',
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
} else {
|
||||
// mcp tool confirmation
|
||||
const mcpProps = confirmationDetails as ToolMcpConfirmationDetails;
|
||||
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
|
||||
key: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||
});
|
||||
options.push({
|
||||
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
key: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
}
|
||||
|
||||
if (availableTerminalHeight === undefined) {
|
||||
return undefined;
|
||||
function availableBodyContentHeight() {
|
||||
if (options.length === 0) {
|
||||
// Should not happen if we populated options correctly above for all types
|
||||
// except when isModifying is true, but in that case we don't call this because we don't enter the if block for it.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (availableTerminalHeight === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Calculate the vertical space (in lines) consumed by UI elements
|
||||
// surrounding the main body content.
|
||||
const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
|
||||
const MARGIN_BODY_BOTTOM = 1; // margin on the body container.
|
||||
const HEIGHT_QUESTION = 1; // The question text is one line.
|
||||
const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
|
||||
const HEIGHT_OPTIONS = options.length; // Each option in the radio select takes one line.
|
||||
|
||||
const surroundingElementsHeight =
|
||||
PADDING_OUTER_Y +
|
||||
MARGIN_BODY_BOTTOM +
|
||||
HEIGHT_QUESTION +
|
||||
MARGIN_QUESTION_BOTTOM +
|
||||
HEIGHT_OPTIONS;
|
||||
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
|
||||
}
|
||||
|
||||
// Calculate the vertical space (in lines) consumed by UI elements
|
||||
// surrounding the main body content.
|
||||
const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
|
||||
const MARGIN_BODY_BOTTOM = 1; // margin on the body container.
|
||||
const HEIGHT_QUESTION = 1; // The question text is one line.
|
||||
const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
|
||||
const HEIGHT_OPTIONS = options.length; // Each option in the radio select takes one line.
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
if (!confirmationDetails.isModifying) {
|
||||
bodyContent = (
|
||||
<DiffRenderer
|
||||
diffContent={confirmationDetails.fileDiff}
|
||||
filename={confirmationDetails.fileName}
|
||||
availableTerminalHeight={availableBodyContentHeight()}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const executionProps =
|
||||
confirmationDetails as ToolExecuteConfirmationDetails;
|
||||
let bodyContentHeight = availableBodyContentHeight();
|
||||
if (bodyContentHeight !== undefined) {
|
||||
bodyContentHeight -= 2; // Account for padding;
|
||||
}
|
||||
|
||||
const surroundingElementsHeight =
|
||||
PADDING_OUTER_Y +
|
||||
MARGIN_BODY_BOTTOM +
|
||||
HEIGHT_QUESTION +
|
||||
MARGIN_QUESTION_BOTTOM +
|
||||
HEIGHT_OPTIONS;
|
||||
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
|
||||
}
|
||||
const commandBox = (
|
||||
<Box>
|
||||
<Text color={theme.text.link}>{executionProps.command}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column">
|
||||
<Box paddingX={1}>
|
||||
{isAlternateBuffer ? (
|
||||
commandBox
|
||||
) : (
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(childWidth, 1)}
|
||||
>
|
||||
{commandBox}
|
||||
</MaxSizedBox>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const infoProps = confirmationDetails;
|
||||
const displayUrls =
|
||||
infoProps.urls &&
|
||||
!(
|
||||
infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt
|
||||
);
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={theme.text.link}>
|
||||
<RenderInline
|
||||
text={infoProps.prompt}
|
||||
defaultColor={theme.text.link}
|
||||
/>
|
||||
</Text>
|
||||
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.primary}>URLs to fetch:</Text>
|
||||
{infoProps.urls.map((url) => (
|
||||
<Text key={url}>
|
||||
{' '}
|
||||
- <RenderInline text={url} />
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
// mcp tool confirmation
|
||||
const mcpProps = confirmationDetails as ToolMcpConfirmationDetails;
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text>
|
||||
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return { question, bodyContent, options };
|
||||
}, [
|
||||
confirmationDetails,
|
||||
isTrustedFolder,
|
||||
config,
|
||||
isDiffingEnabled,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
isAlternateBuffer,
|
||||
childWidth,
|
||||
]);
|
||||
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
if (confirmationDetails.isModifying) {
|
||||
@@ -145,177 +336,29 @@ export const ToolConfirmationMessage: React.FC<
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
question = `Apply this change?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Yes, allow always',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always',
|
||||
});
|
||||
}
|
||||
if (!config.getIdeMode() || !isDiffingEnabled) {
|
||||
options.push({
|
||||
label: 'Modify with external editor',
|
||||
value: ToolConfirmationOutcome.ModifyWithEditor,
|
||||
key: 'Modify with external editor',
|
||||
});
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
|
||||
bodyContent = (
|
||||
<DiffRenderer
|
||||
diffContent={confirmationDetails.fileDiff}
|
||||
filename={confirmationDetails.fileName}
|
||||
availableTerminalHeight={availableBodyContentHeight()}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const executionProps =
|
||||
confirmationDetails as ToolExecuteConfirmationDetails;
|
||||
|
||||
question = `Allow execution of: '${executionProps.rootCommand}'?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: `Yes, allow always ...`,
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: `Yes, allow always ...`,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
|
||||
let bodyContentHeight = availableBodyContentHeight();
|
||||
if (bodyContentHeight !== undefined) {
|
||||
bodyContentHeight -= 2; // Account for padding;
|
||||
}
|
||||
bodyContent = (
|
||||
<Box flexDirection="column">
|
||||
<Box paddingX={1} marginLeft={1}>
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(childWidth - 4, 1)}
|
||||
>
|
||||
<Box>
|
||||
<Text color={theme.text.link}>{executionProps.command}</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const infoProps = confirmationDetails;
|
||||
const displayUrls =
|
||||
infoProps.urls &&
|
||||
!(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
|
||||
|
||||
question = `Do you want to proceed?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Yes, allow always',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always',
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
||||
<RenderInline text={infoProps.prompt} defaultColor={theme.text.link} />
|
||||
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.primary}>URLs to fetch:</Text>
|
||||
{infoProps.urls.map((url) => (
|
||||
<Text key={url}>
|
||||
{' '}
|
||||
- <RenderInline text={url} />
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
// mcp tool confirmation
|
||||
const mcpProps = confirmationDetails as ToolMcpConfirmationDetails;
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
||||
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text>
|
||||
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
||||
options.push({
|
||||
label: 'Yes, allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
|
||||
key: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||
});
|
||||
options.push({
|
||||
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
key: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1} width={childWidth}>
|
||||
<Box flexDirection="column" paddingTop={0} paddingBottom={1}>
|
||||
{/* Body Content (Diff Renderer or Command Info) */}
|
||||
{/* No separate context display here anymore for edits */}
|
||||
<Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
|
||||
<Box
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
overflow="hidden"
|
||||
marginBottom={1}
|
||||
paddingLeft={1}
|
||||
>
|
||||
{bodyContent}
|
||||
</Box>
|
||||
|
||||
{/* Confirmation Question */}
|
||||
<Box marginBottom={1} flexShrink={0}>
|
||||
<Text color={theme.text.primary} wrap="truncate">
|
||||
{question}
|
||||
</Text>
|
||||
<Box marginBottom={1} flexShrink={0} paddingX={1}>
|
||||
<Text color={theme.text.primary}>{question}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Select Input for Options */}
|
||||
<Box flexShrink={0}>
|
||||
<Box flexShrink={0} paddingX={1}>
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={handleSelect}
|
||||
|
||||
Reference in New Issue
Block a user