summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
authorLeo <[email protected]>2025-06-08 18:56:58 +0100
committerGitHub <[email protected]>2025-06-08 10:56:58 -0700
commit9efca40dae2e75477af1a20df4e3e65bf8dfe93d (patch)
tree39e10eef42ddfd4b9c73b7c2410dd5bccb5ed900 /packages/cli/src
parent584286cfd9b53eee8f1189a3ad98462c77eb8fb9 (diff)
feat: Add flow to allow modifying edits during edit tool call (#808)
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/config/config.ts1
-rw-r--r--packages/cli/src/config/settings.ts1
-rw-r--r--packages/cli/src/ui/App.tsx2
-rw-r--r--packages/cli/src/ui/components/HistoryItemDisplay.tsx4
-rw-r--r--packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx45
-rw-r--r--packages/cli/src/ui/components/messages/ToolGroupMessage.tsx4
-rw-r--r--packages/cli/src/ui/components/shared/text-buffer.ts4
7 files changed, 57 insertions, 4 deletions
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 49004776..009da59f 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -172,6 +172,7 @@ export async function loadCliConfig(
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
fileFilteringAllowBuildArtifacts:
settings.fileFiltering?.allowBuildArtifacts,
+ enableModifyWithExternalEditors: settings.enableModifyWithExternalEditors,
});
}
diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
index c56a9767..57b9b4fc 100644
--- a/packages/cli/src/config/settings.ts
+++ b/packages/cli/src/config/settings.ts
@@ -37,6 +37,7 @@ export interface Settings {
contextFileName?: string;
accessibility?: AccessibilitySettings;
telemetry?: boolean;
+ enableModifyWithExternalEditors?: boolean;
// Git-aware file filtering settings
fileFiltering?: {
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index d0e9efb7..365266f8 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -355,6 +355,7 @@ export const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
key={h.id}
item={h}
isPending={false}
+ config={config}
/>
)),
]}
@@ -370,6 +371,7 @@ export const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
// HistoryItemDisplay. Refactor later. Use a fake id for now.
item={{ ...item, id: 0 }}
isPending={true}
+ config={config}
/>
))}
</Box>
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 39cc5308..5ab6b3c9 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -15,17 +15,20 @@ import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { Box } from 'ink';
import { AboutBox } from './AboutBox.js';
+import { Config } from '@gemini-cli/core';
interface HistoryItemDisplayProps {
item: HistoryItem;
availableTerminalHeight: number;
isPending: boolean;
+ config?: Config;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
item,
availableTerminalHeight,
isPending,
+ config,
}) => (
<Box flexDirection="column" key={item.id}>
{/* Render standard message types */}
@@ -60,6 +63,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
toolCalls={item.tools}
groupId={item.id}
availableTerminalHeight={availableTerminalHeight}
+ config={config}
/>
)}
</Box>
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 01372290..c46d36f7 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -13,6 +13,8 @@ import {
ToolConfirmationOutcome,
ToolExecuteConfirmationDetails,
ToolMcpConfirmationDetails,
+ checkHasEditor,
+ Config,
} from '@gemini-cli/core';
import {
RadioButtonSelect,
@@ -21,11 +23,12 @@ import {
export interface ToolConfirmationMessageProps {
confirmationDetails: ToolCallConfirmationDetails;
+ config?: Config;
}
export const ToolConfirmationMessage: React.FC<
ToolConfirmationMessageProps
-> = ({ confirmationDetails }) => {
+> = ({ confirmationDetails, config }) => {
const { onConfirm } = confirmationDetails;
useInput((_, key) => {
@@ -44,6 +47,24 @@ export const ToolConfirmationMessage: React.FC<
>();
if (confirmationDetails.type === 'edit') {
+ if (confirmationDetails.isModifying) {
+ return (
+ <Box
+ minWidth="90%"
+ borderStyle="round"
+ borderColor={Colors.Gray}
+ justifyContent="space-around"
+ padding={1}
+ overflow="hidden"
+ >
+ <Text>Modify in progress: </Text>
+ <Text color={Colors.AccentGreen}>
+ Save and close external editor to continue
+ </Text>
+ </Box>
+ );
+ }
+
// Body content is now the DiffRenderer, passing filename to it
// The bordered box is removed from here and handled within DiffRenderer
bodyContent = (
@@ -63,8 +84,28 @@ export const ToolConfirmationMessage: React.FC<
label: 'Yes, allow always',
value: ToolConfirmationOutcome.ProceedAlways,
},
- { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
);
+
+ // 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('vim') && externalEditorsEnabled) {
+ options.push({
+ label: 'Modify with vim',
+ value: ToolConfirmationOutcome.ModifyVim,
+ });
+ }
+
+ options.push({ label: 'No (esc)', value: ToolConfirmationOutcome.Cancel });
} else if (confirmationDetails.type === 'exec') {
const executionProps =
confirmationDetails as ToolExecuteConfirmationDetails;
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index 71e6a59b..46fcecff 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -10,17 +10,20 @@ import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { Colors } from '../../colors.js';
+import { Config } from '@gemini-cli/core';
interface ToolGroupMessageProps {
groupId: number;
toolCalls: IndividualToolCallDisplay[];
availableTerminalHeight: number;
+ config?: Config;
}
// Main component renders the border and maps the tools using ToolMessage
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls,
availableTerminalHeight,
+ config,
}) => {
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
@@ -80,6 +83,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
tool.confirmationDetails && (
<ToolConfirmationMessage
confirmationDetails={tool.confirmationDetails}
+ config={config}
/>
)}
</Box>
diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts
index 27565324..e5957c7d 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.ts
@@ -1440,7 +1440,7 @@ export interface TextBuffer {
key: Record<string, boolean>,
) => boolean;
/**
- * Opens the current buffer contents in the user’s preferred terminal text
+ * Opens the current buffer contents in the user's preferred terminal text
* editor ($VISUAL or $EDITOR, falling back to "vi"). The method blocks
* until the editor exits, then reloads the file and replaces the in‑memory
* buffer with whatever the user saved.
@@ -1451,7 +1451,7 @@ export interface TextBuffer {
*
* Note: We purposefully rely on the *synchronous* spawn API so that the
* calling process genuinely waits for the editor to close before
- * continuing. This mirrors Git’s behaviour and simplifies downstream
+ * continuing. This mirrors Git's behaviour and simplifies downstream
* control‑flow (callers can simply `await` the Promise).
*/
openInExternalEditor: (opts?: { editor?: string }) => Promise<void>;