From 661d11e60b0ce9d7f8b2d092e3b559d4adcf2e44 Mon Sep 17 00:00:00 2001 From: Nagendhra Madishetti Date: Fri, 8 May 2026 00:39:04 -0400 Subject: [PATCH] fix(web): confirm before clearing the saved Composio API key (#877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Clear button on Settings → Connectors removed the daemon-stored Composio key in a single click with no recovery — a stray click wiped a credential the user had to fetch back from app.composio.dev. Wrap the existing onClick in window.confirm() matching the same pattern the codebase already uses for destructive actions (conversation delete, design delete, FileWorkspace file delete, and the Media providers Clear button shipped alongside this in issue #737). The prompt copy stays in English to match the rest of the Composio section, which is hardcoded English today. Updated the existing 'clears a saved Composio key' test to auto-accept the prompt, plus added a sibling test asserting that dismissing the prompt leaves the daemon-stored key intact in the saved payload. Co-authored-by: Nagendhra --- apps/web/src/components/SettingsDialog.tsx | 14 +++++- .../SettingsDialog.execution.test.tsx | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/SettingsDialog.tsx b/apps/web/src/components/SettingsDialog.tsx index 0d33001838..9214ce1223 100644 --- a/apps/web/src/components/SettingsDialog.tsx +++ b/apps/web/src/components/SettingsDialog.tsx @@ -1856,7 +1856,19 @@ function ComposioSection({ type="button" className="ghost" disabled={!apiKeyConfigured} - onClick={() => updateComposio({ apiKey: '', apiKeyConfigured: false, apiKeyTail: '' })} + onClick={() => { + // Match the existing window.confirm guard the rest of the + // app uses for destructive actions (conversation delete, + // design delete, file delete in FileWorkspace, and the + // Media providers Clear button). Without this a stray + // click wipes the daemon-stored Composio key and the + // user has to paste it back from their secrets manager. + // Issue #734. + if (!confirm('Clear the saved Composio API key? You\'ll need to paste it again to use Composio-backed connectors.')) { + return; + } + updateComposio({ apiKey: '', apiKeyConfigured: false, apiKeyTail: '' }); + }} > Clear diff --git a/apps/web/tests/components/SettingsDialog.execution.test.tsx b/apps/web/tests/components/SettingsDialog.execution.test.tsx index db9f5a6488..b77540930f 100644 --- a/apps/web/tests/components/SettingsDialog.execution.test.tsx +++ b/apps/web/tests/components/SettingsDialog.execution.test.tsx @@ -890,8 +890,15 @@ describe('SettingsDialog connectors interactions', () => { { initialSection: 'composio' }, ); + // Issue #734 added a window.confirm guard on the Clear button so a + // stray click cannot wipe a daemon-stored Composio key. Auto-accept + // the prompt here so the test still exercises the cleared-payload + // path. + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); + fireEvent.click(screen.getByRole('button', { name: 'Clear' })); + expect(confirmSpy).toHaveBeenCalledTimes(1); expect((screen.getByPlaceholderText('Paste Composio API key') as HTMLInputElement).value).toBe(''); expect(screen.getByText(/Keys are stored locally in the daemon/i)).toBeTruthy(); @@ -906,6 +913,46 @@ describe('SettingsDialog connectors interactions', () => { }), false, ); + + confirmSpy.mockRestore(); + }); + + it('cancels Clear when the Composio confirmation is dismissed (issue #734)', () => { + const { onSave } = renderSettingsDialog( + { + mode: 'daemon', + agentId: 'codex', + composio: { + apiKey: '', + apiKeyConfigured: true, + apiKeyTail: 'uQEg', + }, + }, + { initialSection: 'composio' }, + ); + + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + + fireEvent.click(screen.getByRole('button', { name: 'Clear' })); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + // Saved badge survives because apiKeyConfigured is still true. + expect(screen.getByText(/Saved · ••••uQEg/)).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + // Without a confirmation, the saved key must remain in the payload + // so the user does not silently lose their daemon-stored credential. + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + composio: expect.objectContaining({ + apiKeyConfigured: true, + apiKeyTail: 'uQEg', + }), + }), + false, + ); + + confirmSpy.mockRestore(); }); it('does not save Composio edits when cancel is used or the backdrop is clicked', () => {