fix(web): confirm before clearing the saved Composio API key (#877)

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 <nagendhra405@gmail.com>
This commit is contained in:
Nagendhra Madishetti 2026-05-08 00:39:04 -04:00 committed by GitHub
parent 751b2357f1
commit 661d11e60b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 1 deletions

View file

@ -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
</button>

View file

@ -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', () => {