mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-19 06:20:44 -07:00
fix(ui): make tool confirmations take up entire terminal height (#22366)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect, useMemo, useCallback, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { useEffect, useMemo, useCallback, useState, useRef } from 'react';
|
||||
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
import {
|
||||
@@ -85,6 +85,64 @@ export const ToolConfirmationMessage: React.FC<
|
||||
? mcpDetailsExpansionState.expanded
|
||||
: false;
|
||||
|
||||
const [measuredSecurityWarningsHeight, setMeasuredSecurityWarningsHeight] =
|
||||
useState(0);
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
const deceptiveUrlWarnings = useMemo(() => {
|
||||
const urls: string[] = [];
|
||||
if (confirmationDetails.type === 'info' && confirmationDetails.urls) {
|
||||
urls.push(...confirmationDetails.urls);
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const commands =
|
||||
confirmationDetails.commands && confirmationDetails.commands.length > 0
|
||||
? confirmationDetails.commands
|
||||
: [confirmationDetails.command];
|
||||
for (const cmd of commands) {
|
||||
const matches = cmd.match(/https?:\/\/[^\s"'`<>;&|()]+/g);
|
||||
if (matches) urls.push(...matches);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueUrls = Array.from(new Set(urls));
|
||||
return uniqueUrls
|
||||
.map(getDeceptiveUrlDetails)
|
||||
.filter((d): d is DeceptiveUrlDetails => d !== null);
|
||||
}, [confirmationDetails]);
|
||||
|
||||
const deceptiveUrlWarningText = useMemo(() => {
|
||||
if (deceptiveUrlWarnings.length === 0) return null;
|
||||
return `**Warning:** Deceptive URL(s) detected:\n\n${deceptiveUrlWarnings
|
||||
.map(
|
||||
(w) =>
|
||||
` **Original:** ${w.originalUrl}\n **Actual Host (Punycode):** ${w.punycodeUrl}`,
|
||||
)
|
||||
.join('\n\n')}`;
|
||||
}, [deceptiveUrlWarnings]);
|
||||
|
||||
const onSecurityWarningsRefChange = useCallback((node: DOMElement | null) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
observerRef.current = null;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
const newHeight = Math.round(entry.contentRect.height);
|
||||
setMeasuredSecurityWarningsHeight((prev) =>
|
||||
newHeight !== prev ? newHeight : prev,
|
||||
);
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
observerRef.current = observer;
|
||||
} else {
|
||||
setMeasuredSecurityWarningsHeight((prev) => (prev !== 0 ? 0 : prev));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const settings = useSettings();
|
||||
const allowPermanentApproval =
|
||||
settings.merged.security.enablePermanentToolApproval &&
|
||||
@@ -216,37 +274,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
[handleConfirm],
|
||||
);
|
||||
|
||||
const deceptiveUrlWarnings = useMemo(() => {
|
||||
const urls: string[] = [];
|
||||
if (confirmationDetails.type === 'info' && confirmationDetails.urls) {
|
||||
urls.push(...confirmationDetails.urls);
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const commands =
|
||||
confirmationDetails.commands && confirmationDetails.commands.length > 0
|
||||
? confirmationDetails.commands
|
||||
: [confirmationDetails.command];
|
||||
for (const cmd of commands) {
|
||||
const matches = cmd.match(/https?:\/\/[^\s"'`<>;&|()]+/g);
|
||||
if (matches) urls.push(...matches);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueUrls = Array.from(new Set(urls));
|
||||
return uniqueUrls
|
||||
.map(getDeceptiveUrlDetails)
|
||||
.filter((d): d is DeceptiveUrlDetails => d !== null);
|
||||
}, [confirmationDetails]);
|
||||
|
||||
const deceptiveUrlWarningText = useMemo(() => {
|
||||
if (deceptiveUrlWarnings.length === 0) return null;
|
||||
return `**Warning:** Deceptive URL(s) detected:\n\n${deceptiveUrlWarnings
|
||||
.map(
|
||||
(w) =>
|
||||
` **Original:** ${w.originalUrl}\n **Actual Host (Punycode):** ${w.punycodeUrl}`,
|
||||
)
|
||||
.join('\n\n')}`;
|
||||
}, [deceptiveUrlWarnings]);
|
||||
|
||||
const getOptions = useCallback(() => {
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [];
|
||||
|
||||
@@ -389,23 +416,36 @@ export const ToolConfirmationMessage: React.FC<
|
||||
|
||||
// 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 PADDING_OUTER_Y = 1; // Main container has `paddingBottom={1}`.
|
||||
const HEIGHT_QUESTION = 1; // The question text is one line.
|
||||
const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
|
||||
const SECURITY_WARNING_BOTTOM_MARGIN = 1; // Margin on the securityWarnings container.
|
||||
const SHOW_MORE_LINES_HEIGHT = 1; // The "Press Ctrl+O to show more lines" hint.
|
||||
|
||||
const optionsCount = getOptions().length;
|
||||
|
||||
// The measured height includes the margin inside WarningMessage (1 line).
|
||||
// We also add 1 line for the marginBottom on the securityWarnings container.
|
||||
const securityWarningsHeight = deceptiveUrlWarningText
|
||||
? measuredSecurityWarningsHeight + SECURITY_WARNING_BOTTOM_MARGIN
|
||||
: 0;
|
||||
|
||||
const surroundingElementsHeight =
|
||||
PADDING_OUTER_Y +
|
||||
MARGIN_BODY_BOTTOM +
|
||||
HEIGHT_QUESTION +
|
||||
MARGIN_QUESTION_BOTTOM +
|
||||
SHOW_MORE_LINES_HEIGHT +
|
||||
optionsCount +
|
||||
1; // Reserve one line for 'ShowMoreLines' hint
|
||||
securityWarningsHeight;
|
||||
|
||||
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
|
||||
}, [availableTerminalHeight, getOptions, handlesOwnUI]);
|
||||
}, [
|
||||
availableTerminalHeight,
|
||||
handlesOwnUI,
|
||||
getOptions,
|
||||
measuredSecurityWarningsHeight,
|
||||
deceptiveUrlWarningText,
|
||||
]);
|
||||
|
||||
const { question, bodyContent, options, securityWarnings, initialIndex } =
|
||||
useMemo<{
|
||||
@@ -547,10 +587,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
let bodyContentHeight = availableBodyContentHeight();
|
||||
let warnings: React.ReactNode = null;
|
||||
|
||||
if (bodyContentHeight !== undefined) {
|
||||
bodyContentHeight -= 2; // Account for padding;
|
||||
}
|
||||
|
||||
if (containsRedirection) {
|
||||
// Calculate lines needed for Note and Tip
|
||||
const safeWidth = Math.max(terminalWidth, 1);
|
||||
@@ -759,7 +795,11 @@ export const ToolConfirmationMessage: React.FC<
|
||||
</Box>
|
||||
|
||||
{securityWarnings && (
|
||||
<Box flexShrink={0} marginBottom={1}>
|
||||
<Box
|
||||
flexShrink={0}
|
||||
marginBottom={1}
|
||||
ref={onSecurityWarningsRefChange}
|
||||
>
|
||||
{securityWarnings}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user