mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
feat: External editor settings (#882)
This commit is contained in:
168
packages/cli/src/ui/components/EditorSettingsDialog.tsx
Normal file
168
packages/cli/src/ui/components/EditorSettingsDialog.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
if (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) {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user