mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-30 06:54:15 -07:00
fix(cli): restore 'Modify with editor' option in external terminals (#17621)
This commit is contained in:
@@ -32,6 +32,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
vi.mocked(useToolActions).mockReturnValue({
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
confirm: mockConfirm,
|
confirm: mockConfirm,
|
||||||
cancel: vi.fn(),
|
cancel: vi.fn(),
|
||||||
|
isDiffingEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
@@ -274,4 +275,92 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
expect(lastFrame()).toContain('Allow for all future sessions');
|
expect(lastFrame()).toContain('Allow for all future sessions');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Modify with external editor option', () => {
|
||||||
|
const editConfirmationDetails: ToolCallConfirmationDetails = {
|
||||||
|
type: 'edit',
|
||||||
|
title: 'Confirm Edit',
|
||||||
|
fileName: 'test.txt',
|
||||||
|
filePath: '/test.txt',
|
||||||
|
fileDiff: '...diff...',
|
||||||
|
originalContent: 'a',
|
||||||
|
newContent: 'b',
|
||||||
|
onConfirm: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should show "Modify with external editor" when NOT in IDE mode', () => {
|
||||||
|
const mockConfig = {
|
||||||
|
isTrustedFolder: () => true,
|
||||||
|
getIdeMode: () => false,
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
|
confirm: vi.fn(),
|
||||||
|
cancel: vi.fn(),
|
||||||
|
isDiffingEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ToolConfirmationMessage
|
||||||
|
callId="test-call-id"
|
||||||
|
confirmationDetails={editConfirmationDetails}
|
||||||
|
config={mockConfig}
|
||||||
|
availableTerminalHeight={30}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Modify with external editor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show "Modify with external editor" when in IDE mode but diffing is NOT enabled', () => {
|
||||||
|
const mockConfig = {
|
||||||
|
isTrustedFolder: () => true,
|
||||||
|
getIdeMode: () => true,
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
|
confirm: vi.fn(),
|
||||||
|
cancel: vi.fn(),
|
||||||
|
isDiffingEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ToolConfirmationMessage
|
||||||
|
callId="test-call-id"
|
||||||
|
confirmationDetails={editConfirmationDetails}
|
||||||
|
config={mockConfig}
|
||||||
|
availableTerminalHeight={30}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Modify with external editor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT show "Modify with external editor" when in IDE mode AND diffing is enabled', () => {
|
||||||
|
const mockConfig = {
|
||||||
|
isTrustedFolder: () => true,
|
||||||
|
getIdeMode: () => true,
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
|
confirm: vi.fn(),
|
||||||
|
cancel: vi.fn(),
|
||||||
|
isDiffingEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ToolConfirmationMessage
|
||||||
|
callId="test-call-id"
|
||||||
|
confirmationDetails={editConfirmationDetails}
|
||||||
|
config={mockConfig}
|
||||||
|
availableTerminalHeight={30}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('Modify with external editor');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
}) => {
|
}) => {
|
||||||
const { confirm } = useToolActions();
|
const { confirm, isDiffingEnabled } = useToolActions();
|
||||||
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const allowPermanentApproval =
|
const allowPermanentApproval =
|
||||||
@@ -111,9 +111,9 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We hide "Modify with external editor" if IDE mode is active, assuming
|
// We hide "Modify with external editor" if IDE mode is active AND
|
||||||
// the IDE provides a better interface (diff view) for this.
|
// the IDE is actually capable of showing a diff (connected).
|
||||||
if (!config.getIdeMode()) {
|
if (!config.getIdeMode() || !isDiffingEnabled) {
|
||||||
options.push({
|
options.push({
|
||||||
label: 'Modify with external editor',
|
label: 'Modify with external editor',
|
||||||
value: ToolConfirmationOutcome.ModifyWithEditor,
|
value: ToolConfirmationOutcome.ModifyWithEditor,
|
||||||
@@ -210,7 +210,13 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}, [confirmationDetails, isTrustedFolder, allowPermanentApproval, config]);
|
}, [
|
||||||
|
confirmationDetails,
|
||||||
|
isTrustedFolder,
|
||||||
|
allowPermanentApproval,
|
||||||
|
config,
|
||||||
|
isDiffingEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const availableBodyContentHeight = useCallback(() => {
|
const availableBodyContentHeight = useCallback(() => {
|
||||||
if (availableTerminalHeight === undefined) {
|
if (availableTerminalHeight === undefined) {
|
||||||
|
|||||||
@@ -177,4 +177,44 @@ describe('ToolActionsContext', () => {
|
|||||||
throw new Error('Expected onConfirm to be present');
|
throw new Error('Expected onConfirm to be present');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('updates isDiffingEnabled when IdeClient status changes', async () => {
|
||||||
|
let statusListener: () => void = () => {};
|
||||||
|
const mockIdeClient = {
|
||||||
|
isDiffingEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
addStatusChangeListener: vi.fn().mockImplementation((listener) => {
|
||||||
|
statusListener = listener;
|
||||||
|
}),
|
||||||
|
removeStatusChangeListener: vi.fn(),
|
||||||
|
} as unknown as IdeClient;
|
||||||
|
|
||||||
|
vi.mocked(IdeClient.getInstance).mockResolvedValue(mockIdeClient);
|
||||||
|
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useToolActions(), { wrapper });
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
await act(async () => {
|
||||||
|
await vi.waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isDiffingEnabled).toBe(false);
|
||||||
|
|
||||||
|
// Simulate connection change
|
||||||
|
vi.mocked(mockIdeClient.isDiffingEnabled).mockReturnValue(true);
|
||||||
|
await act(async () => {
|
||||||
|
statusListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isDiffingEnabled).toBe(true);
|
||||||
|
|
||||||
|
// Simulate disconnection
|
||||||
|
vi.mocked(mockIdeClient.isDiffingEnabled).mockReturnValue(false);
|
||||||
|
await act(async () => {
|
||||||
|
statusListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isDiffingEnabled).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface ToolActionsContextValue {
|
|||||||
payload?: ToolConfirmationPayload,
|
payload?: ToolConfirmationPayload,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
cancel: (callId: string) => Promise<void>;
|
cancel: (callId: string) => Promise<void>;
|
||||||
|
isDiffingEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolActionsContext = createContext<ToolActionsContextValue | null>(null);
|
const ToolActionsContext = createContext<ToolActionsContextValue | null>(null);
|
||||||
@@ -55,12 +56,28 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
|
|||||||
|
|
||||||
// Hoist IdeClient logic here to keep UI pure
|
// Hoist IdeClient logic here to keep UI pure
|
||||||
const [ideClient, setIdeClient] = useState<IdeClient | null>(null);
|
const [ideClient, setIdeClient] = useState<IdeClient | null>(null);
|
||||||
|
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
if (config.getIdeMode()) {
|
if (config.getIdeMode()) {
|
||||||
IdeClient.getInstance()
|
IdeClient.getInstance()
|
||||||
.then((client) => {
|
.then((client) => {
|
||||||
if (isMounted) setIdeClient(client);
|
if (!isMounted) return;
|
||||||
|
setIdeClient(client);
|
||||||
|
setIsDiffingEnabled(client.isDiffingEnabled());
|
||||||
|
|
||||||
|
const handleStatusChange = () => {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsDiffingEnabled(client.isDiffingEnabled());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.addStatusChangeListener(handleStatusChange);
|
||||||
|
// Return a cleanup function for the listener
|
||||||
|
return () => {
|
||||||
|
client.removeStatusChangeListener(handleStatusChange);
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
debugLogger.error('Failed to get IdeClient instance:', error);
|
debugLogger.error('Failed to get IdeClient instance:', error);
|
||||||
@@ -88,12 +105,12 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
|
|||||||
// 1. Handle Side Effects (IDE Diff)
|
// 1. Handle Side Effects (IDE Diff)
|
||||||
if (
|
if (
|
||||||
details?.type === 'edit' &&
|
details?.type === 'edit' &&
|
||||||
ideClient?.isDiffingEnabled() &&
|
isDiffingEnabled &&
|
||||||
'filePath' in details // Check for safety
|
'filePath' in details // Check for safety
|
||||||
) {
|
) {
|
||||||
const cliOutcome =
|
const cliOutcome =
|
||||||
outcome === ToolConfirmationOutcome.Cancel ? 'rejected' : 'accepted';
|
outcome === ToolConfirmationOutcome.Cancel ? 'rejected' : 'accepted';
|
||||||
await ideClient.resolveDiffFromCli(details.filePath, cliOutcome);
|
await ideClient?.resolveDiffFromCli(details.filePath, cliOutcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Dispatch
|
// 2. Dispatch
|
||||||
@@ -125,7 +142,7 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
|
|||||||
|
|
||||||
debugLogger.warn(`ToolActions: No confirmation mechanism for ${callId}`);
|
debugLogger.warn(`ToolActions: No confirmation mechanism for ${callId}`);
|
||||||
},
|
},
|
||||||
[config, ideClient, toolCalls],
|
[config, ideClient, toolCalls, isDiffingEnabled],
|
||||||
);
|
);
|
||||||
|
|
||||||
const cancel = useCallback(
|
const cancel = useCallback(
|
||||||
@@ -136,7 +153,7 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolActionsContext.Provider value={{ confirm, cancel }}>
|
<ToolActionsContext.Provider value={{ confirm, cancel, isDiffingEnabled }}>
|
||||||
{children}
|
{children}
|
||||||
</ToolActionsContext.Provider>
|
</ToolActionsContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user