diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx
index 377bc56b..55abed08 100644
--- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx
@@ -5,6 +5,162 @@ import ExtensionModal from './ExtensionModal';
import { ExtensionFormData } from '../utils';
describe('ExtensionModal', () => {
+ it('does not show unsaved changes dialog when closing without modifications', async () => {
+ const user = userEvent.setup();
+ const mockOnSubmit = vi.fn();
+ const mockOnClose = vi.fn();
+
+ const initialData: ExtensionFormData = {
+ name: 'Existing Extension',
+ description: 'An existing extension',
+ type: 'stdio',
+ cmd: 'npx some-mcp-server',
+ endpoint: '',
+ enabled: true,
+ timeout: 300,
+ envVars: [
+ { key: 'API_KEY', value: '••••••••', isEdited: false },
+ { key: 'OTHER_VAR', value: '••••••••', isEdited: false },
+ ],
+ headers: [],
+ };
+
+ render(
+
+ );
+
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ await user.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+
+ expect(screen.queryByText('Unsaved Changes')).not.toBeInTheDocument();
+ });
+
+ it('shows unsaved changes dialog when name is modified', async () => {
+ const user = userEvent.setup();
+ const mockOnSubmit = vi.fn();
+ const mockOnClose = vi.fn();
+
+ const initialData: ExtensionFormData = {
+ name: 'Original Name',
+ description: 'An existing extension',
+ type: 'stdio',
+ cmd: 'npx some-mcp-server',
+ endpoint: '',
+ enabled: true,
+ timeout: 300,
+ envVars: [],
+ headers: [],
+ };
+
+ render(
+
+ );
+
+ const nameInput = screen.getByPlaceholderText('Enter extension name...');
+ await user.clear(nameInput);
+ await user.type(nameInput, 'New Name');
+
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ await user.click(cancelButton);
+
+ expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
+ expect(mockOnClose).not.toHaveBeenCalled();
+ });
+
+ it('shows unsaved changes dialog when description is modified', async () => {
+ const user = userEvent.setup();
+ const mockOnSubmit = vi.fn();
+ const mockOnClose = vi.fn();
+
+ const initialData: ExtensionFormData = {
+ name: 'Test Extension',
+ description: 'Original description',
+ type: 'stdio',
+ cmd: 'npx some-mcp-server',
+ endpoint: '',
+ enabled: true,
+ timeout: 300,
+ envVars: [],
+ headers: [],
+ };
+
+ render(
+
+ );
+
+ const descriptionInput = screen.getByPlaceholderText('Optional description...');
+ await user.clear(descriptionInput);
+ await user.type(descriptionInput, 'New description');
+
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ await user.click(cancelButton);
+
+ expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
+ expect(mockOnClose).not.toHaveBeenCalled();
+ });
+
+ it('shows unsaved changes dialog when timeout is modified', async () => {
+ const user = userEvent.setup();
+ const mockOnSubmit = vi.fn();
+ const mockOnClose = vi.fn();
+
+ const initialData: ExtensionFormData = {
+ name: 'Test Extension',
+ description: 'An extension',
+ type: 'stdio',
+ cmd: 'npx some-mcp-server',
+ endpoint: '',
+ enabled: true,
+ timeout: 300,
+ envVars: [],
+ headers: [],
+ };
+
+ render(
+
+ );
+
+ const timeoutInput = screen.getByDisplayValue('300');
+ await user.clear(timeoutInput);
+ await user.type(timeoutInput, '600');
+
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ await user.click(cancelButton);
+
+ expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
+ expect(mockOnClose).not.toHaveBeenCalled();
+ });
+
it('creates a http_streamable extension', async () => {
const user = userEvent.setup();
const mockOnSubmit = vi.fn();
diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx
index 947a6d02..e6cbd4d5 100644
--- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx
@@ -47,6 +47,12 @@ export default function ExtensionModal({
// Function to check if form has been modified
const hasFormChanges = (): boolean => {
+ // Check basic fields
+ const nameChanged = formData.name !== initialData.name;
+ const descriptionChanged = formData.description !== initialData.description;
+ const typeChanged = formData.type !== initialData.type;
+ const timeoutChanged = formData.timeout !== initialData.timeout;
+
// Check if command/endpoint has changed
const commandChanged =
(formData.type === 'stdio' && formData.cmd !== initialData.cmd) ||
@@ -54,35 +60,30 @@ export default function ExtensionModal({
(formData.type === 'streamable_http' && formData.endpoint !== initialData.endpoint);
// Check if headers have changed
- const headersChanged = formData.headers.some((header) => header.isEdited === true);
+ const headersEdited = formData.headers.some((header) => header.isEdited === true);
+ const headersAdded = formData.headers.length > initialData.headers.length;
+ const headersRemoved = formData.headers.length < initialData.headers.length;
// Check if any environment variables have been modified
const envVarsChanged = formData.envVars.some((envVar) => envVar.isEdited === true);
-
- // Check if new env vars have been added
const envVarsAdded = formData.envVars.length > initialData.envVars.length;
-
- // Check if env vars have been removed
const envVarsRemoved = formData.envVars.length < initialData.envVars.length;
- // Check if any environment variable fields have text entered (even if not marked as edited)
- const envVarsHaveText = formData.envVars.some(
- (envVar) =>
- (envVar.key.trim() !== '' || envVar.value.trim() !== '') &&
- // Don't count placeholder values for existing secrets
- envVar.value !== '••••••••'
- );
-
// Check if there are pending environment variables or headers being typed
const hasPendingInput = hasPendingEnvVars || hasPendingHeaders;
return (
+ nameChanged ||
+ descriptionChanged ||
+ typeChanged ||
+ timeoutChanged ||
commandChanged ||
- headersChanged ||
+ headersEdited ||
+ headersAdded ||
+ headersRemoved ||
envVarsChanged ||
envVarsAdded ||
envVarsRemoved ||
- envVarsHaveText ||
hasPendingInput
);
};