summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components')
-rw-r--r--packages/cli/src/ui/components/EditorSettingsDialog.tsx168
-rw-r--r--packages/cli/src/ui/components/HistoryItemDisplay.tsx3
-rw-r--r--packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx45
-rw-r--r--packages/cli/src/ui/components/messages/ToolGroupMessage.tsx3
-rw-r--r--packages/cli/src/ui/components/shared/RadioButtonSelect.tsx4
5 files changed, 189 insertions, 34 deletions
diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.tsx
new file mode 100644
index 00000000..304354b5
--- /dev/null
+++ b/packages/cli/src/ui/components/EditorSettingsDialog.tsx
@@ -0,0 +1,168 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+import {
+ EDITOR_DISPLAY_NAMES,
+ editorSettingsManager,
+ type EditorDisplay,
+} from '../editors/editorSettingsManager.js';
+import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
+import { LoadedSettings, SettingScope } from '../../config/settings.js';
+import { EditorType, isEditorAvailable } from '@gemini-cli/core';
+
+interface EditorDialogProps {
+ onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void;
+ settings: LoadedSettings;
+ onExit: () => void;
+}
+
+export function EditorSettingsDialog({
+ onSelect,
+ settings,
+ onExit,
+}: EditorDialogProps): React.JSX.Element {
+ const [selectedScope, setSelectedScope] = useState<SettingScope>(
+ SettingScope.User,
+ );
+ const [focusedSection, setFocusedSection] = useState<'editor' | 'scope'>(
+ 'editor',
+ );
+ useInput((_, key) => {
+ if (key.tab) {
+ setFocusedSection((prev) => (prev === 'editor' ? 'scope' : 'editor'));
+ }
+ if (key.escape) {
+ onExit();
+ }
+ });
+
+ const editorItems: EditorDisplay[] =
+ editorSettingsManager.getAvailableEditorDisplays();
+
+ const currentPreference =
+ settings.forScope(selectedScope).settings.preferredEditor;
+ let editorIndex = currentPreference
+ ? editorItems.findIndex(
+ (item: EditorDisplay) => item.type === currentPreference,
+ )
+ : 0;
+ if (editorIndex === -1) {
+ console.error(`Editor is not supported: ${currentPreference}`);
+ editorIndex = 0;
+ }
+
+ const scopeItems = [
+ { label: 'User Settings', value: SettingScope.User },
+ { label: 'Workspace Settings', value: SettingScope.Workspace },
+ ];
+
+ const handleEditorSelect = (editorType: EditorType | 'not_set') => {
+ if (editorType === 'not_set') {
+ onSelect(undefined, selectedScope);
+ return;
+ }
+ onSelect(editorType, selectedScope);
+ };
+
+ const handleScopeSelect = (scope: SettingScope) => {
+ setSelectedScope(scope);
+ setFocusedSection('editor');
+ };
+
+ let otherScopeModifiedMessage = '';
+ const otherScope =
+ selectedScope === SettingScope.User
+ ? SettingScope.Workspace
+ : SettingScope.User;
+ if (settings.forScope(otherScope).settings.preferredEditor !== undefined) {
+ otherScopeModifiedMessage =
+ settings.forScope(selectedScope).settings.preferredEditor !== undefined
+ ? `(Also modified in ${otherScope})`
+ : `(Modified in ${otherScope})`;
+ }
+
+ let mergedEditorName = 'None';
+ if (
+ settings.merged.preferredEditor &&
+ isEditorAvailable(settings.merged.preferredEditor)
+ ) {
+ mergedEditorName =
+ EDITOR_DISPLAY_NAMES[settings.merged.preferredEditor as EditorType];
+ }
+
+ return (
+ <Box
+ borderStyle="round"
+ borderColor={Colors.Gray}
+ flexDirection="row"
+ padding={1}
+ width="100%"
+ >
+ <Box flexDirection="column" width="45%" paddingRight={2}>
+ <Text bold={focusedSection === 'editor'}>
+ {focusedSection === 'editor' ? '> ' : ' '}Select Editor{' '}
+ <Text color={Colors.Gray}>{otherScopeModifiedMessage}</Text>
+ </Text>
+ <RadioButtonSelect
+ items={editorItems.map((item) => ({
+ label: item.name,
+ value: item.type,
+ disabled: item.disabled,
+ }))}
+ initialIndex={editorIndex}
+ onSelect={handleEditorSelect}
+ isFocused={focusedSection === 'editor'}
+ key={selectedScope}
+ />
+
+ <Box marginTop={1} flexDirection="column">
+ <Text bold={focusedSection === 'scope'}>
+ {focusedSection === 'scope' ? '> ' : ' '}Apply To
+ </Text>
+ <RadioButtonSelect
+ items={scopeItems}
+ initialIndex={0}
+ onSelect={handleScopeSelect}
+ isFocused={focusedSection === 'scope'}
+ />
+ </Box>
+
+ <Box marginTop={1}>
+ <Text color={Colors.Gray}>
+ (Use Enter to select, Tab to change focus)
+ </Text>
+ </Box>
+ </Box>
+
+ <Box flexDirection="column" width="55%" paddingLeft={2}>
+ <Text bold>Editor Preference</Text>
+ <Box flexDirection="column" gap={1} marginTop={1}>
+ <Text color={Colors.Gray}>
+ These editors are currently supported. Please note that some editors
+ cannot be used in sandbox mode.
+ </Text>
+ <Text color={Colors.Gray}>
+ Your preferred editor is:{' '}
+ <Text
+ color={
+ mergedEditorName === 'None'
+ ? Colors.AccentRed
+ : Colors.AccentCyan
+ }
+ bold
+ >
+ {mergedEditorName}
+ </Text>
+ .
+ </Text>
+ </Box>
+ </Box>
+ </Box>
+ );
+}
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 229672ec..fc1b128d 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -24,6 +24,7 @@ interface HistoryItemDisplayProps {
availableTerminalHeight: number;
isPending: boolean;
config?: Config;
+ isFocused?: boolean;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
@@ -31,6 +32,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
availableTerminalHeight,
isPending,
config,
+ isFocused = true,
}) => (
<Box flexDirection="column" key={item.id}>
{/* Render standard message types */}
@@ -76,6 +78,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
groupId={item.id}
availableTerminalHeight={availableTerminalHeight}
config={config}
+ isFocused={isFocused}
/>
)}
</Box>
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index af9aba6a..0de85ba4 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -13,7 +13,6 @@ import {
ToolConfirmationOutcome,
ToolExecuteConfirmationDetails,
ToolMcpConfirmationDetails,
- checkHasEditor,
Config,
} from '@gemini-cli/core';
import {
@@ -24,14 +23,16 @@ import {
export interface ToolConfirmationMessageProps {
confirmationDetails: ToolCallConfirmationDetails;
config?: Config;
+ isFocused?: boolean;
}
export const ToolConfirmationMessage: React.FC<
ToolConfirmationMessageProps
-> = ({ confirmationDetails, config }) => {
+> = ({ confirmationDetails, config, isFocused = true }) => {
const { onConfirm } = confirmationDetails;
useInput((_, key) => {
+ if (!isFocused) return;
if (key.escape) {
onConfirm(ToolConfirmationOutcome.Cancel);
}
@@ -86,40 +87,12 @@ export const ToolConfirmationMessage: React.FC<
},
);
- // Conditionally add editor options if editors are installed
- const notUsingSandbox = !process.env.SANDBOX;
const externalEditorsEnabled =
config?.getEnableModifyWithExternalEditors() ?? false;
-
- if (checkHasEditor('vscode') && notUsingSandbox && externalEditorsEnabled) {
- options.push({
- label: 'Modify with VS Code',
- value: ToolConfirmationOutcome.ModifyVSCode,
- });
- }
-
- if (
- checkHasEditor('windsurf') &&
- notUsingSandbox &&
- externalEditorsEnabled
- ) {
- options.push({
- label: 'Modify with Windsurf',
- value: ToolConfirmationOutcome.ModifyWindsurf,
- });
- }
-
- if (checkHasEditor('cursor') && notUsingSandbox && externalEditorsEnabled) {
- options.push({
- label: 'Modify with Cursor',
- value: ToolConfirmationOutcome.ModifyCursor,
- });
- }
-
- if (checkHasEditor('vim') && externalEditorsEnabled) {
+ if (externalEditorsEnabled) {
options.push({
- label: 'Modify with vim',
- value: ToolConfirmationOutcome.ModifyVim,
+ label: 'Modify with external editor',
+ value: ToolConfirmationOutcome.ModifyWithEditor,
});
}
@@ -192,7 +165,11 @@ export const ToolConfirmationMessage: React.FC<
{/* Select Input for Options */}
<Box flexShrink={0}>
- <RadioButtonSelect items={options} onSelect={handleSelect} />
+ <RadioButtonSelect
+ items={options}
+ onSelect={handleSelect}
+ isFocused={isFocused}
+ />
</Box>
</Box>
);
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index b01e5f9b..8ce40893 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -17,6 +17,7 @@ interface ToolGroupMessageProps {
toolCalls: IndividualToolCallDisplay[];
availableTerminalHeight: number;
config?: Config;
+ isFocused?: boolean;
}
// Main component renders the border and maps the tools using ToolMessage
@@ -24,6 +25,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls,
availableTerminalHeight,
config,
+ isFocused = true,
}) => {
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
@@ -84,6 +86,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
<ToolConfirmationMessage
confirmationDetails={tool.confirmationDetails}
config={config}
+ isFocused={isFocused}
/>
)}
</Box>
diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
index 22b5cecd..5430a442 100644
--- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
+++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
@@ -19,6 +19,7 @@ import { Colors } from '../../colors.js';
export interface RadioSelectItem<T> {
label: string;
value: T;
+ disabled?: boolean;
}
/**
@@ -97,11 +98,14 @@ export function RadioButtonSelect<T>({
const itemWithThemeProps = props as typeof props & {
themeNameDisplay?: string;
themeTypeDisplay?: string;
+ disabled?: boolean;
};
let textColor = Colors.Foreground;
if (isSelected) {
textColor = Colors.AccentGreen;
+ } else if (itemWithThemeProps.disabled === true) {
+ textColor = Colors.Gray;
}
if (