Skip to content

Commit bf9821b

Browse files
abhipatel12Yuna Seol
authored andcommitted
fix(cli): restore 'Modify with editor' option in external terminals (#17621)
1 parent 8298133 commit bf9821b

File tree

4 files changed

+162
-10
lines changed

4 files changed

+162
-10
lines changed

packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('ToolConfirmationMessage', () => {
3232
vi.mocked(useToolActions).mockReturnValue({
3333
confirm: mockConfirm,
3434
cancel: vi.fn(),
35+
isDiffingEnabled: false,
3536
});
3637

3738
const mockConfig = {
@@ -274,4 +275,92 @@ describe('ToolConfirmationMessage', () => {
274275
expect(lastFrame()).toContain('Allow for all future sessions');
275276
});
276277
});
278+
279+
describe('Modify with external editor option', () => {
280+
const editConfirmationDetails: ToolCallConfirmationDetails = {
281+
type: 'edit',
282+
title: 'Confirm Edit',
283+
fileName: 'test.txt',
284+
filePath: '/test.txt',
285+
fileDiff: '...diff...',
286+
originalContent: 'a',
287+
newContent: 'b',
288+
onConfirm: vi.fn(),
289+
};
290+
291+
it('should show "Modify with external editor" when NOT in IDE mode', () => {
292+
const mockConfig = {
293+
isTrustedFolder: () => true,
294+
getIdeMode: () => false,
295+
} as unknown as Config;
296+
297+
vi.mocked(useToolActions).mockReturnValue({
298+
confirm: vi.fn(),
299+
cancel: vi.fn(),
300+
isDiffingEnabled: false,
301+
});
302+
303+
const { lastFrame } = renderWithProviders(
304+
<ToolConfirmationMessage
305+
callId="test-call-id"
306+
confirmationDetails={editConfirmationDetails}
307+
config={mockConfig}
308+
availableTerminalHeight={30}
309+
terminalWidth={80}
310+
/>,
311+
);
312+
313+
expect(lastFrame()).toContain('Modify with external editor');
314+
});
315+
316+
it('should show "Modify with external editor" when in IDE mode but diffing is NOT enabled', () => {
317+
const mockConfig = {
318+
isTrustedFolder: () => true,
319+
getIdeMode: () => true,
320+
} as unknown as Config;
321+
322+
vi.mocked(useToolActions).mockReturnValue({
323+
confirm: vi.fn(),
324+
cancel: vi.fn(),
325+
isDiffingEnabled: false,
326+
});
327+
328+
const { lastFrame } = renderWithProviders(
329+
<ToolConfirmationMessage
330+
callId="test-call-id"
331+
confirmationDetails={editConfirmationDetails}
332+
config={mockConfig}
333+
availableTerminalHeight={30}
334+
terminalWidth={80}
335+
/>,
336+
);
337+
338+
expect(lastFrame()).toContain('Modify with external editor');
339+
});
340+
341+
it('should NOT show "Modify with external editor" when in IDE mode AND diffing is enabled', () => {
342+
const mockConfig = {
343+
isTrustedFolder: () => true,
344+
getIdeMode: () => true,
345+
} as unknown as Config;
346+
347+
vi.mocked(useToolActions).mockReturnValue({
348+
confirm: vi.fn(),
349+
cancel: vi.fn(),
350+
isDiffingEnabled: true,
351+
});
352+
353+
const { lastFrame } = renderWithProviders(
354+
<ToolConfirmationMessage
355+
callId="test-call-id"
356+
confirmationDetails={editConfirmationDetails}
357+
config={mockConfig}
358+
availableTerminalHeight={30}
359+
terminalWidth={80}
360+
/>,
361+
);
362+
363+
expect(lastFrame()).not.toContain('Modify with external editor');
364+
});
365+
});
277366
});

packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const ToolConfirmationMessage: React.FC<
5252
availableTerminalHeight,
5353
terminalWidth,
5454
}) => {
55-
const { confirm } = useToolActions();
55+
const { confirm, isDiffingEnabled } = useToolActions();
5656

5757
const settings = useSettings();
5858
const allowPermanentApproval =
@@ -111,9 +111,9 @@ export const ToolConfirmationMessage: React.FC<
111111
});
112112
}
113113
}
114-
// We hide "Modify with external editor" if IDE mode is active, assuming
115-
// the IDE provides a better interface (diff view) for this.
116-
if (!config.getIdeMode()) {
114+
// We hide "Modify with external editor" if IDE mode is active AND
115+
// the IDE is actually capable of showing a diff (connected).
116+
if (!config.getIdeMode() || !isDiffingEnabled) {
117117
options.push({
118118
label: 'Modify with external editor',
119119
value: ToolConfirmationOutcome.ModifyWithEditor,
@@ -210,7 +210,13 @@ export const ToolConfirmationMessage: React.FC<
210210
});
211211
}
212212
return options;
213-
}, [confirmationDetails, isTrustedFolder, allowPermanentApproval, config]);
213+
}, [
214+
confirmationDetails,
215+
isTrustedFolder,
216+
allowPermanentApproval,
217+
config,
218+
isDiffingEnabled,
219+
]);
214220

