diff --git a/src/UI/UIWindowProgress.js b/src/UI/UIWindowProgress.js
new file mode 100644
index 000000000..85dc5da9f
--- /dev/null
+++ b/src/UI/UIWindowProgress.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import UIWindow from './UIWindow.js'
+import Placeholder from '../util/Placeholder.js';
+import Button from './Components/Button.js';
+
+/**
+ * General purpose progress dialog.
+ * @param operation_id If provided, is saved in the data-operation-id attribute, for later lookup.
+ * @param show_progress Enable a progress bar, and display `(foo%)` after the status message
+ * @param on_cancel A callback run when the Cancel button is clicked. Without it, no Cancel button will appear.
+ * @returns {Promise<{set_progress: *, set_status: *, close: *, element: Element}>} Object for managing the progress dialog
+ * @constructor
+ * TODO: Error display
+ * TODO: Debouncing logic (show only after a delay, then hide only after a delay)
+ */
+async function UIWindowProgress({
+ operation_id = null,
+ show_progress = false,
+ on_cancel = null,
+} = {}){
+ const placeholder_cancel_btn = Placeholder();
+
+ let h = '';
+ h += `
`;
+ h += `
`;
+ // spinner
+ h += ``;
+ // Progress report
+ h += `
+ ${i18n('preparing')}`;
+ if (show_progress) {
+ h += ` (0%)`;
+ }
+ h += `