215221
const availableBodyContentHeight = useCallback(() => {
216222
if (availableTerminalHeight === undefined) {

packages/cli/src/ui/contexts/ToolActionsContext.test.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,44 @@ describe('ToolActionsContext', () => {
177177
throw new Error('Expected onConfirm to be present');
178178
}
179179
});
180+
181+
it('updates isDiffingEnabled when IdeClient status changes', async () => {
182+
let statusListener: () => void = () => {};
183+
const mockIdeClient = {
184+
isDiffingEnabled: vi.fn().mockReturnValue(false),
185+
addStatusChangeListener: vi.fn().mockImplementation((listener) => {
186+
statusListener = listener;
187+
}),
188+
removeStatusChangeListener: vi.fn(),
189+
} as unknown as IdeClient;
190+
191+
vi.mocked(IdeClient.getInstance).mockResolvedValue(mockIdeClient);
192+
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
193+
194+
const { result } = renderHook(() => useToolActions(), { wrapper });
195+
196+
// Wait for initialization
197+
await act(async () => {
198+
await vi.waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
199+
await new Promise((resolve) => setTimeout(resolve, 0));
200+
});
201+
202+
expect(result.current.isDiffingEnabled).toBe(false);
203+
204+
// Simulate connection change
205+
vi.mocked(mockIdeClient.isDiffingEnabled).mockReturnValue(true);
206+
await act(async () => {
207+
statusListener();
208+
});
209+
210+
expect(result.current.isDiffingEnabled).toBe(true);
211+
212+
// Simulate disconnection
213+
vi.mocked(mockIdeClient.isDiffingEnabled).mockReturnValue(false);
214+
await act(async () => {
215+
statusListener();
216+
});
217+
218+
expect(result.current.isDiffingEnabled).toBe(false);
219+
});
180220
});

packages/cli/src/ui/contexts/ToolActionsContext.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface ToolActionsContextValue {
3030
payload?: ToolConfirmationPayload,
3131
) => Promise<void>;
3232
cancel: (callId: string) => Promise<void>;
33+
isDiffingEnabled: boolean;
3334
}
3435

3536
const ToolActionsContext = createContext<ToolActionsContextValue | null>(null);
@@ -55,12 +56,28 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
5556

5657
// Hoist IdeClient logic here to keep UI pure
5758
const [ideClient, setIdeClient] = useState<IdeClient | null>(null);
59+
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
60+
5861
useEffect(() => {
5962
let isMounted = true;
6063
if (config.getIdeMode()) {
6164
IdeClient.getInstance()
6265
.then((client) => {
63-
if (isMounted) setIdeClient(client);
66+
if (!isMounted) return;
67+
setIdeClient(client);
68+
setIsDiffingEnabled(client.isDiffingEnabled());
69+
70+
const handleStatusChange = () => {
71+
if (isMounted) {
72+
setIsDiffingEnabled(client.isDiffingEnabled());
73+
}
74+
};
75+
76+
client.addStatusChangeListener(handleStatusChange);
77+
// Return a cleanup function for the listener
78+
return () => {
79+
client.removeStatusChangeListener(handleStatusChange);
80+
};
6481
})
6582
.catch((error) => {
6683
debugLogger.error('Failed to get IdeClient instance:', error);
@@ -88,12 +105,12 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
88105
// 1. Handle Side Effects (IDE Diff)
89106
if (
90107
details?.type === 'edit' &&
91-
ideClient?.isDiffingEnabled() &&
108+
isDiffingEnabled &&
92109
'filePath' in details // Check for safety
93110
) {
94111
const cliOutcome =
95112
outcome === ToolConfirmationOutcome.Cancel ? 'rejected' : 'accepted';
96-
await ideClient.resolveDiffFromCli(details.filePath, cliOutcome);
113+
await ideClient?.resolveDiffFromCli(details.filePath, cliOutcome);
97114
}
98115

99116
// 2. Dispatch
@@ -125,7 +142,7 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
125142

126143
debugLogger.warn(`ToolActions: No confirmation mechanism for ${callId}`);
127144
},
128-
[config, ideClient, toolCalls],
145+
[config, ideClient, toolCalls, isDiffingEnabled],
129146
);
130147

131148
const cancel = useCallback(
@@ -136,7 +153,7 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
136153
);
137154

138155
return (
139-
<ToolActionsContext.Provider value={{ confirm, cancel }}>
156+
<ToolActionsContext.Provider value={{ confirm, cancel, isDiffingEnabled }}>
140157
{children}
141158
</ToolActionsContext.Provider>
142159
);

0 commit comments

Comments
 (0)